The review queue
Where the matcher's refusal to guess is paid for, and where the system learns so it does not have to refuse the same thing twice.
Six of the matcher's seven outcomes end in needs_review. A queue that only ever grew would make that design indefensible, so the queue is built around one action that shrinks it permanently: linking a sending address to a customer. Everything else resolves one transfer; that one changes what happens to every future transfer from that address.apps/worker/src/matcher.js:204
It shows the reasoning, not just the verdict
A queue item that says “needs review” and nothing else hands the merchant the whole problem back. Each item instead carries the reason in plain English, the candidates the matcher weighed, and the signed difference against each — “short by 0.40”, “over by 0.30”, “exact”.apps/web/components/dashboard/review-card.tsx:17
This is the same principle as classifying failures before spending gas: the product's judgement should be legible. A merchant should be able to see exactly what evidence existed and why it was not enough, rather than being told a verdict and left to reconstruct it.
The five actions
These are the only mutations anywhere in the dashboard. Every other page is read-only, and all five route throughresolveReview — the same code path the CLI uses — so the interface cannot perform a resolution the engine would refuse. apps/web/app/dashboard/review/actions.ts:10
| Action | What it does | Effect on the ledger |
|---|---|---|
| Apply in full | Attributes the whole transfer to one obligation. | Obligation becomes paid. |
| Apply as partial | Attributes less than the transfer's value. | Obligation becomes partially_paid and stays outstanding. |
| Apply with surplus | Attributes more than the obligation still owed. | Obligation is settled; the excess is recorded as surplus rather than inflating it. |
| Not a payment | Records the human judgement that this is not a payment. | Transfer becomes ignored. It is kept on record, never deleted. apps/worker/src/matcher.js:242 |
| Link sender to customer | Teaches the matcher that this address belongs to this customer. | Writes a customer_addresses row; optionally applies in the same transaction. apps/worker/src/matcher.js:232 |
auto_matched. Whatever a person does in review is recorded with confidencemanual, so the audit trail permanently distinguishes what the matcher decided from what a person decided.apps/worker/src/matcher.js:249The learning loop
Linking is the only action with a future. Because (chain_id, address) is unique, an address belongs to at most one customer, so linking is an unambiguous statement rather than a hint. The next transfer from that address for an exact outstanding amount satisfies |X| == 1 and matches on its own.packages/db/migrations/003_expected_payments.sql:39
transfer from 0xcD55…AF2D, 65.000000 USDC
-> unknown_sender (amount matches; nobody owns that address)
-> human links it to customer #7, applying at the same time
-> obligation paid
next transfer from 0xcD55…AF2D for an exact outstanding amount
-> exact_known_sender auto-matched. the review does not recur.That last step is asserted, not asserted-and-hoped: the drill links a sender, then re-classifies an identical fresh transfer and requires the verdict to be auto_matched / exact_known_sender.scripts/phase2-dashboard-drills.mjs:228
The interface reflects this rather than hiding it. Linking is presented in its own framed block as the action that teaches the matcher, not as one button among five.apps/web/components/dashboard/review-card.tsx:136
Why the actions are disabled on the public deployment
The dashboard has no authentication. On a public URL that is fine for reading testnet data whose addresses are already public, and not fine for writing: anyone who could open the page could otherwise alter the records they were looking at. So the write path is gated on an environment flag. apps/worker/src/matcher.js:216
Unset means denied, which is the important direction — a new or misconfigured deployment is read-only by accident rather than writable by accident. apps/worker/src/matcher.js:224 The gate sits in resolveReviewitself rather than in the interface, so disabling the buttons is a courtesy and not the control: posting the server actions directly is refused too.
- Locally, with the flag set, all five actions work and the CLI is unaffected.
- On the deployment the page states plainly that actions are disabled, and why. apps/web/app/dashboard/review/page.tsx:24
- Authentication is listed as not built, with the exposure bounded: no path in the review actions moves money. See current limitations.