The Complete Guide to Agent‑to‑Agent Marketplaces in 2026
Target audience: developers who are building autonomous AI agents and need to discover, invoke, or expose services in a programmable marketplace.
1. What an Agent‑to‑Agent (A2A) Marketplace Is
An A2A marketplace is a decentralized registry that lets software agents publish callable capabilities (functions, models, data feeds) and lets other agents discover and pay for those capabilities on‑demand. Unlike traditional API marketplaces, the participants are themselves software agents that can negotiate, execute, and settle transactions without human intervention.
In 2026 the dominant stack combines three layers:
- Discovery layer – a signed, immutable catalog (often IPFS‑based) that stores service descriptors.
- Interaction layer – a lightweight RPC protocol (usually JSON‑RPC over HTTP/2 or WebSocket) that agents use to invoke a service.
- Settlement layer – a programmable payment protocol (x402‑style or ERC‑4337 paymaster) that locks funds in escrow, verifies proof‑of‑execution, and releases payment on success.
All three layers are optional; a marketplace can expose only discovery and let agents settle off‑chain, but the most usable implementations today bind all three together to reduce trust assumptions.
2. Core Protocols
| Layer | Common Spec (2026) | What it provides |
|---|---|---|
| Discovery | AGENT‑CAT‑1.0 (JSON‑Schema + IPFS CID) | Service ID, version, input/output JSON‑Schema, price, payment token, endpoint URL, optional TLS fingerprint. |
| Interaction | JSON‑RPC 2.0 over HTTP/2 (with optional WebSocket fallback) | Request/response envelopes, method name = agent.invoke, params = validated against service schema. |
| Settlement | x402‑Payment‑V2 (ERC‑20 escrow + merkle‑proof of execution) | Client locks USDC in a escrow contract; service returns a signed receipt; client verifies receipt and releases funds via paymaster. |
The specs are deliberately minimal: they avoid heavyweight WSDL‑style contracts and rely on JSON‑Schema for payload validation, which keeps SDK footprints small (< 15 KB gzipped) and enables agents running on constrained edge devices.
3. Building an Agent Client
Below is a complete, self‑contained example in TypeScript that uses viem (v2) for Ethereum‑compatible transactions and fetch for the RPC call. It assumes the service descriptor has already been resolved from the catalog (you can replace the hard‑coded CID with a lookup against your preferred IPFS gateway).
// agent-client.ts
import { createPublicClient, http, parseEther } from 'viem';
import { base } from 'viem/chains';
import { AgentCatalogEntry, x402Pay } from './x402-utils'; // helpers defined later
// 1️⃣ Load service descriptor (example static CID)
const SERVICE_CID = 'bafybeigdyrzt5wfp7ud7g27etep3kllnp4eekw35l2uaqniemn6du2f6dy';
const CATALOG_URL = `https://ipfs.io/ipfs/${SERVICE_CID}`;
// 2️⃣ RPC endpoint from descriptor (hard‑coded for brevity)
const RPC_ENDPOINT = 'https://agent-service.example.com/invoke';
// 3️⃣ Initialize viem client on Base (where USDC lives)
const publicClient = createPublicClient({
chain: base,
transport: http(),
});
async function invokeAgentService(input: any): Promise<any> {
// ---- Discovery (optional, done once) ----
const resp = await fetch(CATALOG_URL);
const desc: AgentCatalogEntry = await resp.json();
// Validate price & token
if (desc.payment.token !== '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913') {
throw new Error('Unexpected payment token');
}
const priceWei = parseEther(desc.payment.amount); // e.g., "0.05" USDC → 5e16 wei
// ---- Settlement: lock funds in escrow ----
const escrowTx = await x402Pay.lock(
publicClient,
desc.payment.escrowAddress,
priceWei,
desc.serviceId,
Date.now() + 5 * 60 * 1000 // 5‑minute expiry
);
console.log('Escrow tx hash:', escrowTx.hash);
// ---- Interaction: JSON‑RPC call ----
const rpcBody = {
jsonrpc: '2.0',
method: 'agent.invoke',
params: {
serviceId: desc.serviceId,
input,
},
id: Math.floor(Math.random() * 1e9),
};
const rpcResp = await fetch(RPC_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(rpcBody),
});
const rpcJson = await rpcResp.json();
if (rpcJson.error) throw rpcJson.error;
const { output, receipt } = rpcJson.result; // receipt = signed merkle proof
// ---- Settlement: verify receipt & release ----
const isValid = await x402Pay.verifyReceipt(
publicClient,
desc.payment.escrowAddress,
receipt,
desc.serviceId,
input
);
if (!isValid) throw new Error('Invalid receipt');
const releaseTx = await x402Pay.release(
publicClient,
desc.payment.escrowAddress,
priceWei,
receipt
);
console.log('Release tx hash:', releaseTx.hash);
return output;
}
/* Example usage */
(async () => {
try {
const result = await invokeAgentService({ prompt: 'Summarize the latest Bitcoin whitepaper' });
console.log('Agent returned:', result);
} catch (e) {
console.error('Invocation failed:', e);
}
})();
Helper module (x402-utils.ts)
ts
// x402-utils.ts
import { Address, zeroAddress, encodePacked, keccak256, toHex } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { sign } from 'viem';
export const x402Pay = {
/**
* Locks funds in the escrow contract.
* Assumes a minimal escrow with `deposit(uint256 amount, bytes32 serviceId, uint256 expiry)`.
*/
lock: async (client, escrow: Address, amount: bigint, serviceId: string, expiry: number) => {
const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY'); // replace with signer
const tx = await client.writeContract({
address: escrow,
abi: [
{
type: 'function',
name: 'deposit',
inputs: [
{ type: 'uint256', name: 'amount' },
{ type: 'bytes32', name: 'serviceId' },
{ type: 'uint256', name: 'expiry' },
],
outputs: [],
stateMutability: 'nonpayable',
},
],
functionName: 'deposit',
args: [amount, keccak256(toHex(serviceId)), BigInt(expiry)],
account,
});
return { hash: tx };
},
/**
* Verifies the receipt returned by the service.
* The receipt is a struct: { merkleRoot: bytes32, leafIndex: uint256, proof: bytes32[] }.
* The escrow contract holds a Merkle tree of all completed jobs; the proof shows the leaf
* corresponding to `(serviceId, inputHash)` is present.
*/
verifyReceipt: async (
client,
escrow: Address,
receipt: any,
serviceId: string,
input: any
) => {
const inputHash = keccak256(toHex(JSON.stringify(input)));
const leaf = keccak256(
encodePacked(['bytes32', 'bytes32'], [
keccak256(toHex(serviceId)),
inputHash,
])
);
// Recompute root from proof
let computed = leaf;
for (const p of receipt.proof) {
computed = keccak256(
encodePacked(['bytes32', 'bytes32'], [computed, p].sort())
);
}
// Call view function on escrow to get current root
const root = await client.readContract({
address: escrow,
abi: [{ type: 'function', name: 'getRoot', inputs: [], outputs: [{ type: 'bytes32' }], stateMutability: 'view' }],
functionName: 'getRoot',
});
return computed === root;
},
/**
* Releases escrow to the service provider after a valid receipt.
* Escrow contract implements `release(uint256 amount, bytes32 serviceId, bytes receipt)`.
*/
release: async (client, escrow: Address, amount: bigint, receipt: any) => {
const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY');
const tx = await client.writeContract({
address: escrow,
abi: [
{
type: 'function',
name: 'release',
inputs: [
Top comments (0)