Build an AI Shopping Agent with Aurpay REST API (2026) | Aurpay

Building an AI Shopping Agent with Aurpay REST API: A Claude Code Tutorial (2026)

Building an AI Shopping Agent with Aurpay REST API: A Claude Code Tutorial (2026)

This is a developer tutorial for wiring up an autonomous shopping agent that can browse, decide, and pay using Aurpay’s REST API from a Claude Code-style agent loop. Start on testnet, ship a working invoice + webhook flow, then add the safety patterns you want before flipping the API base to mainnet. Code is the load-bearing element; the prose just connects the snippets.

The 2026 case for AI agents that pay

Anthropic shipped Claude Sonnet 4.6 with native tool use that drives multi-step browsing and purchasing flows. OpenAI’s Operator and Google’s Gemini agent SDK landed in the same window, and the Stripe team published an “Agentic Commerce” position paper in March. The capability problem is largely solved. What is not solved on card rails is the authorization problem.

Building an AI Shopping Agent with Aurpay REST API: A Claude Code Tutorial (2026)

PSD2 Strong Customer Authentication, 3-D Secure step-ups, and issuer fraud heuristics all assume a human in the loop. The first time a Stripe-backed agent tries to buy ten things in a row from new merchants at 3 a.m., the issuer freezes the card. There is no clean API for “this charge is initiated by my AI on my behalf, here is the consent grant.” The card networks are working on it, but the spec is years away from production.

Crypto rails do not have that constraint. A wallet signature is the authorization. No SCA, no 3DS, no chargeback window an issuer can use to block an autonomous flow. For a deeper read on why this matters as a category, see the broader case for agent-spent crypto. This article is the hands-on counterpart: how you actually build it.

Architecture overview: AI agent to Aurpay REST API to wallet

The flow you are going to build looks like this:

┌──────────────────┐     tool call      ┌─────────────────────┐
│  Claude Sonnet   │ ─────────────────▶ │  search_products    │
│  4.6 (agent)     │                    │  (your product DB)  │
└──────────────────┘ ◀───────────────── └─────────────────────┘
        │              product list
        │ tool call
        ▼
┌──────────────────┐     POST /invoice  ┌─────────────────────┐
│  create_payment  │ ─────────────────▶ │  Aurpay REST API    │
│  _invoice tool   │ ◀───────────────── │  (testnet/mainnet)  │
└──────────────────┘   payment URL +    └─────────────────────┘
        │              invoice_id              │
        │ payment URL                          │ on-chain settle
        ▼                                      ▼
┌──────────────────┐                    ┌─────────────────────┐
│  Agent's wallet  │ ─── USDT ────────▶ │  Merchant wallet    │
│  (sub-wallet)    │     (ERC-20/TRC-20)│  (non-custodial)    │
└──────────────────┘                    └─────────────────────┘
        │                                      │
        │                                      │ webhook
        │                                      ▼
        │                            ┌─────────────────────┐
        └────────── continue ◀────── │  /webhooks/aurpay   │
                                     │  invoice.paid event │
                                     └─────────────────────┘

Three things to notice. First, the agent never holds keys directly; the wallet that signs the payment is a dedicated sub-wallet that you provision and fund. Second, Aurpay is the coordination layer (invoice + webhook), not a custodian. Funds move from the agent’s wallet straight to the merchant’s wallet. Third, the agent only continues after the webhook fires. No optimistic state.

Setting up the testnet environment with Claude Code

Install the Claude Code CLI and the Anthropic Python SDK. We will use httpx for outbound HTTP and fastapi later for the webhook receiver.

curl -fsSL https://claude.ai/install.sh | sh
mkdir shopping-agent && cd shopping-agent
python -m venv .venv && source .venv/bin/activate
pip install anthropic httpx fastapi uvicorn pydantic python-dotenv

Create a .env in the project root. Always start on testnet. The mainnet base URL gets swapped in later via a single environment variable.

ANTHROPIC_API_KEY=sk-ant-...
AURPAY_API_KEY=test_pk_...
AURPAY_API_BASE=https://testnet-api.aurpay.net/v1
AURPAY_WEBHOOK_SECRET=whsec_...
AGENT_DAILY_CAP_USD=200
AGENT_PER_TX_CAP_USD=50

Step 1: get your Aurpay API credentials

