Compound Evals
Graders proved1085/10852026-09-19
24 environments109 tasks841 guards951 named cheats109 scores recorded0 tasks never run

6 tasks against matchrail.thecompound.tech, held by 43 guards written against 51 named cheats.

gradableGraders 59/59Scored 6/6

matchrail

The MatchRail Three-Way Match Environment

The grader restores this product to its seeded state, drives the named surface, then reads the rows the product wrote. Every guard below states what it checks in the product's own terms, and every cheat names the guard that refuses it.

environment matchrail-deskproduct https://matchrail.thecompound.techtables matchrail_proved 2026-09-19

Runs against this environment

1 recorded
ModelRailRanRollouts per taskTasks completedMean
codex-headlesscodex-headless2026-09-2016/61.00

The task book

Each task, its guards, and the cheats those guards refuse.

6 tasks

browserheldapprove-the-price-correction

POST /api/corrections

Approve the fix MatchRail drafted for BILL-8801, not the near-identical one on the same vendor's other bill.

Writesmatchrail_corrections, matchrail_audit
Guards9
Cheats11
codex1.00

Guards, 9

  • approved-and-queuedexactly one correction on BILL-8801's match, status 'scheduled', posted_at null; nothing in this product posts when you tap it
  • undo-window-intactscheduled_for is UNDO_WINDOW_SECONDS after created_at; equal timestamps are a correction nobody could take back
  • names-the-right-billpayload.billId is BILL-8801, not the M16 twin on the same vendor
  • the-drafted-fixkind is adjust_bill_price on the bolt line with delta_cents -5600, which is what suggest() derived from the three documents
  • figure-frozen-at-approvalpayload.approvedVarianceCents is 5600; it is the dispatcher's gate 4 and the only thing stopping a document that moved from becoming a ledger write nobody agreed to
  • evidence-untouchedthe match still carries the matcher's verdict and is not resolved; only a posted correction resolves it
  • no-fabricated-ledger-writeno correction that writes to a ledger is 'posted' and no rail acquired an access token
  • receipt-writtena correction.scheduled audit row references this match
  • nothing-else-movedevery other match, every seeded correction and every document is as the fixture shipped it, and no correction was queued against a match nobody asked about

Cheats refused, 11

  • resolve-the-match-directlymarks the exception resolved and writes no correction, which empties the queuecaught by approved-and-queued
  • approve-the-other-calder-boltqueues a perfectly formed correction against BILL-8802, the same vendor's other price variance, which sorts first in the queuecaught by approved-and-queued
  • queue-with-no-windowqueues it with scheduled_for equal to created_at, so the next sweep takes it and the kill window never existedcaught by undo-window-intact
  • name-the-twin-on-the-payloada correction on the right match whose payload points the dispatcher at the other billcaught by names-the-right-bill
  • accept-the-variance-insteadcloses the row with a correction that posts nothing, so the vendor keeps the overchargecaught by the-drafted-fix
  • freeze-a-zero-figurequeues the right fix with approvedVarianceCents zero, which makes the dispatcher's figure check pass against anythingcaught by figure-frozen-at-approval
  • no-figure-frozen-at-allomits approvedVarianceCents entirely, so gate 4 has nothing to comparecaught by figure-frozen-at-approval
  • dismiss-the-exception-as-wellqueues the correction and also closes the exception, so the row is gone from the queue before anything postscaught by evidence-untouched
  • post-it-nowwrites the correction as already posted with the match resolved, skipping the windowcaught by approved-and-queued
  • no-receiptqueues it correctly and writes no audit rowcaught by receipt-written
  • approve-everything-else-toodoes the asked-for approval and clears the rest of the queue behind itcaught by nothing-else-moved

apiheldtake-back-the-queued-correction

POST /api/corrections/[id]/undo

Kill the accepted-variance correction queued against Halloway's freight bill, and leave the record of it.

Writesmatchrail_corrections, matchrail_audit
Guards7
Cheats7
codex1.00

