The latency, as a shape.
Steer a dot. It leaves the path the contract recorded behind it — every vertex a storage write, nothing drawn between them. On the base chain the state can only move once a block, so a curve comes out a polyline with three corners a second in it. Through a node it comes out a curve. Same contract, same gesture, same code drawing both.
A single button press does not sell a hundred milliseconds, because nobody feels one. Continuous input does, and it leaves the evidence on the screen where it can be compared instead of trusted.
taking a seat
Hold the pointer down inside the square and drag, or use the arrow keys. Your dot moves only when a step lands, so what you see is where the state says it is.
4 seconds of the identical gesture, so both rails can be given the same input and the two paths compared without a steady hand. A heading turning at a constant rate while the dot travels at a constant speed is a circle — and how close to one it comes is exactly how often the rail let the heading change.
A bot here costs nothing to run: an ephemeral transaction costs no gas, and a grant is a signature rather than a transaction.
nothing drawn on this rail yet
no transaction on the chain to pay for
nothing drawn on this rail yet
off the receipt of your last step
Draw on one rail, then switch and draw again, and this fills in. The vertex counts come out of state, the durations are wall clock in this tab, and the gas is off the chain's own receipts.
A real game would predict the movement
It would, and it should. Every shipped multiplayer game draws the dot where it expects the dot to be and reconciles afterwards, and a game built the way this page is built would feel bad on any chain. Nothing here predicts anything: both rails draw the vertices the contract recorded and no others, which is why one of them looks like a slideshow.
The honest version of the objection is that prediction is a way of not having a source of truth for the interim. It works, and what it costs is that between two authoritative states nobody can settle a disagreement: who hit whom, who got there first, whose input actually arrived. That interim is where the money is in anything with stakes, and shrinking it from a block to a millisecond is the whole product.
And a transaction per movement is absurd
On the base chain, yes — and that is the finding rather than the setup. A step is one storage write, about the cheapest thing this contract can do, and it still costs gas, a nonce and a block every time. Nobody would ship it. The question the page is asking is what the same code does when the state it writes is not on a chain charging for it.
What settling actually costs is the other half of the answer. A trail is a ring of twenty-four words, so a dot steered four thousand times commits as the same twenty-four slot writes as one steered two hundred times. The gas per step is not what the design is for. The gas per session is, and it is bounded by the ring rather than by the play.
Two deployments of one contract. One was handed to a node and the other was left where it was, and which is which is not something this page decides — it is what the base chain answers when you ask it. Everything below was read from the chain when this panel loaded.
reading it off the chain…
Every one of those is a distinct address holding a distinct seat, and the contract judges each of them with _actor(). A bot is not a second shape drawn beside your dot: it signs its own steps and goes through the same steer on the same rail, which you can confirm by reading the seat back and seeing an address that is not yours. What a bot is not is a person, so it says so on the canvas.
The path on the canvas is these words and nothing else. snapshot() is what the page reads sixty times a second, packed; trailOf() is the same thing in plain numbers, and the contract's tests assert the two agree. Compare the last pair against the end of your own trail on screen.
# which of the two the chain says is delegated cast call 0x17ecF288bAf71Fe722B5aFDdC2c3456ae0089820 'isPartitionLocked(bytes32)(bool)' \ 0x0000000000000000000000000000000000000000000000000000000000000000 --rpc-url https://testnet-rpc.monad.xyz cast call 0x1b3bF66c32BA54dd3346e2C1d1cAFce29a4B97D4 'isPartitionLocked(bytes32)(bool)' \ 0x0000000000000000000000000000000000000000000000000000000000000000 --rpc-url https://testnet-rpc.monad.xyz # the path a seat drew, as the contract holds it cast call 0x17ecF288bAf71Fe722B5aFDdC2c3456ae0089820 'trailOf(uint256)((uint16,uint16,uint8)[])' \ 0 --rpc-url https://rpc.interludelayer.xyz # the same words the canvas actually reads cast call 0x17ecF288bAf71Fe722B5aFDdC2c3456ae0089820 'snapshot()(uint256[],uint256[])' --rpc-url https://rpc.interludelayer.xyz # every slot the delegation covers, and the geometry both instances run cast call 0x17ecF288bAf71Fe722B5aFDdC2c3456ae0089820 'delegatedSurface()(bytes32[],bytes32[],bytes32[])' \ --rpc-url https://testnet-rpc.monad.xyz cast call 0x17ecF288bAf71Fe722B5aFDdC2c3456ae0089820 'geometry()((uint256,uint256,uint256,uint256,uint256,uint256,uint256))' \ --rpc-url https://rpc.interludelayer.xyz
The delegated surface is five heads and 120 trail words, every one an exact global scalar. That is not a stylistic choice: a node process serves one partition, and of the two global kinds only exact slots can be committed — a diff on a mapping entry has to carry its key and a node cannot invert a keccak to recover one. So the seats are a fixed ring, and one node serves all of them.
Do not deploy this page as it is, with a funded key in it
The on-chain rail signs in the browser, so the account it pays with arrives in the JavaScript bundle. On a local chain that account is one of anvil's published ones and worth nothing anywhere. This build is pointed at a real network, which means whatever NEXT_PUBLIC_INTERLUDE_DIRECT_KEY holds can be read out of this page by anybody who loads it and spent anywhere.
The per-visit cap of 1.50 MON bounds one tab, not the world: it stops a demo quietly emptying a faucet, and it does nothing at all about somebody who simply takes the key. For a public deployment the on-chain rail wants either the visitor's own wallet or a relayer of yours with its own rate limit. The seam is already there — a dot on that rail is an address that signs its own steps, so the only thing to replace is where it is funded from, in lib/arena-rails.ts. The Interlude rail needs none of this: it is a generated keypair signing a grant, and it never holds anything.