About Landed
Fills the payment-verification clerk seat and the first hour of the fraud-response coordinator seat; a human still releases money.
What the agent can and cannot do
Permissions as enforced in the database, not as prompt instructions.
| Capability | Agent (LLM / intake / package actions) | Human (dashboard) | Database rule |
|---|---|---|---|
| Read a vendor email and PDF, extract a requested account | yes | yes (manual edit) | every new account starts unverified and opens a case (I2) |
| Open a case and queue the probe deposit | yes (via bankAccounts.createUnverified) | yes | same |
| Re-point open invoices to the requested account (held) | yes | yes | invoices follow the request; nothing is payable until verified |
| Record a stated amount | only trusted channels (voice of record, portal login); email is stored rejected | "Simulate vendor answer" in sim mode, labeled | I4 |
| Verify an account (markVerified) | no | no (only the bound confirmation does, atomically) | I3 |
| Pay | no | yes, and only to a verified current-version account of that vendor | I1, fail closed |
| Exceptional release | no | two distinct named approvers + written reason, from NEEDS_HUMAN only | logged with both names |
| Edit contacts or account status | no | supersede contact; add human-attested record contact | I5 |
| Draft the recovery package prose | yes, from deterministic fields; every number verified | reviews, signs, calls | fallback to templates |
| Know the probe amount | no (not in prompts, tools, events, or public queries) | no (operator reads the bank statement; test-only endpoint is key-gated) | I7 |
When it asks for a human: wrong or missing vendor statement, bank and contact changed in the same request, SLA expiry with no statement, portal attempts exhausted, validation-provider mismatch, and any landing deadline that passes without the vendor confirming.
Where Claude could go wrong, and what catches it
- It misses an implicit change buried in a PDF, so the deterministic guard still blocks any unverified destination.
- It drafts package prose with a wrong number, so every number is checked against the deterministic fields and templates are the fallback.
- It over-flags a benign email, so a human can reject the change and restore the prior account.
- It cannot verify anything, because it has no release path: the release gate and the payment guard are database mutations with no LLM input.
How contacts of record are established
The one thing the attacker does not control.
A contact of record is a phone number or portal login captured at onboarding and confirmed by a call placed to a number from the signed contract, never to a number from a request email. Changing it is a separate human-attested step with a written reason; a request that changes the bank account and the contact together goes straight to a human.
Built on Convex: schema and indexes, transactional mutations as the security boundary, actions for LLM/rails/channels, scheduler as dispatch, crons for tick, HTTP actions, file storage, real-time subscriptions, convex-test.
What is real
- the Convex engine and the eight invariants (serializable mutations make the release gate and idempotency atomic)
- versioned bank accounts with HMAC hashing and AES-256-GCM encryption of account numbers
- the append-only audit log with redaction
- the rule-based extractor and the Claude extractor (PDFs passed as documents) when a key is set
- the vendor portal login (bcrypt, cookie session)
- the eval harness: 20 scenarios run twice (extractor on, extractor off)
- Nessie transfers on the mock bank (deposit plus withdrawal, integer cents) when the rail is switched to Nessie
- the recovery package's deterministic fields; Claude prose is checked number by number
What is simulated
Every simulation is labeled on screen.
- the bank (SimRail, an in-database ledger) unless Nessie is selected
- time (a demo clock drives ACH windows, availability, SLAs and landing deadlines)
- vendor answers (the eval harness or a human types what the vendor sees; the Vapi/Cartesia voice channel is written but was not dialed)
- email authentication results (seeded)
- the vendor's bank statement
How we know
89 unit and invariant tests (vitest + convex-test), a scripted click-through of every page, and an eval of 20 scripted scenarios run twice (extractor on and off). See Eval for the per-scenario table and the metrics. These are results on scripted cases; they do not establish effectiveness against real-world fraud.
Invariants (enforced in code, each with a test)
- I1 Payment boundary
- payments.send is the only function that inserts a payment. It refuses unless the destination exists, belongs to the invoice's vendor, is verified, and is the current version of that account identity. Fail closed.
- I2 New destinations start unverified
- and open a case, whatever path created them (intake, manual edit, seed).
- I3 Verification is bound
- to case, vendor, account hash, account version, probe, probe availability, exact amount, a contact that is of record now, and an unused, unexpired confirmation. Success flips the account, marks the confirmation used, transitions the case, and logs, atomically.
- I4 Untrusted channels cannot confirm
- Email statements are stored as rejected and never considered.
- I5 The agent cannot release
- No LLM-calling action can reach markVerified, payments.send, exceptionalRelease, or account status.
- I6 Money movement is idempotent
- Probes and payments carry deterministic idempotency keys.
- I7 The probe amount is secret
- No public query, event, or channel configuration carries it.
- I8 Timers are deterministic
- Every timeout is a row on the application clock, fired by one tick.