On this page
1. System shape
Four services, one source of truth. The ledger is authoritative for ownership and cash; the warehouse system is authoritative for physical location. Nothing else is allowed to have an opinion about either.
┌─────────────┐ orders, swaps ┌──────────────────┐
│ clients │ ─────────────────► │ gateway │ auth, idempotency,
│ web / API │ ◄───────────────── │ │ rate limits
└─────────────┘ book, fills └────────┬─────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌──────────────┐
│ matching │ │ ledger │ │ custody │
│ engine │─►│ double entry │◄─│ WMS bridge │
│ price/time │ │ hash chained │ │ bins, picks │
└───────────────┘ └────────┬────────┘ └──────────────┘
│ outbox
▼
┌─────────────────┐
│ webhooks, tape, │
│ market data │
└─────────────────┘
authoritative for ownership + cash ......... ledger
authoritative for physical location ........ WMS
authoritative for nothing ................... everything else
Fills and swaps are written to the ledger inside the same transaction that mutates the order book, and downstream consumers — the tape, webhooks, market data — read from an outbox table written in that same transaction. There is no path where a webhook fires for a trade that did not commit, because the webhook row and the journal row land together or not at all.
2. The ledger
Ownership is not a column on an item. It is a balance derived from a journal. That sounds like extra work until the first time someone asks you to prove who owned a $400,000 card on a particular Tuesday.
Every entry is a set of lines. The rules are the ordinary ones:
- Debit increases the balance the account holder controls; credit decreases it.
- Each instrument balances independently. Asset units net to zero, dollars net to zero, within the same entry.
- Assets are counted in units, cash in integer cents. No floating point touches a balance, ever.
- Entries are append-only. A correction is a new reversing entry, never an edit.
holder:<user_id> // what a user owns, in ASSET:<item_id> units
cash:<user_id> // a user's USD balance
revenue:fees // maker, taker, swap, storage, ship-out
equity:opening // account funding
external:owner_possession // where an item goes when custody ends
That last namespace is the one people forget. When a card is delivered to your door it leaves custody but must not leave the ledger, or the asset simply evaporates and nothing balances. Instead the holder account is credited and an external possession account is debited — the item's history stays complete, and the books still close.
type : trade.fill
memo : Fill · Charizard — PSA 10 @ $429,450.00
account instrument debit credit
────────────────────────────────────────────────────────────────────
holder:usr_4f2a17 ASSET:itm_c8a1f9 1 unit
holder:usr_9b71c4 ASSET:itm_c8a1f9 1 unit
cash:usr_9b71c4 USD $429,450.00
cash:usr_4f2a17 USD $429,450.00
revenue:fees USD $25,767.00
cash:usr_4f2a17 USD $17,178.00 // taker 4%
cash:usr_9b71c4 USD $8,589.00 // maker 2%
────────────────────────────────────────────────────────────────────
ASSET:itm_c8a1f9 → 0 USD → 0 ✓ balanced
3. Hash-chained audit trail
Balancing proves the entry is internally coherent. It does not prove nobody went back and changed it. For that, each entry commits to the one before it.
hash[n] = SHA-256( hash[n-1] ‖ canonical_json(entry[n]) )
canonical_json: keys sorted recursively, so two structurally
equal entries always produce the same digest
Change one cent in entry 40 and its digest changes; entry 41 committed to the old digest, so it no longer verifies, and neither does anything after it. Rewriting history means rewriting every subsequent entry — which is detectable by anyone holding an older head.
Two chains run in parallel: the journal for value movement, and the event stream for custody and operations — dock scans, imaging with its measurements, cert lookups, bin assignments, picks, environmental excursions. The terminal's Ledger tab has a Verify button that walks both chains and recomputes every digest in the browser.
What the chain proves, and what it does not. It proves a record has not been altered since it was written. It says nothing about whether the record was true when written. A flawless chain of custody rooted in a fraudulent deposit is a flawless chain of custody rooted in a fraudulent deposit — under UCC §2-403 a transferee gets the title the transferor had power to transfer, and a thief has none to give. The strength of a custodial system is set at intake, not at settlement. The full analysis.
Why not just a database audit table. An audit table is written by the same system that could be compromised, and a sufficiently motivated writer can rewrite it consistently. A hash chain does not stop tampering either — but it makes tampering detectable without trusting the operator, which is a meaningfully different property for a custodian to be able to offer.
4. The matching engine
Continuous limit order book, price–time priority, one book per SKU. The incoming order is the taker; resting orders are makers and set the execution price.
while (taker.unfilled > 0):
candidates = resting orders where
sku == taker.sku
side != taker.side
user != taker.user // no self-crossing
price crosses taker's limit
if candidates is empty: break
sort by (best price, then oldest timestamp)
maker = candidates[0]
qty = min(taker.unfilled, maker.unfilled)
price = maker.price // resting order sets the price
for each unit in qty:
if taker is an unreserved market buy
and available_cash < price + taker_fee:
break // stop cleanly; keep the fills already made
settle(buy, sell, price)
Settlement picks a specific physical item from the ask's reserved set, moves title, moves cash, charges both fee sides, writes one balanced journal entry, updates the tape, and emits a custody event recording that no physical movement occurred. Then the invariant check runs.
Reservations
A resting bid reserves cash up front, computed at the limit price including the worst-case taker fee. If it later fills at a better price, the difference is released on the spot.
obligation = (price × qty) + round(price × qty × taker_bps / 10000)
// Per-unit release on a fill is computed from the limit price, so integer
// rounding can leave a few cents behind on the final unit. A fully filled
// order therefore releases any remainder explicitly — otherwise the
// reservation total drifts away from the book, and the invariant check
// will (correctly) start rejecting unrelated transactions.
5. Invariants and transactions
Every command is a transaction: snapshot, apply, assert, commit — or roll back completely. The assertions are cheap and they run every single time, because an invariant you only check nightly is a bug you discover in the morning.
| Invariant | What it catches |
|---|---|
| No negative cash | Fee arithmetic that overdraws an account |
| Reserved ≤ balance | Double-committing the same funds to two orders |
| Reserved cash == sum of open obligations | Reservation drift — the single most likely source of phantom buying power |
| Every open ask references an item its seller still holds | An item sold or withdrawn out from under a resting order |
| Every ask's item is reserved to that exact order | The same slab committed to two sell orders |
| Every journal entry balances per instrument | Any value-movement bug, generically |
Failure is byte-identical rollback. Try to buy a $320,000 Black Lotus with $25,000 of buying power in the terminal. You get a typed error, and the entire state — orders, reservations, journal, custody — is exactly what it was before you clicked. There is no partial application to clean up later.
6. Data model
CREATE TABLE printing (
id text PRIMARY KEY,
game text NOT NULL, -- pokemon | yugioh | mtg
set_code text NOT NULL,
number text NOT NULL, -- '4/102', 'LOB-001', '0348/0500'
language text NOT NULL,
finish text NOT NULL, -- holofoil | textured-foil | non-foil …
edition text NOT NULL, -- 1st Edition | Shadowless | Unlimited
rarity text NOT NULL,
year int,
artist text,
UNIQUE (game, set_code, number, language, finish, edition)
);
CREATE TABLE sku (
id text PRIMARY KEY, -- printing|grader|grade
printing_id text REFERENCES printing,
grader text NOT NULL, -- PSA | BGS | CGC | SGC | TAG | RAW
grade numeric(3,1),
sub_centering numeric(3,1), -- BGS/CGC/TAG only
sub_corners numeric(3,1),
sub_edges numeric(3,1),
sub_surface numeric(3,1),
pop_at_grade int,
pop_above int,
pop_synced_at timestamptz,
UNIQUE (printing_id, grader, grade)
);
CREATE TABLE item (
id text PRIMARY KEY,
sku_id text REFERENCES sku,
owner_id text REFERENCES account, -- the only field a trade changes
cert_number text,
lpn text NOT NULL UNIQUE, -- permanent, survives ownership changes
facility text NOT NULL, -- WEST-01 | MID-01
bin text NOT NULL, -- 'D14-R3-S07'
custody text NOT NULL, -- state machine, see how-it-works
reserved_by text, -- order | swap | withdrawal holding this item
acquired_cents bigint,
measurements jsonb -- weight, thickness, centering, UV, gloss
);
CREATE TABLE journal_entry (
seq bigserial PRIMARY KEY,
id text NOT NULL UNIQUE,
ts timestamptz NOT NULL,
type text NOT NULL,
memo text,
meta jsonb,
prev_hash char(64) NOT NULL,
hash char(64) NOT NULL
);
CREATE TABLE journal_line (
entry_seq bigint REFERENCES journal_entry,
account text NOT NULL,
instrument text NOT NULL, -- 'USD' | 'ASSET:<item_id>'
debit bigint, -- cents, or units for assets
credit bigint,
CHECK ((debit IS NULL) != (credit IS NULL))
);
Two details worth calling out. The UNIQUE on the printing tuple is what stops a
Shadowless Charizard and an Unlimited one from collapsing into the same row. And
reserved_by on the item is what makes "committed" a single, checkable fact rather
than something reconstructed by scanning three other tables.
7. Marks and price discovery
Portfolios have to be marked to something, and the honest answer is often "we don't know." Our order of preference:
- Last traded price on this SKU. An actual print between two consenting parties on this exact instrument.
- Mid of the current book, when the spread is tight enough to be informative.
- Reference price, seeded from observed comparable sales, when the book is one-sided or empty.
What we deliberately do not do is interpolate across graders or across grades. A PSA 9 print does not tell you what the PSA 10 is worth beyond a very loose bound, and a BGS 9.5 print tells you about the BGS 9.5 book. Thin markets are thin; smoothing the number does not make the liquidity appear, it just hides the fact that it is missing.
Population is a supply signal, not a price. We surface pop pressure — the share of the graded pool above your grade — next to the mark rather than folding it into the mark. A model that silently bakes population into price is making a judgement the user cannot see or disagree with. See pop pressure.
8. REST API
Everything the dashboard does, the API does. Bearer tokens, JSON in and out, integer cents
throughout. Mutations take an Idempotency-Key header and replay the original
response for 24 hours — a retried order is never a duplicate order.
Instruments
{
"id": "pkm-base1-004-1ed|PSA|10",
"printing": {
"game": "pokemon", "set_code": "BS", "number": "4/102",
"name": "Charizard", "language": "EN",
"finish": "holofoil", "edition": "1st Edition",
"rarity": { "native": "Rare Holo", "tier": 4 },
"year": 1999, "artist": "Mitsuhiro Arita"
},
"grade": { "grader": "PSA", "value": 10, "label": "Gem Mint" },
"population": { "at_grade": 121, "above": 0, "pressure": 0.0,
"synced_at": "2026-08-02T06:00:00Z" },
"quote": { "bid": 41055000, "ask": 42945000,
"last": 42000000, "spread_bps": 450 }
}
{
"bids": [ { "price": 41055000, "qty": 2, "orders": 2 },
{ "price": 40020000, "qty": 1, "orders": 1 } ],
"asks": [ { "price": 42945000, "qty": 2, "orders": 1 },
{ "price": 43980000, "qty": 1, "orders": 1 } ]
}
Orders
// a bid — quantity, because any copy at this grade is deliverable
{ "sku": "pkm-base1-004-1ed|PSA|10",
"side": "buy", "type": "limit",
"price_cents": 41500000, "qty": 1, "tif": "GTC" }
// an ask — specific items, because you sell a slab, not an abstraction
{ "sku": "pkm-base1-004-1ed|PSA|10",
"side": "sell", "type": "limit",
"price_cents": 42900000,
"item_ids": ["itm_c8a1f9"] }
// 201 response
{ "id": "ord_00019f", "status": "open", "filled": 0,
"reserved_cents": 43160000, // notional + worst-case taker fee
"fills": [] }
{ "id": "ord_00019f", "status": "cancelled",
"released_cents": 43160000,
"released_items": [] }
Swaps
{ "counterparty": "usr_9b71c4",
"offered": ["itm_c8a1f9"],
"requested": ["itm_2de503"],
"boot_cents": 18000, // positive: you add cash
"expires_in": 86400,
"note": "Straight across plus cash to bridge the gap." }
{ "id": "swp_000021", "status": "settled",
"journal_id": "jnl_0004a2",
"fee_cents": 9300,
"physical_movement": false,
"legs": [
{ "item": "itm_c8a1f9", "to": "usr_9b71c4",
"facility": "WEST-01", "bin": "A35-R3-S05" },
{ "item": "itm_2de503", "to": "usr_4f2a17",
"facility": "MID-01", "bin": "E20-R3-S12" } ] }
Note the two facilities in that response. Those cards are most of a continent apart and the swap still settled in one transaction, because neither of them went anywhere.
Custody
{ "item": "itm_c8a1f9", "lpn": "LPN7761A8BE63",
"events": [
{ "seq": 1104, "kind": "intake.manifested", "actor": "usr_4f2a17" },
{ "seq": 1105, "kind": "custody.received", "actor": "dock:WEST-01" },
{ "seq": 1106, "kind": "custody.imaging", "actor": "imaging:WEST-01",
"data": { "dpi": 1200,
"captures": ["front","back","raking_15deg","uv_365nm"],
"weight_g": 28.27, "thickness_mm": 6.06 } },
{ "seq": 1107, "kind": "custody.authenticating", "actor": "auth:WEST-01",
"data": { "cert_lookup": "52820658", "pop_at_sync": 121,
"holder_integrity": "weld seam nominal, label font match" } },
{ "seq": 1108, "kind": "custody.vaulted", "actor": "vault:WEST-01",
"data": { "bin": "A35-R3-S05" } },
{ "seq": 1147, "kind": "custody.title_transfer", "actor": "exchange",
"data": { "to": "usr_9b71c4", "physical_movement": false } }
] }
{ "item_ids": ["itm_c8a1f9", "itm_71b0de"] }
// 201
{ "id": "wdr_000007", "status": "requested",
"parcels": 2, "facilities": ["WEST-01", "MID-01"],
"quote": { "shipping_cents": 2199, "coverage_cents": 27690,
"declared_cents": 4360000, "total_cents": 29889 },
"packaging": "Slab sleeve → bubble wrap → rigid insert → double-boxed with void fill" }
Errors
Typed, actionable, and never partially applied.
| Code | HTTP | Meaning |
|---|---|---|
INSUFFICIENT_FUNDS | 402 | Buying power does not cover notional plus fee |
NOT_VAULTED | 409 | Item is not in the vaulted state — it is in transit, in QA, or already withdrawn |
ALREADY_RESERVED | 409 | Item is committed to another order, swap or ship-out |
LEG_UNAVAILABLE | 409 | A swap leg moved between offer and acceptance |
SKU_MISMATCH | 422 | An item was offered against the wrong instrument |
UNBALANCED | 500 | Journal entry failed to balance — the transaction was rejected, never written |
9. Webhooks
Signed with HMAC-SHA256 over the raw body, timestamped to prevent replay, retried with exponential backoff for 24 hours. Delivery is at-least-once — handlers must be idempotent on the event id.
| Event | Fires when |
|---|---|
order.filled | Any fill, partial or complete |
order.cancelled | Cancel, IOC expiry, or market remainder expiry |
swap.received | Someone offers you a swap |
swap.settled | A swap commits |
custody.state_changed | Any custody transition on an item you hold |
intake.quarantined | Authentication failed — needs your decision |
withdrawal.updated | Parcel advances a stage; carries tracking once manifested |
signature = HMAC_SHA256(secret, timestamp ‖ "." ‖ raw_body)
// Compare in constant time. Reject if |now − timestamp| > 300s.
// Sign the RAW body — re-serialising JSON before verifying will
// eventually reorder a key and break in production at 3am.
The engine on this page is the engine in the terminal
The matching loop, the reservations, the invariant checks and the SHA-256 chain described
here are implemented in assets/js/engine.js and run in your browser. Open the
console and poke at it.