DEV Community

Cover image for x402 SMS Verification: Pay-Per-Call OTP for Autonomous Agents
VirtualSMS
VirtualSMS

Posted on

x402 SMS Verification: Pay-Per-Call OTP for Autonomous Agents

TL;DR

AI agents need to spend money. Operators don't want to manage API keys for every agent. HTTP 402 Payment Required + Coinbase x402 facilitator = agent pays once, gets api_key + balance, spends down on real services. Here's how I wired it on our SMS verification API, with working Python code.

What is x402?

HTTP 402 has been a "reserved for future use" status code in the spec since 1997. In 2025, Coinbase (with Cloudflare and others) built the x402 protocol on top of it — a standard way to settle USDC micropayments inline with HTTP requests.

The flow is dead simple from an agent's perspective:

  1. Agent makes a normal POST to your endpoint
  2. Server returns 402 Payment Required with a JSON manifest listing accepted networks (Base / Solana / Polygon), token (USDC / USDT), amount, and recipient address
  3. Agent (using x402-fetch or a similar SDK) signs an EIP-3009 authorization, retries with the X-PAYMENT header
  4. Server verifies via the Coinbase facilitator, settles on-chain, returns 200 OK with whatever the agent paid for

No API keys exchanged out-of-band. No paid tier flow. No human in the loop.

Why deposit-first instead of per-call?

The "obvious" x402 design is: every API call returns 402, agent pays per request. We tried it. It doesn't work for variable-priced services.

SMS verification ranges from $0.05 (cheap-country Discord) to $5 (premium WhatsApp on a high-block carrier). If x402 charges a flat $0.10 per call, you either:

  • Lose money on the $5 service (you charged $0.10, paid the carrier $5)
  • Rip off the agent on the $0.05 service (you charged $0.10, retail is $0.05)

Per-call flat-fee only works for fixed-cost services. For variable-priced retail, you need deposit-first:

  1. Agent deposits $X via x402 (one 402 → settle → 200 cycle)
  2. Agent gets api_key + balance
  3. Agent uses api_key on the normal REST endpoints, debited at retail prices
  4. Refunds (when SMS doesn't arrive within timeout) credit back to the api_key balance

Same protocol surface from the agent's perspective (one 402 dance), real retail pricing on the back end.

The endpoint

POST /api/v1/x402/topup

An empty body returns the manifest:

{
  "x402Version": 1,
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "token": "USDC",
      "maxAmountRequired": "5000000",
      "resource": "https://virtualsms.io/api/v1/x402/topup",
      "description": "Top up VirtualSMS API balance — pay $5.00 USDC on base, receive API key + $5.00 balance",
      "payTo": "0xfEc54264350d97d9b63f9Cc415BAF708C4695F32",
      "maxTimeoutSeconds": 60,
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ],
  "error": "Payment required"
}
Enter fullscreen mode Exit fullscreen mode

Three networks accepted: Base USDC, Solana USDC, Solana USDT. $2 minimum deposit, $5 default, $10,000 max.

Working agent code

Using x402-fetch (Coinbase's reference SDK):

import os
import time
from x402 import wrap_fetch
from web3 import Account

# Agent's own wallet (separate from the recipient address)
account = Account.from_key(os.environ["AGENT_PRIVATE_KEY"])
fetch = wrap_fetch(account=account, network="base")

# 1. Deposit — single 402 -> settle -> 200 cycle
deposit = fetch("https://virtualsms.io/api/v1/x402/topup", method="POST")
api_key = deposit.json()["api_key"]
balance = deposit.json()["balance_usd"]
print(f"Got api_key with ${balance} balance")

# 2. Buy a number — normal REST, no x402 needed
order = fetch(
    "https://virtualsms.io/api/v1/customer/purchase",
    method="POST",
    headers={"X-API-Key": api_key},
    json={"service": "discord", "country": "id"},
)
order_id = order.json()["order_id"]
phone = order.json()["phone_number"]
print(f"Got number: {phone} (order: {order_id})")

# 3. Poll for SMS
while True:
    status = fetch(
        f"https://virtualsms.io/api/v1/customer/order/{order_id}",
        headers={"X-API-Key": api_key},
    ).json()
    if status.get("sms_received"):
        print(f"OTP: {status['sms_text']}")
        break
    time.sleep(5)
Enter fullscreen mode Exit fullscreen mode

What the API key buys

Once an agent has the api_key + balance, the rest of the API is pure REST:

  • GET /api/v1/catalog/services — full service catalog (~2,500 services)
  • GET /api/v1/catalog/countries — 145+ countries with live availability
  • POST /api/v1/customer/purchase — buy a number for a service+country
  • GET /api/v1/customer/order/{id} — poll for SMS arrival
  • POST /api/v1/customer/cancel/{id} — cancel + refund (within the 120s cooldown)

Auth header is X-API-Key. The cooldown between cancel and re-purchase is 120 seconds, server-side.

What surprised me building this

  1. Self-payment works on Base. Coinbase's facilitator allows payer == recipient settlements. Useful for ops smoke tests where you don't want to spin up a separate test wallet.
  2. The manifest needs extra: { name, version } for the EIP-712 domain to validate cleanly across all clients. We caught this mid-test.
  3. Solana token-account initialization matters — wallets without a USDC token account just fail silently. Worth noting in the manifest description.
  4. Variable timeouts per supplier — some services activate in 30 seconds, others in 20 minutes. The agent should poll on a backoff curve, not a tight loop.

Why this matters for AI agents

The whole point of x402 is removing the human from the payment loop. An autonomous agent doing account signups, OTP receipts, or workflow automation can't pause to email an operator for an api_key. With deposit-first x402:

  • Agent budgets $50 in USDC at the start of a workflow
  • Agent does whatever — verify accounts, rent numbers, pull OTPs
  • Agent never asks for credentials or keys

This is the same idea Coinbase had when shipping x402 in the first place: HTTP at scale, but agentic.

Try it

If you're building agent workflows that need SMS verification, the endpoint is live. The manifest is real, settlement works, the code above is exact. Hit /api/v1/x402/info for the current state of accepted networks and price floors.

Full API reference: virtualsms.io/api.


Disclosure: I operate virtualsms.io and built this implementation. Happy to answer questions about the protocol or the SMS verification side in the comments.


Source code & install:

Listed in:

Related read:

Top comments (0)