Developers

API

Build Cypher’s private payments and swaps into your own app. Keys stay with you: the pool runs on your side, and the API hands back transactions to sign.

Overview

Cypher’s privacy comes from a shielded pool, and only the owner’s key can spend a note in it. So the pool steps, shielding and unshielding, run on your side with Privacy Cash’s SDK, exactly as they do in the app’s browser. The API does the rest: the pool’s live terms, swaps built for the one-time wallets the pool pays, quotes and submission.

Base URLhttps://cypher.tools/api/v1
  • Nothing to sign up for. No keys or accounts. Limits are per IP.
  • Non-custodial. The API never sees a private key or a pool key, and never holds funds.
  • JSON in, JSON out. Amounts are decimal strings in whole units, like "1.5".

Shielded pool

GET/api/v1/poolThe pool’s tokens, sizes, fees and minimums, live.

notes is how many notes each token’s pool holds: the crowd a payout hides in. A withdrawal costs rate × amount + flat, in that token, kept by the relayer. Shielding is free apart from network fees.

response
{
  "program": "9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD",
  "relayer": "https://api3.privacycash.org",
  "tokens": [
    {
      "symbol": "SOL",
      "mint": "So11111111111111111111111111111111111111112",
      "decimals": 9,
      "notes": 889912,
      "minimumWithdrawal": "0.01",
      "withdrawFee": { "rate": 0.0035, "flat": "0.006" }
    },
    …USDC, USDT
  ],
  "shieldFee": "0"
}

To shield and unshield, use Privacy Cash’s SDK (npm i privacycash). In a browser, derive the pool key from the wallet’s signature over Privacy Money account sign in, the message the app uses, so balances match across both.

TypeScript
import { PrivacyCash } from "privacycash";

// The SDK derives your pool key from your wallet's key pair; it never leaves this process
const pool = new PrivacyCash({ RPC_url: process.env.RPC_URL!, owner: keypair });

// Shield: a public deposit, the last thing on chain tied to you
await pool.deposit({ lamports: 2_000_000_000 });
const { lamports } = await pool.getPrivateBalance();

// Unshield: proven here, sent by the relayer; nothing ties it to the deposit
await pool.withdraw({ lamports: 500_000_000, recipientAddress: recipient });

A private swap

What the Swap page does, from your own code: the pool pays a one-time wallet, Cypher builds that wallet’s swap to the recipient, and you sign it with the one-time key. Your wallet appears nowhere in it.

TypeScript
import { PrivacyCash } from "privacycash";
import {
  createKeyPairSignerFromPrivateKeyBytes,
  getBase64EncodedWireTransaction,
  getBase64Encoder,
  getTransactionDecoder,
  signTransaction,
} from "@solana/kit";

const API = "https://cypher.tools/api/v1";
const headers = { "content-type": "application/json", "x-rpc-url": process.env.RPC_URL! };
const pool = new PrivacyCash({ RPC_url: process.env.RPC_URL!, owner: keypair });

// 1. A one-time wallet for this swap, paid out of the pool.
//    Keep its seed until it's empty: it holds funds in between.
const seed = crypto.getRandomValues(new Uint8Array(32));
const once = await createKeyPairSignerFromPrivateKeyBytes(seed);
await pool.withdraw({ lamports: 1_000_000_000, recipientAddress: once.address });

