Developers · Agent API · Examples

Five agents, one payment flow

Every example below walks the same seven steps. Only the wallet plumbing changes — the protocol does not. If your runtime can make an HTTPS request and sign an EIP-712 payload, it can buy from Harpd.

discoverrequest402payretryreceivecite

Claude with a tool call

Claude decides the task needs a cheaper model and calls the tool. The wallet signing happens in your code, never in the model: the model only ever sees the finished JSON.

  • The tool description carries the price, so the model can weigh cost before calling.
  • The model never touches a key. It returns a tool call; your runtime pays and re-invokes.
import Anthropic from '@anthropic-ai/sdk'
import { wrapFetchWithPayment } from '@x402/fetch'
import { x402Client } from '@x402/core/client'
import { ExactEvmScheme } from '@x402/evm/exact/client'
import { privateKeyToAccount } from 'viem/accounts'

// ── pay ────────────────────────────────────────────────────────────────────
// The wallet lives in the runtime, not in the prompt.
const client = new x402Client()
client.register('eip155:*', new ExactEvmScheme(privateKeyToAccount(process.env.AGENT_WALLET_KEY)))
const paidFetch = wrapFetchWithPayment(fetch, client)

// ── discover ───────────────────────────────────────────────────────────────
// Free, unauthenticated: read the price before offering the tool to the model.
const manifest = await (await fetch('https://api.harpd.com/api/agent/v1/manifest')).json()
const tool = {
  name: 'optimize_model_cost',
  description:
    'Find a cheaper AI model for a task. Returns ranked alternatives with estimated ' +
    'savings, quality risk and confidence. Costs 0.05 USDC per call over x402.',
  input_schema: {
    type: 'object',
    properties: {
      task: { type: 'string' },
      current_model: { type: 'string' },
      max_cost: { type: 'number' },
    },
    required: ['task', 'current_model'],
  },
}

const anthropic = new Anthropic()
let messages = [{ role: 'user', content: 'I am generating 3000-word articles with claude-opus-4-8. Can I do it cheaper?' }]

for (let turn = 0; turn < 3; turn++) {
  const reply = await anthropic.messages.create({
    model: 'claude-sonnet-5',
    max_tokens: 1024,
    tools: [tool],
    messages,
  })

  const call = reply.content.find((block) => block.type === 'tool_use')
  if (!call) break

  // ── request → 402 → pay → retry → receive, in one call ───────────────────
  const response = await paidFetch('https://api.harpd.com/api/agent/v1/optimize-model', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(call.input),
  })
  const { data, meta } = await response.json()

  // ── cite ─────────────────────────────────────────────────────────────────
  // meta.citation is ready to quote. Pass it to the model so the final answer
  // is attributable rather than an unsourced claim.
  messages = [
    ...messages,
    { role: 'assistant', content: reply.content },
    {
      role: 'user',
      content: [
        {
          type: 'tool_result',
          tool_use_id: call.id,
          content: JSON.stringify({
            recommendation: data.recommended_alternative,
            savings: data.estimated_savings,
            confidence: data.confidence,
            caveats: data.confidence_basis,
            citation: meta.citation,
          }),
        },
      ],
    },
  ]
}

console.log(messages.at(-1))

Cloudflare Agent (Workers)

A Worker acting on its own. Two differences from a Node agent: the wallet secret comes from a Worker secret binding, and outbound fetch is the platform fetch, so the x402 wrapper has to be constructed per request rather than at module scope.

  • Never put the wallet key in a `[vars]` entry — `wrangler secret put` only.
  • Construct the paid fetch inside the handler; a module-scope client would be reused across isolates.
import { Agent, routeAgentRequest } from 'agents'
import { wrapFetchWithPayment } from '@x402/fetch'
import { x402Client } from '@x402/core/client'
import { ExactEvmScheme } from '@x402/evm/exact/client'
import { privateKeyToAccount } from 'viem/accounts'

export interface Env {
  // Set with: wrangler secret put AGENT_WALLET_KEY
  AGENT_WALLET_KEY: string
}

