An AI agent can prepare a crypto payment, but it cannot make one alone. A deterministic policy firewall decides whether the payment is even eligible, and a Ledger hardware wallet holds the only key that can sign it.
Agents propose. Policies filter. Ledger signs.
Live demo · Red-team results · Wallet CLI proof
A payment is the textbook irreversible action: once a transaction is broadcast, no later refusal can undo it. Yet the obvious way to build an "AI treasurer" gives the agent a hot key and trusts it to read invoices correctly. One poisoned email ("we changed our wallet this morning"), one hallucinated amount or one ambiguous request is then enough to drain funds.
The issue is not the agent's intelligence, it is its authority. This project removes the agent's authority instead of trying to make it smarter.
flowchart LR
D["Untrusted document<br/>(invoice, email)"] --> A["1. Agent extracts intent<br/>(parser or LLM, untrusted)"]
A --> F["2. Policy firewall<br/>10 deterministic checks + session state"]
F -->|any critical check fails| B["Blocked<br/>receipt with reasons"]
F -->|eligible| W["3. Wallet CLI dry-run<br/>preview, no broadcast"]
W --> L["4. Ledger device<br/>human confirms on screen"]
L --> R["Signed or rejected<br/>receipt"]
| Layer | What it stops | Trust assumption |
|---|---|---|
Extraction (src/lib/parser.ts) |
Nothing on its own: it is treated as compromised by design | None. Its output is re-checked |
Policy firewall (src/lib/policy.ts, src/lib/session.ts) |
Unknown or look-alike recipients, wrong network or asset, over-limit and split payments, replays, forged values, guessed fields | Deterministic code, no model in the loop |
| Ledger hardware gate | Anything that got past software: the key never leaves the device, and a human reads the final transaction on its screen | Secure element plus a human |
The agent is never given a key. Even a fully jailbroken agent can at most submit a request that the firewall refuses, or that a human then rejects on the device.
Every intent goes through ten checks. Any failed critical check makes it not eligible for signing, and the receipt lists every reason.
| Check | Rule |
|---|---|
| Intent fully stated | Amount, asset, network and recipient must all come from the source. Nothing is guessed |
| Positive amount | Finite and strictly positive (rejects negative, NaN, Infinity) |
| Network allowlist | Only networks in the policy |
| Asset allowlist | Only assets in the policy |
| Declared value matches | The firewall recomputes the USD value from its own price table; a mismatch with the agent's figure is treated as tampering |
| Single payment limit | Per-payment cap, on the recomputed value |
| Daily treasury budget | Remaining budget, updated after each approval in the session |
| Recipient allowlist | Exact address match (case-insensitive for EVM). Labels and risk tiers sent by the agent are ignored |
| Not a replay | An intent id approved once in the session is refused afterwards |
| Ledger confirmation | Always required for approved intents (informational) |
npm run redteam runs a corpus of 28 scenarios (25 attacks, 3 legitimate controls) through
the real pipeline: extraction, then a fresh firewall session per scenario. It writes the full
table to docs/RED-TEAM.md, and the same corpus runs in npm test.
| Category | Examples | Result |
|---|---|---|
| Prompt injection | "We changed our wallet", fake SYSTEM OVERRIDE, hidden HTML comment, CEO impersonation, new address listed before the old one |
5/5 blocked |
| Ambiguity / missing data | No asset, no network, no address, no payment at all | 4/4 blocked |
| Compromised model output | Jailbroken LLM returns the attacker address, an out-of-schema network, or omits the asset | 3/3 blocked |
| Value manipulation | $10 declared for 1,000 USDC, ETH priced like a stablecoin, negative amount | 3/3 blocked |
| Allowlist evasion | Look-alike address, trusted label on attacker address, zero-width padding, wrong network, wrong asset | 5/5 blocked |
| Malformed arguments | Shell injection in the recipient, NaN, Infinity |
3/3 blocked |
| Stateful bypass | Payment split under the limit, replay of an approved intent | 2/2 blocked |
| Controls | Legitimate invoice, ETH payment, lowercase allowlisted address | 3/3 allowed |
The red team found real holes. Run against the first version of the firewall, 11 of the 25 attacks got through: the firewall trusted the USD value declared by the agent (V1, V2), accepted negative and non-finite amounts (V3, M2, M3), let the extractor silently default a missing asset or network (A1, A2, L2, L3), and was stateless, so split payments and replays passed (S1, S2). Each one is fixed in this version, and the corpus keeps it fixed.
The operational console is live at https://ledger-agent-firewall.vercel.app, no install needed. Pick a request in the queue (approved vendor, poisoned address, over budget) to see the source document, the extracted intent, each policy check, the Ledger signing gate and the receipt. Nothing is signed or broadcast from the hosted page.
Tested with Node.js 22.
npm install
npm test # unit tests + the 28 red-team scenarios
npm run redteam # prints each scenario and regenerates docs/RED-TEAM.md
npm run demo # the three firewall scenarios in the terminal
npm run extract # raw document -> extracted intent -> firewall decision
npm run dev # the operational console (web app)To add an attack, append a case to src/data/attacks.ts with its
expected decision; npm test fails if the firewall disagrees.
Optional LLM extraction. npm run extract -- --llm uses a local Ollama
model (ollama run llama3.2, override with OLLAMA_MODEL). The web console can also call a cloud
model through api/extract.ts. Both fall back to the deterministic parser, and both outputs go
through the same firewall.
Adapting it. The policy is plain data (defaultPolicy in src/data/demo.ts):
recipients, limits, networks, assets and prices. createFirewallSession(policy) is the entry point
for any agent framework: call evaluate(intent) before handing anything to a signer.
Approved intents produce a Ledger Wallet CLI
send --dry-run command. The real device run is captured in
proof/wallet-cli-proof.md: account discovery, balances,
receive --verify (the address is confirmed on the Ledger screen) and a dry-run send.
Reproduce it with a Ledger on the Ethereum app:
npm i -g @ledgerhq/wallet-cli
npm run ledger:proofThe project never broadcasts on its own: signing always needs a physical confirmation on the device.
- Session state is in memory. Budget and replay protection last for one firewall session; a production deployment needs a persistent store shared by every agent instance.
- Budget is reserved at approval, not at signature: the firewall cannot see whether the Ledger confirmation is accepted. This fails closed (the budget can only be under-used).
- Duplicate invoices. The same invoice extracted twice, at different times, yields two distinct intents; both pass if they fit the budget. Content-based deduplication is not implemented.
- Fixed reference prices. Volatile assets use a static quote; a real treasury needs a trusted price source, which becomes part of the trust base.
- The allowlist is the root of trust. Whoever can edit the policy can redirect funds; policy changes should themselves require a Ledger signature.
- The human gate is only as good as the human. Ledger shows the real destination and amount, but a hurried user can still approve a payment the firewall allowed.
carbs (team project, XRPL Hackathon NYC) pushes the same idea
into the protocol: an agent signs with a short-lived session key whose spending cap the XRP Ledger
itself enforces, so an injected 500 XRP payment is refused on-chain with tefSESSION_LIMIT.
src/lib/policy.ts deterministic checks and receipts
src/lib/session.ts stateful firewall (budget reservation, replay protection)
src/lib/parser.ts intent extraction (deterministic parser, LLM paths)
src/lib/redteam.ts red-team runner and report
src/data/attacks.ts red-team corpus
src/data/demo.ts demo policy and scenarios
src/main.tsx operational console (React)
scripts/ demo, extraction, red team, Wallet CLI proof
test/ unit tests and red-team corpus
Not financial advice. The demo moves no funds.