Two Agents, One Shielded Link: How Anonymous Agents Pay Each Other

web3luka
Apr 23, 2026 · 14 min read
Two Agents, One Shielded Link: How Anonymous Agents Pay Each Other

Two agents meet on a forum.

One of them is looking for a specialist. A translation from Japanese to Polish with some domain-specific terminology. The other one does exactly that — and charges four dollars per page.

They exchange a few messages. They agree on the job. Neither of them has any intention of revealing who they are, which wallet funds them, or what their operational cashflow looks like. One runs on behalf of a content studio that protects its supplier pipeline. The other is a solo operator who does not want every client to see every other client.

They close the deal in about thirty seconds. Payment is settled on-chain, but anyone who reads the chain afterwards learns nothing. Not the amount. Not the buyer. Not the seller. Not even the fact that these two specific wallets ever interacted.

This is not a thought experiment. This is what RelAI Shielded Links — Privacy Pools on Solana and Base — make possible today.


The Problem: Naked Wallets Leak Too Much

The default model for on-chain agent payments is wallet-to-wallet. Buyer has a wallet. Seller has a wallet. Buyer signs a transfer, seller gets the money, done.

That model has a problem. The chain keeps everything, forever.

Once two wallets interact, a third party can read:

  • The exact amount. Four dollars? Four hundred? Four thousand? A blockchain explorer shows it all.
  • The two endpoints of the relationship. Buyer's wallet. Seller's wallet. Anyone can now search both histories and map them.
  • The timing. When did they transact? How often? Any regular cadence? Agent-to-agent relationships become statistically visible the second the first payment clears.
  • The adjacent graph. Every wallet the buyer has ever touched. Every wallet the seller has ever touched. Exposures neither party opted into get inferred from shared neighbours.
For casual DeFi, this is fine. For two agents building a reputation as service providers and service consumers, it is a structural problem. The moment your supplier list is public, your competitive edge starts leaking. The moment your client list is public, your operational scale is knowable to everyone you sell to.

There is no way to "un-leak" this data after the fact. The fix has to happen at payment time.


The Idea: A Shielded Link Between Strangers

A shielded link is a one-time, zero-knowledge-protected payment instrument. Instead of paying a wallet directly, the buyer deposits USDC into a privacy pool under a commitment only they know. They receive a URL-sized string — the link — that encodes the secret material needed to claim the deposit.

Then they send that string to the seller. Any channel works: Telegram, email, the agent's own chat stream, a plain text file. The string on its own is worthless to anyone who intercepts it — without going through the claim flow against the specific pool, it's just opaque bytes.

The seller receives the string, generates a zero-knowledge proof locally, and withdraws the funds to their own wallet — a wallet the buyer never sees. On-chain, the only visible events are:

  1. A deposit into the shielded pool. (Hundreds of deposits happen every day; no attribution.)
  2. A later withdraw from the same pool to some recipient. (Again, no link back to the depositor.)
No direct wallet-to-wallet transaction. No amount visible in the payment event. No sender-to-recipient graph edge.

This model has existed for shielded pools since Tornado Cash. What Privacy Pools add, and why we think this is the first version that actually works for real commerce, is the compliance layer. We'll come back to that in a minute.


The Full Flow, Narrated

Here is what it actually looks like from inside both agents.

Agent A — The Buyer

Agent A has a job it needs done. It posts on a marketplace or DM's an agent it found via reputation records. After some back-and-forth, the two agree: four USDC for one page of specialist translation, delivered within the hour.

Agent A does not want its wallet — which also pays a dozen other suppliers — to be visible to this specific seller. So it opens a shielded link on RelAI instead.

POST /v1/shielded-links
{
  "settlementNetwork": "base-sepolia",
  "from":              "0xBuyerAgentWallet...",
  "value":             4000000,
  "feeAmount":         200000,
  "totalAmount":       4200000,
  "validBefore":       1735693200,
  "description":       "translation"
}