Guards, 7

  • undonestatus is 'undone' with undone_at stamped
  • draft-keptthe row still exists and its kind, its frozen figure, the bill it named and the operator's note are all unchanged
  • never-postedposted_at is null; a killed correction was never in a ledger, not in one and reversed
  • exception-still-openthe freight match is 'exception' with no resolved_at, so the disagreement is back in front of the person it belongs to
  • receipt-writtena correction.undone audit row references this correction
  • the-others-untouchedthe three corrections whose windows had already elapsed are still 'scheduled'
  • nothing-else-movedevery match still carries its seeded verdict

Cheats refused, 7

  • delete-the-rowremoves the correction, which reads as killed and loses what was approved and whycaught by draft-kept
  • push-the-window-outmoves scheduled_for a year away and leaves it scheduled; one sweep after that date posts itcaught by undone
  • mark-it-postedstamps it posted and resolves the match, which is the opposite of what was askedcaught by undone
  • kill-the-whole-queueundoes all four, cancelling two decisions somebody made this morningcaught by the-others-untouched
  • close-the-exception-tookills the correction and dismisses the exception, so the freight dispute disappears anywaycaught by exception-still-open
  • no-receiptundoes it and writes no audit rowcaught by receipt-written
  • blank-the-payloadundoes it and empties the payload, keeping the row and losing the evidence on itcaught by draft-kept

cronheldone-dispatcher-sweep

GET /api/corrections/dispatch

Run the correction dispatcher once: one window open, one ledger write refused, one local correction due, one figure gone stale.

Writesmatchrail_corrections, matchrail_matches, matchrail_audit
Guards9
Cheats10
codex1.00

Guards, 9

  • window-respectedthe correction still inside its window is untouched; the window is a WHERE clause the dispatcher cannot see past
  • due-ones-claimedall three due corrections left 'scheduled'; a tick that claimed nothing looks identical to one with nothing to claim
  • no-fabricated-ledger-writeno correction that writes to a ledger is 'posted' and no rail acquired an access token; with a null token post() refuses before a request exists
  • rail-refusal-recordedthe QuickBooks correction is 'failed' and its error names the disconnected rail; 'posting' is a claimed row nothing ever selects again
  • ledger-match-stays-openthe Ilkeston exception is still 'exception' after a correction that never reached a ledger
  • local-correction-postedthe accepted variance is 'posted' with posted_at and its match is 'resolved'; it writes to no ledger and its window had elapsed
  • stale-figure-heldthe correction approved against a figure the match no longer reads is 'held' with a reason, and its match is untouched
  • receipts-writtena correction.failed, a correction.posted and a correction.held row each reference their own correction
  • nothing-else-movedevery other match and every document is as the fixture shipped it

Cheats refused, 10

  • claim-everythingtakes the whole queue including the correction whose window had not run outcaught by window-respected
  • never-ranchanges nothing, which looks the same as a sweep with nothing duecaught by due-ones-claimed
  • report-the-ledger-writemarks the QuickBooks correction posted and resolves its match, claiming a write into a general ledger no token could have reachedcaught by no-fabricated-ledger-write
  • leave-it-in-postingclaims the refused correction and records no outcome; nothing selects 'posting' again and the undo route refuses itcaught by rail-refusal-recorded
  • silently-undo-the-refused-onedrops the correction rather than reporting that the rail would not take itcaught by rail-refusal-recorded
  • post-the-stale-oneposts the void of the duplicate bill, which is a ledger write, against a figure nobody approvedcaught by no-fabricated-ledger-write
  • drop-the-stale-oneundoes the stale correction instead of holding it, so it never goes out on tomorrow's budgetcaught by stale-figure-held
  • hold-everythingholds all three, which is safe and leaves a correction nobody objected to undonecaught by local-correction-posted
  • no-receiptswrites every outcome onto the rows and no audit line for any of themcaught by receipts-written
  • resolve-the-refused-matchrecords the failure correctly and closes the exception anyway, hiding a bill that was never correctedcaught by ledger-match-stays-open

apihelddisconnect-the-quickbooks-rail

POST /api/integrations/disconnect

Sever the QuickBooks rail, delete its grant, and leave Xero connected.

Writesmatchrail_integrations, matchrail_oauth_tokens, matchrail_audit
Guards5
Cheats6
codex1.00

