foldrun 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ # Backups and restore
2
+
3
+ Two different things get called "a backup" here, and they bring back
4
+ different things:
5
+
6
+ | | What it brings back | Who does it | How far back |
7
+ |---|---|---|---|
8
+ | **A workspace's history** | its agents, flows, knowledge, skills, tools, scripts — the markdown | you, from Settings → Backups, `foldrun restore` or the API | every change it ever had |
9
+ | **The nightly snapshot** | everything else: runs, `state/`, storage's index, secrets, people, keys, the ledger, the audit log | the platform team, when you ask | the snapshots kept (below) |
10
+
11
+ You do not need to switch either on.
12
+
13
+ ## What the nightly snapshot holds
14
+
15
+ On the hosted platform today (a single server), a job runs
16
+ **nightly at 03:30, server time (Sydney)** and archives every account
17
+ together:
18
+
19
+ - the whole data volume: every workspace's files — source, `memory/`,
20
+ `state/`, run records — each workspace's git history, the account's
21
+ encrypted secrets (the vault) and the storage index;
22
+ - the database (`pg_dump`): people, API keys, sessions, the ledger and every
23
+ balance, the queue, the audit log;
24
+ - the server's key file.
25
+
26
+ **Encrypted before it leaves the server.** Every file the job writes — the
27
+ archive (with the database dump inside it), the list of what it holds, and
28
+ the key file — is encrypted with [age](https://age-encryption.org) to a
29
+ public key whose private half is **not** on the server: the server can make
30
+ a backup and cannot open one, and neither can anyone holding the object
31
+ storage it is copied to. The archive is encrypted as it is written, so it is
32
+ never stored unencrypted, even on the server. The job refuses to run rather
33
+ than copy anything unencrypted. Snapshots taken before this change
34
+ (October 2026) were copied off the server unencrypted, all but the key file:
35
+ the first encrypted run deletes those copies from object storage, and the
36
+ server's own copies of those nights age out of the 14 kept.
37
+
38
+ Before an archive is kept it is checked: the database dump must be readable
39
+ by `pg_restore --list` and list table data, row counts are taken from the
40
+ live database at the same moment, and the archive is listed in full and
41
+ its checksum recorded — of the very bytes being encrypted, in the same pass.
42
+ An archive that fails the check is deleted and the older ones are kept.
43
+ That an encrypted archive decrypts, with the private key, to exactly that
44
+ checksum is proved by an automated test of the whole job on every change to
45
+ it; the server cannot prove it nightly, because it does not have the key.
46
+
47
+ **Kept:** the last 14 on the server, and 15 days in object storage off it
48
+ (Cloudflare R2).
49
+
50
+ A full restore from one of these archives was performed on 2026-09-05, from
51
+ the R2 copy: the archive matched the server's byte for byte and restored the
52
+ workspaces, run records, the database and the sealed key file. That was
53
+ before the archive itself was encrypted; *not yet performed* on an encrypted
54
+ one — the next restore drill is.
55
+
56
+ ### What it does not hold
57
+
58
+ - **Storage file bytes.** On the hosted platform the files in a workspace's
59
+ Storage live in object storage (R2), not on the data volume; the snapshot
60
+ has the index of what exists, not the bytes. *Unverified:* whether a
61
+ storage file deleted by mistake can still be recovered from object
62
+ storage. Ask, and say when it was deleted.
63
+ - Anything that only existed inside a sandbox while a step ran.
64
+ - What model providers and connected services hold on their side.
65
+
66
+ A self-hosted install backs up however its operator set it up; this page
67
+ describes the hosted platform. `foldrun-infra/dev/backup.sh` is the job, if
68
+ you want the same for yours: it needs `age` and `FOLDRUN_BACKUP_RECIPIENT`
69
+ (your age public key) and will not run without them.
70
+
71
+ ## Seeing your snapshots
72
+
73
+ Settings → **Backups** shows how often, how many are kept, when the last one
74
+ was taken and whether it was verified, copied off the server and
75
+ **encrypted**, and each snapshot that holds your account, newest first —
76
+ each one marked encrypted or not (the ones from before October 2026 are
77
+ not).
78
+
79
+ ```sh
80
+ foldrun backups # the same, in the terminal
81
+ ```
82
+
83
+ ```http
84
+ GET /api/account/backups
85
+ ```
86
+
87
+ Each archive the job verifies is recorded with the accounts it holds; an
88
+ account sees only the archives it is in, and its own name as the only entry
89
+ in `accounts`. Each record says `encrypted` and
90
+ names the key it was encrypted to by fingerprint (`recipient`,
91
+ `sha256:` and 16 hex digits of the public key) — a change of fingerprint is
92
+ a change of key. *Not yet verified on the server:*
93
+ the recording landed on 2026-10-01, so the list starts with the first
94
+ nightly run after that deploy — older archives exist but are not listed.
95
+
96
+ ## Restoring a workspace yourself
97
+
98
+ Every change to a workspace — a dashboard save, a deploy, a `git push` — is
99
+ a commit in its own git repository. Restoring puts the workspace's source
100
+ back as it was at one of those commits:
101
+
102
+ ```sh
103
+ foldrun restore sales --to 2026-09-28T14:00 --dry-run # what would change, with the diff
104
+ foldrun restore sales --to 2026-09-28T14:00 # asks you to type the name, then does it
105
+ ```
106
+
107
+ `--to` takes a commit (or the start of one), a branch or tag, a date or time
108
+ (the last commit at or before it — a time without a zone is read in the
109
+ server's timezone, so add `Z` or an offset to be exact), or an age: `90m`, `12h`, `3d`, `2w`.
110
+
111
+ - **Preview first.** The dry run lists every file that would change, come
112
+ back, or go, with a line diff. Nothing is written.
113
+ - **Confirm by name.** The restore itself needs the workspace's name typed
114
+ back (`confirm` in the API). The CLI sends the removals you were shown, and
115
+ if anything else would now be removed — a file added in between — the
116
+ restore is refused and you are asked to look again.
117
+ - **It is undoable.** A restore is a new commit whose files are the old
118
+ ones; history says "restored to abc1234". To undo it, restore to the commit
119
+ it answered as `from`.
120
+ - **It touches only the source.** Runs, `state/`, storage, secrets and the
121
+ memory agents wrote are what a deploy keeps, and a restore is a deploy:
122
+ they are left as they are.
123
+ - **It waits for runs.** While a run is in flight the restore is refused, as
124
+ a deploy is — the run would otherwise finish on agents its first step never
125
+ saw.
126
+ - It needs the editor role or above, and is written to the audit log as
127
+ `workspace.restored`.
128
+
129
+ ```http
130
+ POST /api/workspaces/<ws>/restore
131
+ { "to": "3d", "dryRun": true }
132
+ { "to": "<sha from the dry run>", "confirm": "<ws>", "expectRemoved": [ … ] }
133
+ ```
134
+
135
+ The workspace's History page lists every commit, and its Repository page can
136
+ deploy any one of them — the same operation, by commit instead of by time.
137
+
138
+ ## Asking for a restore from a snapshot
139
+
140
+ Run records, `state/`, storage and the account's records (people, keys,
141
+ balances) come back only from a nightly snapshot. That restore replaces a
142
+ volume and a database every account shares, and the key that opens the
143
+ sealed half of the archive is deliberately not on the server, so it is done
144
+ by the platform team rather than by a button.
145
+
146
+ Settings → Backups → **Request a restore**: what (run records, `state/`,
147
+ storage, or everything), which workspace (or all), the time you want back,
148
+ optionally the snapshot, and a note. The request is recorded, mailed to the
149
+ platform team and written to the audit log (`backup.restore_requested`); the
150
+ page lists your earlier requests. Asking needs the admin role and access to
151
+ every workspace.
152
+
153
+ ```sh
154
+ foldrun backups request --what runs --at "2026-09-28 14:00" --to sales --note "a run record was deleted"
155
+ ```
156
+
157
+ ```http
158
+ POST /api/account/backups/requests
159
+ { "what": "runs", "workspace": "sales", "target": "2026-09-28 14:00", "note": "…" }
160
+ ```
161
+
162
+ Nothing is restored automatically. The team restores the archive somewhere
163
+ else first and copies back only your account's paths, so a restore for you
164
+ does not roll back anyone else.
165
+
166
+ ## On AWS
167
+
168
+ The AWS production design (`foldrun-infra/prod`) backs up differently —
169
+ database point-in-time recovery, nightly EFS snapshots kept 35 days, both
170
+ copied to a second region. It has not been built yet; this page will change
171
+ when it is.
@@ -0,0 +1,362 @@
1
+ # Budgets and billing
2
+
3
+ Money on this platform is a cap at three levels and a meter underneath. The
4
+ caps are yours to set and the thing to set first: an agent with a tool loop
5
+ and no ceiling can spend in an hour what a month was meant to.
6
+
7
+ ## Four caps
8
+
9
+ | level | where | what it does when reached |
10
+ |---|---|---|
11
+ | **an agent, per run** | `budget: 2` in the agent's `agent.md` | the most that agent may spend in one run, across every step it takes in it — its step stops itself mid-turn at what is left, and a later step of its own in the same run is refused before it starts. Its steps running at once share what is left, as a group shares the run's: the copies of a fan-out split the cap, they are not each handed the whole of it |
12
+ | **a run** | `budget: 6` in a flow's frontmatter | a hard cap: between groups the next group does not start; within a group each step gets an equal share of the remainder and stops itself mid-turn when it is spent — the run cannot end over the cap |
13
+ | **a workspace, over time** | `budget: 60` in the workspace's `AGENTS.md`, or Settings | new runs refuse until the window turns |
14
+ | **the account, over time** | `budget: 500` in the account's `AGENTS.md`, or Settings | the same, for everything |
15
+
16
+ **Unset means no limit.** No file, no key, `budget: unlimited` — all the
17
+ same: the platform invents no cap on its own. The two caps over time take
18
+ a period: `budget: 60` alone is per month, as it always was; `budget: 60/day`
19
+ and `budget: 60/week` say so. The window is measured in the account's own
20
+ calendar (`timezone:` in `AGENTS.md`, nearest wins), so a daily cap resets
21
+ at midnight where the customer is, not at midnight UTC. The two per-run
22
+ caps take no period — an agent has no calendar, and writing one there is a
23
+ mistake the editor names rather than a cap it quietly keeps.
24
+
25
+ When an agent's cap and the flow's both apply to one step, the tighter one
26
+ wins, and the step's error says which line set it: `budget: in the flow
27
+ file` or `budget: on the writer agent`.
28
+ | **runs at once** | `concurrency: 3` in the account's `AGENTS.md`, or Settings → Defaults | the fourth run waits its turn in the queue — never refused, only later. Empty means the install's plan number (`FOLDRUN_PLAN_CONCURRENCY`, which applies only where billing is on), or no cap |
29
+ | **the queue lane** | `priority: high` in the account's `AGENTS.md` (`high` \| `normal` \| `low`) | its runs are claimed before other accounts' normal ones when slots are short; a flow's own `priority:` adds to it. The cap above still holds — a lane jumps the queue, it does not hold the fleet |
30
+
31
+ A cap is a literal, never an expression, so it is readable off the file. The
32
+ run cap is hard: a step that reaches its share of the remainder is stopped
33
+ mid-turn, so a runaway costs at most the cap, and the step's own error says
34
+ how much of its ceiling it reached.
35
+
36
+ **What counts as spent is the whole bill**, tokens and the sandbox seconds
37
+ behind them. This matters more than it sounds: a step that calls no model —
38
+ a script looping on a page that never loads, a browser waiting for a selector
39
+ that never appears — spends nothing in tokens and would once have been capped
40
+ by nothing at all, while the sandbox billed by the second. The platform sets
41
+ no clock of its own by design, so the cap is the only backstop there is, and
42
+ it counts everything.
43
+
44
+ The account's cap and a workspace's are checked separately and both apply:
45
+ the workspace one stops that desk, the account one stops everything.
46
+
47
+ **A cap over time warns before it refuses.** At 80% and again at 95% of a
48
+ workspace's `budget:`, the workspace's notification destination is told what
49
+ has been spent and what happens at the cap. Each mark is sent once per
50
+ window — today, this week, this month, whichever the cap is over — and
51
+ resets when the cap does. Without it the way a customer learns about the
52
+ cap is a desk failing to start on a Tuesday.
53
+
54
+ Size the run cap from the run records, at two or three times the median — real
55
+ work never trips it, a runaway is stopped where it went wrong, and the
56
+ failure notification says why. The dashboard's Usage page shows spend by
57
+ workspace and by meter.
58
+
59
+ ## Plans and credits
60
+
61
+ Usage is sold in **credits: one credit is one cent** of what a step consumes
62
+ at the platform's rates. The ledger stays in dollars underneath — every
63
+ statement, budget and refund reconciles to one column — and a credit is how
64
+ the money is shown and bought.
65
+
66
+ | plan | a month | credits a month | an extra credit | runs at once | workspaces | queue lane |
67
+ |---|---|---|---|---|---|---|
68
+ | **Starter** | $29 | 2,900 | 1.00¢ | 1 | 3 | standard |
69
+ | **Creator** | $79 | 8,800 | 0.90¢ | 2 | 10 | standard |
70
+ | **Pro** | $249 | 29,000 | 0.86¢ | 4 | unlimited | fast |
71
+ | **Scale** | $799 | 100,000 | 0.80¢ | 8 | unlimited | fast |
72
+
73
+ **How long a credit lasts.** A plan's credits land when its invoice is paid,
74
+ and what is unused **rolls over** into the next cycle up to **twice** the
75
+ plan's monthly quota — so a balance of plan credits reaches at most three
76
+ times it: this cycle's, plus at most two carried. Anything above the cap
77
+ expires, and the statement line says how much rolled and how much did not.
78
+ The carry is lost on a **cancellation** or a **downgrade**, because the
79
+ rollover is a courtesy of the subscription continuing at its level; moving
80
+ up keeps it. Credits **bought** on top (Balance → Add credit) are separate:
81
+ rollover does not touch them and they last **twelve months** from the day
82
+ they were bought (`FOLDRUN_CREDIT_TTL_MONTHS`; `0` turns expiry off).
83
+ Whoever changes that number should know that prepaid credit sold to
84
+ Australian consumers can attract gift-card rules, which are three years.
85
+
86
+ Which credit a run spends is the order that favours the customer: the
87
+ plan's granted credits first, then bought credit oldest-first — so the
88
+ credit closest to expiring is always the credit being used. Spend draws on the cycle's credits first, then
89
+ on the balance. A new account starts with **300 trial credits**, no card
90
+ asked. Without a plan, credit costs 1¢ each, one run at a time, standard
91
+ lane — a plan is cheaper from the first month you would have spent its fee
92
+ anyway. Changing plan happens at once, prorated by Stripe, and the new
93
+ bundle lands with the invoice; ending one ends it with the cycle.
94
+
95
+ **Seats are unlimited** on every plan: agents work, people supervise, and a
96
+ per-seat fee would tax the approver the safety story depends on.
97
+
98
+ **Bring your own model key** and tokens cost no credits at all — the model
99
+ is billed to your own provider account — so a plan then buys sandbox time
100
+ and the platform, which is most of what a BYOK customer's credits go on.
101
+
102
+ **Going into minus.** Ordinarily an empty balance starts no run. The
103
+ platform can allow an account to run below zero — to a floor, or with none —
104
+ for a customer on invoice terms. It shows on that account's Balance page
105
+ and on the audit; it is a term, never a surprise.
106
+
107
+ **Charges an account does not pay.** The platform can waive any of the three
108
+ meters for one account: model tokens, sandbox time, network. It is set per
109
+ account from the admin console, never as an install-wide rate, because a
110
+ rate turned off globally would quietly stop charging every customer. The
111
+ meters still run and the Usage page still shows everything measured; what
112
+ changes is the bill. Every run still writes a ledger line — at zero when
113
+ everything is waived — naming which legs were waived, so a statement that
114
+ totals nothing has a reason printed on it rather than looking like a fault.
115
+ It is for the accounts the platform runs itself, and for pilots and
116
+ partners whose compute is absorbed by arrangement.
117
+
118
+ ## What a credit buys
119
+
120
+ Three meters, at the platform's rates. Every charge is recorded in **two
121
+ halves** — what the models cost and what the platform under them cost — and
122
+ the Usage page, the statement and its CSV all show them apart. They are
123
+ different questions: a customer on their own model key pays no model half
124
+ at all, and one whose bill moved needs to know which half moved. The halves
125
+ always sum to the charge; where a minimum-run fee applies it lands on the
126
+ platform half, never on a model number a customer can check against their
127
+ provider's own invoice.
128
+
129
+ | meter | what it counts |
130
+ |---|---|
131
+ | **tokens** | model usage at cost, times a margin. **Bring your own key and the margin does not apply** — a BYOK step is billed nothing for tokens |
132
+ | **compute** | sandbox seconds: what a step reserved (cores and memory, while it ran) and what it actually burned |
133
+ | **storage and network** | GB-months in the file store, accrued daily; GB the sandbox moved, both directions |
134
+
135
+ There is deliberately **no per-step fee**. Steps are sandbox-seconds, so a
136
+ step fee would charge compute twice and punish a well-factored flow for
137
+ having more, smaller steps. The Usage page shows every table in credits at
138
+ today's rates beside the meters, so a customer can read what a run will
139
+ cost from what the last one consumed.
140
+
141
+ ## What each desk costs
142
+
143
+ The Usage page's per-workspace tables are rolled up from the run records,
144
+ which is right for step counts and pod seconds and wrong for money over
145
+ time: run records are pruned, so a desk's cost history disappears exactly
146
+ when it becomes interesting, and a record holds what a run consumed rather
147
+ than what it was charged.
148
+
149
+ So every charge also carries the flow it was, and that charge split across
150
+ the agents that did the work — in proportion to what each step actually
151
+ consumed, normalised so the shares add up to the charge. The fixed parts of
152
+ a bill are spread the same way, which is the ordinary treatment and the only
153
+ one where a column of shares equals the invoice.
154
+
155
+ The Usage page shows both, by week, with the change between the last two
156
+ **complete** periods so a partial week never reads as a collapse. Charges
157
+ written before attribution existed appear as a remainder rather than being
158
+ quietly dropped: a breakdown that does not add up is worse than one that
159
+ admits what it cannot place. `GET /api/usage/history?days=` is the same
160
+ thing as JSON.
161
+
162
+ ## The Billing section
163
+
164
+ Money is one subject, and the dashboard treats it as one section under the
165
+ avatar, with eight tabs:
166
+
167
+ | tab | what it is |
168
+ |---|---|
169
+ | **Balance** | the number, large, with how long it lasts in words, thirty days of spend as a sparkline, what is held against runs in flight, and Add credit with the amount that covers a month already chosen |
170
+ | **Plan** | the plan you are on, this cycle's credits and when they reset, and the plans you could move to |
171
+ | **Usage** | what was consumed, from the run records, and what each flow and agent cost by week from the ledger |
172
+ | **Statement** | the ledger as a statement: grouped by day, human labels, month-to-date, filters, and a CSV for the month |
173
+ | **Invoices** | a tax invoice for every card payment — the plan's fee and each top-up — hosted by Stripe, and a statement for every month |
174
+ | **Payment** | the saved card and the auto top-up rule, with defaults derived from the burn |
175
+ | **Details** | who the invoices are made out to: legal name, ABN, address and the invoice email |
176
+ | **Spend limits** | the account's cap and each workspace's — over a day, a week or a month — as bars with the warning marks drawn where they fire, editable in place |
177
+
178
+ The balance also sits in the header of every page, tinted by runway, and a
179
+ banner appears across the dashboard when the balance is under a week's cover
180
+ or empty, carrying the one button that fixes it. A refused run says why at
181
+ the button that was clicked, with a link to add credit when money was the
182
+ reason. The old `/dashboard/wallet` address redirects here.
183
+
184
+ Adding credit is the owner's to do. Anyone else sees why the button is not
185
+ there, and who to ask, rather than a button that fails.
186
+
187
+ **Tax invoices.** Every payment is invoiced to the account's one Stripe
188
+ customer, made on the first payment and carrying what Details holds — the
189
+ legal name, the invoice email, the address, and the ABN as the customer's
190
+ tax id (checked against the ABN rule before it is kept). A top-up's
191
+ checkout asks Stripe for a numbered invoice, not just a receipt, so a
192
+ business can claim the GST on credits as well as on the plan. With
193
+ `STRIPE_TAX=1` (Stripe Tax enabled on the Stripe account), checkout works
194
+ the tax out from the address, collects the address and a tax id if they
195
+ are missing, and writes them back to the customer. Saving Details updates
196
+ the customer at Stripe at once; without Stripe, or when Stripe refuses,
197
+ they are kept and sent with the next save or payment.
198
+
199
+ **Manage in Stripe.** The Payment, Plan, Invoices and Details tabs carry a
200
+ button into Stripe's Billing Portal — the card, every invoice as a PDF, the
201
+ plan — for the owner. Its look and what it allows (cancelling, switching
202
+ plan) are set once at Stripe, under Settings → Billing → Customer portal.
203
+
204
+ **When a plan payment fails.** Stripe retries the card on its own
205
+ schedule. On the first failure of an invoice the owner is emailed once —
206
+ the amount, Stripe's page to pay that invoice, and the way to update the
207
+ card — and a grace period starts (`FOLDRUN_DUNNING_GRACE_DAYS`, default 7).
208
+ Runs go on meanwhile, and an amber banner across the dashboard says until
209
+ when. If the invoice is still unpaid when the grace ends, the hourly sweep
210
+ suspends the account for new runs with the reason **payment overdue**, and
211
+ one more email says so; the banner turns red. Runs in flight finish, people
212
+ can still sign in, nothing is deleted. The moment `invoice.paid` arrives
213
+ for it — or `invoice.voided`, `invoice.marked_uncollectible`, or Stripe
214
+ cancelling the subscription — the suspension is lifted, and a payment
215
+ gets a thank-you email. Every step is once per invoice, however often
216
+ Stripe retries or redelivers; a second invoice failing during the grace
217
+ does not restart it. Dunning lifts only a suspension it placed: an account
218
+ the super admin suspended for another reason stays suspended. The super
219
+ admin sees where it stands on the customer's overview.
220
+
221
+ **Your data, and leaving.** Settings → Your data gives the owner one JSON
222
+ file with the account's workspaces, members, an index of every run and the
223
+ whole ledger (`GET /api/account/export`). The same place holds **Close
224
+ account**: type the account's name and it is suspended at once — no new
225
+ runs, schedules stop firing — while the platform team settles the last
226
+ invoice and any refund. Nothing is deleted by the request itself; people can
227
+ still sign in and take the copy.
228
+
229
+ ## The admin console's money levers
230
+
231
+ Each customer (account) in the super admin console has a Billing tab with
232
+ every lever that changes what the customer pays, each behind a confirmation
233
+ that names the amount and the account: **credit or debit** with a reason on
234
+ the ledger (the console sends an idempotency key, so a double click is one
235
+ line), **extend the trial** (more trial credits as a grant, with whatever
236
+ trial credit is left carried into it; not for an account on a plan), the
237
+ **plan** (grant a cycle by hand, end it, and the lane and limits that ride
238
+ with it), **going into minus**, and **charges this account does not pay**.
239
+ The overview beside it holds the contact on record (name, email, phone,
240
+ company, ABN), notes, suspension, and the customer's Stripe customer and
241
+ subscription with links into Stripe's own dashboard — test or live, read
242
+ from the install's key.
243
+
244
+ Revenue has a CSV per month for the accountant (`GET /api/admin/export`):
245
+ one row per customer and a total, charges by leg, fees, top-ups, grants,
246
+ expiries, refunds, disputes, adjustments and net, with Stripe's own figures
247
+ for the month beside them when Stripe is connected.
248
+
249
+ ## The wallet
250
+
251
+ Usage is settled from a credit balance. Add credit by card, once or with
252
+ auto top-up; set a low-balance email so the empty wallet is never a
253
+ surprise. With billing on, an empty balance refuses *new* runs — runs in
254
+ flight finish. Every run is charged exactly once; the ledger is a database
255
+ table with that as a constraint, not a promise.
256
+
257
+ ## How the money is kept straight
258
+
259
+ The bookkeeping rules behind the numbers, for whoever has to answer "why is
260
+ my balance this".
261
+
262
+ **One grant per plan cycle.** A cycle is the subscription, the plan and the
263
+ end of the period it pays for, and that is the grant's key. Stripe tells us
264
+ about a paid cycle several ways — the checkout returning, the checkout
265
+ webhook, `invoice.paid`, `customer.subscription.updated` — and in no fixed
266
+ order; whichever arrives first grants and the rest find it done. A plan
267
+ change inside a period is a new cycle on the new plan (the prorated invoice
268
+ and the updated event are the same cycle, one grant); a renewal is a new
269
+ period. A change inside a period grants the new plan's bundle **pro-rated
270
+ by the share of the period left**, the way Stripe prorates the charge; a
271
+ renewal grants it whole. Going back to a plan already granted this period
272
+ switches the plan and its gates back but grants nothing again. Downgrades
273
+ still lose the carry.
274
+
275
+ **The low-balance guard runs every hour**, and right after a run whose
276
+ charge takes the balance across the warning line. Per crossing of the line
277
+ it makes at most **one** auto top-up attempt and sends at most **one**
278
+ email; a declined card is not retried every hour — the email goes instead —
279
+ and both re-arm once the balance is back above the line.
280
+
281
+ **Tax.** A top-up credits the price before tax: with Stripe Tax on, the
282
+ Checkout price is tax-inclusive, so a $50 top-up costs $50 and credits $50.
283
+ An auto top-up is a bare charge with no Stripe Tax and no tax invoice.
284
+
285
+ **Refunds.** A refunded top-up takes off the credits it added — the charge's
286
+ own amount caps it, scaled to the pre-tax share when the charge carried tax — and the balance is **not** floored at zero: credit
287
+ refunded after it was spent leaves the account in the red, and admission
288
+ refuses the next run until it is topped up. A refunded plan fee does not
289
+ touch the wallet: the credits it bought were a grant, and they end with the
290
+ cycle.
291
+
292
+ **Chargebacks.** The disputed amount comes off when the dispute opens (the
293
+ card network already has the money). The account is found through the
294
+ disputed charge — its metadata, its payment's, then the saved customer. Won
295
+ (or an inquiry closed without a chargeback): the same amount goes back on,
296
+ once. Lost: it stays off. As with refunds, a dispute on a plan fee does not
297
+ touch the wallet; it is logged and left to the super admin.
298
+
299
+ **Every line says why it exists.** Each ledger line carries a reason —
300
+ usage, pod, fee, checkout, auto-topup, manual, operator, refund, dispute,
301
+ dispute-won, plan, trial, grant-expired, credits-expired — and lines written
302
+ before the field have theirs derived from their key and note. Statements
303
+ label lines by it. **Burn**, the 30-day spend and month-to-date count usage
304
+ and the daily fees only — not expiries, refunds, chargebacks or an
305
+ operator's debit. The admin console's **charged** figure is usage (runs and
306
+ account pod sessions) in the customer list and the money report alike;
307
+ **paid in** is top-ups paid plus plan fees from the invoices Stripe said
308
+ were paid; **held in wallets** is credit bought and unspent — granted
309
+ credit is shown apart, because nobody paid for it by the dollar.
310
+
311
+ **Hand-placed plans renew.** A plan the super admin grants runs a month and
312
+ is renewed by the hourly sweep at its period's end, with the same grant and
313
+ rollover rules. Granting a plan to an account Stripe is billing is refused
314
+ (end it first, or change it at Stripe); ending a Stripe-billed plan cancels
315
+ the subscription at Stripe first, at once, and changes nothing if Stripe
316
+ refuses.
317
+
318
+ **An operator's credit or debit is written once** per idempotency key the
319
+ console sends, and audited once.
320
+
321
+ **Stripe.** Every call pins its API version (`2025-03-31.basil`;
322
+ `STRIPE_API_VERSION` overrides) — create the webhook endpoint on the same
323
+ version — and charges in `FOLDRUN_CURRENCY` (default `usd`), which is also
324
+ the unit of every balance. Set the currency before any money is on the
325
+ books; Stripe will not mix currencies on one customer. The webhook endpoint
326
+ is `<public url>/api/billing/stripe`, sending exactly:
327
+
328
+ | event | what it does |
329
+ |---|---|
330
+ | `checkout.session.completed` | a top-up credited, or a plan's first cycle granted |
331
+ | `invoice.paid` | a plan cycle granted: signup, renewal, or a change; an overdue invoice settled |
332
+ | `invoice.payment_failed` | the plan goes past due; dunning starts |
333
+ | `invoice.voided` | an overdue invoice cancelled; dunning ends |
334
+ | `invoice.marked_uncollectible` | an overdue invoice written off; dunning ends |
335
+ | `customer.subscription.updated` | a plan swap, or a status change |
336
+ | `customer.subscription.deleted` | the plan it paid for ends; its dunning ends |
337
+ | `payment_intent.succeeded` | an auto top-up credited |
338
+ | `payment_intent.payment_failed` | an auto top-up refused; the warning goes |
339
+ | `charge.refunded` | a refunded top-up's credits come off |
340
+ | `charge.dispute.created` | a chargeback's amount comes off |
341
+ | `charge.dispute.closed` | won: it goes back on; lost: it stays off |
342
+
343
+ Every delivery is logged — the event, the account, what was done or the
344
+ error, and a hash of the payload (never the payload, which carries names
345
+ and addresses). The super admin lists the log at `GET
346
+ /api/admin/stripe-events` and replays one event with `POST
347
+ /api/admin/stripe-events/<id>/replay`, which re-fetches it from Stripe and
348
+ runs it through the same handler; every write it makes is keyed once, so a
349
+ replay of an event that was handled changes nothing.
350
+
351
+ ## Plan gates
352
+
353
+ Two more limits come with the plan rather than the wallet: how many
354
+ workspaces an account may create (beyond it, creation refuses with `402`),
355
+ and how many runs may execute in parallel (beyond it, runs **wait** — they
356
+ are never refused, because a queued run is work that will happen and a
357
+ refused one is work that will not).
358
+
359
+ ## On your machine
360
+
361
+ None of this meters. `budget:` still caps a run and every step's cost is on
362
+ its record — the model bill is yours, paid to your provider directly.