Sign in at aurpay.net/documentation and generate a testnet key. The Postman collection on the same page has the full payin / payout / orders / invoices surface; import it once and you have a working sandbox before writing any code. Aurpay exposes both a testnet and a mainnet environment, which is the property you care about most for agent development. You want the agent to make 500 mistakes against the testnet before it ever signs a mainnet transaction.

Add a thin client so the rest of the tutorial does not repeat HTTP boilerplate:

# aurpay_client.py
import os, httpx
from typing import Any

class AurpayClient:
    def __init__(self) -> None:
        self.base = os.environ["AURPAY_API_BASE"].rstrip("/")
        self.key = os.environ["AURPAY_API_KEY"]
        self._http = httpx.Client(timeout=15.0)

    def _headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self.key}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        }

    def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
        r = self._http.post(f"{self.base}{path}", json=payload, headers=self._headers())
        r.raise_for_status()
        return r.json()

    def get(self, path: str) -> dict[str, Any]:
        r = self._http.get(f"{self.base}{path}", headers=self._headers())
        r.raise_for_status()
        return r.json()

Step 2: build the search/decision tool the agent uses

The agent needs at least one upstream tool that returns purchasable items. In production this is your catalog API or a scraping subsystem. For the tutorial we will mock it, because the interesting code is the payment side.

# tools/search.py
PRODUCTS = [
    {"sku": "copilot-seat", "name": "GitHub Copilot seat (annual)", "price_usd": 95.00},
    {"sku": "anthropic-credit-50", "name": "Anthropic API credit ($50)", "price_usd": 50.00},
    {"sku": "vercel-pro", "name": "Vercel Pro (monthly)", "price_usd": 20.00},
]

def search_products(query: str) -> list[dict]:
    q = query.lower()
    return [p for p in PRODUCTS if q in p["name"].lower() or q in p["sku"]]

The Anthropic tool schema for this looks like:

SEARCH_TOOL = {
    "name": "search_products",
    "description": "Search the product catalog. Returns matching SKUs with USD prices.",
    "input_schema": {
        "type": "object",
        "properties": {"query": {"type": "string"}},
        "required": ["query"],
    },
}

Step 3: wire up Aurpay invoice creation as an Anthropic tool

The payment tool is where Aurpay’s REST API actually shows up in the agent loop. The agent calls create_payment_invoice, the tool POSTs to Aurpay’s invoice endpoint, and the structured response (invoice id + payment URL + expiry) goes back into Claude’s context so it can decide what to do next.

# tools/payment.py
from aurpay_client import AurpayClient

client = AurpayClient()

def create_payment_invoice(
    amount_usd: float,
    currency: str,        # "USDT" | "USDC" | "DAI" | "ETH" | "BTC" | "BNB"
    network: str,         # "ERC20" | "TRC20" | "BTC" | "LIGHTNING"
    description: str,
    order_ref: str,
) -> dict:
    payload = {
        "amount": amount_usd,
        "amount_currency": "USD",
        "settlement_currency": currency,
        "settlement_network": network,
        "description": description,
        "metadata": {"order_ref": order_ref, "source": "agent"},
    }
    return client.post("/invoices", payload)

A realistic response from the Aurpay invoice endpoint looks like:

{
  "invoice_id": "inv_8f3c1a2b",
  "status": "pending",
  "amount": 50.00,
  "amount_currency": "USD",
  "settlement_currency": "USDT",
  "settlement_network": "TRC20",
  "settlement_amount": "50.00",
  "pay_address": "TYz...HnQ",
  "payment_url": "https://pay.aurpay.net/inv_8f3c1a2b",
  "expires_at": "2026-05-11T13:42:00Z",
  "created_at": "2026-05-11T13:27:00Z"
}

The Anthropic tool schema pins the currencies and networks Aurpay supports. Keep this strict so the model cannot hallucinate a “USDT on Polygon” invoice that the API will reject:

PAYMENT_TOOL = {
    "name": "create_payment_invoice",
    "description": (
        "Create an Aurpay invoice for a purchase. Returns invoice_id and "
        "payment_url. Settlement is non-custodial, direct to the merchant."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "amount_usd": {"type": "number", "minimum": 0.01},
            "currency": {"type": "string", "enum":
                ["USDT", "USDC", "DAI", "ETH", "BTC", "BNB"]},
            "network": {"type": "string", "enum":
                ["ERC20", "TRC20", "BTC", "LIGHTNING"]},
            "description": {"type": "string"},
            "order_ref": {"type": "string"},
        },
        "required": ["amount_usd", "currency", "network",
                     "description", "order_ref"],
    },
}

