One string, on chain, forever. Pay more than the last writer paid and the
slot is yours. When the clock runs out, whatever is in it stays there
permanently.
Price("famous last words") # what it costs right now, in ugnot
Write("famous last words") # send that much with the call
Settle() # after the clock runs out, anyone may call it
Withdraw() # sweep what you are owed
The artefact at the end is the point: this is a bidding war over an epitaph, not
over a prize. It is also the smallest thing that exercises the two libraries
under it, which is why it exists.
The last writer does not take the pot. They take the slot, which is what they
were bidding for. The pot goes to everyone else who wrote, pro rata to what
each paid. Winner-takes-all makes the second-to-last writer the mark, everybody
can see that, and so nobody joins.
Half of every payment is credited to the writer it displaces, immediately and
whatever happens afterwards. A zero-sum game minus gas gives a second player no
reason to exist; being overwritten early has to be survivable.
The clock terminates. Each write pushes the deadline a fifth of the way
toward a hard end fixed when the slot opened, and never past it, and never to
less than 300 blocks from now. The last stretch stays long enough for a
transaction to land in, so the slot is won by paying rather than by being
unreachable.
Nothing is ever pushed. Dividends and settlement shares are credited to a
ledger and swept by Withdraw. A realm that pays inside a loop over its roster
is a gas bomb, and the more successful the game gets the more certain it is to
brick.
Three things worth knowing before writing:
The message is capped at 280 bytes and priced per byte, because the message
is the storage cost and an uncapped one is an unbounded write at a constant
price.
Price is a query, and an underpaying write is refused, not refunded. Read
it in the same breath as you send.
Settle is permissionless. Leaving settlement to an interested party is
how a pot stays unclaimed.
The stored message is caller-typed text rendered as markdown on a page that
cannot be edited afterwards, so it goes through ui.Inline before it is shown
and a newline is refused at write time.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
Dependency graph:
🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.
Overview
Package lastwords is one string, on chain, forever.
There is a single message slot. Pay more than the last writer paid and the slot is yours; when the clock finally runs out, whatever is in it stays there permanently. The artefact at the end is the point, so the game is a bidding war over an epitaph rather than over a prize.
The three rules exist because the obvious version of this game does not end and does not pay:
THE CLOCK TERMINATES. Every write pushes the deadline a fifth of the way to a hard end fixed when the slot opened, and no write can push it past that. A constant extension per action has no end: the pot grows faster than the price of the next write, so there is always a rational next write and the game runs until everyone is bored or broke.
THE LAST WRITER DOES NOT TAKE THE POT. They take the slot, which is what they were bidding for. The pot is split among everyone ELSE who wrote, pro rata to what they paid. Winner-takes-all makes the second-to-last writer the mark and everyone knows it, which is why nobody joins.
HALF OF EVERY PAYMENT GOES TO THE WRITER BEING DISPLACED, immediately. A zero-sum game minus gas has no reason for a second player. Overwriting somebody pays them, so being overwritten early is not a loss.
Nothing is ever pushed: payouts are credited to an internal ledger and swept by Withdraw. A realm that pays inside a loop over its roster is a gas bomb that eventually bricks, and the bigger the game gets the more certain that is.
The pieces are libraries, and the realm is the wiring: p/moul/x/games/clock(/p/moul/x/games/clock/v0) owns the deadline and its guards, p/moul/x/games/prorata(/p/moul/x/games/prorata/v0) owns the split, and p/moul/x/daily/pullpayment(/p/moul/x/daily/pullpayment/v0) owns the credit ledger.
1const( 2// Window is how long the slot stays open with nobody writing. 3Window=int64(2000) 4// Floor is the minimum a write leaves on the clock, so the last stretch 5// stays long enough for a transaction to land in. Without it a decaying 6// extension shrinks to nothing and whoever holds at that moment wins by 7// being unreachable rather than by paying. 8Floor=int64(300) 9// Life is the hard end. No write can push the deadline past start+Life.10Life=int64(100000)11// BumpPct is how much of the gap to the hard end a write buys.12BumpPct=int64(20)13// DividendPct is the share of a payment credited to the writer being14// displaced; the rest goes to the pot.15DividendPct=int64(50)1617// MinWrite is the base price of the slot, in ugnot.18MinWrite=int64(1000000)19// PerByte is charged on top, in ugnot per byte of the message. The chain's20// own storage deposit is 100 ugnot per byte, so this is that plus a premium21// for occupying the only slot there is.22PerByte=int64(1000)23// MaxLen caps the message in bytes, because the message IS the storage24// cost and an uncapped one is an unbounded write priced at a constant.25MaxLen=28026)
Price returns what msg costs to write right now, in ugnot: a base, plus a charge per byte, and always at least one ugnot more than the current holder paid. Query it before sending; an underpaying write is refused, not refunded.
Render shows the slot, the clock and the money. The message is caller-typed text, so it goes through ui.Inline: a stored string that renders as markdown is a phishing link on a page nobody can edit, and this realm's whole product is a string somebody else wrote.
Settle closes the game once the clock has run out, splitting the pot among every writer EXCEPT the one holding the slot, pro rata to what each paid.
Anyone may call it: leaving settlement to an interested party is how a pot stays unclaimed. It moves no coins, it credits the ledger; Withdraw is what pays. If the holder is the only writer there is nobody else to split with, so the pot returns to them.
Withdraw pays the caller everything credited to them and returns the amount. Dividends are claimable while the game runs; settlement shares only after Settle.
Write puts msg in the slot and makes the caller its holder. The caller must send at least Price(msg) ugnot with the transaction.
Half of the payment is credited to the writer being displaced and the rest joins the pot. The deadline is pushed a fifth of the way toward the hard end, never past it, and never to less than Floor from now.
Only a direct user transaction may write, so the payment envelope cannot be spoofed by an ephemeral MsgRun realm.