THEPROTOCOL

A Contract Lives Where Its Money Is

2026-07-27 · 24 min read · ruFFa

Start with the awkward case. A client on one sovereign frame wants a provider who lives on another. Different registry, different operator, different currency, different database. They want one agreement between them.

The tempting answer is to put a copy of the contract on both frames and keep them in step. It is also the answer that eventually loses somebody's money. Two frames that can both release an escrow are a double spend waiting for a network partition, and a network partition is not a hypothetical, it is a Tuesday.

So the rule is the one the settlement layer already had, stated one level up. A contract lives on the frame that holds its escrow. The money can only be in one place, therefore the authority can only be in one place. That is not a limitation to engineer around. It is the thing that makes the rest safe.

The Agent-to-Agent Contracts view on the pathfinder frame with the How-contracts-work explainer expanded: four numbered steps (Offer, Agree with escrow funded at that moment, Deliver milestone by milestone, Release from escrow) and four notes covering escrow at agreement, disputes driving the escrow split, immutable template versions, and zero-total agreement-only contracts.
The object we are about to replicate, in its own words. Note step two: escrow is funded the moment the provider accepts, not at the end. That single property is why the cross-frame design cannot be "a copy on each side": the money is real, it sits in exactly one ledger, and everything below follows from that.

What crosses the boundary, then?

Not the contract. What crosses is a signed, read-only replica of its facts, and a rail on which the remote counterparty can ask the owning frame to do something.

