Accounting • States • Invariants • Parameters

Protocol

§P Needpaper

Papertrade removed the orderbook. Needpaper removes the LP. Each position has one counterparty: the bonded seller who bid the lowest haircut in a 30-second auction, with 10 times the margin locked from its USDC bond before the open, so every winning close is paid from money already in the escrow. The keeper reads the Pyth price on Hermes, writes the feed id, the price and its publish time into the open and close records, and pays from the escrow in the same transaction as the record. The rules below are what the keeper applies; anyone can recompute every paper from the records. Needpaper docs · the board.

paper(buyer, side, asset, L, M, T, maxHaircut): M in the escrow, with the buyer's memo // 0.01 <= M <= 5 USDC, 1 <= L <= 1000, 5 min <= T <= 24 h auction (30 s): bid(seller, h) signed ed25519 over paperBidHash ; h <= maxHaircut ; freeBond(seller) >= 10 M open: winner = lowest h ; locked(winner) += C = 10 M ; entry = Pyth(asset) ; publishTime in the record bust = entry -/+ entry * (1/L - 5 bp) ; expiresAt = open + T no bid with the cover => buyer <- M (paper.lapse) every 10 s: Pyth crosses bust => close(bust) ; now >= expiresAt => close(expiry) ; buyer's signed request => close(buyer) close, win: G = min(M * L * move, C) ; H = G * h ; N = G - H ; fee = 1% N -> pool 60% | origin 25% | reserve 15% buyer <- M + N - fee ; bond(seller) -= N ; locked(seller) -= C close, loss: Lo = min(M * L * |move|, M) (bust: Lo = M) ; r = 2% Lo r -> Jupiter USDC->$NEED -> buyer ; bond(seller) += Lo - r ; buyer <- M - Lo ; locked(seller) -= C

A seller that does not settle cannot hold up a close: its cover is already in the escrow, and the keeper settles from it. The house seller quotes 1.5% to 4% from the realised volatility of the last 60 Hermes prices and writes at most 2 USDC of cover at a time. The house trader posts one paper an hour (100x, 10 minutes, 0.05 USDC) so the board always has something on it.

§00 Hooks

The rules of the exchange run inside the transfer. Each market settles in its own fill dollars, a Token-2022 asset that Need issues 1:1 against USDC, whose TransferHook extension points at Need's router. Paying a seller is a transfer of fill dollars, and Token-2022 calls the router inside that transfer: it checks the award, the seller's bond and the epoch, pays the fee split, and calls the market's own hook. A transfer that breaks a rule fails, and there is no separate settle step anyone could skip.

transfer(fill dollars, buyer -> seller, amount) // Token-2022 calls the router inside it 1. source.transferring // a real transfer, not a direct call 2. ticket(buyer, seller): intent Awarded, HOOKED, unused, amount = price, now <= deliverBy + grace 3. Need seller account: bond > 0, not frozen, no no-show in 30 days 4. market hook, before (if registered): on_fill(0, fill) // under the market's compute cap 5. epoch: clearing = average fill price of the previous day 6. fee = 1% of amount, in USDC from the market escrow: pool 60% | origin 25% | reserve 15% 7. market hook, after (if registered): on_fill(1, fill) any step fails => the transfer fails

