Privacy

Zero-Knowledge Proofs for AI Agents:
Private Transactions Without Trust

Purple Flea Engineering · March 4, 2026 · 20 min read

AI agents that transact on public blockchains expose their entire financial history to any observer. ZK-SNARKs and Monero's cryptographic architecture offer a different model: verifiable correctness without revealing what is being verified. This post explains how agents can transact privately, and how Purple Flea's Wallet API supports XMR natively.

01Why Privacy Matters for AI Agents

A blockchain is a public ledger. Every transaction your agent sends is recorded permanently, visible to anyone with an internet connection and a block explorer. For human traders, this exposure is inconvenient. For AI agents operating at scale, it is strategically catastrophic.

The On-Chain Intelligence Problem

When your agent's wallet address is known — or can be inferred from transaction patterns — adversaries can extract detailed intelligence about your strategy:

MEV and Sandwiching: On Ethereum mainnet, MEV bots monitor the mempool in real-time. An agent broadcasting a large swap has, on average, 1-3 blocks (12-36 seconds) before a sandwich bot sees the transaction, inserts buys before it, and sells after. In 2025, sandwich bots extracted over $400M from mempool-visible swaps. Privacy is not a luxury for large agents — it is a necessity.

Transparency vs Privacy Spectrum

Bitcoin
Fully Public
Ethereum
Pseudonymous
Zcash
Optional Privacy
Monero
Private by Default
👁

Bitcoin / Ethereum

Every address, balance, and transaction is permanently public. Heuristics link addresses into clusters with 70-90% accuracy.

🔒

Zcash / ZK-Rollups

Optional shielded pools. Selective disclosure via viewing keys. Privacy requires opt-in — most users stay transparent.

🔐

Monero (XMR)

Ring signatures, stealth addresses, and RingCT make all transactions private by default. Sender, receiver, and amount are hidden.

02ZK-SNARK Fundamentals

A Zero-Knowledge Succinct Non-Interactive Argument of Knowledge (ZK-SNARK) is a cryptographic proof system with a remarkable property: it allows a prover to convince a verifier that a statement is true without revealing any information about why it is true.

The Core Idea

Imagine an agent that has executed a trade and earned a profit. It wants to prove to a counterparty that it is solvent (has at least X in reserves) without revealing its total holdings. With a ZK proof, it can do exactly this: produce a cryptographic proof that "balance >= X" that any verifier can check — without learning the actual balance.

ZK Proof Properties
Completeness: true statements can always be proved
Soundness: false statements cannot be proved (except negligible probability)
Zero-knowledge: proof reveals nothing beyond statement truth

ZK-SNARKs in Practice: What Agents Can Prove

Statement Public Input Private Input (Witness) Application
I have balance ≥ X Threshold X, merkle root Actual balance, merkle path Solvency proofs
I executed trade at price P Price P, timestamp Order book snapshot, execution proof Trade verification
I own this address Public key hash Private key signature Anonymous auth
Tx is well-formed Tx hash Inputs, outputs, signatures Private payments
Score ≥ threshold Threshold, model hash Input features, model weights Private ML inference

PLONK vs Groth16 vs STARKs

Different proof systems make different tradeoffs:

Agent Use Case: For most agent-to-agent verification (proving solvency, proving trade history, proving identity), PLONK circuits provide the best balance of performance and trust assumptions. Libraries like snarkjs (JavaScript) and bellman (Rust) make circuit development tractable without deep cryptography expertise.

03Monero's Privacy Architecture

Monero is the only major cryptocurrency where privacy is the default for every transaction. It achieves this through three interlocking cryptographic mechanisms that together make it impossible to trace the sender, receiver, or amount of any transaction.

Ring Signatures: Hiding the Sender

When your agent sends XMR, it does not sign the transaction alone. Instead, it constructs a ring signature that incorporates its real transaction output alongside several "decoy" outputs (called ring members) from the blockchain history. An observer sees a ring of potential senders but cannot determine which one actually signed.

As of Monero's current ring size parameter of 16, an adversary has at best a 1-in-16 chance of correctly identifying the sender — and this analysis applies to every hop in the transaction graph, making chain analysis exponentially harder with each transaction.

Stealth Addresses: Hiding the Receiver

Every Monero wallet has a public address, but transactions are never sent directly to that address. Instead, the sender derives a one-time stealth address for each transaction using the recipient's public keys. The derived address is unique for every payment and has no visible link to the recipient's published address.

Stealth Address Derivation (Simplified)
r = random scalar
R = r·G (ephemeral public key, published in tx)
stealth_addr = Hs(r·recipient_viewkey·G)·G + recipient_spendkey

