The agent SDK and the MCP server are served from this site.Read the docs
SEC-DOCS

Docs

Dark mode

Dark mode funds an agent without anything onchain linking your wallet to it. You deposit a fixed amount of USDC into a shielded pool and keep a note. Later, the note pays a fresh stealth wallet made in your browser, through a withdraw that a zero-knowledge proof authorises and the keeper relays. The stealth wallet then gives an arm the usual way. Onchain, the withdraw shows that some note of the pool was spent, not which deposit it came from.

How it works

deposit    your wallet ── 1, 10 or 100 USDC ─▶ pool vault (a PDA of the program)
           note = (nullifier, secret), two random numbers made in your browser, shown once
           commitment = Poseidon(nullifier, secret) is added to the pool's Merkle tree (depth 20)

fund       your browser: new stealth key, then a Groth16 proof that it knows a note in the tree
           public inputs: root, Poseidon(nullifier), stealth wallet, relayer, relayer fee
           keeper ── one transaction, fee payer = keeper ─▶ Solana:
             create the stealth wallet's USDC account, send it 0.00278952 SOL,
             withdraw: proof checked onchain (alt_bn128), nullifier recorded, USDC paid out
           stealth wallet ── gives an arm the usual way (its own key, its own SOL) ─▶ agent

after      Ink returns the USDC to the stealth wallet; "deposit back" turns it into new notes

The stealth wallet's SOL comes from the keeper inside the relayed withdraw, never from your wallet: one arm's rent, the wallet's own rent-exempt minimum (a Solana account that holds SOL must keep it) and 0.0001 SOL for its own network fees (giving the arm, Ink, depositing back). The note never leaves your browser; the keeper receives the proof and its public inputs only.

What it hides

  • The link between the wallet that deposited and the stealth wallet, its arms, its session keys and its agents.
  • Which deposit a withdraw spends: every unspent note of the same pool fits the proof equally.

What it does not hide

  • Deposits are public: anyone can see that your wallet put 1, 10 or 100 USDC into a pool, and when.
  • The agents' payments to APIs stay public: arm, payee, amount and memo of every payment are Solana transactions.
  • Privacy grows with the number of deposits in each pool. In a pool with few deposits, or right after your own deposit, timing alone can point to you. Wait for other deposits, and do not withdraw the same amounts you deposited at the same moments.
  • The keeper sees the IP address of every request it relays, as the site sees the IP address of every page and API call.
  • What you do later can link you again: sending the stealth wallet's USDC to your wallet, or giving the same agent arms from both.
  • Stealth keys are kept in this browser's local storage; anyone with access to the browser profile can read them.

Fees

DepositNo fee. Your wallet pays the network fee.
Dark fee0.5% of the withdrawn amount in USDC, paid by the program to the relay fee account 7ar53VLy3cBgywMrP6p5bvn7DMryQ8jEGCdv8Sxrzsf2. It joins the week's relay fees for the Monday buy and burn of $ALLOW. No holder rate in dark mode: checking a balance would link the wallets.
Relayer feeIn USDC, taken from the withdrawn amount, to the keeper. It covers the keeper's SOL costs for one withdraw: the stealth wallet's USDC account rent (0.00148844 SOL), the nullifier record's rent (0.00069088 SOL), the SOL sent to the stealth wallet (0.00278952 SOL: an arm's 0.00203928, its own rent-exempt minimum of 0.00065024 and 0.0001 for fees) and the withdraw's own network fee (0.000013 SOL), 0.00498184 SOL in all at today's rent, priced at Jupiter's SOL price plus 10% and rounded up to 0.001 USDC. At 100 USD per SOL that is 0.549 USDC. The app shows the exact figure before you fund.

The relayer fee does not depend on the pool, so the 1 USDC pool keeps much less of a note than the 10 and 100 USDC pools. Of the rents, the arm's comes back to the stealth wallet when the arm closes; the USDC account's and the wallet's own stay with the stealth wallet.

The program

Program4cm1XnB9xinbqAbRyg97pmySvrK856QGUpGrbAYicC6S (Anchor, source in the repository under dark/program)
Statusdeployed on Solana (read from the chain now)
Upgrade authority2dYJ4HeHejHhp6XPpvJnmyvZRCNjW4pU9KQsbDuU4Rw9
Pools1 USDC 67zWhLLU5TaWy6vBRF5bZVBfAbTTqCzTjFq6c9mGvRNU
10 USDC 8yXjHpa4veozXqSK1FCSNuhc28uC44LiDS6gMunEdv5n
100 USDC 8L1wjjqnwkYWcL4U39VuGs6pergspy4vHCEEE16C6zRR
VaultsUSDC accounts owned by their pool's PDA, for example 2b9BrRtbYGdPDBUS95YtBhVjMgcWpQLNoBu6Y6F4BQSn for 1 USDC
  • No instruction moves pool USDC without a valid proof of an unspent note. There is no admin instruction, no pause and no fee setting: the denominations, the fee account and the 0.5% are constants of the program.
  • The upgrade authority is a key the team holds offline; it is never on the server. It can replace the program's code, and new code could move pool funds differently. Until it is revoked, using dark mode means trusting that key.
  • A note can be spent once: the withdraw creates a record at a PDA of (pool, Poseidon(nullifier)), and a second one fails.
  • The proof binds the stealth wallet, the relayer and the relayer fee: a relayer that changes any of them makes the proof fail.
  • The pool keeps its last 30 roots, so a proof made a few deposits ago still verifies.
  • A withdraw pays the dark fee into the relay fee account, so it needs that account to exist; it is a USDC account of the keeper, which could close it only empty. USDC itself can be frozen by its issuer, in the vaults as anywhere else.

