SLAB is a fully on-chain central limit order book for tokenized graded collectibles, built on Monad. Not one book — hundreds of thousands, executing concurrently, because that is what the asset class actually requires.
Bids are placed against a grade, not a token. Asks are placed against a specific card. Orders rest, cross, and cancel entirely on-chain — no sequencer, no off-chain matching engine, no operator who can reorder your fill.
| Contract | Address |
|---|---|
| OrderBook | 0x515f7291d570abF63aB33CF7aB51384b3c155866 |
| SeriesRegistry | 0x8Fde7cA9D0F5e7dA335d8ACC33EE81a013658A8B |
| MockUSDC | 0xf59101519884967EC2F430DA2B1EFb93d4aA3E49 |
| MockCard | 0x95EcE2347BC771378EB38aa5d43819CBDaaD9AB8 |
All verified on MonadVision and Monadscan. Nothing here is real money.
- Node 18+
- Foundry
- MON for gas from the Monad faucet
git clone https://github.com/CodeBlocker52/slab.git
cd slab/web
npm install
cp .env.example .env.local # fill in the values below
npm run devweb/.env.example
NEXT_PUBLIC_PRIVY_APP_ID = YOUR_PRIVY_APP_ID
NEXT_PUBLIC_INDEXER_URL = https://slab-indexer.onrender.com
NEXT_PUBLIC_MONAD_RPC = https://testnet-rpc.monad.xyzThe hosted indexer is public, so the app runs against live testnet state without running one yourself. Point NEXT_PUBLIC_INDEXER_URL at http://localhost:PORT if you'd rather run indexer/ locally.
cd slab/contracts
forge build
forge test163 tests, 20 fuzz properties at 5,000 runs each. Everything should pass against a clean checkout with no network access.
contracts/.env.example — only needed to deploy or verify, not to build or test:
# Deployer key. Fund at https://faucet.monad.xyz
PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
MONAD_TESTNET_RPC=https://testnet-rpc.monad.xyz
# Optional, only for `forge verify-contract`
MONADVISION_API_KEY=contracts/ Solidity — OrderBook, SeriesRegistry, mocks, tests, deploy scripts
web/ Next.js app — market grid, depth ladder, terminal
indexer/ Rebuilds the ladder from three events; reconciles against chain state
maker/ Reference market maker — quotes, reprices, cancels across every book
Every order-book venue that exists runs a small number of markets. dYdX and Hyperliquid list a couple hundred perpetuals. Serum had a few hundred pairs. The NYSE and NASDAQ together list under six thousand securities.
Graded cards do not work like that.
Pokémon alone ~30,000 distinct printed cards
× graders PSA · BGS · CGC · SGC
× liquid grades 8 · 9 · 10
─────────────────────────────────────────────
~270,000 tradable instruments
Add Magic, Yu-Gi-Oh, and sports and the number keeps going. That is roughly fifty times the combined listings of the NYSE and NASDAQ, and every one of them is a genuine instrument with its own supply, its own price, and its own book.
No venue on earth runs this. Not because order books are hard — they were solved decades ago — but because a hundred thousand simultaneous books on a serial execution environment all contend on the same state, and the entire exchange runs at the speed of its busiest market.
That is the problem SLAB exists to solve, and it is a Monad problem.
The scale argument only holds if these are genuinely separate instruments rather than a hundred thousand unique items. They are, and this is the design insight the whole project rests on.
Graded cards are fungible within their grade. Grading is standardisation — the same function assaying performs for gold or grading performs for wheat. A PSA 10 Base Set Charizard is interchangeable with any other PSA 10 Base Set Charizard: same set, same card number, same grader, same numeric grade, thousands of units in circulation.
seriesId = keccak256(set, cardNumber, grader, grade)
Base Set · #4 · PSA · 10
Base Set · #4 · PSA · 9
Evolving Skies · #215 · CGC · 10
A series is an instrument. An instrument gets a book. There are a hundred thousand of them.
Every existing platform treats these as unique NFTs, because that is what the token standard suggests. It is the wrong abstraction, and it is why a market doing hundreds of millions a month tolerates a spread nobody competes on.
Tokenized collectibles moved roughly $290M in July 2026, up from $124.5M across the four main marketplaces in August 2025. Courtyard vaults graded cards with Brink's and mints on Polygon; Collector Crypt and Phygitals run comparable businesses on Solana; RIP.FUN is on Base.
Demand is proven. Market structure is not.
Every one of these is a listing board, and every one of them prices the same thing: immediacy.
| Fee | |
|---|---|
| RIP.FUN marketplace | 2.5% |
| RIP.FUN instant buyback | 85% of FMV, 48h |
| Courtyard buyback | ~90% of FMV |
| eBay final value fee | ~13% |
Note the shape of it. A low fee if you will wait indefinitely for a listing to sell, a much worse one if you want out now. That gap is a bid-ask spread — set unilaterally by the platform, identical whether the asset has four thousand siblings or is a genuine one-of-one.
You do not fix that by charging less. You fix it by giving the instrument a book.
Bids and asks are not symmetric here, and they should not be.
Bids are generic. A buyer wants a PSA 10 and does not care which, so a bid carries a seriesId and a quantity, and fills against whichever unit is cheapest.
Asks are specific. A seller owns card #8821 and can only sell that one, so an ask carries a tokenId and quantity is always 1.
BIDS (aggregated by tick) ASKS (individual cards)
───────────────────────── ────────────────────────
4,210.00 ×7 ████ 4,255.00 #8821
4,205.50 ×3 ██ 4,260.00 #1174
4,199.00 ×12 ███████ 4,260.00 #9302
4,180.00 ×4 ███ 4,299.00 #4417
▲ spread 45.00 · 1.06%
The bid side collapses into price levels with size; the ask side is a queue of distinct assets. One percent, discovered by competition, against a 10–15% haircut set by a platform. That number is an outcome of the structure, not a fee schedule anyone promises to keep low.
Stated precisely, because the imprecise version does not survive scrutiny.
Two traders working different series touch entirely separate storage. Under Monad's optimistic parallel execution those transactions run concurrently. The venue scales with the number of instruments rather than serialising behind one contract.
The contracts are written for this deliberately:
- Order IDs are per-series counters, never one global sequence — a global counter would serialise the whole exchange behind a single slot
- No protocol-wide volume or fee accumulator anywhere in the matching path
- Bitmaps and price levels are stored per series, so no two books share a tick structure
This is checkable, not asserted: a test records every storage slot written by two different books and asserts zero overlap.
Being straight about this is worth more than overreaching:
- Cheap on-chain cancels are not new. Serum did it on Solana in 2021. Sei, Injective and Hyperliquid are built around it. Cancel economics are necessary for SLAB and they are not a Monad discovery.
- Sub-second blocks are not unique. Solana and Sei are both in the same range.
- Storage-level parallelism is not unique either. Sealevel has done account-level parallelism since Solana launched.
The asset is EVM-native. Vaulted-card NFTs are ERC-721s, and they are permissionless — anyone can build on them — but only from an EVM chain. Bridging to Solana would mean trading a wrapper of a redemption claim, which breaks the one guarantee that gives the asset value: burn the token, receive the physical card.
So SLAB needs EVM equivalence, and parallel execution across a hundred thousand partitions, and sub-cent cancels, and sub-second blocks, simultaneously.
Monad is the only chain at that intersection. That is a narrower claim than "only Monad can do this," and it has the advantage of being true.
graph TB
subgraph off["Off-chain"]
MK["🤖 Makers<br/>quote across many series"]
BY["👤 Buyers"]
SL["👤 Sellers"]
end
subgraph on["Monad — one partition per series"]
SR["SeriesRegistry<br/>tokenId → seriesId · tick size"]
B1["Book · series A"]
B2["Book · series B"]
B3["Book · series C"]
BN["… ~10⁵ more"]
end
VN["Vaulted card NFTs<br/>permissionless ERC-721"]
VN --> SR
SL --> B1
BY --> B1
MK --> B1
MK --> B2
MK --> B3
B1 --> SR
style B1 fill:#836EF9,color:#fff
style B2 fill:#836EF9,color:#fff
style B3 fill:#836EF9,color:#fff
style BN fill:#B4A5FF,color:#000
Books never touch each other. That is the entire point — the diagram is the thesis.
sequenceDiagram
autonumber
participant B as Buyer
participant OB as OrderBook (series N)
participant SR as SeriesRegistry
B->>OB: placeBid(seriesId, tick, qty, maxFills)
OB->>SR: series registered and active?
OB->>OB: escrow the FULL limit up front
loop while askTick <= tick, up to maxFills
OB->>OB: bitmap → lowest ask tick
OB->>OB: step over cancelled orders
OB->>OB: NFT → buyer, USDC → seller
Note over OB: fill at the RESTING ask's price
end
alt quantity remains
OB->>OB: set bit, push to FIFO queue
end
OB->>B: refund what fills and remainder did not need
Escrowing the full limit before matching means the contract never discovers mid-match that it cannot pay, so no fill can partially settle.
Prices quantise to a per-series tick. Occupancy is tracked in a two-level bitmap: 256 words of 256 bits, plus one summary word marking which words are non-empty. Finding the best price is "find the lowest set bit".
The summary word is not an optimisation, it is a requirement:
| Path | Gas |
|---|---|
| Flat scan of 256 words — 256 cold SLOADs | 2,073,600 |
| With the summary word | 2,488 |
That is the cold-start path, paid by the first bid into every series with no asks. At two million gas per empty book, seeding thousands of markets is impossible.
| Operation | Cost | How |
|---|---|---|
| Insert | O(1) | Set bit, push to queue. No hint, no pointers. |
| Cancel | O(1) | One flag, refund, return. |
| Match | Bounded | Bitmap scan, fills up to caller-supplied maxFills. |
cancel sets one flag, refunds, and returns. It does not unlink, compact the queue, or clear the bitmap. Matching steps over dead orders and clears bits lazily as it walks past them, with the head pointer making that cost amortised.
Two reasons. Cancel is the most-executed write in the system, so it must be the cheapest. And Monad charges on gas_limit with no storage-clearing refund, so zeroing state to reclaim gas reclaims nothing and costs an extra slot write.
| Contract | Responsibility |
|---|---|
OrderBook.sol |
Per-series bitmaps, FIFO queues, matching, custody. No shared state between series. |
SeriesRegistry.sol |
tokenId → seriesId, tick size, active flag. Owner-attested. |
One custody contract, so escrow solvency is a single property over a single balance.
Three events. The indexer rebuilds the entire ladder from them and nothing else.
OrderPlaced(seriesId, orderId, maker, tick, qty, isBid, tokenId)
OrderCancelled(seriesId, orderId, maker)
Fill(seriesId, bidId, askId, buyer, seller, tokenId, tick, price, fee)Fill does not say which side was the taker — it does not need to. The contract emits every fill before its own OrderPlaced, so a fill always names one order already seen (the resting one) and one not yet seen (the taker). npm run reconcile proves the reconstruction matches on-chain state, and exits non-zero if it ever does not.
Custom errors only, no revert strings. The UI maps every one to human copy from a single shared source.
SeriesNotRegistered SeriesInactive TickOutOfRange
PriceNotOnTick ZeroQuantity ZeroMaxFills
NotOrderMaker OrderAlreadyCancelled OrderFullyFilled
OrderNotFound TokenNotAttested TokenSeriesMismatch
FeeTooHigh TransferFailed SelfTrade*