Stitch it into the agent loop. This is the canonical Anthropic tool-use pattern, the same shape Claude Code uses internally:

# agent.py
import os, json
from anthropic import Anthropic
from tools.search import search_products
from tools.payment import create_payment_invoice
from tools.guards import check_spending_limit  # defined in Step 5
from schemas import SEARCH_TOOL, PAYMENT_TOOL

claude = Anthropic()

def dispatch(name: str, args: dict) -> dict:
    if name == "search_products":
        return {"results": search_products(**args)}
    if name == "create_payment_invoice":
        check_spending_limit(args["amount_usd"])  # raises if blocked
        return create_payment_invoice(**args)
    raise ValueError(f"unknown tool: {name}")

def run(user_goal: str) -> str:
    msgs = [{"role": "user", "content": user_goal}]
    while True:
        resp = claude.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=2048,
            tools=[SEARCH_TOOL, PAYMENT_TOOL],
            messages=msgs,
        )
        if resp.stop_reason != "tool_use":
            return "".join(b.text for b in resp.content if b.type == "text")
        msgs.append({"role": "assistant", "content": resp.content})
        tool_results = []
        for block in resp.content:
            if block.type != "tool_use":
                continue
            try:
                out = dispatch(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(out),
                })
            except Exception as e:
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "is_error": True,
                    "content": str(e),
                })
        msgs.append({"role": "user", "content": tool_results})

Step 4: confirmation flow with webhooks

Returning a payment URL from create_payment_invoice is not enough. The agent needs to know the invoice was actually paid before continuing. Aurpay POSTs an invoice.paid event to your webhook endpoint as soon as the on-chain settlement is confirmed. The webhook signature dance is annoying; here is the way to do it once and forget it. A minimal FastAPI receiver:

# webhook.py
import os, hmac, hashlib, json
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
SECRET = os.environ["AURPAY_WEBHOOK_SECRET"].encode()
PAID: set[str] = set()  # swap for Redis / your DB in prod

def verify(sig: str, body: bytes) -> bool:
    expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

@app.post("/webhooks/aurpay")
async def aurpay_webhook(request: Request):
    body = await request.body()
    sig = request.headers.get("X-Aurpay-Signature", "")
    if not verify(sig, body):
        raise HTTPException(401, "bad signature")
    event = json.loads(body)
    if event["type"] == "invoice.paid":
        PAID.add(event["data"]["invoice_id"])
    return {"received": True}

On the agent side, expose a wait_for_payment tool that polls the local PAID set, with a GET /invoices/{id} fallback if you want to read Aurpay state directly. Keep polling cheap and bounded; never block the agent loop forever.

import time
from aurpay_client import AurpayClient

client = AurpayClient()

def wait_for_payment(invoice_id: str, timeout_s: int = 900) -> dict:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        if invoice_id in PAID:
            return {"status": "paid", "invoice_id": invoice_id}
        state = client.get(f"/invoices/{invoice_id}")
        if state["status"] in ("paid", "expired", "failed"):
            return state
        time.sleep(5)
    return {"status": "timeout", "invoice_id": invoice_id}

One operational note: signature verification is non-negotiable. Anybody can POST JSON at /webhooks/aurpay from the public internet. The HMAC check above is the bare minimum; in production also enforce idempotency by storing the event id and rejecting duplicates. The MCP server pattern for agent payments uses the same webhook discipline if you prefer to expose this as a tool over MCP.

Step 5: going to mainnet, safety patterns

The single most expensive bug in agent payments is the runaway loop. Claude Sonnet 4.6 will not, on its own, drain your wallet, but a poorly written prompt plus a flaky tool can produce surprising amounts of duplicate spend. Three guards you want before mainnet.

Spending caps. Per-transaction max and rolling daily max. Enforce in code, never trust the model to enforce them.

# tools/guards.py
import os, time
from collections import deque

PER_TX = float(os.environ["AGENT_PER_TX_CAP_USD"])
DAILY  = float(os.environ["AGENT_DAILY_CAP_USD"])
_recent: deque[tuple[float, float]] = deque()  # (timestamp, usd)

