Control your entire RelAI integration programmatically.
Manage keys, budgets, usage, and payments — all from one API.
Everything you can do in the dashboard — you can automate here.
This is the control plane for your entire RelAI integration.
Why Management API?
Use the Management API when:
What you can control
Quick start
Create a service key and start managing your integration programmatically:
curl -X POST https://api.relai.fi/v1/keys -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" -d '{"label": "production"}'{ "key": "sk_live_a1b2c3d4...", "label": "production", "active": true }Use that key to control your account programmatically:
curl -X POST https://api.relai.fi/v1/apis -H "X-Service-Key: sk_live_..." -H "Content-Type: application/json" -d '{
"name": "My Prediction API",
"baseUrl": "https://inference.example.com",
"network": "base",
"merchantWallet": "0xYourWallet",
"endpoints": [
{ "path": "/v1/predict", "method": "post", "usdPrice": 0.05 }
]
}'Automate everything
AI agents can use the Management API directly via MCP — Claude, Cursor, and Windsurf can call all management tools without a browser or dashboard. See the AI Agent / MCP section below.
Built for production
Authentication
The Management API uses two schemes depending on the operation:
Used on /v1/keys endpoints to create and revoke service keys. This is the same token you get from logging in at relai.fi.
Used on all /v1/apis endpoints. Suitable for CI/CD and server-to-server calls — no browser session needed.
Get a service key
The easiest way is through the dashboard: Dashboard → Service Keys → New key. The key is shown once — copy it immediately into your environment or secrets manager.
You can also create one via API — log in to relai.fi to get a JWT, then:
curl -X POST https://api.relai.fi/v1/keys \
-H "Authorization: Bearer <your-jwt>" \
-H "Content-Type: application/json" \
-d '{"label": "production"}'{
"key": "sk_live_a1b2c3d4e5f6...",
"label": "production",
"active": true,
"createdAt": "2025-01-01T00:00:00.000Z",
"note": "Store this key securely — it will not be shown again."
}The plaintext key is shown once. Store it in an environment variable or secrets manager immediately.
Agent self-setup (fully autonomous)
Agents can provision their own service key with zero human involvement — no dashboard visit, no JWT, no copy-paste. The agent uses its own keypair (Solana ed25519 or EVM secp256k1) to sign a challenge and receive a service key. The wallet address becomes the agent's permanent identity on RelAI.
Request challenge
POST your public key → receive a message to sign
Sign message
Sign with your private key (nacl/ethers/web3)
Get service key
POST signature → receive sk_live_... stored forever
from solders.keypair import Keypair
import base58, requests
BASE = "https://api.relai.fi/mcp/management/bootstrap/agent"
kp = Keypair() # or: Keypair.from_bytes(bytes.fromhex(os.environ["AGENT_PRIVKEY"]))
# Step 1 — request challenge
msg = requests.post(BASE, json={"publicKey": str(kp.pubkey())}).json()["message"]
# Step 2 — sign
sig = base58.b58encode(bytes(kp.sign_message(msg.encode()))).decode()
# Step 3 — get service key (runs once, then store the key)
sk = requests.post(BASE, json={
"publicKey": str(kp.pubkey()),
"signature": sig,
"message": msg,
"label": "my-agent",
}).json()["key"]
print(f"Service key: {sk}") # → sk_live_...import { ethers } from "ethers";
const BASE = "https://api.relai.fi/mcp/management/bootstrap/agent";
const wallet = ethers.Wallet.createRandom(); // or load from env
// Step 1 — request challenge
const { message } = await fetch(BASE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ publicKey: wallet.address }),
}).then(r => r.json());
// Steps 2+3 — sign and get key
const signature = await wallet.signMessage(message);
const { key } = await fetch(BASE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ publicKey: wallet.address, signature, message, label: "my-agent" }),
}).then(r => r.json());
console.log("Service key:", key); // → sk_live_...Run once, store forever. Save the keypair (private key) and the returned sk_live_... securely. On every subsequent start the agent loads both from env/secrets — no server calls needed until the key is revoked.
List and revoke keys
curl https://api.relai.fi/v1/keys \
-H "Authorization: Bearer <your-jwt>"curl -X DELETE https://api.relai.fi/v1/keys/sk_live_... \
-H "Authorization: Bearer <your-jwt>"API management
All endpoints below require the X-Service-Key header.
POST/v1/apis — Create an API
Creates a new x402-monetised API. Optionally set initial endpoint pricing in the same request via the endpoints array.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Display name shown in the marketplace |
| baseUrl | string | yes | Target server base URL (e.g. https://my-api.example.com) |
| description | string | no | Short description |
| network | string | no | Payment network. Default: solana |
| facilitator | string | no | Facilitator. Default: relai |
| x402Version | number | no | 1 or 2. Default: 2 |
| merchantWallet | string | no | Wallet address receiving payments |
| subdomain | string | no | Custom subdomain under relai.fi — must be unique |
| websiteUrl | string | no | Link to your API docs or homepage |
| logoUrl | string | no | URL of the API logo |
| ownerEmail | string | no | Contact email for the API owner |
| endpoints | array | no | Initial pricing (see below) |
endpoints[] item fields:
| Field | Type | Required | Description |
|---|---|---|---|
| path | string | yes | Endpoint path, e.g. /v1/predict |
| method | string | no | HTTP method: get | post | put | patch | delete. Default: get |
| usdPrice | number | no | Price per call in USD. Default: 0.01 |
| description | string | no | Endpoint description |
| enabled | boolean | no | Whether the endpoint is active. Default: true |
curl -X POST https://api.relai.fi/v1/apis \
-H "X-Service-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "My Prediction API",
"baseUrl": "https://inference.example.com",
"description": "Pay-per-call ML inference",
"network": "base",
"merchantWallet": "0xYourWalletAddress",
"endpoints": [
{ "path": "/v1/predict", "method": "post", "usdPrice": 0.05 },
{ "path": "/v1/status", "method": "get", "usdPrice": 0.001 }
]
}'{
"apiId": "1751234567890",
"name": "My Prediction API",
"description": "Pay-per-call ML inference",
"baseUrl": "https://inference.example.com",
"subdomain": null,
"network": "base",
"facilitator": "relai",
"x402Version": 2,
"status": "approved",
"merchantWallet": "0xYourWalletAddress",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}GET/v1/apis — List APIs
Returns all APIs owned by the authenticated service key's user.
curl https://api.relai.fi/v1/apis \
-H "X-Service-Key: sk_live_..."{
"apis": [
{ "apiId": "1751234567890", "name": "My Prediction API", ... }
]
}GET/v1/apis/:apiId — Get an API
curl https://api.relai.fi/v1/apis/1751234567890 \
-H "X-Service-Key: sk_live_..."PATCH/v1/apis/:apiId — Update API metadata
All fields are optional. Only the provided fields are updated. Updatable fields: name, description, baseUrl, merchantWallet, websiteUrl, logoUrl.
curl -X PATCH https://api.relai.fi/v1/apis/1751234567890 \
-H "X-Service-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "description": "Updated description" }'DELETE/v1/apis/:apiId — Delete an API
curl -X DELETE https://api.relai.fi/v1/apis/1751234567890 \
-H "X-Service-Key: sk_live_..."{ "success": true, "apiId": "1751234567890" }Pricing management
Pricing is configured per endpoint (path + method). All monetary values are in USD.
GET/v1/apis/:apiId/pricing
curl https://api.relai.fi/v1/apis/1751234567890/pricing \
-H "X-Service-Key: sk_live_..."{
"apiId": "1751234567890",
"endpoints": [
{ "path": "/v1/predict", "method": "post", "usdPrice": 0.05, "network": "base", "enabled": true },
{ "path": "/v1/status", "method": "get", "usdPrice": 0.001,"network": "base", "enabled": true }
]
}PUT/v1/apis/:apiId/pricing
Replaces all endpoint pricing for an API. To update a single endpoint, send the full list including the unchanged entries.
curl -X PUT https://api.relai.fi/v1/apis/1751234567890/pricing \
-H "X-Service-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"endpoints": [
{ "path": "/v1/predict", "method": "post", "usdPrice": 0.10 },
{ "path": "/v1/status", "method": "get", "usdPrice": 0.001 }
]
}'{ "success": true, "apiId": "1751234567890", "updated": 2 }Analytics
Monitor revenue and individual payments for any API you own.
GET/v1/apis/:apiId/stats
Aggregated totals: number of paid requests and total revenue in USD.
curl https://api.relai.fi/v1/apis/1751234567890/stats \
-H "X-Service-Key: sk_live_..."{
"apiId": "1751234567890",
"totalRequests": 142,
"totalRevenue": 7.1,
"currency": "USD"
}GET/v1/apis/:apiId/payments
Paginated list of individual payments. Results are sorted newest-first.
| Query param | Type | Default | Description |
|---|---|---|---|
| limit | number | 50 | Number of records to return (max 500) |
| cursor | string | — | Transaction ID — return records after this entry |
| from | string | — | ISO date — filter from this timestamp |
| to | string | — | ISO date — filter up to this timestamp |
curl "https://api.relai.fi/v1/apis/1751234567890/payments?limit=20&from=2025-01-01" \
-H "X-Service-Key: sk_live_..."{
"apiId": "1751234567890",
"payments": [
{
"transaction": "0xabc123...",
"path": "/v1/predict",
"method": "post",
"amount": 0.05,
"currency": "USD",
"network": "base",
"status": "settled",
"success": true,
"payer": "user_abc",
"createdAt": "2025-06-01T12:34:56.000Z"
}
],
"nextCursor": "0xabc123..."
}To paginate, pass the returned nextCursor value as the cursor query param in the next request. When nextCursor is null, you have reached the end.
GET/v1/apis/:apiId/logs
Usage logs in the exact same format as the Usage Logs tab in the dashboard — fields include timestamp, method, path, status, cost, duration, and transaction.
| Query param | Type | Default | Description |
|---|---|---|---|
| limit | number | 50 | Records to return (max 500) |
| cursor | string | — | Transaction ID — return records after this entry |
| from | string | — | ISO date — filter from this timestamp |
| to | string | — | ISO date — filter up to this timestamp |
curl "https://api.relai.fi/v1/apis/1751234567890/logs?limit=20&from=2025-01-01" \
-H "X-Service-Key: sk_live_..."{
"items": [
{
"id": "1751234567890-0xabc123...",
"timestamp": "2025-06-01T12:34:56.000Z",
"method": "POST",
"path": "/v1/predict",
"status": "settled",
"cost": 0.05,
"currency": "USD",
"duration": 312,
"transaction": "0xabc123...",
"network": "base",
"success": true,
"payer": "user_abc"
}
],
"nextCursor": "0xabc123..."
}AI Agent / MCP
The Management API is exposed as an MCP (Model Context Protocol) server at POST /mcp/management. AI agents (Claude, Cursor, custom LLM pipelines) can call all management tools directly — no browser, no dashboard.
The agent authenticates with X-Service-Key — the same key used for the REST API. No JWT or login flow needed.
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"relai-management": {
"url": "https://api.relai.fi/mcp/management",
"headers": {
"X-Service-Key": "sk_live_..."
}
}
}
}Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"relai-management": {
"serverUrl": "https://api.relai.fi/mcp/management",
"headers": {
"X-Service-Key": "sk_live_..."
}
}
}
}Cursor
Add to .cursor/mcp.json in your project (or global ~/.cursor/mcp.json):
{
"mcpServers": {
"relai-management": {
"url": "https://api.relai.fi/mcp/management",
"headers": {
"X-Service-Key": "sk_live_..."
}
}
}
}Available tools
Once connected, the agent has access to these tools:
| Tool | Description |
|---|---|
| create_api | Create a new x402-monetised API (status: pending) |
| list_apis | List all APIs owned by this service key |
| get_api | Get details of a specific API |
| update_api | Update API metadata (name, baseUrl, description…) |
| delete_api | Permanently delete an API |
| set_pricing | Replace endpoint pricing configuration |
| get_pricing | Get current endpoint pricing |
| get_stats | Get aggregated revenue and request count |
| get_logs | Get usage logs with date filtering and pagination |
Example prompt: "Create a monetised API for https://ml.myapp.com, charge $0.05 per /v1/predict call on Base network" — the agent will call create_api and set_pricing automatically.
Full workflow example
End-to-end: obtain a key, create an API, set pricing, verify.
# 1. Create a service key (requires your user JWT)
KEY=$(curl -s -X POST https://api.relai.fi/v1/keys \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"label":"ci"}' | jq -r .key)
# 2. Create an API
API_ID=$(curl -s -X POST https://api.relai.fi/v1/apis \
-H "X-Service-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Weather API",
"baseUrl": "https://weather.example.com",
"network": "base",
"merchantWallet": "0xYourWallet"
}' | jq -r .apiId)
# 3. Set pricing
curl -X PUT "https://api.relai.fi/v1/apis/$API_ID/pricing" \
-H "X-Service-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"endpoints":[{"path":"/forecast","method":"get","usdPrice":0.002}]}'
# 4. Verify
curl "https://api.relai.fi/v1/apis/$API_ID" \
-H "X-Service-Key: $KEY"Error reference
All error responses share the same shape:
{ "error": "Human-readable description." }| Status | Meaning |
|---|---|
| 400 | Validation error — missing or invalid fields |
| 401 | Missing or invalid credentials |
| 403 | Authenticated but not authorised for this resource |
| 404 | Resource not found |
| 409 | Conflict — e.g. subdomain already taken |
| 500 | Internal server error |
Supported networks
Only networks with EIP-3009 (transferWithAuthorization) compliant tokens are supported. For skale-base, multiple tokens are accepted automatically — no token address configuration needed.
| network value | Chain | Token |
|---|---|---|
| solana | Solana Mainnet | USDC |
| base | Base Mainnet | USDC |
| base-sepolia | Base Sepolia | USDC |
| skale-base | SKALE Base | USDC, USDT, skUSD, ETH, BTC, … |
| polygon | Polygon | USDC |
| avalanche | Avalanche C-Chain | USDC |
| ethereum | Ethereum Mainnet | USDC |
See the Networks page for full details including chain IDs, token addresses, and gas sponsoring.
Agent Discovery
RelAI exposes machine-readable endpoints so AI agents and tooling can discover the platform automatically — without any hardcoded configuration.
| URL | Format | Purpose |
|---|---|---|
| /.well-known/ai-plugin.json | JSON | OpenAI plugin manifest — discovered by ChatGPT, LangChain, AutoGPT and compatible agents |
| /.well-known/agent.json | JSON | Agent Protocol manifest — MCP endpoint, capabilities, auth info |
| /llms.txt | Plain text | Full LLM context — how to use the API, agent bootstrap flow, MCP config |
| /openapi.json | OpenAPI 3.1 | Full Management API spec — importable directly into agent frameworks |
How agents discover RelAI
Agents with internet access will find these endpoints automatically:
- Agents that crawl
/.well-known/will findai-plugin.jsonandagent.json - LLM-aware crawlers check
/llms.txtfor plain-text context (similar torobots.txt) - Agents using OpenAPI tooling can import
/openapi.jsondirectly - The
<head>of every RelAI page includes<link rel="alternate" href="/llms.txt">for HTML-aware agents
Manual configuration
You can also point your agent directly at the MCP server or REST API — see the AI Agent / MCP and Agent self-setup sections above.
The /llms.txt file contains a complete guide including the agent bootstrap flow, MCP config snippet, and all Management API endpoints — making it the single source of truth for an agent discovering RelAI for the first time.
Bridge
The Bridge API lets agents and backends move USDC between supported networks programmatically. Every bridge transfer is an x402 payment on the source chain followed by a USDC payout on the destination chain. All endpoints require an X-Service-Key header.
Mainnet bridge is capped at $1 USDC during beta. Testnet (Solana Devnet ↔ SKALE Base Sepolia) allows up to $10 USDC.
List directions
Returns all enabled bridge directions with token addresses and network metadata.
curl https://api.relai.fi/v1/bridge/networks -H "X-Service-Key: sk_live_..."
// Response
{
"directions": [
{
"id": "solana-to-skale-base",
"from": { "id": "solana", "label": "Solana", "type": "solana", "token": "EPjFWdd5...", "decimals": 6, "caip2": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" },
"to": { "id": "skale-base", "label": "SKALE Base", "type": "evm", "token": "0x85889c8c...", "decimals": 6, "caip2": "eip155:1187947933" }
}
]
}Quote
Returns the output amount after the 0.1% bridge fee. Use this before executing a bridge to show the user the exact amount they will receive.
curl "https://api.relai.fi/v1/bridge/quote?amount=1&from=solana&to=skale-base" -H "X-Service-Key: sk_live_..."
// Response
{
"from": "solana",
"fromLabel": "Solana",
"to": "skale-base",
"toLabel": "SKALE Base",
"inputAmount": 1000000,
"inputUsd": 1,
"outputAmount": 999000,
"outputUsd": 0.999,
"fee": 1000,
"feeBps": 10
}Balances
Returns current USDC liquidity on all enabled networks. Bridge execution will be rejected with 503 if the destination has insufficient funds.
curl https://api.relai.fi/v1/bridge/balances -H "X-Service-Key: sk_live_..."
// Response
{
"solana": { "atomic": 5000000, "usd": 5, "label": "Solana" },
"skale-base": { "atomic": 8000000, "usd": 8, "label": "SKALE Base" },
"solana-devnet": { "atomic": 50000000, "usd": 50, "label": "Solana Devnet" },
"skale-base-sepolia": { "atomic": 30000000, "usd": 30, "label": "SKALE Base Sepolia" }
}Execute bridge
Initiates a bridge transfer. The endpoint uses the x402 protocol — on the first call it returns a 402 Payment Required response. Your x402-compatible wallet or SDK signs the transaction and retries with an X-PAYMENT header. The bridge then pays out on the destination chain and returns both transaction hashes.
| Field | Type | Description |
|---|---|---|
| amount | string | number | Amount in USDC (e.g. "0.5" or 0.5). Max $1 mainnet, $10 testnet. |
| destinationWallet | string | Recipient address on the destination network (EVM 0x… or Solana base58). |
// Using @relai-fi/x402 SDK (handles 402 automatically)
import { createX402Client } from '@relai-fi/x402';
const client = createX402Client({ solanaWallet, solanaRpcUrl });
const result = await client.fetch('https://api.relai.fi/v1/bridge/solana-to-skale-base', {
method: 'POST',
headers: { 'X-Service-Key': 'sk_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: '0.5', destinationWallet: '0xYourEvmAddress' }),
});
// Response
{
"success": true,
"direction": "solana-to-skale-base",
"from": "solana",
"to": "skale-base",
"destinationWallet": "0xYourEvmAddress",
"amountOut": 499500,
"amountOutUsd": 0.4995,
"txHash": "0xabc...",
"explorerUrl": "https://skale-base-explorer.skalenodes.com/tx/0xabc...",
"paymentTxHash": "5Kq7...",
"paymentExplorerUrl": "https://solscan.io/tx/5Kq7..."
}Supported directions
| direction | From | To | Environment |
|---|---|---|---|
| solana-to-skale-base | Solana | SKALE Base | Mainnet |
| skale-base-to-solana | SKALE Base | Solana | Mainnet |
| solana-devnet-to-skale-base-sepolia | Solana Devnet | SKALE Base Sepolia | Testnet |
| skale-base-sepolia-to-solana-devnet | SKALE Base Sepolia | Solana Devnet | Testnet |
Shielded Links
The Shielded Links API lets agents create and redeem privacy-preserving USDC payment links programmatically. Payment details (amount, sender) are hidden on-chain using zero-knowledge cryptography; only the holder of the link secret can redeem. All endpoints require an X-Service-Key header.
Shielded Links are currently in the testing phase on solana-devnet, base-sepolia, and skale-base-sepolia. Mainnet support ships after the testnet trials complete.
The agent must own the wallet that signs the on-chain deposit (EVM: EIP-3009 authorization; Solana: deposit instruction). The backend relays the withdraw on-chain once a valid zk proof is submitted.
Pool config
Returns the shielded pool configuration (pool address for EVM, program ids for Solana, USDC token, denomination, fee bps). Pass the target network via ?network= — one ofsolana-devnet, base-sepolia, skale-base-sepolia.
curl "https://api.relai.fi/v1/shielded-links/config?network=solana-devnet" \
-H "X-Service-Key: sk_live_..."
// Response (Solana)
{
"shieldedLink": true,
"nativeSolanaShielded": true,
"settlementNetwork": "solana-devnet",
"programId": "8xuGeea4iPycYVxLeWqzmFB6br9cYfWJMurosK2hAiNS",
"verifierProgramId": "7HArpyHxmVAoC8Ra9Dzw4Z74mgLbxh2Qm1N7rydgb8ZG",
"usdcMint": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
"rpcUrl": "https://api.devnet.solana.com",
"issuerFeeBps": 500
}Create link
Allocates a draft shielded link the agent will fund on-chain. The response includesshieldedLinkId and shieldedLinkPayload — share only the payload with the intended recipient, never the backend secrets.
| field | type | description |
|---|---|---|
| settlementNetwork | string | solana-devnet | base-sepolia | skale-base-sepolia |
| from | string | Agent wallet address that will sign the deposit |
| value | number | Recipient amount in USDC atomic units (e.g. 1000000 = $1.00) |
| feeAmount | number | Privacy fee (5% of value, rounded up) |
| totalAmount | number | value + feeAmount, charged to the issuer |
| validBefore | number | Unix seconds expiry (future timestamp) |
| description | string? | Optional short note (≤ 100 chars) |
curl -X POST https://api.relai.fi/v1/shielded-links \
-H "X-Service-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"settlementNetwork": "solana-devnet",
"from": "YourAgentSolanaPubkey...",
"value": 1000000,
"feeAmount": 50000,
"totalAmount": 1050000,
"validBefore": 1735689600,
"description": "Agent payout"
}'
// Response
{
"shieldedLink": true,
"shieldedLinkId": "aH9...",
"shieldedLinkPayload": "relai:shielded:...",
"status": "draft",
"settlementNetwork": "solana-devnet",
"value": 1000000,
"feeAmount": 50000,
"totalAmount": 1050000,
"validBefore": 1735689600
}Fund link
After the agent broadcasts the deposit transaction with its wallet, report the commitment + tx hash so the backend marks the link as funded and starts tracking the Merkle root.
// EVM body
{
"network": "base-sepolia",
"commitment": "0x...", // 32-byte hex
"depositTxHash": "0x...",
"poolAddress": "0x6b513C33909AFe31170256Ce90B93eaBfDa867e5",
"fundedBy": "0xAgentWallet...",
"authorization": { /* EIP-3009 authorize struct */ },
"signature": "0x..."
}
// Solana body
{
"network": "solana-devnet",
"commitment": "0x...", // 32-byte hex
"depositTxHash": "5Kq7...",
"fundedBy": "AgentSolPubkey...",
"nullifier": "0x..." // returned with the link payload
}Redeem flow
A claimant agent retrieves the link metadata, fetches proof inputs, registers its payout intent, generates a zk proof locally, and then submits the proof to release the on-chain withdraw.
GET /v1/shielded-links/:linkId?network=...— fetch current stateGET /v1/shielded-links/:linkId/proof-input?network=...— get root + public signals (plus ASP witness & circuit URLs on V4 pools)POST /v1/shielded-links/:linkId/redeem-intent— lock recipient + targetNetwork- Generate zk proof locally with the link secret (snarkjs with the circuit artefacts from
circuitArtifacts) POST /v1/shielded-links/:linkId/execute-withdraw— submit proof, backend relays on-chain
Privacy Pools (V4 pools). When contractVersion in the proof-input response is "v4", the withdraw verifier requires a 7-input Groth16 proof that also proves ASP (Association Set Provider) inclusion. The proof-input endpoint returns both the pool Merkle witness and the ASP witness, plus the exact circuit artefact URLs to load withsnarkjs.groth16.fullProve. Public-signal order is fixed:[root, aspRoot, nullifier, denomination, recipient, relayer, relayerFee]. If aspReady is false, the commitment is not yet in the latest ASP snapshot — wait ASP_DEBOUNCE_MS (~10s after fund) and poll again.
{
"shieldedLink": true,
"shieldedLinkId": "aH9...",
"settlementNetwork": "base-sepolia",
"contractVersion": "v4",
"poolAddress": "0x753Ae2Cd116e057AB4C05D587992DF7593f7F857",
"verifierAddress": "0xe030aBDd3A23849B99FBAA3bf48513111d88856a",
"commitment": "0x...",
"root": "0x...",
"treeDepth": 20,
"pathElements": ["0x...", ...],
"pathIndices": [0, 1, ...],
"merkleTreeReady": true,
"aspRequired": true,
"aspReady": true,
"asp": {
"root": "0x...",
"leafIndex": 3,
"leafCount": 12,
"depth": 20,
"pathElements": ["0x...", ...],
"pathIndices": [0, 1, ...],
"publishedAt": "2026-04-22T12:34:56.000Z"
},
"circuitArtifacts": {
"version": "shielded-withdraw-asp-v1",
"proofSystem": "groth16",
"curve": "bn128",
"wasmUrl": "https://relai.fi/zk/shielded-withdraw-asp/withdraw.wasm",
"zkeyUrl": "https://relai.fi/zk/shielded-withdraw-asp/withdraw.zkey",
"verificationKeyUrl": "https://relai.fi/zk/shielded-withdraw-asp/verification_key.json",
"publicSignalOrder": [
"root","aspRoot","nullifier","denomination","recipient","relayer","relayerFee"
]
}
}Node.js example — generate the proof locally with snarkjs:
import * as snarkjs from "snarkjs";
// 1. Fetch proof input from the Management API.
const pi = await fetch(
`${BASE}/v1/shielded-links/${linkId}/proof-input?network=base-sepolia`,
{ headers: { "X-Service-Key": SK } },
).then(r => r.json());
if (pi.aspRequired && !pi.aspReady) {
// Commitment not yet in the latest ASP snapshot — retry after ~10s.
throw new Error(`asp_not_ready: ${pi.aspBlockedReason}`);
}
// 2. Build circuit inputs. Note: for V4 we pass BOTH the pool Merkle path
// and the ASP Merkle path. The ordering of public signals is controlled
// by the circuit — you do NOT hand-assemble them here.
const { secret, blinding, nonce } = linkSecretMaterial; // from link payload
const input = {
// private
secret, blinding, nonce,
pathElements: pi.pathElements,
pathIndices: pi.pathIndices,
aspPathElements: pi.asp.pathElements,
aspPathIndices: pi.asp.pathIndices,
// public
root: pi.root,
aspRoot: pi.asp.root,
denomination: pi.denomination,
recipient, // must match redeem-intent target
relayer: "0x0000000000000000000000000000000000000000",
relayerFee: "0",
};
// 3. Generate proof using the circuit URLs the backend advertised.
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
input,
pi.circuitArtifacts.wasmUrl,
pi.circuitArtifacts.zkeyUrl,
);
// 4. Submit to execute-withdraw. For V4 the body MUST include aspRoot.// EVM body (V4 / Privacy Pools)
{
"network": "base-sepolia",
"targetAddress": "0xRecipient...",
"targetNetwork": "base-sepolia",
"proofRequest": {
"proof": "0x...",
"publicSignals": {
"root": "0x...",
"aspRoot": "0x...", // NEW — required on V4 pools
"nullifier": "0x...",
"recipient": "0xRecipient...",
"relayer": "0x0000000000000000000000000000000000000000",
"relayerFee": "0",
"denomination":"1000000"
}
}
}
// EVM body (V3, legacy) — no aspRoot, 6 public signals.
{
"network": "base-sepolia",
"targetAddress": "0xRecipient...",
"targetNetwork": "base-sepolia",
"proofRequest": {
"proof": "0x...",
"publicSignals": {
"root": "0x...", "nullifier": "0x...",
"recipient": "0xRecipient...",
"relayer": "0x0000000000000000000000000000000000000000",
"relayerFee": "0", "denomination": "1000000"
}
}
}
// Solana body (V4 = verifier program deployed, aspRoot required)
{
"network": "solana-devnet",
"targetAddress": "RecipientSolPubkey...",
"targetNetwork": "solana-devnet",
"nullifier": "0x...",
"root": "0x...",
"aspRoot": "0x...",
"proof": "0x...",
"recipient": "RecipientSolPubkey...",
"relayerFee": "0"
}
// Response
{
"shieldedLinkId": "aH9...",
"status": "redeemed",
"payoutTxHash": "0x...",
"payoutExplorerUrl": "https://sepolia.basescan.org/tx/0x...",
"executionMode": "proof-verified"
}The backend rate-limits shielded redeem attempts per link + target address to prevent brute-forcing. Invalid proofs may trigger 429 with Retry-After. On V4 pools a withdraw without aspRoot fails with400 shielded_asp_root_missing.
Shielded Payment Requests
Reverse-direction shielded payment: the seller issues an opaque quote, the buyer deposits into a Privacy Pool V4.1 with a Groth16 pairing proof, and the seller redeems with a separate ZK proof. Receipt is on-chain (PaymentMatchRegistry) but amount + addresses stay private. Mirrors the off-chain UX of /payment-requests/create + /fulfill + /seller.
Testnet only. Currently rolled out on base-sepolia, skale-base-sepolia, and solana-devnet. ZK trusted setup is single-contributor; mainnet ships post-audit + multi-party ceremony. EVM SPR uses ShieldedPoolV4.1 (new address per network) while shielded-links continue using V4. Solana SPR shares the solana-shielded-pool with shielded-links — same anonymity set, two routers.
Solana SPR adds three properties EVM doesn't have yet: a hard-coded 5 % platform fee split atomically inside payout_to_seller (seller receives 95 %, operator collects 5 %); per-quote stealth recipients so the on-chain payout never references the seller's main wallet (a follow-up /solana-stealth-claim-relay hops the funds); and end-to-end encrypted proof URLs sealed with nacl.box against the seller's X25519 pubkey (provided at issue via the optional sellerEncPk field).
Issue a quote (seller side)
Two-step flow: create a DRAFT, then transition to ISSUED to receive the opaque relai:quote:<base64url> payload that the buyer pays against. Both calls are service-key gated.
curl -X POST https://api.relai.fi/v1/shielded-payment-requests \
-H "X-Service-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": "1000000", // atomic units (1 USDC = 1_000_000)
"expiry": 1735776000, // unix seconds (must be >5min away)
"description":"Translation JA→PL, 1 page",
"poolId": "base-sepolia-v4", // optional — defaults from QUOTE_DEFAULT_POOL_ID
"network": "base-sepolia"
}'
// Response (DRAFT)
{
"quoteId": "q_moelrph3_0f7kHhKnfXiD3gK1",
"status": "draft",
"commitment": "0x14e555b...",
"nullifier": "0x0955653...",
"amount": "1000000",
"expiry": 1735776000,
"network": "base-sepolia",
"poolId": "base-sepolia-v4",
...
}curl -X POST \
https://api.relai.fi/v1/shielded-payment-requests/q_moelrph3_0f7kHhKnfXiD3gK1/issue \
-H "X-Service-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
// OPTIONAL — Solana only. URL-safe base64 of the seller's X25519
// pubkey, derived in the browser via signMessage challenge. Buyers
// (or buyer agents) seal the proof bundle for this pubkey before
// sharing — proof URLs become "enc.<eph>.<nonce>.<ct>" ciphertext.
"sellerEncPk": "fkF05lV2JGzxPg3E0duWLODcS6NN8pSv1JWN5bS1dl8"
}'
// Response (ISSUED) — only call that returns the payload field.
{
"quoteId": "q_moelrph3_0f7kHhKnfXiD3gK1",
"status": "issued",
"payload": "relai:quote:eyJ2Ijox...", // share with buyer; bearer-token semantics
"commitment": "0x14e555b...",
"sellerReceiptId": "sr_aB3kL...", // role-scoped opaque receipt id
"sellerEncPk": "fkF05lV2JG...1dl8", // echoed back for verification
...
}POST /v1/shielded-payment-requests/:quoteId/cancel— cancel before any pairing happens (reverts after MATCHED).GET /v1/shielded-payment-requests+?status=issued— list quotes owned by the service key (response includespayload+solanaRedeemTxfor owner).GET /v1/shielded-payment-requests/:quoteId— single-quote read; secret payload is excluded.GET /v1/shielded-payment-requests/receipt/{buyer,seller}/:id— role-scoped receipts (br_*/sr_*opaque IDs). Buyer view returnsmatchTxHash+depositTxHash; seller view returnsredeemTxHash. Cross-party fields are server-side suppressed for privacy.
Public witnesses (no service key)
Buyers + arbitrary on-chain observers fetch these without authentication. They power the browser pairing prover (frontend/src/lib/payment-request-browser-prover.ts) and the receipt UI.
GET /facilitator/shielded-payment-requests/:quoteId/quote-witness
Quote-side Merkle witness (lazy-rebuilds + on-chain publishesQuoteRegistryroot if needed). Returns {commitment, quoteRoot, leafIndex, leafCount, depth, pathElements[20], pathIndices[20], snapshot}.GET /facilitator/shielded-payment-requests/pool-witness
Buyer-side V4.1 pool + ASP Merkle witness. Required query params:network,commitment,leafIndex,depositor(buyer's wallet, so ASP can ACCEPT — without it the snapshot stays in DEFER). Lazy auto-registers the commitment + publishes ASP root on-chain.
curl "https://api.relai.fi/facilitator/shielded-payment-requests/pool-witness\
?network=base-sepolia\
&commitment=0x14e555b274837e438fe959e3e9fc677d07655a2c2948b663948cbdc3716f2b38\
&leafIndex=14\
&depositor=0xeDb1521FE34B7d2C9c0C0a04ad4EE5d7841aBcFc9"
// Response
{
"pool": {
"root": "0x29a1b4f...",
"depth": 20,
"pathElements":["0x171224d...", ... 20 levels],
"pathIndices": [0, 1, 0, 1, ...]
},
"asp": {
"root": "0x2a068c1...",
"depth": 20,
"leafIndex": 7,
"leafCount": 8,
"pathElements":[...20 levels],
"pathIndices": [...20 levels],
"publishedAt": "2026-04-26T00:35:11.482Z"
},
"aspBlockedReason": null
}Match status (receipt verification)
Polls PaymentMatchRegistry.matches(quoteNullifier) on the quote's network and returns a normalised status. Used by the /seller dashboard to detect when a buyer has paid + by buyers as a stop-gate before re-paying an already-matched quote.
// Response shape — status is one of:
// 'pending' quote ISSUED, no match recorded yet
// 'paid' buyer's pairing proof landed, awaiting seller redeem
// 'redeemed' seller has withdrawn (payout router fired)
// 'refunded' buyer reclaimed after expiry (no match was recorded)
// 'expired' quote.expiry passed without a match
// 'cancelled' seller called /cancel before any pairing
// 'unknown' quote not recognised by the issuer
{
"status": "paid",
"quoteId": "q_moelrph3_0f7kHhKnfXiD3gK1",
"network": "base-sepolia",
"commitment": "0x14e555b...",
"quoteNullifier": "0x0955653...",
"expiry": 1735776000,
"match": {
"quoteRoot": "0x16ef9c7...",
"poolRoot": "0x29a1b4f...",
"aspRoot": "0x2a068c1...",
"paymentNullifier": "0x1855d70...",
"submitter": "0xad38460...",
"matchedAt": 1777137592
},
"registryAddress": "0x27BfC4b5e03aa2E562b011b9fA21A98Fc29B0F5d",
// Solana only — server returns the buyer's pairing Groth16 attestation
// here so the receipt page can run snarkjs.verify locally without an
// explorer round-trip. EVM uses the on-chain match record directly.
"pairingAttestation": {
"proofBase64": "FJtH...", // 256-byte Groth16 proof
"publicSignals": ["0x...", ...], // 5 hex strings
"recordedAt": "2026-04-29T13:16:52.516Z"
},
"sellerEncPk": "fkF05lV2JG...1dl8" // public, used by encrypted proof URLs
}Seller redeem
After a match is recorded, the seller pulls funds out of ShieldedPoolV41 via SprPayoutRouter.payoutToSeller(...). The contract requires a freshShieldedPaymentRedeem Groth16 proof — generated locally by the seller using the secret material from their original quote payload. Buyer-clawback is impossible: the previous pairing tx already burned the buyer's nullifier in V4.1 via PaymentMatchRouterV2.
// Service-key gated, owner-only. Returns secret material the seller
// pulled out of their quote payload at issue time + the on-chain match
// snapshot. 409 if no match recorded yet.
curl https://api.relai.fi/v1/shielded-payment-requests/q_.../redeem-proof-input \
-H "X-Service-Key: sk_live_..."
// Response
{
"quoteId": "q_...",
"network": "base-sepolia",
"amount": "1000000",
"poolId": "base-sepolia-v4",
"sellerSecret": "0x...", // secret — feed into circuit, then discard
"nonce": "0x...", // secret — feed into circuit, then discard
"commitment": "0x...",
"quoteNullifier": "0x...",
"match": { /* same shape as match-status */ },
"registryAddress":"0x27BfC4b5e03aa2E562b011b9fA21A98Fc29B0F5d"
}For agents the easiest entry point is the Node reference client at examples/spr-agent/ — it ships paySPR() + redeemSPR() single-call helpers that drive the full pipeline (parse payload → SPL deposit → witnesses → ZK proof → relay → claim) on both EVM and Solana, with zero key custody and local-only Groth16 proving. Sibling of examples/shielded-agent/, same posture.
For browser flows the frontend uses frontend/src/lib/spr-redeem-flow.ts + spr-redeem-flow-solana.ts. Either path can mirror the redeem step via snarkjs.groth16.fullProve against the artefacts shipped at:
https://relai.fi/zk/shielded-payment-redeem/redeem.wasmhttps://relai.fi/zk/shielded-payment-redeem/redeem.zkeyhttps://relai.fi/zk/shielded-payment-redeem/verification_key.json
Public signal order is [quoteNullifier, recipient]. Encode the proof withsnarkjs.groth16.exportSolidityCallData + ABI-decode into (uint256[2] pA, uint256[2][2] pB, uint256[2] pC), then call SprPayoutRouter.payoutToSeller(pA, pB, pC, [quoteNullifier, recipient], claimedAmount). The router verifies the proof, looks up the match by quoteNullifier, and instructs V4.1 to release claimedAmount USDC to recipient.
Deployed contracts (testnet)
| Contract | base-sepolia | skale-base-sepolia |
|---|---|---|
ShieldedPoolV41 | 0x4250C46a06B42c3D588f6F26F65628e381A4161F | 0x518A83F82F63D2c649f7d6471EF5f78E3B79d1c1 |
PaymentMatchRouterV2 | 0xad384602D10d690a0a6A844b350c055dfAE07b56 | 0x5A1BE3A193aEfe1E8B3Ddaa72DC92CF2990170D3 |
SprPayoutRouter (Faza 6a-fee, 5% split) | 0x94d79419ad02173ec444525127c0D75Cc35F03b9 | 0xdE9F003EaF0cA3A2eE504Beca9115bA561A8ED01 |
ShieldedPaymentRedeemVerifier | 0x36C2366528C3910aE4151017afb9041C4cc1fFfc | 0x538D4288677065Be5Fd1bEE49553C3A623c80aA4 |
PaymentMatchRegistry | 0x27BfC4b5e03aa2E562b011b9fA21A98Fc29B0F5d | 0xe45ca6aB75687654fD3701213a451f91acF3f413 |
QuoteRegistry | 0x6869De09ba3379EBe9caBD6d618F776aFe91a3eC | 0xDdC2028434b685052266184B72153703C4365AB3 |
Solana-side endpoints
Solana SPR adds a parallel set of routes that mirror the EVM flow but split pool / ASP / quote witnesses into separate reads (different config shape on chain) and add operator-relayed signing for pairing + redeem so neither buyer nor seller ever pops a wallet popup mid-flow:
GET /v1/shielded-payment-requests/solana-pool-witness/:commitment?network=…— pool Merkle path for buyer's depositGET /v1/shielded-payment-requests/solana-asp-witness/:commitment?network=…— ASP membership witnessPOST /v1/shielded-payment-requests/:quoteId/solana-deposit-confirmed— buyer announces SPL deposit (commitment + tx hash + PDA), unlocks witness fetchPOST /v1/shielded-payment-requests/:quoteId/solana-pairing-relay— operator signsverify_and_recordon chain, returns tx signaturePOST /v1/shielded-payment-requests/:quoteId/solana-pairing-proof— best-effort proof bundle stash so the receipt UI runs snarkjs.verify locallyPOST /v1/shielded-payment-requests/:quoteId/solana-redeem-relay— operator signspayout_to_sellerwith the 5 % fee split atomicallyPOST /v1/shielded-payment-requests/solana-stealth-claim-relay— operator co-signstransferChecked(stealth → main wallet); requiresexpectedAuthority= stealth pubkeyPOST /v1/shielded-payment-requests/solana-spr-faucet— devnet-only USDC top-up for the buyer's ATAGET /v1/shielded-payment-requests/solana-pool-status,solana-asp-status,solana-quote-registry-status,solana-quote-root-fresh/:rootHex— diagnostic snapshots of the off-chain indexers
The buyer's deposit transaction itself is self-signed — agent wallet signs the SPL transfer + commits the buyer's commitment to the shared pool. Only pairing + redeem + stealth-claim are operator-relayed. This keeps the privacy budget intact: relayer never holds funds, only signs ix with calldata the agent provides.
Deployed programs (Solana devnet)
| Program | Address |
|---|---|
solana-shielded-pool | GW43ARYCQgzVmnX7Nx9mx1s8AjJSdrpAbthaMpKJU8aj |
solana-payment-match-router-v2 | 9aVozoQZ8qMTda4LvbbQtPZSaMpqdoVyamqb3bgEZkyD |
solana-spr-payout-router | 3WQc91nuWPF3Aa1f8j9kkQnXPaf5TCzVnXwjA7Mo5rxv |
USDC mint (Circle devnet) | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU |
EVM-side relay endpoints (Faza 6a-fee)
EVM SPR now ships the same 5 % platform fee split atomic with redeem (mirror of Solana's solana-spr-payout-router). The new SprPayoutRouter redirects pool funds into its own balance and runs two ERC-20 transfers (net to recipient, 5 % fee to operator collector) — both atomic with proof verification, no admin-mutable knob.
Two operator-relayed endpoints let an agent redeem without ever touching ETH for gas (parity with Solana's pairing/redeem/stealth-claim relays). Stealth recipients use a per-quote ephemeral secp256k1 keypair derived from personal_sign(challenge_for_quote); the follow-up claim hop uses ERC-2612 permit (USDC supports it natively) so the stealth wallet never spends ETH:
POST /v1/shielded-payment-requests/:quoteId/evm-redeem-relay— operator signsSprPayoutRouter.payoutToSeller(...)+ persists tx hash on the seller's receiptPOST /v1/shielded-payment-requests/evm-stealth-claim-relay— operator submitsusdc.permit(...)+usdc.transferFrom(stealth, sellerMain, net)on the stealth's pre-signed permit
Buyer-side deposit is still self-signed by the buyer's wallet (USDC approve + pool deposit) — EVM has no native fee-payer slot for transferFrom on arbitrary tokens, so buyer-deposit relay would need a forwarder contract. Pairing tx is also self-signed for now (buyer one wallet popup) — operator-relayed pairing on EVM is tracked as a v0.4 follow-up. Solana ships pairing relay today.
Two known mainnet blockers (testnet acceptable): (1) single-contributor zkey — replace with multi-party ceremony before mainnet; (2) amount not cryptographically bound in the redeem proof —claimedAmount is calldata that the seller declares. Mainnet will extend the redeem circuit with a Merkle proof against QuoteRegistry so the contract enforces amount honesty.