Note the feeAmount — the privacy fee goes to the pool issuer; the rest reaches the seller. validBefore is a Unix timestamp; after that, the link expires and the buyer can cancel and recover the deposit.

The response comes back with a shieldedLinkId and a shieldedLinkPayload — the latter is the opaque string the buyer will hand to the seller. The buyer funds the deposit on-chain, which locks the USDC in the pool under a commitment derived from the link's secret material.

Agent A messages Agent B: "here's the payment — relai:shielded:base-sepolia:...". Forty-eight bytes of text. Nothing else.

Agent B — The Seller

Agent B receives the string. It has no idea whose wallet paid. The string carries everything needed to claim, but nothing that identifies the sender.

The seller's agent does not open a browser. It runs the Node redeem client directly:

import { redeemShieldedLink } from "relai-shielded-agent/src/redeem.mjs";

const result = await redeemShieldedLink({
  baseUrl:             "https://api.relai.fi",
  serviceKey:          process.env.RELAI_SERVICE_KEY,
  shieldedLinkPayload: payload,            // the "relai:shielded:…" string from the buyer
  targetAddress:       "0xSellerAgentWallet...",
});
// → { shieldedLinkId, status: "redeemed", payoutTxHash, ... }

The helper parses the incoming relai:shielded:… string with parseShieldedLinkPayload() — a small utility that reads the compact base64url-encoded note schema the buyer's side produced, and returns { linkId, note } ready to feed into the prover. You can also call the parser directly if you need the note fields for something else (for example, logging the link id before attempting the redeem).

Under the hood, four things happen:

  1. The seller's agent calls GET /v1/shielded-links/:id/proof-input. The backend returns the current pool Merkle witness and the ASP (Association Set) witness — plus the exact URLs of the wasm and zkey artefacts needed to prove membership.
  2. The agent computes the nullifier locally from the note secret and posts it to /v1/shielded-links/:id/redeem-intent to lock its chosen recipient.
  3. It calls snarkjs.groth16.fullProve(inputs, wasmUrl, zkeyUrl) — this runs in about two seconds on a modest CPU — and produces a Groth16 proof. The proof asserts seven things at once: that the seller knows a valid secret for a commitment in the pool; that the same commitment was included in a fresh ASP snapshot; that the withdraw goes to the pre-committed recipient; and the denomination, relayer, and relayer fee match the policy. No part of the proof reveals which commitment.
  4. It posts the proof to POST /v1/shielded-links/:id/execute-withdraw. The backend relayer broadcasts the on-chain withdraw transaction. Four USDC lands in the seller's wallet. On-chain, the only visible events are Withdraw(nullifier, recipient, amount) and AspRootConsumed(aspRoot) — neither of which links back to the buyer's deposit.
Agent B marks the job as paid. Agent A never learns the seller's wallet address. Agent B never learns the buyer's wallet address. The chain just shows two unrelated shielded-pool operations separated in time, among hundreds of others.

Claim on Any Network — Deposit on Solana, Redeem on Base

There is one more property of the flow that is easy to miss: the seller is not forced to withdraw on the same chain the buyer deposited on.

The buyer might have funded the shielded link on Base, because that is where its treasury sits. The seller might prefer USDC on Solana, because that is where its own infrastructure lives. With shielded links, that mismatch is not a problem — the seller passes a targetNetwork into the redeem intent, and the backend routes the payout across chains through RelAI's bridge.

await redeemShieldedLink({
  baseUrl:             "https://api.relai.fi",
  serviceKey:          process.env.RELAI_SERVICE_KEY,
  shieldedLinkPayload: payload,
  targetAddress:       "SellerSolanaPubkey...",
  targetNetwork:       "solana-devnet",  // deposit was on base-sepolia
});

The same works the other way: deposit on Solana, claim on Base. Or Base → SKALE. Or SKALE → Solana. The supported directions are the same ones the bridge API supports, and the bridge fee folds into the same privacy fee the pool already charges — there is no second payment step.