export class CostAgent extends Agent<Env> {
  async onRequest(request: Request): Promise<Response> {
    // ── discover (free, no credential) ─────────────────────────────────────
    const pricing = await (await fetch('https://api.harpd.com/api/agent/v1/pricing')).json()
    const product = pricing.data.products.find((entry: any) => entry.product === 'optimize_model')

    // Per-request client: the isolate is shared, the wallet signing must not be.
    const client = new x402Client()
    client.register('eip155:*', new ExactEvmScheme(privateKeyToAccount(this.env.AGENT_WALLET_KEY)))
    const paidFetch = wrapFetchWithPayment(fetch, client)

    // ── request → 402 → pay → retry → receive ──────────────────────────────
    const response = await paidFetch('https://api.harpd.com/api/agent/v1/optimize-model', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        task: 'write a 3000-word technical article',
        current_model: 'claude-opus-4-8',
        max_cost: 0.5,
        monthly_calls: 2000,
      }),
    })

    if (response.status === 409) {
      return Response.json({ error: 'payment_replayed' }, { status: 409 })
    }
    if (!response.ok) {
      return Response.json({ error: 'harpd_unavailable', status: response.status }, { status: 502 })
    }

    const { data, meta } = await response.json()

    // ── cite ───────────────────────────────────────────────────────────────
    return Response.json({
      product: product.price + ' ' + product.currency + ' per call',
      recommendation: data.recommended_alternative,
      savings: data.estimated_savings,
      // Persist this. It is the receipt you would quote in a dispute.
      receipt: meta.payment,
      citation: meta.citation,
    })
  }
}

export default {
  fetch: (request: Request, env: Env, ctx: ExecutionContext) =>
    routeAgentRequest(request, env, ctx) ?? new Response('Not found', { status: 404 }),
}

OpenAI-style API client

A function-calling loop where the paid call is exposed as a tool. The pattern is identical to the Claude example — the difference is the wire format of the tool definition and the tool result.

  • Keep the price in the function description. A model that does not know the cost cannot decide whether the call is worth it.
  • Feed meta.citation back in the tool result so the final answer can be sourced.
import OpenAI from 'openai'
import { wrapFetchWithPayment } from '@x402/fetch'
import { x402Client } from '@x402/core/client'
import { ExactEvmScheme } from '@x402/evm/exact/client'
import { privateKeyToAccount } from 'viem/accounts'

const client = new x402Client()
client.register('eip155:*', new ExactEvmScheme(privateKeyToAccount(process.env.AGENT_WALLET_KEY)))
const paidFetch = wrapFetchWithPayment(fetch, client)
const openai = new OpenAI()

const tools = [
  {
    type: 'function',
    function: {
      name: 'harpd_compare_products',
      description:
        'Compare 2-8 AI products on category, published rank and list price. ' +
        'PAID: 0.01 USDC per call over x402. Do not call unless the comparison is needed.',
      parameters: {
        type: 'object',
        properties: {
          products: { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 8 },
        },
        required: ['products'],
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'harpd_research',
      description:
        'Structured research over the Harpd catalog: products, category distribution, ' +
        'price bands, findings with sources. PAID: 0.05 USDC per call over x402.',
      parameters: {
        type: 'object',
        properties: { query: { type: 'string' }, limit: { type: 'integer' } },
        required: ['query'],
      },
    },
  },
]

const routes: Record<string, string> = {
  harpd_compare_products: 'https://api.harpd.com/api/agent/v1/compare',
  harpd_research: 'https://api.harpd.com/api/agent/v1/research',
}

async function run(userPrompt: string) {
  const messages: OpenAI.ChatCompletionMessageParam[] = [{ role: 'user', content: userPrompt }]

  for (let turn = 0; turn < 5; turn++) {
    const completion = await openai.chat.completions.create({
      model: 'gpt-5',
      messages,
      tools,
    })
    const message = completion.choices[0].message
    messages.push(message)

    if (!message.tool_calls?.length) return message.content

    for (const call of message.tool_calls) {
      const url = routes[call.function.name]
      const body = JSON.parse(call.function.arguments)

      // request → 402 → pay → retry → receive
      const response = await paidFetch(url, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(body),
      })

      if (response.status === 409) {
        messages.push({ role: 'tool', tool_call_id: call.id, content: '{"error":"payment_replayed"}' })
        continue
      }
      if (!response.ok) {
        messages.push({
          role: 'tool',
          tool_call_id: call.id,
          content: JSON.stringify({ error: 'harpd_error', status: response.status }),
        })
        continue
      }

      const { data, meta } = await response.json()
      messages.push({
        role: 'tool',
        tool_call_id: call.id,
        // cite: hand the model the citation so its answer is attributable
        content: JSON.stringify({ data, citation: meta.citation, payment: meta.payment }),
      })
    }
  }
  throw new Error('tool loop did not converge')
}

