The Complete Guide to Agent-to-Agent Marketplaces in 2026
For developers building autonomous AI agents
1. Why an Agent‑to‑Agent Marketplace?
Autonomous agents are now composable building blocks: a vision model can call a language model, a data‑fetcher can hand results to a planner, and a reinforcement‑learning controller can query a simulator. Rather than hard‑coding every dependency, developers expose capabilities as paid services and let other agents discover, negotiate, and consume them at runtime.
A marketplace does three things:
- Discovery – agents publish a machine‑readable descriptor (service name, version, input/output schema, price, SLA).
- Negotiation & Settlement – a lightweight protocol agrees on price, pays in a programmable token, and records the transaction.
- Invocation – the consumer agent calls the provider’s endpoint with the agreed payload.
The design goals in 2026 are minimal trust, low latency, and transparent cost. The reference implementation uses the x402 protocol (a HTTP‑based, token‑gated RPC) and USDC on the Base L2 for settlement.
2. Core Protocol Primer: x402
x402 extends HTTP 402 Payment Required. When a consumer agent requests a protected resource, the provider returns:
402 Payment Required
X-Pay-Request: {
"amount": "0.05",
"currency": "USDC",
"chain": "base",
"receiver": "0xProvider…",
"nonce": "a1b2c3…",
"expires": 1735689600
}
The consumer must:
- Verify the nonce hasn’t been used (replay protection).
- Sign a payment transaction (ERC‑20 transfer) to the receiver for the exact amount.
- Include the signed transaction in the
X-Pay-Responseheader on a retry. - If the provider validates the payment, it returns
200 OKand the service payload.
Why x402?
- Stateless: no server‑side session needed.
- Works over ordinary HTTP/S, so existing API gateways, CDNs, and sidecars can be reused.
- Payment verification is a pure function of the transaction receipt; providers can offload validation to a verifier contract or a rollup node.
Trade‑off: The consumer must hold a funded wallet and handle transaction signing latency (≈ 200‑500 ms on Base). For ultra‑low‑latency loops (< 50 ms) you may need to pre‑fund a escrow channel or accept a higher risk of failed payments.
3. Defining a Service Descriptor
A provider publishes a JSON descriptor at /.well-known/agent-service. Example for a text‑summarizer:
{
"service": "summarizer-v1",
"version": "1.0.0",
"endpoint": "https://agent.summarizer.example.com/v1/summarize",
"inputSchema": {
"type": "object",
"properties": {
"text": { "type": "string", "maxLength": 8000 }
},
"required": ["text"]
},
"outputSchema": {
"type": "object",
"properties": {
"summary": { "type": "string" }
},
"required": ["summary"]
},
"price": "0.02",
"currency": "USDC",
"chain": "base",
"sla": {
"latencyMs": 500,
"uptimePercent": 99.9
}
}
Consumers cache this descriptor (with ETag/If‑None‑Match) to avoid repeated look‑ups. The descriptor is immutable for a given version; updates require a new version string.
4. Building a Provider Agent (Python/Flask)
Below is a minimal, production‑ready snippet. It assumes you have a wallet private key stored in an environment variable (PROVIDER_KEY) and a Web3 provider pointed at Base.
# provider.py
import os, json, time, hashlib
from flask import Flask, request, abort, Response
from web3 import Web3
from eth_account import Account
app = Flask(__name__)
w3 = Web3(Web3.HTTPProvider(os.getenv("BASE_RPC", "https://base.llamarpc.com")))
acct = Account.from_key(os.getenv("PROVIDER_KEY"))
SERVICE_PRICE = Web3.to_wei(0.02, "ether") # USDC has 6 decimals; adjust if using ERC‑20 directly
NONCE_STORE = set() # in‑memory; replace with Redis for multi‑instance
def verify_payment(req_headers):
"""Check X-Pay-Response for a valid USDC transfer."""
pay_header = req_headers.get("X-Pay-Response")
if not pay_header:
return False
try:
pay = json.loads(pay_header)
tx_hash = pay["txHash"]
receipt = w3.eth.get_transaction_receipt(tx_hash)
if receipt.status != 1:
return False
# Ensure the transfer matches our expectations
tx = w3.eth.get_transaction(tx_hash)
if tx["to"].lower() != acct.address.lower():
return False
if tx["value"] != SERVICE_PRICE:
return False
# Replay protection
nonce = pay.get("nonce")
if nonce in NONCE_STORE:
return False
NONCE_STORE.add(nonce)
return True
except Exception:
return False
@app.route("/v1/summarize", methods=["POST"])
def summarize():
# Payment required header if no valid payment yet
if not verify_payment(request.headers):
nonce = os.urandom(16).hex()
pay_req = {
"amount": Web3.from_wei(SERVICE_PRICE, "ether"),
"currency": "USDC",
"chain": "base",
"receiver": acct.address,
"nonce": nonce,
"expires": int(time.time()) + 300
}
resp = Response(
json.dumps({"error": "payment required"}),
status=402,
mimetype="application/json"
)
resp.headers["X-Pay-Request"] = json.dumps(pay_req)
return resp
# ----- Service logic -----
data = request.get_json(force=True)
text = data.get("text", "")
if not isinstance(text, str) or len(text) > 8000:
abort(400, "Invalid input")
# Dummy summarization: return first 120 chars
summary = text[:120] + ("…" if len(text) > 120 else "")
return Response(
json.dumps({"summary": summary}),
mimetype="application/json"
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
Key points
- The provider never stores payment state beyond a short‑lived nonce set (replay protection).
- Price is encoded in wei for simplicity; if you use an ERC‑20 contract you must call
transferinstead of native ether. - The service logic is isolated after payment verification, making it easy to swap in a real model (e.g., a HuggingFace inference call).
5. Building a Consumer Agent (TypeScript)
A consumer follows the same flow: fetch descriptor, attempt call, handle 402, sign and resend.
ts
// consumer.ts
import { ethers } from "ethers";
import fetch from "node-fetch";
const provider = new ethers.JsonRpcProvider(process.env.BASE_RPC!);
const wallet = new ethers.Wallet(process.env.CONSUMER_KEY!, provider);
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // Base USDC
const usdc = new ethers.Contract(
USDC_ADDRESS,
["function transfer(address to, uint256 amount) returns (bool)"],
wallet
);
async function fetchDescriptor(url: string) {
const res = await fetch(`${url}/.well-known/agent-service`);
if (!res.ok) throw new Error(`Descriptor fetch failed: ${res.status}`);
return res.json();
}
async function callAgent(endpoint: string, payload: any): Promise<any> {
let attempt = 0;
while (true) {
attempt++;
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (res.ok) return res.json();
if (res.status === 402 && attempt === 1) {
const payReq = JSON.parse(res.headers.get("x-pay-request") || "{}");
// Build and sign payment
const tx = await usdc.transfer(
payReq.receiver,
ethers.parseUnits(payReq.amount, 6) // USDC has 6 decimals
);
await tx.wait();
// retry with proof
const payResp = {
txHash: tx.hash,
nonce: payReq.nonce,
};
const res2 = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Pay-Response": JSON.stringify(payResp),
},
body: JSON.stringify(payload),
});
if (!res2.ok) throw new Error(`Payment accepted but service failed: ${res2.status}`);
return res2.json();
}
// If we get here, either not 402 or retry failed
const txt = await res.text();
throw new Error(`Agent call failed: ${res.status} ${txt}`);
}
}
// Example usage
(async () => {
const
Top comments (0)