The platform already computes both halves of the arbitrage trade. They live on separate surfaces, and the product does not connect them.
The opportunity half. The PSA arbitrage tab (backed by a stored opportunities table) keys every
opportunity on a tcg_product_id: one row per card, carrying the raw market price, the PSA10 median
sold price, and the stored spread. Critically, that spread is computed from TCGplayer raw prices and
PSA pop/sales data, net of the grading cost (verified: the residual of median minus raw minus spread
is exactly the $20 grading fee on every row). It does not read eBay raw listings at all. The drain
neither creates nor changes the spread. What the tab then shows is the opportunity and the graded
PSA10 comps, the sell side. It does not surface the raw buy listings.
The buy half. Raw card listings surface in a different place: the eBay deals browser (the auction deals view, matched-only filter). That view shows buyable raw cards matched to catalog products. It is where the drain's work lands.
The missing join. Nothing cross-links the two. Standing on a chase card in the arbitrage tab, the
product cannot show you the buyable raw listings for that exact card, and standing in the deals browser
you are not told which of those cards carry a live PSA10 spread. The buy side and the opportunity side
both exist in the data, keyed on the same tcg_product_id, and the platform never joins them on the
read path. That join is the arbitrage read-path the client sketched, and it is unbuilt. This page is
about sizing it, not claiming it.
The whole platform, not just the drain. Across active matched listings (recon22.out B5 and B4),
read 2026-06-09 12:27 UTC:
| The arbitrage surface today | Count |
|---|---|
Distinct matched cards (active, by tcg_product_id) |
13,219 |
| Of those, cards with ANY computed arb opportunity row | 1,377 |
| Of those, with a positive spread | 1,174 |
That is the headline, and it is a coverage headline: only about 10% of matched cards (1,377 of 13,219) have any arbitrage opportunity computed at all (about 9% carry a positive spread). The other ~11,800 matched cards have no arb row, because the PSA pop and sales ingestion that feeds the opportunities table has not reached them. So whatever the read-path is worth, it currently sits on a roughly 10%-built surface.
On that slice, the buyable inventory already exists. Joining active matched raw card listings to the surface-eligible positive-spread opportunities (at least 2 PSA10 sold comps, positive stored spread, published 2023 or later or undated):
| Buyable raw inventory on the arb surface | Count |
|---|---|
| Active matched raw card listings on surface-eligible positive-spread cards | 587 |
| Distinct cards those listings cover | 252 |
So 252 distinct cards right now have both buyable matched raw inventory AND a surface-eligible positive PSA10 spread. The buy side and the opportunity side coexist on these 252 cards; the product just does not connect them on a single screen.
And the buy-raw/sell-graded unit is observable in the data: 1,263 distinct cards carry BOTH an
active matched raw card listing AND an active matched graded PSA listing at the same time
(recon22.out B6). The full trade (acquire raw, grade, list as PSA10) is already represented in the
listing pool. The cross-link would make it navigable.
Across those 252 distinct cards with buyable raw inventory and a live positive spread, the aggregate
addressable spread is $56,598 (one PSA10 copy per card, net of the $20 grading cost;
recon22.out B4b). The distribution is heavily skewed, so the median is the honest "typical" figure:
| Spread across the 252 cards with buyable raw + live spread | USD (net grading) |
|---|---|
| Total (one PSA10 copy each) | $56,598 |
| Median per card | $94.29 |
The top of the surface is real chase-card territory: the single largest per-card spread on the live read exceeded $2,400 on a roughly $400 raw price, and the long tail runs to four-figure ROI percentages on double-digit raw prices. (The per-card table, names and prices, is withheld from this public rendering: it is the client's live product output. The aggregate total and median above are carried unchanged.)
Honesty on what the spread is and is not. It is the gross profit on one copy IF it grades PSA10. The realized expected value weights by the PSA10 hit rate, which the surface exposes where populated. It is not populated for every card, so this page states the per-PSA10 spread and does not multiply through to a single EV number that would imply more certainty than the data carries. And the headline caveat sits above all of it: $56,598 is the spread on the 252 cards inside the roughly 10%-covered slice. It is a floor on a fraction, not the size of the full opportunity. The full opportunity is what the pop/sales ingestion plus the cross-link would light up across the other ~90%.
This section is the M3 validation record, measured 2026-05-30 against the drain's clean-commit cohort, not a live read. The drain set is a closed cohort (the figures below do not move with the eBay feed); the surface it sat on was larger then (383 cards) than the live read in the sections above (252 cards), because the listing pool has aged over since.
The honest, narrower claim. The M3 queue drain populated raw buy-side inventory. Its clean commits put
buyable raw card listings into the matched pool that the eBay deals browser reads from; while those
same listings sat in the near-miss band they were unmatched (null tcg_product_id) and the deals
browser, which filters to matched listings, never showed them. The drain moved them from invisible to
visible on the buy surface. Near-miss listings stay hidden from that browser by design, which is the
correct conservative behavior (a low-confidence guess is exactly the wrong card to surface).
How much of the buy side the drain touched (as of the 2026-05-30 drain measurement). Of the 383 cards
that then had buyable raw inventory and a live spread, 83 carried at least one drain-committed raw
listing (recon20b.out: 232 drain buy-candidates across 83 distinct cards). Six of those 83 were
sole-source: every active raw listing on that card came from this drain, so the drain took them from
zero buyable listings to one or more. The other 77 already had a raw listing; the drain added depth.
What the drain did NOT do. It did not unlock executable arbitrage, because executing the arbitrage requires the cross-link, and the cross-link is not built. The drain made more raw cards visible on the buy surface. It did not connect that surface to the opportunity surface. A user who wants to act on a spread still has to leave the arbitrage tab, go to the deals browser, and search for the card by hand. The drain improved one half of an unbuilt read-path. That is the accurate framing, and it is the one this page leads with.
The gate, encoded verbatim in the platform's strict-matcher header comment (the client's name is replaced): "Wrong matches = wrong arbitrage = [the client] buys the wrong card. Return null rather than guess." The buy-side inventory is only as trustworthy as the matches under it. Two readings, one historical and one live, both point at the same gap.
The M3 validation name-check (2026-05-30). Every one of the drain's 232 surface-eligible raw
buy-candidates was name-checked: does the catalog card the listing was committed to actually match the
listing title (arb_unlock_classify.py, distinctive-token plus foreign-printing heuristics)? Result:
231 of 232 were name-consistent, 1 was a confirmed wrong-card pair (about 0.4%). The bad pair was a
Japanese-print Togekiss listing committed to the catalog card Magikarp 203/193 (Paldea Evolved): the
listing is a Togekiss, the catalog row a Magikarp, a foreign-printing card-number collision where a
Japanese card's printed number lines up with a different English card. (A note on method, because it
cuts both ways: the heuristic first flagged 6, but reading each one, 5 were false positives where the
distinctive-token picker chose "Alternate" from "(Alternate Art)" rather than the character name.
Reporting the raw 6 would have been a fabricated number; the verified count was 1.)
What the live read shows now. That specific Japanese Togekiss listing has since aged out of the
feed, so the phantom spread is gone, and the Magikarp 203/193 card now carries a correctly-matched
English listing (0.98 confidence, print_language=en). The card was never the problem; the one
foreign listing under it was, and it is no longer there. The class behind it is still wide open,
though, and reproducible live: active committed card matches carrying a foreign-language title number
212 on one-piece, 105 on dragon-ball-super, 7 on pokemon, and 0 on the flagship game
(recon22.out A5, read 2026-06-09 12:27 UTC). The seven surviving pokemon rows are all the same
shape: European-language prints whose printed numbers line up with different English cards. The
flagship game returns zero because its foreign-print guard exists; the expansion games have no
equivalent, so a foreign-language print can resolve to the wrong English card on any of them.
This is the same failure class the drain held 141 commits back for (graded-via-fuzzy and Latin-script foreign), and the same class as a French promo-print Pikachu caught in the M3 revert set. It argues for one concrete follow-up: extend the foreign-language hold guard from the dry-run's flag step into the live auto-commit path, so the production sync abstains on a foreign-printing card-number collision instead of committing it. This matters more, not less, the moment the cross-link exists: a wrong match becomes a wrong arbitrage the instant a buy listing is shown next to a spread.
The read-path is the scoped work. Two pieces, both on the database/low-level side:
The cross-link. Join the opportunity surface to the matched raw inventory on the read path, keyed on
the tcg_product_id both already carry, so a chase card in the arbitrage tab shows its buyable raw
listings and the deals browser flags which matched cards carry a live spread. This is the join that
turns two coexisting datasets into one navigable read-path. It is unbuilt today.
The coverage expansion. Extend PSA pop and sales ingestion past the current ~10% of matched cards, so the opportunity surface covers more than 1,377 of 13,219 cards. The $56,598 addressable spread is the floor measured on the built ~10%; the expansion is what scales it.
Built to scale from the start. Because the read-path is a per-card join against a surface that grows with every sync, the materialized-view and hot-cache work from the M1 audit folds in here, not as a separate effort: build the cross-link to scale from the get-go rather than retrofitting it.
The accuracy guard travels with it. The foreign-hold-on-auto-commit fix from the section above is part of the same work-stream, because the cross-link is exactly where a wrong match becomes a wrong arbitrage.
Dollar figures on this page ($56,598 addressable spread, $94.29 median) are card-market domain data, the subject of the analysis. No engagement pricing appears here.
Live figures read 2026-06-09 12:27 UTC (recon22.out / recon22b.out); drain-cohort figures are the
M3 validation record, read 2026-05-30 (recon20.out / recon20b.out).
| Claim | As of | Verified against |
|---|---|---|
| Coverage gap: 13,219 distinct matched cards, 1,377 arb rows, 1,174 positive (~10% covered) | 06-09 live | recon22.out B5 / B5b (live opportunities table + active matched listings) |
| 587 buyable raw on 252 surface-eligible positive-spread cards | 06-09 live | recon22.out B4 |
| Addressable spread $56,598 total / $94.29 median over the 252 cards | 06-09 live | recon22.out B4b |
| 1,263 cards with active matched raw AND active matched graded-PSA sibling | 06-09 live | recon22.out B6 |
The Magikarp 203/193 live listing is a correct EN match (0.98, print_language=en); the Japanese Togekiss listing has aged off |
06-09 live | recon22b.out C4 |
| Foreign-title committed card matches per game: one-piece 212, dbs 105, pokemon 7, flagship 0 | 06-09 live | recon22.out A5 + recon22b.out C1-C3 |
| Grading cost baked into the stored spread (residual exactly $20 on every row) | 06-09 live | recon22.out B8 |
| Drain cohort lands on 83 of the then-383 cards; 232 buy-candidates; 6 sole-source / 77 augmented | 05-30 M3 record | recon20.out + recon20b.out |
| 231 name-consistent / 1 confirmed wrong pair (Togekiss listing on the Magikarp row); 5 heuristic false positives read and discarded | 05-30 M3 record | arb_unlock_classify.py on the 232-row buy-candidate set + manual review |
| Accuracy gate quote | stable | the strict matcher's header comment, verified in a local clone of the platform repo |
Arbitrage model (spread formula, tcg_product_id keying, opportunity-vs-deals split) |
stable | the platform's arbitrage API route and views, read in the repo |