A curated list with skin in the game. Anyone lists an entry by locking a deposit, anyone
challenges an entry by matching that deposit with a bond, and after a voting window the
loser's money goes to the winner. The deposit does not make an entry good. It makes a bad
one expensive to leave standing, which is the only reason a list anybody can write to is
worth reading at all.
The engine is p/moul/x/social/curated/v0, a
pure package that returns errors and moves no coins. This realm is the chain wiring: the
envelope, the banker, the events and Render.
The API
call
pays
does
Apply(key, url, description)
exactly 1 GNOT
lists the entry immediately
Challenge(key)
exactly that entry's deposit
opens a vote for 1000 blocks
Vote(key, keep)
nothing
one address, one vote, while the window is open
Resolve(key)
nothing
anyone, after the window: pays the winner
Unlist(key)
nothing
the owner takes their own entry down, if unchallenged
A key is a slug: lowercase ASCII letters, digits, -, _ and ., starting alphanumeric,
at most 64 bytes. A key whose entry was removed is free to apply for again: a challenge
that wins removes an entry, it does not burn the name.
A majority of keep votes keeps the entry and credits its owner the challenger's bond.
Otherwise the entry is removed and the challenger is credited the bond plus the deposit.
A tie keeps the entry, including the tie of nobody voting at all: the incumbent paid
first and is already at risk, so the burden is on the challenger to produce a reason. If
ties went the other way, a challenge that convinced nobody would still win and listing
anything would be pointless.
The trap it avoids
Nothing is ever sent to you. Every payout is a credit in an internal ledger, and the
payee calls Withdraw() for their own. A realm that looped over winners and sent to each
one would fail entirely when one of them could not be paid, and would hand a griefer a
denial of service for the price of one entry. Withdraw zeroes the credit before the
coins move, so a reentrant call finds nothing left to take.
The consequence is an invariant worth knowing: every ugnot at this realm's address is
either backing a live entry or an open challenge, or already assigned to somebody. The
tests assert exactly that, against the chain balance rather than against the realm's own
books.
Voting is sybil-prone, deliberately and visibly
Vote is one address, one vote, unweighted, and an address is free. A resolution says
"nobody with a stake objected enough", never "this is true". Deciding who counts as a
person is a different problem with its own realm behind it, r/moul/x/social/vouch, a
sibling in this family; until a vote is gated on a vouched identity, the two addresses
with money on the outcome are the only ones whose vote means anything.
Which is the second thing: voters are paid nothing in v0. Voting costs gas and returns
nothing, so there is no reason for a disinterested address to show up at all. A share of
the loser's stake for the winning side is the standard answer, and it is the first thing
this realm should grow. After that, an application period, so a bad entry is not visible
before anybody can object to it.
Why the bonds are GNOT and not a token of this list's own
A bond has to be denominated in something the challenger already holds. A list that minted
its own token and demanded it as the bond would be asking a newcomer to acquire a token
whose only use is challenging entries on a list nobody reads yet, which is the
chicken-and-egg problem in its purest form: the token is worth something once the list is
worth gaming, and the list cannot become worth gaming until challenges work.
So v0 bonds are native GNOT, which every account on the chain already has.
The answer becomes yes under one condition: the list is valuable enough that being on it
is contested, and the challenge flow is busy enough that a bond denominated in a list
token would have a real market price. Concretely, a steady stream of challenges from
addresses that are not the two parties, and a reason to hold the token between challenges.
At that point the token earns its own job, and it can pay the voters the paragraph above
says go unpaid, which is the sink a bond alone does not provide.
The slot it would drop into is p/moul/x/social/coin/v0, a GRC20 in this family that
refuses to exist until its mint rule, its sink and its buyer are all declared. This realm
deliberately does not import it: the three answers are not available yet, and a token
issued before they are is a token with no reason to be held.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
On mainnet:
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 curated is a list anyone can get onto by locking a deposit, and anyone can try to get somebody else off by matching that deposit with a bond. The loser of the challenge pays the winner.
It is the registry member of a family of small social apps: a list is the thing every other one of them eventually needs, and a list is only worth reading if being on it cost something.
Example
1Apply(key, url, description) -send 1000000ugnot list it, immediately
2Challenge(key) -send <the deposit> object to it
3Vote(key, keep) while the window is open
4Resolve(key) anyone, after it closes
5Unlist(key) the owner, if unchallenged
6Withdraw() collect what you won
Money in, money out
Coins arrive in the envelope of a call and sit at this realm's address. They never leave by being pushed: a payout is a credit in a ledger and the payee calls Withdraw for their own. A realm that looped over winners and sent to each one would fail entirely when one of them could not be paid, and would hand a griefer a cheap denial of service for the price of one entry.
Every ugnot here is therefore either backing a live entry or an open challenge, or already assigned to somebody. The realm's own tests assert exactly that against the chain balance.
Voting is sybil-prone, and the bonds are GNOT
Vote is one address, one vote, unweighted, and an address is free. Read a resolution as "nobody with a stake objected enough", never as a verdict. Gating a vote on something harder to manufacture is a different problem with its own realm behind it, r/moul/x/social/vouch, a sibling in this family.
Bonds are GNOT and not a token of this list's own: see the README on why that is the only answer available at v0 and what would change it.
What v0 does not do, in the order it should be fixed
Voters are paid nothing. Voting costs gas and returns nothing, so the only two addresses with a reason to vote are the ones with money on the outcome. A share of the loser's stake for the winning side is the standard answer and the first thing to add.
There is no application period: Apply lists immediately.
A challenge cannot be withdrawn and a vote cannot be changed.
1const( 2// Denom is what a deposit and a bond are paid in. Native coins, so the 3// amounts are real to a challenger before this list is worth anything. 4Denom="ugnot" 5 6// Deposit is what listing costs, and therefore also what challenging one 7// of today's entries costs. One GNOT: enough to make a thousand junk 8// entries a real expense, cheap enough that one honest entry is not a 9// decision.10Deposit=int64(1000000)1112// ChallengeBlocks is how long a challenge takes to resolve. Long enough13// for a reader who is not watching the chain to notice and vote, short14// enough that an entry is not held hostage.15ChallengeBlocks=int64(1000)16)
Apply lists an entry under key, in exchange for exactly Deposit ugnot.
The entry is on the list the moment this returns: there is no application period in v0, and the check on a bad entry is that anybody can challenge it. The deposit comes back through Unlist and Withdraw if nobody ever does.
key is a slug: lowercase ASCII letters, digits, '-', '_' and '.', starting alphanumeric. A key whose entry was removed is free to apply for again.
Resolve closes a challenge whose window has passed. Anyone may call it, so neither party can stall the other by sitting still.
A majority of keep votes keeps the entry and credits its owner the bond. Otherwise the entry is removed and the challenger is credited the bond plus the deposit. A tie, including nobody voting at all, keeps the entry: the incumbent is the one already at risk, so a challenge that convinced nobody loses, which is what makes being wrong cost something.
Unlist takes the caller's own entry off the list and credits them the deposit back, which Withdraw then pays out.
It is refused while a challenge is open: an owner who could walk away mid-challenge would be risking nothing, which is the one thing the deposit exists to prevent.