- commit
- 2cce7940ab78663ff3a7f28c12ca91909de1a303
- parent
- 379866f9ad888214a3aa1568f9face9c9347230d
- author
- Dami <dami.etoile@gmail.com>
| M | README.md | 2 | +- |
| M | docs/ | 504 | +++++++++--------------------- |
@@ -55,7 +55,7 @@ Site hosting and mirroring are on by default; Marmot and GRASP are off until ena
5555 - [Hosting bind.ws](docs/11-hosting-bindws.md): run your own on your own domain, and watch it through logs, traces and metrics.5656 - [Hosting without Cloudflare](docs/16-hosting-without-cloudflare.md): the same Worker on celld, with your own bucket and proxy, and what "supported" promises.5757 - [Develop and extend](docs/12-develop-extend.md): layout, tests, adding methods and NIPs, the console, the templates.58-- [Costs and margins](docs/15-costs-and-margins.md): tenant meters, host accounting gaps, margin scenarios and the proposed maintenance budget contract.58+- [Costs and margins](docs/15-costs-and-margins.md): application budgets, retained storage, inventory interpretation and deployment margins.5959 6060 ### Draft NIPs6161 @@ -5,371 +5,163 @@ audience: developer
55 66 # Costs and margins77 8-What a relay costs the host, what fuel charges for it, and how to keep the two apart as the bitcoin price moves. The numbers come from one script:8+How applications budget a tenant's relay usage, interpret fuel meters and9+control storage growth. Fuel prices the relay's measured resources; hosting10+expenses determine the service's margins. The two serve different purposes.11+[Understanding fuel](02-understanding-fuel.md) covers allowances and top-ups12+for relay owners.913 10-```11-npm run margin # today's price12-npm run margin -- --btc 60000 # a price you pick13-npm run margin -- --relays 5000 # the deployment at that size14-```15-16-It prints markdown and exits with code 1 when any priced line is under the target margin. A GitHub Actions workflow runs it every Monday, puts the tables in the run summary and opens or refreshes one issue when a line is under target.17-18-## The model19-20-Fuel prices four resource lines: SQLite event storage, R2 media storage,21-Durable Object duration and rows written. Each fuel price is a sats figure in22-`wrangler.jsonc`. Each Cloudflare price is a dollar figure on their pricing23-pages, copied into the script with the date it was checked. The bitcoin price24-joins the two.25-26-The four lines are the tenant-facing budget, not a complete provider invoice.27-The relay records stored event bytes, stored media bytes, its awake-time28-counter and rows written by its SQLite work. GRASP objects share the media29-storage allowance with Blossom files and dumps; NIP-5A mirroring does the same.30-Marmot events share event storage and ordinary row accounting. Site proxy and31-Git request bytes are visible traffic with no fuel price, and tenant usage does32-not gain a separate feature surcharge.33-34-Some provider costs remain outside the four tenant-priced lines. R2 operations, KV reads and35-writes, and platform requests can be billed by operation even when the relay36-only exposes bytes. The row meter drains cursors recorded by `Store.x`; direct SQL bookkeeping37-in GRASP, site queues and other modules bypasses it. The awake-time meter38-uses entrypoint gaps capped at ten seconds, so a long Git or remote-fetch39-operation is not measured as an exact request duration.40-Those costs belong in the host's deployment review and margin target; they do41-not silently change the four meters shown to a relay owner.42-43-For a line with cost `c` dollars per unit and price `p` sats per unit at `B` dollars per bitcoin, revenue per unit is `p / 1e8 * B` and margin is `1 - c / revenue`. The script defaults to a 33% margin on revenue, which prices a line at about 1.493x cost. The floor is 20%. Under the target the weekly run flags it; under the floor, review pricing.44-45-Because prices are in sats and costs in dollars, margins move with the exchange rate on their own. A rising bitcoin widens them. A falling one narrows them, and the script prints, for each line, the bitcoin price under which the target no longer holds and the price under which the floor breaks.14+## Budget from the relay's meters4615 47-## The lines, checked Sept. 3, 202616+`GET /fuel` returns current usage, allowances, rates and balance without17+authentication. The Health tab shows the same information. Applications read18+these values from the relay instead of hard-coding the public service's19+prices, since another deployment can set different rates and allowances.4820 49-At $81,500 per bitcoin:50-51-| Line | Cloudflare | Fuel | Margin | Target holds above |52-|---|---|---|---|---|53-| Events stored | $0.20 per GB-month | 400 sats | 39% | $74,600 |54-| Files stored | $0.015 per GB-month | 30 sats | 39% | $74,600 |55-| Time awake | $0.0058 per hour | 11 sats | 36% | $78,200 |56-| Rows written | $1.00 per million | 2,000 sats | 39% | $74,600 |57-58-Time awake is an hour of a Durable Object held in memory at 128 MB, which Cloudflare bills as 460.8 GB-seconds. Hibernating objects incur no duration charge, but retained storage and alarm work can still cost the host.59-60-## The free tier61-62-At the dated rates above, a relay that spends its whole allowance costs the host about $0.86 a month. Time awake is most of it. Storage is nearly free at these sizes. The rows allowance was a million until Sept. 2026, which made it $1.00 of a $1.61 tier; it came down to 250,000, about 15 times what a personal relay writes, because rows are the one line Cloudflare charges real money per unit.63-64-| Allowance | Amount | Cost |21+| Fuel line | Usage and allowance fields | What the meter includes |6522 |---|---|---|66-| Events stored | 100 MB | $0.02 |67-| Files stored | 1 GB | $0.015 |68-| Time awake | 100 hours | $0.58 |69-| Rows written | 250,000 | $0.25 |70-71-Few relays get near the allowance. At the dated rates above, a typical personal relay, 20 MB of events, 100 MB of files, eight hours awake and 100,000 rows a month, costs about 15 cents once the plan's included quotas are used up, and those quotas cover the first few dozen relays outright.72-73-## The deployment74-75-The script projects a fleet from two illustrative shapes, a typical relay and a heavy one that passes its allowances and pays fuel, at a chosen share of heavy relays. These are assumptions, not measured customer workloads or a validated provider bill. The fleet projection includes platform requests, rows read and R2 operations as host costs even though these have no separate tenant price. Worker CPU, logs, traces, Analytics Engine, site-index KV operations, custom-hostname fees, payment fees and support still need deployment measurements or separate budgets.76-77-The free tier is a cost the paid relays and the host share. Unpriced operations consume part of the margin on paid usage. A target on four priced lines alone does not establish a fleet margin after free users and shared costs.78-79-The bounded GRASP implementation adds a relay-wide ceiling of 320 MiB for80-retained Git objects, with 4 MiB packs, 16 MiB packed history and bounded81-transaction, ref and object counts. This is a safety limit and shares the82-existing media allowance; it is not a new free tier. Retained bytes can remain83-billable after expired metadata is hidden when immutable Git history or a84-cleanup transaction still keeps them.85-86-The current [R2 pricing](https://developers.cloudflare.com/r2/pricing/),87-[Durable Object pricing](https://developers.cloudflare.com/durable-objects/platform/pricing/)88-and [KV pricing](https://developers.cloudflare.com/kv/platform/pricing/)89-distinguish storage, execution and operation charges. The historical fleet90-shapes below the script's pricing tables are not measured site or Git workloads.91-Use deployment telemetry before treating their projected margin as coverage92-for these features.93-94-## Weekly95-96-The workflow is `.github/workflows/margin.yml`. Read the summary. When a line is flagged:97-98-1. Run the script locally to see the suggested prices for the target at today's rate.99-2. Change the `SATS_PER_*` values in `wrangler.jsonc`. The comment above them records the rate they were set at; update it.100-3. Deploy. Fuel meters are unchanged; only the price of what is over the allowance moves, from the next charge.101-102-Prices go up when bitcoin falls. When it rises, do nothing until a line has sat above twice the target for a month, then bring it down. Relay owners see prices on the front page and on their Fuel section, so every change is visible the moment it deploys. The table above was checked on Sept. 3, 2026; rerun `npm run margin` and verify current provider pricing before using it for a new forecast.103-104-## Accounting coverage, checked Sept. 4, 2026105-106-The following inventory describes the host with the current repository107-inventory contract and the owner-only inventory admission path. The pinned ntig 0.3.0108-package supports both formats; installing it does not migrate roots.109-Observing an expense does not authorize a new customer charge. `/fuel` and the110-Health tab expose aggregate tenant meters, not every operation the host pays111-for.112-113-| Dimension | Existing source and persistence | Coverage limit |23+| Events stored | `eventBytes`, `freeEventBytes` | Allocated SQLite storage, including relay metadata as well as events |24+| Files stored | `mediaBytes`, `freeMediaBytes` | Uploaded files, dumps, imports, mirrored site files and reserved Git storage |25+| Time awake | `activeMs`, `freeActiveMs` | An estimate from relay entrypoints, including client requests, jobs and alarms |26+| Rows written | `rowsWritten`, `freeRowsWritten` | Writes counted by the relay's event store |27+28+`rates` gives storage prices in sats per GB-month, awake time in sats per29+hour and writes in sats per million rows. Balance and credited or charged30+amounts use millisatoshis: `balanceMsats / 1000` is the balance in sats.31+Storage above its allowance is charged daily, pro rata. Awake time and rows32+use the usage accumulated in `month` against their monthly allowances.33+34+`bytesIn`, `bytesOut` and `rowsRead` are diagnostics with no fuel price.35+A request can still increase awake time or stored data even when its traffic36+is unpriced. Sites, Marmot and Git share the existing allowances; enabling a37+feature creates neither a separate allowance nor a request surcharge.38+39+The meters describe application accounting, not a complete provider bill.40+Awake time is estimated from entrypoint gaps capped at ten seconds, rather41+than timing each job to completion. Direct SQL bookkeeping outside the event42+store is not included in the row meter. Usage is flushed in batches, so it is43+not a durable record of every request. Applications use the reported balance44+and `outOfFuel` for admission status rather than reconstructing charges from45+traffic, event counts or wall-clock timings.46+47+## How features consume the budget48+49+| Workload | Resource use | Application choice |11450 |---|---|---|115-| Usage and credits | [Fuel](../src/fuel.ts), monthly SQLite usage and unique receipt IDs | Counters become durable only after a flush; receipts prevent duplicate credit |116-| SQLite storage | `Relay.eventBytes`, the database's allocated size | Includes tables beyond events; daily charging uses the current size, not a continuous storage integral |117-| R2 storage | `Relay.mediaBytes`, blobs + dumps + imports + `grasp_objects` | Shared allowance; retained history and conservative reservations can outlive visible refs |118-| Awake time | `Relay.touch`, in-memory ten-second entrypoint gaps | No handler completion timing; long async work, idle gaps and restart estimates differ from provider duration |119-| SQL rows | [Store](../src/store.ts), cursors from `Store.x`, then `Relay.tally` | Direct SQL in settings, GRASP, sites, media and other modules bypasses the meter; DO key-value and alarm work is not covered |120-| Traffic | `Relay.meterBytes` and socket counters, then monthly usage | No fuel price; socket string lengths and attempted/produced HTTP bytes are not exact delivered wire bytes |121-| Requests | [Worker routing](../src/index.ts), HTTP, RPC, sockets and alarms | No durable per-tenant request ledger; application REQ counters are not platform request counts |122-| R2 operations | [Git object wrapper](../src/grasp.ts), [Blossom](../src/blossom.ts), [site mirrors](../src/site-mirror.ts) and dumps | No durable per-tenant Class A/B ledger; observer callbacks are best-effort evidence |123-| Jobs and alarms | [Jobs](../src/jobs.ts) and `Relay.alarm` | Required cleanup, mirrors and retries consume host work; no optional maintenance budget exists |124-| KV and external services | [Host routing](../src/edge.ts), [site index](../src/sites.ts), notifications and provider fetches | No tenant operation meter for reads, writes, misses, retries or external fees |125-| Observability | `Relay.flushUsage`, console deltas and optional Analytics Engine | Logs, traces and analytics are expense and diagnosis sources, not the durable fuel ledger |126-127-[Relay](../src/relay.ts) batches usage in memory. A restart can lose an128-unflushed batch. `flushUsage` resets counters before `Fuel.record`; a failed129-record can therefore lose that batch too. Its final `Store.drain` discards130-pending cursors, so a direct flush without `tally` can omit rows. Usage is131-assigned to the month of the flush rather than split across the work's month132-boundary. These are accounting gaps, not grounds for reconstructing charges133-against old tenant balances.134-135-Git reserves the larger retained object size in SQLite before PUT. Success136-reconciles the size; a definite CAS loss uses HEAD to reconcile it. An137-exception leaves the reservation because the write may have succeeded. No138-automatic reconciliation queue or orphan collector exists. Counting PUT139-bytes again as stored bytes would double-charge existing media accounting.140-Future temporary packs also need reservations while both copies exist.141-142-For an explicitly migrated format-2 repository, ref advertisements read two143-R2 objects, the root and manifest. This saves legacy history and pack reads144-on that path; full loads and Git transfers still read all stored packs.145-Each new commit also retains a manifest and receipt-index path nodes beside146-its record and optional pack. Ref-only workloads can pass 128 transactions,147-but their metadata and reservation rows continue to grow. All of these keys148-use the same pre-PUT storage reservation. CAS-loser objects remain accounted149-for, and an ambiguous PUT can leave a reservation larger than physical150-storage. Neither receipt lookup nor checkpointing reclaims old bytes.151-Operation counts are host costs, not a new tenant charge; lower read costs152-alone do not establish a margin after metadata writes and retained storage.153-154-Git HTTP requests reuse bounded immutable payload in a session that ends with155-the request. Repeated validation and promotion can reuse those bytes, while156-mutable roots and all writes still reach R2 and its accounting adapter. New157-requests start fresh, oversized objects bypass the cache, and Git validation158-still runs. Lower R2 read counts do not imply lower CPU cost or a complete159-request-memory bound. Inventory uses its own 16 MiB total-read, 4 MiB object,160-10,000-key, 100-page, 10,000-node and 10,000-receipt budgets,161-with a 1,024-byte key and 8,192-byte cursor limit; it returns no partial report162-when a budget or root-stability check fails.163-164-The isolated checkpoint fixture separates physical storage from the objects165-referenced by the current format. Both workloads use one 214-byte Git pack,166-260 transactions and an explicit checkpoint after transaction 128. One keeps167-updating the same ref to the same commit; the other retains 259 new tags.168-These local fixtures were first measured with ntig 0.2.1. The ntig 0.3.0169-runtime inventory reproduces their physical, current-format and reservation170-totals exactly. They are not production usage or a per-repository minimum.171-172-| Retained Git payload | One fixed ref | Growing tags |173-|---|---:|---:|174-| Pack | 214 bytes | 214 bytes |175-| Current root and manifest | 430 bytes | 16,888 bytes |176-| All 260 retry records and current receipt index | 136,165 bytes | 126,982 bytes |177-| Current-format total | 136,809 bytes | 144,084 bytes |178-| Superseded manifests | 42,900 bytes | 1,671,120 bytes |179-| Superseded receipt-index nodes | 332,722 bytes | 332,722 bytes |180-| Physical objects and reserved bytes | 512,431 bytes | 2,147,926 bytes |181-182-Each runtime scan uses 349 R2 GETs and one LIST. It reads 137,239 bytes for183-fixed refs and 160,972 bytes for growing tags. At the R2 unit rates below,184-those requests represent about $0.00013 per scan before pooled allowances185-and billing-unit rounding, excluding execution and SQL costs. The scan does186-not reclaim bytes or add a tenant fuel rate; its operation counts are187-diagnostics rather than a billing ledger.188-189-The ledger equals the paginated R2 inventory for every key and size in these190-quiet fixtures: there is no reservation slack. Each untouched fixture191-has no Git R2 keys; its first commit uses 631 bytes in three keys. SQLite192-allocation, provider operations and execution are separate quantities.193-The larger growing-tag total comes mainly from retained copies of earlier194-ref maps. Its current manifest is only 16,783 bytes. Fixed-ref transaction195-records are larger because their updates include an old object ID.196-197-All historical retry records remain required by the current retention198-contract. Current-format membership includes every pack in the manifest;199-it does not establish that every object inside those packs is reachable200-from today's refs. Superseded metadata is outside the latest root's201-dependency set, but older readers and in-flight publishers can still need202-it. This inventory supplies no online deletion authority. No collector or203-storage saving is deployed, and these bytes still count toward tenant file204-usage. Coordinated metadata collection is the larger target in this fixture;205-changing receipt retention would change retry semantics.206-207-`npm test -- test/object/grasp-checkpoint.test.ts --reporter=verbose --disableConsoleIntercept`208-reproduces the inventory, current-format traversal, all historical receipt209-lookups and unchanged root checks. The measurements apply to healthy,210-quiescent fixtures; ambiguous writes can still leave reservation slack.211-212-`repository-access.ts` carries one live owner token across awaits. Git,213-non-GET controls, alarms, event ingestion and direct lease, adoption and214-teardown paths use the same admission seam, and nested work owned by that215-token proceeds. Async handlers can still interleave across independent216-instances or R2 clients. The token is admission control, not persisted crash217-recovery or authority to delete data. Repository CAS and persisted authority218-remain necessary.219-220-The coordination tests pause actual Git HTTP and alarm requests at an R2221-root read. Competing Git requests return 429 without reading R2. The active222-`repositoryAccess` token admits nested work owned by that operation and223-refuses unrelated controls, event writes and alarms until it releases. This224-is live-instance admission evidence, not proof of crash recovery or exclusive225-maintenance across clients. Both successful and failed R2 reads release the226-token. Consuming the completed HTTP response performs no more R2 reads in this227-fixture.228-229-| Entry point | Existing coordination | Limit before collection |230-|---|---|---|231-| Git advertisement, fetch and push | `repositoryAccess` owns the live token across the Git operation | Live-instance admission only; no persisted reader lease |232-| Accepted state and promotion | Run inside the Git token or the alarm token; promotion rechecks candidates after reads | An immutable read session caches bytes but acquires no cross-instance lock |233-| Non-GET HTTP management | The control token refuses an active Git operation | Nested work owned by the active token proceeds |234-| Signed events, imports and pulls | The event token refuses an unrelated Git or control operation | Independent instances can still interleave |235-| Alarm, PR expiry and pending promotion | The alarm defers and reschedules when another token is active, otherwise it owns the alarm token | Alarm work is not crash recovery |236-| Whole-relay deletion | Management or lease-expiry alarm enters the teardown token | Teardown deletes the whole slug prefix; it is not a metadata collector |237-| Direct repository helper calls | Production callers enter through the repository access seam | Helpers do not become durable locks or deletion authority |238-239-The current HTTP handler constructs its response bytes before the read240-session closes. A future streaming path would need to preserve coordination241-until its last storage read. The owner token does not coordinate an242-independent R2 client, another object ID sharing a prefix, or recovery after243-the owning instance is replaced. CAS protects root publication, but does not244-prevent deletion of an object that a reader or unpublished writer still245-needs. Removing a repository announcement also leaves its WAL and quota246-rows behind; visibility is not a storage-liveness test. The repository helper247-itself has no host admission; production callers enter through248-`repositoryAccess`.249-250-The owner-only `gitstorage` management method makes this distinction visible251-without turning it into a cleanup operation. It walks one repository under252-bounded budgets and reports physical, live, unreferenced and unknown objects253-by class, SQL reservations and the reservation-to-listed-byte difference. A254-complete report is required, and a changed root, budget exhaustion or backend255-failure returns no partial result. The 60-second per-instance cooldown limits256-repeated scans. Unreferenced bytes remain tenant storage until a future,257-separately authorized collection protocol exists.258-259-Before metadata collection, a complete inventory needs explicit object,260-byte and operation budgets and must refuse an incomplete mark. The host261-also needs a proven ownership and recovery protocol covering active262-readers, unpublished writes, administrative deletion and maintenance.263-Budget exhaustion, missing or corrupt dependencies and changed roots must264-prevent a deletion decision. Stable before/after roots alone are insufficient:265-a reader may hold an older root, or a publisher may have uploaded objects266-without publishing its new root yet. This remains a collection blocker; the267-tests exercise the runtime admission seam but introduce no collector.268-269-`npm test -- test/object/grasp-coordination.test.ts` reproduces the paused270-HTTP/alarm checks. The checkpoint inventory above remains an isolated,271-quiescent fixture rather than an online maintenance API.272-273-Provider request accounting separates HTTP/RPC/alarm invocations from274-incoming WebSocket messages, which have a 20:1 billing ratio. Socket frames275-do not become Worker requests. DO duration counts overlapping work once per276-object; idle objects eligible for hibernation have no duration charge.277-DO key-value operations and alarm writes also consume SQLite rows. Included278-allocations and billing-unit rounding apply at account level.279-See [Durable Object pricing](https://developers.cloudflare.com/durable-objects/platform/pricing/).51+| Events and Marmot messages | Event storage and database writes; encrypted envelopes still occupy space | Restrict admitted writers and set retention for kinds that do not need permanent history |52+| Blossom files, dumps and imports | File storage alongside database metadata | Bound uploads and remove stored copies that are no longer needed |53+| Sites | Manifests occupy event storage; mirrored files occupy file storage; remote fetches and mirrors wake the relay | Reuse unchanged files and choose whether the relay mirrors remote content |54+| Git | Packs, transaction receipts and retained metadata occupy file storage; clone, fetch and push wake the relay | Budget for repository history and ref updates, not just the checked-out files |55+| Pulls, backfills and scheduled dumps | Jobs wake the relay and can add events, rows and files without a connected client | Use once-only jobs for one-time transfers and remove recurring jobs that no longer serve the application |56+57+[Relay templates](../relay-templates/README.md) provide starting policies.58+[Data and names](04-data-and-names.md) describes retention, dumps and jobs.59+Feature limits still apply below the fuel allowance: a positive balance does60+not increase a maximum upload size, Git transaction limit or repository cap.61+The limits and admission rules are in62+[GRASP-01 Git hosting](22-grasp-01-git-hosting.md) and63+[HTTP reference](14-http-reference.md).64+65+## Account for retained Git storage66+67+Git storage has no fixed reservation per repository. It grows with packs,68+transaction receipts and metadata, including ref-only updates. Removing a69+branch, hiding an announcement or expiring a PR does not remove its immutable70+history. Retained objects continue to count toward file usage and the relay's71+Git storage cap. There is no automatic Git garbage collector.72+73+A reservation records bytes before an object write. A confirmed result74+reconciles the size; an uncertain result keeps the reservation because the75+write may have succeeded. The reservation total can therefore exceed the76+physical objects currently listed. It is not a second charge on top of those77+objects. Repository format upgrades also retain history; they are not a way78+to reclaim storage.79+80+The relay owner can inspect one hosted repository with the `gitstorage`81+management method. It takes the repository owner's hex public key and the82+repository identifier. The repository owner can differ from the relay owner.83+[Scripts and agents](13-scripts-and-agents.md) describes signing the call;84+[HTTP reference](14-http-reference.md) defines its responses.85+86+| Report field | Interpretation |87+|---|---|88+| `inventory.listed` | Physical objects found in the repository's storage namespace |89+| `inventory.live` | Objects referenced by the captured storage root, including retained receipts and packs; not a reachability check of every Git object inside a pack |90+| `inventory.unreferenced` | Recognized objects outside that root's dependency set; older readers or unpublished writes can still need them |91+| `inventory.unknown` | Listed keys the inventory cannot classify |92+| `reservations` | The repository's quota reservations, included in file usage |93+94+`reservationMinusListedBytes` compares the reservation and physical byte95+totals. A difference is a diagnostic, not proof that particular reservations96+can be removed. Neither `live` nor `unreferenced` grants deletion authority.97+The report changes no stored objects, reservations or fuel rates.98+99+Inventory is a manual diagnostic, not a health poll. It reads storage and100+holds the relay's admission owner while it runs, so competing Git requests101+and mutations receive a retry response. Server limits bound reads and listing102+work; an incomplete scan returns an error instead of partial totals. The103+60-second cooldown limits repeated scans in a live instance and resets when104+that instance restarts. It is not a recommended polling interval.105+106+`operations` and `inventory.observed` describe the scan's requests and read107+bytes. They help explain the work a scan performs, but are not tenant billing108+meters or a complete hosting cost. Existing activity and storage accounting109+still applies; there is no separate inventory fee.110+111+## Handle limits without multiplying work112+113+A client checks the response reason before retrying. A transient admission114+refusal can clear; a storage or repository limit requires a change in the115+workload or available budget.116+117+| Condition | Client behavior |118+|---|---|119+| `outOfFuel` | Show the relay's allowance and balance; stop repeated writes until the budget permits them |120+| Busy relay or rate limit (`429`) | Honor `Retry-After` or the response's `retryAfter` when supplied, and back off |121+| Upload or repository limit | Keep the rejected operation visible to the user; a top-up does not raise a hard limit |122+| Incomplete inventory | Keep the last complete report labelled with its capture time; an error does not mean zero usage or free storage |280123 281-R2 Standard includes pooled storage and operation allowances. Beyond them,282-storage is $0.015/GB-month, Class A operations $4.50/million and Class B283-$0.36/million, with billing-unit rounding. Free egress does not remove those284-operation costs. See [R2 pricing](https://developers.cloudflare.com/r2/pricing/).285-[Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/),286-[KV pricing](https://developers.cloudflare.com/kv/platform/pricing/) and287-[Cloudflare for SaaS plans](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/plans/)288-cover other host expenses. These sources were checked on Sept. 4, 2026.124+Fuel exhaustion does not delete data. Ordinary reads remain available under125+the read policy, but Git requests and remote site fetches also require fuel.126+Already stored site files remain readable under their access rules. Client127+interfaces distinguish these cases instead of treating every refusal as a128+network error.289129 290-## A 40% margin scenario130+## Margins for deployments291131 292-This is a planning target; the deployed rates, allowances and weekly default293-remain unchanged. A 40% gross margin means `price = cost / 0.60`, or about294-1.667x cost. Multiplying cost by 1.40 produces only a 28.57% margin.132+For developers hosting the service, `npm run margin` compares configured fuel133+rates with provider costs. It reads `wrangler.jsonc`, uses the provider rates134+recorded in the script and either fetches a bitcoin price or accepts an135+explicit scenario:295136 296137 ```297-npm run margin -- --btc 60000 --target 0.40298-npm run margin -- --btc 80000 --target 0.40299-npm run margin -- --btc 100000 --target 0.40138+npm run margin139+npm run margin -- --btc 60000140+npm run margin -- --btc 60000 --target 0.40 --relays 5000300141 ```301142 302-These are Sept. 4 planning scenarios, not bitcoin spot quotes. At the303-current sats rates, before free users and other expenses:304-305-| BTC scenario | Events, files and rows margin | Awake margin |306-|---|---|---|307-| $60,000 | 16.67% | 12.73% |308-| $80,000 | 37.50% | 34.55% |309-| $100,000 | 50.00% | 47.64% |310-311-The 40% threshold is about $83,333/BTC for events, files and rows, and312-$87,273/BTC for awake time at $0.00576 per provider hour. The script already313-uses gross margin; its existing 33% default was never a 40% markup formula.314-315-For a whole deployment, the planning condition is316-`revenue >= (provider bill + shared operating costs + subsidy) / 0.60`.317-The subsidy is only free-user cost not already included in the provider bill;318-counting it twice overstates expense. Provider allocations are pooled once,319-including other services on the account, rather than granted anew per relay.320-321-An illustrative personal relay consumes about $0.152 of the four resource322-lines at marginal list rates: 20 MB events, 100 MB files, eight provider hours323-and 100,000 written rows. A relay consuming all current allowances reaches324-about $0.861 before other costs and account rounding. Actual duration and row325-usage must come from provider evidence because the tenant meters are partial.326-327-Storage-light workloads can still be expensive. One million Class A and ten328-million Class B operations beyond the pooled allocation cost $8.10 even if329-they repeatedly access only a few kilobytes. Across 100 such relays that is330-$810 before duration, requests or rows; storage revenue alone cannot cover it.331-A fleet forecast therefore needs active tenant share, utilization, operation332-mix, cache misses, retries and concentration in its busiest tenants, alongside333-FX and payment-fee sensitivity. The script's two shapes do not establish this334-distribution.335-336-## What can be bundled337-338-These are proposed operating boundaries, not new allowances or charges.339-340-| Work | Proposed treatment |341-|---|---|342-| Bounded metadata and health reads during ordinary use | Bundled with request and abuse caps; no separate tiny surcharge |343-| Service-caused retries and ambiguous-write reconciliation | Host operating budget; record attempts and expense without charging a second logical operation |344-| Existing retained data | Existing storage meter, including reserved and temporary bytes; no second Git storage charge |345-| Free tenants | Explicit monthly fleet subsidy budget with utilization and heavy-tail review |346-| High-volume scans, repeated cold clones and uncached site fetches | Bounded work and request quotas; operation-heavy use cannot be treated as unlimited because bytes are unpriced |347-| Optional optimization | Tenant-visible budget and deferral; extra wake-ups require opt-in |348-349-## Proposed maintenance contract350-351-The current ntig 0.3.0 package has no resumable optimization runner. No352-maintenance job, budget setting, wake schedule, format migration or garbage353-collection is enabled by this contract. Existing ref cleanup, retention and354-alarms keep their current roles. An eventual integration has these boundaries:355-356-| Stage | Host and backend contract |357-|---|---|358-| Eligibility | Cheap stored statistics identify pack/read amplification, reclaimable bytes or a limit approaching; no daily full scan or unconditional repack |359-| Admission | Optional work starts only while the tenant is already active by default; foreground requests take priority before each bounded step |360-| Reservation | A durable job ID and UTC budget month reserve bounded work, operation counts and temporary retained bytes before execution |361-| Execution | A resumable cursor, bounded I/O and a cooperative deadline limit each step; a timer alone cannot preempt arbitrary work safely |362-| Completion | Persist outcome and actual expense once per step; stable attempt IDs separate retries from completed logical work |363-| Recovery | A crash or ambiguous write keeps its reservation until reconciliation; CAS and persisted authority protect state across restarts |364-| Scheduling | Extra wakes require opt-in, a monthly budget and an allowed window; later window, manual run and pause are separate choices |365-| Exhaustion | Optional work defers; a write that cannot safely fit a hard limit fails clearly without discarding protected data |366-| Retention | Optimization never authorizes deleting retained history; reachability, readers and retention rules need a separate proof |367-368-Host expense accounting stays separate from tenant charge calculation. A369-future duration observer measures the union of registered work intervals,370-including long steps, without claiming it is the provider's entire active371-lifetime. Its validation covers overlap, mid-step flush, month boundaries,372-crash before and after persistence, retries, reservation exhaustion and373-foreground arrival. The host contract also needs tests for ambiguous PUT,374-temporary copies, authority changes and paused scheduling before it runs375-against customer data.143+The explicit bitcoin prices are scenario inputs, not quotes. For cost `c`144+dollars per unit, price `p` sats per unit and bitcoin price `B` dollars,145+revenue is `p * B / 100000000` and gross margin is `1 - c / revenue`.146+A target margin `m` requires `price = cost / (1 - m)` in the same currency.147+The script defaults to a 33% target and a 20% floor. It exits with code 1 when148+a priced line falls below target; the weekly workflow records the result and149+opens or refreshes an issue.150+151+Per-line margins do not establish a deployment margin. Provider allowances152+are shared across the account, while tenants receive the relay allowances.153+Free usage, R2 operations, platform requests, CPU, KV, observability and payment154+costs also consume the service's revenue. A small stored dataset can generate155+many reads or writes. The script's fleet shapes are illustrative assumptions;156+actual workload mix and provider usage determine whether revenue covers the157+deployment.158+159+Provider prices and included allocations are documented under160+[Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/),161+[Durable Object pricing](https://developers.cloudflare.com/durable-objects/platform/pricing/),162+[R2 pricing](https://developers.cloudflare.com/r2/pricing/) and163+[KV pricing](https://developers.cloudflare.com/kv/platform/pricing/).164+[Hosting bind.ws](11-hosting-bindws.md) describes deployment configuration.165+Running the margin script changes no tenant rates; a rate change requires a166+configuration change and deployment, and appears in the relay's `/fuel`167+response.