Relayer API Reference
HTTP API endpoints for the ZKMix relayer
Relayer API Reference
The ZKMix relayer exposes an HTTP REST API that clients use to submit withdrawal requests and query relayer status. This document describes all available endpoints, their request and response formats, and error handling.
All endpoints accept and return JSON. The relayer listens on the configured port (default: 8080) and is typically deployed behind a reverse proxy (nginx, Caddy) with TLS termination for production use.
Base URL
https://<relayer-hostname>:<port>For the official ZKMix relayer on devnet:
https://relayer-devnet.zkmix.devAuthentication
The relayer API does not require authentication. Any client can submit a withdrawal request. Rate limiting is applied per IP address to prevent abuse.
Endpoints
POST /relay
Submit a withdrawal proof for the relayer to process. This is the primary endpoint that users and the SDK interact with.
Request Body:
{
"proof": {
"a": [
"0x2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a",
"0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a"
],
"b": [
[
"0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
"0x4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a"
],
[
"0x5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a",
"0x6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a"
]
],
"c": [
"0x7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a",
"0x8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a"
]
},
"root": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"nullifierHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"recipient": "8xQmK4pR7vN2wL5jH3nF9bC6dA1eG0mT8yU4iO2pS7qW",
"poolAddress": "ZKMxPool1SOL111111111111111111111111111111111",
"relayerFee": 3000000
}Request Fields:
| Field | Type | Required | Description |
|---|---|---|---|
proof | object | Yes | Groth16 proof containing a, b, and c points |
proof.a | string[] | Yes | G1 point as two hex-encoded field elements |
proof.b | string[][] | Yes | G2 point as 2x2 hex-encoded field elements |
proof.c | string[] | Yes | G1 point as two hex-encoded field elements |
root | string | Yes | Merkle root the proof was generated against |
nullifierHash | string | Yes | Hash of the nullifier (prevents double-spending) |
recipient | string | Yes | Base58-encoded Solana address to receive funds |
poolAddress | string | Yes | Base58-encoded address of the mixer pool |
relayerFee | number | Yes | Fee to pay the relayer in smallest token units |
Success Response (200 OK):
{
"success": true,
"txSignature": "5K7pQr8sT2nU4vW6xY0zA1bC3dE5fG7hI9jK0lM1nO2pQ3rS4tU5vW6xY7zA8bC",
"message": "Withdrawal relayed successfully",
"blockTime": 1710512400,
"slot": 245678901
}Error Response (4xx/5xx):
{
"success": false,
"error": "NULLIFIER_ALREADY_SPENT",
"message": "The provided nullifier hash has already been used in a previous withdrawal"
}Possible Error Codes:
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | INVALID_PROOF_FORMAT | Proof data is malformed or missing fields |
| 400 | INVALID_RECIPIENT | Recipient is not a valid Solana address |
| 400 | INVALID_POOL | Pool address not recognized or not supported |
| 400 | UNKNOWN_MERKLE_ROOT | The provided root is not in the root history |
| 400 | NULLIFIER_ALREADY_SPENT | This nullifier has been used before |
| 400 | FEE_TOO_LOW | The relayer fee is below the relayer's minimum |
| 400 | FEE_TOO_HIGH | The fee exceeds the pool denomination |
| 422 | PROOF_VERIFICATION_FAILED | The Groth16 proof is invalid |
| 429 | RATE_LIMITED | Too many requests from this IP address |
| 500 | TRANSACTION_FAILED | The transaction was submitted but failed on-chain |
| 500 | RPC_ERROR | Failed to communicate with the Solana RPC node |
| 503 | RELAYER_BUSY | Too many pending transactions, try again later |
| 503 | INSUFFICIENT_BALANCE | Relayer lacks SOL to pay the transaction fee |
GET /status
Returns the current status and capabilities of the relayer. Useful for the SDK to determine whether a relayer is operational and what pools it supports.
Request:
GET /statusNo request body or parameters required.
Response (200 OK):
{
"relayerAddress": "7xKpM3nQr4sT5vW6xY0zA1bC3dE5fG7hI9jK0lM1nO2",
"version": "1.2.0",
"network": "mainnet-beta",
"isReady": true,
"uptime": 172800,
"stats": {
"totalRelayed": 4521,
"last24hRelayed": 187,
"successRate": 0.994
},
"supportedPools": [
{
"address": "ZKMxPool01SOL111111111111111111111111111111111",
"token": "SOL",
"denomination": 100000000,
"denominationFormatted": "0.1 SOL"
},
{
"address": "ZKMxPool1SOL1111111111111111111111111111111111",
"token": "SOL",
"denomination": 1000000000,
"denominationFormatted": "1 SOL"
},
{
"address": "ZKMxPool10SOL11111111111111111111111111111111",
"token": "SOL",
"denomination": 10000000000,
"denominationFormatted": "10 SOL"
}
],
"queueDepth": 2,
"maxQueueDepth": 50
}Response Fields:
| Field | Type | Description |
|---|---|---|
relayerAddress | string | The relayer's Solana address |
version | string | Relayer software version |
network | string | Solana cluster the relayer is connected to |
isReady | boolean | Whether the relayer is accepting requests |
uptime | number | Seconds since the relayer started |
stats | object | Aggregate statistics |
supportedPools | array | Pools this relayer will process withdrawals for |
queueDepth | number | Number of transactions currently being processed |
maxQueueDepth | number | Maximum concurrent transactions |
GET /fees
Returns the current fee schedule for this relayer. Fees can vary by pool and may be adjusted dynamically based on network conditions.
Request:
GET /feesNo request body or parameters required.
Response (200 OK):
{
"feePercentage": 0.3,
"minFee": 10000,
"fees": [
{
"poolAddress": "ZKMxPool01SOL111111111111111111111111111111111",
"token": "SOL",
"denomination": 100000000,
"feeAmount": 300000,
"feeFormatted": "0.0003 SOL",
"feePercentage": 0.3
},
{
"poolAddress": "ZKMxPool1SOL1111111111111111111111111111111111",
"token": "SOL",
"denomination": 1000000000,
"feeAmount": 3000000,
"feeFormatted": "0.003 SOL",
"feePercentage": 0.3
},
{
"poolAddress": "ZKMxPool10SOL11111111111111111111111111111111",
"token": "SOL",
"denomination": 10000000000,
"feeAmount": 30000000,
"feeFormatted": "0.03 SOL",
"feePercentage": 0.3
}
],
"gasCostEstimate": 5000,
"priorityFeeEstimate": 100000,
"lastUpdated": "2025-03-15T14:30:22.451Z"
}Response Fields:
| Field | Type | Description |
|---|---|---|
feePercentage | number | Default fee as a percentage of denomination |
minFee | number | Minimum fee in lamports/token units |
fees | array | Per-pool fee breakdown |
fees[].feeAmount | number | Calculated fee for this pool in smallest units |
fees[].feeFormatted | string | Human-readable fee amount |
gasCostEstimate | number | Estimated Solana transaction fee in lamports |
priorityFeeEstimate | number | Current estimated priority fee in lamports |
lastUpdated | string | ISO 8601 timestamp of last fee recalculation |
GET /health
A lightweight health check endpoint intended for monitoring systems and load balancers.
Request:
GET /healthResponse (200 OK):
{
"status": "healthy",
"solBalance": 2450000000,
"rpcConnected": true,
"timestamp": "2025-03-15T14:30:22.451Z"
}Response (503 Service Unavailable):
{
"status": "unhealthy",
"reason": "RPC connection lost",
"solBalance": 0,
"rpcConnected": false,
"timestamp": "2025-03-15T14:30:22.451Z"
}The health endpoint returns 200 when the relayer is fully operational and 503 when it cannot process requests. Unhealthy conditions include:
- RPC connection failure
- SOL balance too low to pay transaction fees
- Too many pending transactions (queue full)
Request Validation
The relayer performs several validation checks before submitting a transaction to the network:
- Format validation -- All fields are present and correctly formatted. Proof points are valid hex strings of the correct length. Addresses are valid Base58-encoded Solana public keys.
- Pool validation -- The pool address corresponds to a known, active ZKMix pool that this relayer supports.
- Fee validation -- The relayer fee meets the relayer's minimum fee requirement. The fee does not exceed the pool denomination.
- Nullifier check -- The relayer queries the on-chain NullifierSet to verify the nullifier has not been spent. This pre-check avoids submitting transactions that are guaranteed to fail on-chain, saving the relayer gas costs.
- Root check -- The relayer verifies the provided Merkle root matches a known root in the on-chain root history.
These pre-flight checks are performed before the relayer constructs and signs the transaction. If any check fails, the relayer returns an appropriate error response without incurring any on-chain costs.
Rate Limiting
The relayer enforces rate limits to prevent abuse and denial-of-service attacks:
| Limit | Default | Description |
|---|---|---|
| Global requests/minute | 60 | Total requests across all clients |
| Per-IP requests/minute | 10 | Requests from a single IP address |
| Concurrent transactions | 50 | Maximum in-flight transactions |
When rate limited, the relayer returns a 429 status code with a Retry-After header indicating how many seconds the client should wait before retrying:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{
"success": false,
"error": "RATE_LIMITED",
"message": "Rate limit exceeded. Please retry after 30 seconds.",
"retryAfter": 30
}CORS Configuration
The relayer supports CORS to allow browser-based clients (such as the ZKMix web app) to interact with the API directly:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-TypeFor production deployments, it is recommended to restrict Access-Control-Allow-Origin to specific trusted domains rather than using the wildcard *.
SDK Integration
The ZKMix SDK handles all relayer API interaction automatically. You do not need to call these endpoints directly unless you are building a custom integration:
import { ZKMix } from "@zkmix/sdk";
const zkmix = new ZKMix({ connection, cluster: "mainnet-beta" });
// The SDK calls GET /status and GET /fees internally
// to select the best relayer, then calls POST /relay
const result = await zkmix.withdraw({
deposit: savedDepositNote,
recipient: newAddress,
useRelayer: true,
});