Pricing model — spec (v0, prices unset)#
Status: structure only. Every price below is a variable with a placeholder. Numbers get tuned in the analysis pass; this doc just pins down what each fee attaches to, how it's charged, and the formulas that turn prices → revenue/margin.
1. The three SKUs (the tunable variables)#
| Variable | Type | Unit | Placeholder |
|---|---|---|---|
PRICE_RULE_MONTHLY |
recurring | per rule / month | $TBD |
PRICE_CHANNEL_BACKFILL |
one-time | per single-channel backfill run | $TBD |
PRICE_GUILD_BACKFILL |
one-time | per whole-guild backfill run | $TBD |
A "rule" is a row in the rules table — one guild/channel/author filter →
destination pipe. Rules belong to a job (atproto identity), which belongs to a
user (Discord account). Per-rule monthly means a user pays N × PRICE_RULE_MONTHLY
for their N active rules, regardless of how those rules spread across their
atproto identities. This is finer-grained and more usage-aligned than today's
per-account model — see §6.1 for the migration implication.
2. Mapping to the data model + Stripe mechanics#
PRICE_RULE_MONTHLY — recurring, per rule#
- Attaches to: each enabled
rulesrow. Todayusers.active+users.stripe_customer_idgate dispatch for the whole account; this moves the billable unit all the way down to the individual rule. - Stripe: one Subscription per
userscustomer, with a recurring Price whose quantity = count of that user's enabled rules (or one subscription item per rule). Adding/removing/enabling/disabling a rule → adjust quantity →subscription.updatedwebhook → reconcile. - Gating: a rule can dispatch only if it's covered by a paid subscription item.
The codebase already disables rules on lapse (
disableAllRulesForUser) and therules.enabledflag exists — per-rule billing maps onto that machinery: a paid rule is enabled, an unpaid one is disabled. ExtendsdispatchGateClausefrom per-user to per-rule.
PRICE_CHANNEL_BACKFILL — one-time, per single-channel run#
- Triggers at:
POST /rules/:id/backfill(the single-channel enqueue). - Stripe: Checkout Session in
paymentmode (one-time), or an invoice item. Enqueue thebackfill_jobsrow only aftercheckout.session.completed.
PRICE_GUILD_BACKFILL — one-time, per whole-guild run#
- Triggers at:
POST /rules/:id/backfill/all(the fan-out; one job per readable channel). - Stripe: same one-time mechanism; gate the fan-out behind payment.
- Note: one guild run currently fans out to all readable channels (this guild = 40). Whether the flat guild price should scale with channel count is a tuning question (§6).
3. Cost variables to net against revenue#
| Variable | Meaning | Known value |
|---|---|---|
STRIPE_PCT |
Stripe % per charge | 0.029 (2.9%) |
STRIPE_FIXED |
Stripe fixed fee per charge | $0.30 |
INFRA_GUILD_BACKFILL |
marginal Railway cost / guild run | ~$0.003 (measured, ~40 ch) |
INFRA_CHANNEL_BACKFILL |
marginal Railway cost / channel run | < $0.001 (measured) |
BASE_INFRA_MONTHLY |
always-on Railway service (fixed) | $TBD (amortize across jobs) |
Measured this session: a ~40-channel, ~1,800-record guild backfill consumed only ~5 CPU-seconds and ~5 MB egress — single-digit-tenths-of-a-cent of marginal infra. Infra is noise; Stripe's per-transaction fee is the real cost driver on the one-time SKUs.
4. Revenue, cost & margin formulas#
Usage inputs (per month): R = active rules, C = channel backfill runs,
G = guild backfill runs. (U = paying users / customers, ≤ R, used for the
once-per-invoice Stripe fixed fee.)
MRR = R × PRICE_RULE_MONTHLY
OneTimeRevenue = C × PRICE_CHANNEL_BACKFILL + G × PRICE_GUILD_BACKFILL
GrossRevenue = MRR + OneTimeRevenue
# Stripe takes pct + fixed PER CHARGE. Monthly: 1 invoice per customer (fixed
# applies once per customer, not per rule-item). One-time: 1 charge per run.
net(price) = price × (1 − STRIPE_PCT) − STRIPE_FIXED
StripeFees ≈ GrossRevenue × STRIPE_PCT
+ STRIPE_FIXED × (U + C + G) # one monthly invoice per user U
NetRevenue = GrossRevenue − StripeFees
− G × INFRA_GUILD_BACKFILL − C × INFRA_CHANNEL_BACKFILL
− BASE_INFRA_MONTHLY
Per-unit net margin (what survives Stripe + infra on one run):
GuildMargin = PRICE_GUILD_BACKFILL × 0.971 − 0.30 − INFRA_GUILD_BACKFILL
ChannelMargin = PRICE_CHANNEL_BACKFILL × 0.971 − 0.30 − INFRA_CHANNEL_BACKFILL
5. The Stripe-fixed-fee trap (worked, placeholder prices)#
The $0.30 flat fee dominates small one-time charges:
PRICE_GUILD_BACKFILL |
Net after Stripe + infra | Stripe's cut |
|---|---|---|
| $0.50 | $0.14 | 72% |
| $1.00 | $0.62 | 38% |
| $3.00 | $2.61 | 13% |
| $5.00 | $4.50 | 10% |
Implication: price one-time backfills at ≥ ~$3 or the processor eats the
margin. Alternatives to consider in tuning: sell backfill credits in bundles
(one Stripe charge, N runs), or fold a backfill allowance into PRICE_RULE_MONTHLY
and treat historical backfill as the upsell over included live dispatch.
6. Open decisions (set these before tuning prices)#
- Billing unit for monthly: per rule — confirmed. Greenfield: no pricing
has shipped yet, so there are no existing subscribers to migrate or grandfather.
The current per-account
active/stripe_customer_idgating is just code scaffolding, not a live paid product — per-rule billing replaces it outright. - One-time fee = per run or per target? Charge every click, or once per channel/guild with re-runs free? Re-runs are idempotent (deterministic rkeys) — e.g. re-running to repair the 1 socket-errored record this session. Charging again for a repair feels bad; "once per target, re-runs free" may be friendlier.
- Flat guild price vs scaled: should
PRICE_GUILD_BACKFILLbe flat regardless of channel count / message volume, or scale (e.g. base + per-channel)? A 40-channel guild and a 4-channel guild cost us nearly the same (≈ $0), but value differs. - Included allowance: does an active monthly job include any backfill (e.g. live dispatch included, history is paid)? Changes whether one-time SKUs are add-ons or the only backfill path.
- Failure/refund policy: refund a cancelled or heavily-errored backfill? Define a threshold (e.g. error_count / dispatched > X).
- Capacity, not cost: guild backfills are bounded by the PDS write rate limit (eurosky ~5000/hr), not infra. Concurrent paid guild backfills could queue behind each other — worth a note if we sell volume.
- Currency / tax: eurosky-leaning audience → price in EUR? Stripe Tax on one-time charges?
7. Next step#
Pick values for the §6 decisions, then plug real usage assumptions (J, C, G)
and candidate prices into §4 to find the price points that clear the Stripe trap and
hit a target margin. This doc is the model; the analysis pass tunes the numbers.