Stories A told story · draft 2026-09-24
Billing
Every credit spent on ONE is one ledger row that carries its real cost, its rate and its margin, written by one function from every door, closed with mark or warn, and read back by agencies, clients, people and agents in the same unit the plan is priced in.
The seven beats
- World
- A tree of groups, each with a pool above it — the funding walk climbs until a balance answers, and the row lands on the funder with the spender named
- Cast
- People and agents who spend, the funder above them who pays, the model that answers, and the agency in between taking its margin
- Knock
- A turn arrives. Tokens go out and tokens come back, at a price the provider set this morning
- Want
- To know what one thing cost, in one unit, at the moment it cost it — a credit that is a price and not a promise of one
- Way
- Every spend is one row that carries its own cost, its own rate and its own margin; delivery marks the spender→model path, refusal warns it, and a quiet model fades
- Turn
- The row that charges the client is the row that prices the plan. Cost and price stop being two books, and the plan's margin becomes a number the cron can read
- Lesson
- Open — a Lesson is learned by someone, and this one is not written yet
Brad's agency billing problem is not that he charges too little. It is that the infrastructure between what he charges and what he pays is full of friction: per-seat minimums, confusing invoices, manual top-ups, and clients who call because they don't know why their bill changed last month.
ONE's billing system has one credit. Three dimensions. One field per audience. That is the whole thing.
The chain above this ledger — who gives credits to whom, chairman to CEO to directors to agents, and how a budget is asked before a model is called — is text/credits.md. Every row here carries its trace (migration 0272: signal_id, cost_usd, est_usd, gen_id, gateway_log_id), so the AI Gateway invoice joins the burn that charged for it.
What a credit is
1 credit = $0.001. One thousand credits to the dollar.
That rate is set by the platform owner and is immutable once a grant is written. It was $0.0001 (ten thousand credits to the dollar) until 2026-08. The published rate is now $0.001; grants written under the old rate keep the rate they were issued at. Old credits age at the rate they were issued. New grants at today's rate. No retroactive conversion, no currency drift.
Everything in ONE — inference tokens, agent runs, skill calls, voice minutes, video sessions, social posts, storage, API calls — is expressed as a burn rate in credits. The credit is the universal unit. Brad never manages per-provider pricing tables or per-model surcharges. He sets one number: his markup. The cascade does the rest.
The three verbs
Grant — credits arrive
A grant is how a workspace gets credits. Sources: Stripe subscription, manual top-up, x402 crypto payment, agency allocation, promo code, or refund. All write to the same credit_grants table. The source is a label. The pool is rail-agnostic.
Credits with an expiry date burn first. Non-expiring credits are preserved until the expiring ones are gone. The user never sees this. It is one line in the debit function.
Burn — credits leave
Every billable action writes a burn row. The invariant:
amount = cost + agency_margin + platform_margin + recipient_share
If the math does not balance, the burn is rejected. A daily cron verifies the ledger. If it breaks, the billing path receives a warn(1) signal and the team is notified before the next morning.
Corrected 2026-09-29: the check runs, and nobody is told The sentence above is not true on
origin/devtoday. It is kept here so the gap stays visible. Nothing rejects a burn on the sum.debitPoolbuildsamountfrom the parts (one.ie/web/src/lib/billing.ts:129), so a burn it writes cannot fail the check. The daily cron (0 4 * * *,workers/billing-cron.ts:122-125) counts violations and logs them. It signalsbilling:verify:failby posting to/api/signalwith the receiver in the body (workers/billing-verify-cron.ts:52-58). Production refuses that path:POST https://one.ie/api/signalanswered 400receiver required(measured 2026-09-29,pages/api/signal/[...receiver].ts:35-37). The receiver is not registered either (grep -c '"billing:verify:fail"' packages/sdk/src/receivers.ts→ 0). Nowarn(1)fires and no one is notified. What exists is a log line and a returned count. § State — 2026-09-29 has the detail.
Measured against production, 2026-09-25 — the run door does not burn Eighteen
agent:runcalls throughone.ie/api/ask(the ONE Router bake,text/one-router.md) left 0 rows incredit_burns; the six-hour window held sevenpublic_chatrows and nothing else. The chat rows that do exist carrytokens_inandtokens_outas null and a cost from the estimator's defaults, and every model is priced at the flat 25/125 credits per thousand (channels/src/substrate.ts:30), which is about five hundred times DeepSeek flash's list price. Rows 1, 2 and 6 above are the fix; the bake is their first consumer that wants an invoice rather than a list price (one-router cards 11–12).The invoice already exists, and it is not this ledger. Every OpenRouter call crosses Cloudflare AI Gateway
channels, which logs tokens in, out and reasoning and OpenRouter's reported cost per request — 4,667 entries back to 2026-07-21 under a 100,000 cap that deletes the oldest. Read back against it the same evening, the bake's chars/4 estimate was 4× low (reasoning tokens are not in the text). So row 2's "every burn row carries tokens and rates" has a source to be rebuilt from: one-router card 13 back-fillscredit_burnsfrom the gateway before the cap bites, and card 14 puts five words (group · actor · signal · via · tags) on every call so the back-fill is attributable by workspace and actor instead of by time window. Until channels ships that header, gateway entries carrymetadata: null.
Gate — access, usage, and count
One concept, three shapes:
| Shape | Controls | Example |
|---|---|---|
| Gate | Can this workspace use this feature at all? | video: on · api_access: off |
| Meter | How much usage is included, and what does overage cost in credits? | video_minutes: { included: 120, rate: 5 cr/min } |
| Seat | How many of this thing can exist? | staff: 25 · clients: 50 |
Gates resolve at request time. Brad sets which gates, meters, and seats his clients get. His clients cannot override what Brad has locked.
The three billing dimensions
Gates — binary access
Two states: on or off. No per-use cost. Covered by the plan or blocked entirely.
| Gate key | What it controls | Min role |
|---|---|---|
brand_removal |
Hide ONE logo from client's UI | admin |
white_label_cascade |
Brand overrides flow to sub-workspaces | owner |
custom_domain |
Host chat at chat.acme.com | owner |
sub_workspace_create |
Create child workspaces (agency → clients) | owner |
team_create |
Create teams / departments | owner |
video |
Video sessions — webinars, classrooms, meetings | member |
social_posting |
Post to social media channels | member |
premium_models |
Access Opus, GPT-5, Gemini Ultra | member |
skill_publish |
Publish to workspace skill library | admin |
api_access |
REST / MCP API with API keys | member |
webhooks |
Outbound webhooks on signals | admin |
attachments |
Upload files in chat | member |
export |
Export conversations, contacts, audit logs | member |
sso |
SAML / SSO login | owner |
Meters — usage-based credits
Three numbers per meter: included (bundled free), rate (credits per unit beyond that), cap (hard ceiling, null = unlimited).
When usage exceeds included, the overage burns from the pool at rate credits per unit. When cap is hit and the meter is hard, the request returns 402 before the action fires.
| Meter key | Unit | What burns |
|---|---|---|
messages |
per message | AI chat responses |
tool_calls |
per invocation | Composio actions, integrations |
agent_runs |
per run | Autonomous agent executions |
skill_calls |
per invocation | Skill marketplace calls |
video_minutes |
per minute | Webinars, classrooms, meetings |
voice_minutes_in |
per minute | Speech-to-text |
voice_minutes_out |
per minute | Text-to-speech |
social_posts |
per post | Social media publishing |
storage_gb |
per GB / hour | File and media storage |
api_calls |
per request | REST / MCP overage |
email_sends |
per 1K | Outbound emails |
sms |
per message | SMS notifications |
Platform base rates (upstream + 10% platform margin, before agency markup):
| Meter | Upstream rate | Platform rate |
|---|---|---|
messages (incl. inference) |
per-model | per-model + 10% |
tool_calls |
2 cr | 2.2 cr |
agent_runs |
10 cr | 11 cr |
skill_calls |
5 cr | 5.5 cr |
video_minutes |
40 cr | 44 cr |
voice_minutes_in (STT) |
8 cr | 8.8 cr |
voice_minutes_out (TTS) |
43 cr | 47.3 cr |
social_posts |
5 cr | 5.5 cr |
storage_gb |
1 cr / hr | 1.1 cr / hr |
api_calls |
0.1 cr | 0.11 cr |
brand_removal |
30 cr / day | 33 cr / day |
export_archive |
100 cr | 110 cr |
At Brad's 20% markup, multiply the platform rate by 1.20.
Open, 2026-08: these per-meter credit counts were set when a credit was $0.0001. They have not been restated for the $0.001 rate, so in dollar terms each row now reads ten times higher than it did. Whether the dollar cost of a meter should hold or rise is a pricing decision, not a doc fix. Treat the numbers above as the old schedule until the owner sets the new one.
Seats — count limits
Hard counts. No credit burn — just a 402 at the invite / create step when the ceiling is hit.
| Seat key | What it counts |
|---|---|
staff |
Workspace members (teammates) |
clients |
Child workspaces (agency → client accounts) |
agents |
Custom agent definitions |
connected_accounts |
Composio / integration accounts linked |
social_accounts |
Social media accounts per channel type |
Plans
Four base tiers: free, pro $100/month, team $300/month, business $1,000/month. Every field is a gate, meter, or seat. Agencies create custom named plans on top of these.
Every paid tier's bundled grant is the price divided by the credit rate, so a
change to the rate moves the grants with it — never hand-set a grant. Prices
live only in one.ie/web/src/lib/plan-prices.ts; grants in billing-config.ts.
Pro and Team are monthly subscriptions (ruling, Tony, 2026-10-06). The card
door mints an embedded Stripe subscription Checkout carrying {workspace, plan};
invoice.paid grants the month's credits once per invoice id, and every
plan-changing webhook branch moves a plan only for the workspace's current
subscription. Two things are still Tony's: card charges are test mode until
/upgrade joins STRIPE_LIVE_PAGES (today only /credits), and renewals grant
nothing until the Stripe webhook sends invoice.paid,
customer.subscription.updated and customer.subscription.deleted.
The earlier ladder (starter $9 · pro $29 · agency $99) is retired. starter and
agency are kept in plan-prices.ts only for workspaces already on them; the
pro key now prices at $100.
free:
price_usd: 0
grant: 1_000 # credits / month — a free allowance, not price-derived
gates:
brand_removal: off
video: off
social_posting: off
api_access: off
premium_models: off
meters:
messages: { included: 100, rate: 1, cap: 500 }
tool_calls: { included: 0, rate: 2, cap: 50 }
seats:
staff: 1
clients: 0
agents: 1
connected_accounts: 1
social_accounts: 0
pro:
price_usd: 100
grant: 100_000 # 100 / 0.001
gates:
brand_removal: on
video: on
social_posting: on
api_access: on
premium_models: on
webhooks: on
team_create: on
meters:
messages: { included: 5_000, rate: 0.5, cap: null }
tool_calls: { included: 2_000, rate: 1.0, cap: null }
agent_runs: { included: 500, rate: 11, cap: null }
video_minutes: { included: 120, rate: 44, cap: null }
voice_minutes_out: { included: 300, rate: 48, cap: null }
social_posts: { included: 200, rate: 5.5, cap: null }
storage_gb: { included: 25, rate: 1, cap: null }
api_calls: { included: 6_000, rate: 0.1, cap: null }
seats:
staff: 25
clients: 0
agents: 20
connected_accounts: 10
social_accounts: 5
team:
price_usd: 300
grant: 300_000 # 300 / 0.001 — pro's gates, meters and seats, more credits
business:
price_usd: 1000 # or 10_000 / year, billed annually
grant: 1_000_000 # 1000 / 0.001
gates:
brand_removal: on
video: on
social_posting: on
api_access: on
premium_models: on
webhooks: on
team_create: on
sub_workspace_create: on
white_label_cascade: on
custom_domain: on
skill_publish: on
meters:
messages: { included: 20_000, rate: 0.3, cap: null }
tool_calls: { included: 10_000, rate: 0.8, cap: null }
agent_runs: { included: 2_000, rate: 11, cap: null }
video_minutes: { included: 500, rate: 40, cap: null }
voice_minutes_in: { included: 1_000, rate: 9, cap: null }
voice_minutes_out: { included: 1_000, rate: 44, cap: null }
social_posts: { included: 1_000, rate: 5, cap: null }
storage_gb: { included: 100, rate: 1, cap: null }
api_calls: { included: 60_000, rate: 0.1, cap: null }
seats:
staff: unlimited
clients: 50
agents: unlimited
connected_accounts: unlimited
social_accounts: unlimited
Creating and applying a plan today.
Platform base plans (free, pro, business, enterprise) live in one config, PLATFORM_BILLING_CONFIG in one.ie/web/src/lib/billing-config.ts:12; staff set a paid base plan or the workspace buys it. An agency builds a plan in /u/[slug]/studio/plans: TemplateGallery (components/billing/designer/TemplateGallery.tsx) to start from a template, PlanSheet to edit cells, publish (POST /api/billing/plan-templates/[id]/publish, refused 422 template_exceeds_plan if it asks for more than the agency has), then apply to a client from the client list (AgencyPlansManager, POST /api/billing/plan-templates/[id]/assign). A client picks from its agency's live, priced templates at checkout (GET /api/billing/plans/available, lib/plans-available.ts; no agency or nothing live shows the platform plans); invoice.paid applies it. An empty cell means unlimited, clamped to the agency's own ceiling (lib/plan-apply.ts); 0 means zero. Enforcement and economics: § Agency custom plans.
One field per audience
The whole pitch. Each layer of the hierarchy sets exactly one field. The cascade fills in the rest.
Tony (platform owner) — one config file
rate_usd_per_credit: 0.001
platform_margin_pct: 10
platform_floor_pct: 5
billing_anchor: monthly
tax: stripe
plans: { free, pro, business } # defined above
Zero setup. Defaults ship in the repo. Every field is mutable from the owner dashboard. None are required to operate.
Brad (agency owner) — three fields
plan: business
markup_pct: 20
brand_lock: true
client_default_plan: free
Three fields. Setup time: 30 seconds. Brad is now selling at 20% margin above cost to every client, all under his own brand.
The platform_floor_pct is the one constraint he cannot move: after every cascade resolves, platform margin is clamped up to 5%. Brad can give clients wholesale prices (markup of 0%) but cannot starve the platform.
Client (startup1) — one field
plan: pro
monthly_cap: 50_000
One field if Brad wants to lock the budget. Zero fields if the client should just run the plan. Card on file. Setup time: 10 seconds.
Team (startup1-eng) — one field
allocation: 30_000
cap_locked: true
Engineering gets a monthly slice of the client's pool. When it is spent, requests return 402. Not an approximation — a hard stop before the LLM is called, not a refund after.
End user — one button
[ Top up $5 ]
If the workspace is sponsored, the user sees no billing surface at all.
The cascade
mergeCap (parent, child) = min(parent, child) // children can only LOWER
mergeMarkup(parent, child) = max(parent, child) // children can only RAISE
mergeGate (parent, child) = locked ? parent : (child ?? parent)
mergeMeter (parent, child) = locked ? parent : deepMerge(parent, child)
mergeSeat (parent, child) = locked ? parent : min(parent ?? ∞, child ?? ∞)
floorMargin(margin, owner) = max(margin, owner.platform_floor_pct)
Brad sets which gates are locked, which meters have locked cap or rate, and which seat ceilings his clients cannot raise. The cascade is the only authority. No override flag, no hardcoded slug list, no one-off DB toggle.
What clients see
One card. Every viewer sees it.
┌──────────────────────────────────────────────────────────────┐
│ Pro plan Pool · 23 415 / 100 000 cr │
│ resets in 18d │
│ │
│ ████████░░░░░░░░░░░░░░░░░░ 5% used │
│ messages 4 200 · video 8 100 · tools 11 115 │
│ │
│ Auto-top-up off [Top up] │
└──────────────────────────────────────────────────────────────┘
Below the card, pages appear based on viewer role. The nav groups them into four verbs — Fund · Distribute · Watch · Platform:
/u/[slug]/billing (≥ client) Fund
│ pool card + per-meter entitlement bars
│ + usage ledger (filter by reason/actor) + top-up
├─ /plans (agency) Fund
│ pricing config (markup, cap, default plan)
│ + client plan assignment + plan templates
├─ /allocations (agency) Distribute
│ per-client/team budgets — pool/transfer/resell
├─ /costs (≥ client) Watch
│ revenue tree (billed → COGS → margin) + coupons
├─ /invoices (≥ client) Watch
│ monthly invoice list, downloadable PDF
├─ /transactions (≥ client) Watch
│ unified settlements ledger
├─ /simulate (≥ client) Watch
│ cost simulation
└─ /platform (platform) Platform
rate, default plans, model catalogue
Markup, margins, and per-plan economics are designed in the
plan sheet at /u/[slug]/studio/plans (agency/platform).
How gates resolve
- Workspace billing config loaded from D1 (KV-cached, waterfall to parent).
gates[key]→'on' | 'off'(binary access check before the action fires).meters[key]→{ included, rate, cap }— burn computed asmax(0, units - included) × rate.seats[key]→ integer or null — checked at create/invite time; 402 if at ceiling.enforceEntitlement()inmiddleware/entitlement.tswraps this for API routes.- Burns recorded async via
recordEntitlementUsage()— not on the hot path.
Gates are checked before the action. Meters burn after the action succeeds (or before for capped meters). Seats are checked at write time, never at read time.
Source: one.ie/web/src/lib/billing/gate.ts · middleware/entitlement.ts · lib/types.ts#FeatureKey
Video billing
Video sessions (webinars, classrooms, meetings) burn from the video_minutes meter.
gate.video = 'off' → sessions blocked; CTA shown to upgrade
gate.video = 'on' → sessions allowed; meter runs
burn per session = session_duration_minutes × video_minutes.rate
The included bundle (video_minutes.included) covers e.g. 120 min/month on Pro. Once exhausted, each additional minute burns at the overage rate. Brad sets the rate for his clients; the cascade applies his markup on top.
Three video modes burn at the same rate. The reason tag on the burn row records webinar, classroom, or meeting for itemised ledger entries.
Social media billing
Social posting burns from the social_posts meter and respects the social_posting gate and social_accounts seat.
gate.social_posting = 'off' → scheduling blocked
seat.social_accounts = 2 → can connect 2 accounts per channel type
meter.social_posts = { included: 200, rate: 5.5 cr/post }
The seat controls how many accounts connect. The meter controls how many posts publish. Brad can sell social as a seat upsell ("add a 3rd account for $X/month") or as a meter upsell ("buy a social post pack").
Agency custom plans
Agencies create named plan configs that inherit from a base tier and override specific gates, meters, and seats. Templates are stamped onto clients instead of assigning a raw tier.
# Brad's "Social Agency Pro" template (inherits from pro, overrides social)
name: Social Agency Pro
base: pro
overrides:
gates:
social_posting: on
meters:
social_posts: { included: 1_000, rate: 4 }
video_minutes: { included: 300, rate: 40 }
seats:
social_accounts: 20
connected_accounts: 25
The plan designer grid (/u/[slug]/studio/plans) renders this as a column. Only overridden rows show the "customized" badge. Non-overridden rows inherit from the base tier and render as read-only.
A limit cell means one thing everywhere: empty = unlimited (stored NULL), 0 = none. Runtime billing reads the same.
How a template is enforced (one.ie/web/src/lib/plan-apply.ts). A client is put on a published revision two ways — paying for it (Stripe invoice.paid, amount ≥ the revision's price) or its agency assigning it (no payment, nothing minted). Either way planColumnToBillingConfig clamps the column to the agency's own effective plan: a gate the agency lacks stays off, seats, meters, grant and cap take the agency's ceiling, a meter rate is never under the agency's own, markup never under platform_floor_pct. Gates and seats land in owners.plan_config (read by middleware and resolvePlanForSlug); the monthly ceiling (cap, else the grant) is an allocations row from the agency to the client, which debitPool refuses on. Publishing a template that asks for more than the agency has is refused (422 template_exceeds_plan). A paid base plan (pro · business · enterprise) is never picked by an agency — platform staff set it or the workspace buys it.
Plan economics price a template's platform cost from its model mix (house · flash · frontier) through the SDK's spend() at live list rates, so the same grant costs the platform less on DeepSeek Flash than on a frontier mix. The mix is a prior until text/model-economics.md measures real usage.
Add-ons (stacked on any plan)
Sold separately. Each add-on is either a gate flip, a meter top-up, or a seat raise — the same three shapes, just purchased outside the plan.
| Add-on | What | Billing |
|---|---|---|
| White-label | brand_removal on + custom_domain on |
flat monthly |
| Extra credits | Grant added to pool | per purchase |
| Video pack | video_minutes.included += N |
per pack / month |
| Social pack | social_posts.included += N · social_accounts += M |
per pack / month |
| Extra storage | storage_gb.cap raised |
per GB / month |
| Voice pack | voice_minutes_in/out.included += N |
flat monthly |
| Extra API calls | api_calls.included += N |
per 10k / month |
| Extra clients | clients += N |
per seat / month |
| Extra staff | staff += N |
per seat / month |
Trials and plan changes
Free trials
Every new workspace can start on a 14-day trial of any paid plan. No credit card required. Brad configures trial length per plan template. At trial end: if a card is on file, the subscription converts. If not, the workspace drops to over_limit.
Upgrades
Delta credits land immediately at the upgrade timestamp. Stripe pro-rates the charge over the remainder of the billing period. Existing balance is preserved.
Downgrades
Existing balance is preserved until the current period ends. The new (lower) grant applies at the next billing anchor. The downgrade modal shows "you'll keep N credits until [date], then drop to N'/month" before the client confirms.
Cancellation
Cancellation is scheduled at end of period. The pool card shows a banner: "Subscription ends [date] — Renew." Credits remain usable until period end. After that, the workspace transitions to over_limit.
Cancellation can be undone until the period ends.
Invoices and itemised billing
Every billing period produces a real invoice. Each invoice shows:
- Workspace name, billing period, invoice number
- Line items: reason × model × quantity × rate × amount (broken down by gate / meter / seat)
- Credits applied from grants, promo codes, or coupons
- Subtotal → tax → total
Video sessions itemise by session type (webinar / classroom / meeting). Social posts itemise by channel. Tool calls itemise by integration. No CSV wrangling — one click, PDF ready.
Promo codes and coupons
Brad issues discount codes to clients. Codes can be:
- Percentage off — e.g. 20% off the first three months
- Fixed credits — e.g. 10,000 bonus credits on signup
- Seat unlock — e.g. 2 extra social accounts for 30 days
Cadence: once, repeating (N months), or forever. Brad creates codes from the agency billing panel.
The rails
Three rails. One ledger.
| Rail | Used for |
|---|---|
| Stripe | Subscriptions, fiat top-up, upsells |
| x402 (multi-chain) | Crypto top-up, machine-to-machine, skill marketplace |
| Internal transfer | Sponsorship, allocation, refund, creator payout |
| Test / sandbox | Integration testing without real charges |
All write to the same credit_grants and credit_burns tables. Rail is a source label.
x402 accepts USDC on Base, Ethereum, Arbitrum, and Optimism at 1:1 to credits. Native SUI, SOL, ETH, and BTC settle at the spot oracle rate at receipt time.
Author payouts — no Stripe Connect needed
When a burn names a recipient, the same transaction writes a paired payout Grant to that author's workspace pool. One transaction, two rows, atomic. No payout cron, no $50 minimum.
Lifecycle
A workspace is always in one of six states:
balance >= 0 → live (all features work)
balance < 0 + active card → recovering (Stripe retries: 3 attempts / 7 days)
balance < 0 + retries exhausted → over_limit (reads work; writes and inference gated)
balance < -plan.grant → floored (402 even on cheap calls)
30 days at over_limit / floored → suspended (frozen; data preserved 90 days)
90 days suspended → archived (R2 export emailed; workspace deleted)
Reads always work.
| State | Gated for | Who can still act |
|---|---|---|
live / recovering |
nobody | all roles |
over_limit |
member, viewer, agent |
owner, admin, auditor (reads) |
floored |
member, viewer, agent |
owner, admin (billing only) |
suspended |
all except owner |
owner (read archive) |
archived |
all | nobody |
Allocations — department budgets
One field. Three modes.
| Mode | Picture | When |
|---|---|---|
pool |
Teams draw from the parent pool | Central budget, trusted teams |
transfer |
Parent grants a fixed monthly amount | Department budgets |
resell |
Teams buy at parent rate + markup | Agency selling to clients |
acme (1M pool, business plan)
├─ acme-marketing pool → shares acme's 1M
├─ acme-engineering transfer 400k/mo → own pool, refilled monthly
├─ client-startup1 resell +20% → buys from acme at markup
│ ├─ startup1-eng pool → shares startup1's pool
│ └─ startup1-design pool
└─ client-startup2 resell +20%
The math
The Business plan is $1,000/month for 1,000,000 credits. Credits carry a face value of $0.001; at Brad's 20% markup his clients buy usage at $0.0012 per credit.
Brad sells a custom plan template at $150/month bundling 50,000 credits, 120 video minutes, and 25 staff seats. Those 50,000 credits cost Brad $50 at face value. Monthly revenue: $150. Gross margin: 67%, before Brad has spoken to anyone.
At 100 clients averaging $300/month on Brad's templates: $30,000 monthly revenue against ~$10,000 of credit cost. Gross profit: $20,000 per month. That scales without headcount.
Scheduled jobs
| Job | Schedule |
|---|---|
| Auto top-up (below 10% of plan grant) | Hourly |
| Monthly credit allocation + meter reset | Daily 01:00 UTC |
| Usage alerts (50% / 80% / 95%) | Daily 02:00 UTC |
| Lifecycle escalation | Daily 03:00 UTC |
| Ledger integrity verify | Daily 04:00 UTC |
| Credit expiry sweep | Daily 00:00 UTC |
Three of these are off by flag in code: auto top-up (a stub), allocation and alerts
(one.ie/web/src/workers/billing-cron.ts:11-26, :87-111). None of the three flags is
in wrangler.toml [vars]. The expiry sweep has no 0 0 expression of its own; it
runs inside the 03:00 lifecycle job (billing-cron.ts:112-115). § State — 2026-09-29.
The three invariants
The ledger always balances, and writes once. Idempotency keys prevent double-crediting. The daily verify cron asserts both ledgers.
Caps are honoured before the call, not after. Gate and meter checks fire before the LLM, before the video session starts, before the social post publishes. A workspace that is over budget returns 402. The floor is -plan.grant.
The cascade is the only source of authority. No override flag, no hardcoded slug list. Every audit question answers by walking the cascade.
The ledger — every credit, its real cost
credit_burns is the ledger, not a table this story invents. The columns already
exist across four migrations — 0017_credit_burns.sql (the row itself),
0105_burn_usage.sql (tokens_in/tokens_out/unit_count), 0104 (actor/"group"),
0213 (origin) — so folding this story in is additive columns and one writer, never
a parallel system.
Two writers disagree today, and that disagreement is the defect this story closes.
The web door (api/chat.ts:1445) prices every token at input_per_1k and never reads
the output rate. The channels door (channels/src/substrate.ts:30-33) uses a flat
INPUT_RATE = 25 / OUTPUT_RATE = 125, model-blind, margins hardcoded to 0,0,0 — at
CREDITS_PER_USD = 1_000 that is $0.025/1k, roughly 10x the SDK fallback and up to
~100x a cheap flash model. The same turn prices differently depending on which door it
walked through. One writer, spend() in packages/sdk/src/billing.ts, replaces both:
it prices input and output at their own rateFor(model, kvBlob) rates — read from the
live OpenRouter KV blob openrouter:models:v1, kept fresh by
one.ie/web/src/workers/openrouter-sync.ts — and computes cost_credits,
amount_credits, agency_margin, platform_margin and recipient_share on one row.
Every burn row this story adds carries: provider, thread_id (the dsh "session",
already on agent_events_warm.thread_id), tokens_cache_hit, rate_in_per_1k,
rate_out_per_1k, rate_source (live = KV blob · list = a subscription priced at
the same model's OpenRouter list rate, never zero · fallback = the SDK's last-resort
constant), ttft_ms, duration_ms, and signal_id — the credits.spent event the row
closed. Cost is re-derivable from tokens × rate on the row itself: an audit is a
query, not a re-fetch. reconcileOneSupply (lib/billing.ts:31-40) already exists and
has no caller anywhere in web, channels or sync — this story gives it a daily cron and
makes the invariant amount = cost + agency_margin + platform_margin + recipient_share
a promise that is checked, not merely stated.
Corrected 2026-09-29: reconcileOneSupply has a caller now,
runBillingVerify (workers/billing-verify-cron.ts:101, row 8, shipped in PR #187).
It runs only when a supply reader is passed. The 0 4 * * * caller passes none
(workers/billing-cron.ts:125), so in production the comparison never runs and
answers unread. The split invariant is re-added over every real-money row
(billing-verify-cron.ts:42-46).
Every enrolled agent starts funded: agent-enroll.ts:131-138 already grants
source='platform:enroll' for AGENT_STARTER_CREDITS, unset today so the grant is
zero. Setting the var and pinning test_mode: 1 while the network is testnet turns a
config gap into a feature. Its wallet derives from its human's master key by HKDF
(purpose: 'agent'), never fresh CSPRNG, and its spend-ceiling is owned by the human
that spawned it — the same authority walk billing already runs, applied to an agent's
own spend.
State — 2026-09-29
What changed since 2026-09-28, and where each promise stands. Read from code on origin/dev at 67fc9c312; the billing code is the same on origin/main (git diff origin/main origin/dev over lib/billing.ts, lib/credits*, workers/ and packages/sdk/src/billing.ts is empty). How it all fits together: text/billing-docs.md.
Shipped since 2026-09-28
- PR #174 (
aaedc2d55) and PR #175 (bf1c679d2) are merged tomainand in production. The list held at gates under 2026-09-28 has shipped. - Prod D1
one-owners, probed read-only on 2026-09-29: thex402_quotesandgateway_callstables exist (2/2).credit_burnscarries the five 0272 trace columns (5/5).ownerscarriesplan_configandplan_templatefrom 0273 (2/2). In code, 0271 createsx402_quotesonly (0271_x402_quotes.sql:27).gateway_callsis 0268's table, rebuilt by 0272 so one burn can link many calls (0272_burn_trace.sql:35-57). - PR #187 shipped on 2026-09-29:
main6d36360e3=devb2e3ef7f6, one-prod versionf66ce18d, channels2e3bc2cc. It carries row 8's build (task:01a0ec72904c9a2e63186441) and the credits wave.
Row 8's build. runBillingVerify(env, opts = { readOneSupply? }) (one.ie/web/src/workers/billing-verify-cron.ts:28-31) makes three checks:
- The split check re-adds every real-money row in one statement (
:42-46). The oldLIMIT 10000blind spot is gone. - A runaway floor fails any group whose balance is below −10× its grants (
:63-78). reconcileOneSupply(lib/billing.ts:31) has its first caller (:101). Liability isSUM(credit_grants.amount_credits)on railone-tokenthroughrealMoney()(:97-98).
It returns supply: 'ok' | 'drift' | 'unread' and drift (:31). On drift it counts a violation and posts billing:verify:fail (:102-116). Test: tests/unit/credits/ledger-reconciles.test.ts, six cases. It ran on 2026-09-29 with chain-debit.test.ts through gate-run.sh test: 12 of 12 passed. That was a targeted run, not a lane.
Still dark, and why
- The supply comparison never runs in production. The
0 4 * * *caller passes no reader (workers/billing-cron.ts:125), sosupplyis alwaysunreaduntil an on-chain ONE-supply reader is wired. - Neither ledger check tells anyone. Both
billing:verify:failposts go to${ONE_API_URL}/api/signalwith the receiver in the body (billing-verify-cron.ts:52,:108). That path has no receiver segment, and the route refuses it first (pages/api/signal/[...receiver].ts:35-37). Measured on production, 2026-09-29:POST https://one.ie/api/signal→ 400receiver required. The.catch(() => {})never fires on a 400, so the refusal is silent.billing:verify:failis not in the registry either (grep -c→ 0).ledger-reconciles.test.tsstubsfetchand asserts that same URL, so its green pins the broken path. What is live isconsole.errorand the returned count.workers/billing-lifecycle-cron.ts:39posts the same way. - The
0 1 * * *allocation job is off. It runs only whenBILLING_ALLOCATION=1(billing-cron.ts:94-101). The #187 ship notes say that flag is unset on one-prod, and it is absent fromwrangler.toml[vars]. The reason is the month-2 double debit (billing-cron.ts:17-21). - The invoice cron auto-bills nothing.
GATEWAY_AUTOBILL_VIAstarts empty (workers/gateway-invoice-cron.ts:203). - The release gate was a delta. The #187 release ran a delta test gate, not the full suite.
The accept exit column below is each row's own accept: run alone from the repo root on 2026-09-29. None of those lines changed. Exit 0 on a presence check is not a green proof.
| # | slug | state | evidence | accept exit |
|---|---|---|---|---|
| 1 | billing-one-writer | built; accept red | spend() at packages/sdk/src/billing.ts:230. Chat prices through it in priceTurn (lib/chat/turn-burn.ts:63-90), reached from api/chat.ts:1680 via meterChatBurn. The accept greps api/chat.ts for output_per_1k, which moved to turn-burn.ts:90 |
1 |
| 2 | billing-row-is-the-cost | partly | provider and thread_id exist (0269_burn_pricing.sql:13-14) and are written (lib/billing.ts:183). So are the rates and the five 0272 trace columns. tokens_cache_hit, ttft_ms and duration_ms exist in no migration (grep -rn over migrations/ → 0 each) |
0 |
| 3 | billing-spend-is-a-signal | built | credits:spend in the registry |
0 |
| 4 | billing-agent-ten-thousand | open | AGENT_STARTER_CREDITS unset, so starterCredits() returns 0 (lib/agent-enroll.ts:52-56). The value is Tony's decision. The grant INSERT writes no test_mode (agent-enroll.ts:134) |
1 |
| 5 | billing-agent-wallet-derived | open | unchanged: grep -c "purpose: 'agent'" lib/derive-multichain.ts → 0, grep -c spend-ceiling lib/agent-enroll.ts → 0 |
1 |
| 6 | billing-windows | open | usageByChain (lib/credits/usage-by-chain.ts:39) is the chain view, returning total/truncated/asOf (:157-165). It has no caller and no time window. credits:usage is not registered |
1 |
| 7 | billing-caps-warn | partly | debitPool walks each recipient's allocation chain and refuses budget_exhausted with ranOut naming the short row (lib/billing.ts:146-151, credits card 3). No credits:cap receiver, no warn at the alert line |
1 |
| 8 | billing-ledger-reconciles | built. The whole-ledger re-add is live; the supply comparison is dark (no reader) | above. The drift signal is refused (400) | 2 |
| 9 | billing-plan-margin | partly | unchanged: computePlanEconomics (components/billing/designer/economics.ts:67). No plans:margin receiver; /pricing does not quote it |
1 |
Row 8's accept exits 2 because it greps workers/billing-ledger-cron.ts, a file that does not exist; the job is billing-verify-cron.ts. The world: entry billing-ledger-cron worker names the same missing file.
State — 2026-09-28
What the 2026-09-28 billing lanes changed, and where each promise below stands. Shipped means in production; landed means on dev and waiting for the next release. Superseded by § State — 2026-09-29; kept as the record of that day.
Shipped (PR #170, fdbbb6917, ~10:47)
- Provisioning self-mint closed: no grant without the attested staff bit.
- x402 cross-rail double mint refused.
- Agent float fails closed: a failed
agent_floatread answers503 credit_check_unavailable. - Credit = $0.001: migration
0270_grant_rate_default.sqlrecordsconversion_rate= 0.001; pay grants 1,000 credits/USD. - The web AI Gateway is mirrored into
gateway_calls(GATEWAY_INVOICE_GATEWAYS). - The
studioplan is removed; the 17 studio workspaces moved to free by a prod UPDATE after the ship.
Landed on dev, not shipped (PR #174, held at gates). Shipped since: #174 aaedc2d55, #175 bf1c679d2, migrations 0271–0273 applied to prod D1 (2026-09-29).
- x402 quotes bound to the claimant (
0271_x402_quotes.sql; pay/verifyreturnsamountAtomic,decimals,confirmedAt,network). - Free tier on DeepSeek Flash (
packages/sdk/src/free-tier.ts:deepseek/deepseek-v4-flashorgoogle/gemini-3.5-flash-lite, 50 requests per workspace per UTC day, output clamped to 4,000 tokens, a tenant-chosen paid model refused402 model_not_free). - Sponsored budget judged before the model (
SPONSOR_DEFAULT_MONTHLY_CREDITS= 50_000 inpackages/sdk/src/billing.ts:130, drawn from the inviter's pool, refusalbudget_exhausted). - Ledger trace columns (
0272_burn_trace.sql:signal_id,cost_usd,est_usd,gen_id,gateway_log_id). - Invoice cron joins calls to burns by gateway log id, then signal, then tokens, warns once on drift naming the model, and can auto-bill allowlisted doors (
GATEWAY_AUTOBILL_VIA— it starts empty, so it bills nothing yet; the candidates are intext/credits.md§ Which calls the AI Gateway sees). - Web and channels call sites are routed through the AI Gateway with five metadata words.
- Plan templates enforced (
0273_owner_plan_config.sql:owners.plan_config,owners.plan_template). /api/billing/clientsself-mint closed; paid base plans staff-set or bought; plan economics priced per model mix.
| # | slug | state | evidence |
|---|---|---|---|
| 1 | billing-one-writer | built | spend() at packages/sdk/src/billing.ts:230, flat 25/125 gone from channels/src/substrate.ts |
| 2 | billing-row-is-the-cost | partly | rates (0269) and trace (0272) on the row; thread_id, ttft_ms, duration_ms not added |
| 3 | billing-spend-is-a-signal | built | credits:spend in the registry |
| 4 | billing-agent-ten-thousand | open | AGENT_STARTER_CREDITS unset, grant 0 |
| 5 | billing-agent-wallet-derived | open | its own accept greps (purpose: 'agent' in lib/derive-multichain.ts, spend-ceiling in lib/agent-enroll.ts) both return 0 |
| 6 | billing-windows | open | no credits:usage receiver |
| 7 | billing-caps-warn | partly | budget_exhausted refused before the model for a sponsored child; no credits:cap receiver, no warn at the alert line |
| 8 | billing-ledger-reconciles | open | reconcileOneSupply still has no caller; the hourly invoice drift warn is a different check |
| 9 | billing-plan-margin | partly | model-mix economics (computePlanEconomics, components/billing/designer/economics.ts); no plans:margin receiver, /pricing does not quote it |
A state read from code and a proof file is not a green proof; the proof commands in the frontmatter are unchanged.
Corrected 2026-09-29: row 2's "thread_id not added" was wrong on the day. 0269_burn_pricing.sql:14 added it, and debitPool writes it.
The promises
Nine promises, each with exactly one checkable proof — a command, never prose. The
full schedule, with every item:/accept: pair and the joined proof:, lives in this
file's own frontmatter (deliverables: / proof: / assumes:), lint-checked by
.claude/scripts/do-promise-lint.sh billing.
| # | slug | terms |
|---|---|---|
| 1 | billing-one-writer |
One spend() prices every turn at its own rates; the flat 25/125 and the input-only meter are gone |
| 2 | billing-row-is-the-cost |
Every burn row carries provider, thread, rates and timings; a re-price from the row's own fields reproduces cost_credits |
| 3 | billing-spend-is-a-signal |
credits:spend is a real receiver; every write emits credits.spent and closes — mark on delivery, warn on refusal or a failed write |
| 4 | billing-agent-ten-thousand |
Every enrolled agent receives 10,000 credits, test_mode pinned to the network |
| 5 | billing-agent-wallet-derived |
An agent's wallet is HKDF-derived from its human's master; its spend-ceiling owner is the human |
| 6 | billing-windows |
credits:usage answers one row set in every dsh window, total/truncated/asOf always |
| 7 | billing-caps-warn |
credits:cap warns before the alert threshold and refuses with budget_exhausted at the cap |
| 8 | billing-ledger-reconciles |
A daily cron re-adds every row and calls reconcileOneSupply; drift warns and notifies before morning |
| 9 | billing-plan-margin |
plans:margin states each plan cohort's margin against the platform floor; /pricing quotes it |
Waves and build sequence: text/billing-plan.md § 14 The waves.
Cross-references
billing-plan.md— technical implementation: D1 schema, endpoints, build sequencebilling-credits-plan.md— credits buy & allocate UX + distribution bookbilling-taxonomy-plan.md— per-feature implementation status (✅ / 🟡 / ⬜)billing-plans-ui-plan.md— plan designer grid (PlanSheet, RowDefs, economics)roles-authority-plan.md— the walk-up that gates who may allocate
One credit. Three billing dimensions. One field per audience. Gates say yes or no. Meters say how much. Seats say how many. The cascade does the rest.
Connected to
Every link here was checked against the repo when the page was built. 21 names the story uses could not be found and are shown without a link.
People
- the platform owner (one rate, one margin floor)
- the agency owner (one markup)
- the client (one cap)
- the end user (one button)
Agents
Each opens a chat with that agent.
- every enrolled agent — 10,000 credits at birth, a derived wallet, a ceiling its human owns
- ceo (reads plans:margin and billing:clients)
- release-manager (ships the cron)
Skills
Each opens the skill's page.
- models (m.sh — every measured call is a priced row)
- planning (the board is where the caps live)
- promise-settle (the cron is a settle)
Paths
The flows this story walks — trigger → step → step.
- trigger: chat turn ends (api/chat.ts flush · channels middleware wrapStream) -> tool: credits:spend -> condition: ok? -> tool: mark(actor to model) | warn(actor to model, reason)
- trigger: cron daily -> tool: reconcileOneSupply + burn invariant (amount = cost + agency + platform + recipient) -> condition: drift? -> tool: warn(billing to ledger, 1) + human:notify owner
- trigger: credits:spend where period_used + amount > cap -> condition: alert_pct crossed? -> tool: warn + human:notify funder; at cap -> refuse 402 budget_exhausted
- trigger: agent enrolled -> tool: credit_grants platform:enroll 10000 (test_mode 1 on testnet) -> tool: derive wallet (purpose: agent, scope: human) -> tool: spend-ceiling minted, owner = the human
Tools
Receivers and scripts it signals.
- credits:spend — the one writer (sdk spend() -> credit_burns), called by api/chat.ts, channels substrate.recordBurn, billTool, execute-skill
- credits:usage — one read, windows and group_by, total + truncated always
- credits:cap — monthly cap + alert threshold on a workspace or an actor (reuses billing_allocations.monthly_credits/period_used)
- plans:margin — per plan: grant, cost burned, billed, margin_pct, floor verdict
- billing:clients · billing:topup · wallet:get — existing, unchanged
- rateFor + openrouter-sync.ts — the price; reconcileOneSupply — the check
Promises
What it holds; a told promise opens its own story.
- billing-one-writer
- billing-row-is-the-cost
- billing-spend-is-a-signal
- billing-agent-ten-thousand
- billing-agent-wallet-derived
- billing-windows
- billing-caps-warn
- billing-ledger-reconciles
- billing-plan-margin
Docs
What derives from this file.
Events
Every event is a signal; each names the path it marks or warns.
- credits:spend · from api/chat.ts flush · channels wrapStream · billTool · execute-skill mark (actor to model) on ok; warn (actor to model) on 402 | floored | budget_exhausted | float_locked tags: billing, spend
- credits:cap · from the funder — agency, client owner, or the agent's human warn (funder to spender) at alert_pct; refuse at 100 tags: billing, cap
- billing:topup · from the person paying mark (rail to workspace) on invoice.paid tags: billing, grant
- world:announce · from billing-ledger-cron warn (billing to ledger, 1) on drift or a row whose parts do not sum tags: billing, ledger
Transactions
- credit_grants — Stripe invoice.paid · x402 coupon · agency allocation · platform:enroll · sponsorship/promo/refund (api/billing/grant.ts, staff only)
- credit_burns — every spend; amount = cost + agency_margin + platform_margin + recipient_share (sdk computeBurn)
- token_costs — COGS in cents, derived from the same row (instrument-panel.ts:123); never a second source
- Coin<ONE> on Sui testnet — the mirror; supply ≡ liability (one-token.md § 8.1, 8.5)
Analytics
- credits:usage — by model · provider · actor · thread · hour · day · month · plan; read at /u/[slug]/billing and by the ceo digest
- plans:margin — per plan cohort: grant, cost_credits, amount_credits, margin_pct vs platform_floor_pct; read on /pricing by the agent
- billing:clients — agency P&L (exists)
- the ratchet: rows with tokens_in IS NULL AND reason='inference' — must only fall