// 2. Cypher builds the swap, from the one-time wallet to the recipient
const built = await fetch(`${API}/swap`, {
  method: "POST",
  headers,
  body: JSON.stringify({ from: once.address, to: recipient, inputMint: "So11111111111111111111111111111111111111112", outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", amount: "0.98" }),
}).then((r) => r.json());
if (built.error) throw new Error(built.error.message);

// 3. Sign as the one-time wallet and submit
const tx = getTransactionDecoder().decode(getBase64Encoder().encode(built.transaction));
const signed = await signTransaction([once.keyPair], tx);
await fetch(`${API}/submit`, {
  method: "POST",
  headers,
  body: JSON.stringify({ transaction: getBase64EncodedWireTransaction(signed) }),
});

Swap a little less than arrives: the one-time wallet pays the network fee and opens the recipient’s token account. Then send whatever SOL it has left on with a plain transfer, so it ends empty.

Rate limits

Shared RPC1/ secondChain reads, simulation and submission run on Cypher’s RPC.
Your RPC10/ secondSend x-rpc-url and every chain call for that request goes to your node.

Limits are per IP and apply to every endpoint. Your RPC URL is used for that one request and is never stored or logged. It must be https and publicly reachable. Every response reports where you stand:

x-ratelimit-limitheaderalwaysRequests a second for this tier: 1 or 10.
x-ratelimit-remainingheaderalwaysRequests left in the current second.
x-cypher-rpcheaderalwaysWhich RPC served the request: shared or own.
retry-afterheaderon 429Seconds to wait before trying again.

Relay send

POST/api/v1/sendA send split through throwaway relays, without the pool.

Cypher’s earlier route, for any SPL token: the amount splits through throwaway relays with decoys mixed in, all in one transaction that comes back signed by its relays. It makes a trail harder to follow, but the sender still signs it, so it isn’t the pool’s privacy. Prefer the pool for SOL, USDC and USDT.

fromaddressrequiredThe sender. Signs and pays the fees.
toaddressrequiredThe recipient.
amountstringrequiredHow much to send, in whole units: "1.5" SOL, "25" USDC.
mintaddressoptionalAn SPL token mint. Leave it out to send SOL.
levelstring"standard""light", "standard" or "max": how many relays, parts and decoys.
versionnumber11 is a 4 KB transaction. 0 fits older libraries in 1,232 bytes, with a smaller route.
import {
  createKeyPairSignerFromBytes,
  getBase64EncodedWireTransaction,
  getBase64Encoder,
  getTransactionDecoder,
  signTransaction,
} from "@solana/kit";

const API = "https://cypher.tools/api/v1";
const headers = { "content-type": "application/json", "x-rpc-url": process.env.RPC_URL! };
const wallet = await createKeyPairSignerFromBytes(secretKey);

// 1. Build: Cypher routes the send, and its throwaway relays sign it
const built = await fetch(`${API}/send`, {
  method: "POST",
  headers,
  body: JSON.stringify({ from: wallet.address, to: recipient, amount: "1.5", level: "standard" }),
}).then((r) => r.json());
if (built.error) throw new Error(built.error.message);

// 2. Sign: add your signature as the sender and fee payer
const tx = getTransactionDecoder().decode(getBase64Encoder().encode(built.transaction));
const signed = await signTransaction([wallet.keyPair], tx);

// 3. Submit, through this API or your own RPC
const { signature } = await fetch(`${API}/submit`, {
  method: "POST",
  headers,
  body: JSON.stringify({ transaction: getBase64EncodedWireTransaction(signed) }),
}).then((r) => r.json());
response
{
  "transaction": "gQUAAQ8AAAC5Lfk1Rvm80+iSOVO9baQt…",
  "encoding": "base64",
  "version": 1,
  "lastValidBlockHeight": "440181372",
  "signer": "5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9",
  "level": "standard",
  "plan": { "relays": 4, "chunks": 3, "decoys": 3 },
  "size": 972,
  "limit": 4096
}

Swap

POST/api/v1/swapA swap that can land in another wallet.

The best route for the amount, built so the output goes straight to the recipient. It reads on chain as your swap, with no transfer to them. It comes back unsigned.

fromaddressrequiredThe wallet that pays and signs.
toaddressfromWhere the output lands. To another wallet, the output can’t be SOL.
inputMintaddressrequiredThe token you pay with. SOL is So111…1112.
outputMintaddressrequiredThe token they receive.
amountstringrequiredHow much of the input, in whole units.
slippageBpsnumber50Slippage tolerance in basis points, 1 to 5000.
request
curl https://cypher.tools/api/v1/swap \
  -H "content-type: application/json" \
  -d '{
    "from": "<your wallet>",
    "to": "<recipient>",
    "inputMint": "So11111111111111111111111111111111111111112",
    "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "0.25",
    "slippageBps": 50
  }'
response
{
  "transaction": "AQAAAAAAAAAAAAAAAAAAAAAA…",
  "encoding": "base64",
  "version": 0,
  "lastValidBlockHeight": "440181839",
  "signer": "<your wallet>",
  "recipient": "<recipient>",
  "quote": {
    "inAmount": "0.25",
    "outAmount": "25.2753",
    "minOutAmount": "25.148924",
    "priceImpactPct": 0,
    "slippageBps": 50,
    "route": [{ "venue": "Kipseli", "percent": 100 }]
  }
}

Quote

GET/api/v1/quoteWhat a swap would return, without building it.

Takes inputMint, outputMint, amount and slippageBps as query parameters, and returns the swap’s quote object. Try it:

GET

Prices SOL → USDC through the shared RPC, so it counts toward this page’s 1 request a second.

Submit and status

POST/api/v1/submitSends a signed transaction.
transactionstringrequiredThe signed transaction, base64. Returns { "signature" }.
GET/api/v1/statusWhere a transaction has got to.
signaturestringrequiredQuery parameter. status is processed, confirmed, finalized, failed or unknown.
response
{
  "signature": "5LPA6GqDrCN8kCqpfiMS6J2bNjWHyzQt…",
  "status": "finalized",
  "slot": "462156087",
  "err": null
}

Errors

Errors come back with a status code and a body like this:

429
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limited: 1 request a second on the shared RPC. Pass your own in x-rpc-url for 10.",
    "retryAfterMs": 783
  }
}
400bad_address · bad_amount · bad_level · bad_version · bad_slippage · bad_json · same_wallet · same_mint · bad_rpc_urlSomething in the request isn’t right. The message says what.400insufficient_fundsfrom can’t cover the amount, the fees and a 0.005 SOL reserve.404no_routeNo swap route for this pair and amount right now.422simulation_failed · preflight_failed · build_failedThe transaction would fail on chain. Logs come back when the RPC gives them.429rate_limitedOver your rate limit. Wait for retry-after and try again.502rpc_error · rpc_unreachableThe RPC failed or couldn’t be reached. With your own RPC, it’s yours.502relayer_unreachableThe pool’s relayer didn’t answer /pool. Try again shortly.503no_shared_rpcThis server has no shared RPC configured. Pass your own.

Before you sign

  • Keep one-time keys until they’re empty. A one-time wallet holds funds between the pool’s payout and the swap. Store its seed until it has delivered everything.
  • It expires. Transactions carry a recent blockhash. Sign and submit before lastValidBlockHeight, about a minute, or build again.
  • Version 1 needs a current library. 4 KB transactions decode with @solana/kit. If yours can’t read them, ask for version: 0.
  • Check what you sign. Simulate it on your own RPC if you want to see every balance change first.

The proof hides which note paid; amounts and timing can still give it away. The habits in the docs’ Good practice apply here too.

Cypher

Private sends and swaps on Solana, through a zero-knowledge shielded pool.

No custody. No accounts. Proven, not traced.cypher.tools · Built on Solana