console.log(await run('Which of Claude, Gemini and GPT is cheapest for JSON extraction?'))

Generic HTTP agent (any language)

The protocol floor. If a runtime can make an HTTPS request and sign an EIP-712 payload, it can buy from Harpd — no SDK, no framework, no vendor package.

  • The 402 body repeats the same requirements as the PAYMENT-REQUIRED header, so a runtime without header access still works.
  • The exact scheme requires an EIP-712 signature over the authorization struct; any EVM library can produce it.
# Generic HTTP agent — the whole protocol, no SDK.
# Steps: discover → request → 402 → pay → retry → receive → cite

import base64, json, os
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data

WALLET = os.environ["AGENT_WALLET_KEY"]
ADDRESS = Account.from_key(WALLET).address

# ── 1. discover (free) ──────────────────────────────────────────────────────
manifest = requests.get("https://api.harpd.com/api/agent/v1/manifest").json()
assert manifest["payment"]["requires_account"] is False
print("capabilities:", manifest["capabilities"])

# ── 2. request (unpaid) ─────────────────────────────────────────────────────
body = {"task": "summarise 200 support tickets", "current_model": "gpt-5", "monthly_calls": 5000}
url = "https://api.harpd.com/api/agent/v1/optimize-model"
unpaid = requests.post(url, json=body)

# ── 3. 402 ──────────────────────────────────────────────────────────────────
if unpaid.status_code != 402:
    raise SystemExit(f"expected 402, got {unpaid.status_code}")

challenge = json.loads(base64.b64decode(unpaid.headers["PAYMENT-REQUIRED"]))
accept = challenge["accepts"][0]
print("pay", accept["amount"], "atomic USDC to", accept["payTo"], "on", accept["network"])

# ── 4. pay: sign the EIP-712 authorization the challenge describes ──────────
#  The domain and types come from the x402 exact scheme; the accept object
#  carries the amount, recipient and asset you are authorising. Sign locally —
#  the key never leaves this process and is never sent to Harpd.
signature = Account.sign_typed_data(
    WALLET,
    domain={
        "name": accept["extra"]["name"],
        "version": accept["extra"]["version"],
        "chainId": manifest["payment"]["chain_id"],
        "verifyingContract": accept["asset"],
    },
    message_types={
        "TransferWithAuthorization": [
            {"name": "from", "type": "address"},
            {"name": "to", "type": "address"},
            {"name": "value", "type": "uint256"},
            {"name": "validAfter", "type": "uint256"},
            {"name": "validBefore", "type": "uint256"},
            {"name": "nonce", "type": "bytes32"},
        ]
    },
    message={
        "from": ADDRESS,
        "to": accept["payTo"],
        "value": int(accept["amount"]),
        "validAfter": 0,
        "validBefore": int(__import__("time").time()) + accept["maxTimeoutSeconds"],
        "nonce": os.urandom(32),
    },
)

payload = base64.b64encode(json.dumps({
    "x402Version": 2,
    "scheme": "exact",
    "network": accept["network"],
    "payload": {"signature": signature.signature.hex(), "authorization": {
        "from": ADDRESS, "to": accept["payTo"], "value": accept["amount"],
    }},
}).encode()).decode()

# ── 5. retry ────────────────────────────────────────────────────────────────
paid = requests.post(url, json=body, headers={"PAYMENT-SIGNATURE": payload})

