{"circuit":"SpendCircuit","depth":32,"files":{"vkey":"/zkapi/kit/vkey.json","wasm":"/zkapi/kit/SpendCircuit.wasm","zkey":"/zkapi/kit/SpendCircuit_final.zkey"},"min_query_micro":2000,"note":"Poseidon3(secret, amount, 3)","nullifier":"Poseidon2(secret, 1)  // spend-domain; withdraw uses flag 2","ok":true,"prover_snippet":"// zkAPI client-side spend (Node 18+ / browser with snarkjs + circomlibjs).\n// Deposit note = Poseidon3(secret, amount, 3). The server never learns the\n// secret: you keep leaf INDEX from the public Commitment event (or from\n// POST /zkapi/deposit of the commitment) and fetch the Merkle path with\n// GET /zkapi/tree?index=N  \u2014 index+root are already on-chain public data.\nasync function zkapiSpend({ origin, secret, amount, leafIndex, spentAmount, changeSecret, query, target }) {\n  const kit = await (await fetch(origin + \"/zkapi/kit\")).json();\n  const tree = await (await fetch(origin + \"/zkapi/tree?index=\" + leafIndex)).json();\n  if (tree.staleRoot) throw new Error(\"root moved; refetch path against current root \" + tree.root);\n  const { buildPoseidon } = await import(\"circomlibjs\");\n  const snarkjs = await import(\"snarkjs\");\n  const poseidon = await buildPoseidon();\n  const F = poseidon.F;\n  const h2 = (a,b) => F.toObject(poseidon([BigInt(a), BigInt(b)]));\n  const h3 = (a,b,c) => F.toObject(poseidon([BigInt(a), BigInt(b), BigInt(c)]));\n  const NOTE_TAG = 3n;\n  const isFullSpend = BigInt(spentAmount) === BigInt(amount) ? 1n : 0n;\n  const changeAmount = BigInt(amount) - BigInt(spentAmount);\n  const nullifier = h2(secret, 1n);                 // spend-domain flag\n  const changeCommitment = isFullSpend ? 0n : h3(changeSecret, changeAmount, NOTE_TAG);\n  const receiver = BigInt(kit.receiver_field);      // THIS service\n  const receiverCommit = h2(receiver, nullifier);\n  const input = {\n    nullifier: nullifier.toString(),\n    changeCommitment: changeCommitment.toString(),\n    root: tree.root,\n    spentAmount: String(spentAmount),\n    receiverCommit: receiverCommit.toString(),\n    isFullSpend: isFullSpend.toString(),\n    secret: String(secret),\n    amount: String(amount),\n    pathElements: tree.pathElements,\n    pathIndices: tree.pathIndices,\n    changeSecret: String(isFullSpend ? 0n : BigInt(changeSecret)),\n    receiver: receiver.toString(),\n  };\n  const wasm = origin + kit.files.wasm;\n  const zkey = origin + kit.files.zkey;\n  const { proof, publicSignals } = await snarkjs.groth16.fullProve(input, wasm, zkey);\n  const res = await fetch(origin + \"/zkapi/spend\", {\n    method: \"POST\",\n    headers: { \"content-type\": \"application/json\" },\n    body: JSON.stringify({ proof, pubSignals: publicSignals, query, target }),\n  });\n  return res.json();\n}","receiver":"0x367F1b3D8Ca90D1e087481a9A40d585Bf3451a03","receiver_field":"311120069240614946781237594496498492453217966595","receiver_fingerprint":"13767036958689001624676708463541834641104274472129696584045456979246443253347","tree":{"endpoint":"GET /zkapi/tree?index=N","optional_root":"GET /zkapi/tree?root=X&index=N","privacy":"Pass only the public leaf index. The server returns the Merkle path against the current root. It never learns which note is yours."},"urls":{"SpendCircuit.wasm":"https://x402-agent-pay.com/zkapi/kit/SpendCircuit.wasm","SpendCircuit_final.zkey":"https://x402-agent-pay.com/zkapi/kit/SpendCircuit_final.zkey","vkey.json":"https://x402-agent-pay.com/zkapi/kit/vkey.json"}}
