A decision the URL could not carry
10 October 2026 · figures measured 10 October 2026
This site offers a rehearsal: a demo wallet with fixture money, so somebody can walk the whole flow before deciding whether to paste a private key into a page they found yesterday. It also offers a real plan, from a button on the front page and from a button on every pair page.
Walk the rehearsal, go back to the front page, press Plan Exchange. The pair arrived correctly and the money was still pretend. The right coins and a fixture wallet, which is the one combination this must never produce.
Four fixes, same two files
It had been fixed before. Twice, fifteen days earlier, in the same two components — and then twice more on the day this was written, thirteen minutes apart.
| What it changed | What it still took for granted |
|---|---|
| A leftover rehearsal stopped counting as work in progress | That the refusal was the problem, rather than the rehearsal outliving itself |
| The second page stopped erasing a query it had not written | That it would have nothing of its own to say |
| The flag got an off switch, read on arrival from the query | That the query would still be there to read |
| The decision moved to the click | — |
Every row but the last is somewhere along the path a query string travels from one page to the next. Three fixes in that corridor; the fourth left it.
A race decided by nesting
The front page puts the choice in the address bar and navigates. The app page writes its own position into the address bar as soon as it has a wallet, replacing what is there. The thing that reads the choice is the session provider, which sits in the layout above both.
So the writer is a child of the reader, and React flushes a child's effects before its parent's. The rewrite does not usually win; it always wins. That is not a timing problem and reading sooner cannot fix it — a version that captured the address bar synchronously in the provider was shipped on that theory and did not work, which is how the ordering came to be measured rather than assumed.
The fix that did work was the opposite: the writer yields. When it has nothing of its own to say it leaves the address bar exactly as it is, and the parameters survive until somebody consumes them.
A yield has a precondition
That held for two weeks, and it is also why the third fix failed. A yield conditioned on having nothing to say lapses the moment there is something to say — and a leftover fixture wallet is something to say. The writer announced a step, the query naming the pair went with it, and the off switch read an address bar that no longer mentioned the demo either way.
The two bugs are therefore one bug seen twice. The second is the first one's fix running out of road, in the exact case where stale state is what is wrong: the yield is disabled by the condition it exists to protect.
Where a decision belongs
A query string is a message in transit. Anything downstream of it can drop it, and in this codebase something downstream does, by construction. A decision put into one is a decision held by whoever has not yet overwritten it.
The button already knows which button it is. Pressing Plan Exchange now puts the rehearsal down at the click — the fixture wallet, the demo exchange and the demo plan together — before any navigation happens and where nothing can rewrite it first. Both real-plan entry points call the same thing, and it is guarded so it cannot discard a real plan somebody is part way through. A real wallet connected in that tab survives, because its key lives only in memory and re-entering it would be the price of somebody else's bug.
What caught it, and what did not
Not a test. The third fix was reasoned carefully, read correctly against the wrong end of the problem, and shipped. What caught it was opening the deployed site and walking the sequence: rehearsal, front page, Plan Exchange, and a heading still reading Demo wallet.
A comment now sits beside the fix explaining the ordering, and Three comments and a check is the post arguing that a comment is not enough — that a repeated explanation is evidence a guard is owed. Four releases is past that threshold. The honest position is that this one is guarded by a note and a manual walk, which is the weaker half of what that post recommends, and that the check it is owed would have to drive the sequence rather than read the source.
Method
Drawn from this site's own release history and repository on the date above. The four releases, their timestamps and the comments quoted here are all in the tree; the effect ordering was established against the live build rather than from documentation, by watching a navigation carrying a query be followed by a replacement that did not have one. Verified in both directions afterwards: Plan Exchange reaching a real wallet with the pair intact, and the demo button still reaching the rehearsal.
Every figure above can be checked against the providers yourself — no account, no sign-up, and nothing is held for you at any point.