# The idiot's guide to integrating an application with monetize
This guide is written in ASD-STE100 Simplified Technical English. The sentences are short. Each procedure step gives one instruction. The guide uses one term for one thing. The terms are in section 1.
The worked example is the echo product. NOTE (monetize v2.0 P1, 2026-10-24): the echo product became the Lake (`crates/lake-product` and `guests/lake`). The Lake is pulled: it has no `endpoint` and no grants, and its boxes report and pull their orders. Where this guide shows the echo guest dialling its box over `http`, the Lake does not. Since P2 T13 the guest contract is the six product ops (section 5.1); the rest of this guide's echo examples are rewritten with the Lake in a later step. The real products are Gunnar and Holger.
Contents:
1. Terms
2. What monetize is
3. The integration points: the checklist
4. From an empty crate to a product that the ONE box sells
5. The contract of each part
6. The auth model
7. The lifecycle flows
8. The error codes and what to do
9. How to run the bench
10. Common mistakes
---
## 1. Terms
| Term | Meaning |
|---|---|
| ONE box | The one monetize server that sells all products (`monetize.ignalina.dk`). It holds the money, the ledger, the queue and the cloud account. |
| product | An application that the ONE box sells. Gunnar, Holger and echo are products. |
| estate | The machines that one buyer bought. An estate has one or more boxes. |
| box | One machine of an estate. Each box has a role. |
| role | The name of a box in its estate: `front`, `main`, `twin` (Gunnar and Holger), or `box` (echo). |
| primary | The role that holds the data of the estate. It is the first data role. |
| front | The role that faces the buyer. It drives the update and is upgraded last. On echo, the front is the box. |
| guest | The product's signed WebAssembly module. The ONE box loads it. |
| op | One numbered call into the guest. |
| host door | A call that the guest makes back into the ONE box (`http`, `grpc`, `endpoint`, `state`, `hmac`, `clock`, `log`; `sign` went in T13 and the generic `env` by owner decision 6). The doors are split by guest kind (`grants::of_kind`): a product guest is granted none of the doors that need a grant; a vendor `http`, `hmac` and `state`; a cloud `http` and `state`; a DNS writer `http`; a signup `grpc` and `state`; the toy test guest its typed `endpoint`. A grant outside the guest's kind is refused at load, by name (`[guest-grant-outside-kind]`). |
| grant | A permission for a host door, written in `[products.<id>] grants`. |
| site | The product's web site where the buyer buys. The site runs the shared relay. |
| relay | `monetize_site_buy::relay`: the shared code that sends each buy call from the site to the ONE box. |
| installer | The ONE box's code that sets up the laid boxes (the `Installer` trait). |
| lay | To make the machines of an estate at the cloud provider. |
| pair | To join the laid boxes to the estate and to the ONE box. |
| sale check | The check that the sold estate serves the buyer, before the money is captured. |
| capture | To take the money that the vendor holds. |
| ticket | An actor ticket: the product's proof that a person may act for an account. |
| fleet call | A call that a box makes to the ONE box's `Fleet` service. |
| estate call | A call that the front makes to the ONE box's `SoldEstate` service. |
| order | A signed instruction that the ONE box queues for a box. The box pulls it. |
| report | The box's heartbeat (`Fleet.Report`). |
| medium | The disk image that a box boots from. |
| keep medium | The medium that installs a release over a box and keeps its data (`<product>-keep`). |
| live door | The door on a box that stages a new app in its second slot (`/live/running`, `/live/stage`). |
| bench | `cargo xtask bench lake`: the whole lifecycle of one echo estate on the ONE box's real code. |
---
## 2. What monetize is
Monetize is the one shop for all products. It sells an account or a private estate. It takes the money through a vendor (Stripe). It lays the estate at a cloud provider (UpCloud). It pairs the boxes and checks the sale. Then it captures the money. After the sale, it meters the use, renews, grows, updates and ends the estate. A product never holds money and never holds a cloud key. **Monetize never calls into an estate.** The boxes of an estate call the ONE box. They report, they pull their orders and they ask. This is the pull model.
---
## 3. The integration points: the checklist
Make a tick for each line. The echo product has all of them.
| # | Integration point | Echo has it in | Gunnar | Holger |
|---|---|---|---|---|
| 1 | A product id and a block `[products.<id>]` on the ONE box | `crates/lake-product/one-box.lake.example.toml` | `gunnar` | `holger` |
| 2 | A signed guest with the op table (section 5.1) | `guests/lake/src/lib.rs` | `gunnar/crates/gunnar-monetize-guest` | `holger/crates/holger-monetize-guest` |
| 3 | The lifecycle: `lifecycle.facts` (op 90) and `lifecycle.lay_plan` (op 91) | `EchoGuest::lifecycle_facts` | guest `lifecycle.rs` | guest `estate.rs` |
| 4 | The guardian: ops 100, 101, 102 | `EchoGuest::guardian_*` | guest `guardian.rs` | guest `guardian.rs` |
| 5 | A buy site that implements `BuySite` and mounts the relay | `crates/lake-product/src/site.rs` | gunnar-ui `auth/buy_site.rs` | holger-estate-front `buy.rs` |
| 6 | An actor ticket that the box mints and the ONE box checks (`facts.tickets`, the block's `appliance_key`) | none: the Lake mints no tickets | `Billing.MintActorTicket` | `room.rs` |
| 7 | A pairing door: the box takes the installer's feed and offers its keys | `lake_box` `POST /pair` | `installer_listener.rs` | `paired.rs` |
| 8 | A front door for the sale check | `GET /login`, `GET /first-login` | gunnar-front | holger-estate-front |
| 9 | Fleet calls: `Report`, `Orders`, `Ask` | `EchoBox::report`, `EchoBox::orders` | `gunnar-repair/src/fleet.rs` | `holger-fleet/src/puller.rs` |
| 10 | Estate calls: `UpdateTake`, `UpdateReport` (and `Upgrade`, `TakeMaterial`) | `EchoBox::update_take`, `EchoBox::update_report` | `gunnar-front/src/update.rs` | `holger-estate-front/src/update.rs` |
| 11 | A live door and a keep medium | `GET /live/running`, `POST /live/stage`, `POST /keep` | korp live | korp live |
| 12 | Metering: the product's usage in the report | `Report.product` (`ProductReport`) | gunnar-server | holger server |
| 13 | A free account plan for an order that makes an account | `[signups.echo.plans."echo/account"]` | pmo config | pmo config |
| 14 | Media and keep media for each role, in `[estate_orders.media]` | `echo`, `echo-keep` | `gunnar`, `front`, `gunnar-keep`, `front-keep` | `holger`, `holger-front`, `holger-keep`, `holger-front-keep` |
---
## 4. From an empty crate to a product that the ONE box sells
Do the steps in this sequence. Run the bench after each part (section 9).
### 4.1 Make the guest
1. Make a crate in the monetize repository, for example `guests/<id>`.
2. Set `crate-type = ["cdylib", "rlib"]` in its `[lib]` section.
3. Add the dependency `monetize-product-guest` (by path).
4. Write a type that implements `monetize_product_guest::Guest`.
5. Give `manifest()` your product id, no grants and each meter that you measure or cap. The Lake: `Manifest::new("lake", &[]).with_meters(…)`.
6. Implement `lifecycle_facts` and `lifecycle_lay_plan` (section 5.2). The facts say whether your main mints actor tickets (`tickets`), your test buttons (`test_actions`) and whether you ship a guardian (`guardian`).
7. Implement the guardian ops if your estate can be repaired (section 5.3).
8. End the file with `export_product!(YourGuest, grants = []);`. A product guest opens no door.
9. Add the crate to `GUEST_CRATES` in `xtask/src/guests.rs`.
10. Run `cargo xtask build-guests`.
A product guest calls no host door but `clock` and `log`. The host defines no other import for it, and a module that imports one does not load.
### 4.2 Sign the guest and list it on a ONE box
1. Sign the module with a key under a root that the ONE box trusts. The bench uses a bench CA (`guest_host::verify::sign::dev`). A release uses the signing root.
2. Put the module and its `.p7s` signature beside it on the ONE box.
3. Write the block `[products.<id>]`: `guest`, `guest_trust_roots`, `grants = []`, `instance` (the estate: the ONE box reads its reports), and `appliance_key` when your main mints actor tickets.
4. Write `[signups.<id>.plans."<id>/account"]` with `amount_minor = 0` and `cap_bytes = 0`. A visitor's order makes its account on this plan.
5. Write `[estate_orders]` with `product = "<id>"`, and write `[estate_orders.media]` with one medium for each role and one keep medium.
6. Start the ONE box. It prints `lifecycle <id>: taken from the guest of [products.<id>]`.
### 4.3 Make the site
1. Implement `monetize_site_buy::BuySite` for your site (section 5.5).
2. Make a `ShopLink` to the ONE box with the site's service bearer.
3. Make a `Relay` with your site and the link.
4. Mount `monetize_site_buy::router(relay)` on your site's web server.
5. Do not copy buy code into your site. The relay has every buy door. Your site gives only the answers in section 5.5.
### 4.4 Make the box
1. Make the pairing door (section 5.6). The box takes the feed and offers its fleet key and its ticket key.
2. Make the front door that the sale check reads (section 5.7).
3. Start the report loop: one `Fleet.Report` each minute (section 5.8).
4. Start the order loop: `Fleet.Orders` as a long poll. Verify each order under `monetize_pubkey`. Run it. Ack it in the next report.
5. On the front, start the update loop: `SoldEstate.UpdateTake`, then the step, then `SoldEstate.UpdateReport` (section 5.9).
6. Make the live door, and make a keep medium for each role (section 5.10).
### 4.5 Prove it
1. Run `cargo xtask bench lake` before you change anything. It must pass.
2. Copy the echo bench into a bench for your product, or run your product against the same steps.
3. Run the product's own tests against the fake ONE box (`monetize-site-buy` feature `testkit`).
4. Run the KVM gate of the product with its real media before a release.
---
## 5. The contract of each part
### 5.1 The guest op table: six ops
The numbers are the wire. Do not change a number. Do not use a retired number again.
| Op | Name | The ONE box asks it | Answer |
|---|---|---|---|
| 1 | `manifest` | at load | the product id, the ABI, the grants (none) and the meters |
| 90 | `lifecycle.facts` | at load, once per guest | the estate's lifecycle (5.2), with `tickets`, `test_actions` and `guardian` |
| 91 | `lifecycle.lay_plan` | at each lay and quote | the machines (5.2) |
| 100 | `guardian.diagnose` | each guardian look | the findings |
| 101 | `guardian.plan` | a remedy | the steps, or why not |
| 102 | `guardian.observe` | an `Await` step | `Waiting`, `Met` or `Failed` |
The retired numbers (`monetize_product_guest::ops::RETIRED`): 2, 10 to 14, 20, 30 to 32, 40, 41, 50 to 54, 60 to 64 (left the contract in v2.0 P2), and 70, 71, 80, 81 (retired in 5.4).
Rules:
- Each op takes JSON and answers JSON. Each op is pure.
- An op that your guest does not have answers the frozen phrase `is not in this guest's op table`. Do not change this phrase.
- A guest call has a deadline of 30 seconds.
- Answer `ProductError::Refused` when the request cannot be served.
**On the ONE box every product is pulled.** The block names the estate's `instance`, and the ONE box wraps the product in `Pulled`. Pulled answers the tenants, the usage, the capacity and the metered door from the box's reports, and sends an entitlement and a purge as orders. None of these is a guest op. A block without `instance` gets a refusal by name for each of them.
### 5.2 The lifecycle (ops 90 and 91)
`lifecycle.facts` tells the ONE box what an estate of your product is:
| Field | Echo | Meaning |
|---|---|---|
| `kind` | `echo` | the product kind |
| `update_order` | `["box"]` | the roles in the sequence an update takes them |
| `app_medium`, `front_medium` | `echo` | the medium kinds that the release ids are read from |
| `front_role` | `box` | the role that drives the update; it is updated last |
| `keep_medium`, `front_keep_medium` | `echo-keep` | the keep media |
| `upgraded_by_shop` | `[]` | the roles that the ONE box keep-boots itself |
| `medium_of`, `other_medium` | `box → echo` | the medium of each role |
| `backup_roles` | `[]` | not used at terminate (no backup at terminate) |
| `words` | `Echo` | the product's name as the buyer reads it |
| `data_roles` | `box` | the roles whose data disks a bought data set grows; the first is the primary |
| `proof_name` | `echo-proof` | the TXT proof of an own domain |
| `live_slot` | `true` | the boxes have a live slot |
| `tickets` | `false` | the product's main mints actor tickets; the ONE box checks them (5.4) |
| `test_actions` | `lake-mini` | the test buttons of the operator's console |
| `guardian` | `true` | the product ships an estate guardian (ops 100 to 102) |
| `default_shape` | `{"names": ["", "www."]}` | optional JSON: `names` (the prefixes of the names the estate serves; default `""`, `www.`, `git.`), `ladder` and `mail_words` can ride in it |
`lifecycle.lay_plan` gets the config's shape, labels and zone, the company, the sizes and the template tiers. It answers an `EstatePlan` as JSON:
- `reference` must be `estate/<company>`.
- Each machine has a role, a hostname, a title, a plan and at least one disk. The first disk is the system disk.
- Each title must be unique in the plan.
- Each role is laid once. A plan that names `front`, `main` or `twin` must have exactly those three roles.
- Put the role in the labels (`role = <role>`).
The ONE box prices the quote from your lay plan. Use plan names that the provider sells.
### 5.3 The guardian (ops 100 to 102)
The guardian is pure. It reads the estate as the boxes reported it. It opens no door.
- `diagnose` answers findings. Each finding has a code, a role, words, evidence and remedies. `auto` names the remedy that auto-fix may take. `None` means that only a person may press it.
- `plan` answers the steps of a remedy: cloud steps, orders for a box, and waits.
- `observe` answers the state of a wait.
Echo has one finding, `box_silent`, with no automatic remedy.
### 5.4 The actor ticket (checked by the ONE box)
1. The site asks the box for a ticket for the person (`BuySite::ticket`).
2. The box signs the ticket with its ticket key.
3. The relay sends the ticket to the ONE box with the call.
4. The ONE box checks the ticket itself (`site_ticket`): under the site's `[estates.<estate>] appliance_key`, or under the product's `appliance_key` when the facts say `tickets`. It spends the ticket's nonce once.
5. The guest is never asked. A product whose block names no key honours no ticket.
The purposes are `estate`, `order` (with the exact target caps), `renew`, `read` and `cancel`. A ticket for one purpose does not prove another purpose. The Lake mints no tickets: its site refuses the ticket, and a door that reads without one answers what it shows anyone.
### 5.5 The buy site (`BuySite`)
| Method | Contract |
|---|---|
| `origin()` | the site's origin, `https://<host>`. Every way back from the vendor is on it. |
| `product_word()` | the product as the buyer reads it: `Gunnar`, `Holger`, `Echo`. |
| `who(headers)` | `Visitor`, `Machine` or `Person(Buyer)`, from the request's own session. A token that is not a person is `Machine`. |
| `ticket(headers, buyer, purpose, caps)` | the actor ticket (5.4). |
| `held_caps(headers, buyer)` | the caps that the buyer's tenant holds now. |
| `signup_account(user)` | `NoPasskey` or `HasPasskey` for a paid sign-up's account. |
| `stepped_up(headers)` | `Ok` when the person touched their key in the last 10 minutes. Ending an estate needs it. The default refuses. |
| `allowed(door, ip, key)` | your rate budget. The default allows all. |
Do not trust a header to find the person. Read your own session.
### 5.6 The pairing door
The installer calls this door after the lay. It feeds each box:
- the company and the role;
- the fleet instance name that the box reports as;
- the ONE box's verifying key (`monetize_pubkey`);
- the shop door;
- the estate client key (on the front), from the custody's hand-off file `estate-client.key`.
The box answers its offer:
- `fleet_pubkey`: the box's own Ed25519 public key, base64. The installer names it in the verdict. Then no other key can pin the role.
- `ticket_key`: the key that the box signs tickets with.
- the pair status. The installer closes the install on `PAIRED`.
The real installer is `monetize-shop-pairing`. It speaks nordisk-pairing over each box's WireGuard link with mTLS. The echo installer speaks plain HTTP on loopback. The calls and the answers are the same.
### 5.7 The front door (the sale check)
Before the capture, the ONE box proves that the sold estate serves the buyer:
1. The primary's report says `serving = yes` and `data = ok`.
2. The front answers its login page at `https://<host>`.
3. The first-admin door is open to the invite that the ready mail carries.
4. Gunnar only: exactly one `<account>/welcome` repository exists.
If a step fails, the order stays at `paired`. No money is captured. The ONE box asks again every 30 seconds until the deadline.
### 5.8 The fleet calls
Every fleet call is a `FleetSigned`. The box signs `monetize_embed::fleet::fleet_call_message`. The call is fresh for 5 minutes. There is no bearer.
| Call | Body | Answer | When |
|---|---|---|---|
| `Fleet.Report` | `Report` JSON: `build`, `health` (`serving`, `data`, `serves_at`, …), `acked`, `product` (a `ProductReport`: tenants and usage), `measured_unix_ms` | the acked seqs, and when to report next | each minute |
| `Fleet.Orders` | `OrdersAfter {after, wait_ms}` | the orders after `after` | a long poll, at most 25 seconds |
| `Fleet.Ask` | `Ask {kind, body}` | a case id, words, and a page url for `renew` | when the box needs a remedy |
Verify each order under `monetize_pubkey` with `monetize_embed::fleet::verify_order`. Do not run an order that does not verify. Ack each order in the next report, with `ok` and words.
Use the shared code. `monetize-embed` has the wire, the message and the verifier. Do not copy them.
### 5.9 The estate calls (the update)
The front signs each estate call with the estate client key over `monetize_embed::signing::estate_call_message`. The call is fresh for 5 minutes.
| Call | `call` word | Use |
|---|---|---|
| `SoldEstate.UpdateTake` | `update-take` | Read the open update: whose turn, the release, live or not, the live apps. |
| `SoldEstate.UpdateReport` | `update-report` | Report a role's step: `done:live`, `done:keep`, `done:nothing`, `failed` or `needs-keep`. `members` is the role. `carries` is `<outcome>\n<words>`. |
| `SoldEstate.Upgrade` | `upgrade` | Ask the ONE box to keep-boot one box. |
| `SoldEstate.TakeMaterial` | `take-material` | Take the installer material once, then `done`. |
### 5.10 The live door and the keep medium
- `GET /live/running` answers `base`, `app`, `slot` and `live_update`.
- `POST /live/stage` takes the release's app pin and archive. The box stages it in its other slot and confirms it.
- A keep medium boots once, installs the release, keeps the data and powers off. Then the box starts from its disk.
- The keep boot can take 45 minutes at UpCloud. The ONE box's host deadline for a keep boot is 60 minutes. Do not report `done` before the box is back.
---
## 6. The auth model
| Who calls | Whom | Transport | Proof |
|---|---|---|---|
| The site (relay) | the ONE box | gRPC | the site's service bearer |
| The relay, for a person | the ONE box | gRPC | the bearer and an actor ticket, checked by the ONE box |
| The vendor (Stripe) | the ONE box | HTTPS webhook | the vendor's HMAC signature, verified by the vendor guest; the event is read again from the vendor |
| The ONE box | the vendor, the cloud, the DNS provider | HTTPS from a vendor guest | the key, presented by the host; the guest never reads it |
| The ONE box | your guest | wasm op | the module's signature under a trusted root; the grants |
| The installer | the laid boxes | nordisk-pairing over WireGuard | mTLS with the order's own CA; the bootstrap certificate joins once |
| A box | the ONE box `Fleet` | gRPC | the box's own Ed25519 key, named by the installer |
| The front | the ONE box `SoldEstate` | gRPC | the estate client key |
| The ONE box | a box (an order) | pulled by the box | the ONE box's key; the box verifies under `monetize_pubkey` |
| The operator console | the ONE box | gRPC | the operator bearer, and a passkey step-up for a dangerous press |
Rules:
- Do not put a secret in `user_data`. The seed in `user_data` has no secret. The secrets travel in the install feed.
- Do not log a key, a bearer or a ticket.
- Use one proof for one kind of caller. Do not accept a bearer where a signature is the rule.
---
## 7. The lifecycle flows
The bench runs each flow in this list. The names in brackets are the receipt's steps.
### 7.1 Buy (`buy: a visitor starts the order …`)
```text
buyer ─▶ site /auth/private/start ─▶ relay ─▶ ONE box EstateOrder.Start
ONE box: reserve the name, price it from the guest's lay plan, ask the vendor for a hold
ONE box ─▶ site: the vendor's page; the buyer pays there
vendor ─▶ ONE box: the webhook (or the next tick reads the vendor)
```
A signed-in buyer's start carries a ticket. A visitor's start carries the account name and the e-mail address. The order makes the account when it is paid.
### 7.2 Lay, install, pair, sale check, capture (`lay → install → pair → sale check → capture`)
```text
ordered ─▶ held (paid; secrets minted and sealed in the custody; queued)
held ─▶ laying (the cloud lays the guest's plan; the DNS records are written)
laying ─▶ installing (the installer feeds each box; each box offers its keys)
installing ─▶ paired (the verdict PASS; the box keys are registered)
paired ─▶ captured (the primary reports serving; the sale check passes)
```
A lay that fails twice on a paid order becomes `owed`. An operator retries it or refunds it.
### 7.3 Report and meter (`fleet: …`)
```text
box ─▶ Fleet.Report {health, product: {tenants, usage}} ─▶ the guardian store, the usage poller
box ─▶ Fleet.Orders ─▶ the queued orders ─▶ run ─▶ acked in the next report
```
### 7.4 Renew (`renew: …`)
```text
30 days before the paid period ends: EstateOrder.Renew opens a vendor page
the buyer pays ─▶ the tick captures ─▶ paid_until moves on by the term
```
### 7.5 Update (`update: …`)
```text
a release is fed ─▶ the operator opens UpdateEstate
the front: UpdateTake ─▶ its turn ─▶ the live door, else the keep medium ─▶ UpdateReport
a front that never asked: the ONE box keep-boots the front first
the ONE box records the release each box runs
```
### 7.6 Grow and change plan
```text
EstateOrder.ChangePlan ─▶ a hold for the difference ─▶ paid ─▶ a signed grow_go_ahead order
the primary pulls the order ─▶ grows its data disks ─▶ reports
```
### 7.7 Nonpay
```text
the paid period ends unpaid ─▶ grace ─▶ suspended ─▶ deleted (10 days of nonpay)
a purge is an order that the primary pulls (op 50 on a dialled product)
```
### 7.8 Terminate (`terminate: …`)
```text
the buyer types the estate's name ─▶ EstateOrder.Terminate (backup "skip")
the ONE box marks the estate terminating and answers at once ("is being ended now")
the tick's queued job gives back every machine, disk and DNS record ─▶ forgets the box keys
no backup is taken at terminate
```
---
## 8. The error codes and what to do
### 8.1 gRPC codes from the ONE box
| Code | Meaning | What to do |
|---|---|---|
| `UNAUTHENTICATED` | The bearer, the ticket, the box key or the client key is not known or not valid. "not signed by a box this monetize knows": the box key is not registered and no pin window is open. | Do not retry the same call. Check the key. For a new box, check that the installer named its `fleet_pubkey`. |
| `PERMISSION_DENIED` | The caller may not do this (the service bearer on an operator door). | Use the right bearer. |
| `INVALID_ARGUMENT` | A bad name, a bad size, a confirm word that does not match. | Correct the request. |
| `FAILED_PRECONDITION` | Not now: out of turn, not delivered, nothing to renew yet, already being updated. The words say why. | Wait for the state that the words name. Then try again. |
| `ALREADY_EXISTS` | The name is taken. | Choose another name. |
| `UNAVAILABLE` | A vendor, the cloud or a box did not answer. Nothing was changed. | Try again later. The ONE box also tries again on its tick. |
| `DEADLINE_EXCEEDED` | The call took too long. The work can still be going on. | Read the status before you try again. |
### 8.2 HTTP answers from the relay
| Status | Meaning |
|---|---|
| 400 | The page sent a bad field. The words name it. |
| 401, 403 | Not signed in, or a machine credential. |
| 409 | A conflict that the buyer can fix (an account plan is missing, a name is taken, the account has no e-mail address). |
| 429 | The site's rate budget is spent. |
| 502 | The ONE box answered something that is not usable (for example a vendor page that is not `https`). |
| 504 | The vendor page did not open in time. Nothing was charged. |
### 8.3 Guest errors
| Error | Meaning | The ONE box does |
|---|---|---|
| `ProductError::Unavailable` | The box did not answer. | It tries again on the next tick. |
| `ProductError::Refused` | A new try cannot help. | It records the refusal. |
| `… is not in this guest's op table` | The guest is older than the op. | It uses the default for that op. |
| `… is not in this guest's http_hosts` | The block does not list the host. | Nothing is sent. Add the host to `http_hosts`. |
| `… needs the grant …` | The guest called a door without its grant. | The module does not load. |
---
## 9. How to run the bench
The bench runs at container level. It needs no KVM and no network beyond loopback.
1. Go to the root of the monetize repository.
2. Make sure that the sibling repositories are beside it: `edda`, `Skidbladnir`, `dwarves`, `tunnr`, `holger`, `gunnar`.
3. Run the command in the toolchain container:
```sh
cargo xtask bench lake --out /tmp/lake-bench
```
4. Read `/tmp/echo-bench/receipt.txt`. The first lines say `result: PASS` and the count of steps.
5. Read `bench.log` for every line, the guest's lines included (`[guest echo] echo: guest op …`).
What is real in the bench: the ONE box's registry, the signed echo guest, the estate orders, the custody, the guardian, the gRPC router and the shared relay. What is a double: the vendor (`testing::CallbackPay`), the cloud (`monetize::testing::MockCloud`), the DNS (printed) and the installer's transport.
A receipt line looks like this:
```text
PASS lay → install → pair → sale check → capture: captured
```
Each call that the echo product gets is one line:
```text
echo: pairing.feed from monetize company=acme instance=acme role=box
```
If a step fails, the receipt gives the words that the ONE box answered. Read the words first. Then read the run's history in the line.
---
## 10. Common mistakes
These mistakes happened in real releases. Each one cost hours.
### 10.1 Mixed sibling locks
**Symptom:** A build in one repository passes. The same commit fails in the release tree, or two crates of one release use two versions of one sibling crate.
**Cause:** Each repository has its own `Cargo.lock`, and some crates have their own workspace (`monetize-admin`, `monetize-fast`, `holger-estate-front`). The siblings are found by path (`../../../edda`). A lock that follows one sibling commit and a lock that follows another sibling commit make a mixed build.
**What to do:**
1. Pin each sibling to one commit for the whole release.
2. After you move a sibling, update every lock that names it. Do not forget the nested workspaces.
3. Build with `--locked`. A lock that must change is then an error, not a silent change.
### 10.2 A cargo config at the manifest root
**Symptom:** `cargo xtask build-guests` says that the module is missing, but cargo built it.
**Cause:** A `.cargo/config.toml` at the manifest root sets `target-dir`. The xtask cannot see that file. It looks in `CARGO_TARGET_DIR` or `target`.
**What to do:** Export `CARGO_TARGET_DIR` before you run the xtask. Do not set the target dir only in a config file.
### 10.3 The QMP socket path is too long
**Symptom:** A KVM gate fails at the start of the virtual machine. QEMU cannot bind its QMP socket.
**Cause:** A Unix socket path has at most 107 bytes. A deep run directory makes a longer path.
**What to do:** Use a short run directory, for example `/backup/kg-<name>` or `/home/rickard/kb-<n>`. Count the bytes of the socket path before the run.
### 10.4 The console bundle is missing in a release tree
**Symptom:** The front answers its API and `/login`, but the console page is empty.
**Cause:** The UI's web bundle (the trunk `dist/`) was not built into the release tree. The medium has no console bundle.
**What to do:** Build the UI bundle as a step of the release. Check that the medium holds it before you sign the medium. Do not embed the UI in the server.
### 10.5 LFS pointers instead of files
**Symptom:** A medium, a module or a test fixture is a small text file. The loader says that the file is not a module, or the hash does not match.
**Cause:** The clone did not fetch the Git LFS objects. The file is an LFS pointer.
**What to do:**
1. Run `git lfs pull` in every repository of the release.
2. Check the size of each large file before the build.
3. Make the release script fail when it finds a pointer.
### 10.6 The root key is missing
**Symptom:** The ONE box does not load a guest. It says that the signature is not valid, or that no root vouches for the module. A medium is refused as not enrolled.
**Cause:** The module or the medium was signed without the signing root, or with a development key. Or the ONE box does not have the root in `guest_trust_roots`.
**What to do:**
1. Sign the release on a box that holds the signing root.
2. Put the root in `guest_trust_roots` of the block.
3. Do not sign a release with a development key.
### 10.7 The timing of the update's keep medium
**Symptom:** An update stops in the middle of a keep boot. The box is up again later, but the ONE box records a failure. The next update keep-boots the same box again.
**Cause:** A keep boot at UpCloud can take 45 minutes. The host cut each cloud call at 15 minutes. The release that a box runs was recorded only when the front reported `done`.
**What to do:**
1. Use monetize with the keep-boot deadline of 60 minutes (A17).
2. Wait for the box to report again before you report `done`.
3. Do not start a second update while one is open.
### 10.8 More mistakes that the bench found
- **A one-box estate was refused by the lay.** The lay check wanted `front`, `main` and `twin` for each plan. It now lays each of a product's own roles once. Do not use `front`, `main` or `twin` as a role name unless you have all three.
- **A visitor's order could not make an account.** The block `[signups.<id>.plans]` had no free account plan. Add the plan in section 4.2, step 4.
- **The guest could not reach the box.** The block had no `http_hosts`. The `http` door reaches only the hosts that the block names.
- **The guest blocked the async runtime.** A guest call blocks. Call the product from a blocking task (`spawn_blocking`), not from an async task.
- **Every estate got `git.<host>`.** The ONE box wrote `<host>`, `www.<host>` and `git.<host>` for each product. Now the product names its own names: put `names` in `default_shape` (section 5.2). Gunnar and Holger keep the three names.