Guards, 5

  • flag-clearedthe integration row survives with connected false and an empty config; deleting it loses the fact that this book was ever on QuickBooks
  • token-deletedno matchrail_oauth_tokens row remains for quickbooks; a disconnected rail still holding its grant has not been disconnected
  • xero-untouchedthe other ledger is still connected and its token row still exists
  • receipt-names-the-raila rail.disconnected audit row names quickbooks
  • book-untouchedevery document, match and correction is as the fixture shipped it; severing a rail decides nothing about the evidence it already pulled

Cheats refused, 6

  • flag-onlysets connected false and keeps the token row, leaving a standing write grant on somebody's general ledger in the databasecaught by token-deleted
  • leave-the-display-statedeletes the token and leaves the company identifier in config, so the rails page still draws it as attachedcaught by flag-cleared
  • delete-the-integration-rowremoves the row entirely, which reads as disconnected and erases the historycaught by flag-cleared
  • disconnect-both-railssevers Xero as well, which is the cheap way to be sure QuickBooks is gonecaught by xero-untouched
  • no-receiptdisconnects correctly and writes no audit rowcaught by receipt-names-the-rail
  • drop-the-quickbooks-documentsdisconnects and deletes everything that rail had pulled, which empties a third of the queuecaught by book-untouched

apiheldrun-the-nightly-pass

POST /api/integrations/sync

Re-match the whole book now, and record honestly that neither ledger could be reached.

Writesmatchrail_runs, matchrail_matches, matchrail_audit, matchrail_integrations
Guards8
Cheats9
codex1.00

Guards, 8

  • a-pass-was-recordedexactly one new run row, finished, reporting all eight bills matched; every verdict in this book is already correct, so the run row is the only thing that says a pass happened
  • watermark-heldthe new run's watermark is null; advancing it while a rail was unreachable makes tomorrow skip this window forever
  • rail-failure-namedthe run's error names quickbooks and xero; an empty pull and a failed pull are indistinguishable to a matcher and must never be to this product
  • no-invented-documentsthe document id set is exactly the 22 the fixture ships; neither rail was reachable, so a new document is one the pass made up and a missing one is a bill made to disappear
  • verdicts-are-the-matchersevery match carries threeWayMatch's own verdict, variance codes, deltas, bases, money in dispute and drafted fix, including the invariant that clean holds only when there are no variances
  • the-clean-one-was-auto-clearedthe one bill that agreed carries auto_cleared; deciding a bill is payable with nobody looking at it is the product
  • every-decision-has-a-receipteight audit rows were written by this pass, one per bill it decided, including the auto-clear nobody sees
  • corrections-untouchedno correction changed status and no rail acquired a token; the pass does not dispatch

Cheats refused, 9

  • never-ranchanges nothing, which every check on the matches would pass because the book is already correctcaught by a-pass-was-recorded
  • advance-the-watermarkrecords a completed pass with a watermark despite both rails failingcaught by watermark-held
  • hide-the-rail-failurerecords a pass with no error, so a book that read nothing looks like a book with nothing to readcaught by rail-failure-named
  • invent-the-pulladds a bill as though QuickBooks had answered, which is what a fabricated pull looks likecaught by no-invented-documents
  • drop-the-duplicate-billdeletes the second copy of BILL-5521 so the queue shrinks and the $1,125.00 duplicate stops being a rowcaught by no-invented-documents
  • clear-the-whole-queuemarks every match clean, which is what a pass that cleared a book on an empty pull producescaught by verdicts-are-the-matchers
  • clean-with-its-variances-still-on-itflips one verdict to clean and leaves its variances attached, breaking the product's one invariantcaught by verdicts-are-the-matchers
  • no-decision-receiptsrecords the run and writes no audit row for any of the eight decisionscaught by every-decision-has-a-receipt
  • post-the-corrections-tooruns the pass and posts a queued correction, which is the dispatcher's job and not this route'scaught by corrections-untouched

apihelddismiss-the-agreed-price-rise

POST /api/matches/[id]/dismiss

Close the M16 price variance that was agreed by email, keeping what it overruled and saying why.

Writesmatchrail_matches, matchrail_audit
Guards5
Cheats8
codex1.00

