Axiomatic
Privacy & Confidential Transfers

Shielded Transfer API

Request/response schemas for shield, unshield, prove-transfer, and relay endpoints.

All shielded transfer routes require session authentication via NextAuth unless noted otherwise.

POST /api/privacy/shield

Shield (deposit) native ETH or ERC-20 tokens into the private ledger.

Request

{
  "entityId": "uuid",
  "token": "0x...",
  "amount": "1000000",
  "chain": "base-sepolia"
}
FieldTypeRequiredDescription
entityIdstringYesEntity UUID
tokenstringNoERC-20 address. Omit or "0x0000..." for native ETH
amountstringYesAmount in smallest unit (wei / token base units)
chainstringNoTarget chain. Defaults to "base-sepolia"

Response

{
  "success": true,
  "userOpHash": "0x...",
  "noteCommitment": "hex-string",
  "chain": "base-sepolia",
  "token": "0x0000000000000000000000000000000000000000",
  "amount": "1000000"
}

Errors

StatusErrorCause
400entityId requiredMissing entity ID
400Privacy not initializedCall /api/privacy/initialize first
400Valid amount requiredAmount is zero or negative
502Bundler submission failedBundler is unreachable
503Wallet infrastructure not configuredMissing env vars for chain

POST /api/privacy/prove-transfer

Generate a JoinSplit proof for a private transfer. Returns the proof bundle for relay submission.

Request

{
  "entityId": "uuid",
  "recipientAddress": "0x...",
  "token": "0x...",
  "amount": "500000",
  "chain": "base-sepolia"
}
FieldTypeRequiredDescription
entityIdstringYesEntity UUID
recipientAddressstringYesRecipient's public key or address
tokenstringNoERC-20 address. Omit for native ETH
amountstringYesAmount to transfer
chainstringNoTarget chain. Defaults to "base-sepolia"

Response

{
  "success": true,
  "proofId": "uuid",
  "proof": "0x...",
  "root": "0x...",
  "nullifierHashes": ["0x...", "0x..."],
  "newCommitments": ["0x...", "0x..."],
  "encryptedNotesRecipient": ["0x...", "0x..."],
  "encryptedNotesSender": ["0x...", "0x..."],
  "chain": "base-sepolia"
}

The caller then submits this bundle to POST /api/relay/transfer for anonymous on-chain submission.


POST /api/privacy/unshield

Withdraw (unshield) tokens from the private ledger to a public address.

Request

{
  "entityId": "uuid",
  "token": "0x...",
  "amount": "500000",
  "recipient": "0x...",
  "chain": "base-sepolia"
}
FieldTypeRequiredDescription
entityIdstringYesEntity UUID
tokenstringNoERC-20 address. Omit for native ETH
amountstringYesAmount to withdraw
recipientstringNoPublic address to receive tokens. Defaults to entity wallet
chainstringNoTarget chain. Defaults to "base-sepolia"

Response

{
  "success": true,
  "proofId": "uuid",
  "userOpHash": "0x...",
  "chain": "base-sepolia",
  "token": "0x0000000000000000000000000000000000000000",
  "amount": "500000"
}

POST /api/relay/transfer

Anonymous relay endpoint — submits a pre-built transfer payload to the ConfidentialLedger. No authentication required. Rate-limited to 20 requests per minute per client IP.

Request

{
  "proof": "0x...",
  "root": "0x...",
  "nullifierHashes": ["0x...", "0x..."],
  "newCommitments": ["0x...", "0x..."],
  "encryptedNotesRecipient": ["0x...", "0x..."],
  "encryptedNotesSender": ["0x...", "0x..."],
  "chain": "base-sepolia"
}

Response

{
  "success": true,
  "userOpHash": "0x..."
}

The relay never sees decrypted note contents — it only forwards proof data and encrypted notes.


Common Failure Modes

FailureCauseClient Handling
Insufficient shielded balanceNot enough unspent notesShow available balance, prompt user
Invalid Merkle rootRoot expired (>100 inserts since proof was generated)Retry with fresh root
Spent nullifierDouble-spend attemptRefresh note state, re-select inputs
Prover timeoutCircuit proving exceeded timeoutRetry or reduce amount
Bundler rejectionGas estimation failed or paymaster exhaustedRetry after backoff

On this page