Integrating an application with monetize v2.0
Write a small operator and a resource. Monetize is the control plane that runs it. If you know Kubernetes, the parts map like this:
| Kubernetes | monetize v2 | Who writes it |
| custom resource | the shape: roles (e.g. front, be, fe), sizes, disks, networks, as data (section 3.3) | you |
| operator | the product operator (guest): your signed .wasm with 6 ops (section 3.1) |
you |
| client-go | the SDK monetize-product-guest |
premade, open; you link it |
| control plane / reconcile loop | the ONE box, monetize-admin |
premade |
| status | the order state and your boxes' reports | monetize and your boxes |
| cloud provider | the UpCloud plugin (upcloud.wasm) |
premade |
| external-dns | the Cloudflare plugin (dns_cloudflare.wasm) |
premade |
The SDK is a library compiled into your operator: the types and the signatures of the 6 ops. The product operator is your signed .wasm; the ONE box pulls it and calls it through the 6 ops. Unlike a Kubernetes operator it runs no loop of its own and calls nothing: monetize runs the loop and asks it the product questions (pull only, no secrets).
Premade with monetize: Stripe, UpCloud and Cloudflare DNS.
This guide is for the author of an application that the ONE box is to sell. It describes the v2.0 target (MONETIZE-V2.0-PLAN.md, A24, A25), not today's 28-op contract. The older ASD-STE100 manual (integration-guide.md) describes today's code and stays until P3 lands; where the two differ, this guide wins for new work.
Every statement that depends on a phase that has not landed carries its tag: [P1], [P2], [P3], [P4]. A statement with no tag holds on main today. Where a v2.0 name differs from today's, today's name follows in brackets.
The pictures of this guide are operator-v2.svg (the whole idea, above), integration-v2.svg (the parts and the plugins, section 2), datamodel-v2.svg (the data model, section 7) and buyflows-v2.svg (the buy flows, section 8).
Contents:
- The parts and who owns them
- The plugins, and who writes each one
- Write: your product operator, your pages, your estate front
- How it ships
- How you test it
- A worked example: the Lake
- The data model: what is stored where
- The buy flows, step by step
- Where it lives: traits and crates
1. The parts and who owns them
Three parties. Each owns what it writes.
| Part | Owner | What it is |
monetize-admin (the ONE box) |
monetize (open, AGPL) | PID 1 of one machine. Inside it runs the open monetize-server: the ledger, the estate state machine, custody, the pull door every box reports to, the guardian runner, nonpay, mail, DNS, the shop's installer, the operator console. It knows no product name (xtask/tests/product_names.rs holds it). |
| The signed vendor guests | monetize-impl (private) | stripe.wasm, upcloud.wasm, dns_cloudflare.wasm live today; scaleway.wasm, klarna.wasm, swish.wasm are next and are built, signed and tested in CI [P4]. The Build Thing of monetize-impl signs every guest the ONE box runs. |
| The shared crates | monetize (open) | monetize-product-guest (the SDK of your product operator), monetize-site-buy (the buy flow, the relay, the browser scripts), monetize-estate-side (the box side: the pull pump over Fleet.Report / Orders / Ask, and the front's signed shop door, over Fleet.Ask since P3 (the old SoldEstate door served one release more); landed in P4a, the products move onto it [P4]), nordisk-pairing (pairing and the live door; Skidbladnir). monetize-embed is the product's entitlement cache, not the pump. The shared update step moves into nordisk-pairing [P4]; today it is three copies: gunnar-front/src/update.rs, holger-estate-front/src/update.rs and monetize-server's estate_order/update.rs. |
| Your product operator (guest) | you | A signed .wasm with 6 pure ops. It says what an estate of your product IS and plans ONE order. It opens no door and holds no key. |
| Your box side | you | Your box binaries implement Estate [P3/P4] over the shared pump, and nordisk-pairing's Product and FrontProduct. |
| Your site | you | Your pages implement BuySite and mount the shared relay. The buyer stays on your site; the only page outside it is the vendor's checkout. |
| Your declaration | you + the operator | One [products.<id>] block in the ONE box's feed (solo.toml in private-monetize-ops): the module pin, prices, bundles, sign-up plans, how your kit makes media, mail words. |
Two rules shape everything below.
- PULL only. The ONE box never calls into an estate. Your boxes report and pull their orders. Your product operator dials nothing.
- Runtime code calls Rust libraries, never
cargo,xtask,dwarvesor a shell.
2. The plugins, and who writes each one
The ONE box runs four kinds of plugin, one slot per trait. A plugin is a signed .wasm guest that the box pulls from the release bucket and pins by sha256, or an open impl linked into the open binary. You write exactly one of them: your product operator (guest). Everything else is premade.
The picture integration-v2.svg draws the same list. Its colours mean the same in every picture of this guide: green = you write it, cyan = premade in open monetize, amber = premade in monetize-impl (private, signed), grey = outside (the buyer, the vendors, the operator). A dashed outline is a stub of the lowest priority (owner rule).
| Plugin | Type (slot) | Who provides it | Where it lives | Signed? | Priority |
gunnar_monetize_guest.wasm |
Product | the integrator (Gunnar) | nordisk/gunnar crates/gunnar-monetize-guest |
yes, by the Build Thing | live |
holger_monetize_guest.wasm |
Product | the integrator (Holger) | nordisk/holger crates/holger-monetize-guest |
yes, by the Build Thing | live |
the Lake's product operator (monetize-guest-lake) |
Product | the integrator; kept in open monetize as the example of what a new product writes: a thin operator and its shape as data | nordisk/monetize guests/lake |
yes, by the Build Thing | the reference [P1] |
stripe.wasm |
Payment | premade, monetize-impl | monetize-impl guests/stripe |
yes | high: live on the ONE box, the one that matters |
klarna.wasm |
Payment | premade, monetize-impl | monetize-impl guests/klarna |
yes | stub, lowest |
swish.wasm |
Payment | premade, monetize-impl | monetize-impl guests/swish |
yes | stub, lowest |
invoice |
Payment | premade, open | nordisk/monetize crates/monetize-pay/src/invoice |
no: linked into the open binary | open default |
sandbox (payment) |
Payment | premade, open | nordisk/monetize crates/monetize-pay/src/sandbox |
no: linked | the money-free default [P1] |
upcloud.wasm |
Cloud | premade, monetize-impl | monetize-impl guests/upcloud |
yes | high: live on the ONE box, the one that matters |
scaleway.wasm |
Cloud | premade, monetize-impl | monetize-impl guests/scaleway |
yes | stub, lowest |
server |
Cloud | premade, open | nordisk/monetize crates/monetize-cloud/src/server |
no: linked | open default: the box's own iron, buys nothing |
sandbox (cloud) |
Cloud | premade, open | nordisk/monetize crates/monetize-cloud/src/sandbox |
no: linked | the money-free default [P1]: fake iron at real prices |
dns_cloudflare.wasm |
DnsWriter | premade, monetize-impl | monetize-impl guests/dns-cloudflare |
yes | high: live on the ONE box |
no DNS guest fed (ManualDns) |
DnsWriter | premade, open | nordisk/monetize crates/monetize-server/src/estate_order.rs |
no: linked | open default: the alert estate-orders.dns-by-hand asks the operator to set the records |
The money-free default (owner 2026-10-24) is the open build's default features: the sandbox payment and cloud and the invoice vendor. A fresh open build sells free plans and lays on play iron until a payment guest and a cloud guest are fed.
Test doubles stand in for a vendor's API, not for a plugin. They run only in tests:
| Test double | Stands in for | Who provides it | Where it lives |
mock-upcloud, mock-scaleway |
the UpCloud and Scaleway APIs | premade, open | nordisk/monetize crates/mock-upcloud, crates/mock-scaleway |
fake_stripe_serve, fake_klarna_serve, fake_swish_serve |
the Stripe, Klarna and Swish APIs | premade, monetize-impl (PRIVATE today) | monetize-impl crates/monetize-pay-impl/examples/ |
toy, toy-vendor, toy-cloud, toy-dns, toy-signup |
one ABI per slot, end to end | premade, open | nordisk/monetize guests/toy* |
An outside author cannot run the fake Stripe or monetize's cargo xtask trials (the 12 buy trials, which build from the private siblings ../monetize-impl, ../gunnar and ../holger). Use the money-free default.
The crates you link are not plugins: monetize-product-guest (the SDK of your product operator), monetize-site-buy (the buy flow, the relay, the browser scripts), nordisk-pairing (pairing and the live door) and monetize-estate-side (the box pump). Section 9 lists them with their paths.
Also reused as they are:
monetize-admin: the box itself. The operator lays it frommonetize-admin-installer. You never build it.- The open
monetize-serverinside it: the state machineinstantiate(order)is lay → install → sale check → capture → DNS → ready. It is the only place that lays, pairs and captures. - The shop's installer (
monetize-shop-pairing): it pairs your laid boxes over WireGuard and mTLS, once, at the install. It holds no product code: it pairs by the contract your facts declare and the sale check reads the paths yourfacts().proofnames. monetize-site-buy: the 26 relay routes, the budgets, the return pages, the browser scriptsBUY_JSandWELCOME_JS. Gunnar's copies of the grow and renew doors go into it [P4].- Sign-up: the engine's own sink on the ONE box. The private sign-up guests are deleted [P2].
What a vendor guest reads is NAMES only: MONETIZE_STRIPE_<INSTANCE>_SECRET_KEY, UPCLOUD_TOKEN, and so on. The keys stay on the ONE box in the operator's custody. Your product never sees them.
3. Write: your product operator (guest), your pages, your estate front
3.1 The product operator: one crate, six ops
Make a crate in your own repository, crates/<id>-monetize-guest, with crate-type = ["cdylib", "rlib"] and the dependency monetize-product-guest. Gunnar's is gunnar/crates/gunnar-monetize-guest; the Lake's is monetize/guests/lake [P1].
The trait, as A24 §2.2 writes it [P3]. The names in brackets are today's ops (monetize_product_guest::Guest), which are what you implement until P2/P3 land.
pub trait Product {
/// Op 1. Who the product is: id, ABI, meters. No grants.
fn manifest(&self) -> Manifest; // [today: manifest(), the meters in it (Manifest::with_meters), P2 T13]
/// Op 90. What an estate of this product IS. Pure data, read once per guest digest.
fn facts(&self) -> Facts; // [today: lifecycle_facts(host) -> LifecycleFacts, with tickets, test_actions, guardian (P2 T13)]
/// Op 91. The plan of ONE order: machines (role, plan, disks, medium), stores, names.
fn plan(&self, order: &Order, ctx: &PlanContext) -> Result<EstatePlan, Refusal>; // [today: lifecycle_lay_plan(host, &LayPlanReq) -> serde_json::Value]
/// Ops 100-102. The guardian: pure, over the estate as the boxes reported it.
fn diagnose(&self, view: &EstateView) -> Vec<Finding>; // [today: guardian_diagnose]
fn remedy(&self, remedy: Remedy, view: &EstateView) -> Result<Vec<Step>, String>; // [today: guardian_plan]
fn observe(&self, role: &str, what: &str, view: &EstateView) -> StepState; // [today: guardian_observe]
}
export_product!(YourGuest); // [today: export_product!(YourGuest, grants = [])]
What happened to the other 22 ops of the old table (A25 §2; P2 T13, monetize_product_guest::ops::RETIRED):
| Today | Fate |
list_tenants, read_usage, read_activity, read_usage_page, tenant_owners |
die: the ONE box reads them from your boxes' reports |
push_entitlement, purge_tenant, add_users, users_fit |
die: they are orders your boxes pull |
can_absorb, servable, metered_door, twins |
die: answered from the fresh report |
grow_flush, grow_start, grow_status, grow_resume |
die: a grow is a new order and a plan diff |
may_act_for |
moved to the host: the ticket is checked on the ONE box (site_ticket) under the block's appliance_key or the site's key [the install-time custody of ticket_pubkey: P3] |
id, meters, tenant_renewal, test_actions, lanes |
merged into manifest() (op 1) and facts() (op 90: tickets, test_actions, guardian) |
A guest implements only the six. Grants: none, for every product guest (Gunnar, Holger and the Lake ask for [] since P2 T13).
3.2 manifest()
Manifest::new("<id>", meters![...]) // [today: Manifest::new("<id>", &[]) and meters() -> Vec<WireMeter>]
One meter per thing you measure or cap. Gunnar: pack_bytes (enforced, disk). Holger: disk_bytes (enforced, disk). The Lake: ingested_bytes (enforced, disk: Backing::DiskBytes) and query_seconds (cap only).
3.3 facts(): the shape as data
Everything the core and the shop's installer know about your product is in this one answer. Nothing about your product is written in the core [P3].
pub struct Facts {
pub kind: String, // "gunnar" | "holger" | "lake"
pub words: Words, // name, title, account_home, the mail words [today: words + default_shape.mail_words]
pub roles: Vec<RoleFacts>, // one per role the plan may lay [today: medium_of, other_medium, keep_medium, data_roles, live_slot, backup_roles]
pub front_role: String, // serves the public names, drives the update and is upgraded LAST [today: front_role]
pub primary_role: String, // holds the tenants, the ticket key, the backups [today: the first data role]
pub twin: Option<TwinFacts>, // None = no election, no mirror [today: a data role with mirrors_primary = true]
pub update_order: Vec<String>, // [today: update_order]
pub names: Vec<String>, // served name prefixes: "", "www.", "git." [today: default_shape.names]
pub proof: SaleProof, // the routes the sale check reads and what proves the sale [today: per-kind code in monetize-shop-pairing sale_check.rs; the type: P3a]
pub form: OrderForm, // the fields a buyer sets, their ranges, the named offers [today: default_shape.ladder and EstateSizes]
pub grow: GrowRule, // what may change on a running estate [today: "data never shrinks" in the core]
pub tickets: bool, // your main mints actor tickets [today: LifecycleFacts.tickets]
pub test_actions: Vec<TestAction>, // the shop's test buttons [today: LifecycleFacts.test_actions]
pub feed: Vec<FeedKey>, // the product-specific feed keys your boxes read [today: monetize-shop-pairing CONTRACTS]
}
pub struct RoleFacts { role, medium, keep_medium: Option<String>, live_slot: bool, backed_up: bool, data: Option<DataRole>, many: bool, optional: bool }
Landed (P3a, additive): monetize_product_guest::facts::Facts, today's LifecycleFacts field for field plus the shop contract's fields as data (billing, offsite, boxes_hold_console_token, welcome, sold_variant, site_variant, box_kind, free_feed; proof_name is domain_proof_name, and the old name is still read) and proof: Option<SaleProof>. Every field is defaulted, so today's lifecycle.facts answer reads as a Facts.
Landed (P3, T15): Facts IS the answer to lifecycle.facts (op 90); wire::LifecycleFacts is its old name. It also carries console_token_feed, passkey_rp_feed, welcome_readme (your welcome README, {host}, {account}, {repo}), reserved_accounts (your boxes' own principals, never a buyer's account) and form (your OrderForm, below). The shop's installer reads its contract for your estate off these facts (monetize_shop_pairing::ProductContract::of) and holds none of its own: a fact your guest does not declare is refused by name ([contract-…]), and no estate of yours is paired or captured until it does. roles, twin and grow are still to come.
Each product declares its own sizes, networks and disks. The core has no opinion:
- Gunnar:
front,main,twin; four data disks on main; the twin mirrors main and is elected; names"",www.,git.. - Holger:
front,main,twin; one data disk; every disk on thehddtier; the lean shape (STARTER-1xCPU-1GBfront,STARTER-1xCPU-2GBmain,STARTER-1xCPU-1GBtwin); the front has a public and a utility IPv4, main and twin a utility IPv4 only (they sit behind the front); names"",www.. - The Lake:
front,ingestor(n),be(x),fe(y),graph(0 or 1), and a storeinputof classobject; no twin.
Today the shape rides in LifecycleFacts.default_shape as JSON (front, main, twin machine shapes with plan, disks, interfaces). Keep that until P3; A24 §4.1-4.3 shows the same facts in the target form.
3.4 plan(order, ctx): the "instantiate" function
Pure. The buyer's order is validated against facts().form before it reaches you. ctx is what the operator fed: zone, labels, the template tiers (today's LayPlanReq: config, company, sizes, twin_plan, template_tiers).
You answer an EstatePlan: reference = estate/<company>, zone, one machine per role instance (role, hostname, title, plan, boot order, labels with role = <role>, disks with the system disk first), and stores [P3]. The core prices it at the cloud (Cloud.price), lays it (Cloud.lay), and on a change diffs the laid plan against your new one (Cloud.change) [P3]. A refusal refuses the order with your words; there is no native fallback [P3].
Today's rules that still hold: each title unique; each role laid once (the Lake names its n-th member <role>-<n> [P1]); a plan that names front, main or twin must have exactly those three [until P3].
Gunnar's and Holger's plan is one call to the shared plan_from_shape(ctx, &shape) [P4]; today each has its own lay_plan (lifecycle.rs, estate.rs).
3.5 The guardian: diagnose, remedy, observe
Unchanged from today. Pure, over EstateView (what the boxes reported). diagnose answers findings with a code, a role, words, evidence and the remedies a person or auto-fix may press. remedy answers steps (cloud steps, orders for a box, waits). observe answers whether a wait is met. The shared step helpers (guardian::steps::plan, ::observe) replace the four copies [P4].
3.6 Your declaration: the toml block
The operator's feed file (solo.toml in private-monetize-ops) carries one block per product. You write it with the operator. After P3 the product's own facts are gone from it: no grants, no shape, no contract.
[products.<id>]
module = "<id>_monetize_guest.wasm" # your signed product operator, pinned by sha256 (cargo xtask pins)
sha256 = "..."
purge_after_days = 10 # an unpaid account is deleted 10 days after its end
prices = """
[SE]
disk_gib_month = 1000
"""
[products.<id>.media] # how YOUR kit makes the media the ONE box lays from
kit = "private-<id>-ops"
argv = ["cargo", "xtask", "estate-media", "--where", "{where}", "--media-to", "{to}", "--apply"]
[products.<id>.bundles."10gb"]
cap_delta = { pack_bytes = 10737418240 }
[products.<id>.signup]
cap_meter = "pack_bytes"
[products.<id>.signup.plans."<id>/account"] # the free account an order lands on
amount_minor = 0
cap_bytes = 0
[products.<id>.mail]
The ONE box also needs [vendors.stripe.instances.<id>] (the operator's; it names the key NAMES) and [boxes.prod.estates.<site>] for each site that sells your product (product, vendor, owner). Your estate's own declaration on its media (Holger's RON for its server) is your kit's business; monetize does not read it.
Migration (RA2-16, 2026-10-26): two retired keys are refused by name. [products.<id>.plans.<plan>] and return_hosts fed only the ONE box's old /kop purchase page, which OD6 deleted. A config or a feed (plugin.<id>.toml) that still sets either is now refused at load, naming the key; before, both were parsed and ignored. To migrate, delete the [products.<id>.plans.*] tables and the return_hosts line from the kit that writes the feed (for private-monetize-ops, solo.toml and the writer in xtask/src/solo.rs). Nothing else changes: a product sells through its own sign-up ([products.<id>.signup.plans], fed as [signups.<id>.plans]) and its [products.<id>.bundles]. Deploy the kit's change BEFORE this monetize: an older monetize reads a block without those keys as before.
3.7 Your buy pages: BuySite and the assets
Implement monetize_site_buy::BuySite once, in your site's server (Gunnar: gunnar-ui-server/src/auth/buy_site.rs; Holger: holger-estate-front/src/buy.rs). Twelve methods, 6 required + 6 defaults (P4b added operator; A32-9 deleted the grow_needs_checked_tickets knob: every site's grow doors wait for checked tickets; monetize-site-buy tests/h15_stepped_up_refuses.rs holds the count). The default of stepped_up REFUSES (Err(STEP_UP_WORDS)): a site that does not prove a fresh key touch cannot end an estate. Implement it.
pub trait BuySite: Send + Sync + 'static {
fn origin(&self) -> String; // "https://www.holger.rs"
fn product_word(&self) -> String; // "Holger"
fn who(&self, headers: &HeaderMap) -> Future<Result<Who, Refusal>>; // Visitor | Person | Machine, from YOUR session
fn ticket(&self, headers, buyer: &Buyer, purpose: &str, caps: &BTreeMap<String, u64>) -> Future<Result<Vec<u8>, Refusal>>; // your main signs it
fn held_caps(&self, headers, buyer: &Buyer) -> Future<Result<BTreeMap<String, u64>, Refusal>>;
fn signup_account(&self, user: &str) -> Future<Result<Account, Refusal>>; // NoPasskey | HasPasskey
// defaults: pages(), allowed(), trusted_proxies(), fixed_buy_url(), operator(), and
fn stepped_up(&self, headers) -> Future<Result<(), String>> { Err(STEP_UP_WORDS) } // a fresh key touch; ending an estate needs it, so implement it
}
Then: a ShopLink to the ONE box with your site's bearer, a Relay over your site and the link, and monetize_site_buy::router(relay) mounted on your web server. Do not copy a buy door; the relay has all 26.
The browser side is one script, rendered once: monetize_site_buy::assets::buy_js(&Settings) and welcome_js(&Settings). Your Settings names the product word, the storage prefix, the free plan id, the manage URL, the bearer key, the status and offer URLs and the contact. Your UI server serves the rendered script; your markup and CSS are your own. No product server serves a byte of UI.
The relay carries your product's own order: your page sends the buyer's form as order (a JSON object) through untouched, and the relay passes it on as EstateSizes.order on EstateOrder.Start and ChangePlan. The ONE box checks it against your facts' form (OrderForm::check: every key a field of the form, each value of its kind and in its range, the rest filled in from the defaults) before anything is priced or held, keeps the filled order with the run, and hands it to your plan() as LayPlanReq.sizes.order. A product with no form is sized by the three plan fields (front_plan, main_plan, main_data_gib), and an order for it is refused by name.
The buyer's path: your sign-up page → your relay → the ONE box mints the vendor's page → the buyer pays at the vendor (the only page off your site) → back to your paid page → your welcome page polls until the estate is ready. The buyer never sees monetize. Stripe once, e-mail once, "Pay ".
3.8 Your estate front and boxes
Every box of your estate runs:
- nordisk-pairing's box side:
Product(kind,tunnel_pubkey,capacity,offer,fed, the live door). The box offers itsfleet_pubkeyandticket_pubkeyat the pairing. The shop pinsfleet_pubkeyfor every box; it pins the primary'sticket_pubkeyonly for a site order today (site_ticket_key), and for every estate [P3]. - the shared pump in
monetize-estate-side(pump, with yourPumpBox: health, report, the executor of your order kinds; landed in P4a, the products move onto it [P4]) (until then your own loop overFleet.Report,Fleet.Orders,Fleet.Ask: Gunnargunnar-repair/src/fleet.rs, Holgerholger-fleet/src/puller.rs), with yourEstate[P3]:
pub trait Estate: Send + Sync {
fn report(&self) -> Report; // build, health, usage per tenant, twin review
fn execute(&self, order: &Order) -> Result<Ack, Refusal>; // one signed order the box pulled; an unknown kind is refused BY NAME
fn box_key(&self) -> &SigningKey; // what the box signs its Fleet calls with
}
The core's kinds are ONE list, monetize_product::pull (A28 M25): 7 orders, entitlement, add_principal, add_users, grow_room, purge_tenant, grow_go_ahead, sign_in_again (order::CORE), and 1 ask, grow_set (ask::CORE). A product whose guardian runs a twin adds its own guardian kinds (Gunnar's repair verbs: monetize_embed::fleet::kind::GUARDIAN). Name the kinds your boxes run in your facts (order_kinds); Facts::runs(kind) is the refusal BY NAME for any other kind, from those facts alone.
The front also runs nordisk-pairing's FrontProduct (the relay's tunnel key, its offer, the live door) and the shared update step [P4] (Fleet.Ask kinds update_take / update_report since P3, the old SoldEstate.UpdateTake / UpdateReport served one release more; the step itself is still in gunnar-front/src/update.rs and holger-estate-front/src/update.rs). The update is an order the front pulls; the front stages the live app through each box's live door, in facts().update_order, or reports falls_to_keep.
The sale check: before the capture the ONE box reads, over public HTTPS, the routes your facts().proof names on the front [P3]. A SaleProof (landed in P3a: monetize_product_guest::facts, run by monetize_shop_pairing::sale_proof) is a list of typed checks, each one request and what its answer must hold: the method and body (POST with {invite} and {account}), a JSON status field (passkey_enabled true, bootstrap_spent false), a non-empty challenge, and the bearer list rule (exactly one <account>/welcome). Each product names its own routes; there is no shared /auth/first-admin (Gunnar: /auth/status and /auth/passkey/register/start; Holger: /-/auth/passkey/config; the Lake: /first-login). The shop runs your guest's own facts().proof and has no judge of its own (P3: the judges went once their SaleProof was proven to give the same answers). Serve the login page and the first-admin door on the front from the first boot.
4. How it ships
Pull only, at every hop.
- The Build Thing of monetize-impl builds your product operator (guest) from your repository (
[[build.variant]] name = "<id>-guest",manifest_dir = "../<id>",target = "wasm32-unknown-unknown",sign.purpose = "plugin") and signs it under the plugin root the ONE box trusts. Every vendor guest is a row of the same file. cargo xtask pins(private-monetize-ops) checks the sha256 of every built guest againstsolo.toml. A stale pin is refused before anything is laid.cargo xtask release --publish --applywrites the signedrelease-manifest.tomland puts the guests into the release bucket:release/plugins/<blake3>/<name>and its.p7s.cargo xtask feed --where upcloud --box prod --applyputs the feed built fromsolo.tomlon the box's door. It prints key NAMES only.- The ONE box pulls: its own
Plugins.Update(monetize box plugins --apply) takes the plugins the manifest names and verifies each against the root; a module that is not its pin is refused by name. - Your media come the same way: your kit's
estate-mediastep uploads them to the release bucket and writesmedia-<kind>.toml; the ONE box lays from them.
A release of the ONE box itself is one verb (self-release) and goes out only from a green, gated main. Your estates are updated by the ONE box (EstateOrder.UpdateEstate), which your front pulls.
5. How you test it
Three rings, innermost first.
| Ring | Verb | What it runs | Time |
| unit | cargo test in your product operator's (guest's) crate |
your facts() and plan() as pure functions; the conformance table of monetize-product-guest over your signed .wasm: each op answers a known case, an unknown op answers the frozen phrase, a trap is refused by name [P2] |
seconds |
| fast | cargo xtask fast --product <id> (holger, gunnar; the names come from your product operator's lifecycle.facts) |
the REAL monetize-admin server in a container with your signed product operator, mock-upcloud with its container engine (one container per laid machine, from your box image), the fake Stripe, the printed DNS writer; today the operator orders the site, pays at the fake, and the box lays, installs, pairs and captures (ready), then the run tears down; the buyer over your site's HTTP, grow, update and the estate's terminate [P3]; a receipt row per check |
minutes |
| gate | cargo xtask gate <id> (private-monetize-ops) |
the KVM gate: real media under QEMU, the real ONE box medium, WireGuard on the mock's utility fabric; before a release | about 45 minutes |
What the fast test checks, among others [P1]: the quote's words match the plan your product operator answered; the vendor page is minted once; the lay has exactly the plan's machines; every box offered fleet_pubkey; the election happened where facts.twin is Some and did not where it is None; your product operator made ZERO door calls over the run; a grow changed exactly the diff at the mock; a shrink was refused by name; the terminate left zero resources with the reference, backups included.
The Lake is the reference product: it runs the same rings in monetize's own CI (cargo xtask ci). When your product behaves like the Lake, it behaves like Gunnar and Holger.
Money: your tests run on the money-free default [P1] or the fake Stripe. A change to the money flow is proven by one real purchase on test2 before gunnar.rs. Test addresses are *@mock.invalid.
6. A worked example: the Lake
The Lake is the parametric case: the buyer sets numbers, the product operator (guest) turns them into a plan, the core prices the plan at the cloud. No Lake code is in the core. On main it is guests/lake and crates/lake-product [P1]; what it still waits for from P3 is listed in its P3_WAITS.
The order:
struct LakeOrder { store_gib: u64, ingestors: u32, ingestor_plan: String, be: u32, fe: u32, graph: bool }
// store_gib 100..=100_000 in steps of 100; ingestors 1..=64; be 1..=32; fe 1, 3 or 5; graph yes/no
The facts (A24 §4.3, target form) [P3]:
// one medium for every role: "lake" (keep medium "lake-keep"); one binary runs each role
roles: [ role("front", medium "lake", live_slot),
role("ingestor", medium "lake", many),
role("be", medium "lake", many, data: data1),
role("fe", medium "lake", many),
role("graph", medium "lake", optional),
store("input", class "object") ],
front_role: "front", primary_role: "fe", twin: None, // today the core's primary is the first data role, `be` (P3_WAITS)
update_order: ["ingestor", "fe", "be", "front"], // the front LAST (guests/lake role::UPDATE_ORDER)
names: ["", "www.", "s3."],
proof: SaleProof { checks: [check("the login page", GET "/login"), check("the first-login door", GET "/first-login")] }, // its own routes (lake_box.rs)
form: OrderForm { fields: [gib("store_gib", 100..=100_000, step 100), count("ingestors", 1..=64), plan("ingestor_plan"),
count("be", 1..=32), count("fe", 1..=5, odd), bool_("graph")],
offers: [offer("Starter", {store_gib: 500, ingestors: 2, be: 3, fe: 1, graph: false}), offer("Team", ...)] },
grow: GrowRule { never_shrinks: ["store_gib"], may_add: ["ingestors", "be", "fe"], may_remove: ["ingestors"] },
tickets: true, test_actions: [private("lake-mini")], // "Test: the smallest Lake"
The plan (plan_of on main today [P1]; plan() at P3):
let o: LakeOrder = order.parse()?;
let mut p = EstatePlan::new(ctx, "lake");
p.machine("front", "STARTER-1xCPU-2GB", disks![system 20]);
for i in 1..=o.ingestors { p.machine_n("ingestor", i, &o.ingestor_plan, disks![system 20]); }
for i in 1..=o.be { p.machine_n("be", i, "STARTER-4xCPU-16GB", disks![system 20, data1 (o.store_gib / o.be).max(50) tier "maxiops"]); }
for i in 1..=o.fe { p.machine_n("fe", i, "STARTER-2xCPU-4GB", disks![system 20]); }
if o.graph { p.machine("graph", "STARTER-4xCPU-16GB", disks![system 20, data1 100]); }
p.store("input", o.store_gib, "object"); // [P3: today the input store is the BE data disks]
Ok(p)
Machines laid: 1 + n + x + y (+1), roles front, ingestor, ingestor-2, be, be-2, be-3, fe, graph.
What the core does with it, with no Lake code of its own:
- the quote:
Cloud.price(plan)plus the margin; - a grow: a new order
{be: 4}→plan(new)→ the diff against the laid plan (one BE added) →Cloud.change(laid, new)[P3] → an order to the FE leader ("a new BE joins") that the Lake's box executes; - a shrink of
store_gib: refused by name fromfacts().grow[P3]; - the guardian: a silent
be→ remedyNewServerof that index; a silent FE leader → a person.
The Lake's box side, as a target [P4]: one binary for every role (--role front|ingestor|be|fe|graph --index n) with impl Estate, nordisk-pairing's Product, and FrontProduct on the front. Today it is lake-product/src/lake_box.rs, one in-process LakeBox per role. It runs the kinds its facts name (order_kinds: entitlement, add_principal, purge_tenant, grow_room) and refuses any other kind BY NAME in its ack, from those facts alone (A28 M23; a member joining is a target, no join_member kind exists). Its site is LakeSite: BuySite with Settings { product: "Lake", .. }. Today the Lake implements neither Product nor FrontProduct: its bench pairs its boxes with an in-process double (lake-product/src/installer.rs). nordisk-pairing can pair it since P3a (seed::Kind::Box(role): each box names its own kind, which is not elected). The Lake names its kind in its facts (box_kind = "lake", a box kind, never elected; T15, A28 H13).
The Lake's fast test is monetize's own cargo xtask fast --product lake [P3] (today the verb refuses lake and names cargo xtask bench lake), and its receipt adds the rows that only a parametric product can prove: a grow be: 3 → 4, a shrink refusal, an order with graph: true, a terminate that lists zero stores.
7. The data model: what is stored where
- The buy flows, step by step
- Where it lives: traits and crates
The picture is datamodel-v2.svg. Three stores hold the data of a sale, and each has one writer. Only four links cross between them: the tenant name, the paid sign-up that makes an account, the order that ties an account to a sold estate, and the first-login invite.
On the selling site (gunnar.rs, holger.rs). The product owns its people. Gunnar keeps them in one redb file, control.redb: the AuthDoc (refs/gunnar/auth) holds principals, passkeys (WebCredential), SSH keys, tokens, grants and invites; the catalog doc holds namespaces and repos; refs/gunnar/entitlements holds the signed fact per tenant. An account is a principal and the namespace of the same name (owns_account(pid, ns) is pid == ns); both are made in one call (ControlPlane::add_principal). A grant gives a principal a level (Read, Write, Admin) on the server, a namespace or a repo. An invite stores the hash of its secret, never the secret. The site keeps nothing of monetize but its ShopLink (the ONE box's URL, the site's bearer, product, vendor). The tenant name it uses at monetize is the account's namespace (Gunnar) or the account's repository (Holger) (monetize_site_buy::Buyer.tenant).
In monetize (the ONE box). monetize-admin's server writes every row, in redb files and one run file per sold estate. An ACCOUNT is the pair (product, tenant): its signed entitlement fact (ledger.redb entitlements), its usage (usage.redb), and its rows: one subscription, any number of purchases, any number of private instances (monetize.v1.Accounts). A purchase is append-only ledger rows under the reference <product>/<tenant>/<period>. A paid or free sign-up is a row in signups.redb keyed by the vendor's (vendor, session). A sold estate is estate-orders/<company>/run.json (EstateRun): who owns it (account), which site sold it (estate), its quote, its phase and what was laid. Its secrets are sealed apart, in custody/<company>.sealed. The fleet store (fleet.redb) pins each box's key and queues the orders the boxes pull. The operator console keeps its own passkeys and sessions in state.redb. Sites, vendor instances, plans and the release are the feed (solo.toml, the signed release manifest); they are configuration, not customer data.
On the sold instance (acme.gunnar.rs). The instance has its own store, with the same model as the selling site. Its first admin is the principal named EstateRun.account. The installer feeds the hash of the first-login invite (operator_invite_sha256), and Gunnar stores it as the invite operator-bootstrap (single use, short lived). The buyer redeems it from the "ready" mail (/login?invite=…&user=<account>) and registers the first passkey. Everyone else on the instance is invited by that admin. Neither the selling site nor monetize holds a row of the instance's people. Its boxes report and pull their orders. The ONE box never calls into them.
| Entity | Owner (writer) | Key fields | Lifetime |
| Principal | the selling site (Gunnar AuthDoc) |
id, name, kind, created | kept on the site; the ONE box may order its tenant purged (purge_tenant) after a lapse |
Passkey (WebCredential) |
the selling site | id, principal_id, public_key_hex, sign_count | with its principal |
| Account = namespace | the selling site | name (= principal id), repos, entitlement held | with its principal |
| Grant | the selling site | principal, scope (Server, Namespace, Repo), level | with its principal |
| Invite | the selling site or the sold instance | id, principal_id, secret_hash, created_by, expires | single use; spent once a passkey exists |
Site ([estates.<label>], SiteRecord) |
the operator (feed or console) | label, product, vendor, owner, domain, first_admin, terms | as long as it sells; gunnar.rs is never terminable unless it says terminable = true |
Account (product, tenant) |
monetize | EntitlementFact (plan, state, caps, paid_until, signature), usage |
kept; an unpaid account is purged purge_after_days (10) after its end |
Sign-up (SignupRecord) |
monetize | (vendor, session), username, email, plan, estate, card fingerprint | kept (it makes the webhook idempotent) |
| Purchase (ledger rows) | monetize | reference, steps (Reserved, Ensured, Captured, Settled, Released) | append-only, never deleted |
| Plan / offer | the operator (feed) | signup plans (amount, cap_bytes, period_days), bundles, prices | per release of the feed; [P3] from facts().form of the product operator (guest) |
Sold estate (EstateRun) |
monetize | company, reference, estate (site), account, phase, quote, paid_until, laid | the run file stays after terminate, phase terminated, as the record |
| Custody seal | monetize | first-login invite, signing and pairing seeds, break-glass, estate client seed, pairing material | forgotten at terminate or deletion |
Fleet (fleet.redb) |
monetize | box key, newest report, orders to ack, cases | a box is forgotten at terminate |
| Vendor instance | the operator (feed) | stripe/<instance>, guest digest, key NAMES |
per feed; key values stay in custody |
| Cloud resources | the cloud guest, for monetize | machines and disks under reference = estate/<company>, journal, spend cap |
destroyed at terminate, NO backup [P1: the backups too, F-T-01] |
| Release and feed | the operator (signed release) | solo.toml, release-manifest.toml, plugins by blake3, media |
per release |
| First admin and users of the sold instance | the sold instance | as on the selling site | gone with the instance at terminate |
What changes in v2.0: the sign-up guests are deleted and the entitlement is only a pulled order, never a push [P2]; the feed loses grants, shapes and the shop-pairing contracts [P2]; the quote carries the product's own Order JSON instead of the three Gunnar sizes, the plan offers come from facts().form, SoldEstate folds into Fleet (the front's five calls are Fleet.Ask kinds upgrade, recharge, material, update_take, update_report, signed as role = front by the estate's client key; the ONE box serves the old door for one release more), and custody also keeps the main's ticket_pubkey [P3]; terminate destroys the cloud backups with the estate [P1].
A visitor who buys a private instance gets two things.
- An account on the selling site, made with the payment. The page sends
EstateOrder.Startwithnew_account = true, the account name and the e-mail. When the vendor says PAID, the ONE box queues a signedadd_principalorder. The site's main pulls it and makes the principal and its namespace, and the sign-in link is mailed. The buyer registers a passkey there. A signed-in buyer sends an actor ticket instead, and no new account is made. - The instance itself,
<company>.gunnar.rs, with its own first admin. The instance is theEstateRunof that account. Its first admin has the same name as the site account, but it is a separate principal in a separate store, made by the first-login invite. The selling site's account sees the instance as oneinstancerow (its phase, its price, renew, terminate). It holds no rights inside the instance.
8. The buy flows, step by step
Every buy method has an id (B1 … B10), and every step of it has one (B3.4). The picture buyflows-v2.svg shows each method as one row of hops, in the lane of the party that acts. The tables below give, for each step: who calls whom, the relay route and the gRPC call it maps to, the state the order is in after it, the exact words a buyer or the log can show there, and the log lines it writes. The words are copied from the code on main (2026-10-25), with file:line; {…} is a value the code fills in.
Use it two ways. For design: read a method top to bottom. For an error: search this section for a phrase of the error or the log line, and its row names the step, the file and the line.
| Id | Method | Doors (relay) | gRPC on the ONE box |
| B1 | A free account (a card check, nothing charged) | /auth/signup/* |
Signup.Plans, CheckName, Start, Status, SendSignInAgain |
| B2 | A paid account (pay once, then the account is made) | /auth/signup/* |
as B1, plus the vendor's callback and Signup.Rename |
| B3 | A private instance as a visitor (new_account = true) |
/auth/private/start, /status |
EstateOrder.Ladder, Start, Status |
| B4 | A private instance, signed in (actor ticket) | as B3 | as B3 |
| B5 | Grow the account (a bundle) | /api/account/subscription/* |
Purchase.Bundles, Start, Status |
| B6 | Grow an instance (one step of the ladder) | /auth/private/change |
EstateOrder.ChangePlan |
| B6b | An instance's sign-in link again | /auth/private/sign-in-again |
EstateOrder.SendSignInAgain |
| B7 | Renew (a: an instance, b: the account) | /auth/private/renew, /api/account/subscription/renew |
EstateOrder.Renew, Purchase.Quote / Start{renew} |
| B8 | Cancel (a: an unpaid account order, 409 once paid; b: an instance's renewal; c: an unpaid instance order) | /api/account/subscription/cancel, /auth/private/cancel |
Purchase.Cancel, EstateOrder.Cancel, Settle |
| B9 | Terminate (no backup) | /auth/private/terminate |
EstateOrder.Terminate |
| B10 | Test orders (the operator's) | /api/test-purchase/start, the admin door /site/test-order |
TestPurchase.Start, EstateOrder.OrderSite{test_order} |
8.1 The files the tables cite
Every file is named once here with its path in nordisk/monetize. The tables use the short name on the left.
| short | path in nordisk/monetize |
| relay.rs | crates/monetize-site-buy/src/relay.rs |
| rules.rs | crates/monetize-site-buy/src/rules.rs |
| site.rs | crates/monetize-site-buy/src/site.rs |
| urls.rs | crates/monetize-site-buy/src/urls.rs |
| link.rs | crates/monetize-site-buy/src/link.rs |
| sb-purchase.rs | crates/monetize-site-buy/src/purchase.rs |
| buy.js / welcome.js | crates/monetize-site-buy/assets/{buy.js,welcome.js} |
| svc-signup.rs | crates/monetize-server/src/services/signup.rs |
| signup.rs | crates/monetize-server/src/signup.rs |
| svc-eo.rs | crates/monetize-server/src/services/estate_order.rs |
| eo.rs | crates/monetize-server/src/estate_order.rs |
| eo/account.rs, eo/term.rs, eo/change.rs, eo/terminate.rs, eo/sold.rs, eo/site.rs, eo/pages.rs | crates/monetize-server/src/estate_order/… |
| svc-purchase.rs | crates/monetize-server/src/services/purchase.rs |
| callback.rs | crates/monetize-server/src/callback.rs |
| returns.rs | crates/monetize-server/src/returns.rs |
| signup_pull.rs | crates/monetize-server/src/signup_pull.rs |
| test_purchase.rs | crates/monetize-server/src/test_purchase.rs |
| tx.rs | crates/monetize/src/transaction.rs |
| admin-door.rs | crates/monetize-admin/src/door.rs |
| pull.rs | crates/monetize-product/src/pull.rs |
| embed-fleet.rs | crates/monetize-embed/src/fleet.rs |
Log targets.
- Relay audit lines are
tracingevents on targetmonetize_site_buy::audit(relay.rs:55). Each has anevent = "…"field. - Server lines are
eprintln!("monetize-server: …"). - ALERTs have the form
monetize-server: ALERT <kind> <ref>: …. - functional_status rows have the form
skidbladnir::functional_status(<component>, <what>, ok, words).
8.2 The relay's routes (relay.rs router_of 1363–1406; the doc table at 8–34)
| group (Doors) | method + path | handler (relay.rs line) | gRPC on the ONE box |
| signup | GET /auth/signup/ladder[?market=] | signup_plans 146 | Signup.Plans |
| signup | GET /auth/signup/name?u= | signup_name 185 | Signup.CheckName |
| signup | POST /auth/signup/start | signup_start 229 | Signup.Start (minting channel) |
| signup | GET /auth/signup/status?session= | signup_status 363 | Signup.Status, then BuySite::signup_account |
| signup | POST /auth/signup/rename | signup_rename 393 | Signup.Rename |
| signup | POST /auth/signup/sign-in-again | signup_sign_in_again 434 | Signup.Status, then Signup.SendSignInAgain |
| private | GET /auth/private/ladder[?company=] | private_ladder 565 | EstateOrder.Ladder (+ EstateOrder.Sells via private_door) |
| private | POST /auth/private/start | private_start 602 | EstateOrder.Start (minting) (+ Sells) |
| private | POST /auth/private/change | private_change 670 | EstateOrder.ChangePlan (minting) (+ Sells) |
| private | GET /auth/private/status?company= | private_status 704 | EstateOrder.Status |
| private | POST /auth/private/renew | private_renew 784 | EstateOrder.Renew (minting) |
| private | POST /auth/private/cancel | private_stop_renewing 815 | EstateOrder.Cancel ("Stop renewing") |
| private | POST /auth/private/terminate | private_terminate 883 | EstateOrder.Terminate |
| private | POST /auth/private/restore-front | private_restore_front 839 | EstateOrder.RestoreFront |
| private | POST /auth/private/sign-in-again | private_sign_in_again 739 | EstateOrder.SendSignInAgain |
| private | GET /paid/estate/{company} | paid_estate 921 | none: 303 to {pages.private}&company=co (urls.rs:53) |
| private | GET /paid/estate/{company}/{change} | paid_estate_change 926 | none: 303 with &change=1 only for change-<n> (urls.rs:62) |
| account | GET /api/account/purchases | buy_purchases 1227 | EstateOrder.Purchases (+ Signup.Plans for the offer) |
| account | GET /api/account/subscription/bundles | buy_bundles 962 | Products.List (always, A32-9), Purchase.Bundles |
| account | GET /api/account/subscription | account_offer 1112 | Products.List, then Purchase.Quote{renew} |
| account | GET /api/account/subscription/history | account_history 1169 | Products.List, then Purchase.History |
| account | POST /api/account/subscription/cancel | buy_cancel 1202 | Purchase.Cancel |
| account | GET /api/account/subscription/status?reference= | buy_status 1050 | Purchase.Status |
| orders | POST /api/account/subscription/buy-bundle | buy_bundle 989 | Purchase.Bundles, then Purchase.Start{bundle} (minting) |
| orders | POST /api/account/subscription/renew | account_renew 1141 | Products.List, then Purchase.Start{renew} (minting) |
| offer | GET /auth/buy/offer | buy_offer 1249 | EstateOrder.Sells, Signup.Plans |
Routes outside the relay:
-
Vendor webhook.
POST /callback/{*vendor}on monetize-server (crates/monetize-server/src/lib.rs:282), handled at callback.rs:80. -
The site's own return pages.
/paid/<reference>and/cancel/<reference>belong to the site, not the relay (relay.rs:1420 test; returns.rs:14–18). -
Operator doors on monetize-admin. These are at admin-door.rs:869:
POST /site/order→ EstateOrder.OrderSite{test_order:false}POST /site/test-order→ OrderSite{test_order:true}POST /site/cancel→ EstateOrder.CancelPOST /site/terminate→ EstateOrder.TerminatePOST /site/settle→ EstateOrder.Settle
-
Operator UI. monetize-ui-server has
POST /api/test-purchase/start→ TestPurchase.Start andPOST /api/sites/order→ OrderSite withtest_order: false(crates/monetize-ui-server/src/serve.rs:57–62; site_order.rs:98). -
ONE shop's own pages. None are left:
/shop/signup*was deleted too (A33-12). The ONE box's own order pages/shop/private*and/kop*(and the/shopintake that linked to/kop) were deleted by owner decision OD6 (2026-10-26): buying happens on each product site's own sign-up. The edge answers them 404 (monetize-adminedge::RETIRED_PAGES).
Relay deadlines (link.rs:65–76):
- connect 3 s, call 10 s, mint 20 s
- buy-bundle adds MINT_GRACE 2 s
- Sells/Plans wait 2 s
8.3 How a refusal reaches the buyer
Private-instance, purchase and grow doors use from_status (relay.rs:524) / refused_call (relay.rs:942), which call rules::http_status_of (rules.rs:190–199):
| gRPC code | HTTP |
| InvalidArgument | 400 |
| AlreadyExists, FailedPrecondition | 409 |
| ResourceExhausted | 503 |
| NotFound | 404 |
| Unauthenticated, PermissionDenied | 403 (PermissionDenied since BUYFLOW-4, also on the sign-up doors) |
| anything else, including Unavailable, Unimplemented, Internal | 502 |
Sign-up doors have their own maps:
-
signup_start:- FailedPrecondition or InvalidArgument → 409, anything else → 502 (relay.rs:304–307).
- Unimplemented → fixed buy link, or 501
ACCOUNT_NOT_SOLD_HERE(relay.rs:291–300).
-
signup_rename: InvalidArgument or FailedPrecondition → 409, NotFound → 404, else 502 (relay.rs:409–413). -
signup_sign_in_again: ResourceExhausted → 429, NotFound or FailedPrecondition → 409, InvalidArgument → 400, else 502 (relay.rs:443–448). -
signup_status: any error → 502 (relay.rs:375). -
signup_plans: Unimplemented → 501; other errors → 503 with the last offer it holds (relay.rs:167–179).
What the buyer reads. rules::words_for_a_visitor (rules.rs:173–187) decides the error text:
-
PermissionDenied → WHO_NOT_CONFIRMED (rules.rs:159): "This page could not confirm who you are. Reload it and try again; if it happens again, sign in again." The audit event is
buy.refused_ticket(rules.rs:177). -
InvalidArgument, FailedPrecondition, AlreadyExists, NotFound, OutOfRange or Unauthenticated → the server's message verbatim.
- Exception: if the message contains "monetize", "one box", "ignalina", "appliance key", "[products.", "[estates." or "[estate_orders" (rules.rs:166), the buyer reads NOT_DONE_HERE (rules.rs:161): "This cannot be done here right now. Try again in a while." The audit event is
buy.refused_internal(rules.rs:181).
- Exception: if the message contains "monetize", "one box", "ignalina", "appliance key", "[products.", "[estates." or "[estate_orders" (rules.rs:166), the buyer reads NOT_DONE_HERE (rules.rs:161): "This cannot be done here right now. Try again in a while." The audit event is
-
Anything else → SHOP_DID_NOT_ANSWER (rules.rs:12): "The shop did not answer. Try again in a minute."
Fixed relay refusals:
- Mint timeout → 504 START_TOO_SLOW (relay.rs:62): "The payment page did not open in time. Nothing has been charged — please try again in a minute."
- Bad pay URL → 502 PAY_URL_REFUSED (rules.rs:220): "the payment address is not one this console will send a browser to".
- Every refusal body is
{"error": "<sentence>"}(relay.rs:110–112).
Cancel of a paid order = 409. Purchase.Cancel returns FAILED_PRECONDITION "{ref} has been paid, so it cannot be cancelled: it finishes as paid, and ending it now is a refund (Abandon)" (svc-purchase.rs:2030–2033). refused_call maps that to 409 (test: crates/monetize-site-buy/tests/a16_cancel_door.rs:100–105).
monetize-admin door map (admin-door.rs:223–233):
- InvalidArgument 400, NotFound 404, AlreadyExists/FailedPrecondition 409
- PermissionDenied/Unauthenticated 403, Unavailable 503, else 502
- body
{"refused":"<Code>","words":"…"}
8.4 B1 – Free account sign-up (the free row: card check, nothing charged)
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B1.1 | browser → site | GET /auth/buy/offer → EstateOrder.Sells, Signup.Plans | — | — | — |
| B1.2 | browser → relay → box | GET /auth/signup/ladder → Signup.Plans | — | 429 "too many requests from here; wait a moment" (relay.rs:152); 501 "An account is not sold here right now." (relay.rs:60,169); 503 "The shop is unavailable right now — try again in a few minutes." (relay.rs:58,179); box: "Signup.Plans names no product: one monetize sells several (gunnar, holger), and a call says which" (svc-signup.rs:900); "no signup sink is loaded for {product}: …" (svc-signup.rs:907) | relay signup.plans_failed (relay.rs:168,172) |
| B1.3 | buy.js → relay → box | GET /auth/signup/name?u= → Signup.CheckName | — | 200 {free:false, reason:"a username is 2 to 32 characters of a-z, 0-9, '.', '_' or '-', starting with a letter or a digit"} (relay.rs:188; rules.rs:9); 429 "too many names checked from here; wait a moment" (relay.rs:192); box taken: "username {name:?} is taken; choose another" (signup_pull.rs:112, pulled product); sink down → Unavailable SHOP_DOWN "The shop is temporarily down for maintenance. Please try again in a little while." (signup.rs:138; svc-signup.rs:923) → 502; buy.js: "Choose another account name." (buy.js:612) |
relay signup.name_rate_limited (relay.rs:191), signup.name_failed (relay.rs:200); box monetize-server: signup name check unavailable: {why} plus functional_status ("monetize-server/signup","name check") (svc-signup.rs:95–96) |
| B1.4 | buy.js → relay → box | POST /auth/signup/start {username,email,plan:'@@FREE_ACCOUNT@@',next:'/'} (buy.js:622) → Signup.Start | card check minted. Store holds CardCheckStarted under (vendor, cs_…) (svc-signup.rs:607) and a name hold of 35 min (signup.rs:87; svc-signup.rs:608). No SignupRecord yet. | relay: 403 "{line} A member buys nothing: {account}'s room is its admin's to buy." (relay.rs:224,231); 400 username rule (relay.rs:237); 400 "an e-mail address is required: it is where your welcome mail and receipt go" / "that does not look like an e-mail address (name@example.com)" / "the two e-mail addresses do not match" (rules.rs:146,149,152); 400 "that is not a plan this console can ask for" (relay.rs:246); 400 market "X" is not one this estate sells in (SE, FI, DK) (rules.rs:52); 429 "too many signup attempts from here; wait a moment" (relay.rs:254); 504 START_TOO_SLOW (relay.rs:276); 502 "the checkout address is not one this console will send a browser to" (relay.rs:286). Box (→409): name refusal (svc-signup.rs:1067); "This email address already has an account here. Sign in to it instead — nothing has been charged." (signup.rs:142; svc-signup.rs:1083); "Someone is signing up with this account name right now. Choose another name, or try again in half an hour." (signup.rs:153; svc-signup.rs:1092); "A free account cannot be made here right now. Nothing has been charged." (signup.rs:151; svc-signup.rs:555,614,619); plan {id:?} has room and no price: … (svc-signup.rs:577); "REFUSED [signup-other-site] the {which} page {u:?} is not on this estate's site {site}: a signup returns where it began" (svc-signup.rs:1001); "REFUSED [estate-unnamed] …" (PermissionDenied → 502 + WHO_NOT_CONFIRMED) (svc-signup.rs:1054); vendor Unavailable → SHOP_DOWN (svc-signup.rs:616) |
relay signup.start_rate_limited (relay.rs:253), signup.start_timed_out (275), signup.start_bad_url (285), signup.start (288), signup.start_failed (303), signup.start_fallback (294), signup.not_sold (298). Box: "monetize-server: a card check for a free {product} account {:?} was minted at {vendor} ({session})" (svc-signup.rs:609); "…vendor {:?} checks no card (or named no session): no free {product} account is made without one" (613); "…vendor {:?} would not mint a card check: {e}" (618); "…a free {product} account was asked for and its vendor {route:?} is not loaded: no card check, no account" (554); one line on every return: "monetize-server: signup start {user:?}: {outcome} in {ms} ms — {stages}[ — SLOW: past the 5 s a signup may take (budget FAILED)]" with outcome "card check" or "account" (svc-signup.rs:960–968, 1133,1141) |
| B1.4b | box (no vendor routed: open build) | same call → start_account | SignupRecord Created under vendor account, session acct_<32hex> (signup.rs:388,407–411). The URL returned is the site's own /welcome. |
"Payment received, but your account could not be created automatically. Please contact us …" (signup.rs:134; svc-signup.rs:302) | "monetize-server: an account at the shop was made for {:?} (account)" (svc-signup.rs:349); sign-in mail line (800/801) |
| B1.5 | browser → Stripe (mode=setup) → back | — (the success URL is {origin}/welcome?session={session}[&next=], urls.rs:28) |
card checked at Stripe | buy.js: "Your account could not be made. Please try again." (buy.js:627) | — |
| B1.6 | welcome.js → relay → box (every 2 s; after 10 min every 60 s, welcome.js:16,24–25) | GET /auth/signup/status?session= → Signup.Status → finish_card_check (svc-signup.rs:1397, 709) | Open → PENDING. Expired → REFUSED (Final). Checked → make_account → SignupRecord Created (svc-signup.rs:777). | PENDING words "Your card is being checked. Nothing is charged." (signup.rs:149); REFUSED "The card check was not finished, so no account was made. Nothing has been charged — start again to make your free account." (signup.rs:147); "This card already has a free account here — sign in with it, or choose a plan. Nothing has been charged." (signup.rs:145); "{why} Nothing has been charged — start again with another name." (svc-signup.rs:783); vendor unreadable → Unavailable SHOP_DOWN → relay 502 (svc-signup.rs:722–742); relay 400 "that is not a checkout session this console can ask about" (relay.rs:64,365); 429 "too many signup lookups from here; wait a moment" (relay.rs:369); welcome.js: 404 "This console does not do that" / "paid signup is not offered here." (204); 400 "That link is not one of ours" (210); !ok "Setting up your account …" / "@@PRODUCT@@ is not answering yet. Try again in a moment." (221); unknown for 30 s → "This session is not known to us" / "Nothing here matches that address. If you paid, we will find it." (291–292); pending "Waiting for Stripe to confirm your card." (296); refused "We could not finish this" (282); after 10 min "Still setting up" / "This is taking longer than usual. …" (371–372) | box "card check {vendor}:{session}: the vendor said {e}" (729); "card check …: the card already has a free {} account here ({held}); no second one" (751); "an account at the shop was made for {:?} (account, card checked)" (349); "the sign-in link for {username:?} was mailed: {sent}" / "…was NOT mailed to {masked} ({e:?}); the account is made" (800–801); functional_status ("monetize-server/signup","card check") on unavailable (95); relay signup.not_created (relay.rs:337) |
| B1.7 | box → mail | — | mail kind account.sign_in, subject "Your sign-in link for https://{host}" (svc-signup.rs:818–819) |
— | as above |
| B1.8 | relay → site (passkey) | (status created) → BuySite::signup_account | relay answers invite_url /login?signup=<s>&user=<u> (urls.rs:73) |
"that order is for {:?} and this console is {product}'s" (relay.rs:346); "this signup's registration link has already been used" (relay.rs:354); the site's own Refusal, e.g. 409 while the product has no such person yet (site.rs:164–166) | relay signup.link (relay.rs:351), signup.link_failed (relay.rs:356) |
| B1.9 | welcome → relay → box | POST /auth/signup/sign-in-again → Signup.Status + Signup.SendSignInAgain | — | relay 409 "This account is not made yet: its sign-in link is mailed once it is." (relay.rs:457), "This account already has a security key: sign in with it." (relay.rs:461); box "no {product} signup {session:?} here" (svc-signup.rs:842 →409); "It was sent a moment ago. Look in your inbox (and spam), or try again in a minute." (869 → 429); "The mail could not be sent right now. Try again in a while." (881 → 502); OK words "We’ve sent it again to {masked}." (877) | box "the sign-in link for {username:?} was mailed again: {sent}" (876) / "…was NOT mailed again …" (880) |
8.5 B2 – Paid account (plan row; pay once, account made after the payment)
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B2.1–2.2 | as B1.2–B1.3 | ||||
| B2.3 | buy.js → relay → box | POST /auth/signup/start {plan: step.id} (buy.js:622–624) → Signup.Start | Checkout minted. record_minted, record_welcome and a name hold of 35 min (svc-signup.rs:1305–1308). Oversell slot held. |
as B1.4, plus: "The price of this plan changed while the page was open. Look again: nothing has been charged." (svc-signup.rs:1114); plan {:?} is {of}'s, and this checkout is for {product} (1109); plan {asked:?} is not sold here; [signups.<product>.plans] names {known:?} (118) → the buyer reads it verbatim: the internal-word filter (rules.rs:166) does not list "[signups." (see 8.15); "this deployment sells {n} signup plans and the request named none; …" (125); NO_ROOM "We cannot create an account of this size here right now. Nothing has been charged. Please ask us and we will set it right, or choose a smaller size." (signup.rs:190; svc-signup.rs:1201,1240) → Unavailable → 502 SHOP_DID_NOT_ANSWER at the relay; Unimplemented "no vendor here can sell a {product} signup at …" (1263) / "vendor {:?} minted a checkout with no session id: …" (1300) / "vendor {:?} mints no signup checkout; …" (1317) → relay fixed link or 501; "vendor: {e}" (1322 → 409) |
relay as B1.4; box timing line, outcome "minted" (svc-signup.rs:1309); queued past the wall: oversell queue_note line (1236); functional_status ("monetize-server/signup","capacity"/"checkout") (95) |
| B2.4 | browser → Stripe checkout → /welcome?session=cs_… | — | — | buy.js "Taking you to pay …" (buy.js:633) | — |
| B2.5 | Stripe → box webhook | POST /callback/stripe (lib.rs:282) | SignupRecord Pending, then settled behind the answer (callback.rs:256–285) | 413 "callback body too large" (callback.rs:84); 503 "busy; retry" (88); 404 "no vendor {vendor:?} is linked into this server" (188); 401 "callback did not verify" (200); 503 "temporarily unavailable; retry" (218); 400 "callback not understood: {e}" (310); 500 "internal error" (105); not minted here → 200 ignored "signup {v}:{s} was not minted by this monetize — another system's checkout on the same account; not settled here" (247) | "monetize-server: {vendor} callback REFUSED ({kind}) #{seq} — {detail} [recorded; the console reads CallbackAdmin.ListRefusals]" (callback.rs:177); "{vendor} callback IGNORED: {why}" (249); "callback signup {v}:{s} settled behind the answer: {PENDING/CREATED/REFUSED}" (266); "callback signup …: kept and answered PENDING within {ms} ms; the account is made behind the answer" (277); functional_status ("monetize-server/callback", vendor, …) (175, 209, 217, 248) |
| B2.6 | box settle → sink | (signup::settle, signup.rs:1683) | Created {namespace, username}, Refused {Name/Final}, or Pending (sink Unavailable; refused after the 24 h PENDING_CEILING, signup.rs:107) | Refused (checks): plan {:?} is not sold here; …, "paid {} ({} of it tax, {} net), but plan {:?} costs {} here" (signup.rs:1257–1270); sink Refused → customer reads COULD_NOT_CREATE (signup.rs:134); NameTaken → renameable; NEVER_ANSWERED "sink never answered: {why}" (signup.rs:112) |
"signup {v}:{s} could not be recorded: {e}" (1696); "signup {v}:{s} stays pending: {why}" + functional_status (1760–1761); "…was decided {next:?} and the store refused it: {e}" (1774); "…has no welcome page to mail" (1790); functional_status line "{v}:{s} CREATED {user} in {ns}" / "{v}:{s} REFUSED {reason}" (1795–1797); the room is queued to the grow batch (grow/batch.rs:807–815, "grow-batch {product}: request {id} was NOT queued: {why}"); sign-in mail (svc-signup.rs:800) |
| B2.6p | (pulled product, e.g. Holger) box queues → estate main pulls add_principal |
Fleet.Orders (pull.rs:37) | stays Pending (sink Unavailable "queued: …"). The main acks: ok → Created and the entitlement order is queued (pull.rs:24); taken… → Refused Name |
"queued: {instance}'s main creates {:?} when it next calls monetize" (signup_pull.rs:82); "username {:?} is taken on {instance}; choose another — the money is already at the vendor and nothing was refunded automatically ({why})" (92–95); "{instance}'s main refused the account {:?}: {why}" (96) | as B2.6 (stays-pending line) |
| B2.7 | welcome.js → relay → box (2 s poll) | GET /auth/signup/status → Signup.Status (svc-signup.rs:1387): finish_paid_signup asks the vendor when no row exists yet (640) | PENDING / CREATED / REFUSED / UNKNOWN | PENDING words (signup::pending_words, PAID-WORDS): "Payment received. Setting up your account; this takes a few seconds." (also while a pulled queue or any "not now" waits); past SETTLE_BOUND_MS (2 min) "…This is taking longer than usual; your payment is safe…"; "…temporarily down for maintenance…" ONLY when the product says so by name (SignupError::Maintenance, RA4-20); vendor open: "Waiting for your payment to be confirmed. Nothing more is needed from you." (155); closed unpaid → REFUSED "The payment was not finished, so no account was made. Nothing has been charged — start again to sign up." (157); CREATED words "Payment received. Your account is ready." (131); welcome.js as B1.6, plus pending fallback "The payment has reached us. Making the account now." (306), refused+renameable "Choose another username" (275) |
"signup {v}:{s} read PAID at the vendor by its return page: {decided:?}" (svc-signup.rs:658); "…the vendor could not say where it stands ({e}); the webhook decides" (671); relay signup.not_created (337) |
| B2.8 | welcome → relay → box | POST /auth/signup/rename → Signup.Rename | Refused(Name) → Created / Refused | "no signup here has that checkout session" (svc-signup.rs:1535 → 404); NotRenameable why (1536 → 409); new-name refusal (1537 → 409); Unavailable → MAINTENANCE (1539 → 502); relay 400 username / NOT_A_SESSION (relay.rs:396–399); 429 (403); welcome.js "Use a-z, 0-9, dot, underscore or dash — 2 to 32 characters, starting with a letter or a digit." (317), "@@PRODUCT@@ is not answering. Try again in a moment." (348) | relay signup.rename_rate_limited (402), signup.rename_failed (408), signup.renamed (420); box "{v}:{s} RENAMED {:?} -> {user} REFUSED {reason}" (signup.rs:1990) |
| B2.9 | as B1.8–B1.9 |
8.6 B3 – Private instance as a visitor (new_account = true, "pay once")
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B3.1 | browser → relay → box | GET /auth/private/ladder → (Sells) EstateOrder.Ladder | — | 429 (relay.rs:535); 404 "A private {word} is not sold here." (538); Ladder NotSold "A private {title} is not sold here right now." (svc-eo.rs:77–79,109); per-step reason comes from the quote error (eo/ladder.rs:166); buy.js "Nothing is on sale here right now." (buy.js:560) |
box "monetize-server: estate order: not sold here: {w}" (svc-eo.rs:108); relay private.rate_limited (relay.rs:534) |
| B3.2 | buy.js → relay → box | POST /auth/private/start {company,email,account,domain,sizes} (buy.js:649–651) → EstateOrder.Start{new_account:true, actor_ticket:[], welcome_url = {origin}/welcome?session={session}&next=<private>&company=co (urls.rs:42), cancel_url = {origin}{pages.private} (urls.rs:48)} |
EstateRun phase ordered. Customer state waiting_for_payment "Waiting for your payment on the payment page." (eo.rs:969). Name held under ACCOUNT_VENDOR (svc-signup.rs:495–499). History "{shown} ordered at {price}; the hold of {amount} is asked for" (eo.rs:3258) |
relay 400: "which instance? a company name: lowercase letters and digits, a letter first, at most 16" (relay.rs:605); "that email address is too long" / "that domain is too long" / "which size? that plan is not one sold here" (499–505); "which account? the name your new account is to have" (619); "the e-mail address your new account's sign-in link goes to" (622); 504 START_TOO_SLOW (655); 502 PAY_URL_REFUSED (660). Box, NewAccount checks: "the account name: an account on this site, without @/" (svc-eo.rs:167 → 400); "the cancel page is an https:// address" (svc-signup.rs:396 → 400); "An account cannot be made with this order here. Sign in to your account, or create it first." (375,421 → 409); "the welcome page is an https:// address with {session} in it" (433 → 400); name refusal (439 → 409); email refusal (443 → 400); EMAIL_HAS_ACCOUNT (447 → 409); NAME_BEING_BOUGHT (452 → 409); no_sink (Unimplemented → 502 SHOP_DID_NOT_ANSWER). Box, start_with_on: a company name is 1 to 16 characters; {name:?} is {n} / "a company name starts with a lowercase letter: …" / "…lowercase letters and digits only: …" / "{name}" is reserved / "…is reserved for the test estates" (monetize-cloud/src/lay.rs:445–462 → 400); "{host} is taken" (eo.rs:3096,3134,3151 → AlreadyExists 409); "{domain} is already ordered" (3138 → 409); the account name {account:?} is one the new {title} keeps for itself … (2769 → 400); "an order needs the address its "ready" mail goes to" (3111); "a domain of your own cannot be used here yet: leave it empty, and your {title} is {host}" (3120); NotSold "this shop has no way to install a private {title} yet ([estate_orders] …)" / NO_DNS_WRITER (2116–2123, 3155, 3162) → buyer reads "A private {title} is not sold here right now." (svc-eo.rs:109); Price → "This size is not offered right now." (svc-eo.rs:99,116 → 409); Vendor "the vendor {:?} is not linked here" (eo.rs:3178) / "the vendor refused the hold: {e}" (3214) → Unavailable → 502 SHOP_DID_NOT_ANSWER; Busy "{host} is being worked on right now; ask again in a minute — nothing was held or changed" (eo.rs:2620 → 502) |
box "monetize-server: estate order estate/{co}: ordered" (eo.rs:3317); on a refused start after the page opened: "ALERT estate-orders.page {ref}: the order was refused ({e}) and its payment page {v}:{id} is NOT closed ({…}); close it at the vendor" (eo.rs:3226); nudge "estate orders: the tick an answer nudged is still running after 2 s; answered, it finishes behind" (eo.rs:3387); relay private.start (relay.rs:662), private.monetize_failed (527) |
| B3.2r | same visitor retries (F-B-01) | same | the same unpaid ORDERED run is answered (eo.rs:3131–3132, 3327–3350) | — | — |
| B3.3 | browser → Stripe checkout | — (reference estate/<co>; return per the vendor's config, see 8.15) |
— | buy.js "The payment page did not open. Nothing has been charged — please try again." (buy.js:666) | — |
| B3.4 | box tick → vendor | (driver, eo.rs:3806–3835) | ordered → held on capture ("paid: {amt} captured ({ref}); the estate's secrets are made and sealed; queued", eo.rs:4935). Unpaid at order_ttl → released "the payment page expired unpaid; nothing was held" (3823); declined → released "the payment was not authorised; nothing was held" (3833) |
park whys: "waiting for the buyer's payment" (3830); "the capture did not go through yet: {e}" (3812); "the vendor did not answer: {e}" (3834); "paid, and the custody refused: {e}" (4936) | "estate order {ref}: ordered → held" (eo.rs:3710); "…→ failed: {why}" (3726) |
| B3.5 | box (in paid()) → sink: makes the visitor's account |
make_paid_order_account (svc-signup.rs:514) | SignupRecord Created under ACCOUNT_VENDOR with the order's session. Until then the run parks in ordered, captured |
"ALERT estate-orders.account-not-made: paid, and its account {:?} could not be made ({why}); an operator sets it right — nothing is laid for another's account" (svc-signup.rs:537); "ALERT estate-orders.account-not-made: paid, and the sink refused its account {:?}; …" (538); "paid; its account {:?} is not made yet: {msg}" (539) | printed as "monetize-server: estate order {ref}: {why}" (eo.rs:4931); "an account at the shop was made for …" (svc-signup.rs:349); sign-in mail account.sign_in with the welcome_url (800) |
| B3.6 | box tick: queue / own-domain DNS | — | held → waiting_dns (own domain; "waiting for the customer's records at {domain}", eo.rs:3838) → held → laying ("a slot is free; laying", 4075) | customer state queued "Your {t} is paid and being prepared." (eo.rs:970); waiting_dns "{…} Add the two records below at the DNS provider of {domain}; we look every minute." (972–975); fail "the records for {domain} were not in place within {h} hours" (3842); queue whys "queued: {n} estate(s) are being laid" (3862), "queued behind {host}, ordered first" (3870), "orders wait: raise max_estates: …" (4052), "orders wait: raise the cap: …" (944, 4059, 4077) |
"estate order {ref}: queued: waits for the cap or a slot ({words})" (eo.rs:4118); "ALERT estate-orders.queue {ref}: {words}" (4130, at most hourly); functional_status ("monetize-server/estate-orders","queue") (4132) |
| B3.7 | box → cloud: lay | — | laying → installing ("{n} server(s) and {d} disk(s) laid; {dns line}", eo.rs:3948); DNS records written | Fail: "the lay was refused: {e}" (4038); "the cloud {:?} is not linked" (3979); re-check price sentence (4007); Park: "the cloud is unavailable; laying again next tick: {w}" (4034); "the DNS records were not written: {e}" (3934); back to held "orders wait: raise the cap: …" (4002, 4037) | "ALERT estate-orders.dns-takeover …" for site orders only (3970) |
| B3.8 | box → installer: install | — | installing → paired ("the installer's verdict is PASS…", eo.rs:4328) | Park "the estate's DNS records do not answer yet: …" (1160, 4199); Fail "the installer's verdict is {v}: {detail}" (4296); "the installer: {e}" (4298); "the custody could not make the buyer's first-login invite: {e}" (4243); deadline "the install did not finish within {m} minutes ([estate_orders] install_deadline_mins) without every box PAIRED: …" (4800, 4864) | "ALERT estate-orders.install-deadline {ref}: {why}" (4792, 4844–4846, 3897); "ALERT estate-orders.guardian {ref}: installed (PASS), but the guardian was NOT told of the estate yet: …" (4303, 4321); customer state installing "{t} is being installed on your machines." (977) |
| B3.9 | box ← boxes pair / call home (Fleet.Report), then sale check | — | stays paired until all three capture steps pass | whys "held at step 1 of 3 (the call home): …", "held at step 2 of 3 (the main's word that it serves): …" (4360–4364, 4395–4406); "the sale check over https://{host} did not pass: {why}; nothing is captured" (2050, 4461), asked every 30 s (2047); "the estate did not answer within {s} s of its lay" (3792 → failed) | — |
| B3.10 | box: capture (already paid at the order) | — | captured, paid_until = now + term ("the estate called home …; paid at the order (…), for … from now: paid until …", eo.rs:4419–4427) | — | "estate order {ref}: paired → captured" (3710) |
| B3.11 | box → mail: ready mail | tell() (eo.rs:5149) | told = true | subject "Thank you — your {title} is ready: https://{host}" (kind estate.ready) with the first-login link https://{host}/login?invite=…&user=… (5136); waits without the invite: history "estate.ready mail NOT sent: the first-login invite could not be read from the custody; asked again within the hour" (5163) |
"ALERT estate-orders.ready {ref}: the first-login invite could not be read; the ready mail waits" (5164); "ALERT estate-orders.ready {ref}: the {kind} mail was not sent ({e:?}); asked again within the hour" (5246); history "{kind} mail sent: …" |
| B3.12 | buy.js watch → relay → box (every 3 s until ready/failed, buy.js:711) | GET /auth/private/status?company= → EstateOrder.Status (a visitor has no ticket: state + words only, svc-eo.rs:232–233) | ready: "Your {t} is ready at https://{host}; it is paid until {date}. Your first sign-in link was mailed to you." (eo.rs:980–987) plus " The sign-in link for your new account is in your mail: open it to register your passkey, then sign in." (svc-eo.rs:70,228) | "which estate? ?company=" (relay.rs:707); NotFound "no order for {co:?}" (svc-eo.rs:219 → 404); buy.js "no answer yet" (buy.js:703) | — |
| B3.13 | failure path: box give-back | — | unpaid: failed → released ("the hold is released", eo.rs:4507); paid: failed → held (retry 1 of 1, eo.rs:4498) → owed | customer: "Your {t} could not be set up. Nothing will be charged: the hold is being released." (1000); paid/owed: "Your {t} is paid. Setting it up takes longer than usual; we are on it, and you get a mail when it runs." (998); mail estate.failed "Your {name} {host} could not be set up — nothing was charged" (5232–5236) |
"ALERT estate-orders.owed {ref}: paid, failed {n} time(s), last: {why}; retry or cancel with a refund" (4501); functional_status ("…/estate-orders","owed") (4502) |
8.7 B4 – Private instance, signed in (actor ticket)
Same as B3 except for the following rows.
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B4.1 | relay: who + ticket | (inside POST /auth/private/start) BuySite::who, ticket(PURPOSE_ESTATE) | — | 401 "A private {word} is bought by your account at {host}, where you see and grow what you bought. Sign in, or Create your account at {host} first." (relay.rs:516–518,548; only on the signed-in doors, because a visitor at /start takes B3); 403 "{line} Members don’t buy: {buys} buys for {account}." (552); the site's ticket Refusal (554) | — |
| B4.2 | relay → box | POST /auth/private/start → EstateOrder.Start{account, actor_ticket, email:"", cancel_url} (relay.rs:642–651) | ordered (OrderedBy::Shop when cancel_url is set, svc-eo.rs:198–203) | Unauthenticated → 403 "a private {title} is bought, read and changed by its {home }account: sign in to that account and ask again" (eo/account.rs:30–33; svc-eo.rs:133); Unavailable "the account cannot be checked right now: {why}" (svc-eo.rs:134 → 502); "This account has no e-mail address here for the order's mail. Contact us, and nothing has been charged." (svc-signup.rs:472,489 → 409); otherwise as B3.2 start_with_on | "an estate call for account {account:?} was refused: {why}" (eo/account.rs:51); "estate order …: ordered" (eo.rs:3317); relay private.start |
| B4.3–B4.12 | as B3.3–B3.13, without B3.5 (no account is made) | Status carries the ticket → whole answer (paid_until, records, change_status) (svc-eo.rs:235–264) | same phases | owner_of refusals: "an estate call for {co:?} was refused: no such order" (eo/account.rs:121); "…from {caller} was refused: it was bought on {bought_on:?}" (127) → both 403 with NOT_PROVEN words | — |
8.8 B5 – Grow the account (bundle; caps)
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B5.1 | buy.js → relay → box | GET /api/account/subscription/bundles → (Products.List) + Purchase.Bundles with a read ticket (relay.rs:950–958) |
— | 401 "Sign in to the account that buys first." (relay.rs:938); 403 member (935); 501 "this appliance's monetize does not verify tenant renewal tickets, so a subscription cannot be renewed from here. An operator can renew it." (1078, every site since A32-9); box "tenant is required" (svc-purchase.rs:1605); check_actor "no actor ticket, and this request did not come through the operator's door: an empty ticket is nobody" (1234 → PermissionDenied → 502 + WHO_NOT_CONFIRMED); "{product} cannot be asked who you are right now, so this request cannot be authorised: {why}" (1257). While reading: relay window pending "Awaiting the cloud provider…" / late "The cloud provider has not answered yet. The sizes on offer appear here as soon as it does." (sb-purchase.rs:115–117); buy.js re-asks every 3 s (buy.js:539); "None of the sizes on offer can be added to this account right now." (sb-purchase.rs:119) |
box "the shop window for {p}/{t} is still being read …; answered reading, the console asks again" (svc-purchase.rs:1621–1625); "bundle not offered for {p}: {why}" (987); relay info "a bundle monetize cannot serve is left out of the window" (sb-purchase.rs:133) |
| B5.2 | buy.js → relay → box | POST /api/account/subscription/buy-bundle {bundle} → held_caps, Purchase.Bundles again, ticket order with the target caps (or renew for a renewal row), then Purchase.Start{bundle} (relay.rs:989–1045) |
Ledger Reserved row (attempt open). PurchaseState Pending, phase waiting_for_payment (svc-purchase.rs:1377) |
relay 400 "that is not something this account can order right now. Reload the page for what is available." (relay.rs:1010); 504 (1037); 502 PAY_URL_REFUSED (1042). Box: {bundle:?} is not a bundle {p} offers; ask Purchase.Bundles for the ones it does (1090 → 409); {bundle:?} is not on sale here (1096 → 409); "a request names a bundle OR caps, never both. …" (1073 → 400); "a renewal names no bundle: …" (1685 → 400); free product "nothing is sold here: {p} is served free on this deployment …" (900 → 409); quote refusals → FAILED_PRECONDITION with the operator's text (tx.rs:387–436, e.g. "nothing to sell {tenant}: the order raises no cap above what they already have", "the product cannot deliver this to {tenant}: {reason}", "the sale to {tenant} at {price} is under the floor …", "the plan {tenant} asked for is not sold: {why}") → 409; customer words ride in metadata monetize-customer-words-bin (svc-purchase.rs:871–883) but this door shows the message (see 8.15); no vendor → NotFound "this order costs {price} in {market:?}; {said} (asked: {asked:?}), and no vendor here takes that money; serving: {…}" (1127, 1759 → 404); "vendor: {e}" Unavailable (619 → 502) |
"purchase refused for {p}/{t}: {refusal}" (1746); queued past the room: oversell queue_note / grow queue_past_room line (1766–1773), which queues a grow_room order for a pulled product's main (pull.rs:51; monetize-guardian/src/pulled.rs:271); "bundle {:?} was ordered and is not offered for {p}: {why}" (1095) |
| B5.3 | browser → Stripe → site /paid/<reference> |
— (site page; is_relay_path excludes /paid/<ref>, relay.rs:1420) |
— | buy.js "The payment page did not open. Nothing has been charged — please try again." (666); a no-URL answer shows j.words/j.reason (664) |
— |
| B5.4 | Stripe → box webhook | POST /callback/stripe → ledger attempt → inbox → grant (callback.rs:313–389) | ledger Captured (+ Settled), then EntitlementPushed → attempt closed Pushed: PurchaseState Paid, phase done; while captured but not pushed: paid_adding (svc-purchase.rs:1376) |
200 "no open attempt for {ref}" (315); 409 "{ref} was reserved as {v}:{id}, callback names {v}:{id}" (326–331, HandleMismatch row); duplicate → {…,"duplicate":true} (348) |
"{vendor} callback {id} for {ref} (delivery {n}): kept; granting behind the answer" (357); "…delivered again …: already {word} — answered from the kept row, nothing moves" (344); "…its grant is in flight — answered at once, nothing started" (351); "callback grant {v}:{id} {ref} attempt {a}/{t}: {said}" (inbox.rs:343, 356); functional_status ("monetize-server/callback", v, …) "{ref} -> {state} {reason}" (callback.rs:371) |
| B5.5 | site page → relay → box | GET /api/account/subscription/status?reference= → Purchase.Status with a read ticket (asks the vendor when due) |
Pending / Paid / Released / Declined / Unknown; reason "reserved, awaiting the vendor" or "captured, entitlement not yet pushed" (svc-purchase.rs:1842) | "this tenant has no purchase by that reference" (1285, 1336 → 404); "an unscoped read of any order is the operator's, through the bearer's door" (1312) | "status {ref}: the vendor was asked while the customer watched: {state:?}" (1828); "status {ref}: asking the vendor: {msg}" (1830) |
| B5.6 | box → product | entitlement push (EntitlementPushed). For a pulled product: an entitlement order the main pulls (pull.rs:24) |
granted. Words ORDER_GRANTED "Your account's limit is raised now — the extra room is already yours to use. There is nothing to set up and nothing to wait for." (tx.rs:549–550) | — | — |
8.9 B6 – Grow an instance (ChangePlan; one step of the ladder)
| # | who → whom | relay route → gRPC | state after (PlanChange.status, eo/change.rs:41–46) | error words (file:line) | log events (file:line) |
| B6.1 | buy.js → relay → box | GET /auth/private/ladder?company= (with the estate ticket) → EstateOrder.Ladder | current, floor |
"which instance? ?company=" (relay.rs:570); NOT_PROVEN 403; "no order for {co:?}" (eo/ladder.rs:208) | — |
| B6.2 | buy.js → relay → box | POST /auth/private/change → EstateOrder.ChangePlan (minting) (relay.rs:670–699) | price difference > 0: waiting_for_payment (hold opened, reference estate/<co>/change-N, change.rs:250); 0: queued (no URL; buy.js shows words, buy.js:659) |
relay "which instance? a company name" (673); 504 (688); 502 PAY_URL_REFUSED (693). Box: "there is no estate {host}" (change.rs:325); "{host} is {phase}: only a delivered, paid estate changes plans" (327); "a plan change is already {status} ({ref})" (330); "data never shrinks: main's data disks are {a} GB each now, and {b} GB was asked" (215–218 → 400); the product's grow rule refuses a machine added or taken away, or one whose data its estate does not move, by name ("… nothing was held"; A4-FACTS); "the new numbers grow the data disks of …, and this cloud grows no disk" (a cloud with no grow path); "nothing to change: front and main are already on those plans and main's data at that size" (233) → all NotNow → 409; Price "the provider's plan list: {e}" (239) → "This size is not offered right now." (409); "the vendor refused the hold: {e}" (280 → 502) | history "plan change {ref} opened: {words}" (change.rs:307); relay private.change (relay.rs:695) |
| B6.3 | browser → Stripe → /paid/estate/<co>/change-N → relay 303 → {private}&company=co&change=1 |
GET /paid/estate/{co}/{change} (relay.rs:926; urls.rs:62) | — | buy.js "Payment received. Your change is being made: the machines restart for a few minutes while it is done, and come back by themselves." (buy.js:726) | — |
| B6.4 | box term tick (change::advance) → vendor | — | waiting_for_payment → queued (Reserved) | why "waiting for the payment" (407); fail "the payment page expired unpaid; nothing was changed" (398) / "the payment was not authorised; nothing was changed" (408) / "the vendor CAPTURED the money before the change; it must be refunded" (404, 409) / "the estate's paid period ended ({phase}); the change is called off and nothing is charged" (389) | history "plan change {ref}: {before} → {after} ({why})" (change.rs:362) |
| B6.5 | box: notice, price check, cap | — | queued → resizing | why "{host} is told first: its machines stop at {time}" (453); "orders wait: raise the cap: …" (475, 488, via cap_alert); fail "at the buy: {sentence} For this plan change: nothing was changed." (501); "the provider's price: {e}; nothing was changed" (467) | "ALERT estate-orders.queue {ref}: …" (eo.rs:4130) |
| B6.6 | box → cloud resize, estate calls home, capture | — | resizing → done | fail "{role} could not change to {plan}: {e}; {left}" (548); "resized, and the estate did not call home within {m} minutes ({e}); …" (557); whys "resized; waiting for the estate to call home: {e}" (559), "the capture did not go through yet: {e}" (574) | — |
| B6.7 | box → estate main + twin pull grow_go_ahead (data step only) |
Fleet.Orders kind grow_go_ahead (embed-fleet.rs:58), then the box's Fleet.Ask grow_set (embed-fleet.rs:73) |
run.growth signed | grow_set refusals: "{host}'s {role} data is paid to {paid} GB a disk; {gib} GB is bought at {apex} first", "a disk never shrinks: …", "a growth of {host}'s disks is already running here" (eo/sold.rs:944–953) | history "the growth go-ahead is signed for {co}: …" (sold.rs:143–147); "ALERT sold-estate.grow the go-ahead ({ref}) was NOT queued for {co}'s boxes: {e}" + functional_status ("monetize-server/sold-estate","grow-go-ahead") (sold.rs:151–154) |
| B6.8 | box → mail | finish() | change recorded | mail estate.changed "Your {name} {host} has its new plans" (change.rs:616–617); estate.change_failed "Your {name} {host}'s plan change did not go through — nothing was charged" (644–645) |
— |
| B6.9 | buy.js watch | GET /auth/private/status → change_status / change_words (why only when failed, svc-eo.rs:249) | — | — | — |
8.10 B6b – Instance sign-in link again (method the code has beyond the list)
POST /auth/private/sign-in-again → EstateOrder.SendSignInAgain (relay.rs:739–758; svc-eo.rs:267).
-
Refusals (all NotNow → 409):
- "{host} is not delivered yet: its ready mail will carry the first sign-in link" (eo.rs:2076)
- "{host}'s order names no account to sign in as" (2079)
- "a new sign-in link was asked a moment ago; ask again in {n} minute(s)" (2084)
- "{host}'s {role} cannot pull a new sign-in link: {why}" (2088)
-
What happens: the estate main pulls a
sign_in_againorder (embed-fleet.rs:66) and mails the link itself. -
Logs: relay
private.sign_in_again(relay.rs:753).
8.11 B7 – Renew
B7a – Renew an instance ("Renew ")
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B7a.0 | box → mail before the end | term::remind | — | mail estate.renewal_reminder "Your {name} {host} is paid until {date}" (eo/term.rs:690–691) |
history "{kind} mail sent/NOT sent" (term.rs:655–657) |
| B7a.1 | buy.js renewing() → relay → box | POST /auth/private/renew {company} → EstateOrder.Renew (minting) | run.renewal opened, reference estate/<co>/renewal-N (term.rs:297). Phase unchanged (captured/grace/suspended) |
relay 504 (relay.rs:799), 502 PAY_URL_REFUSED (804); a visitor gets a link "Sign in at {host} to renew {co}" (buy.js:845–848). Box: "there is no estate {host}" (term.rs:318); "{host} is {phase}: there is nothing to renew" (320); "{host} is paid until {date}; its renewal opens on {date}" (324) → 409; "the renewal's old payment page could not be closed yet: {e}; ask again in a minute" (276 → 502); "the vendor refused the renewal: {e}" (307 → 502). OK words "{term} more: {amount} ({monthly} a month). Pay on the payment page; it is charged at once." (svc-eo.rs:295) | "ALERT estate-orders.queue {ref}: renewed over its spend cap: {…}" (term.rs:295); history "renewal {ref} opened: …" (310); relay private.renew (relay.rs:806) |
| B7a.2 | browser → Stripe → /paid/estate/<co>/renewal-N (returns.rs:14–17, 42–45) → relay 303 to plain {pages.private} (urls.rs:62–65, because it is not change-) |
GET /paid/estate/{co}/{change} | — | — | — |
| B7a.3 | box term tick → vendor capture | term::step (term.rs:418–489) | paid_until extended; grace/suspended → captured ("paid until {date}; the machines are started again, every disk as it was", 513; "paid until {date}", 515) | park "the renewal's capture did not go through yet: {e}" (436-ish); "renewal {ref} was paid as its page expired; capturing it" (451); notes "renewal {ref} expired unpaid" (447), "renewal {ref} was not authorised" (460) | history "renewal {ref} captured ({vref}): …" (477); mail estate.renewed "Your {name} {host} is paid until {date}" (480–486); a re-signed go-ahead is queued (sold::renewed, sold.rs:167) |
| B7a.4 | unpaid path | term::step | captured → grace → suspended → deleting → deleted (561, 586, 598, 623) | mails estate.lapsed / estate.unpaid (term.rs:728–730), estate.suspended (577), estate.deleted (615); customer "overdue"/"suspended"/"deleted" words (eo.rs:989–996) |
— |
B7b – Prolong the account (hosted plan)
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B7b.1 | page → relay → box | GET /api/account/subscription → Products.List, then Purchase.Quote{renew} with a renew ticket |
quote (no move) | 501 RENEWAL_NOT_VERIFIED (relay.rs:1078,1089,1095); FAILED_PRECONDITION becomes a 200 Renewal with can_serve=false and words from metadata (relay.rs:1131–1134; sb-purchase.rs:410); "We cannot renew this account automatically. Nothing has been charged — please contact us and we will set it right." (tx.rs:522–523) | — |
| B7b.2 | page → relay → box | POST /api/account/subscription/renew → Purchase.Start{renew:true} | Reserved (Pending) → as B5.3–B5.6; RENEWAL_GRANTED "Your subscription is extended — your account carries on exactly as it is. …" (tx.rs:~560) | 504 (relay.rs:1156); 502 (1161); "a renewal names no bundle and no caps" (svc-purchase.rs:1115 → 400); "nothing to renew for {tenant}: {why}" (tx.rs:419 → 409) | relay subscription.renewed (relay.rs:1163) |
8.12 B8 – Cancel
B8a – Cancel an unpaid account order ("Cancel order")
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B8a.1 | page → relay → box | POST /api/account/subscription/cancel {reference} → Purchase.Cancel{reason: "the customer cancelled the order before paying" (relay.rs:1188)} with a cancel ticket |
vendor hold released, ledger Released "cancelled: {reason}" (tx.rs:1745); PurchaseState Released (or ReleasedWithOrphans), phase cancelled; a second press answers already_closed | 401/403 (relay.rs:938,935); box "cancel needs a reason: it is written into the ledger's Released row" (svc-purchase.rs:1995 → 400); "no order was ever placed under {ref}" (2002 → 404); "this tenant has no purchase by that reference" (1336 → 404); "{ref} was reserved at {v:?}, which is not linked into this server — the hold must be let go where it was taken" (2005–2008 → 409); "another caller is finishing this purchase right now; ask again in a moment" (720,2016 → 502) | functional_status ("monetize-server/purchase","cancel") "{ref} cancelled: {reason}" (2053); relay subscription.cancelled (relay.rs:1218) |
| B8a.2 | already paid → 409 | same | the capture is written down and the order finishes as paid (resume_reference, svc-purchase.rs:2027) | 409 "{ref} has been paid, so it cannot be cancelled: it finishes as paid, and ending it now is a refund (Abandon)" (svc-purchase.rs:2030–2033); granted: "{ref} was paid and granted; there is nothing unpaid to cancel" (tx.rs:1195 → 409); "no attempt was ever made for {ref}" (tx.rs:1189 → 404) | "cancel {ref}: finishing the paid order: {msg}" (2028) |
B8b – Cancel an instance ("Stop renewing")
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B8b.1 | buy.js → relay → box | POST /auth/private/cancel {company} → EstateOrder.Cancel (estate ticket) | cancelled_at set; phase stays captured/grace; an open renewal page is released | "{host} is not running a paid period it could stop renewing ({phase})" (eo/term.rs:212 → 409); "no order for {co:?}" (207 → 404); OK words "{host} runs until {date}, the end of its paid period. Then it is stopped, and every disk and all your data are kept until {date}, when it is deleted. Renew before then to keep it." (217–221) | history "cancelled by its buyer: no renewal; stopped after {d}, deleted on {d}" (236); mail estate.cancelled "{host} is cancelled: it runs until {date}" (237); relay private.cancel (relay.rs:831) |
| B8b.2 | box tick at the period's end | term::step | → suspended ("cancelled by its buyer: stopped at the end of the paid period, every disk kept; deleted on {d}", term.rs:549) → deleting → deleted | park "cancelled, and the paid period ended; stopping the machines: {e}" (533) | mail estate.stopped (541) |
B8c – An unpaid instance order
There is no buyer door. The vendor's Cancel returns to cancel_url {origin}{pages.private} (urls.rs:48). The tick releases the order after order_ttl_secs: released "the payment page expired unpaid; nothing was held" (eo.rs:3823).
The operator can instead call EstateOrder.Settle refund:
- Not yet paid → released "the operator cancelled it before it was paid; nothing was charged" (eo.rs:4666).
- Paid → refunded with mail
estate.refunded(4640–4656). -
Refusals:
- "{ref} is {phase}: an order is refunded before anything is laid for it (an owed or waiting one)" (4629)
- "{ref}'s buyer authorised the payment just now: it is captured on the next tick; refund it after that" (4661)
8.13 B9 – Terminate (no backup)
| # | who → whom | relay route → gRPC | state after | error words (file:line) | log events (file:line) |
| B9.1 | buy.js → relay | POST /auth/private/terminate {company, confirm, backup:'skip'} (buy.js:898) | — | 400 "which instance? a company name" (relay.rs:886); 400 "Type the instance's name, {co}, to terminate it. Nothing was ended." (889); 400 "the backup: take or skip" (893); 403 {"error":"{why} Nothing was ended.","code":"stepup"} with default why "This needs a fresh touch of your key (a user-verified login from the last 10 minutes)." (relay.rs:900–904; site.rs:95–98); 401/403 from account_ticket |
— |
| B9.2 | relay → box | EstateOrder.Terminate (estate ticket) | terminating (history "terminate asked by its account for {host}", eo/terminate.rs:210); OK words "{host} is being ended now: its machines, disks and DNS records are being given back." (232); done:false |
protected "{site} is one of the operator's own sites and is protected: it is never terminated here (its [estates.{label}] does not say terminable = true)" (116 → 409, which a buyer reads as NOT_DONE_HERE because of "[estates."); "{name} is one of the operator's own estates and is protected: …" (120); backup take → "[terminate-no-backup] backups are not offered at terminate" (38,171 → 409); "{name} is not an estate this shop runs: nothing to terminate" (181 → 404); "{estate} was adopted from its kit's install, not laid by this box: …" (eo/adopt.rs:357); "the name typed back is not {co}: type {co} exactly to terminate it. Nothing was ended." (197 → 400); "{host} is still being set up; it can be terminated once it runs" (200 → 409); "the cloud {:?} is not linked here: …" (206) | "monetize-server: estate order: a terminate of {name} was refused: protected" (175); "ALERT estate-orders.terminate {ref}: no backup taken: none is offered at terminate" (93,97,156,211); ledger Instance row per step (150–154); relay private.terminate (warn) (relay.rs:914) |
| B9.3 | box tick → cloud give-back → fleet forget | terminate::step / finish (283–339) | terminated ("terminated by its account") | park "being terminated: {why}" (332); give-back failure backs off 10 s doubling up to 5 min (342–351) | "ALERT estate-orders.terminate {ref}: every machine, disk and DNS record given back: …" (290); "…the ONE box's records of it forgotten: …" (306); "…terminated: nothing of it is left, and nothing more is charged" (312); "…the give-back FAILED (try {n}; asked again in {s} s): {why}" (362–365); "…a terminate step failed (asked again each tick): {why}" (103–105); "ALERT estate-orders.terminate {co}: the terminate's drive FAILED to take the run's pen …" (eo.rs:3683) |
| B9.4 | watch | Status → "terminated" "Your {t} was terminated, as you asked: its machines, disks and records are gone, and nothing more is charged." (eo.rs:995) | — | — | — |
8.14 B10 – Test-order paths (operator)
| # | who → whom | route → gRPC | state after | error words (file:line) | log events (file:line) |
| B10.1 | operator UI → box | POST /api/test-purchase/start (ui-server serve.rs:58) → TestPurchase.Start{product, action, site, live} | TestRun kept (tp-<stamp>, test_purchase.rs:310). HostedPlan → start_signup(PayBy::TestPayment) → settle (B2.6). PrivateInstance → account via TestPayment (made at once, no card check) + estate start_with_on(OrderedBy::Test{payer}) → run.test = true (eo.rs:3060–3066, 3292) |
"the test button is the operator's: the operator bearer, or the console's signed call" (services/test_purchase.rs:21); "product {p:?} is not served here" (278); "{p} declares no test action {a:?}" (279); "REFUSED [test-site-unnamed] …" (283–286); "REFUSED [test-site-undeclared] {site:?} is not a site {p} is sold as here: nothing is bought for it" (289); offer.why_not, e.g. "no vendor is routed for it here", "{v} has no test payment method: nothing can pay without a person", "{v}: the vendor's key is not a TEST key (live, or it cannot say): a test button never spends live money" (195–203; svc-signup.rs:1005); live without confirm: "{host}: LIVE — you will be charged. This site's vendor key is live: …" (298–301); pay_as_test "{v}: {NOT_A_TEST_KEY}" (svc-signup.rs:1340), "vendor {:?} cannot pay a signup without a person (it has no test payment method)" (1346), "vendor (test payment): {e}" (1347) | "monetize-server: test run {id}: {source}: {words}" (test_purchase.rs:267), sources signup/vendor/order/run, e.g. "stopped: {why}" (397); "a TEST signup {v}:{s} for {:?} was paid with the vendor's test payment method: {decided:?}" (svc-signup.rs:1357) |
| B10.2 | CLI monetize … site test-order <site> → admin door |
POST /site/test-order (admin-door.rs:254,869; cli lib.rs:200–207) → EstateOrder.OrderSite{test_order:true} | site run: test=true, site_order=true, no checkout page (eo.rs:3216). Then the same driver as B3.4–B3.11; the ready mail is the site variant "The site is ready: https://{host}" (eo.rs:5197); the site never lapses (term.rs:497) | CLI "site-test-order-return" refusal (cli lib.rs:204); box "REFUSED [test-order-return] a test order opens no page, so it names no return page" (svc-eo.rs:501); "ordering a site is the operator's: …" (495); test_card_of: "REFUSED [test-order-live-key] …", "REFUSED [test-order-no-test-card] …", "REFUSED [test-order-key-unknown] …" (eo/site.rs:89–91), "REFUSED [test-order-no-vendor] …" (99); "{estate:?} is not a declared operator site …" (site.rs ~117); "{estate} stands: its {roles} called home in the last {m} minutes — a site that runs is not laid over" (~125); "{estate} is already ordered" (~132); operator cap "raise the estate's spend cap first: …" (eo.rs:4171–4175). The door reports {"refused":"<Code>","words":…} (admin-door.rs:232) |
admin tracing warn "door: the operator ordered the site {estate} laid by this box ({ref}), paid with the vendor's TEST card" (admin-door.rs:283); OK words "{estate} is ordered and paid with the vendor's TEST card ({card}): {amt} held, nobody pays at a page, and this box lays it" (svc-eo.rs:517) |
| B10.3 | operator's own buy (site admin buying on an operator site) | EstateOrder.Start (B4) | operator_test = true, held at EUR 0.50 (eo.rs:912, 3166–3167); description prefix "operator test: " (3185) | — | — |
8.15 What the trace found worth a look
-
Where the vendor sends the buyer back after paying for an instance (B3/B4). Fixed by BUYFLOW-4 (2026-10-25): a console's buyer (signed in, or a visitor paying once) now returns to
https://<site>/paid/estate/<co>of the site bought on, and the page shows "paid, setting up": the steps, where the order stands, and the instance's link when ready. As traced before:- The relay always sends cancel_url, so a signed-in start becomes
OrderedBy::Shop{back_to:"", cancel_to}(svc-eo.rs:198–203). A visitor's start isOrderedBy::NewAccountwith an empty back_to. - Both therefore call
reserve_returning(req, "", cancel_to)(eo.rs:3196–3213). The default trait impl ignores cancel_to and callsreserve(req)(crates/monetize-pay/src/lib.rs:456–463). - Only
OrderedBy::Buyergetssite_returns(https://<site>/paid/estate/<co>, eo.rs:3204). - So the return page depends on the vendor's configured success_url (
https://{site}/paid/{reference}, monetize-admin/src/app_config.rs:178) and on whether the Stripe guest honours cancel_to. The trace did not read that guest. - The visitor's welcome page is reached through the mailed sign-in link (welcome_url plus the
acct_session), not through the vendor return.
- The relay always sends cancel_url, so a signed-in start becomes
-
Renewal return lands on the plain private page. Fixed by BUYFLOW-4:
renewal-<n>lands on{private}&company=<co>, as the first buy's way back. As traced before: A renewal's return is/paid/estate/<co>/renewal-N(returns.rs).paid_change_toonly recogniseschange-<n>, so the buyer lands on plain/signup?plan=privatewithout&company=(urls.rs:62–65). That looks unintended. -
Buyers may see operator text on bundle and renew refusals. Fixed by BUYFLOW-4:
rules::buyer_wordsshows the customer's words from metadata first; without them, a message with a word ofrules::OPERATOR_WORDS(lay, estate, guest, operator, cap, …) reads NOT_DONE_HERE; tests/buyflow4.rs scans the buy pages' and the relay's sentences. As traced before: On buy-bundle and renew,refused_callshows the gRPC message, which is the operator's sentence, e.g. "the product cannot deliver this to …". It does not use the customer words in metadatamonetize-customer-words-bin(svc-purchase.rs:876–883); onlyaccount_offerreads them (relay.rs:1133). The NOT_DONE_HERE filter catches only messages containing the keywords at rules.rs:166. -
PermissionDenied surfaces as 502. Fixed by BUYFLOW-4: 403 with WHO_NOT_CONFIRMED. As traced before:
http_status_ofdoes not map PermissionDenied, so it falls to the default 502 (rules.rs:190–199) with WHO_NOT_CONFIRMED words. Examples: "REFUSED [estate-unnamed] …" and a refused actor ticket. -
Pulled-product sign-up shows a maintenance sentence while it waits. While the main has not yet acked
add_principal,note_waitingis set, so a pulled product's pending sign-up reads the MAINTENANCE sentence (signup.rs:1762, 1850–1851), not "being created". -
Some line numbers are approximate. The lines marked "~" in eo/site.rs (~117–132) and term.rs (~436) were not read one by one. The B2.3 note about
[signups.is also uncertain: the internal-word filter (rules.rs:166) does not list "[signups.", so that plan refusal probably reaches the buyer verbatim. -
Not traced:
- (The ONE shop's own
/shop/signup*,/shop/private*and/koppages are deleted: OD6, A33-12.) - Purchase.Abandon (refund of a captured order, svc-purchase.rs:1946–1983).
- EstateOrder.RestoreFront details.
- (The ONE shop's own
Proposal (not done in this change): put the step id (for example B3.4) into each of these error and log lines in the code, so that a log line maps to its row of this guide.
9. Where it lives: traits and crates
Every trait and crate an integrator touches or should know. The links go to the source on gunnar.rs. monetize-impl is private and is not on gunnar.rs; its rows name the path in that repository only. "Integrator" = you implement or link it; "premade" = you only name it. The last column names the buy steps of section 8 the row serves.
9.1 The traits
| Trait | Repo + path | Open or private | What it is for | Who implements it | Buy steps |
Cloud (9 ops) |
nordisk/monetize crates/monetize-cloud/src/cloud.rs |
open | a cloud in nine verbs: manifest, catalog, price, lay, change, boot, firewall, destroy, inventory, each kind as an enum (Priced, Change, Boot, Give, Listing). The server and the core speak only these; the spend cap and the own-iron wall are the host's. A guest's wire keeps CloudGuest's op numbers, answered from the nine ops |
premade: upcloud.wasm, scaleway.wasm (monetize-impl); server, sandbox (open) |
B3, B4, B6, B8b, B9, B10 |
CloudGuest (the wire) |
nordisk/monetize crates/monetize-cloud-guest/src/lib.rs |
open | what a cloud guest compiles against: the op table, export_cloud! |
premade (the cloud guests) | as Cloud |
Payment (10 ops) |
nordisk/monetize crates/monetize-pay/src/payment.rs |
open | a payment vendor in ten verbs: manifest, page, status, capture, release, refund, callback, settlement, paid_since, livemode. The server holds every vendor as a monetize_pay::Vendor (its instance name and markets as data, over these ten ops); a guest's wire is still VendorGuest's 20 ops |
premade: stripe.wasm, klarna.wasm, swish.wasm (monetize-impl); invoice, sandbox (open) |
B1, B2, B3, B4, B5, B6, B7, B8, B10 |
VendorGuest (the wire) |
nordisk/monetize crates/monetize-pay-guest/src/lib.rs |
open | what a payment guest compiles against: the op table, export_vendor! |
premade (the payment guests) | as Payment |
DnsWriter (6 ops) |
nordisk/monetize crates/monetize-cloud/src/dns.rs |
open | writes a sold estate's names at the zone: write, write_taking_over, remove, listed, settled | premade: dns_cloudflare.wasm (monetize-impl); ManualDns (open, the alert) |
B3, B4, B9, B10 |
DnsGuest (the wire) |
nordisk/monetize crates/monetize-dns-guest/src/lib.rs |
open | what a DNS guest compiles against: the op table, export_dns! |
premade (the DNS guest) | as DnsWriter |
Product (6 ops) [P3] |
target: A24 §2.2; today Guest in nordisk/monetize crates/monetize-product-guest/src/lib.rs (28 ops → 6 [P2]) |
open | what an estate of your product IS (facts) and the plan of ONE order (plan); the guardian (diagnose, remedy, observe) |
the integrator | B3, B4, B6, B10 |
Facts, SaleProof (landed in P3a) |
nordisk/monetize crates/monetize-product-guest/src/facts.rs |
open | your product's shape and the routes the sale check reads, as data | the integrator (answers them) | B3, B4, B10 |
Product (the host's plugin seam) |
nordisk/monetize crates/monetize-product/src/lib.rs |
open | the native side the guest host serves a guest through (usage in, entitlement out) | premade (the guest host, monetize-product-wasm) |
B1–B10 |
Store |
nordisk/monetize crates/monetize-front/src/ledger_backup.rs |
open | where a product keeps its front's backups (Holger: a raw repository on its primary). The store of an EstatePlan (the Lake's object store input) is P3 |
the integrator, where the product keeps backups | — (not a buy step) |
BuySite |
nordisk/monetize crates/monetize-site-buy/src/site.rs |
open | your site's half of the buy flow: who, ticket, held_caps, signup_account, stepped_up | the integrator | B1–B9 |
PumpBox (the box's half of the pump) |
nordisk/monetize crates/monetize-estate-side/src/pump/puller.rs |
open | what your box reports and how it runs a pulled order (add_principal, entitlement, grow_room, grow_go_ahead, sign_in_again) |
the integrator | B2, B3, B4, B5, B6, B6b, B7 |
Estate [P3] |
target: A24 §2.3 (section 3.8) | open | report, execute(order), box_key over the shared pump | the integrator | as PumpBox |
Product (box side) |
nordisk/Skidbladnir crates/installer/nordisk-pairing/src/boxside.rs |
open | the box at pairing: kind, tunnel_pubkey, capacity, offer, fed, the live door | the integrator | B3, B4, B6, B10 |
FrontProduct |
nordisk/Skidbladnir crates/installer/nordisk-pairing/src/frontside.rs |
open | the front at pairing and update: the relay's tunnel key, its offer, the live door | the integrator | B3, B4, B10 |
9.2 The crates
| Crate | Repo + path | Open or private | What it is for | Who uses it | Buy steps |
monetize-product-guest |
nordisk/monetize crates/monetize-product-guest |
open | the SDK of your product operator (guest): the op table, the wire, the host doors, export_product!, Facts |
the integrator links it | B3, B4, B6, B9, B10 |
monetize-site-buy |
nordisk/monetize crates/monetize-site-buy |
open | the buy flow your site relays: BuySite, ShopLink, the relay router (26 routes), buy_js / welcome_js |
the integrator links it | B1–B9 |
nordisk-pairing |
nordisk/Skidbladnir crates/installer/nordisk-pairing |
open | pairing over WireGuard and mTLS, the live door, the shared update step [P4] | the integrator links it (box and front) | B3, B4, B6, B10 |
monetize-estate-side |
nordisk/monetize crates/monetize-estate-side |
open | the box pump (Fleet.Report / Orders / Ask) and the front's signed shop door (P4a; over Fleet.Ask since P3, the old SoldEstate door for one release more) |
the integrator links it [P4] | B2, B3, B4, B5, B6, B6b, B7 |
monetize-embed |
nordisk/monetize crates/monetize-embed |
open | the product's entitlement cache (Ed25519-verified, answers while the ONE box is unreachable) and the pull channel's signed forms (SignedOrder, the order kinds) |
the integrator links it | B1–B7 |
monetize-admin |
nordisk/monetize crates/monetize-admin |
open | the ONE box: PID 1 runs server and ui |
premade (the operator lays it) | B1–B10 |
monetize-server |
nordisk/monetize crates/monetize-server |
open | the core: the doors (Signup, EstateOrder, Purchase, Fleet, the callbacks), the ledger, the estate state machine, custody, mail | premade | B1–B10 |
monetize-shop-pairing |
nordisk/monetize crates/monetize-shop-pairing |
open | the shop's installer: pairs a laid estate once, runs the sale check | premade | B3, B4, B10 |
monetize-cloud |
nordisk/monetize crates/monetize-cloud |
open | Cloud (nine ops) and ask, DnsWriter; the open clouds server and sandbox |
premade | B3, B4, B6, B8b, B9, B10 |
monetize-pay |
nordisk/monetize crates/monetize-pay |
open | Payment, Vendor; the open vendors invoice and sandbox |
premade | B1–B8, B10 |
monetize-product-wasm, monetize-cloud-wasm |
nordisk/monetize crates/monetize-product-wasm, crates/monetize-cloud-wasm |
open | the guest hosts: load a signed .wasm, check its signature, run it under fuel, deadline and memory caps |
premade | B1–B10 |
mock-upcloud, mock-scaleway |
nordisk/monetize crates/mock-upcloud, crates/mock-scaleway |
open | the clouds' APIs, faked, for tests | premade (tests) | B3, B4, B6, B9, B10 in tests |
lake-product, the Lake's product operator |
nordisk/monetize crates/lake-product, guests/lake |
open | the reference product: a parametric operator, its box side and its bench | the integrator's example | B3, B6, B9, B10 |
gunnar-monetize-guest |
nordisk/gunnar crates/gunnar-monetize-guest |
open (Gunnar) | Gunnar's product operator | the integrator (Gunnar) | B1–B10 |
holger-monetize-guest |
nordisk/holger crates/holger-monetize-guest |
open (Holger) | Holger's product operator | the integrator (Holger) | B1–B10 |
monetize-guest-stripe, -klarna, -swish |
monetize-impl guests/stripe, guests/klarna, guests/swish |
private | the payment guests (Klarna and Swish: stub, lowest priority) | premade | B1–B8, B10 |
monetize-guest-upcloud, -scaleway |
monetize-impl guests/upcloud, guests/scaleway |
private | the cloud guests (Scaleway: stub, lowest priority) | premade | B3, B4, B6, B8b, B9, B10 |
monetize-guest-dns-cloudflare |
monetize-impl guests/dns-cloudflare |
private | the DNS guest | premade | B3, B4, B9, B10 |
monetize-pay-impl |
monetize-impl crates/monetize-pay-impl (examples/fake_stripe_serve.rs, fake_klarna_serve.rs, fake_swish_serve.rs) |
private | the paid vendors' native impls and their fakes for tests | premade (tests) | B2–B8 in tests |
monetize-impl-build-thing |
monetize-impl crates/monetize-impl-build-thing |
private | the Build Thing: builds and signs every guest under the plugin root the ONE box trusts | premade (the release) | — (ships the plugins) |
Sources: MONETIZE-V2.0-PLAN.md (§2 decisions, §3 traits, §6 phases); A24 monetize-small-trait-design (§2.2 Product, §2.3 Estate, §2.4 BuySite, §4.1-4.3 the three guests, §8.2 the fast test); A25 p2-cut-list §2 (the 28 → 6 mapping); A21 (what runs on the ONE box); the STE manual integration-guide.md (today's names). The data model: monetize-server (estate_order.rs, custody.rs, signup.rs, signup_pull.rs, config.rs), monetize/src/ledger.rs, monetize-guardian/src/store.rs, monetize-proto (accounts.proto, site_book.proto, estate_order.proto), monetize-site-buy/src/site.rs, and Gunnar's gunnar-core (model.rs, auth.rs) and gunnar-server (control_plane.rs, fed_principals.rs, repair.rs).