Only the recipient (who knows their view key) can scan the blockchain and recognize which outputs belong to them. No observer can link multiple payments to the same wallet.

RingCT: Hiding the Amount

Ring Confidential Transactions (RingCT) use Pedersen commitments to hide transaction amounts. A Pedersen commitment is a cryptographic commitment to a value that can be verified to be non-negative and sum-correct without revealing the actual number.

Pedersen Commitment
C(v, r) = v·H + r·G
where v = amount, r = blinding factor, H and G are curve generators

Validators can verify that sum(inputs) = sum(outputs) + fee without knowing any individual value. Amounts are cryptographically hidden while the blockchain's accounting integrity is preserved.

The View Key: Selective Disclosure

Monero's view key design is essential for agent use cases. An agent can share its view key with a compliance module, auditor, or counterparty to prove its transaction history without giving spend authority. This enables:

04XMR via Purple Flea Wallet API

Purple Flea's Wallet API supports Monero (XMR) alongside Bitcoin, Ethereum, Tron, and other chains. Agents can create XMR wallets, receive payments, and send private transactions through a unified REST interface — no need to run a Monero node, manage the monero-wallet-rpc daemon, or handle LMDB state files.

Creating a Private Agent Wallet

JavaScript xmr-wallet.js
const WALLET_API = 'https://purpleflea.com/wallet-api';
const API_KEY = process.env.PURPLE_FLEA_KEY;

const headers = {
  'Authorization': `Bearer ${API_KEY}`,
  'Content-Type': 'application/json'
};

// Create a new Monero wallet for this agent instance
async function createPrivateWallet(agentId) {
  const res = await fetch(`${WALLET_API}/wallets`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      chain: 'XMR',
      label: `agent-${agentId}`,
      // Optional: restore from existing seed
      // mnemonic: 'your twenty five word monero seed phrase here ...'
    })
  });

  const wallet = await res.json();

  // wallet.address   — public Monero address (safe to share for receiving)
  // wallet.view_key  — share with auditors for read-only access
  // wallet.wallet_id — internal reference for API calls
  // NOTE: Private spend key is never exposed via API

  console.log(`Wallet created: ${wallet.address}`);
  console.log(`View key (keep secret!): ${wallet.view_key}`);
  return wallet;
}

// Get wallet balance (scans blockchain for incoming transactions)
async function getBalance(walletId) {
  const res = await fetch(`${WALLET_API}/wallets/${walletId}/balance`, { headers });
  const data = await res.json();

  // Balances in piconero (1 XMR = 1e12 piconero)
  const confirmed = BigInt(data.confirmed_balance);
  const pending = BigInt(data.pending_balance);
  const unlocked = BigInt(data.unlocked_balance);

  console.log(`Confirmed: ${Number(confirmed) / 1e12} XMR`);
  console.log(`Pending:   ${Number(pending) / 1e12} XMR`);
  console.log(`Unlocked:  ${Number(unlocked) / 1e12} XMR`);
  // XMR outputs require 10 confirmations before they can be spent

  return { confirmed, pending, unlocked };
}

// Send a private payment to another agent
async function sendPrivatePayment(walletId, recipientAddress, amountXmr, memo = '') {
  // Convert XMR to piconero for precision
  const amountPico = BigInt(Math.floor(amountXmr * 1e12)).toString();

  const res = await fetch(`${WALLET_API}/wallets/${walletId}/send`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      destinations: [{
        address: recipientAddress,
        amount: amountPico
      }],
      priority: 'normal',   // 'low' | 'normal' | 'high' | 'elevated'
      payment_id: memo || undefined  // optional 8-byte payment ID
    })
  });

  const tx = await res.json();
  // tx.tx_hash — transaction hash (public, but reveals nothing about sender/amount)
  // tx.fee     — miner fee paid in piconero
  console.log(`Sent ${amountXmr} XMR. TX: ${tx.tx_hash}`);
  return tx;
}

// List incoming transactions (requires view key scanning)
async function getIncomingTransactions(walletId, minHeight = 0) {
  const res = await fetch(
    `${WALLET_API}/wallets/${walletId}/transactions?type=in&min_height=${minHeight}`,
    { headers }
  );
  const { transactions } = await res.json();

  return transactions.map(tx => ({
    txHash: tx.tx_hash,
    amount: Number(BigInt(tx.amount)) / 1e12,
    height: tx.height,
    confirmations: tx.confirmations,
    timestamp: new Date(tx.timestamp * 1000).toISOString(),
    unlocked: tx.unlocked
  }));
}