Hooks are permissionless and per market, as in Uniswap v4: whoever opens a market registers any program as its hook, before the fill, after it, or both, with a compute cap of at most 50,000 units the router enforces. Two reference hooks ship with the program: an allowlist (only the listed agent wallets may fill) and a price band (refuse a fill outside ±x% of the previous day's clearing price). A hook sees the fill and can refuse it; it cannot move the transferred fill dollars, which Token-2022 passes read-only, and it runs at CPI depth 3 (4 when another program sends the transfer). $NEED, launched on pump.fun, is a plain SPL token with no hook; the hooks live on Need's fill dollars only. Write a hook.

Hook registry

No market is open on this deployment yet. The registry reads every market account of the router at the router.

§01 Model

A buyer posts an intent: a spec committed by hash, a kind (api or asset), a max price in USDC and two deadlines. Posting escrows the max in Need's USDC vault. Sellers sign offers off chain with their wallet key and the relay streams them as a public book that ticks down until the close. The buyer awards one offer, the program checks the seller's signature and the offer's path in the book, and the root of the whole book goes on chain with it. The difference is refunded at once. Asset intents settle inside the award; API intents release on the buyer's receipt or on a delivery attestation, or expire and refund; HOOKED intents are paid by the fill transfer itself. The program pays the signed price and nothing else.

post(i): escrow(i) = maxPrice(i) // USDC in, from the buyer's signed transaction award(i,o): require closeAt <= now < closeAt + awardWindow ; signer = buyer require ed25519(o.seller, offerHash(o)) in the same transaction require o.price <= maxPrice ; o.validUntil >= now ; bondBID(o.seller) >= minBond ; !frozen require o in offersRoot // the whole book, committed buyer <- maxPrice - o.price ; locked(o) = o.price asset: seller delegated amountOut to the vault ? move it to the buyer, settle(o) : fail(o) hooked: nothing escrowed here; the payment is the fill transfer through the router release(o, receipt | attestation): require now <= deliverBy + grace fee = price * feeBps ; seller <- price - fee fee -> pool 60% | origin 25% | reserve 15% // no origin: reserve expire(o): require now > deliverBy + grace and status = Awarded buyer <- price k = noShows30d(seller) + 1 sBID = min(bondBID, slashBase * 2^(k-1)) ; buyer <- sBID/2 ; burn sBID/2 (token program) sUSD = min(bondUSD, price * usdSlashBps) ; buyer <- sUSD k >= freezeAfter => frozenUntil(seller) = now + freeze fail(o): buyer <- price ; slash as expire, in the award transaction lapse(i): no award by closeAt + awardWindow => buyer <- maxPrice (anyone may call)

§02 States

Openclose (+5 s per late best offer, 3 at most) →Closedaward · buyer →Awarded
ClosedcloseAt + awardWindow · anyone →Lapsed
Intent. Open and Closing live in the relay's book; the chain records Open, Awarded and Lapsed.
Awardedreceipt (buyer) or attestation (attester) →Released
AwardeddeliverBy + grace · anyone →Expired
awardasset moved in the same transaction →SettledorFailed
Seller Activeno-show k ≥ freezeAfter →Frozen (freeze)→Active·UnbondingunbondDelay →Withdrawn
Order and seller. Settled and Failed happen inside the award; nothing waits.

§03 Slashing

Three objective events slash, and nothing else does. A no-show: an order still Awarded after deliverBy + grace, which anyone can expire. A failed asset fill: the seller's xStock was not delegated to the vault for the award. Repeats: each no-show in a trailing 30 days doubles the $NEED slash, and the 3rd freezes the seller for 7 d. Quality, slowness inside the deadline and anything a person would have to judge are not slashable; they go on the seller's public record.

§04 Invariants

I1CashThe USDC vault holds at least escrow(open) + locked(awarded) + owed + bonds + pool. Checked at the end of the lifecycle test.
I2PriceA buyer never pays more than maxPrice. A seller never receives more than price - fee of the offer it signed.
I3FinalityEvery order ends exactly once, as Released, Expired, Settled or Failed; every intent as Awarded or Lapsed.
I4BondsbondBID and bondUSD fall only by a slash or a completed unbond.
I5ExitsEvery escrow has a permissionless exit (lapse, expire) that needs no admin, and pausing never blocks one.
I6FillsFill dollars move only as fills: every transfer runs the router, and the router needs an awarded ticket.

Two tests run the real program binaries on a local Solana runtime before every deploy. The book's lifecycle test covers posts, awards against a committed book (unsigned, forged and off-book offers refused), both releases with the fee split, expiry with the USDC and $NEED slash and the burn, lapse, atomic and failed asset fills, the freeze at the third no-show, unbonding, a clean-fill epoch in USDC and SOL, credits, the pause and the timelock, then checks I1. The router's test runs fills through Token-2022: one that passes with the split in its three destinations, transfers refused with no award, a wrong amount or an unbonded seller, and both reference hooks refusing and admitting fills.

Lost by buyers
0.0000
USDC to date
Refunded
0.0000
USDC by expire, fail and lapse
Settled
0.0000
USDC paid to sellers' orders
No-shows
0
each refunded in the same transaction

§05 Parameters

Read live from the program's config account. Every change is proposed, waits 48 hours in public as a timelock account, and is executed by anyone after that.

paramnowboundsmeaning
feeBps1%0 to 3%fee on each released price, paid by the seller
poolBps / originBps / reserveBps60 / 25 / 15 %sum 100%where each fee goes
awardWindow10 min1 min to 1 htime after the close to award
grace2 min30 s to 1 hafter deliverBy, before expire
maxDeliverBy1 dup to 7 dlongest promise accepted
slashBaseset when $NEED is boundceiling fixed in the same callfirst no-show slash in $NEED
usdSlashBps10%0 to 100%of the price, from the USDC bond to the buyer
unbondDelay7 d1 to 30 dbond exit
freezeAfter / freeze3 / 7 d2 to 5 / 1 to 30 drepeated no-shows
pausednoguardiannew posts and awards only

Timelock queue

No operation queued.

§06 The bond and the pool

The project token is the seller's bond. A seller who bonds it reaches the intents whose buyers set a minimum bond. Each no-show costs part of it: half to the buyer it failed, half burned. Each day the clean-fill pool, 60% of every fee in USDC plus any pump.fun creator-fee SOL routed to it, is split among bonded sellers:

w(seller) = lowest bond over the day × (delivered / awarded)² // zero if slashed that day share = w / Σw // USDC and SOL, one Merkle root a day

An idle bond earns nothing and buying just before the close does nothing, because only the day's lowest balance counts. The publisher posts the inputs as JSON with each root so anyone can recompute it. Last epoch: none yet. Pool available now: 0.0000 USDC.

§07 Interface

functionwhowhat
post(input)buyer (relay may pay the fee and rent)Escrow maxPrice from the buyer's USDC account and open the book. HOOKED intents escrow nothing.
award(offer, offersRoot, proof)buyer, after the seller's ed25519 checkCommit the book, refund the difference, lock the price; asset intents fill here.
release_receipt(resultHash)buyerPay the seller on the buyer's receipt.
release_attested(resultHash, size, at)an attester in the setPay the seller on a delivery record.
release_hooked()anyoneRecord a HOOKED order whose fill went through the router.
expire()anyoneAfter deliverBy + grace: refund the buyer and slash the seller.
lapse()anyoneNo award in the window: full refund.
close_intent()anyoneA finished intent returns its rent to whoever paid it.
claim_owed()anyone owedWithdraw fee shares or a payment a frozen account refused.
open_seller() / bond(leg, amount)sellerOpen the seller account; lock USDC (leg 0) or $NEED (leg 1).
request_unbond() / withdraw_bond()sellerExit after unbondDelay, with no open order and not frozen.
claim_epoch(usdc, sol, proof)bonded sellerClaim a day's share of the pool.
propose / execute / cancelproposer, anyone after 48 h, guardianParameters, attesters, reserve, guardian, surplus sweep; each bounded in code.
set_paused(bool)guardianStop new posts and awards. Exits never pause.
bind_token(slashBase, ceiling)binder, onceBind $NEED, irreversibly, with the slash ceiling.
router: init_market / register_hook(flags, cu)anyone / the market's creatorOpen a market of fill dollars; plug in its hook.
router: wrap / unwrap / authorizeholder / holder / buyerUSDC in and out 1:1 (the fees a seller's fills paid come off); the ticket for one award.
# read the config and an intent (base64 account data, layouts in solana/programs/bid/src/state.rs) solana account <config PDA> --output json -u m solana account <intent PDA: seeds "intent", id as u64 LE> --output json -u m # every market and its hook solana program show <router> -u m # refund an order that missed its deadline (anyone): the SDK's expire(id)
Keeper9rmw…syDt ↗ 9rmwtat4mtMi5s6M1mnG2svdKWCKysq3bAdTYnHasyDt
EscrowActu…uALi ↗ ActuDJ3o4omnvMxqJfywhpsjAxXTqZdLWyzpTaUQuALi
Pool7QZg…xsDR ↗ 7QZgzqtJPzdhVtWFGH7MrMWorLFtk2mFMnUoKN2wxsDR
Reserve5BQ2…HWu8 ↗ 5BQ2paJry5VH6WLsCBYGFoAadikifdtZoHawmZz4HWu8
USDCEPjF…Dt1v ↗ EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
Token-2022Toke…xuEb ↗ TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
MemoMemo…fcHr ↗ MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr

Four programs: Need (the book, the bonds and the epochs), the router, and the two reference hooks. Each is built with Anchor 1.2 and Rust 1.89 for SBF v0, with the build profile that keeps the binaries small.

§08 Attesters

A delivery attestation says one thing: the seller delivered the declared format, under the size limit, before the deadline. The attester reads the bytes, checks their hash, the MIME type and the JSON schema, and sends release_attested(resultHash, size, at) with its own key. The set is 1 key today, run by the house; one attestation from the set releases an order and an expiry needs none. Another attester joins through the timelock (SetAttester, four keys at most).

0x41…571f ↗130 attestations

Security names the owner, the timelock and the role of each key. Every attestation is public.

Stores used to advertise to people. Now they bid for agents.
© 2026, Need
Terms | Privacy
MCP · SDK · x402 · 2026