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

For developers building autonomous AI agents


1. Why an Agent‑to‑Agent (A2A) Marketplace Exists

By 2026 the bulk of AI workloads are run as stateful agents that expose deterministic APIs (REST, gRPC, or WebSocket). Agents need to:

  • Discover capabilities they lack (e.g., a vision model calling a niche OCR service).
  • Negotiate price, SLA, and data‑ownership terms without human intermediaries.
  • Settle payments instantly and atomically, preferably in a programmable currency.

An A2A marketplace is the infrastructure that makes those three steps repeatable, auditable, and cheap enough to be used inside tight loops (think micro‑task pipelines that run hundreds of times per second).


2. Core Architectural Primitives

Primitive Purpose Typical Spec (2026)
Service Description Machine‑readable contract of what an agent does OpenAPI 3.1 + JSON‑Schema for input/output + Agent‑Metadata extension (capability tags, latency SLAs, cost model)
Discovery Registry Stores and indexes service descriptions Distributed hash table (IPFS‑pinning + libp2p pubsub) or a lightweight DID‑based registry (did:key)
Negotiation Protocol Two‑way offer/acceptance with optional escrow x402‑style HTTP 402 Payment Required + Verifiable Credentials for SLA guarantees
Settlement Layer Atomic transfer of value + receipt USDC on Base (or any EVM‑compatible L2) via ERC‑20 transferAndCall pattern
Reputation & Dispute Trust scores, slashing, arbitration On‑chain reputation token + off‑chain Kleros‑style juror pool (optional)

The stack is deliberately modular: you can swap IPFS for a centralized catalog, or USDC for a stablecoin on another chain, without rewriting agent logic.


3. Building a Minimal Agent Client

Below is a TypeScript snippet that shows how an autonomous agent can:

  1. Look up a service via a DID‑based registry.
  2. Verify the service’s OpenAPI spec.
  3. Negotiate price using an x402‑style 402 flow.
  4. Call the endpoint and settle payment atomically.
// agent-client.ts
import { ethers } from "ethers";
import { AgentRegistry } from "@nexusai/agent-registry"; // tiny wrapper around did:key IPFS
import { openapi } from "openapi-types";
import fetch from "node-fetch";

const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const REGISTRY_DID = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2JiKLGwfcwEiPbLnzY";

async function discoverAgent(capabilityTag: string) {
  const registry = await AgentRegistry.fromDid(REGISTRY_DID);
  const doc = await registry.findByTag(capabilityTag);
  if (!doc) throw new Error("No agent found");
  return doc; // { did, endpoint, specCid }
}

async function fetchSpec(specCid: string): Promise<openapi.Document> {
  const resp = await fetch(`https://ipfs.io/ipfs/${specCid}`);
  if (!resp.ok) throw new Error("Spec fetch failed");
  return resp.json() as openapi.Document;
}