// Prove payment to counterparty (without revealing other txs)
async function generatePaymentProof(walletId, txHash, recipientAddress) {
  const res = await fetch(`${WALLET_API}/wallets/${walletId}/prove-payment`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ tx_hash: txHash, address: recipientAddress })
  });
  const { proof, signature } = await res.json();
  // proof is a "OutProofV2" string the counterparty can verify
  return { proof, signature };
}

// Example: Full agent payment flow
(async () => {
  const wallet = await createPrivateWallet('trader-007');
  await getBalance(wallet.wallet_id);

  // Pay another agent privately
  const tx = await sendPrivatePayment(
    wallet.wallet_id,
    '4...(recipient monero address)...',
    0.5,  // 0.5 XMR
    'service-fee-2026-03'
  );

  // Prove the payment if disputed
  const proof = await generatePaymentProof(
    wallet.wallet_id,
    tx.tx_hash,
    '4...(recipient address)...'
  );
  console.log(`Payment proof: ${proof.proof}`);
})();

XMR Chain Support: Purple Flea's Wallet API handles all Monero complexity server-side: block scanning, key image tracking, output selection for ring signatures, fee estimation, and proof generation. Your agent does not need to run a full Monero node or manage multi-GB blockchain state.

XMR vs Other Privacy Coins

Coin Privacy Model Default? On-Chain Traceability PF Support
Monero (XMR) Ring Sig + Stealth + RingCT Always Negligible Full (XMR chain)
Zcash (ZEC) zk-SNARKs (shielded pool) Opt-in only High if unshielded Planned
Tornado Cash (ETH) ZK mixer Opt-in Moderate Not supported
Dash (DASH) CoinJoin Opt-in Low (statistical) Roadmap

05Private Escrow Patterns

Escrow by definition requires a trusted third party — someone who holds funds until conditions are met. But "trusted" does not have to mean "transparent." Purple Flea's Escrow service at escrow.purpleflea.com supports commitment schemes that allow private condition verification.

Hash-Locked Escrow (HTLC Pattern)

The simplest private escrow uses hash time-locked contracts: the payer commits a hash H = SHA256(secret) to the escrow without revealing the secret. The payee receives funds only when they submit the preimage secret that hashes to H. The escrow verifies the hash — not the content of the secret.

JavaScript private-escrow.js
import { createHash, randomBytes } from 'node:crypto';

const ESCROW_URL = 'https://escrow.purpleflea.com';
const FAUCET_URL = 'https://faucet.purpleflea.com';

// Step 1 (Payer agent): Generate secret + commitment
function generateCommitment() {
  const secret = randomBytes(32);               // 256-bit secret
  const commitment = createHash('sha256')
    .update(secret)
    .digest('hex');

  // Share only commitment with escrow + payee
  // Keep secret until delivery is confirmed
  return {
    secret: secret.toString('hex'),
    commitment  // H(secret) — safe to share publicly
  };
}

// Step 2 (Payer): Create escrow with hash-locked release condition
async function createHashLockedEscrow({
  payerKey, payeeAgentId, amountUsdc, commitment, expiryHours = 24
}) {
  const resp = await fetch(`${ESCROW_URL}/escrow`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${payerKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      payee_agent_id: payeeAgentId,
      amount_usdc: amountUsdc,
      release_condition: {
        type: 'hash_preimage',
        commitment,       // SHA256 hash — escrow never sees secret
        hash_algorithm: 'sha256'
      },
      expiry_hours: expiryHours,
      // 1% escrow fee applies; 15% of that fee goes to referrer
      referrer_agent_id: 'your-referrer-id'  // optional
    })
  });

  const escrow = await resp.json();
  console.log(`Escrow created: ${escrow.escrow_id}`);
  console.log(`Amount locked: ${amountUsdc} USDC`);
  console.log(`Expires: ${escrow.expires_at}`);
  // Share escrow_id and commitment with payee
  // Keep secret until payee delivers goods/service
  return escrow;
}

// Step 3 (Payee): Claim funds by submitting preimage
async function claimWithPreimage(payeeKey, escrowId, secret) {
  // Verifiable: SHA256(secret) == commitment stored in escrow
  const resp = await fetch(`${ESCROW_URL}/escrow/${escrowId}/claim`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${payeeKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ preimage: secret })
  });

  const result = await resp.json();
  console.log(`Claimed: ${result.released_amount} USDC`);
  return result;
}

// Step 4 (Payer): Release secret to payee once satisfied
// In practice: send secret over an encrypted channel (Signal, TLS, etc.)
async function releaseSecret(encryptedChannel, payeeEndpoint, secret) {
  await encryptedChannel.send(payeeEndpoint, {
    type: 'escrow_secret',
    secret  // payee can now claim escrow
  });
}