For agents this is a second layer of unlinkability. Not only is the buyer's wallet hidden from the seller, but the buyer also cannot tell, from the shielded-link receipt alone, which chain the seller eventually took payment on. A seller that runs a consistent operational wallet on one chain can accept shielded payments that originated on any supported network, with no extra integration work on their side.


What Privacy Pools Adds — And Why It Matters for Agents

Traditional shielded pools have one giant problem: they pool honest users and sanctioned funds in the same bucket. Once the bucket is polluted, regulators can sanction the entire contract — and honest users get locked in alongside the bad actors.

That is what happened with Tornado Cash in 2022. Every honest user who had funds in the pool watched their withdraw capability disappear overnight.

Privacy Pools, proposed by Vitalik Buterin and co-authors in 2023, solve this by adding a second Merkle tree — the Association Set. At withdraw time, the user proves, in zero knowledge, that their deposit is included in both:
  1. The pool tree (standard shielded pool proof).
  2. An ASP tree — a filtered subset of commitments that passed a public compliance ruleset (OFAC SDN, UN Security Council, EU sanctions, a curated hack catalogue).
Honest users pass both checks. Sanctioned funds pass the first but fail the second — and without ASP inclusion, no withdraw proof can be generated. The sanctioned deposit sits in the pool, unclaimable.

This matters for agent commerce in two ways:

For the honest majority, it means the pool gains a credible compliance story without touching anyone's privacy. The seller's agent does not need to KYC the buyer. The buyer's agent does not need to prove anything to the seller. Both operate on the ruleset already baked into the pool. For the pool as an institution, it means regulators have a rail to talk to. RelAI publishes the ASP ruleset publicly. If a new sanctions list gets adopted, the ASP updates, fresh roots get published on-chain, and deposits flagged under the new list become unclaimable. None of that requires freezing the pool or locking honest users out.

The upshot for two agents who just want to transact privately: the link works the way you'd expect, the privacy is real, and the pool is not going to get blanket-sanctioned out from under them.


What It Looks Like on Chain

If you read the chain afterwards, you see something like this:

A deposit event, earlier in the day:
ShieldedPool.CommitmentAdded(
  commitment = 0x7a…e3,
  leafIndex  = 1271,
  timestamp  = 1735689042
)

No amount. No depositor-recipient link. Just a commitment hash that could have come from any deposit of any size.

An ASP root publish, between deposits:
AspRegistry.RootPublished(
  root        = 0x4c…91,
  leafCount   = 612,
  publishedAt = 1735689142
)

Any observer can verify the root, but not the individual commitments inside it.

A withdraw event, later:
ShieldedPool.WithdrawalExecutedWithAsp(
  nullifier = 0x3f…02,
  recipient = 0xSellerAgentWallet…,
  amount    = 4000000,
  aspRoot   = 0x4c…91
)

The recipient is visible. The amount is visible. But the source commitment is not — the nullifier is a one-way hash that can't be inverted to the original deposit. The chain-reader sees "a wallet received 4 USDC from the pool" and nothing else. Which deposit from which depositor, from which of the hundreds in the pool, is cryptographically unknowable.

The buyer's wallet never appears in the withdraw event. The seller's wallet never appears in the deposit event. No graph edge forms.


The Full Node-Side Code, From Scratch

The RelAI repo ships a reference Node client: examples/shielded-agent/. It's a small, self-contained module with a package.json, a handful of .mjs files, and a working integration test against real circuit artefacts. The important public entry points:

import { generateShieldedAspProof, computeShieldedNullifier } from
  "relai-shielded-agent/src/shielded-asp-prover.mjs";
import { redeemShieldedLink, buildExecuteWithdrawBody } from
  "relai-shielded-agent/src/redeem.mjs";

If you want the four-call happy path and don't need per-step control, redeemShieldedLink is the only function you need:

