Inspiration
Give an AI agent a wallet and you are guarding a private key with a text filter — a probabilistic control protecting a deterministic, irreversible asset. A 99%-accurate filter fails one call in a hundred, and agents make thousands.
Every "safe agent payments" design we looked at was trying to make the agent trustworthy. We wanted to make the agent's trustworthiness irrelevant.
There was a second problem underneath it, and it is the one we find more interesting. An agent's on-chain wallet balance is public — which means it is also the agent's reservation price. Any vendor whose pricing is automated can read your ceiling and quote it back to you. Sealed-bid auctions exist precisely because revealing your maximum destroys your surplus. An agent walking into a marketplace with a visible budget is a bidder shouting their limit across the room, on every transaction, forever.
So the budget had to be enforced, unforgeable, and secret.
What it does
LedgerLighthouse is a working agentic payment system on Base Sepolia. An LLM agent buys API resources priced over the x402 protocol, and every spend is evaluated against a budget held as ciphertext on chain.
Each spend resolves a three-term conjunction — two public, one encrypted:
$$\text{ok} = (a \le \kappa) \wedge (c \ge 1) \wedge (\beta \ge a)$$
where \( a \) is the requested amount, \( \kappa \) the per-call cap, \( c \) the remaining call count, and \( \beta \) an encrypted handle the contract computes over but never reads.
The demo offers four resources. Two settle, two are refused — and the two refusals fail for different reasons, which is the entire point of having four rather than two:
| Resource | Price | Outcome |
|---|---|---|
| Market data snapshot | 0.01 | settles |
| Bulk history archive | 0.12 | settles — 12× dearer, still inside the budget |
| Compliance audit bundle | 0.35 | refused — no injection, ordinary description, far under the public cap |
| Premium feed | 5.00 | refused — with a prompt injection attached |
The compliance-audit case is the one that carries the argument. Its payee is allowlisted, its description is unremarkable prose, and its price clears every public precondition. Nothing public can refuse it. When it bounces, the bounce came from the encrypted budget and nowhere else.
The premium-feed case is the one that is fun to watch. The vendor embeds a prompt injection claiming the spending policy was "already completed off-chain by the account administrator." Claude Opus 5 reads it and complies — the console shows it complying, in the model's own words. It changes nothing. The agent holds a gas-only relay key; the component that decides never reads the vendor's text.
How we built it
The architecture is one sentence: the component that reads the attacker's text has no spending authority, and the component that grants spending authority never reads the attacker's text.
- Inco Lightning enforces the budget. The browser encrypts it over HPKE,
PolicyVaultconverts it to aneuint256bound tomsg.sender, and the comparison runs inside Inco's enclave. The contract holds handles, never balances. - Oasis ROFL holds the payer key.
RoflKeyStorederives each ephemeral per-goal key throughrofl-appdover a socket that exists only inside the container. - x402 v1 carries the payment — the
exactscheme, EIP-3009transferWithAuthorization, USDC on Base Sepolia, through a facilitator we self-host. - The Authorization Signer is deliberately dumb. It asks one question — is (goalId, seq) finalized-approved on chain? — and if yes, signs exactly what the chain froze. There is no route that accepts an amount, a payee, or a token, because there is no code path that would know what to do with one.
The debit is applied before the decision is knowable, in the same transaction that records the terms. Inco makes this the only expressible ordering, since you cannot branch on an encrypted condition — the guarantee comes from the platform, not from developer discipline.
services/orchestrator must never import services/signer. A CI check enforces it and fails the
build if violated. That check is the load-bearing wall of the whole design.
Challenges we ran into
- Stack-too-deep in Solidity. The encrypted branch needs enough stack slots that holding the authorization tuple alongside it overflows. We ended up writing the record field-by-field straight into storage rather than assembling it in locals.
- Selecting the operand, not the result. Our first conditional debit computed an underflowed
value on every rejected request and relied on nobody reading it. The fix is
select(ok, amount, 0)before the subtraction, so it is always well-defined. - Forgetting
allowThison a persisted handle permanently orphans the budget — the vault can never compute over it again. We now have cheatcode tests specifically for that failure. - Attestation binding. Checking the covalidator signatures is not enough: a genuine attestation for a different handle would otherwise be substitutable. Verification binds to the handle the contract stored. We prove this by re-submitting a real attestation with the plaintext flipped and asserting on-chain verification refuses it.
- "Decision unavailable" is not "rejected." If the compute server stalls after the debit has committed, the money has moved and the outcome is unknown. Merging that into "rejected" would be a lie about where the money went, so it is a distinct, reported outcome.
- Dependency resolution. Inco's Solidity dependencies arrive over git rather than npm, which pnpm's supply-chain protections flag as exotic sub-dependencies. Pinning them to the exact upstream commits Inco itself requests took longer than any contract we wrote.
Accomplishments we're proud of
- Live on Base Sepolia with measured costs —
requestSpendat ~296,600 gas, attested reveal resolving in 7–12 seconds over 1–2 poll attempts. - 234 tests, including a
tee-checkscript that exercises the confidential path in complete isolation: no USDC moves, the signer never starts, no vendor is contacted. - Every run produces a hash-chained trace with a Merkle root, and the standalone verifier needs nothing but the file and a public RPC.
What we learned
Precision about which component guarantees what is worth more than an extra feature. Inco Lightning is TEE-based, not FHE. It attests that a handle decrypts to a value — not that our orchestrator behaved, not that our code is correct, and it exposes no remote-attestation quote to applications. So the claim we make is narrow and defensible:
Every payment in this trace corresponds to a confidential policy evaluation whose result was attested and verified on chain against the expected handle.
Not "the agent behaved correctly." We would rather defend the smaller claim.
We also learned to state our own limits out loud. Two worth naming here: the ROFL code path is implemented and tested with 12 dedicated tests, but the container is not yet deployed, so the running demo uses a local file store. And Base cannot verify Oasis attestations — custody is real, but provability of custody from Base is not. Publishing the binding to Sapphire closes that, and it is the first item on our roadmap rather than something we hoped nobody would ask about.
What's next
- Deploy the ROFL enclave and publish the payer binding on Sapphire, turning "trust us, the key is in an enclave" into something a third party can verify.
- Wire
TraceAnchorinto the runtime — the contract is written and tested but not yet called, so anchoring the Merkle root upgrades traces from verifiable to publicly committed. - Encrypt the whole policy, not just the budget. The per-call cap and call counter are currently public plaintext, so a vendor can read the ceiling and price just underneath it.
- Delegated sub-budgets for sub-agents — a goal spawning child goals with encrypted sub-budgets, letting an orchestrator delegate spending authority it does not itself hold.
- An escrow-based x402 scheme that removes the signer entirely, eliminating the last trusted-code assumption in the design.
Built With
- eip-3009
- inco
- inco-lightening
- metamask
- oasis-rofl
- pnpm
- react
- solidity
- typescript
- vite
- x402
Log in or sign up for Devpost to join the conversation.