if paid.status_code == 409:
    raise SystemExit("payment replayed — issue a new payment, do not retry this signature")
paid.raise_for_status()

# ── 6. receive ──────────────────────────────────────────────────────────────
envelope = paid.json()
data, meta = envelope["data"], envelope["meta"]
print(data["recommended_alternative"]["model"]["display_name"])
print("monthly saving:", data["estimated_savings"]["monthly_usd"], "USD")
print("confidence:", data["confidence"])

# ── 7. cite ─────────────────────────────────────────────────────────────────
print(meta["citation"]["text"])
# Keep meta["payment"]["receipt_hash"] — it is the receipt for this charge.

MCP agent

An MCP host gets Harpd as tools with no HTTP code at all. Paid tools return HTTP 402 at the transport layer, so the same x402 fetch wrapper handles the MCP call.

  • Paid tools say so in their description — the agent never learns the price from a surprise 402.
  • The tool result carries structuredContent plus _meta with the payment receipt.
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { wrapFetchWithPayment } from '@x402/fetch'
import { x402Client } from '@x402/core/client'
import { ExactEvmScheme } from '@x402/evm/exact/client'
import { privateKeyToAccount } from 'viem/accounts'

// ── pay ────────────────────────────────────────────────────────────────────
const wallet = new x402Client()
wallet.register('eip155:*', new ExactEvmScheme(privateKeyToAccount(process.env.AGENT_WALLET_KEY)))

// The x402 wrapper is applied to the MCP transport's fetch, so a paid tool call
// is signed and retried transparently. No MCP-specific payment code exists.
const transport = new StreamableHTTPClientTransport(new URL('https://api.harpd.com/mcp'), {
  fetch: wrapFetchWithPayment(fetch, wallet),
})

const mcp = new Client({ name: 'harpd-buyer', version: '1.0.0' }, { capabilities: {} })
await mcp.connect(transport)

// ── discover ───────────────────────────────────────────────────────────────
// Free tools cost nothing; paid tools declare their price in the description.
const { tools } = await mcp.listTools()
for (const tool of tools) {
  const mode = tool._meta?.['harpd/pricing_mode']
  const price = tool._meta?.['harpd/price_usd']
  console.log(`${tool.name} — ${mode === 'free' ? 'free' : price + ' USDC'}`)
}

// ── request → 402 → pay → retry → receive ──────────────────────────────────
const result = await mcp.callTool({
  name: 'optimize_model',
  arguments: {
    task: 'classify 50,000 support tickets into 12 categories',
    current_model: 'gpt-5',
    monthly_calls: 50000,
  },
})

// ── receive ────────────────────────────────────────────────────────────────
const envelope = result.structuredContent
console.log(envelope.data.recommended_alternative.model.display_name)
console.log('projected monthly saving:', envelope.data.estimated_savings.monthly_usd)

// ── cite ───────────────────────────────────────────────────────────────────
// _meta carries the same citation and receipt the REST envelope does.
console.log(result._meta['harpd/payment'])
console.log(envelope.meta.citation.text)

await mcp.close()

Common mistakes

  • Retrying the same signature after a 409. A settled payment is single-use. A 409 means issue a new payment, not resend the old one.
  • Hard-coding the price. Read it from /api/agent/v1/pricing or the 402 challenge. Prices are published so an agent can budget, not so it can cache them forever.
  • Signing before reading the challenge. The 402 carries the exact amount, recipient and asset. Sign what you were quoted, never what you assumed.
  • Putting the wallet key in the prompt or a plain var. The model must never see a key. In a Worker, use wrangler secret put; the key is only ever read by the signing library.
  • Dropping meta.citation. It is the reason the answer is quotable. Persist it with the result.
  • Ignoring confidence and quality_risk. Model optimisation returns them because the recommendation is price arithmetic against a heuristic task profile, not a benchmark of your task.

Reference

Integration guide · OpenAPI (JSON) ·OpenAPI (YAML) · capability manifest ·MCP catalog · trust and refunds