Let agents pay the invoices. Prove they paid the right thing.
ETHGlobal Lisbon 2026 · Classic track · remithq.xyz
I run a padel club in Portugal. We get a lot of supplier invoices, and they are a pain — to manage, to approve, to check line by line, and to actually pay. The same job at a large company runs to hundreds or thousands of invoices a month.
Agents should obviously be doing this work. So why aren't they?
It is not AI capability. Models read invoices fine.
The blocker is authorisation. No finance director lets an agent pay a thousand invoices a month unless the control survives the agent being wrong, confused, or compromised. Today's controls cannot survive it.
Every company will tell you it has a control: two people must approve. Ask
what that actually verifies and it falls apart. It verifies that two accounts
clicked approve — not that two people did, not that they approved this
payment rather than one edited afterwards, and not that the thing paid is the
thing approved. That control was designed for a human in a browser. Point an
agent at it and a second "independent" approver costs one new Wallet().
Every payment request becomes one exact identifier — supplier, current account, proposed account, amount, purpose, expiry, nonce — hashed into a single digest. That digest is the payment, not a description of it. Then the work splits the way a finance director actually would:
| The 990 | Known supplier, unchanged account, within tolerance → the agent settles it unattended. Nobody is asked anything. |
| The 10 | Anything that changes where money can go → escalates and demands two provably distinct humans. |
┌──────────────────────────────────────────┐
invoice ────────▶│ packages/protocol │
│ canonical action (12 fields, Zod) │
│ digestCanonicalValue → action digest │
│ evaluatePaymentPolicy → route │
└───────────────┬──────────────────────────┘
│
STRAIGHT_THROUGH ◀──┴──▶ HUMAN_APPROVAL
│ │
│ ▼
│ ┌─────────────────────────────┐
│ │ packages/domain │
│ │ validateApprovalQuorum │
│ │ counts distinct HUMANS │
│ └──────────┬──────────────────┘
│ │ resolves via
│ ▼
│ ┌─────────────────────────────┐
│ │ packages/world-adapter │
│ │ AgentKit / AgentBook │
│ │ agent wallet → humanId │
│ │ World Chain eip155:480 │
│ └──────────┬──────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────┐
│ Hedera Testnet │
│ x402 paid verification (crypto service) │
│ supplier payment (crypto service) │
│ audit marker (token service) │
└───────────────┬──────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ packages/persistence (Postgres/Neon) │
│ payment_actions, approval_facts, │
│ workspace_people — scoped by org │
└──────────────────────────────────────────┘
Three facts are kept deliberately separate and never conflated:
- Human backing — is a unique human behind this agent? → World AgentKit
- Company role — is that person your treasurer? → the company's own issuer
- Action permission — may this proceed unattended? → policy over the digest
World tells you someone is a distinct person. It does not tell you their job. Conflating those is how you build a control that looks rigorous and isn't.
A beneficiary-changing invoice, end to end:
- Freeze. The request is parsed into
PaymentActionCoreV1and hashed withdigestCanonicalValueunder a domain separator. Change one character of the IBAN and the digest changes, which voids every approval bound to it. - Route.
evaluatePaymentPolicyreturnsSTRAIGHT_THROUGHorHUMAN_APPROVALwith a required quorum. A changed beneficiary withdraws the standing mandate, so the route becomesHUMAN_APPROVALwithactionHumanQuorum: 2. - Collect approvals. Each approver's agent wallet is resolved against
AgentBook to an anonymous
humanIdat approval time. The mapping is never stored — a cached agent→human would let anyone who reached our database manufacture a quorum. - Count humans, not accounts.
validateApprovalQuorumgroups byactionHumanPrincipal. Two wallets backed by one person collapse to one vote and the surplus is reported asACTION_HUMAN_NOT_DISTINCT. - Buy the evidence. The agent requests a beneficiary check and receives
402 Payment Required. It signs an exact-scheme payload; a facilitator verifies the payer signature on-chain and settles real HBAR after consensus. The agent pays the price; the facilitator absorbs the network fee — the agent is a paying customer, not a gas payer. The answer is signed over (digest, result, payment tx), so it cannot be lifted onto another invoice. - Keep the payment claim exact. Act 1 executes a separate routine supplier payment as a native HBAR transfer. The beneficiary-changing action proves approval and paid verification, but the current demo does not claim its supplier settlement is adapter-bound.
- Publish a marker. The approved action digest is minted as a no-value, treasury-held HTS NFT, its configuration and metadata are read back from Mirror Node, and it is burned as an explicit lifecycle operation. The marker is not the invoice, a receivable, payment authority, or settlement.
Routine invoices skip steps 3–5 entirely: the agent pays them directly.
JS/TS SDK only. No Solidity. No smart contracts deployed.
git ls-files '*.sol' returns nothing.
Three native Hedera services, matching the track's own examples:
| Service | Where it is used |
|---|---|
| Token Service | no-value operational marker — TokenCreateTransaction (NFT, finite supply 1, supply key only) → TokenMintTransaction with the action digest → TokenBurnTransaction |
| Mirror Node | the collection's key surface, digest metadata, mint and burn are independently read back before the demo states them |
| Cryptocurrency | a routine supplier payment (TransferTransaction) and x402 settlement of the verification fee |
That is token creation, configuration, and two lifecycle operations.
The isolated codex/hedera-dual-token-settlement branch also contains an
ambitious Testnet mechanism proof:
- an HTS KYC-flag-gated Payable NFT (
RMPAY) is issued to the action's preconfigured synthetic claimant; - an HTS KYC-flag-gated, frozen Control NFT (
RMCTL) is a one-use mechanics artifact; - a sealed, no-value synthetic-EUR token (
RMEURT) is the explicitly mapped settlement asset; and - one HIP-551 batch unfreezes the Control holder, transfers the exact settlement atoms while returning both NFTs, burns both NFTs, and publishes an HCS settlement commitment.
Every inner transaction has its normal role signatures. A canonical intent guard
binds the action, exact holder-beneficiary, amount, asset, audit topic, both
serials, outer and ordered inner IDs, batch key, size and validity window. A
separate pinned-protobuf verifier then decodes every outer node candidate and
requires the exact five bodies, transfers, burns, HCS message, IDs, duration,
batch key and cryptographically valid signer sets with no allowances or extras.
An HCS mechanics-execution.v2 event references the runner-observed receipt
after consensus because an in-batch message cannot truthfully contain a receipt
that does not yet exist.
The standalone runner uses a visibly labelled authorization-fixture.v2 event
and a MECHANICS_FIXTURE Control state to exercise network mechanics. Every HCS
event repeats that authority mode. It does not claim fresh World/x402
authority for that synthetic action, production readiness, legal KYC, legal
assignment of a receivable, or replay protection. See
ADR 0011.
Executed against real networks and verified by reading it back from Mirror Node, not from our own call:
| Claim | Evidence |
|---|---|
| Agent pays for verification via x402 | 0.0.9758618-1785026905-665197442 |
| Three-party economics | agent −1,000,000 tinybar · service +1,000,000 · facilitator −282,113 fee |
| Sealed marker collection | 0.0.9762937: finite supply 1; supply key only |
| Marker carries the exact digest | Mirror NFT record matches byte-for-byte |
| Marker lifecycle burn | deleted=true, total_supply=0, max_supply=1 |
| Sealed synthetic settlement asset | RMEURT 0.0.9764805: 48,000 atoms, finite supply, no management keys or custom fees |
| Strict HIP-551 rollback proof | INNER_TRANSACTION_FAILED; three children are REVERTED_SUCCESS |
| Wire-admitted dual-token atomic settlement | successful batch; RMPAY and RMCTL supplies are zero |
| Atomic and receipt-referencing HCS audit | topic 0.0.9764806 sequences the labelled fixture, commitment and execution event |
More in docs/evidence/.
corepack enable
pnpm install --frozen-lockfile
pnpm test # 522 tests
pnpm demo -- --offline # the full narrative, no network
pnpm demo # the full narrative, live on Hedera testnet
pnpm demo:dual-token -- --offline
pnpm demo:dual-token # experimental Testnet mechanics proof
pnpm gate:agentbook # does one human's two agents collapse to one id?The demo refuses to invent a human identity. Unregistered agents are marked
[SIMULATED] on every line they touch, and --simulate-humans makes that
explicit for rehearsal. A rehearsal can never be mistaken for a proof.
Worth separating precisely, because they are different claims.
Live on World Chain mainnet. Two agent wallets are registered in AgentBook and both resolve to the same anonymous human:
A1 0xA03F5F37…6f34 -> 0x157f9bb0a0a52ceab5931798d421683ae5bff28b5e956c9588b3eea88bb160c0
A2 0x4EaB3ef9…275B -> 0x157f9bb0a0a52ceab5931798d421683ae5bff28b5e956c9588b3eea88bb160c0
pnpm gate:agentbook reproduces that in about ten seconds. It is the claim the
product rests on: two wallets, two company roles, one person — so
validateApprovalQuorum refuses ACTION_HUMAN_NOT_DISTINCT on real identities,
not invented ones.
Live browser path on the demo branch. /approvals/INV-2026-0912 can open
World App for A1 using IDKit.request, with the exact payment action and signal
signed by the configured relying party. The browser returns the unmodified IDKit
result to Remit's server, which first checks the action, signal, nonce,
environment, user presence, credential and proof shape, then sends it to World's
v4 verification endpoint. A success consumes that short-lived session and
action-human combination in the running demo process.
Still simulated. The three quorum buttons and their approval events remain a local reducer. The verified phone proof is not yet admitted to the durable AP repository or payment quorum, and the page says so. Completing a phone scan demonstrates World's action-bound proof and server verification, not settlement authority.
Why. World proves that a unique human approved this payment. Remit still has to prove that the person's agent has the correct company role and atomically store the verified approval before settlement. Keeping those claims separate is the product's authority boundary, not an unfinished UI detail.
Remit proves the integrity of the authorisation path. It does not prove that a supplier owns a bank account, that an invoice is genuine, that a person is honest, or that a verification service is truthful.
- The HTS token is an audit marker, not payment authority. Its burn does
not prevent replay — replay is refused by the approval layer via
REPLAY_DETECTEDandACTION_DIGEST_MISMATCH. Seedocs/evidence/README.md. - Act 3's x402 payment is real and settles on Hedera. The beneficiary
verification service is not — there is no such service, so the demo stands
one in with a generated keypair and a fixed answer, marked
[SIMULATED]on screen. The signature binding is genuine; the verdict it carries proves nothing about the beneficiary. - The marker is not the legal or fiscal invoice and does not assign a receivable. Its metadata contains no invoice fields. See ADR 0010.
- The experimental Payable and Control NFTs add holder-consented lifecycle constraints, but neither NFT is company authority or a replay control. The Payable holder must independently equal the immutable action beneficiary.
- Act 1's transfer is a direct HBAR payment;
packages/hedera-settlement-adapteris not implemented. - The demo speaks x402 directly rather than through
packages/hedera-x402-adapter, so it is protocol-real but not adapter-bound. - 0G was evaluated and not integrated: opening a compute ledger requires a 3
0G minimum enforced by the LedgerManager contract. See
docs/sponsors/ZERO-G-NO-GO.md.
It inherits World's proof-of-personhood rather than creating it: the strength of the quorum guarantee equals the strength of World's verification level.
Being precise about this is not a weakness in the pitch. It is why the claims we do make are believable.
Node.js 24.11.0, pnpm 11.17.0 via Corepack. The root demo launcher finds the
required Node binary when the current shell is on another version, starts only
the web app under Webpack, isolates its cache, and caps the V8 heap at 2 GiB.
pnpm check # format, lint, typecheck, test, build
pnpm migrate # apply persistence migrations
pnpm dev # resource-bounded web demo on http://localhost:3000
pnpm dev:reset # also discard the isolated demo cache
pnpm dev:all # all seven long-running services; only when neededpnpm dev is the normal judge/demo command. It deliberately does not start the
control API and five workers because the browser walkthrough does not need them.
pnpm dev:all starts the complete development topology:
| Process | Port |
|---|---|
| Web | 3000 |
| Control API | 4100 |
| Extraction worker | 4150 |
| Verifier | 4200 |
| x402 facilitator | 4300 |
| Payment agent | 4400 |
| Settlement worker | 4500 |
The web app uses Google through Better Auth for application identity and
organization membership. Copy only the variable names from .env.example into
apps/web/.env.local and provide:
BETTER_AUTH_URL(http://localhost:3000locally);- a random
BETTER_AUTH_SECRETof at least 32 characters; GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET;- pooled
DATABASE_URL; and - unpooled
DATABASE_URL_UNPOOLEDfor migrations.
The Google OAuth client must allow
http://localhost:3000/api/auth/callback/google locally and the corresponding
production origin callback. Apply the isolated, idempotent auth migration before
enabling sign-in:
cd apps/web
pnpm auth:migrateThe migration owns only the invoiceguard_auth schema. Google and Better Auth
organization roles never grant payment authority; that remains a separate
control-API decision.
| Process | Local port |
|---|---|
| Web | 3000 |
| Control API | 4100 |
| Extraction worker | 4150 |
| Verifier | 4200 |
| x402 facilitator | 4300 |
| Payment agent | 4400 |
| Settlement worker | 4500 |
Run the complete local quality gate before every push:
pnpm checkWork began in this repository during ETHGlobal Lisbon 2026. See HACKATHON_PROVENANCE.md.
Apache-2.0.