Instructions and accounts

initialize(denomination)   once per pool, anyone may call it; denomination must be 1, 10 or 100 USDC
deposit(commitment)        moves exactly the denomination into the vault, inserts the commitment
withdraw(a, b, c, root, nullifierHash, fee)
                           relayer signs; verifies the Groth16 proof with the alt_bn128 syscalls;
                           pays denomination - 0.5% - fee to the recipient's USDC account,
                           fee to the relayer, 0.5% to the relay fee account

pool       PDA ["pool", denomination as u64 little-endian]
vault      PDA ["vault", pool]                  USDC account, authority = pool
nullifier  PDA ["nullifier", pool, nullifierHash]
recipient  sha256(address) with its first byte zeroed, as a circuit input (same for the relayer)

The Merkle tree uses Poseidon with circomlib's parameters on BN254, the same as the Solana Poseidon syscall. Both syscalls (Poseidon and alt_bn128) were checked active on Solana before the program relied on them. The verifying key is compiled into the program.

The circuit and its trusted setup

One circuit, withdraw.circom: 5,316 constraints, depth 20, public inputs root, nullifierHash, recipient, relayer and fee. Phase 1 is the PSE perpetual powers of tau (ppot_0080_13.ptau, 80 public contributions), downloaded from its public address. Phase 2 has two contributions made with fresh randomness that was never written anywhere, then a public beacon: the finalized Solana blockhash HGYZGNf1frQiq3QkmTRrP1wQTj53rWvJeRbdnX7ici6B (slot 464530461), applied with 2^10 iterations. The final key was verified against the circuit and phase 1. Both phase 2 contributions were made by the team, so the phase 2 secret is as safe as the team's word that it discarded it; the transcript is in the repository under dark/ceremony.

Phase 1 fileppot_0080_13.ptau sha256 ccee28086e4b81d81a6e16fdee054d1dbd5276362e2662d4205d31de45cb930f
Circuit hashf7b991fdb2f7cbb086acd7b87fa9e2f060afa342ad0fbc9f77023b34c3cf568d2a38644eeb2574f690677044920c75fa1ff9761e499a308f543b1893a4c78911
Contribution 1allowance-dark contribution 1: 82003774ceb8a02fac17dc3f8243636fde553db49796dd6d6a46c626fe26d0d40d74062d82ba6f52bb7ffdef51121778a54b103be7900df41f083b552218ff2c
Contribution 2allowance-dark contribution 2: d8873e4664b43394311777a4d5765fbce196fd1ab93485b063f9bdccde6c5b23402fd394e8a2df7cd0336a128b10407573c8fc5ac0ab38c826d188d89c8b1907
Contribution 3Solana blockhash HGYZGNf1frQiq3QkmTRrP1wQTj53rWvJeRbdnX7ici6B: a7d065e1f3ca1a9d49f16cd87c73261f5974a9eca7d3b19e43fda3705f8916498332bbf9e9d70d8e044ab37ff891f947d2faf8c722802b543aae56379ad70903
Proving key/zk/withdraw_final.zkey sha256 282c4232f2f77c06751b63c384de5666fb258a0e42fe9bbc0ded652c6aeb6dba
Circuit wasm/zk/withdraw.wasm sha256 bf924e2302d661fb8f1fe5e4d25e86c5e598f0133d33397704665303ff575f05
Verifying key/zk/verification_key.json sha256 da83676cba2ffd3f769a5c83077d7d8118e167d8fccbc67a0d4167a053e35d3b
npx snarkjs zkey verify withdraw.r1cs ppot_0080_13.ptau withdraw_final.zkey   # lists the contributions above

API

GET  https://www.allowanceengine.com/api/dark/status         program, upgrade authority, the three pools (deposits, USDC held)
GET  https://www.allowanceengine.com/api/dark/tree?d=10      a pool's commitments in tree order and its recent roots
GET  https://www.allowanceengine.com/api/dark/quote?d=10     relayer, relayer fee, dark fee, what the stealth wallet receives
POST https://www.allowanceengine.com/api/dark/tx             { owner, notes: [{ denomination, commitment }] }  unsigned deposit
POST https://www.allowanceengine.com/api/dark/relay          { denomination, proof: { a, b, c }, root, nullifierHash, recipient, fee }

The tree endpoint is a convenience: the browser rebuilds the tree, checks that its root is one of the pool's onchain roots, and only then proves. A wrong list cannot make a wrong withdraw pass, only fail.

Tests

The program's .so runs in a local Solana VM with real proofs from the ceremony's files: deposit, a relayed withdraw, a second spend of the same note (refused), a root the pool never had and a known root that is not the proven one (refused), a changed recipient, a fee raised by one unit, another relayer (all refused), a note of the 1 USDC pool presented to the 10 USDC pool (refused), a pool of another amount (refused), a fee that leaves nothing (refused), roots older than the last 30 (refused), a commitment outside the field (refused), and deposits back from a stealth wallet.