Inspiration

Built solo in about thirty hours at Cal Hacks 13.0.

Freight brokers pay thousands of carrier invoices. Many carriers sell their receivables to a factoring company, so the party you must pay is often not the carrier on the invoice, but the factor named in a Notice of Assignment (NOA). Fraudsters exploit this with "updated banking" emails and invoices that quietly change the remit-to. A model that "pays invoices" is the wrong design: one persuasive document should never be enough to move money. We wanted an agent that does the reading and the paperwork, and is structurally unable to pay the wrong party.

What it does

Remit-To runs three stories on a live board that two people watch from two browser tabs:

  • Redirect (L-2041): A June invoice and an "updated banking" email ask us to pay the carrier directly. A verified March notice of assignment routes payment to Meridian Capital. The policy holds the payment (rule R2), shows the NOA as evidence, and Claude drafts the escalation email to the factor. No payment request can be created.
  • Duplicate (L-2037 re-bill, L-2033 race): A second invoice for an already-requested load arrives under a new number and is held (rule R3). Then two requests for the same load are sent in the same instant: the database keeps one reservation and logs the other as a refusal.
  • Clean (L-2037, L-2029): The invoice matches the rate confirmation and proof of delivery. The agent or the AP lead requests payment; a different person approves; the runner submits with an idempotency key it saved first; the board walks through submitted to simulated_settled.

Separation of duties is enforced by the database, not the UI: the agent identity cannot approve anything, the AP lead cannot approve their own request, approvals are bound to a snapshot of amount, payee, and bank version, and a bank change before dispatch sends the request back to pending. Every refusal is a row in the audit log with the real sender's identity.

How we built it

  • Read: Rendered PDFs go to Claude (claude-sonnet-4-5) with vision. It returns typed fields (invoice number, amounts, remit-to matched against the carrier and factor roster, confidence) and a short explanation. Results are cached by content hash. Claude also writes the plain-English explanation and the escalation email from the policy result. The policy never reads prose.
  • Decide: module/rules.ts is one pure TypeScript file: eight rule families, thirteen checks per invoice, plus checkApproval. The agent imports it to explain; the SpacetimeDB module imports the same file unchanged to decide.
  • Enforce: A SpacetimeDB 2.13 TypeScript module with 11 tables and 11 reducers. Every reducer derives the actor from ctx.sender; no client ever names its own role. Reducers re-run the policy on trusted rows inside the transaction that reserves the obligation. Unique indexes on the load and on carrier plus normalized invoice number are the reservation.
  • Dispatch: A payment runner claims an approved request, freezes the payload and idempotency key in the database, then calls a labeled in-process mock provider. If it crashes after the provider accepts, the restarted runner looks the transfer up by the stored key: one transfer, one submitted event.
  • Show: A Vite app: a landing page with a live read-only board embed, and the board itself, where each persona tab is its own database identity. npm run demo:up resets, seeds, and starts everything with one command.

Challenges we ran into

  • Auditable refusals: In SpacetimeDB, a thrown error rolls back the whole transaction, including the audit row we just wrote. We verified this with a probe reducer, then made every business refusal log and return normally so the refusal is a committed event.
  • Keeping the model out of the decision while keeping it useful: Claude produces typed fields and prose. The prose is display-only, the reducer recomputes from trusted rows, and the agent's own policy run is only a preview the database can overrule.
  • The crash between "provider accepted" and "we recorded it": Solved by persisting the idempotency key and frozen payload before dispatch and reconciling by key on restart.
  • Concurrency: Two workers picking up the same invoice is a real failure mode. Serialized reducers plus unique indexes make the second request a refusal, not a second payment.

Accomplishments we're proud of

  • The agent cannot pay anyone, and that is enforced by identity and transaction boundaries rather than by prompt.
  • 36/36 exact on the offline scorer, 0/24 false holds, 0/12 unsafe requests. These are synthetic development fixtures with an explicit answer key the agent never reads, not a blind evaluation.
  • 22 offline tests and 15 live integration tests against a fresh database, including the race, self-approval, stale approval, and crash recovery.

What we learned

  • When the identity boundary and the transaction boundary are the same boundary, a whole class of authorization bugs disappears. There is no API server to get wrong.
  • Explicit, auditable policy is a feature in AP, not a limitation. Finance teams need to know why a payment was held.
  • "Unknown" is not "failed," and neither is permission to pay again.

What's next for Remit-To

  • A real payment rail behind the runner's adapter (a bank or payouts sandbox). The runner already uses the idempotency-key pattern those APIs use.
  • Live carrier authority lookups and bank-change callback verification instead of seeded state.
  • Accessorials, assignment releases, and conflicting assignments.
  • Policy suggestions learned from reviewed holds, always behind human approval.

Built With

Share this project:

Updates

Submission history