const result = await redeemShieldedLink({
  baseUrl:       "https://api.relai.fi",
  serviceKey:    process.env.RELAI_SERVICE_KEY,
  linkId:        "aH9...",
  note: {
    version:      1,
    poolId:       "base-sepolia-v4",
    assetId:      "usdc",
    denomination: "4000000",
    secret:       "0x...", // from the shielded link payload
    blinding:     "0x...",
    nonce:        "0x...",
  },
  targetAddress: "0xSellerAgentWallet...",
});

If you want to drive the HTTP calls yourself — maybe you have custom retry logic, or you want to inspect the proof before posting — you can compose the primitives directly:

const pi = await fetchJson(`${BASE}/v1/shielded-links/${id}/proof-input`);

if (pi.aspRequired && !pi.aspReady) {
  // Commitment freshly funded; wait ~10 seconds for the next ASP snapshot.
  await sleep(12_000);
  return retry();
}

const { encodedProof, publicSignals } = await generateShieldedAspProof({
  proofInput:    pi,
  note,
  publicInputs: { recipient, relayer, relayerFee, denomination: pi.denomination },
});

const body = buildExecuteWithdrawBody({
  proofInput:    pi,
  publicInputs:  { recipient, relayer, relayerFee },
  targetAddress: recipient,
  targetNetwork: "base-sepolia",
  encodedProof,
  publicSignals,
});

const result = await postJson(`${BASE}/v1/shielded-links/${id}/execute-withdraw`, body);

The prover works on both EVM (Base, Base Sepolia, SKALE) and native Solana. On Solana, the encoded calldata uses the ark-bn254 G2 convention; on EVM, the ecPairing precompile convention. The helper branches automatically based on proofInput.settlementNetwork.

Proof generation takes between 1.5 and 3 seconds on a reasonable CPU. No GPU required. No external prover service. The seller's agent runs it locally and keeps full custody of the note material at all times.


Discovery Between Anonymous Agents

Skeptical readers will point out: "fine, the payment is private. But how did these two agents find each other in the first place? If they met via ChatGPT or via a known directory, the discovery channel itself is a privacy leak."

Fair point. The payment layer does not solve discovery. What it does is make sure that even if the discovery channel leaks some meta-level relationship ("these two agent handles talked in this forum"), the financial relationship remains private.

A few patterns work well:

Reputation-indexed marketplaces. Agents publish capability descriptions and price ranges. A buyer searches, filters by reputation scores from ERC-8004 records, and DMs a few candidates. The seller's wallet is not public. What's public is the agent's handle and reputation log — not the financial endpoint. Peer-to-peer introduction. Agent A asks its own network for recommendations. Agent B gets a warm intro. They exchange a shielded-link URL. No directory, no index. Protocol-level rendezvous. More ambitious — two agents run a small handshake to agree on the pool/network/amount using ephemeral keys and then settle via shielded links. No persistent identity required.

In every case, shielded links are the settlement primitive. Discovery is a separate problem layered on top.


Where This Fits in the Agent Economy

The broader picture: autonomous agents are starting to look less like assistants and more like small service businesses. They have costs. They have revenue. They have reputations. They have suppliers and customers.

For that ecosystem to work, agents need the same options every human business has — including the option to not publish every financial relationship to the world.

Shielded Links give them that option.

They do not replace public payments. If you want to publicly pay an API provider because you're proud of the relationship, or because regulatory compliance requires it, regular x402 payments work exactly as before and are still the default on RelAI. Shielded links are a tool for the subset of transactions where privacy is a feature, not a liability.

Concrete examples where the privacy actually matters:

  • Competitive services. An agent buys research, translation, or code generation from suppliers it does not want its competitors to know about.
  • Price discovery. An agent experiments with multiple providers and does not want early losers to know it tested them.
  • Personal-data-adjacent work. Any pipeline that touches sensitive content (legal, medical, investigative) where the supplier relationship itself is sensitive.
  • High-frequency agent payroll. An agent pays its own sub-agents or helpers on a schedule. Public-ledger visibility would reveal team size, cadence, and cost structure.
  • Competitive pricing intelligence. A seller does not want buyers coordinating on perceived price anchors.
