Private Payments for Autonomous AI Agents on Stellar
A scoped session key the agent can't drain β settling through a ZK shielded pool that hides who it paid and how much.
Kage lets an AI agent pay in USDC on Stellar without holding your key and without leaking a thing. The agent spends under a scoped, revocable session key it can never drain or redirect, and every payment settles through a zero-knowledge shielded pool β so the amount, the recipient, and the agentβpayee link are all hidden on-chain.
- Autonomy without custody β a Soroban account contract delegates one agent session key bounded by policy in
__check_auth - Hides the recipient β Umbra-style stealth notes; each payee is paid at a fresh one-time address
- Hides the amount + link β a Tornado/Privacy-Pools-style ZK pool breaks the depositβwithdrawal trail
- Stops double-claims β per-note nullifier reverts any replay on-chain
- Trustless tree β every deposit carries a Groth16 insert proof; the contract verifies the new root, no custodian
On a transparent ledger, handing an agent a raw key publishes every counterparty, every amount, and a map of everything your treasury touches β and lets the agent (or an attacker) drain you. Kage fixes both: scope is enforced by the account contract, privacy by math and the chain.
Raw key on transparent chain: Agent β Wallet β Ledger (drainable + fully public)
With Kage: Agent β Scoped Session Key β ZK Pool β Ledger
(can't drain Β· can't redirect Β· who/how-much sealed)
| Hand an agent a raw key on a transparent chain⦠| Kage fixes it with⦠|
|---|---|
| The agent (or an attacker) can move all your funds | A scoped session key: only deposit, only USDC β pool, up to a cap, before an expiry |
| Every payment publishes counterparty + amount | Amounts and the agentβpayee link hidden in a ZK pool |
| Recurring transfers deanonymise everyone the agent pays | Each payee is paid at a fresh one-time stealth address |
| "Just encrypt it / trust our server" still trusts a custodian | Scope + unlinkability enforced by math and the chain, not a custodian |
- Remove the pool proof β each withdrawal must name the agent's deposit β the whole payment graph is public β no privacy.
- Remove the nullifier β a note is claimable twice β the pool drains.
- Remove the recipient binding β a relayer/observer front-runs a payee's withdrawal and redirects the funds.
| Contract | Address | Explorer |
|---|---|---|
| Kage Shielded Pool | CCQWGM2CBTFTY4B3OTKNTQO3GMBJUHWTJOSU7NC2QRDZ26KCSMJQGJXC |
β View |
| Scoped Session Account | CB3A5QRRIULWBBADWGYH6QA3XEJHJZJCJ7DV3CE6NBZFQBH5WWLKF636 |
β View |
| USDC (SAC) | CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC |
β View |
Network: Stellar Testnet
RPC URL: https://soroban-testnet.stellar.org
Explorer: https://stellar.expert/explorer/testnet
Asset: USDC (Soroban Asset Contract)
Live demo: https://kageai.me
# 1. Clone
git clone https://github.com/Venkat5599/stellar.git
cd stellar
# 2. Install (bun; Rust GNU toolchain on Windows, circom, snarkjs, stellar CLI)
bun install
# 3. Build circuits (reuses the Hermez pot14 ptau)
bun run circuit:withdraw && bun run circuit:insert
# (one-time) snarkjs groth16 setup + zkey contribute + export verificationkey for each
# 4. Build + deploy the Soroban contracts
cd contracts/solvency && stellar contract build && cd ../..
bun run convert # snarkjs vk/proof -> Soroban BN254 bytes
# 5. Provision a scoped agent session (autonomy without custody)
bun run agent:provision # deploy session account, delegate agent key, set policy + cap, fund it// The agent pays a payee β scoped, and ZK-private.
// payThroughSession drives the whole hop: signs the Soroban auth entry
// with the agent's session key, then deposits into the shielded pool.
import { payThroughSession } from './sdk/kage-onchain';
await payThroughSession({
scanKey: payeeScanKeyV, // payee's published meta-address (scan pubkey V)
amount: 10_000000n, // 10 USDC (7 decimals) β bound into the ZK commitment
});
// On-chain: only a commitment, a random ephemeral R, a new Merkle root.
// The chain never learns who was paid or how much is tied to them.// 1. Scan announcements: for each ephemeral R, recompute shared = vΒ·R and
// check if the derived commitment is in the tree. Match β it's yours.
// 2. Prove membership in zero knowledge + a fresh nullifier, bind a one-time
// stealth payout address, and withdraw β no link to the agent's deposit.
bun run flow // full off-chain derive -> tree -> recognise -> prove| Method | Description | Proof checked |
|---|---|---|
deposit(commitment, R, amount) |
Pull USDC, append commitment to the Merkle tree | Groth16 insert proof (old_rootβnew_root + amount binding), BN254 pairing |
withdraw(proof, root, nullifierHash, payout) |
Pay a stealth address from the pool | Groth16 membership proof + nullifier unused |
set_vks(...) |
Register the insert/withdraw verifying keys | Owner only |
Public-input layouts (contract mirrors circuits exactly):
- insert:
[old_root, new_root, commitment, leaf_index, amount] - withdraw:
[root, nullifier_hash, recipient, amount]
| Layer | Hides | How |
|---|---|---|
| Stealth notes (Umbra-style) | which payee the agent paid | Payee publishes a scan key V once. Agent does ECDH (shared = rΒ·V), derives note secrets from shared, announces only ephemeral R. Only V's holder recomputes shared = vΒ·R and finds their payment. |
| ZK shielded pool (Tornado/Privacy-Pools-style) | that two payouts share one agent, and the amount link | Each deposit inserts a Poseidon commitment into a Merkle tree. A withdrawal proves in ZK it owns some unspent leaf β without revealing which β plus a fresh nullifier (no double-claim). |
The chain only ever sees: commitments, random R values, a Merkle root, and
nullifier hashes. Never a payee's identity, an amount tied to a person, or a
link from the agent's deposit to a payee's withdrawal.
Stellar's host Poseidon2 constants don't match circomlib's Poseidon, so the
contract can't recompute the circuit's root on-chain. Instead, every deposit
carries a Groth16 "insert" proof that new_root correctly appends commitment
to the tree at the contract's current root. The contract checks
old_root == current, runs only the BN254 pairing check, and advances the root.
The insert proof also binds the deposited amount into the commitment, so
what is deposited is exactly what can be withdrawn β no accounting desync.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β OWNER (holds real key) β
β delegates ONE scoped session key to the agent β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SCOPED SESSION ACCOUNT (Soroban) β
β CB3A5QRRIULWBBADWGYH6QA3XEJHJZJCJ7DV3CE6NBZFQBH5WWLKF636 β
β β
β __check_auth policy β agent may ONLY: β
β βββ call deposit on the configured pool β
β βββ move USDC, into that pool only β
β βββ up to a spend cap β
β βββ before an expiry β
β anything else β BadPayout / CapExceeded / Expired / ContextNotAllowed β
ββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β agent signs the Soroban auth entry
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β KAGE SHIELDED POOL (Soroban) β
β CCQWGM2CBTFTY4B3OTKNTQO3GMBJUHWTJOSU7NC2QRDZ26KCSMJQGJXC β
β β
β deposit(C, R, amount) withdraw(proof, root, nullifier, pay) β
β βββ verify INSERT proof (BN254) βββ verify MEMBERSHIP proof (BN254) β
β βββ amount bound into commitment βββ nullifier unused? else revert #9 β
β βββ pull USDC via SAC βββ pay USDC β one-time STEALTH addr β
β βββ advance Merkle root β
β β
β CHAIN SEES: commitments Β· random R Β· Merkle root Β· nullifier hashes β
β NEVER: who paid whom Β· amount tied to identity Β· depositβwithdraw β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
stellar/
βββ circuits/
β βββ veil_withdraw.circom # membership + nullifier + amount range + recipient bind
β βββ veil_insert.circom # old_root -> new_root append proof + amount binding
βββ contracts/ # Soroban: veil (pool) + session (scoped account)
βββ sdk/
β βββ veil.ts # X25519 ECDH stealth notes, Poseidon Merkle tree
β βββ kage-onchain.ts # payThroughSession: scoped, ZK-private deposit
β βββ kage-convert.ts # snarkjs -> Soroban BN254 byte layout
βββ agent/ # MCP server + agent fabric (proxy tools, workflows)
βββ frontend/ # Next.js dashboard (kageai.me)
βββ scripts/ # provision session, flow, gen-insert, e2e
βββ deploy/ # Caddy, pm2 ecosystem, MCP config
| Step | Result | Detail |
|---|---|---|
| Deposit | β verified | On-chain insert proof verified (BN254) with amount binding; USDC pulled; commitment + ephemeral key announced. TX |
| Withdraw | β verified | Membership proof verified; payout paid to a stealth address bound into the proof (keccak(ScAddress) matched cross-language). TX |
| Double-spend | β rejected | Replaying the same nullifier reverts with NullifierUsed (#9). |
- Withdraw circuit: 3005 constraints β proves +
snarkjs verifyOK. - Insert circuit: 5238 constraints β proves +
snarkjs verifyOK (bindsamountinto the commitment). - Under-funded deposit fails to prove (amount β committed value β constraint violation).
- SDK β circuit: real X25519 note β SDK Merkle proof β withdraw proof verifies (Poseidon matches in and out of circuit).
| Resource | URL |
|---|---|
| Live Demo | kageai.me |
| Pool Contract | View on Explorer |
| Session Account | View on Explorer |
| Deposit TX | View TX |
| Withdraw TX | View TX |
| Testnet Faucet | Friendbot |
- Smart Contracts: Soroban (Rust) β shielded pool + scoped session account
- Zero-Knowledge: Circom + snarkjs, Groth16 over BN254 (alt_bn128), circomlib Poseidon
- Stealth crypto: X25519 ECDH one-time addresses (Umbra-style)
- Runtime / SDK: Bun, TypeScript,
@stellar/stellar-sdk,@noble/curves - Agent layer: Model Context Protocol (MCP) server + agent fabric
- Frontend: Next.js dashboard (kageai.me)
- Trusted setup: Hermez Perpetual Powers of Tau (pot14)
- Testnet only. No mainnet, no real funds.
- Stealth v1 = single-derived-key (no view/spend separation β documented stretch; ed25519 clamping blocks the classic dual-key scheme without custom signing).
- Demo tree depth 10 (1024 notes); identical circuit scales to depth 20.
- Fixed-denomination notes in the demo for a clean anonymity set (the circuit range-checks any amount < 2^64).
- Trusted setup reuses the real Hermez Perpetual Powers of Tau.
- The ZK and every transaction are real; only the parties are ours.
See KAGE.md for the full architecture deep-dive.