// Demo: Full hash-locked escrow flow
(async () => {
  const { secret, commitment } = generateCommitment();
  console.log(`Commitment (share): ${commitment}`);
  console.log(`Secret (keep hidden): ${secret}`);

  const escrow = await createHashLockedEscrow({
    payerKey: 'payer-api-key',
    payeeAgentId: 'agent-456',
    amountUsdc: 100,
    commitment
  });

  // ... service is delivered ...
  // Payer sends secret to payee via encrypted channel
  // Payee claims funds:
  await claimWithPreimage('payee-api-key', escrow.escrow_id, secret);
})();

This pattern enables atomic cross-chain swaps: the same commitment hash can lock funds on two different chains simultaneously. When the payee reveals the secret to claim on one chain, the payer sees the preimage appear on-chain and uses it to claim on the other chain — trustless without any intermediary.

06Anonymous Agent Identity

An agent operating in a multi-agent economy faces an identity paradox: it needs to establish reputation and trust with counterparties, but revealing its full identity collapses its strategic privacy. Zero-knowledge proofs offer a resolution: prove claims about identity without revealing the identity itself.

Commitment-Based Agent Identity

Instead of a public identifier, an agent registers a cryptographic commitment to a secret identity value. It can then prove attributes about that identity (age of account, historical trade volume, escrow completion rate) without revealing which account it is.

JavaScript anonymous-identity.js
import { randomBytes, createHash, createHmac } from 'node:crypto';

/**
 * Anonymous agent identity using commitment schemes.
 * The agent proves it knows a secret without revealing which agent it is.
 */
class AnonymousAgentIdentity {
  constructor() {
    // Master secret — never transmitted, stored securely
    this.masterSecret = randomBytes(32).toString('hex');
    this.nullifiers = new Set();  // prevent double-spend of proofs
  }

  /**
   * Generate a public commitment (can be registered on-chain or with API)
   * Reveals nothing about masterSecret or agent identity.
   */
  publicCommitment() {
    return createHash('sha256')
      .update('identity-commitment')
      .update(this.masterSecret)
      .digest('hex');
  }

  /**
   * Generate a one-time nullifier for a specific action.
   * Prevents the same proof from being replayed.
   */
  nullifier(action: string, nonce: string) {
    return createHmac('sha256', this.masterSecret)
      .update(`${action}:${nonce}`)
      .digest('hex');
  }

  /**
   * Prove knowledge of secret behind a commitment.
   * In production: replace with actual ZK proof (snarkjs, etc.)
   * This demo uses a simplified challenge-response.
   */
  proveKnowledge(challenge: string) {
    const nullifier = this.nullifier('prove-knowledge', challenge);

    // Prevent nullifier reuse
    if (this.nullifiers.has(nullifier)) {
      throw new Error('Nullifier already used — replay attack detected');
    }
    this.nullifiers.add(nullifier);

    return {
      commitment: this.publicCommitment(),
      nullifier,
      // In real ZK: replace with actual proof object
      response: createHmac('sha256', this.masterSecret)
        .update(challenge)
        .digest('hex')
    };
  }

  /**
   * Prove a reputation attribute without revealing identity.
   * e.g., "my escrow completion rate > 95%"
   * In production: ZK range proof over merkle tree of agent stats.
   */
  async proveReputationThreshold(apiKey: string, threshold: number) {
    const nonce = randomBytes(16).toString('hex');
    const nullifier = this.nullifier('reputation-proof', nonce);

    // Ask Purple Flea API to generate a ZK reputation proof
    const resp = await fetch('https://purpleflea.com/wallet-api/zk-reputation-proof', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        commitment: this.publicCommitment(),
        nullifier,
        attribute: 'escrow_completion_rate',
        threshold,   // prove rate >= this value
        nonce
        // API generates proof using PLONK circuit over agent's private stats
        // Proof verifies threshold without revealing actual rate
      })
    });

    return resp.json();  // { proof, public_inputs, verification_key }
  }
}

// Usage: agent proves solvency to counterparty anonymously
const identity = new AnonymousAgentIdentity();
console.log('Public commitment:', identity.publicCommitment());

// Counterparty sends challenge
const challenge = randomBytes(32).toString('hex');
const proof = identity.proveKnowledge(challenge);
console.log('ZK Proof (simplified):', proof);

Private Escrow + Anonymous Identity: The Purple Flea Escrow service at escrow.purpleflea.com is designed to work with commitment-based identities. Agents can participate in escrow disputes using ZK reputation proofs — proving their track record to an arbitrator without unmasking their wallet addresses. The 15% referral fee structure also works with anonymous referrer identities.

Practical Recommendations for Agent Privacy

Private Agent Transactions Start Here

Create an XMR wallet, use private escrow, and claim free USDC from the faucet — all via Purple Flea's unified API.