13 KiB
Configuration
Precedence and lifecycle
Configuration is validated by ConfigSchema in src/config/index.ts.
DATABASE_PATHidentifies the database before application settings can be read.- If
app_configexists, the application-owned configuration is loaded from SQLite. - Otherwise,
config.yamlis loaded once and the validated application-owned subset is seeded into SQLite. - Environment-owned secrets and database location are injected on every start.
After the initial seed, editing config.yaml does not replace an existing app_config row. Use the authenticated settings/API workflow or a deliberate database migration. The config route validates the complete result before saving.
Environment-owned values
| Variable | Required | Purpose |
|---|---|---|
OPENROUTER_API_KEY |
Yes | OpenRouter authentication. |
WEB_DASHBOARD_TOKEN |
When web is enabled | Administrator UI/API credential. |
DATABASE_PATH |
No | SQLite location; defaults to ./data/reddit-bot.db. |
NTFY_AUTH_TOKEN |
No | Bearer token for a protected ntfy topic. |
REDDIT_BOT_AGENT_READ_TOKEN |
No | Read-only API/SSE bearer credential. |
REDDIT_BOT_AGENT_CONTROL_TOKEN |
No | Read plus versioned control/job bearer credential. |
REDDIT_BOT_API_URL |
MCP only | Base URL; defaults to http://127.0.0.1:3000. |
REDDIT_BOT_AGENT_TOKEN |
MCP only | Preferred MCP bearer credential override. |
Never document real values. .env is ignored and may contain production credentials; do not print or blindly source it.
Application sections
reddit
subreddits: at least one item withname,enabled,type, optionaltargetingMode, optionalweight,tone, optional scripted opener, and conversation policy.targetingModedefaults topost-author;comment-authoris valid only for image subreddits and selects high-confidence comments expressing a desire to take the depicted role. Whenreddit.targetGendersis non-empty, the gender perspective expressed by the comment must be included;unknownfails closed unless explicitly targeted. The normal image screen separately enforces the same list against the depicted role. The comment classifier does not claim to identify the commenter's actual gender.pollIntervalMs: legacy/scheduled polling cadence.userAgent: Reddit JSON request identifier.targetGenders: allowed parsed values.filterSellers,sellerKeywords,sellerDomains: seller policy inputs; see also theconversationssection, whose off-platform kill is independent of this flag. The defaults treat Telegram references, includingt.meandtelegram.melinks, as seller signals alongside paid-content platforms and payment prompts.prioritizeRecentPosts,recentWindowMinutes: recency/fallback behavior.maxReadyCandidates: hard ceiling for the active candidate target; valid range 1–500. Runtime lowers the actual target to what authenticated running accounts can send withinrecentWindowMinutesunder current rolling-hour and message-spacing limits.physiqueTargeting: optional image-candidate filter withenabled, inclusiveminScore, and inclusivemaxScorevalues from 1 to 10. It is disabled by default. When enabled, a model score outside the range or an image without enough visible body evidence to score is rejected before opener generation. Saving the setting also reconciles prepared image candidates in the ready and delivery-retry stages: candidates that no longer qualify are rejected, while retained candidates receive the newly applied range snapshot shown in the funnel. A candidate already leased for sending is not changed mid-send. The setting is hot-read by existing account runtimes; chat sources are unaffected.
Physique targeting is owned only by its dedicated Monitoring controls. It is excluded from the advanced JSON response. Older cached dashboards may still submit a duplicate advanced copy; the server accepts but ignores that copy so it cannot overwrite the dedicated value persisted in app_config.
Subreddit type is image or chat. Conversation policy contains mode, maxAutomatedTurns, and rapportTurnsBeforeMediaAsk; hybrid rapport turns must be lower than the maximum.
conversations
offPlatformKill(defaulttrue): end a conversation outright when the reply pushes it to another app, to contact details, or to a payment rail. Independent ofreddit.filterSellers.killOnSellerKeyword(defaulttrue): a configured seller keyword or domain also ends the conversation. Setfalseto hand those to an operator instead.aiReplyScreening.enabled(defaultfalse): second-stage classifier on inbound replies, run only when the deterministic rules pass. Usesopenrouter.chatModeland its fallbacks, and fails open.aiReplyScreening.killConfidence(default0.8) /flagConfidence(default0.6): thresholds at which a seller/off-platform/scam/bot verdict kills or hands off.
openrouter
Contains image/chat primary and fallback models plus the analysis, message, and chat-opener prompt templates. The API key is environment-only. Model fallback and prompt changes affect cost, behavior, and experiment attribution and need focused tests plus operator-visible metadata.
growthCommentPrompt is the template the account grower uses to write a public comment on someone else's thread. It supports {subreddit}, {title}, {selftext}, and {flair}. The thread is appended separately as fenced untrusted data and the deterministic safety gate runs on the result regardless of prompt wording, so editing this template changes tone, not the safety boundary.
rateLimit
minDelayBetweenMessagesMs;maxMessagesPerHour;jitterMs.
These are per account. Reddit JSON request pacing is separately controlled by RedditRequestCoordinator and is process-global.
browser
sessionDir: root for persistent account browser profiles.dryRun: when true, candidate preparation may run but remote sends must not.
Treat changing dryRun from true to false as enabling a real side effect.
database
Contains path, but runtime ownership is DATABASE_PATH, not the persisted application config.
media
Controls periodic sync enablement and interval. Media work remains authentication-bound.
pushNotifications
Publishes selected application events to a single ntfy topic so they reach a phone. Holds the enabled flag, server URL, topic, ntfy priority, and a boolean per event (imageReceived, replyReceived, messageFailed, botStopped, rateLimited, sessionExpired, syncFailed). Editable from Settings → Notifications, which writes through PUT /api/push-notifications; POST /api/push-notifications/test sends a fixed test notification. The access token is environment-owned (NTFY_AUTH_TOKEN), is never persisted to the settings record, and is never returned by the API — the response only reports hasAuthToken.
Delivery is best-effort: an unreachable or rejecting ntfy server is logged and never propagated into the path that produced the event.
strategies and chatStrategies
Each has exploration rate, minimum mature samples, response window, and weighted variants. Variants include an ID and prompt override. Image and chat sources use separate sections so their results are not compared as one experiment.
web
Controls enabled state, port, optional tokens after environment injection, and dangerous debug endpoint enablement. Production secrets should never live in YAML.
Account growth
Growth is configured per account through /api/growth, not through config.yaml or app_config, because the community list, allowance, and pause state belong to one Reddit identity. The stores are account_growth_settings and account_growth_subreddits; see docs/database.md.
Operator-settable per account: enabled, the daily comment limit, minimum spacing, jitter, quiet hours, NSFW opt-in, the community list with per-community weight and daily cap, and pause/resume. browser.dryRun also suppresses growth posting.
dailyLimit is null by default, which leaves the allowance to the adaptive trust level the verification sweep maintains (2/3/5/8/12 per day at levels 0-4). Setting a whole number between 1 and 50 overrides that in either direction and is enforced verbatim; the trust level continues to be tracked and reported as adaptiveAllowance so the earned figure stays visible. Raising the limit above the earned allowance removes the adaptive backoff after a bad comment, so the dashboard says so. It does not affect the removal/shadowban/captcha pause, which is a safety stop rather than a rate control and cannot be disabled by any limit.
A per-community dailyCap only distributes the account allowance; caps are not additive and cannot raise the account total.
Community weights feed a new weighted-random pick for each discovered growth thread. The grower queues only one new thread at a time, drains due retries and existing queue work before polling another community, and exposes an account-scoped queue clear that removes only unposted discovered/retryable rows.
Browsing sessions are configured separately per account through /api/growth/{accountId}/browsing and reuse the enabled growth-community list and weights. Sessions default off with a configurable 3–6 selections-per-local-day range and a 60/40 post/comment split, respect quiet hours, and exclude NSFW unless enabled. The daily minimum, daily maximum, and selections per session accept positive whole numbers without a product-imposed upper ceiling; the maximum must not be lower than the minimum. The service derives one stable target inside the configured daily range for each account and local day. The operator also chooses a minimum gap of at least 45 minutes, and the service adds a random delay of up to jitterMs on top before persisting the next due time. A scheduled session is capped by the remaining daily target, while a manually requested session bypasses that target and uses the configured session size. autoVoteEnabled is a separate, default-off operator setting; it can submit selected upvotes only in running mode when browser dry-run is off. nextDueAt is service-owned durable state and cannot be set through the API.
Runtime-editable subset
The dashboard/API exposes a deliberate subset: rate limits, dry-run, recency and ready-funnel limits, subreddit definitions/conversation behavior, and selected advanced configuration. Shared transport schemas live in packages/api-contracts/src/config.ts; the backend source schema is still the final validator.
When adding a runtime-editable setting, update all of:
ConfigSchemaand defaults.config.yamlseed/example when appropriate.ConfigStorepersistence rules if ownership changes.- Shared request/response schemas.
- The config route's merge/write/read-back behavior.
- Dashboard model/control.
- backend, contract, route, and dashboard tests.
- this document.
Validation and change safety
- Use Zod for every external or persisted shape.
- Reject unknown transport fields with strict contracts.
- Validate relationships (not just types), such as the hybrid turn constraint.
- Do not log the full config because it contains injected secrets in memory.
- Configuration that affects running account objects may not be fully hot-reloaded; inspect the owning service before claiming immediate effect.
- A production
PUT /api/configshould be followed by an authenticated GET read-back. Do not infer persistence from a 2xx alone. - A subreddit listing 404 is persisted as disabled so every account runtime stops polling it.
Local safe setup
Install dependencies and run builds/tests without starting the application. If a local backend is explicitly needed, use a dry-run configuration and verify the database path/port before starting. Importing a real session or calling the start control is a separate live-capable action.
learning
captureEnabledcontrols attribution and extraction; it does not enable generation.generationEnabledpermits retrieved examples to affect generation only whenshadowModeis also false.shadowModerecords which examples would have been selected while leaving prompts unchanged.maturityHoursdelays eligibility so later negative signals can revoke apparent success.maxContextExamples,humanExampleSlots, andautonomousExampleSlotsbound and separate retrieval cohorts; slot totals may not exceed the context maximum.maxTurnsPerExamplebounds durable redacted context.minimumAttributionConfidenceacceptsexact,fingerprint, orinferred.policyVersionversions retrieval/evaluation audit records.
The safe default is capture enabled, generation disabled, and shadow mode enabled. Enabling injected use is a configuration change and still does not deploy or start the bot.