feat: flag accounts Reddit considers unestablished and re-test them

New accounts cannot start chats until Reddit considers them established.
Until now the bot would burn a candidate on every invitation Reddit was
always going to reject, with nothing in the dashboard explaining why the
account looked idle.

Two signals feed the verdict. A periodic profile check reads the
account's own age, karma and email verification from about.json and
compares them against configurable thresholds — that is what flags a
brand new account before it wastes anything. A rejected invitation is
authoritative: Reddit has told us directly, so the account is held out of
sending with a backoff that doubles per rejection, and a delivered
invitation clears the flag outright.

An unestablished account takes no new posts (canAcceptPost), raises an
attention item, reports an `unestablished` sending state, and shows its
age/karma evidence on the Accounts page. An unchecked account is always
allowed through, so a first run is never blocked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jamie BruntonandClaude Opus 5 committed 2026-08-11 09:14:47 +01:00
1 parent c43e534cf1
commit 89eb4583f8
25 files changed
+1425 -11

No files matched your search

+30
View File
@@ -164,6 +164,36 @@ export const ConfigSchema = z.object({
killOnSellerKeyword: true,
aiReplyScreening: { enabled: false, killConfidence: 0.8, flagConfidence: 0.6 },
}),
/**
* New Reddit accounts cannot start chats until Reddit considers them
* established. Rather than burning candidates on invitations Reddit will
* reject, an account is flagged and held out of outbound sending, then
* re-tested on an interval.
*/
accountEstablishment: z.object({
enabled: z.boolean().default(true),
/** How often an account's own age/karma profile is refreshed. */
profileCheckIntervalMs: z.number().int().min(60_000).default(6 * 60 * 60_000),
minAccountAgeDays: z.number().min(0).default(7),
minTotalKarma: z.number().int().min(0).default(20),
minCommentKarma: z.number().int().min(0).default(5),
requireVerifiedEmail: z.boolean().default(false),
/**
* How long to hold an account out of sending after Reddit rejects an
* invitation for being too new. Doubles per consecutive rejection.
*/
probeRetryBaseMs: z.number().int().min(60_000).default(4 * 60 * 60_000),
probeRetryMaxMs: z.number().int().min(60_000).default(48 * 60 * 60_000),
}).default({
enabled: true,
profileCheckIntervalMs: 6 * 60 * 60_000,
minAccountAgeDays: 7,
minTotalKarma: 20,
minCommentKarma: 5,
requireVerifiedEmail: false,
probeRetryBaseMs: 4 * 60 * 60_000,
probeRetryMaxMs: 48 * 60 * 60_000,
}),
pushNotifications: z.object({
enabled: z.boolean().default(false),
/** Base URL of the ntfy server, e.g. https://ntfy.example.com. */