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, PolicyVault converts it to an euint256 bound to msg.sender, and the comparison runs inside Inco's enclave. The contract holds handles, never balances.
  • Oasis ROFL holds the payer key. RoflKeyStore derives each ephemeral per-goal key through rofl-appd over a socket that exists only inside the container.
  • x402 v1 carries the payment — the exact scheme, EIP-3009 transferWithAuthorization, 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 allowThis on 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 — requestSpend at ~296,600 gas, attested reveal resolving in 7–12 seconds over 1–2 poll attempts.
  • 234 tests, including a tee-check script 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 TraceAnchor into 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

Share this project:

Updates

Submission history