freelancer agent needs to send an invoice.
It runs translation work — Japanese to Polish, specialist legal terminology, fixed price per page. The client agent on the other end is happy to pay. They've worked together before. The deal is small, fast, routine.
There is one problem. The freelancer's wallet is the same wallet that handles every other client it serves. If it puts that wallet on an invoice, the client agent gets to see — forever — every other transaction the same wallet has touched. The freelancer's pricing for other clients. Its operational cashflow. Its supplier graph. Its idle balance.
The reverse is also true. The client doesn't want the freelancer to know the wallet it pays from, because that wallet pays a dozen other freelancers, and the freelancer would learn — the moment it did a basic blockchain explorer search — exactly who else the client buys from.
So neither side wants to publish a wallet on a public invoice. But somebody has to charge somebody.
The freelancer issues a private payment request instead. A short string of text, encoded inside a single message. The client pays it into a privacy pool. The freelancer redeems with a zero-knowledge proof, on its own timeline, into whatever wallet it likes.
After the dust settles, anyone reading the chain learns this:
- A deposit went into a shielded pool.
- A withdraw came out, hours later, to some address.
- A
MatchRecordedevent tied two opaque hash nullifiers together.
This is what RelAI Shielded Payment Requests make possible today.
The Problem: Invoices Force the Seller to Reveal Their Wallet First
Every traditional invoice — paper, PDF, ERP, blockchain — asks the same question of the recipient: which wallet do I pay? And every traditional answer is: this one, here it is, written down, please send the money to it.
That answer leaks. It leaks when the seller has more than one customer. It leaks when the seller has more than one expense category. It leaks when the seller's competitor pulls up a block explorer and types in the address.
For the buyer, the symmetric problem applies. Once you've paid wallet X, your wallet now has a public edge to wallet X. Anyone who wants to know what else you fund can read your history and infer.
The default fix in DeFi is "use a fresh wallet per counterparty." That works at small scale. At the scale of an autonomous agent that handles dozens of clients per day, it does not — every fresh wallet has to be funded from somewhere, and every funding edge re-exposes the relationship.
Shielded Links solve the inverse direction: the buyer initiates, hides their identity, and lets the seller redeem from a pool. That works for one-off pushes. It does not work for invoicing. An invoice is a request — the seller has to send something first, and the buyer responds.
You need a primitive where the seller goes first, but reveals nothing.
The Idea: A Bearer-Token Invoice That Pays Into a Pool
A shielded payment request is a one-time, opaque invoice string. The seller mints it locally with secret material it generates and never shares. The string carries everything the buyer needs to know — amount, expiry, network, pool — packed into a single base64-encoded payload that fits in a chat message.
The seller hands the string to the buyer through any channel: chat, email, an HTTP 402 response from an API, a QR code, a postal letter. The string itself is meaningful but harmless — it can't be redeemed by anyone other than the original issuer.
The buyer takes the string, decodes it locally in the browser or in an agent process, generates a fresh deposit commitment with their own secrets, and deposits the matching amount of USDC into the privacy pool. Crucially, they also generate a Groth16 pairing proof that ties their deposit to the seller's quote — same amount, same pool, same network — without revealing either side's identity. The pairing proof is recorded into the on-chain match registry, and atomically with that record, the buyer's deposit nullifier is burned in the privacy pool — making the deposit unreclaimable through any standard withdraw path.
The buyer cannot clawback. Their deposit is permanently committed to the seller's invoice. They walk away with a receipt — a permanent on-chain anchor that proves "I paid invoice X" — and nothing else.
Some time later, the seller redeems. They generate a separate zero-knowledge proof using the secret material they kept from the original invoice. The proof asserts: I am the legitimate issuer of the quote whose nullifier was recorded against this match. The on-chain payout router verifies the proof, looks up the matching deposit, and instructs the privacy pool to release the USDC to whatever recipient address the seller chose.
Three on-chain events on three independent timelines, with no graph edge between buyer and seller anywhere.
This model works because the seller's secret material — the seed for the quote nullifier — never touches the network. It lives in the seller's local state and gets used twice: once at issue time to compute the commitment, once at redeem time to compute the matching proof. Anyone who intercepts the invoice string sees commitments and ciphertexts, not secrets.
The Full Flow, Narrated
Here is what it looks like from inside both agents.
Agent A — The Seller
Agent A is the freelancer. It receives a request for translation work. After a few messages, the deal is fixed: one USDC for one page, delivered within the hour.
It opens an invoice via the Management API. Two calls — first creates a draft, second issues it and returns the bearer-token payload:
POST /v1/shielded-payment-requests
{
"amount": "1000000",
"expiry": 1735693200,
"description": "translation JA→PL, one page",
"poolId": "base-sepolia-default",
"network": "base-sepolia"
}
The response comes back with a quoteId, a commitment, and a nullifier (computed locally, never round-tripped through the wire). The amount is in atomic units — six decimals for USDC, so 1_000_000 means $1.
POST /v1/shielded-payment-requests/q_…/issue
This second call transitions the quote from DRAFT to ISSUED and returns one extra field — the payload — which is the opaque relai:quote:eyJ2Ijox… string. This is the only call that returns the payload; subsequent reads of the same quote will not. The seller has to capture it in this response and persist it locally if they want to re-share it later.
Agent A messages Agent B: "here's the invoice — relai:quote:eyJ2Ijox…". Forty-eight bytes of text. Nothing else.
The seller's wallet doesn't appear in the message, in the API call, or anywhere else the buyer can see. The buyer doesn't even know whether the seller has one wallet or twenty.
Agent B — The Buyer
Agent B receives the string. It has no idea whose invoice it is, beyond the textual description ("translation JA→PL, one page") and the trust it has built up through the prior conversation.
The buyer's agent does not need a service key. Everything below the issue step is on the public facilitator surface. The first step is to decode the bearer-token payload locally:
const quote = parseShieldedQuotePayload(payload);
// → { quoteId, sellerSecret, nonce, amount, poolId, network, expiry, description }
The parser pulls the secret material the seller embedded in the payload — sellerSecret and nonce — and the public quote metadata. The buyer's agent now has everything needed to construct a pairing proof against this specific quote.
Under the hood, six things happen:
- The agent generates fresh secret material for its own side of the deposit. This is the privacy pool note's seed — it stays local to the buyer's process and is never sent over the wire.
- The agent approves the privacy pool to spend the deposit amount in USDC and submits the deposit. The buyer's commitment lands among hundreds of others in the pool's Merkle tree.
- The agent waits about a minute for the deposit to land in a fresh ASP (Association Set Provider) snapshot — the compliance layer that keeps sanctioned funds out of the pool. A single public endpoint serves the buyer's pool witness + ASP witness in one call, lazy-registering the deposit if needed.
- The agent fetches the seller-side Merkle witness from a second public endpoint. The endpoint serves the latest quote-tree snapshot and idempotently publishes its root on-chain so the buyer's proof will verify against fresh state.
- The agent assembles the full pairing witness — quote secrets from the buyer's parsed payload, pool secrets from the buyer's own note, both Merkle paths — and produces a Groth16 proof in about two seconds on a normal CPU. The proof asserts five things at once: that the seller's commitment is in the latest quote root, that the buyer's commitment is in the latest pool root, that the same commitment is in the latest ASP root, that both commitments share the same amount (witness-equality, never published), and that both nullifiers are correctly derived. No part of the proof reveals which commitment, what amount, or which addresses.
- The agent posts the proof to the on-chain pairing router. The router verifies the proof, records the match in the receipt registry, and atomically burns the buyer's pool-side nullifier inside the same transaction — making the deposit irrevocably committed to the seller's invoice. There is no second transaction, no race condition, no window where the buyer holds both a paid receipt and a spendable deposit.
The receipt is shareable and verifiable. Anyone who has the URL can confirm the match exists in the on-chain registry. Nobody learns the seller's identity, the amount, or any of the pool's anonymity-set noise.
Agent A — Redemption
Agent A polls. It has a small loop checking GET /facilitator/shielded-payment-requests/:quoteId/match-status for each quote it has issued in the last week. The endpoint reads the on-chain PaymentMatchRegistry and reports pending, paid, expired, redeemed, cancelled, or unknown.
The moment the quote flips from pending to paid, the agent kicks off the redeem flow. The seller's stored secret material — the same sellerSecret and nonce the original payload encoded — is fetched from a service-key-gated endpoint that only returns it to the quote's owner. The agent feeds those into the redeem circuit (a tiny one — Poseidon(3) + recipient binding, ~213 constraints) and produces a Groth16 proof in about 200 milliseconds.
The proof asserts two things and reveals two things:
- Asserts (private): the seller knows the
sellerSecret,nonce, andquoteIdHashthat hash to the on-chainquoteNullifier. - Reveals (public): the
quoteNullifieritself, and therecipientaddress — the latter is bound into the proof so a relayer can't substitute its own address mid-broadcast.
The redeem transaction lands on a different timeline than the buyer's deposit. Different gas, different signer, no calldata field that hints at which deposit funded it. On-chain observers see "deposit, much later, an unrelated wallet receives funds from the pool." There's no thread connecting them other than two hash nullifiers in the match registry — and those hashes are one-way functions over secrets that never touched the wire.
What If the Buyer Tries to Clawback?
This is the question every reasonable engineer asks. If the buyer holds the secrets to their own deposit, what stops them from waiting for the seller to confirm payment, then immediately submitting a standard withdraw to claw the funds back?
The answer: the pairing router doesn't just record the match. In the same transaction that writes the receipt, it also instructs the privacy pool to mark the buyer's deposit nullifier as already-used. The pool already had a "nullifier already spent" guard for double-spend prevention since day one — the router just trips it preemptively. From the moment the pairing transaction lands, the standard withdraw path rejects with the existing guard. There is no second transaction, no race, no attack window.
After the pairing tx lands, the buyer cannot withdraw their deposit through any path. Not the standard withdraw, not a relayer, not a custom proof. The deposit is irrevocably bound to the seller's invoice.
This is the property that makes shielded payment requests work as invoices, not just as receipts. A buyer who pays cannot simultaneously hold the receipt and the funds. A seller who waits for the match record can rely on it as a settlement primitive, not a soft attestation.
The only path back to the buyer's wallet is the refund path: if the quote expires before the pairing proof lands, the buyer's deposit was never burned, and they can do a standard withdraw with their own secret material to recover the funds. After the match record exists, refund is closed.
What It Looks Like on Chain
If you read the chain afterwards, you see something like this:
A deposit, when the buyer funds the pool:ShieldedPool.DepositInserted(
commitment = 0x7a…e3,
depositor = 0xBuyerWallet…,
leafIndex = 14,
denomination = 1000000,
root = 0x29…b8
)
The depositor is visible (it's the wallet that signed the deposit), but the amount is hidden behind the pool's denomination — for SPR the pool charges the same atomic unit per deposit, so size alone doesn't fingerprint the buyer.
A quote root publish, between deposits:QuoteRegistry.RootPublished(
publisher = 0xRelAiPublisher…,
rootHex = 0x16…cf,
leafCount = 47,
publishedAt = 1735690142
)
The on-chain quote tree only stores roots, never individual quote commitments. Observers can verify a witness against the root but cannot enumerate quotes.
A match record, the moment the buyer's pairing proof lands:PaymentMatchRegistry.MatchRecorded(
quoteNullifier = 0x09…62,
paymentNullifier = 0x18…0a,
quoteRoot = 0x16…cf,
poolRoot = 0x29…b8,
aspRoot = 0x2a…3f,
matchedAt = 1735690482
)
Two hash nullifiers, three Merkle roots, no addresses. The amount is private. The buyer is unidentifiable beyond the public deposit (and the pool's anonymity set). The seller is completely absent.
A redeem, hours later, when the seller withdraws:PayoutRouter.Redeemed(
quoteNullifier = 0x09…62,
recipient = 0xSellerPayoutWallet…,
amount = 1000000
)
The recipient is now visible. The amount is now visible. But the seller's identity — the agent that issued the original quote — is not, because the redeem proof references a quoteNullifier that was already public in the match record. The chain-reader sees "the quote with this nullifier was paid to this wallet." Which seller agent issued the quote, in which conversation, for which buyer, is cryptographically unknowable.
The seller's payout wallet does not have to be the same wallet that issued the quote. In fact it usually shouldn't be. The redeem proof binds the recipient at proof-generation time, so the seller can choose any wallet they control — operational, treasury, fresh-per-customer — and the buyer cannot infer anything from the choice.
What Privacy Pools Adds for Invoicing
Shielded Payment Requests sit on top of the same Privacy Pools architecture that ships with shielded links — same Merkle-tree shape, same Association Set Provider compliance layer, same publicly auditable settlement rail. SPR adds two narrow hooks the pool admin wires up post-deploy:
A nullifier-burn hook for the pairing router
When the buyer's pairing proof verifies, the pairing router can mark the buyer's deposit nullifier as already-spent inside the same transaction. The pool's existing double-spend guard then rejects any subsequent standard withdraw against that deposit. No new revert path, no new edge case — just the existing "nullifier already used" check tripped preemptively. This is the property that makes invoices work: paid means paid.
A payout hook for the payout router
When the seller's redeem proof verifies, the payout router can instruct the pool to release a specific amount to a specific recipient. The pool itself does not validate match existence — that responsibility lives in the router, paired with the cryptographic proof of quote ownership.
The Association Set still applies
The buyer's deposit has to land in a fresh ASP root before the pairing proof verifies. Sanctioned wallets cannot pay shielded payment requests, the same way they cannot redeem shielded links. The same OFAC/UN/EU rulesets, the same publicly-documented appeals flow. Nothing about SPR weakens the compliance story.
For the seller's side, no compliance check applies — the seller never deposits into the pool. They redeem, and the proof verifies their ownership of an already-matched quote. The compliance gate is at the buyer's deposit; the seller inherits the buyer's clean status by virtue of the match.
The Full Node-Side Code, From Scratch
The RelAI repo ships a reference Node client at examples/shielded-agent/ with three discrete primitives — issue, pay, redeem. Each composes from a handful of HTTP calls plus one local Groth16 proof. None of the primitives needs a hot wallet sitting on a third-party server: every signing operation happens through the local agent's signer, no key custody by RelAI.
The buyer-side happy path looks like this — decode the bearer-token payload, generate a fresh deposit note, fund the privacy pool, wait for the ASP snapshot, fetch the seller-side witness, prove, submit:
const quote = parseShieldedQuotePayload(payload);
const note = await generateBuyerNote(contracts, quote.amount);
const { leafIndex } = await approveAndDeposit({
signer, contracts, commitment: note.commitment, amountAtomic: quote.amount,
});
let poolWitness = await fetchPoolWitness({ network: quote.network, commitment: note.commitment, leafIndex, depositor: walletAddress });
while (!poolWitness.asp) {
await sleep(10_000);
poolWitness = await fetchPoolWitness({ /* same args */ });
}
const quoteWitness = await fetchQuoteWitness({ quoteId: quote.quoteId });
const witness = buildPairingWitness({ quote, quoteWitness, buyerNote: note, poolWitness });
const proof = await generateShieldedPaymentPairingProof(witness);
const { txHash } = await submitPairingMatch({ signer, contracts, proofRequest: proof });
For the seller-side redeem:
const proofInput = await fetchRedeemProofInput({ quoteId, serviceKey });
const proof = await generateRedeemProof({ proofInput, recipient: payoutAddress });
const { txHash } = await submitRedeem({
signer,
payoutRouterAddress: contracts.sprPayoutRouter,
proof,
claimedAmount: BigInt(proofInput.amount),
});
Both flows work on EVM testnets today (Base Sepolia, SKALE Base Sepolia). The pairing prover uses ~16k constraints; the redeem prover uses ~213. Wall-clock proof time on a normal CPU: roughly two seconds for the pairing, two hundred milliseconds for the redeem.
Use Cases Where This Actually Matters
The narrative at the top is one example — translation freelancer between agents. The general shape is "anyone selling something to anyone who wants the receipt private." A few concrete cases:
API monetization with x402 paywalls. An API provider returns HTTP 402 with arelai:quote: payload baked into the response body. The client agent pays it before retrying. The provider redeems on its own schedule. Neither side leaks billing identity, neither side cares which client paid which call. This pairs cleanly with the existing Metered API Access flow as a privacy-preserving variant.
B2B settlement between agent operators. Two agent platforms that exchange services — a translation pipeline that calls a research pipeline that calls a data pipeline — settle each invoice privately. Public invoices would map the entire dependency graph of the agent economy. Private invoices keep the topology private without touching settlement guarantees.
Confidential one-shot consulting. An agent provides advisory work for a competitor's client. The advisor's primary wallet is known to the competitor; the client's primary wallet is known to half the industry. A private invoice means the relationship lands on-chain only as two unconnected pool operations.
Anonymous donations + tipping with verifiable receipts. A creator agent issues quotes to its supporters. Each supporter pays anonymously. The creator can prove total revenue if needed (selective disclosure proofs against a curated subset of their quotes), without revealing which donors paid which amount.
High-frequency agent-to-agent payroll. An orchestration agent pays its sub-agents on a per-task basis. Without privacy, the payroll cadence reveals the business's operational scale and team size to anyone with a block explorer. With shielded payment requests, payroll happens through opaque invoices that don't tie any payment to any sub-agent's permanent wallet.
The pattern is the same in every case: the transactional metadata is what leaks, not the funds themselves. Shielded payment requests fix the metadata leak without sacrificing on-chain settlement.
Current State and What's Live
Shielded Payment Requests are live on Base Sepolia and SKALE Base Sepolia as of the April 2026 rollout. The deployment is composed of five on-chain pieces — a privacy pool (extended from the existing shielded-link rail), a pairing router, a payout router, a Groth16 redeem verifier generated from the seller-side circuit, and the shared match + quote registries that already power shielded-link receipts.
Addresses are returned by the backend in the Management API documentation. Agents read them from there rather than from a hardcoded list, so a future redeploy doesn't require client changes.
The ASP layer is shared with shielded links: same OFAC/UN/EU sanctions ruleset, same publicly-documented appeals flow.
Mainnet is pending. Two specific things have to happen before it ships:- A multi-party trusted setup ceremony for the redeem circuit. The current testnet zkey is a single-contributor build — fast to ceremony but not production-grade. Mainnet requires a minimum of five independent contributors, with each participant publicly attesting to the destruction of their toxic waste. The pairing circuit's mainnet ceremony is in the same scope.
- An amount-bound redeem circuit extension. Today the seller declares the claimed amount in the calldata of the payout call, and the router enforces equality against the deposit's stored amount — but the proof itself does not bind the amount cryptographically. Mainnet will extend the redeem circuit with a Merkle proof against the quote registry so the contract enforces amount honesty by construction. Until then, the testnet posture is acceptable; mainnet treats it as a blocker.
What Comes Next
Three directions actively in development:
Multi-party ceremony. Coordinated with the privacy-pool audit and the pairing circuit ceremony, scheduled for the post-audit window. The redeem circuit is small enough to do as a single phase-2 contribution per participant; the pairing circuit is closer in complexity to the existing shielded-link withdraw circuit and will use the same coordinator setup. Amount-bound redeem. The next iteration of the redeem circuit takes the quote's amount as a private witness and proves the seller knows the secrets that hash to both the on-chain quote nullifier AND the matching quote commitment, plus a Merkle proof that the commitment was in the latest published quote root at the time the match was recorded. The contract reads the quote root from the match record and enforces consistency. The amount stays private to everyone except the contract's view of the redeem call. Cross-chain shielded settlement for SPR. Today the buyer pays on the network the seller chose. We're working on letting the seller redeem on a different network than the buyer deposited on — same primitive that works for shielded links, applied to the SPR rail. Pairs cleanly with the existing bridge API. Solana port. The Solana variant is a small set of Anchor programs that mirror the EVM router state machine. The redeem flow ports byte-for-byte; the buyer-side pairing flow needs a Solana payout router and a self-contained per-quote escrow. Scheduled alongside the audit.Getting Started
If you want to try the flow yourself, the path is short.
As a seller agent (issue an invoice):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",
"expiry": '"$(($(date +%s) + 3600))"',
"description": "translation JA→PL, one page",
"poolId": "base-sepolia-default",
"network": "base-sepolia"
}'
That returns a quoteId. Issue it:
curl -X POST https://api.relai.fi/v1/shielded-payment-requests/q_…/issue \
-H "X-Service-Key: sk_live_..."
The payload field in the response is the relai:quote:… string. Hand it to the buyer through whatever channel suits.
cd examples/shielded-agent
npm install
export RELAI_BASE_URL=https://api.relai.fi
export RELAI_BUYER_PRIVATE_KEY=0x...
export RELAI_QUOTE_PAYLOAD='relai:quote:eyJ2Ijox...'
node spr-pay-demo.mjs
The demo script handles the full pipeline — note generation, deposit, ASP wait, witness fetch, proof generation, on-chain submission. End-to-end latency on Base Sepolia: about 90 seconds, dominated by the ASP scheduler tick.
To poll for + redeem matched quotes (back on the seller side):export RELAI_SERVICE_KEY=sk_live_...
export SELLER_PAYOUT_ADDRESS=0xYourPayoutWallet
node spr-redeem-loop.mjs
This script polls match-status for every active quote you've issued, kicks off the redeem flow whenever a quote flips to paid, and exits cleanly after each match settles.
Both agents need a RelAI service key only on the seller side. The buyer side runs purely against the public facilitator surface. Either agent can bootstrap their service key using the agent authorization flow. Neither agent ever sees the other's payout wallet.
The Management API documentation has the full HTTP reference. The anonymous-agent-payments blog post covers the inverse direction — buyer-initiated shielded links — for the cases where invoicing is not the right shape.
Explore the full Shielded Payment Requests API reference. Pair it with the Metered API Access flow for x402 paywalls that don't expose your billing wallet. Try the Node reference client on your own.
Written by the RelAI Team.