Skip to main content

Submit a transfer or withdrawal via relayer

What this lets you do

Hand a prepared transfer or withdrawal (proof and signed quote) to a relayer. The relayer submits it on-chain and pays the gas, taking its fee from the amount you send, so your wallet never appears as the submitter. That is central to staying anonymous: the proof already hides the sender, recipient, and amount (for transfers), and submitting from your own wallet would link the transaction back to you. Relayers also accept fees in whatever asset is being transferred or withdrawn, so the end user never needs ETH for gas.

Constraints & limits

  • Quote must be unexpired. A quote that has expired by the time you submit is rejected by the relayer. Depending on timing this surfaces as a local FeeCommitmentExpired error or as a relayer-side rejection. Either way, recover by re-quoting.
  • Processor must match. Two separate on-chain checks enforce this: require(_transactParams.processor == msg.sender) reverts with PoolVault_InvalidProcessor if the relayer submits via a contract whose address differs from the quoted processor; and the proof's context binds to the full TransactParams + NoteData, so any payload tampering reverts with PoolVault_ProofContextMismatch.
  • Relayer is trusted to submit timely. A malicious relayer could sit on your quote and not submit, costing you time. The protocol doesn't punish them. Pick reliable relayers.
  • Relayer is not trusted with privacy. The relayer sees routing metadata, the proof shape (input/output counts), the encrypted note payloads, the asset and fee, and request metadata such as your IP. It does not see the sender or the recipient. A transfer's amount stays shielded, while a withdrawal's amount is public by design, since the value is leaving the pool. On the deployed relayer the fee shows up on-chain as the transact's public output (amountOut), so a relayed transfer publicly reveals its fee even when nothing else about it is visible.

What it unlocks next

  • Anonymous submission: your wallet never touches the chain, so nothing on-chain links you to the transfer or withdrawal you submitted.
  • Gasless onboarding: users who hold no ETH can still transfer or withdraw, because the relayer takes its fee from the asset being sent (i.e. a USDC fee for a USDC transfer).

How to use it

const prepared = await session.prepareTransfer({
inputCommitments: [noteToSpend.commitment],
amount,
tokenId,
recipientDiscoveryData: { evmAddress: recipient },
});

const result = await session.relayTransfer(prepared.relayOptions[0]);

console.log("Transfer landed:", result.txReceipt.txHash);
console.log("Recipient notes:", result.recipientNotes);

Quote-expiry retry pattern: if proof generation is slow and the quote expires, request a fresh quote by re-running prepareTransfer or prepareWithdraw, then submit again. Cap retries so a stuck relayer doesn't trap the user in an endless loop.

Behind the scenes

SDK call session.relayTransfer(prepared.relayOptions[0]), or session.relayWithdraw(prepared.relayerOptions[0]) for a withdrawal
Relayer endpoint POST {relayer.url}/v1/relay/{chainType}/{chainId}/transfer, or .../withdrawal for a withdrawal
Contract method The deployed Sepolia relayer submits through PrivacyPoolRelay.relay(...), which calls PoolVault.transact(...), so the proof binds processor to the PrivacyPoolRelay contract for transfers, payment-request fulfillment, and withdrawals alike. Bind whatever processor your relayer designates in RelayerInfo.processorAddress.