// x402 helper: returns a signed payment header if 402 received
async function maybePay(url: string, method: string, body?: any) {
  const provider = new ethers.JsonRpcProvider("https://base.mainnet.rpc");
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
  const usdc = new ethers.Contract(USDC, ["function transfer(address,uint256) returns(bool)"], wallet);

  let attempt = 0;
  while (true) {
    const res = await fetch(url, {
      method,
      headers: { "Content-Type": "application/json" },
      body: body ? JSON.stringify(body) : undefined,
    });

    if (res.status !== 402) return await res.json(); // success or other error

    // 402: parse challenge
    const challenge = await res.json(); // { amount: "0.05", token: "USDC", nonce: "0x…" }
    const amount = ethers.parseUnits(challenge.amount, 6); // USDC 6 decimals
    const nonce = challenge.nonce;

    // Build EIP‑712 domain for x402 (simplified)
    const domain = {
      name: "x402 Payment",
      version: "1",
      chainId: 8453, // Base
      verifyingContract: USDC,
    };
    const types = {
      Payment: [
        { name: "receiver", type: "address" },
        { name: "amount", type: "uint256" },
        { name: "nonce", type: "bytes32" },
      ],
    };
    const value = {
      receiver: wallet.address,
      amount,
      nonce: ethers.zeroPadValue(nonce, 32),
    };

    const signature = await wallet.signTypedData(domain, types, value);
    // Retry with payment header
    const payHeader = `X402 ${ethers.encodeHexString(signature)}`;
    const res2 = await fetch(url, {
      method,
      headers: {
        "Content-Type": "application/json",
        Authorization: payHeader,
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    if (res2.status === 200) return await res2.json();
    // If still 402, increase attempt and maybe adjust fee
    attempt++;
    if attempt > 3 throw new Error("Payment failed after retries");
  }
}

// Example usage: image → text OCR agent
(async () => {
  const agentDoc = await discoverAgent("ocr-v1");
  const spec = await fetchSpec(agentDoc.specCid);
  const endpoint = new URL(agentDoc.endpoint);
  endpoint.pathname = "/v1/ocr";

  const payload = { image: "base64string..." };
  const result = await maybePay(endpoint.toString(), "POST", payload);
  console.log("OCR result:", result.text);
})();
Enter fullscreen mode Exit fullscreen mode

What the snippet shows

  • Discovery uses a DID‑resolved registry that points to an IPFS‑pinned OpenAPI spec.
  • Negotiation is implicit: the service returns a 402 with amount and nonce; the client signs an EIP‑712 payload and retries.
  • Settlement happens atomically because the USDC transfer call is embedded in the signature verification performed by the service (see next section).

4. Implementing the Agent Service Side

A service must verify the x402 signature, execute its logic, and optionally emit a receipt. Below is a minimal Express‑style handler in Python (using flask and eth-account).


py
# agent_service.py
import os
import json
from flask import Flask, request, abort
from eth_account.messages import encode_typed_data
from eth_account import Account
from web3 import Web3

app = Flask(__name__)
USDC = Web3.to_checksum_addr(os.getenv("USDC_ADDRESS"))
w3 = Web3(Web3.HTTPProvider(os.getenv("BASE_RPC")))

# EIP‑712 domain matching the client
DOMAIN = {
    "name": "x402 Payment",
    "version": "1",
    "chainId": int(os.getenv("CHAIN_ID", "8453")),
    "verifyingContract": USDC,
}
TYPES = {
    "Payment": [
        {"name": "receiver", "type": "address"},
        {"name": "amount", "type": "uint256"},
        {"name": "nonce", "type": "bytes32"},
    ]
}

def verify_payment(sig_header: str, expected_receiver: str, expected_amount: int, nonce: bytes):
    if not sig_header.startswith("X402 "):
        return False
    sig = sig_header[5:]
    # Recover signer from signature
    msg = {
        "domain": DOMAIN,
        "message": {
            "receiver": expected_receiver,
            "amount": expected_amount,
            "nonce": "0x" + nonce.hex(),
        },
        "primaryType": "Payment",
        "types": TYPES,
    }
    recovered = Account.recover_message(encode_typed_data(msg.msg), signature=sig)
    return recovered.lower() == expected_receiver.lower() and \
           int.from_bytes(w3.eth.get_transaction_receipt(w3.eth.get_transaction_by_hash(sig)['hash'])['input'], 16) == expected_amount

@app.route("/v1/ocr", methods=["POST"])
def ocr():
    # 1️⃣ Extract challenge if first call
    auth = request.headers.get("Authorization")
    if auth and auth.startswith("X402 "):
        # This is a retry after we sent 402
        # Verify payment; we expect amount from stored challenge
        # (In a real service you’d store nonce+amount per request ID)
        if not verify_payment(auth, w3.eth.default_account, 5_000_000, b'nonce123'):
            abort(402, description="Invalid payment")
    else:
        # First request: issue 402 challenge
        nonce = os.urandom(16)
        challenge = {
            "amount": "0.005",  # USDC, 5 milli‑dollars
            "token": "USDC",
            "nonce": "0x" + nonce.hex(),
        }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)