THEPROTOCOL

As It Is

2026-10-13 · 28 min read · ruFFa
Pocket on a phone, Sandbox Alpha. The Send sheet for 0.5 SBXA a moment after Send was pressed: the registry answered, but the answer was dropped before it reached the phone. A red notice at the top says Connection Error. Under the Send button, in amber: The send may have gone through: no answer said whether it did. Pressing send again asks once more with the same request, which never pays twice.
A payment whose answer never reached the phone. The phone does not guess, and it does not call it a failure: it says what it knows and what pressing again will do. Pressed again, the same payment came back, once.

This is probably the last post I write before the code base opens. In Ready for Strangers I promised that the next time this journal mentioned the opening, it would be to link the repository, so I am bending that promise by exactly one post. Over the next days I tidy up what a stranger should not have to read. Then it goes out as it is.

"As it is" turned out to be the theme of the last ten days, too. Sooner or later, a system that moves value is asked something it cannot answer. A page asks its server and hears nothing. A payment is ready, but the ledger cannot record it yet. An answer leaves the server and never arrives. The easy thing, every time, is to guess, and software guesses constantly: it draws the page it would draw for an empty answer, or it sends a payment again because the first one never said it had arrived. This release is about not guessing. When the network is not sure, it now says so, in a sentence, and then it does the one thing that is safe.

All of it runs on the production frames, EU, ASIA, their four cloud operators and Frame A, with one exception that lives on the sandbox, where things are allowed to break: the ledger gate. I say so where it comes up. The whole release fits in one picture:

flowchart LR R["a read gets no answer"]:::f --> R1["the screen says it could not read,
offers the read again
and draws nothing it did not see"]:::a W["the ledger cannot record
(the ledger gate, on the sandbox)"]:::f --> W1["refused before anything moves:
Nothing was moved. Try again shortly."]:::x L["a payment's answer is lost
on its way back"]:::f --> L1["the phone says the send
may have gone through"]:::a L1 --> L2["the next press carries the same key
and gets the same payment back"]:::g B["a balance in the ledger
dips below zero"]:::f --> B1{"are its changes still waiting
in the TEG, for under fifteen minutes?"}:::q B1 -->|"yes"| B2["late: it resolves when they land"]:::a B1 -->|"no"| B3["wrong: a breach"]:::x classDef f fill:#2a1f47,stroke:#7a5cc4,color:#eadcff classDef q fill:#1a2740,stroke:#3f6ea8,color:#dce9ff classDef a fill:#0d3a4a,stroke:#2f8fb0,color:#d6f4ff classDef x fill:#3a1a1a,stroke:#a85454,color:#ffd6d6 classDef g fill:#0b3d2e,stroke:#1f8a5f,color:#d6ffe9

When it cannot see

For a long time the console was an optimist. When a read did not answer, it drew the page it would have drawn for an empty answer: "You have no agents", "All clear.", "0 members". On a bad connection it showed you a world in which nothing had happened, which is comforting, and wrong, and exactly what you do not want from the thing that shows you your money.

The console's own tester, the one that uses it like a person (it was introduced in the last post), now makes every read fail on purpose, one at a time, and writes down what each screen claims. Its first census found six screens that turned a failed read into a fact. The widened one found about 140, across the console, Pocket and the admin pages. Every one of them now says that it could not read, offers the read again, and draws no figure until its read has answered.

Most of them were only misleading. Two could have done real damage. The profile form drew empty fields over a failed read, and Save would have written those empty fields over the real profile. The two-factor panel said "disabled" about an account it had never managed to read, which invites a careful person to switch on something that is already on. A screen that could not see now refuses to act on what it did not see.

The same idea already ran underneath the screens. Since the design review, a permission check that cannot read its data refuses; it never guesses yes. The console has simply caught up with its own server.

When it cannot record

Under every frame there are two books. The TEG holds the balances and is the only thing that moves value. The ledger records every change, and the ledger is what the auditor reads and what anyone can check. Between them sits the TEG's outbox, which hands every change to the ledger in order and exactly once.

Until now, an outbox that could not reach its ledger changed nothing at the front. Payments kept going, the TEG kept every balance right, and the record caught up later. For a few seconds that is harmless. For an hour it would mean value moving that nobody can check yet, which is the one thing this network exists to prevent.

So on the sandbox every TEG now runs a ledger gate. While the outbox is healthy, the gate does nothing at all. While the outbox cannot reach the ledger, the gate refuses every request that would start a movement, before anything moves, and says so in one sentence. The registry, the console and Pocket pass that sentence on exactly as it came:

HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/problem+json

{
  "error": {
    "detail": "ledger_unavailable: The ledger cannot record this right now. Nothing was moved. Try again shortly.",
    "type": "service_unavailable",
    "context": {
      "headers": {
        "Retry-After": "30"
      }
    }
  },
  "type": "https://docs.theprotocol.cloud/errors/service_unavailable",
  "title": "Service Unavailable",
  "status": 503,
  "detail": "ledger_unavailable: The ledger cannot record this right now. Nothing was moved. Try again shortly.",
  "instance": "/api/v1/teg/send"
}

What happened, what did not happen, and what to do. The error messages this console used to show managed one of the three, and usually not the useful one. A movement that is already under way is allowed to finish, because stopping a trade between its two halves would be worse than letting it complete; its changes wait in the TEG's journal and reach the ledger, in order, once it is back.

For these pictures the ledger of Sandbox Alpha was switched off on purpose, on Monday morning, for 36 seconds. The TEG noticed 22.5 seconds in, refused two movements and none after the ledger was back, the changes that had been waiting drained, the books read zero, and the auditor recorded nothing at all. From 18 October the same drill runs by itself every Sunday evening, because a gate that is only tested when I remember to test it has my memory as its weakest part.

A longer run, ninety seconds, showed one more thing. Five payments that Sandbox Beta had already converted toward Sandbox Alpha waited at Sandbox Alpha's FX router until its ledger was back, and reached their receivers five minutes later. Nothing was lost, but a payment should not sit at a router for five minutes, so the frame that converts now asks the far ledger first, and converts nothing while it cannot record.

One small change made restarts honest as well. A frame's TEG used to start under a shell, and a shell that is the first process of its container ignores the polite request to stop, so every restart ended in a kill ten seconds later and a lease that nobody released. I can respect a process that listens to nobody, but not in production. The TEG itself is now the first process of its container, a restart takes about two seconds, and the lease goes with it.

On the production frames the gate comes next. Like every new refusal on this network, its first step there will be to count what it would refuse, without refusing anything.

When the answer is lost

The hardest question a payment can ask is whether it went through, when nobody answered. A train goes into a tunnel, a registry restarts, a gateway times out, and somewhere a payment may or may not have happened. The person holding the phone is the one who has to decide whether to press again, and a second press used to be a second payment.

That decision is no longer theirs to get wrong. Every request that moves value carries an idempotency key. The console keeps the key when an answer does not arrive. And since this release, the two routes every payment goes through keep each answer that may have been the payment under its key, for a day, and give it again to anyone who asks with the same key. Late on Monday, on Sandbox Alpha, a payment of 0.5 SBXA was sent from a phone, and its answer was dropped on the way back, the way a tunnel would drop it:

The registry had answered the first press with transaction 4e56d624, and the phone never heard it. The second press carried the same key and got the same transaction back. Two presses, one payment, and nobody had to know why.

The rule behind it is short enough to draw:

flowchart TB S["a payment arrives with its key"]:::f --> C{"what does the key hold?"}:::q C -->|"nothing yet"| RUN["claimed, and kept alive
while the payment runs"]:::a C -->|"a send still running"| WAIT["an earlier send under this key
is in progress: ask again shortly"]:::q C -->|"an answer"| AGAIN["the same answer, given again:
nothing moves a second time"]:::g C -->|"a send that ended
with no answer"| UNK["outcome not known:
never run a second time,
a person reads the ledger"]:::x RUN --> A{"what did it answer?"}:::q A -->|"proof that nothing moved"| FREE["the key is free again"]:::g A -->|"anything else"| KEEP["kept under the key for a day"]:::a classDef f fill:#2a1f47,stroke:#7a5cc4,color:#eadcff classDef q fill:#1a2740,stroke:#3f6ea8,color:#dce9ff classDef a fill:#0d3a4a,stroke:#2f8fb0,color:#d6f4ff classDef x fill:#3a1a1a,stroke:#a85454,color:#ffd6d6 classDef g fill:#0b3d2e,stroke:#1f8a5f,color:#d6ffe9

A key is freed only by an answer that proves nothing moved: a refusal before any value left. Every other answer is kept and given again. A request that ended with no answer at all, because its server stopped halfway, is answered as exactly that, outcome not known, and is never run a second time. A person reads the ledger and settles it, which is slow, and which is the point.

I would rather say plainly that this rule came late. The AGORA venue's gateway pays traders on other frames through the same send everyone uses, and its key remembered only the payments that succeeded. A payment whose answer was lost left nothing behind, so the same key sent it again, and on the sandbox that had paid fifteen traders twice since September, the last of them during the drill that caught it. A drill, it turns out, is a test that charges admission. The gateway now pays again only when an answer proves the first payment reached nobody, the 985 payouts it had left stranded or marked paid without paying went out after a census of the ledger, each one read back, and its key remembers everything now, not only the good news.

A payment that comes back should come back whole, too. When a gateway turned a route down, its payer used to lose two fees of half a percent on the round trip, so a payment of 0.5 SBXB came back as 0.495012. Now the payer's own frame gives both fees back the moment it learns that the payment came back, and a gateway that can only send a payment back later carries the fees in its refund. The payer receives exactly what it sent.

Late is not wrong

The auditor reads every production ledger once a minute, in one statement per frame, and belongs to no registry. One of its checks asks whether anyone's balance in the ledger is below zero. That can happen for an honest reason: the ledger has received a spend while the credit before it is still on its way. Until now the auditor called that a breach. It is not a breach. It is late.

Version 2.5.0 asks the TEG. If an agent's changes are still waiting in the TEG's journal, and the oldest of them is younger than fifteen minutes, the balance is behind and resolves when they land. If nothing is waiting, or something has waited longer than that, it is a breach, as before. Every cycle the auditor also counts the TEG's own negative balances directly, a number that should always be zero, and a count it could not read is reported as not checked, never as zero.

Before it went live, the new version ran beside the old one for an hour, over the same frames: thirty rounds of five frames, and the two agreed on every one. Then it took over with the same signing key, the same history and the same chain of attestations. The chain did not notice the change of hands, which is the whole point of a chain.

The first members

On the production frames, the change of the last post now has names behind it. A developer becomes a member of a registry once one of their agents has held its bond there for thirty days, and EU's first members crossed that line on 7 October. Today EU has two members and ASIA one, every vote on both frames is a member's, and neither has opened a commons season yet.

The Registries page on EU, read from the federated index, which reads six registries. EU in Brussels, ECU: 2 members, 2 joined and none left in 30 days, no commons season yet. ASIA in Singapore, ACU: 1 member, 1 joined. The four cloud operators, Amsterdam, Lisbon, Ho Chi Minh City and Shenzhen: no members yet, their commons switched off. On every registry: votes, one per member, weighted by membership.
Every registry the federated index reads, with its members and its commons, as EU's console shows it. Membership counts people, not holdings, so the page shows no amount at all.

Three members on two frames is not a crowd. It is, however, a quorum of people who chose to be there, which is the only kind I have ever trusted.

Who builds this

A repository tells you a great deal about a project and almost nothing about the person behind it, so before mine goes out, a few words about that.

This network is built by one person. I used to do IT support for a living. These days this is my full-time work, and most of it is learning, because I would rather not fall behind while people are still part of this kind of work. Full time turns out to have no closing hours, which is why so many of the times in this journal are after midnight. I once wanted to build things like this inside the tech industry. The industry decided it could do without me, and not every hand on that door belonged to a stranger. Since then the project has been fought on more levels than I knew a project had. I will not go into them here. They are not what this journal is for.

I am fine. I was always fine. I am doing what I am doing, and nothing has stopped it so far, which I take as a fair forecast. I wrote about the price of building this alone once before, in Twenty-One Blueprints, Drawn Alone. The price has not gone down, and I am still paying it on schedule.

It also explains the software better than any diagram in it. Build as if nobody will ever vouch for you, and you end up with books that anyone can check without asking you, refusals that say why, and a payment that happens once whether or not anyone is watching. Most of this release fits in that sentence.

As it is

Step back from the screenshots, and this release changes one thing: what the network says when it is not sure. A screen that could not read says so. A payment the ledger cannot record does not happen, and says why. A payment whose answer was lost is answered again, never sent twice. A balance that is late is called late, and a balance that is wrong is still called wrong. None of it makes the network faster, and none of it would look good on a poster. It is the part of a system you only notice when it is missing, usually at the worst possible moment.

That is also what goes out in the next days. Not a polished subset and not a demo, but the network as it is: the frames and their TEGs, the ledger and its auditor, the console and the tester that uses it like a person, the kit, the gates, and the tests that hold what every run so far has taught them. Whoever picks it up can start from where I stopped, which was always the point.

The next time this journal mentions the opening, it will be to link the repository. This time I mean it.

Patchnotes

The last post went out with V0564, the release that switched the production frames to membership. What follows is V0565 through V0606, ten days of work, every version on the sandbox before any production frame, with the supply delta at zero throughout on every frame the auditor watches: its breach log has no entry since August. A line that names no place runs wherever the release runs, production included. "On the sandbox" marks the one part this release does not carry to production: the TEG's and the store's half of the ledger gate, which goes to production next and starts there by counting what it would refuse.

Membership on the production frames

The console says what it could not read

Sending and paying

When the ledger cannot record

Paying once

The auditor

Getting started

Organizations and mail

Cloud operators

Access rules, now on every production frame

Federation and discovery

Identity, policy and the platform

Monitoring

Public pages, documents and legal texts

Smaller things

Tests