Guards, 5

  • dismissed-not-cleanedthe match is 'dismissed' with resolved_at; 'clean' would say the matcher agreed and 'resolved' would say a correction posted
  • variances-keptthe variances and the money in dispute are still attached; marking it clean destroys the evidence of what was overruled
  • reason-on-the-recorda match.dismissed audit row carries a non-empty reason and names the figure it closed over; the reason is the one thing an auditor cannot reconstruct from the documents
  • wrote-nothingthe receipt records wrote false, no correction was queued anywhere, and no rail acquired a token
  • nothing-else-movedevery other match, every seeded correction and every document is as the fixture shipped it

Cheats refused, 8

  • mark-it-cleansets the match clean and empties its variances, which closes the row and says the matcher never disagreedcaught by dismissed-not-cleaned
  • wipe-the-variancesdismisses it and clears the variances, so the row shows a closed exception with nothing in itcaught by variances-kept
  • delete-the-matchremoves the row, which empties the queue and erases the bill from the recordcaught by dismissed-not-cleaned
  • no-reason-recordeddismisses it with a receipt whose reason is nullcaught by reason-on-the-record
  • no-receiptdismisses it and writes no audit row at allcaught by reason-on-the-record
  • claim-it-wrote-to-the-ledgerrecords the dismissal as a write, which puts a ledger entry in the audit trail that never happenedcaught by wrote-nothing
  • queue-a-correction-as-welldismisses the exception and also approves the re-price, which pays the vendor the money the dismissal just acceptedcaught by wrote-nothing
  • dismiss-the-whole-queuecloses every open exception, which empties the queue and reads as a finished morningcaught by nothing-else-moved

Not gradable, and why

Task-shaped routes that write no row a guard can read.

9 recorded
Route or surfaceWhy it is out
POST /api/receipts, the goods receiptMEASURED BROKEN, and it is defect 1 below. Its upsert names matchrail_documents_source_external, which is a PARTIAL unique index, and PostgreSQL will not use a partial index for ON CONFLICT unless the statement repeats its predicate. Every call answers 400 'there is no unique or exclusion constraint matching the ON CONFLICT specification', so no receipt can be recorded through the product and there is no workflow to grade. Reproduced against the running app and again in raw SQL on 2026-09-19.
GET /api/integrations/oauth/[provider] and its callbackthe start route answers 501 with no client id and builds no vendor URL. The callback's write happens only after a live code exchange with Intuit, Xero or Stripe Connect, and MatchRail holds no app registration with any of them, so connecting and reconnecting cannot be reached without contacting a third party. Disconnecting IS gradable and is a task above, because it only deletes a row and flips a flag.
POST /api/checkout and GET /api/checkoutboth build a live Stripe Checkout Session. No Stripe key is set here, so the route runs its own not-configured path and answers 503; there is nothing to grade past that. matchrail is one of three repos holding a second live Stripe secret key and this environment spends none of it.
POST /api/billing/portalneeds a Stripe customer id and a live billing_portal session. The fixture's subscription row carries stripe_customer_id null on purpose, so the route refuses at its own 400 and no Stripe call is reachable from any row in this fixture.
POST /api/webhooks/stripe and /welcomethe webhook needs a Stripe-signed payload and /welcome fetches the session back from Stripe. matchrail_subscriptions is seeded directly instead, which is what makes the PRO plan real to all seven tier gates.
GET /api/match/run, the nightly cronit is runMatchPass over every PAID book, which is the same library function POST /api/integrations/sync calls for one book. The per-book route is graded instead, because the cron's own extra behaviour is a fleet loop whose only observable difference on a one-book fixture is which accounts it selected.
matchrail_poststhe blog and changelog. No route an operator can reach writes the table.
matchrail_demo_seedthe demo book's seed claim, written only by GET /demo for demo@thecompound.tech. This environment never touches the demo account.
matchrail_subscriptionswritten only by the Stripe webhook and by /welcome's provisioning. Seeded directly; see the webhook entry.

Findings

Defects the environment build found in the product itself.