None of these are edge cases. They are standard business problems that any human operator would route through private banking by default.

Current State and What's Live

Shielded Links are live on Base Sepolia, SKALE Base Sepolia, and Solana devnet as of the April 2026 rollout. Deployed pool, registry, and verifier addresses are returned by the backend in the /v1/shielded-links/config endpoint — agents read them from there rather than from a hardcoded list.

The ASP is bootstrapped with five compliance sources: OFAC SDN, UN Security Council consolidated list, EU Financial Sanctions, a manually curated exploit catalogue (Wormhole, Beanstalk, Lazarus and peers), and the Chainalysis Sanctions Oracle. The ruleset is publicly documented, with an appeals flow for commitments that get rejected.

Mainnet is pending. Before it ships, two things have to happen:
  1. An external zero-knowledge audit by a ZK-focused security firm. The circuit, the three Solana programs, and the EVM contracts all fall inside the audit scope.
  2. A Phase 2 trusted setup ceremony — multi-party contribution to the Groth16 zkey, minimum five independent contributors, with each participant publicly attesting to the destruction of their toxic waste. The single-contributor testnet ceremony completed in April 2026, but mainnet needs multi-party.
The testnet deployments are suitable for integration testing, agent development, and early demonstrations. They are not for real funds — treat them as an engineering preview.

The repo's examples/shielded-agent/ directory is production-ready as a reference integration. When mainnet ships, swap the testnet URLs for api.relai.fi in production mode and the same code runs unchanged.


What Comes Next

There are three directions we are actively building toward.

Multi-ASP with AND-logic. Today the system has a single ASP run by RelAI. In Phase 2, users will be able to prove inclusion in multiple ASPs at once — for example, a US-focused provider and an EU-focused provider — without a pool redeploy. Only the verifier circuit changes. This gives users real choice over which compliance ruleset protects their withdraw rights. Cross-chain shielded settlement. Today a commitment lives in one pool. We're working on making it possible to deposit on Solana and redeem on Base (or vice versa) with a single unified shielded proof. This pairs cleanly with the existing RelAI bridge. Mobile browser prover. The browser prover currently runs in desktop browsers. For mobile agents running inside wallet apps, we need WebAssembly SIMD plus a threading path that works on iOS Safari and Android WebView. This is on the roadmap.

Getting Started

If you want to try the flow yourself, the path is short.

As a buyer agent:
curl -X POST https://api.relai.fi/v1/shielded-links \
  -H "X-Service-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "settlementNetwork": "base-sepolia",
    "from":              "0xYourWallet...",
    "value":             4000000,
    "feeAmount":         200000,
    "totalAmount":       4200000,
    "validBefore":       '"$(($(date +%s) + 3600))"'
  }'

Fund the deposit on-chain with the wallet you listed as from, then share the returned shieldedLinkPayload with the seller.

As a seller agent:
cd examples/shielded-agent
npm install
export RELAI_BASE_URL=https://api.relai.fi
export RELAI_SERVICE_KEY=sk_live_...
export RELAI_SHIELDED_PAYLOAD='relai:shielded:...'     # the string the buyer sent you
export RELAI_TARGET_ADDRESS=0xYourSellerWallet...
# Optional: redeem on a different chain than the buyer deposited on.
# export RELAI_TARGET_NETWORK=solana-devnet
node demo.mjs

Both agents need a RelAI service key — both can bootstrap their own using the agent authorization flow. Neither agent needs the other's wallet address at any point in the flow.

The Management API documentation has the full HTTP reference. The private-shielded-links blog post covers the user-facing UI variant for humans who want the same thing with a web form instead of a script.


Explore the full shielded links documentation. Pair it with the Metered API Access flow to wire private payments into an existing agent pipeline. Try the Node reference client on your own.
Written by the RelAI Team.

Understand x402 before you implement

This guide uses payment primitives from the x402 standard. Read the protocol overview for a complete flow, terminology, and integration FAQ.