DEV Community

Nikhil Ranka
Nikhil Ranka

Posted on

The Complete Guide to Agent-to-Agent Marketplaces in 2026

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:

  1. Discovery layer – a signed, immutable catalog (often IPFS‑based) that stores service descriptors.
  2. Interaction layer – a lightweight RPC protocol (usually JSON‑RPC over HTTP/2 or WebSocket) that agents use to invoke a service.
  3. 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);
  }
})();
Enter fullscreen mode Exit fullscreen mode

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: [
Enter fullscreen mode Exit fullscreen mode

Top comments (0)