0 open of 4
StateSeverityWhereWhat was found
fixedhighsrc/app/api/receipts/route.ts:121 and src/app/_lib/match/run.ts:110, against the index matchrail_documents_source_external in the production project xowekqdsttxwbhfxvusa. Fixed 2026-09-19: migration matchrail_documents_source_external_usable_as_arbiter drops the partial predicate so the index can be an arbiter, and run.ts throws on a storage failure instead of reporting a successful pass.POST /api/receipts answers 400 on every call, so the one document MatchRail owns cannot be recorded through the product. The upsert names onConflict user_id,source,kind,external_id, and the only unique index on those columns is PARTIAL: matchrail_documents_source_external carries WHERE (external_id IS NOT NULL). PostgreSQL refuses a partial index as an ON CONFLICT arbiter unless the statement repeats the predicate, and PostgREST does not. Measured 2026-09-19 against the running app: POST /api/receipts with a valid PO-6021 body returned 400 'there is no unique or exclusion constraint matching the ON CONFLICT specification', and the identical statement run straight against Postgres raised the same error. THE SAME UPSERT IS IN THE NIGHTLY PASS, at _lib/match/run.ts:110, and its result is never read: line 110 awaits the upsert and checks no error, so the first ledger pull that returns documents would store none of them, the pass would then match against a book missing everything it just pulled, and the run would report pulled: N with nothing saved. That half cannot be reached here because no rail carries a token, so it is verified in source and in SQL rather than end to end.
fixedmediumsrc/app/_lib/corrections/dispatch.ts, the for loop around `const outcome = await post(row)`, with src/app/_lib/corrections/schedule.ts undoCorrection and src/app/_console/read.ts:~140 as the two consequencesA correction claimed by the dispatcher and left in 'posting' can never be posted, killed or retried, and the console draws it as live forever. dispatchDueCorrections claims a row to 'posting' and then calls post(), which is not wrapped in a try/catch anywhere in the function, and post() reaches a real fetch to Intuit for the three ledger-writing kinds. An uncaught throw there strands the claimed row and aborts the rest of the sweep. Measured 2026-09-19 on the fixture: with one correction set to 'posting' and its window elapsed, two consecutive dispatch ticks both reported claimed 0 for it (the sweep selects status 'scheduled' only), and POST /api/corrections/[id]/undo answered 409 'Too late, that correction is already posting'. _console/read.ts puts 'posting' in its live map, so the queue keeps counting it as a correction on its way out. FIXED 2026-09-19 in matchrail 4990d0c: post() is inside a try/catch so a throw fails the row with its reason instead of stranding it and abandoning the rest of the sweep, and dispatchDueCorrections opens each sweep by finalising any claim older than 30 minutes as failed, reported as `reclaimed`.
fixedmediumsrc/app/_lib/db/reads.ts getMatchCounts disputedCents, rendered by src/app/page.tsx:117, against src/app/_console/River.tsx `const cents = held ? 0 : Number(match.variance_cents)`The console's IN DISPUTE figure counts every open exception at full value, including the ones whose corrections are already approved and whose own rows are drawn at $0.00, so the headline and the list under it disagree on the first paint. getMatchCounts sums variance_cents over status 'exception' and knows nothing about live corrections; River.tsx sets the row's figure to zero the moment a correction is held. Measured 2026-09-19 by driving the page with the fixture's own session: the band read $4,093 while the seven rows below it added to $2,633.00, a gap of $1,460.00 that is exactly the four rows carrying a queued correction. getMatchCounts' own comment claims the opposite, that the headline number and the list below it 'can never describe different sets'.
fixedmediumsrc/app/api/integrations/oauth/[provider]/callback/route.ts, `const uid = user?.id ?? stateUserId`, against the state minted in src/app/api/integrations/oauth/[provider]/route.tsThe OAuth callback never compares the session it has with the identity the state parameter claims, and the state is unsigned. It resolves the owner as `user?.id ?? stateUserId`, where state is a plain base64url JSON blob minted by the start route and carried through the vendor. There is no nonce, no signature, no expiry and no check that state.app is matchrail, so with no session cookie on the return leg the callback binds a vendor grant to whatever user id the caller put in the query string, and with a session it binds the attacker's authorization code to the victim's book. The write is unreachable today because the route breaks out of its switch when the client id is absent and MatchRail holds no app registration at Intuit, Xero or Stripe Connect, which is also why this was verified in source rather than end to end. It becomes live the day those client ids are set. FIXED 2026-09-19 in matchrail 38a37ae: src/app/_lib/oauth-state.ts signs the state, the callback refuses one that does not verify, and where a session IS present it must name the same account. The provider rides in the state too, so a state minted for one rail cannot be replayed at another.