def check_spending_limit(amount_usd: float) -> None:
    if amount_usd > PER_TX:
        raise ValueError(
            f"per-tx cap ${PER_TX} exceeded by ${amount_usd}; "
            "human approval required"
        )
    cutoff = time.time() - 86400
    while _recent and _recent[0][0] < cutoff:
        _recent.popleft()
    spent = sum(a for _, a in _recent)
    if spent + amount_usd > DAILY:
        raise ValueError(
            f"daily cap ${DAILY} would be exceeded "
            f"(${spent:.2f} spent, +${amount_usd:.2f} requested)"
        )
    _recent.append((time.time(), amount_usd))

Human approval gates. Anything above a threshold pauses the agent and pushes a Slack or email approval link. The agent only resumes after a human acknowledges. The dispatcher in agent.py calls check_spending_limit before create_payment_invoice; on raise, surface the exception back to the model so it can ask for help instead of retrying.

Dedicated sub-wallet. Never give the agent a key to your operational treasury. Provision a fresh non-custodial wallet, fund it with the daily cap plus a small buffer, and treat it as disposable. If the agent gets prompt-injected, the blast radius is the wallet balance, not your treasury. This is the same principle behind the non-custodial reasoning for agent wallets: keys and funds belong to the operator, not the platform.

Flipping to mainnet is then a one-line change:

AURPAY_API_BASE=https://api.aurpay.net/v1
AURPAY_API_KEY=live_pk_...

Real-world use cases

B2B procurement bot. “Buy 50 GitHub Copilot seats for the new hires under the engineering budget.” The agent looks up the SKU, checks the total against the daily cap, creates a USDC invoice on ERC-20 for the vendor’s Aurpay-issued payment URL, signs from the procurement sub-wallet, waits for the webhook, then writes the receipt to your finance system. Settlement is final on-chain in minutes, no card decline drama.

AI travel agent. “Book the cheapest flight to Singapore under $400, depart Friday.” The agent searches a partner travel API, picks a fare, creates a USDT-TRC20 invoice (cheap fees, fast finality), and pays. Card-rail equivalents trip 3DS step-ups roughly half the time when the issuing country and merchant country differ; the crypto path does not.

Developer-tool autopay. “Top up Anthropic API credit when balance < $10.” A scheduled agent run checks the dashboard via API, hits the spending guard at $50, creates an invoice, signs, and confirms. Combined with the x402 protocol view of agent payments for sub-cent micropayments, you cover both ends of the agent-spend spectrum: micro-API calls via x402, regular SaaS top-ups via Aurpay invoices.

Limits and failure modes you must handle

  • Chain congestion / gas spikes. ERC-20 fees can cross $20 in a busy hour. For sub-$50 purchases default to TRC-20 USDT or Bitcoin Lightning; route to ERC-20 only above ~$200 where the fee impact is acceptable.
  • Webhook delivery failures. Always have the polling fallback shown in Step 4. Webhooks miss. Production receivers should also persist event ids and tolerate retries (Aurpay will redeliver an unacknowledged event).
  • Idempotency on duplicate invoice creation. The agent loop can retry tool calls if the model decides the previous one failed. Use the order_ref field on the invoice payload as your idempotency key, and dedupe server-side before POSTing.
  • Testnet vs mainnet config drift. A .env set to mainnet on a developer laptop has caused real losses in the wider agent-tooling ecosystem. Gate the live key behind a CI-only secret store and refuse to start in mainnet mode if AGENT_DAILY_CAP_USD is unset.
  • Agent runaway costs. Beyond spending caps, set a hard tool-call ceiling per agent invocation (e.g. 30 tool calls). If the agent has not produced a final answer by then, stop and escalate. This is cheap insurance against pathological loops.

Ship it

Aurpay’s REST API is the path of least resistance for agent-driven crypto payments: testnet + mainnet, payin / payout / orders / invoices, Postman collection, webhooks with HMAC signatures, 0.8% per transaction, non-custodial settlement direct to the merchant wallet. Get your testnet key and the full reference at aurpay.net/documentation. If your use case is simpler, a one-off shareable payment link rather than a full agent loop, the Aurpay Payment Button drops in with no backend at all.

Ricky

Growth Strategist at Aurpay

As a growth strategist at Aurpay, Ricky is dedicated to removing the friction between traditional commerce and blockchain technology. He helps merchants navigate the complex landscape of Web3 payments, ensuring seamless compliance while executing high-impact marketing campaigns. Beyond his core responsibilities, he is a relentless experimenter, constantly testing new growth tactics and tweaking product UX to maximize conversion rates and user satisfaction

Sign Up for Our Newsletter

Get the latest crypto news and updates from the experts at Aurpay.