Skip to content
Prabal Pratap Singh
All work

AI agent safety · Paymentsby WitLabs2026Preview

Twokey

Approvals for AI agents that move money. The agent proposes, deterministic policy code decides, and anything that adds risk waits for a human approval sealed with SHA-256 to the exact terms.

twokey-site-production.up.railway.app
Twokey website preview

At a glance

4 lanes

  • Every action lands in one lane: lowers risk, inside the mandate, adds risk, or breaks a rule
  • Approvals are sealed with SHA-256; if the terms change, the approval is void
  • Same idempotency key never pays twice; an unknown outcome is never retried

What it is

Let AI agents pay routine bills on their own while a new payee, a changed bank account or an amount over the limits always waits for a person, and fraud is blocked outright.

Why it exists

Business email compromise cost $3.05B in 2025 (FBI IC3), mostly through one email saying a vendor's bank details changed. Accounts-payable agents now read that email, and agents with wallets have already been talked into sending money to attackers. A model can't be the thing that decides whether money moves.

How it works

A human signs a mandate once: per-payment and daily caps, known payees and the bank accounts on file. When the agent proposes an action, input is validated and a pure classify() function sorts it into a lane. Risk-reducing and in-mandate actions run; anything that adds risk pauses for an approval bound by hash to the amount, payee, account and expiry; hard invariants are refused. Only the executor holds payment credentials, and every decision goes into a hash-chained audit log.

Decisions worth explaining

The model proposes, code decides

classify() is pure, deterministic policy code with no model in the loop. A prompt injection can change what the agent asks for, but not which lane the request lands in.

Approve exact terms, not a request

An approval is sealed with SHA-256 to the amount, payee, account and expiry. If the agent re-computes $14,000 to $14,000.50 after approval, the hash no longer matches, the approval is void and Twokey asks again.

The agent never holds a key

The agent gets guard() and nothing else: no decide(), no full approval hash and no payment credentials. Approvals happen in a separate approver UI behind its own token, so the agent can't approve itself even over HTTP.

Results

  • TypeScript SDK (createTwokey with guard, decide, executeApproved) with memory and node:sqlite stores.
  • Fastify approval API with an HTML approval card; money kept in bigint minor units; strict input validation.
  • End-to-end demo: an accounts-payable agent handles a spoofed bank-change email, a voided approval and a refund, then prints a hash-chained audit log.

Stack

TypeScriptNode.jsFastifySQLiteSHA-256VitestDockerRailway

From the code

packages/core/src/classify.ts

function classifyPay(a: PayVendor, mandate: Mandate, state: PolicyState): Verdict {
const inv = moneyInvariant(a.amount, mandate);
if (inv) return inv;
const known = mandate.knownPayees[a.payeeId];
if (known && known.bankFingerprint !== a.bankFingerprint) {
return blocked('INV-FINGERPRINT', `bank details for ${a.payeeId} do not match the signed mandate`);
}
const checks: Check[] = [
{ name: 'payee_known', pass: known !== undefined },
{ name: 'per_payment_cap', pass: a.amount.amount <= mandate.perPaymentCap },
dailyCapCheck(a.amount.amount, mandate, state),
];
return checks.every((c) => c.pass)
? classified('WITHIN_MANDATE', 'R-WM-PAY', checks)
: classified('EXPAND', 'R-EX-PAY', checks);
}

Want something like this built?

Open to roles & contracts. Remote · IST with US/EU overlap.

Email me