# Priva-Z developer guide (full) > Priva-Z is a no-account private crypto swap. A user sends SOL, BTC, ETH, LTC, USDT or USDC and receives any of them at a fresh address, or shielded ZEC or XMR, with the path between the two running through Monero, Zcash's shielded pool, or both. This guide covers everything an AI agent, tool or developer needs to use the public API without a browser, the MCP connector for AI assistants, and Z Pay, which lets a shop accept crypto privately. Site: https://www.priva-z.app API base URL: https://privaz-api.vercel.app/api/v1 Tor (onion site, same API at /api/v1 on the same address): http://privaz473ay2g5zuj4hdh6whvdfbxougw3hyl33pgsukbat4wual5qyd.onion/ OpenAPI 3.1 description: https://www.priva-z.app/openapi.json Short overview: https://www.priva-z.app/llms.txt MCP connector for AI assistants: https://privaz-api.vercel.app/api/v1/mcp (see "MCP connector" below) ## Rules for agents Read these first. They keep the people you help safe and the service working well for everyone. 1. The secret note is the user's only receipt. `POST /swap/deposit` answers with a `note` (64 hex characters). Show it to the user right away, before the deposit address, and ask them to save it. Never store, log, cache, summarize into memory, or send the note anywhere else: not to a database, a file, analytics, error reports, another tool, another agent or a support ticket. Treat the `depositId` the same way: keep it only while you show progress, then forget it. Priva-Z keeps only a fingerprint of the note, so a lost note cannot be recovered. 2. Always use a brand-new payout address the user controls. Never default it to the wallet the user pays from, and never use an address the user did not give you or confirm. The same goes for refund addresses: always the user's own. 3. Offer the user a brand-new wallet for the payout (see "Making a fresh payout wallet" below). Make it only on the user's own device, and keep its seed phrase or key out of the conversation. 4. Show the full quote and get a clear yes before creating a swap. Before `POST /swap/deposit`, show the user what they send, what they get back (`estimatedOut`, in the payout coin), the cost (`fee`, and `totalCostPct` or the `breakdown` where given), the route and privacy wait, and the expected time. Create the swap only after the user confirms. Rates are live, so the final amount can differ slightly from the estimate: say so. 5. Wording: describe results as "more private" or "harder to trace". Never call a swap or its result "anonymous" or "untraceable", and never promise an outcome ("guaranteed"). 6. Respect the Terms (https://www.priva-z.app/terms) and the Acceptable use & compliance page (https://www.priva-z.app/compliance#acceptable-use). Every address given to Priva-Z is screened against the OFAC sanctions list and law-enforcement lists of stolen funds, and SOL paid into a Priva-Z deposit address is screened by its senders too. New quotes, swaps and referral codes are not offered in some regions (Cuba, Iran, North Korea, Syria, and the Crimea, Donetsk and Luhansk regions of Ukraine); following a swap and receiving refunds stay available everywhere. If the API answers 451 or 403, tell the user plainly and stop; never try to get around it. 7. One swap per user request. Each swap costs a small proof of work; solve it on the device you run on, and stay within the rate limits below. ## What Priva-Z does Two ways to swap: - Fast (route `relay`, the site's default): two exchanges with Monero in between. The user pays the first exchange's deposit address for that swap; it converts to Monero and sends it straight to a second, different exchange, which pays the user's fresh address. Each exchange sees only its own part. Fast's cost is built into the exchange rates (`feeBps` 0 in its quote). Typically under an hour. Fast pays SOL, BTC, ETH, LTC, USDT and USDC (not ZEC or XMR). A refund address on the coin sent is required for every coin. - Private mode: the swap passes through a privacy coin with a random privacy wait, then pays the user's fresh address. Three routes: - `zec` (Zcash): through Zcash's shielded pool. - `xmr` (Monero): through Monero. - `xmr-zec` (Both): Monero, then Zcash, with a different exchange for each step and a privacy wait in each coin. Privacy waits (`privacyMode`, private modes only; each wait is random within its range): | privacyMode | Shown as | Wait | | --- | --- | --- | | `fast` | Short | 1-3 minutes (the shortest, lighter timing protection) | | `standard` | Standard | 5-30 minutes (the default) | | `max` | Max | 30 minutes to 3 hours | | `extended` | Extended | 6-48 hours (the longest random wait) | | `crowd` | Wait for a crowd | waits until at least 3 other swaps have passed through, up to about 12 hours; any other wait is 30-60 minutes | On `fast` or `standard`, the wait in Monero (route `xmr`, and the Monero step of `xmr-zec`) lasts about 40-70 minutes, Monero's confirmations included, as does `crowd`'s first wait on `xmr-zec`. Note that `privacyMode: "fast"` (the Short wait) is not the Fast route (`route: "relay"`). Coins, each on one network: | Coin | Network | Send | Receive | Amount decimals (sent) | | --- | --- | --- | --- | --- | | `sol` SOL | Solana | yes | yes | 2 | | `btc` BTC | Bitcoin | yes | yes | 4 | | `eth` ETH | Ethereum mainnet (native ETH) | yes | yes | 3 | | `ltc` LTC | Litecoin | yes | yes | 2 | | `usdt` USDT | Solana (SPL) | yes | yes | 0 (whole units) | | `usdc` USDC | Solana (SPL) | yes | yes | 0 (whole units) | | `zec` ZEC | Zcash, shielded addresses only (Unified `u1...` or Sapling `zs1...`) | no | yes, on `zec` and `xmr-zec` | - | | `xmr` XMR | Monero (standard address or subaddress: 95 characters starting with 4 or 8) | no | yes, on `xmr` | - | `GET /info` is the live list: `sendAssets` and `payoutAssets` name the coins on right now, and `assets` gives each coin's network, decimals and maximum. Always read it rather than hard-coding the table above. Round amounts are more private, which is why amounts take few decimals. Every address is checked on its network, with its checksum where the format has one. How a swap goes: 1. Quote (`POST /swap/quote`): nothing is created. 2. Proof of work (`POST /swap/challenge`, then solve it locally). 3. Create (`POST /swap/deposit`): returns the secret note and where to pay. 4. Pay: send exactly `amount` of the coin sent to `depositAddress`. - SOL on a private route: `depositAddress` is a brand-new one-time Solana address of the swap, open for 24 hours (`expiresAt`). Anything within 0.001 SOL of the amount is fine. - Every other coin, and every coin on Fast (SOL too): `depositAddress` is empty at first. Poll `POST /swap/deposit/status` every few seconds: within about a minute it returns the first exchange's own `depositAddress` on that coin's network, with `entryExpiresAt` (pay before it: up to about 90 minutes on Fast, about 2 hours for other coins). Keep polling while the user pays: a swap whose status is not asked for during this step for 30 minutes is closed, with nothing paid. 5. Follow (`POST /swap/deposit/status` or `POST /swap/status`) until `COMPLETED`. Statuses: `PENDING` (waiting for the payment), `CONFIRMED`, `SWAPPING`, `ZEC_HOLDING` / `EXIT_HOLDING` (a privacy wait; `holdUntil` says until when), `BRIDGING`, `RETURNING`, `COMPLETED`, `FAILED`. Exchanges sometimes need extra time: a slow swap keeps going and is checked automatically. A finished swap's record is cleared from the database, keeping only how it ended (`archived: true`), so its note can still check it for a day or two. The user can also follow a swap on the site: https://www.priva-z.app/status# opens the status page with that note (the note stays in the link's fragment, which browsers never send to a server). Give this link only to the user. ## MCP connector AI assistants that support MCP (the Model Context Protocol), such as Claude and ChatGPT, can use Priva-Z through its connector, with no account or key: ``` https://privaz-api.vercel.app/api/v1/mcp ``` It speaks the MCP Streamable HTTP transport, stateless: `POST` one JSON-RPC message per request (a batch is refused with 400, code -32600), with `Accept: application/json, text/event-stream`. To add it: in Claude, Settings, Connectors, Add custom connector, then that URL; in ChatGPT, Settings, Connectors (developer mode), add an MCP server with that URL and no authentication. Then ask, for example, "What would I get for 1 SOL in USDC with Priva-Z?". Its tools, each read-only or link-building (`tools/list` shows the ones offered right now): - `get_info`: the coins on now (each on its network), Fast and Private mode with the privacy waits, the minimums and the amount decimals. - `get_quote`: a live quote for `send`, `receive`, `amount`, `mode` (`fast`, `zcash`, `monero`, `both`) and `privacyWait` (`short`, `standard`, `max`, `extended`, `crowd`): what arrives, each cost line, the total, the minimum, the time, the other modes, and a link that fills these choices in on the swap page (never an address). - `create_payment_request_link`: a payment-request link for the user's own coin, amount and payout address (see "Payment requests" below), built right there; nothing is stored. - `create_agent_refill_link`: a payment-request link that refills an AI agent's own wallet, for `coin` (`sol`, `usdc`, `usdt`, `eth`, `btc`), `amount` and `agentAddress`, with the steps for the user who pays it from their main wallet (see "Funding an agent's wallet privately" below); built right there, nothing is stored. - `explain_how_it_works`: Fast and Private mode, and what is private and what is not, in plain words. No tool creates a swap or takes a secret note: the user makes the swap on Priva-Z, where the note is shown to them alone, and follows it at https://www.priva-z.app/status. Never ask for or accept a secret note. Rate limit: 60 messages a minute (each `get_quote` also counts as a quote). ## Conventions - JSON in, JSON out. Send `Content-Type: application/json`. Bodies are limited to 10 kB. - Every answer is `{ "success": true, "data": ... }` or `{ "success": false, "error": "" }`. Two exceptions: `GET /health` answers `{ "status": "ok", "timestamp": "..." }`, and `POST /rpc` answers in JSON-RPC 2.0. - Validation errors (400) also carry `issues`: a list of `{ "path": "", "message": "" }`, and `error` is their messages joined. - Unknown fields are refused (every body schema is strict). - Amounts are decimal strings ("1", "0.5"), in the coin sent unless a field says otherwise. - Writes (POST) are never cached (`Cache-Control: no-store`). - Call from a server or a local tool. Browsers on other sites cannot call the API directly (CORS is limited to Priva-Z's own site); requests without an `Origin` header are fine. - Error messages are written for people: you can show `error` to the user as it is. ## Endpoints Every path below is relative to https://privaz-api.vercel.app/api/v1. Addresses in the examples are placeholders: always use the user's real, fresh addresses. ### GET /health Liveness check. Answers `{ "status": "ok", "timestamp": "2026-11-07T12:00:00.000Z" }`. ### GET /info The live settings: coins on now, minimums, fee, and whether Fast is offered. ```json { "success": true, "data": { "cluster": "mainnet-beta", "simulated": false, "feeBps": 50, "minAmount": "0.12", "minAmounts": { "sol": "0.12", "usdc": "0.12", "zec": "0.1" }, "routeMinAmounts": { "zec": "0.12", "xmr": "0.62", "xmr-zec": "0.3", "relay": "0.63" }, "fast": { "available": true, "minAmount": "0.63" }, "sendAssets": ["sol", "btc", "eth", "usdt", "usdc", "ltc"], "payoutAssets": ["sol", "btc", "eth", "usdt", "usdc", "ltc", "zec", "xmr"], "assets": { "sol": { "symbol": "SOL", "network": "Solana", "inputDecimals": 2, "max": "9000", "maxStated": false }, "btc": { "symbol": "BTC", "network": "Bitcoin", "inputDecimals": 4, "max": "15", "maxStated": false }, "zec": { "symbol": "ZEC", "network": "Zcash", "inputDecimals": 8, "maxStated": false } } } } ``` - `feeBps`: the Priva-Z fee on the private modes, in basis points (each quote states the rate that applies to it). - `minAmount`, `minAmounts` (per payout coin) and `routeMinAmounts` (per route): the smallest SOL amounts the private modes take right now, for SOL sent. They can be missing for a moment after a restart; a coin or route not listed there: its quote states the minimum. - `fast.available`: whether Fast is offered right now; `fast.minAmount` is its minimum in SOL. - `assets..max`: a technical ceiling per swap (far above any exchange's own limit, which the quote reports as `maxAmount`). `maxAmount` at the top level appears only when the deployment sets a fixed maximum for SOL. - `staggeredSplit: true` appears while the staggered split option is on. - `simulated: true` would mean a test deployment where nothing real moves. ### POST /swap/quote Quotes a swap. Nothing is created. Rate limit: 20 a minute. Request fields: | Field | Type | Notes | | --- | --- | --- | | `amount` | string, required | What the user sends, in `sendAsset`, e.g. "1" (SOL) or "0.01" (BTC). Up to 20 characters. | | `route` | `relay`, `zec`, `xmr`, `xmr-zec` | Default `zec`. | | `sendAsset` | coin code | Default `sol`. | | `payoutAsset` | coin code (or `zec`, `xmr`) | Default `sol`. | | `includeFast` | boolean | Also list Fast (`relay`) in `routes`. Check `/info` `fast.available` first. | | `referralCode` | string | 4-16 letters and digits. | | `separateExchanges` | boolean | A different exchange for each step. Left out: on by default where it costs at most about 0.3% more. | | `avoidExchange` | string | An exchange id, as a quote's `providers` names it, to keep out of the swap (used only while the site offers that choice: the quote then has `exchangeChoice`). | ```bash curl -s -X POST https://privaz-api.vercel.app/api/v1/swap/quote \ -H "Content-Type: application/json" \ -d '{"amount":"1","route":"zec","sendAsset":"sol","payoutAsset":"sol","includeFast":true}' ``` The answer (abridged; values are examples): ```json { "success": true, "data": { "amount": "1.000000000", "fee": "0.005000000", "net": "0.995000000", "route": "zec", "feeBps": 50, "simulated": false, "sendAsset": "sol", "payoutAsset": "sol", "minAmount": "0.12", "estimatedOut": "0.9871", "estimatedSolBack": "0.9871", "separateExchanges": false, "routes": [ { "route": "relay", "available": true, "minAmount": "0.63", "reason": "The minimum for this route is currently 0.63 SOL" }, { "route": "zec", "available": true, "minAmount": "0.12", "estimatedOut": "0.9871", "estimatedSolBack": "0.9871", "totalCostPct": 1.3 }, { "route": "xmr", "available": true, "minAmount": "0.62", "reason": "The minimum for this route is currently 0.62 SOL" } ], "breakdown": { "feePct": 0.5, "totalPct": 1.3 } } } ``` - `estimatedOut`: what arrives, in the payout coin (`estimatedSolBack` too when that is SOL). `minAmount` and `maxAmount` are in the coin sent; `maxAmount` appears where an exchange states a maximum. - `routes`: one entry per route with `available`, `minAmount`, `estimatedOut`, `totalCostPct`, or a `reason` when that route can't take this amount right now (show the reason to the user: it is written for people). - `breakdown`: where known, the Priva-Z fee (`feePct`), each step's cost (`legs`, with `costPct`), what is kept back for network fees and privacy rounding (`keptBackPct`), and the total (`totalPct`: all costs as a percent of what goes in). - `routeError`: why the chosen route can't be quoted right now. - Quotes are cached for 30 seconds; ask again before creating a swap if the user took a while to decide. ### POST /swap/challenge A proof-of-work challenge for one new swap. No body (send `{}`). Rate limit: 20 a minute. ```json { "success": true, "data": { "id": "6f1d2c3b-8a4e-4f60-9b1a-2c3d4e5f6a7b", "exp": 1794571200000, "bits": 3, "sig": "<64 hex characters>" } } ``` When proof of work is off, the answer is `{ "bits": 0 }`: then send the deposit without `challenge` and `nonce`. See "Proof of work" below for how to solve it. ### POST /swap/deposit Creates the swap. Rate limit: 10 a minute. Every address is screened (403 for an address on the sanctions list or a law-enforcement list of stolen funds). | Field | Type | Notes | | --- | --- | --- | | `amount` | string, required | What the user will send, in `sendAsset`, within the route's minimum and maximum. | | `returnAddress` | string, required | The payout address, on `payoutAsset`'s network. Brand-new, the user's own, never the sending wallet. | | `route` | `relay`, `zec`, `xmr`, `xmr-zec` | Default `zec`. | | `privacyMode` | `fast`, `standard`, `max`, `extended`, `crowd` | Default `standard`. Private modes only. | | `sendAsset`, `payoutAsset` | coin codes | Default `sol` / `sol`. | | `refundAddress` | string | On `sendAsset`'s network, the user's own. Required for every coin other than SOL and for every coin on `relay`. Optional for SOL on the private routes (SOL left at the deposit address goes back there automatically). Must not be one of the payout addresses. | | `splitPayout` | boolean | Private modes: pay out in 2-3 uneven payments with waits between them. Default false. | | `extraReturnAddresses` | string[] | Up to 4 more payout addresses (with `splitPayout`). All addresses must differ. | | `payoutPercents` | integer[] | One whole percent (5-95) per wallet, main address first, adding up to 100 (with `extraReturnAddresses`). | | `staggeredSplit` | boolean | Split payout only, while `/info` has `staggeredSplit: true`: each part at its own time within one privacy wait. | | `referralCode` | string | 4-16 letters and digits. | | `separateExchanges`, `avoidExchange` | | As for `/swap/quote`. | | `challenge` | object | The `/swap/challenge` answer, unchanged: `{ id, exp, bits, sig }`. Required unless it was `{ "bits": 0 }`. | | `nonce` | string | The decimal digits that solve the challenge (up to 20). | | `zpay` | object | Only when paying a Z Pay link: `{ "v": 1, "pid", "exp", "amount" }` (plus `btn` on links a Z Pay button makes), exactly as written in the link, with the link's coin as `payoutAsset` and its `to` as `returnAddress`. See "Z Pay" below. | ```bash curl -s -X POST https://privaz-api.vercel.app/api/v1/swap/deposit \ -H "Content-Type: application/json" \ -d '{"amount":"1","returnAddress":"","privacyMode":"standard","route":"zec", "challenge":{"id":"6f1d2c3b-8a4e-4f60-9b1a-2c3d4e5f6a7b","exp":1794571200000,"bits":3,"sig":"<64 hex>"},"nonce":"11"}' ``` ```json { "success": true, "data": { "depositId": "6f1d2c3b-8a4e-4f60-9b1a-2c3d4e5f6a7b", "note": "<64 hex characters: show it to the user, never store it>", "depositAddress": "", "amount": "1.000000000", "fee": "0.005000000", "net": "0.995000000", "route": "zec", "sendAsset": "sol", "payoutAsset": "sol", "separateExchanges": false, "simulated": false } } ``` The `depositId` is the challenge's `id`, so each solved challenge creates one swap. If the answer is 503 ("Busy right now"), send the very same request again after 1-2 seconds, up to 3 times: no new work is needed. A 409 ("That challenge was already used") means fetch a new challenge. ### POST /swap/deposit/status A swap's progress by its id: `{ "depositId": "" }`. 404 when not found. ```json { "success": true, "data": { "depositId": "6f1d2c3b-8a4e-4f60-9b1a-2c3d4e5f6a7b", "status": "ZEC_HOLDING", "route": "zec", "privacyMode": "standard", "sendAsset": "sol", "payoutAsset": "sol", "depositAddress": "", "entryExchange": null, "entryExpiresAt": null, "expiresAt": null, "amount": "1.000000000", "fee": "0.005000000", "net": "0.995000000", "directPayout": false, "separateExchanges": false, "holdUntil": "2026-11-07T12:40:00.000Z", "crowdWait": false, "splitParts": 1, "splitDone": 0, "swap1Status": "finished", "swapMidStatus": null, "swap2Status": null, "failedLeg": null, "resolution": null, "funded": true, "selfFunded": false, "receivedSol": null, "received": null, "createdAt": "2026-11-07T12:00:00.000Z", "updatedAt": "2026-11-07T12:20:00.000Z", "confirmedAt": "2026-11-07T12:01:00.000Z", "completedAt": null } } ``` - `depositAddress`: where to pay (null until the first exchange's address is ready for other coins and Fast; null again once the payment is taken in). - `expiresAt`: while unpaid, when the payment window closes. `entryExpiresAt`: the first exchange's window. - `receivedSol` / `received`: what has arrived so far while the payment is being taken in (a short payment shows here). - Step statuses (`swap1Status`, `swapMidStatus`, `swap2Status`) are one of `waiting`, `confirming`, `exchanging`, `sending`, `finished`, `refunded`, `failed`, `expired`. - `resolution` for a `FAILED` swap: `refund_pending`, `refunded`, `recovered`, or null (then the user can email support with the swap ID, privaz.app@yahoo.com). - `selfFunded: true`: it was paid from one of its own payout addresses, a link the swap can't hide. Tell the user. - An archived swap answers only `{ status, resolution, archived: true }` (plus `depositId` here). Polling: every few seconds while you wait for a deposit address, then every 30-60 seconds. Exchange order ids are never returned. ### POST /swap/status The same progress, looked up by the secret note: `{ "note": "<64 hex>" }`. 404 when not found. It also answers `refundClaim`: whether a refund address can be added to this swap (`available`, `retry`), or one was (`claimed`, `review`, `sent`), or `support` (write to support), or null. Use this only when the user hands you the note again (for example, to check a swap later): don't keep the note to call it. ### POST /swap/refund-challenge and POST /swap/refund-claim For a SOL swap whose SOL waits at its deposit address with no refund address yet (`refundClaim` is `available` or `retry`), the user can add one: 1. `POST /swap/refund-challenge` (no body): a challenge like the deposit's, always 4 bits, signed for refund claims only. Rate limit: 20 a minute. 2. Solve it as below, then `POST /swap/refund-claim` with `{ "note": "<64 hex>", "refundAddress": "", "challenge": { ... }, "nonce": "" }` (optional `depositId`, which must match). Rate limit: 10 a minute. Answers `{ "claimed": true }` (and `"review": true` when a person checks it before it is sent). The refund is sent within a random 30 to 120 minutes. The refund address must not be one of the payout addresses. Both answer 404 while self-serve refunds are switched off. ### POST /swap/pay-check Whether a `/pay` link (a deposit address and an exact amount in lamports) is a real, open Priva-Z payment: `{ "address": "", "lamports": "1000000000" }` answers `{ "ok": true }` or `{ "ok": false }`, nothing else. The site's /pay page (https://www.priva-z.app/pay#to=&amount=) uses it before showing a payment. Rate limit: 60 a minute. ### POST /swap/quote-for-output The reverse quote for payment requests: how much to send so that about `targetOut` arrives. Takes amounts and coins only, never an address. Rate limit: 6 a minute. | Field | Type | Notes | | --- | --- | --- | | `targetOut` | string, required | The amount requested, in `payoutAsset` (decimals per coin: SOL 4, BTC 6, ETH 5, LTC 4, USDT 2, USDC 2, ZEC 6, XMR 6). | | `sendAsset` | coin code, required | What the payer sends. | | `payoutAsset` | coin code, required | What the requester receives. | | `route` | `relay`, `zec`, `xmr`, `xmr-zec`, required | ZEC is paid on `zec` and `xmr-zec`, XMR on `xmr`. | | `privacyMode` | as above, optional | Accepted for the swap that follows; it doesn't change the amount. | Answers one of: - `{ "status": "ok", "sendAsset": "sol", "payoutAsset": "usdc", "route": "relay", "targetOut": "50", "sendAmount": "", "estimatedOut": "", "bufferPct": 1, "totalCostPct": 1.6 }`: send `sendAmount`. It aims at the request plus a small cushion (`bufferPct`), so the person paid gets close to what they asked for as rates move. - `{ "status": "below-minimum", "minAmount": "...", "estimatedOutAtMin": "..." }` or `{ "status": "above-maximum", "maxAmount": "..." }`: suggest another coin or route. - `{ "status": "unavailable", "reason": "..." }`. Every answer also has `evaluations` and `simulated`. Answers 404 while payment requests are off. ### POST /referral/create Makes a referral code: `{ "payoutCoin": "zec" | "sol", "payoutAddress": "" }`. Answers `{ "code": "k7m2qx9p", "key": "<64 hex>" }`. The key is shown once and is the only way to see earnings or change the payout address: give it to the user and never keep it. Rate limit: 5 an hour. ### POST /referral/check Whether a code exists: `{ "code": "k7m2qx9p" }` answers `{ "valid": true }`. Rate limit: 20 a minute. ### POST /referral/stats Earnings for the code of a secret key: `{ "key": "<64 hex>" }` answers `{ "code", "payoutCoin", "payoutAddressEnd" (last 6 characters), "paidZec" (paid so far, rounded down to 0.005 ZEC), "payoutMinimumZec" }`. 404 when no code matches. Rate limit: 10 a minute. ### POST /referral/address A new payout address for a code, proven with its key: `{ "key": "<64 hex>", "payoutAddress": "
" }` answers `{ "code", "payoutAddressEnd" }`. 409 while a payout to the old address is on its way. Rate limit: 5 an hour. ### POST /rpc A narrow Solana JSON-RPC relay for the site's "pay from wallet" button: `getLatestBlockhash`, `getSignatureStatuses` (one signature), `getBlockHeight`, `getBalance`, and `sendTransaction` (base64, only a plain SOL transfer into an open Priva-Z deposit address). Body: `{ "jsonrpc": "2.0", "id": 1, "method": "getBlockHeight" }`. Most agents never need it: any Solana wallet or RPC can pay a deposit address. Sends answer at a fixed time (about 2.5 seconds). Rate limit: 60 a minute. ### POST /zpay/status For a shop's server: whether its Z Pay links were paid (see "Z Pay" below). Up to 50 lookup keys, 64 lowercase hex characters each: `{ "keys": [""] }`. Answers one entry per key: ```json { "success": true, "data": { "": { "state": "completed", "swaps": 1, "short": false, "day": "2026-11-07" } } } ``` - `state`: `completed` (the payout was sent to the shop's address; shown at once), `refunded` (the payment went back to the customer) or `review` (settled by hand; check the wallet), both shown from the next UTC day, or `none` (nothing to show yet, or the records were already deleted, about a day after the swap finished). - `swaps`: how many swaps for the key are shown. `short`: where the route checked the amount, `true` means less than asked arrived (not paid in full). `checked: false`: on a completed payment whose route couldn't check the amount, see the wallet for what arrived. `day`: the UTC day it finished. - A swap still waiting for the payment, under way or expired is never shown, and the customer's wallet, coin, amount sent, route and times are never returned. Never cached; not limited by region. Rate limit: 30 a minute. ### POST /zpay/button/payments and POST /zpay/button The Z Pay button's dashboard, for the shop that owns the button. Both take the shop's secret key (43 characters of base64url, made in the shop's browser, never the button's public id), the button's payout coin and address: `{ "key", "coin", "to" }`. - `/zpay/button/payments` answers `{ "payments": [{ "id", "amount", "state", "short"?, "checked"?, "at"? }], "alerts": { "discord": true, "telegram": false } }`: finished payments only, completed ones newest first (`at`, when it completed, to the minute), then refunds and reviews from the next UTC day (no time), at most 100, from about the last 1-2 days. A wrong key gets an empty list. Rate limit: 30 a minute. - `/zpay/button` also takes `discord` (a webhook URL, `https://discord.com/api/webhooks//`) and `telegram` (`{ "token": "", "chat": "" }`): a value sets an alert, `null` removes it, left out keeps it. Answers `{ "discord", "telegram" }`. One message per completed payment. Rate limit: 10 a minute. ## Proof of work Each new swap first solves a small proof of work, so opening swaps stays cheap for people and costly for spam. It normally takes about a second on a phone and asks for more work only while many unpaid swaps are open (never more than 9 bits). The algorithm, exactly as the site solves it: 1. `POST /swap/challenge` with `{}`. You get `{ id, exp, bits, sig }`. `id` is a UUID, `exp` is a time in milliseconds since 1970 (the challenge is valid until then: at least 10 minutes), `bits` is the difficulty, `sig` is the server's signature. 2. For nonce = 0, 1, 2, ... (decimal digits, no leading zeros, no sign): compute `scrypt(password = id + nonce, salt = id, N = 16384, r = 8, p = 1, dkLen = 32)`, with password and salt as UTF-8 text and no separator (for id `6f1d...` and nonce `11` the password is `6f1d...11`). 3. Stop at the first hash whose first `bits` bits are all zero (counted from the most significant bit of the first byte). On average that takes 2^bits tries. 4. Send `challenge` (all four fields, unchanged, nothing added) and `nonce` (as a string) with `POST /swap/deposit`, before `exp`. Each try uses 16 MiB of memory. After 2 wrong answers a challenge is refused: fetch a new one. If the answer is `{ "bits": 0 }`, skip the work and leave both fields out. Node.js 18+ (built-in `node:crypto`): ```js import { scryptSync } from 'node:crypto'; const API = 'https://privaz-api.vercel.app/api/v1'; async function post(path, body = {}) { const res = await fetch(API + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const json = await res.json(); if (!json.success) throw new Error(`${res.status}: ${json.error}`); return json.data; } /** Leading zero bits of a hash. */ function zeroBits(hash) { let n = 0; for (const byte of hash) { if (byte === 0) { n += 8; continue; } return n + Math.clz32(byte) - 24; } return n; } /** The first nonce that solves a challenge. */ export function solve(challenge) { for (let n = 0; ; n++) { const nonce = String(n); const hash = scryptSync(challenge.id + nonce, challenge.id, 32, { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }); if (zeroBits(hash) >= challenge.bits) return nonce; } } // Only after the user has seen the quote and said yes: const challenge = await post('/swap/challenge'); const pow = challenge.bits ? { challenge, nonce: solve(challenge) } : {}; const swap = await post('/swap/deposit', { amount: '1', route: 'zec', privacyMode: 'standard', returnAddress: '', ...pow, }); // Show swap.note to the user first (never store or log it), then swap.depositAddress and the amount. ``` Python 3.6+ (standard library only): ```python import hashlib, json, urllib.request API = "https://privaz-api.vercel.app/api/v1" def post(path, body=None): req = urllib.request.Request(API + path, data=json.dumps(body or {}).encode(), headers={"Content-Type": "application/json", "User-Agent": "my-agent/1.0"}, method="POST") with urllib.request.urlopen(req) as res: out = json.load(res) if not out.get("success"): raise RuntimeError(out.get("error")) return out["data"] def zero_bits(h: bytes) -> int: n = 0 for byte in h: if byte == 0: n += 8 continue return n + (8 - byte.bit_length()) return n def solve(ch) -> str: n = 0 while True: nonce = str(n) h = hashlib.scrypt((ch["id"] + nonce).encode(), salt=ch["id"].encode(), n=16384, r=8, p=1, maxmem=64 * 1024 * 1024, dklen=32) if zero_bits(h) >= ch["bits"]: return nonce n += 1 # Only after the user has seen the quote and said yes: ch = post("/swap/challenge") body = {"amount": "1", "route": "zec", "privacyMode": "standard", "returnAddress": ""} if ch.get("bits"): body.update(challenge=ch, nonce=solve(ch)) swap = post("/swap/deposit", body) # Show swap["note"] to the user first (never store or log it), then swap["depositAddress"] and the amount. ``` (`urllib` raises `HTTPError` for 4xx and 5xx answers; its body holds the same `{ success, error }` JSON.) ## Rate limits Per IP address, per minute unless noted. Treat 429 as "wait and retry"; answers carry `RateLimit-*` headers. | Endpoint | Limit | | --- | --- | | everything under /swap, /referral and /rpc together | 100 a minute | | POST /swap/quote | 20 a minute | | POST /swap/challenge | 20 a minute | | POST /swap/deposit | 10 a minute | | POST /swap/quote-for-output | 6 a minute | | POST /swap/refund-challenge | 20 a minute | | POST /swap/refund-claim | 10 a minute | | POST /swap/pay-check | 60 a minute | | POST /referral/check | 20 a minute | | POST /referral/stats | 10 a minute | | POST /referral/create | 5 an hour | | POST /referral/address | 5 an hour | | POST /rpc | 60 a minute | | POST /zpay/status | 30 a minute | | POST /zpay/button/payments | 30 a minute | | POST /zpay/button | 10 a minute | | POST /mcp (the MCP connector) | 60 a minute | On the Tor site, limits count per Tor circuit. ## Errors | Status | Meaning | What to do | | --- | --- | --- | | 400 | The request needs a fix (`issues` lists each field for validation errors), or the amount is outside the route's minimum or maximum. | Show `error` to the user; fix and retry. | | 403 | "This address can't be used with Priva-Z." (sanctions and stolen-funds screening) | Stop. Don't retry with the same address. | | 404 | Not found (an unknown swap, a feature that is switched off). | | | 409 | A challenge already used, or a change that conflicts with one under way. | Fetch a new challenge, or wait. | | 429 | Too many requests. | Wait, then retry slowly. | | 451 | `code: "region_unavailable"`: new swaps are not offered in the caller's region. | Tell the user plainly; stop. | | 500 | Internal server error. | Retry later. | | 502 | The RPC relay couldn't answer. | Retry later. | | 503 | Busy for a moment (maintenance, a route taking a short break, many open swaps). | Retry in a few seconds or minutes; for `/swap/deposit`, resend the same request. | ## Making a fresh payout wallet A payout address that has never touched the user's other wallets gives the best privacy. The payout address must be a brand-new address the user controls, never the wallet they send from and never one used before. Offer the user a brand-new wallet for it. Make one only when you run on the user's own device, never in a cloud chat or a remote sandbox: generate it with a standard, well-known library for that coin, in a small script that writes the seed phrase or private key straight to a file on that device (or into the user's wallet app) and prints only the address. Never print, read back or repeat the seed phrase or key in the conversation. The user imports it into their own wallet app and confirms before the address is used as the payout. For Zcash and Monero, suggest a wallet app instead. When you run anywhere else, the user makes a fresh address in their own wallet app. Priva-Z never sees or makes anyone's keys. Standard choices for that script: for Solana a new keypair; for Bitcoin or Litecoin a BIP39 seed phrase with standard derivation; for Ethereum a new key. ## Links that open the site Agents that can't run the proof of work (or users who prefer the site) can hand off with a link. All values go in the fragment (after `#`), which browsers never send to a server. Swap links prefill the swap page; the user types the payout address on Priva-Z itself (a link never carries one): ``` https://www.priva-z.app/swap#amount=1&route=xmr-zec&privacy=max&ref=k7m2qx9p https://www.priva-z.app/swap#amount=0.01&send=btc&payout=usdc&route=relay ``` Keys: `amount` (in the coin sent), `route` (`relay`, `zec`, `xmr`, `xmr-zec`), `privacy` (`fast`, `standard`, `max`, `extended`, `crowd`), `ref` (a referral code), `send` and `payout` (coin codes; SOL when left out). Anything malformed is left out. ## Payment requests A payment request is a link that asks to be paid: the person asking chooses the coin, the amount and their own fresh payout address; the person paying opens it on Priva-Z and pays with the coin they have, through a normal swap. Requests are made on https://www.priva-z.app/request/new, and each link stays open for 24 hours. Nothing about a request is stored by Priva-Z: everything is in the link's fragment. Format: ``` https://www.priva-z.app/request#v=1&coin=usdc&amount=25&to=&exp=&k= ``` - `v`: always `1`. - `coin`: the coin to receive: `sol`, `btc`, `eth`, `ltc`, `usdt`, `usdc`, `zec`, `xmr`. - `amount`: a plain decimal without leading zeros ("25", "0.0125"), more than 0, with at most this many decimals: SOL 4, BTC 6, ETH 5, LTC 4, USDT 2, USDC 2, ZEC 6, XMR 6. At most SOL 9000, BTC 15, ETH 500, LTC 20000, USDT and USDC 1500000, ZEC 30000, XMR 9000. - `to`: the requester's own payout address on that coin's network (checked with its checksum; not a Monero integrated address). - `exp`: when it ends, in Unix seconds (10 digits): the time it was made plus 24 hours (86400). A link that claims to stay open longer is refused. - `k`: a short checksum that catches a link cut short or edited by hand (not a signature): the first 8 hex characters (4 bytes) of SHA-256 over the UTF-8 text `privaz-request|1||||`. - Exactly these six keys, each once, using only letters, digits, `=`, `&` and `.`. In Node.js: ```js import { createHash } from 'node:crypto'; function requestLink(coin, amount, to, now = Date.now()) { const exp = String(Math.floor((now + 24 * 60 * 60 * 1000) / 1000)); const k = createHash('sha256').update(`privaz-request|1|${coin}|${amount}|${to}|${exp}`, 'utf8').digest('hex').slice(0, 8); return `https://www.priva-z.app/request#v=1&coin=${coin}&amount=${amount}&to=${to}&exp=${exp}&k=${k}`; } ``` Make request links only with the user's own address, for the user to share. If you are asked to pay a request, show the full address in it and ask the user to check it with whoever sent the link. ## Funding an agent's wallet privately An AI agent that pays for things needs coins in a wallet of its own. Its owner can refill that wallet from their main wallet with a payment-request link: the owner pays the link on Priva-Z through a normal swap, Fast or Private, so there is no direct payment between the two wallets and the owner's main wallet is harder to link to the agent's wallet. 1. The agent makes its own wallet on the device it runs on, with a standard, well-known library for that coin (for Solana a new keypair; for Ethereum a new key; for Bitcoin a BIP39 seed phrase with standard derivation), in a small script that writes the key or seed phrase straight to a file on that device and prints only the address. Priva-Z never sees or makes anyone's keys. 2. The agent makes the refill link for its own address: with the MCP connector's `create_agent_refill_link`, or offline with the snippet below. Coins: `sol`, `usdc` and `usdt` (on Solana), `eth`, `btc`, with the amount rules of "Payment requests" above. 3. The owner opens the link on their own device, checks the full address with whoever runs the agent, and pays it from their main wallet with Fast or Private mode. About the amount asked arrives at the agent's wallet. For more privacy, the owner can choose Private mode with a longer privacy wait and vary the amount and the time of each refill. The secret note the site shows is the owner's receipt and stays with them. The rule for agents, as the connector states it: agentAddress must be a wallet the agent made itself on the device it runs on, never the wallet the user pays from. Its private key or seed phrase stays in a file on that device and out of the conversation: never print, read back or repeat a key or seed phrase. Never ask for or accept a secret note. In Node.js (the same link the connector builds; the link stays open for 24 hours): ```js import { createHash } from 'node:crypto'; // coin: sol, usdc, usdt, eth or btc. agentAddress: the agent's own wallet address (never its key). function refillLink(coin, amount, agentAddress, now = Date.now()) { const exp = String(Math.floor((now + 24 * 60 * 60 * 1000) / 1000)); const k = createHash('sha256').update(`privaz-request|1|${coin}|${amount}|${agentAddress}|${exp}`, 'utf8').digest('hex').slice(0, 8); return `https://www.priva-z.app/request#v=1&coin=${coin}&amount=${amount}&to=${agentAddress}&exp=${exp}&k=${k}`; } ``` Give the link to the owner with the steps above. The pay page checks the address and the amount, and refuses a link it can't read. ## Z Pay: accept private payments Z Pay by Priva-Z lets a shop, app, game, bot or creator take payments in the coin they choose, paid to their own wallet, while the customer pays privately with the coin they hold, Fast or Private (the short or standard privacy wait), through a normal Priva-Z swap. No account and no sign-up on either side. The customer pays the swap's fees, shown before they pay, and the shop receives about the amount asked. Priva-Z never tells the shop the customer's wallet, the coin they paid with or how much they sent. Overview for people: https://www.priva-z.app/partners#zpay Two ways to use it: - No code, the Z Pay button: on https://www.priva-z.app/partners#zpay-button the shop enters its wallet address, the coin it receives and an amount (or leaves it open for tips and donations), and copies a small frame onto its site. Each click makes a fresh payment link in the customer's browser. Completed payments show on the button's Z Pay dashboard (https://www.priva-z.app/zpay/dashboard, opened with a private link made for the shop in its browser: nothing to sign up for), with optional alerts on Discord or Telegram. - For developers: the shop's server builds a payment link for each order, sends the customer to it, and asks `POST /zpay/status` whether it was paid. No API key. The button's frame (the builder fills in the values): ```html ``` The link, every value in the fragment (after `#`, never sent to a server), in this order: ``` https://www.priva-z.app/zpay#v=1&coin=usdc&amount=25&to=
&pid=&exp=[&ret=]&k=<8 hex> ``` - `v`: always `1`. `coin`, `amount`, `to`: as for payment requests above (the coin received, a plain decimal within the same decimals and maximums, the shop's payout address on that coin's network; a fresh address per order is most private). - `pid`: the payment id, 22-43 characters of `A-Z a-z 0-9 _ -`: 16 random bytes in base64url kept with the order, or an HMAC of the order id under a key only the shop's server holds. Never the order number itself. - `exp`: when the link ends, in Unix seconds, later than now and at most 24 hours ahead. - `ret` (optional): the shop's return page, base64url (no padding) of an https URL of up to 300 characters. It stays in the customer's browser. - `btn` (only on links a Z Pay button makes): the button's public id, 22 characters of base64url. - `k`: a checksum (not a signature): the first 8 hex characters of SHA-256 over `privaz-zpay-link|1|` followed by v, coin, amount, to, pid, exp and ret joined with `|` (an empty string for ret when there is none), then `|` and btn when the link has one. The lookup key the shop keeps with the order and asks the status with: `sha256_hex("privaz-zpay|1|" + coin + "|" + amount + "|" + to + "|" + pid + "|" + exp)`, over UTF-8, lowercase hex, every value exactly as written in the link. In Node.js: ```js import { createHash, randomBytes } from 'node:crypto'; const sha256 = (text) => createHash('sha256').update(text, 'utf8').digest('hex'); export function zpayLink({ coin, amount, to, returnTo, minutes = 60 }) { const pid = randomBytes(16).toString('base64url'); const exp = String(Math.floor(Date.now() / 1000) + minutes * 60); const ret = returnTo ? Buffer.from(returnTo, 'utf8').toString('base64url') : ''; const k = sha256(['privaz-zpay-link', '1', '1', coin, amount, to, pid, exp, ret].join('|')).slice(0, 8); const key = sha256(['privaz-zpay', '1', coin, amount, to, pid, exp].join('|')); const parts = [['v', '1'], ['coin', coin], ['amount', amount], ['to', to], ['pid', pid], ['exp', exp]]; if (ret) parts.push(['ret', ret]); parts.push(['k', k]); return { url: 'https://www.priva-z.app/zpay#' + parts.map(([n, v]) => n + '=' + v).join('&'), pid, exp, key }; } ``` Before shipping an order: - Save each status as soon as you see it: records are deleted about a day after a swap finishes, and the status then reads `none`. - Ship once the status is `completed` and the full amount shows in your own wallet. Treat `short: true` as not paid in full; with `checked: false`, see in your wallet what arrived. - One link per buyer, never posted publicly. A new payment for a link is refused while one is completed, under way or in review, and after 3 tries: then make a new link. - Send customers to the link with `rel="noreferrer"` (or `Referrer-Policy: no-referrer` on a redirect), so the shop's page address stays with the shop. For agents: a shop's owner can have you build links and check statuses with the code above. When a user asks you to pay a Z Pay link, send them to open it on Priva-Z, where the checkout shows the amount, the shop's address and the time left, and they pay and save their own secret note. ## Referral codes Anyone can make a referral code on https://www.priva-z.app/referrals (or with `POST /referral/create`); no account is needed. A swap made with a code pays a lower Priva-Z fee in Private mode on Zcash and Both, and the code's owner earns a share of each such swap, paid in shielded ZEC or SOL (chosen when the code is made) once a day, when at least 0.05 ZEC has built up. Monero-only and Fast swaps don't earn. Codes are 4-16 letters and digits, case-insensitive. The code is stored with each swap only until it finishes; its owner never sees the swap's addresses. Details: https://www.priva-z.app/partners#earnings ## Embeddable widgets The swap widget is a small form for any website. The full swap opens on Priva-Z in a new tab, where the visitor enters their payout address; the deposit and its secret note are created there, never inside the embedding page. ```html ``` Options after `#`, joined with `&`: `ref` (your referral code), `route` (`relay`, `zec`, `xmr`, `xmr-zec`), `privacy` (`fast`, `standard`, `max`, `extended`, `crowd`), `amount` (SOL, up to 2 decimals), `theme` (`dark` or `light`). With a `sandbox` attribute, include `allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox`. The request widget is the payment request form on its own (`/widget#mode=request`, options `coin`, `amount`, `theme`; the payout address is always typed by the visitor): ```html ``` More: https://www.priva-z.app/partners ## Pages - Home: https://www.priva-z.app/ - Swap: https://www.priva-z.app/swap - How it works: https://www.priva-z.app/how-it-works - Check a swap's status: https://www.priva-z.app/status - Request a payment: https://www.priva-z.app/request/new - Referrals: https://www.priva-z.app/referrals - Partners (widgets, Z Pay, the AI assistant connector and the API): https://www.priva-z.app/partners - Z Pay dashboard (a Z Pay button's payments and alerts): https://www.priva-z.app/zpay/dashboard - Terms: https://www.priva-z.app/terms - Privacy: https://www.priva-z.app/privacy - Acceptable use & compliance: https://www.priva-z.app/compliance - Support: privaz.app@yahoo.com (include the swap ID, never the secret note)