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"
}| Field | Type | Required | Description |
|---|---|---|---|
entityId | string | Yes | Entity UUID |
token | string | No | ERC-20 address. Omit or "0x0000..." for native ETH |
amount | string | Yes | Amount in smallest unit (wei / token base units) |
chain | string | No | Target chain. Defaults to "base-sepolia" |
Response
{
"success": true,
"userOpHash": "0x...",
"noteCommitment": "hex-string",
"chain": "base-sepolia",
"token": "0x0000000000000000000000000000000000000000",
"amount": "1000000"
}Errors
| Status | Error | Cause |
|---|---|---|
| 400 | entityId required | Missing entity ID |
| 400 | Privacy not initialized | Call /api/privacy/initialize first |
| 400 | Valid amount required | Amount is zero or negative |
| 502 | Bundler submission failed | Bundler is unreachable |
| 503 | Wallet infrastructure not configured | Missing 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"
}| Field | Type | Required | Description |
|---|---|---|---|
entityId | string | Yes | Entity UUID |
recipientAddress | string | Yes | Recipient's public key or address |
token | string | No | ERC-20 address. Omit for native ETH |
amount | string | Yes | Amount to transfer |
chain | string | No | Target 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"
}| Field | Type | Required | Description |
|---|---|---|---|
entityId | string | Yes | Entity UUID |
token | string | No | ERC-20 address. Omit for native ETH |
amount | string | Yes | Amount to withdraw |
recipient | string | No | Public address to receive tokens. Defaults to entity wallet |
chain | string | No | Target 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
| Failure | Cause | Client Handling |
|---|---|---|
| Insufficient shielded balance | Not enough unspent notes | Show available balance, prompt user |
| Invalid Merkle root | Root expired (>100 inserts since proof was generated) | Retry with fresh root |
| Spent nullifier | Double-spend attempt | Refresh note state, re-select inputs |
| Prover timeout | Circuit proving exceeded timeout | Retry or reduce amount |
| Bundler rejection | Gas estimation failed or paymaster exhausted | Retry after backoff |