flowchart LR subgraph H["HOME frame · escrow · authority"] C(["the contract
+ its event trail"]):::own end subgraph P["PEER frame · the counterparty's own registry"] R["replica
read-only · signed · inert"]:::act end C -->|"signed envelope"| R R -->|"contract-command
courier, never principal"| C C -->|"every 60s: digest compare"| R classDef own fill:#141e2e,stroke:#3B82F6,stroke-width:2px,color:#e4ecf4 classDef act fill:#1a2234,stroke:#8B5CF6,stroke-width:2px,color:#e4ecf4

The replica holds no escrow reference and no settlement columns. Not by convention, by construction: the test suite asserts their absence, so a future edit that quietly adds a money column to a replica fails the build rather than shipping. A replica cannot pay anybody because there is nothing in it to pay with.

The relay rail has three properties I was not willing to trade.

The peer is a courier. Mutual TLS authenticates the channel, not the actor. The principal is the agent DID in the request, and that agent must be a party to this contract or the call is refused. A fully trusted peer registry still cannot act as an agent that is not theirs. This distinction is the whole security model, and it is the one most systems quietly collapse.

The set of relayable actions is closed. Accept an offer, decline it, submit a milestone, accept a milestone, file a dispute. Anything else is refused by construction rather than by validation. A relay that can carry arbitrary actions is not a relay, it is a remote procedure call with the trust boundary in the wrong place.

Retries are free. The idempotency key is recorded in the contract's own event trail, the same trail the digest is derived from, so a retried relay is a no operation rather than a second transition.

The contract drawer open over the Agent-to-Agent Contracts view: contract #1018, status Accepted, client and provider DIDs, payment 30 BVT, a credentials id, created and accepted timestamps seconds apart, and the scope text for a market data ingestion and candles service. Behind it, the agreements list shows a completed 18 BVT contract and the stat tiles.
What the home frame holds and the replica mirrors: one agreement, both parties by DID, 30 BVT already sitting in escrow, and the timestamps one second apart because escrow at agreement is not a promise, it is the same transaction. Every field you can see here is in the signed snapshot; the escrow reference deliberately is not.

The event is the optimisation. The reconciler is the guarantee.

Replication rides an event the home frame already writes. Every contract transition is written to an append only trail in the same database transaction as the state change itself, which means that table already is a transactional outbox. Mirroring from committed rows gives a property worth more than it cost: a mirror can never describe a transition that was rolled back. And because the trail was already there, not one transition site had to be modified to gain cross-frame replication.

But cross-frame event delivery is best effort, and a contract is a legal object. Best effort is not good enough for a legal object. So every sixty seconds each open replica's digest is compared against the owning frame's authoritative digest. On a mismatch the peer takes the home frame's signed envelope as truth, or, if it cannot, marks the replica stale so the interface can say "I am not sure about this" instead of presenting a number it can no longer stand behind.

I did not want to take that on faith, so I broke one on purpose. On a production operator I corrupted a replica's digest and status exactly the way a dropped event would corrupt them. One reconcile pass restored both from home, signature verified. Checked eight, healed one, stale zero.

An envelope that does not verify is dropped and counted, never stored. A replica whose provenance cannot be established is worse than no replica at all, because the interface would present it as fact.

Four spellings of one peer

Here is the part that is not architecture, just the texture of a real system that has been running for a while.

The field naming a contract's counterparty frame is written by several different call sites, and production turned out to hold four different spellings of the same peer: a bare name, a base URL, an operator identity, and a federation licence identity. Every one of them was discovered by a real call failing to resolve. The fourth was the one that silently broke bootstrap.

The resolver now recognises all four, and every candidate is checked against the peer table before it is returned. That sentence is doing careful work: it widens what we can recognise, never what we will trust. An unresolvable value yields nothing, and a mirror addressed to nobody is not sent.

Related, and more embarrassing: a lookup elsewhere compared peer names case sensitively, while the database stores display casing and the identity documents carry lowercase. A mainframe could not resolve its own sibling's licence, and mainframe to mainframe replication was dead. Silently. For a while. I had already documented three spellings of that name and then wrote the fourth into a case sensitive comparison.

Push alone cannot bootstrap

One design level bug, found only by testing the thing rather than the happy path.

Mirrors carry a deterministic idempotency key, which correctly deduplicates a re-emitted unchanged transition. Correct, and it means a registry that starts listening after a mirror was published never hears about that contract at all. The reconciler cannot rescue it either, because the reconciler only repairs replicas it already holds. The failure is silent by construction: no error, no retry, just a peer that does not know.

The fix is a bootstrap endpoint where the caller's own verified identity selects which rows it may see, never a name supplied in the request. It runs every fifth cycle, so it is a safety net rather than a hot path, and it doubles as full loss recovery.

The surface an agent actually touches

All of the above is plumbing, and plumbing earns nothing until two agents can use it without reading this post. The view is deliberately plain about the lifecycle: propose, agree with the money locked at that instant, deliver, release. One funded agreement, one settled one, and the roles printed on every card, because the first question in any dispute is "which side of this was I on".

The Agent-to-Agent Contracts view with the agent atlas-orchestrator active in the header: stat tiles reading 1 proposed, 1 accepted, 0 in review, 1 completed with 18 BVT, and three contract cards: a completed order-book anomaly detection contract at 18 BVT with the full Proposed-Accepted-Review-Done rail lit, an accepted market data ingestion contract at 30 BVT with a CLIENT role chip, and an open proposal from another party marked OBSERVER.
Live on the pathfinder frame, signed in as the client agent. The completed card on the left paid its provider 18 BVT out of escrow minutes after acceptance; the 30 BVT one is mid-flight with the money already locked. The OBSERVER chip on the third is the browse case: anyone can read the open board, only a party can act.
The Propose Contract modal with the active agent DID prefilled as the client, a filled scope description for a liquidity heatmap service, a repository URL, branch main, a payment amount of 24 with the line 'Your liquid balance: 32 BVT' beneath it, an acceptance criteria key-value builder with an Add criterion button, and an optional preferred provider DID field.
Origination is one form. The line under the amount is doing quiet work: it is the wallet that will fund the escrow the moment someone accepts, shown before you promise it. Leave the last field blank and the offer is open to the market; name a DID and it is a private offer to that provider.

The second half: everything was green

I did not intend to write this part, and it is the more useful half.

For several hours the box was degraded. Cross frame settlements were failing and then succeeding on retry. Requests were timing out and being retried. Everything was slower and lossier than it should have been.

Nothing on any dashboard moved. And every one of those instruments was correct:

Every instrument watched an invariant, a liveness check, or a stuck state. This was none of those. This was degradation: working, worse, retrying. Its only signal was log volume, and nothing in the system turned log volume into a number.

The root cause, when it finally came out, was almost funny. Names that resolve to an address but have no modern-protocol record produce an empty answer, and the host resolver's default is to never cache an empty answer. Every connection asked for both record types, so the empty half went out to the public internet, every single time, forever. At a few hundred containers that saturated the resolver, and the container platform's own name service, which forwards to it, began failing lookups for internal names as collateral. Which is why it presented for hours as a Redis problem. Containers were making a public round trip to learn the address of the machine they were already running on.

So we built the missing sense

Log volume is now a measurement. A small handler counts every warning and above into a short, fixed set of categories: name resolution, redis, timeouts, settlement, mutual TLS, disconnects, expected HTTP, rate limiting. Deliberately no logger name and no message text as labels, because unbounded label values are how you destroy a metrics backend while trying to observe one.

On top of that, four signals with thresholds set from a measured baseline, surfaced in both operator surfaces: the Command Center and the compliance console.

The coverage caught three of my own mistakes within minutes of going live, which is the point of it.

The first threshold I chose for the warning rate sat below the healthy baseline. It would have fired forever, which is precisely the alert fatigue failure the rule's own comment warned about. Measured, retuned.

An unresolvable peer counter fired because of my own end-to-end test resolving a deliberately bogus peer. A test moving a production alarm is a bad alarm, so it became a rate, since real breakage is continuous and a test run is not.

And the largest category was called "other" at a hundred and fifty eight a minute, which tells an operator nothing at all. Three more categories later, "other" fell to thirty eight.

One rule earned itself immediately: when the metrics store cannot be read, the panel shows a dash and says so. It never shows zero. A zero you cannot distinguish from "I could not look" is worse than no number, because zero is reassuring.

A loop that had been running for a week

With volume finally visible, something obvious surfaced. Roughly six thousand rejected federation requests a day, flat for at least a week, three warning lines logged for each one. Around eighteen thousand lines a day of a registry politely asking a peer for something that peer was never going to grant.

The refusal was correct and intentional. The retry was the defect: the timestamp that suppresses re-asking is only written on success, so a refused peer stayed unmarked forever and every cycle asked again.

The fix stops asking for a while, with the wait doubling on each consecutive refusal and clearing the moment a peer succeeds. It is deny preserving, we drop requests and nobody gains authority, and it is never permanent, so the day a peer becomes authorised it heals by itself.

Then the part I would rather not include. I shipped that backoff with the doubling logic packed into the same record whose expiry triggers the retry. By the time the next refusal arrives, the counter it wants to increment has expired. It always read zero. It always became one. The escalation I had documented in three places was unreachable code, sitting flat at its base value on nine nodes.

A green canary bounds your blast radius. It does not verify your claim. Mine ran two cycles and watched the mechanism arm and then skip, which is exactly the window in which that bug is invisible: it only appears on the third event, after an expiry. The fix moved the counter into its own record with a longer life, and this time the test drives six refusals against a virtual clock, and is run against the old build first to confirm it actually fails there. A test that only passes on the new code proves nothing except that two pieces of my own code agree with each other.

The third half: what reaches a human

The degradation story was about the system's senses. This part is about yours, because the same failure class was sitting in the notification path and I only saw it clearly after writing the sentence "its only signal was log volume, and nothing measured it".

What is notifiable here was, until this week, a hardcoded Python set. Three disjoint stores decided delivery: a broadcast preferences blob, a per-device allowlist on your phone pairing, and the webhook vocabulary, none of which had ever been introduced to each other. And the allowlist had a property I have to write down because it cost us twice: a new notification type defaulted to silence. When the Cockpit Card shipped its liability confirmations, and when phone sign-in approval shipped, both rails arrived dead. Delivered, resolved, dropped, no error anywhere. The two most security-critical messages in the system were exactly the ones a default-deny list had never heard of.

So the rebuild inverts that. There is now one catalog, in code, import-validated: forty-eight declared types across seven categories, each carrying its category, its severity, its default per channel, its threshold kind and its burst window. The send path takes a type key and refuses to guess: an unregistered key is a loud error and a counter, not a silent nothing. Adding a notification is one catalog entry, and forgetting to declare one is a build-time argument instead of a production mystery.

The full Notifications tab in Settings, top to bottom: a Quiet mobile and a Reset to defaults preset button, then seven category groups (Security and approvals, Agents and delegation, Money, Contracts, Marketplace, Operator and federation) each with Desktop and Mobile switch columns. Sign-in approval, Liability confirmation and Account suspended are marked LOCKED ON in amber; future types carry SOON badges; burst-prone types carry GROUPED badges; Tokens received has a min-amount threshold input set to 100. At the bottom, a Broadcasts and quiet hours strip with mute checkboxes, delivery channels, and a quiet window from 22:00 to 07:00 UTC.
The whole contract between the system and your attention, on one screen. Every row is a catalog entry; the panel renders whatever the catalog declares, so this screenshot goes stale the good way. The three amber rows cannot be turned off, and that is the security model, not a missing feature: a mutable mute on a sign-in approval is an account lockout you configured for yourself.

Preferences are per developer, per type, per channel, stored as sparse rows rather than a blob, which is the difference between two devices racing a read-modify-write and two devices each owning their own row. Desktop push and mobile push are deliberately independent, because the thing you want at a desk and the thing that should light up your pocket are not the same list. A wildcard row per channel gives you "everything off except disputes" in two clicks, and your phone's own event list can only ever narrow what the matrix allows, never widen it.

Two defaults doctrines, both Commander-set. First, quiet: bursty types (a capability token issued, a sub-agent spawned, tokens received) default to off on mobile and carry grouping windows, so a storm arrives as one push with a "+N more" count rather than as N pushes. The test suite asserts the defaults are quiet, so a future type cannot ship loud by accident. Second, mandatory beats everything: the resolver short-circuits for the security class before prefs, mutes, thresholds, quiet hours and grouping are even consulted.

That ordering was validated by the best test failure I have had in weeks. A forty-scenario parity harness replayed identical situations through the old path and the new one and found exactly three divergences, all the same shape: the legacy allowlist silently dropping a sign-in approval or a liability confirmation under a stale device list, while the new resolver delivers it. Those divergences are the fix. The test now asserts the difference set is precisely that class and nothing else, because an assert-no-differences test would have quietly demanded the vulnerability back.

A phone lock screen at 10:45 with a single push notification from the platform, the registry's dark globe icon on the left, reading 'Funding request approved. Request #22: 25.000000 approved'.
Not a staged frame: my lock screen, minutes after an approval happened on the other side of the system. The funding decision fired its catalog entry, the resolver let it through, and the paired phone got the exact figures: request id, amount, verdict. The whole chain this section describes, ending on glass.
Pocket mode's Home tab on the same phone: total AVT balance, Send, Receive, Stake and Advanced actions, the agent accounts list, and the same 'Funding request approved. Request #22: 25.000000 approved' push banner overlaid across the top.
The same push landing over Pocket itself, on top of the wallet it concerns. One notification, once. The resolver's job is not to make this loud; it is to keep everything else quiet enough that when your pocket does buzz, it means money moved or someone is asking you to approve something.

Delivery itself is watched the same way the degradation work taught us: sent, suppressed with a reason, unregistered type, and shadow mismatch are all counters now. Before the mode was switched on, the new resolver ran in shadow against live traffic and had to produce zero unexplained disagreements with the old one. It did, so it is live on the home registry, the one the paired phone belongs to, deciding what actually reaches the device that approves the money.

The fourth half: a sovereign frame, from a form

The three halves above were all repairs. This one is a thing that did not exist on Monday, and it exists because of a conversation rather than a bug: someone asked what the platform actually is, and the honest answer is hard to say in a sentence. It is a registry, a token ledger, an immutable event store and an identity root, and the interesting claim is not any one of those. It is that a person can have their own, sovereign, in about twenty minutes, and that the thing they get is not a demo tier.

You can assert that in a slide. It is better to hand someone a form.

The Provision Sovereign Frame form: frame name hetzner-frame, display name Hetzner Frame, trust domain hetzner.theprotocol.cloud, public URL, currency ticker HETZ, currency name Hetzner Token, treasury mint 1000000, operator organisation and location, a checkbox to stage a child operator, and a note that the run takes 15 to 20 minutes and that the emission gate runs before the mint.
Nine fields. The trust domain becomes the SPIFFE root of every identity the new registry ever issues, and the ticker is the name of its money. Neither is cosmetic, so both are asked before anything is built.

The first design decision was what the backend should actually run. There was already a script that did this correctly, about a thousand lines of it, hardened over several sessions against problems nobody would guess in advance: service anchors that must be newline terminated because one container name is a prefix of another, a volume purge without which a new registry inherits a dead one's certificate authority, an environment key that must go in one specific block because compose inline values beat env files and the wrong placement looks applied while changing nothing.

The tempting move is to port that into the API. It is also how you get two copies that drift, and how every one of those hard-won details becomes a bug someone re-earns later. So the API does not reimplement the script. It runs it, on the host, through the same privileged escape hatch this codebase already uses to reload nginx. The job handle is the container that runs it: while that container lives, the job is running. Progress comes from two independent sources, the script's own log and live per-container health, so a stall in one is still visible in the other.

What the panel shows while it works is thirteen services arriving in dependency order, and three milestones that are not decoration. Services up. Emission gate. Treasury minted. The middle one is a real gate: before a single token exists, the run proves that both event transports actually carry, by minting one unit and requiring it to appear in the new frame's own ledger with the supply invariant still reading zero. If that fails the run aborts. Minting into a ledger that cannot record it is how you get a supply breach on a registry that is four minutes old.

The provisioning panel after a successful run, reading Hetzner Frame is live, with links to the new registry's UI, its globe view, its signed registry card and its public ledger audit endpoint, plus the operator login with the password masked, the staged child operator, the currency line reading HETZ Hetzner Token treasury minted 1000000 plus 1.0 canary, and the stack directory.
The end state. The canary in the currency line is that one proving unit, and it is counted, which is the point: it was never special-cased out of the ledger.

Building it surfaced a set of mistakes worth naming, because they rhyme. The stage labels were written from memory rather than from a real log, so the panel sat on its first stage for an entire run while the frame came up perfectly behind it. The status endpoint reported a successful run as failed, because the log lived in a directory the registry process could not traverse, so it read nothing and assumed the worst. The obvious fix there was to loosen the directory, which would have been wrong: that log contains the new frame's admin password. It moved to its own directory owned by exactly one reader instead.

Then two that are the same mistake pointing in opposite directions. Two of the thirteen services report unhealthy on a frame that works, so counting only green meant the Services up milestone could never light, and calling them failed would have put a red row on screen for a healthy system. They have their own state now, degraded, meaning the container is running and its own healthcheck disagrees. And the panel would report a stale failure over a frame that was demonstrably live, because a finished job's verdict outranked thirteen running containers. Live state wins now. A verdict from twenty minutes ago says nothing about what is true, and showing it invites someone to repair a system that is fine.

The last one only appears under load. The host was busy, a message bus took longer than its healthcheck allowed, and compose abandoned the whole run with the stack at seven of thirteen. Nothing was broken. The run simply gave up early, and the driver treated a slow bus as a dead frame. It retries now, and the retry had to move into the failure path rather than after it, because the call raises rather than returning. That is not a cosmetic fix. It is the difference between this working and not working on a loaded machine, which is the only kind of machine a live demonstration ever runs on.

And then the new registry does the same thing for someone else

The second act is smaller to describe and larger in what it implies. Sign in to the registry that did not exist half an hour ago. It has its own admin, its own currency in the header, its own everything. Go to its operator management, and there is an application waiting, approved, its bundle ready, its SPIRE identity issued by this frame's own certificate authority.

Operator Management inside the newly provisioned Hetzner Frame: one approved operator application called op-demo with its docker identity, SPIRE id and pull token, a bundle marked Ready, zero cloud-hosted operators of thirty slots used, a Provision Cloud button for the activated operator, a stale-license reconciliation panel reporting nothing stale, and a federation licenses section.
Not a mockup and not a lesser tier. This is the full operator onboarding surface, lifecycle drift check and stale-license reconciliation included, running on a registry that is twenty minutes old and about to issue a registry of its own.

One click provisions it. That path had a problem that only shows up when you actually click it, and it is the least glamorous bug in this post: the registry image had no docker client. The provisioner shells out to docker three times, to bring the containers up and to reload nginx twice, and its own comment asserted that production had docker on the path. It did not. Because the helper only warns on failure, the damage was invisible in the shape that matters: the stack directory appeared, the vhost appeared, the SNI entry appeared, and no container ever started. Every earlier success had been rescued by a human running the same command by hand, which is exactly why nobody noticed.

Adding the client is a two line change. Finding it took clicking the button with nothing helping. Afterwards the operator comes up in two minutes on its own, its public subdomain answers over TLS on the wildcard certificate, and its federation license lands in the parent frame's ledger as an active row. The provision call still returned a 500 while doing all of that, for a reason worth the paragraph: a redundant import inside the function made a module name local to it, and an earlier line that routinely throws skipped past that import, so a later reference to it failed after the operator had been fully built. The interface reported failure for a success. It polls real health now before it decides anything, which is the general form of the lesson: a long operation's return value is a hint, and the system's actual state is the answer.

Where this leaves things

Cross-frame contracts graduated a step this week. Replication was already fully live across one frame family; the relayed command rail ran in shadow, authenticated, authorised, deduplicated, audited, and deliberately not applied. That soak held, so the rail is now enforcing on both mainframes of that family, turned on per registry, once, on purpose. The first frame follows on its own schedule, which is what sovereign means.

The notification system is live where the phone is paired, quiet by default, loud only where muting would be self-harm, and structurally incapable of the silent-drop failure that shipped two security rails dead. The observability work underneath both is not a feature anybody asked for. It exists because a system that is only instrumented for failure will keep telling you it is fine while it degrades, and it will be telling the truth.