Axiom Stack

Buy quota (USDC top-up)

When your included monthly allotment runs out, an attestation request returns 402. Buy more by paying USDC to POST /v1/topup, which uses the x402 payment protocol settled through the Coinbase Developer Platform (CDP) facilitator. Quota you buy this way (usdc_quota_remaining) rolls over and is consumed after your monthly allotment.

Devnet beta: payments are devnet USDC (mint 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU).

The flow

  1. POST /v1/topup with your bearer key + the amount, no payment → server returns 402 with x402 PaymentRequirements.
  2. Build + sign an x402 exact Solana payment that satisfies those requirements.
  3. Retry with the base64 payment in the X-PAYMENT header → server runs CDP /verify/settle → re-verifies on-chain → credits quota → 200.

See the full HTTP contract in the REST API reference.

Building the signed payment

The 402 response tells you everything you need in accepts[0]: asset (USDC mint), maxAmountRequired (micro-USDC), payTo (treasury owner wallet), and extra.feePayer (the facilitator's fee payer). Build a v0 versioned transaction with 3 instructions — compute-unit limit, compute-unit price, and an SPL transfer_checked to the treasury's associated token account — with the facilitator as fee payer (it signs + pays gas at settle), then partial-sign with your payer key:

import base64, json, httpx, base58
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.message import MessageV0, to_bytes_versioned
from solders.transaction import VersionedTransaction
from solders.hash import Hash
from solders.signature import Signature
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from spl.token.instructions import (
    transfer_checked, TransferCheckedParams, get_associated_token_address)
from spl.token.constants import TOKEN_PROGRAM_ID

RPC = "https://api.devnet.solana.com"
payer = Keypair.from_bytes(base58.b58decode(PAYER_SECRET_BASE58))   # your funded USDC wallet

def build_x_payment(reqs: dict) -> str:
    mint   = Pubkey.from_string(reqs["asset"])
    owner  = Pubkey.from_string(reqs["payTo"])               # treasury OWNER (not the ATA)
    feepay = Pubkey.from_string(reqs["extra"]["feePayer"])   # facilitator fee payer
    amount = int(reqs["maxAmountRequired"])
    src    = get_associated_token_address(payer.pubkey(), mint)
    dst    = get_associated_token_address(owner, mint)        # facilitator derives the same ATA

    transfer = transfer_checked(TransferCheckedParams(
        program_id=TOKEN_PROGRAM_ID, source=src, mint=mint, dest=dst,
        owner=payer.pubkey(), amount=amount, decimals=6, signers=[]))
    ixs = [set_compute_unit_limit(60_000), set_compute_unit_price(1), transfer]  # 3 instructions

    bh = httpx.post(RPC, json={"jsonrpc":"2.0","id":1,"method":"getLatestBlockhash",
                               "params":[{"commitment":"finalized"}]}).json()["result"]["value"]["blockhash"]
    msg = MessageV0.try_compile(feepay, ixs, [], Hash.from_string(bh))   # fee payer = facilitator
    sig = payer.sign_message(to_bytes_versioned(msg))                    # sign the versioned payload
    vtx = VersionedTransaction.populate(msg, [Signature.default(), sig]) # facilitator slot empty

    payload = {"x402Version": 1, "scheme": "exact", "network": "solana-devnet",
               "payload": {"transaction": base64.b64encode(bytes(vtx)).decode()}}
    return base64.b64encode(json.dumps(payload).encode()).decode()

API = "https://mcp.axiomstack.dev/v1/topup"
hdr = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
body = {"amount_usdc_micros": 1_000_000}                # $1.00

r1 = httpx.post(API, headers=hdr, json=body)           # step 1 -> 402 + requirements
reqs = r1.json()["accepts"][0]
r2 = httpx.post(API, headers={**hdr, "X-PAYMENT": build_x_payment(reqs)}, json=body)
print(r2.status_code, r2.json())                        # 200 -> {credited_attestations, usdc_quota_remaining, ...}

Three details that the facilitator enforces (and that are easy to get wrong):

  • v0 versioned transaction, 3–6 instructions. Legacy / single-instruction transactions are rejected.
  • Sign to_bytes_versioned(message), not bytes(message) — otherwise the signature fails simulation.
  • payTo is the owner wallet, not a token account — the facilitator derives the destination ATA itself.

What you get credited

The amount maps to attestations at your tier's rate (marginal-rate math) — see Pricing & tiers. The credit is idempotent: replaying a settled payment returns 409 already_credited, never a double credit.