Quick start
- Add a store in the dashboard with the Solana wallet your money should go to. A public address only: never a seed phrase.
- Create an API key for that store (Dashboard → API keys). It is shown once; keep it on your server.
- Make an invoice and send your customer to its
pay_page. - Get told when it's paid: a signed
invoice.confirmedwebhook, or pollGET /api/v1/invoices/:id.
curl -X POST https://gateway.advancebitcoin.org/api/v1/invoices \ -H "Authorization: Bearer abcpay_live_..." \ -H "Content-Type: application/json" \ -d '{"amount": 250, "order_id": "order-1042", "success_url": "https://yourshop.com/thanks"}'
Reply 201:
{
"id": "fe4bd314-4288-4fbc-9378-c6a402a52bc6",
"status": "pending",
"amount": 250.017,
"token_mint": "3BWcLccJqqiy2k3QsfnmZpsrKTH1jjVCNTFkVQ8dcY6d",
"order_id": "order-1042",
"description": null,
"fiat_amount": null,
"fiat_currency": null,
"test_mode": false,
"reference": "H8Doj858tCc96ZWsg3m3XX5fWcyGMgWQviTtjSFD2Dtx",
"recipient": "<your store wallet>",
"pay_page": "https://gateway.advancebitcoin.org/pay/fe4bd314-4288-4fbc-9378-c6a402a52bc6",
"pay_url": "solana:<your store wallet>?amount=250.017&spl-token=3BWc...&reference=H8Do...",
"expires_at": "2026-10-02T12:15:00.000Z",
"created_at": "2026-10-02T12:00:00.000Z"
}The amount gets a few extra digits (250.017): every open invoice has a unique amount, so even a payment sent by hand, without the QR, is matched to the right invoice.
Keys and test mode
Send the store's key on every call as Authorization: Bearer abcpay_…. Only a hash of each key is stored; revoke one any time on the API keys page.
Test mode is a switch in the dashboard. Test stores run on Solana devnet with free test coins, and their keys start abcpay_test_. Invoices, links and webhooks from them carry "test_mode": true: check it and never ship an order for a test payment. Test SOL comes from faucet.solana.com and test USDC from faucet.circle.com; switch the paying wallet to devnet (Phantom: Settings → Developer Settings → Testnet Mode).
Invoices
POST /api/v1/invoices
| Field | Type | What it does |
|---|---|---|
amount | number | Amount in token units. Required unless you send fiat_amount. |
fiat_amount | number | A dollar price instead of amount (see Prices in dollars). Send with fiat_currency: "USD". |
token_mint | string | null | Omit for the store's default ($ABC on live stores). null = SOL. Or any SPL mint: USDC EPjFWdd5…Dt1v, USDT Es9vMFrz…wNYB. |
order_id | string | Your own reference, echoed back in the webhook. |
description | string | What it's for; shown to the customer on the pay page and receipt. |
success_url / cancel_url | string | Where to send the customer after paying / if they leave. |
GET /api/v1/invoices/:id
Checks the chain first, so the status is current. Adds amount_paid, tx_signature, payer_wallet and customer_note.
POST /api/v1/invoices/:id/cancel
Cancels a pending invoice. A confirmed one can never be cancelled.
Statuses: pending → confirmed, or expired after 15 minutes unpaid, or cancelled. A payment that lands in the last seconds is still credited for 24 hours after expiry.
Prices in dollars
Send fiat_amount with fiat_currency: "USD" and the customer pays the matching amount of the token at today's price, fixed for the invoice's 15 minutes. USDC and USDT count as exactly $1. SOL uses the middle of four public price sources (Coinbase, Kraken, Jupiter, CoinGecko); with none answering, no invoice is made rather than a wrong one. Amounts round up, never short. Dollar pricing in $ABC switches on by itself once $ABC trades with real depth.
curl -X POST https://gateway.advancebitcoin.org/api/v1/invoices \ -H "Authorization: Bearer abcpay_live_..." \ -H "Content-Type: application/json" \ -d '{"fiat_amount": 25, "fiat_currency": "USD", "token_mint": null, "description": "Logo design"}' // token_mint null = paid in SOL; the reply's amount is the SOL for $25 right now
Payment links
One link and QR code for a product or price, shared anywhere and paid again and again: each customer who goes ahead gets an invoice of their own, so webhooks work exactly as above (they carry the link_id). Fixed-price links can also be paid straight from a wallet scan.
POST /api/v1/links
| Field | Type | What it does |
|---|---|---|
title | string | Required. What the customer is paying for. |
amount | number | Fixed price. Leave it out to let the customer choose (min_amount sets a floor). |
price_currency | "USD" | Make amount and min_amount dollars, paid in token_mint at today's price. |
token_mint | string | null | As for invoices. |
ask_note / note_required | string / boolean | A question for the customer ("Table number"), and whether they must answer. |
max_payments | number | Stop after this many payments. |
description, success_url | string | Shown on the link page / where to send the customer after paying. |
curl -X POST https://gateway.advancebitcoin.org/api/v1/links \ -H "Authorization: Bearer abcpay_live_..." \ -H "Content-Type: application/json" \ -d '{"title": "Flat white", "amount": 4.5, "price_currency": "USD", "token_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"}' // reply: { "id": "...", "url": "https://gateway.advancebitcoin.org/l/8hK2pQ3xYz", "active": true, ... }
GET /api/v1/links lists them; GET /api/v1/links/:id adds paid_count; PATCH /api/v1/links/:id with {"active": false} turns one off.
Webhooks
Set a webhook URL on the store. When a payment confirms we POST invoice.confirmed, signed with the store's webhook secret as X-ABC-Signature: sha256=<hex HMAC of the raw body>. If your server doesn't answer 2xx we try again for about a day (1 min, 5 min, 30 min, 2 h, 12 h), so make your handler safe to run twice. Confirmation runs every minute even when nobody has the pay page open.
{
"event": "invoice.confirmed",
"invoice": {
"id": "fe4bd314-4288-4fbc-9378-c6a402a52bc6",
"order_id": "order-1042",
"amount": 250.017,
"token_mint": "3BWcLccJqqiy2k3QsfnmZpsrKTH1jjVCNTFkVQ8dcY6d",
"status": "confirmed",
"tx_signature": "5xQ...",
"payer_wallet": "7Np4...",
"reference": "H8Do...",
"description": null,
"fiat_amount": null,
"fiat_currency": null,
"customer_note": null,
"link_id": null,
"test_mode": false
}
}import crypto from "node:crypto"; import express from "express"; const app = express(); // keep the raw body: the signature covers those exact bytes app.post("/webhooks/abc", express.raw({ type: "application/json" }), (req, res) => { const expected = "sha256=" + crypto .createHmac("sha256", process.env.ABC_WEBHOOK_SECRET) .update(req.body) .digest("hex"); const got = String(req.get("X-ABC-Signature") || ""); const valid = got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected)); if (!valid) return res.sendStatus(401); const { event, invoice } = JSON.parse(req.body); // a test payment is play money: never ship an order for it if (event === "invoice.confirmed" && !invoice.test_mode) { // mark invoice.order_id as paid (safe to run twice: retries can repeat) } res.sendStatus(200); });
Agent payments (x402)
Charge per API call. AI agents and apps pay with the open x402 standard: your API answers 402 Payment Required with a price, the agent signs a USDC transfer to your store wallet, and the gateway, as the x402 facilitator, checks it, pays the network fee and confirms it on Solana. Point any x402 library at https://gateway.advancebitcoin.org/api/x402 and send your API key on verify and settle.
| Field | Type | What it does |
|---|---|---|
Networks | CAIP-2 | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp (live stores), solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 (test stores) |
Tokens | USDC, USDT or $ABC on mainnet; test USDC on devnet. Smallest price $0.001 (1 $ABC). | |
Pay to | Must be your store's wallet, which must already hold a USDC account. | |
Fees | The gateway pays the network fee. Payments asking for an unusually high fee are refused. |
import { paymentMiddleware, x402ResourceServer } from "@x402/express"; import { ExactSvmScheme } from "@x402/svm/exact/server"; import { HTTPFacilitatorClient } from "@x402/core/server"; const facilitator = new HTTPFacilitatorClient({ url: "https://gateway.advancebitcoin.org/api/x402", createAuthHeaders: async () => { const auth = { Authorization: `Bearer ${process.env.ABC_API_KEY}` }; return { verify: auth, settle: auth, supported: {} }; }, }); const server = new x402ResourceServer(facilitator) .register("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", new ExactSvmScheme()); app.use(paymentMiddleware({ "GET /weather": { accepts: [{ scheme: "exact", price: "$0.01", network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", payTo: "<your store wallet>" }], description: "Weather for one city", }, }, server));
The full setup, an agent-side example and your paid calls are on Dashboard → Agent payments. To see one happen, the gateway runs its own paid API (a token safety check): be the agent and pay for one call, or point any x402 client at https://gateway.advancebitcoin.org/api/agent/token-check?mint=<mint>.
Checkout and wallets
/pay/:id is the hosted pay page; /l/:slug is a payment link. Both QR codes are Solana Pay transaction requests (/api/pay/:id/tx, /api/link/:slug/tx): the wallet asks the gateway for the exact payment, and a wallet that can't pay (not enough of the token, or of SOL for the fee) is told why before anything is signed. A basic transfer QR is offered for older wallets.
Errors and limits
| Field | Type | What it does |
|---|---|---|
400 | Something in the request is wrong; error says what. | |
401 | Missing, wrong or revoked API key. | |
404 | Not found, or it belongs to another store. | |
429 | Too many requests (30 a minute per key for creating invoices or links). Retry-After says when. | |
503 | Temporarily unavailable. Safe to retry; nothing was created. |
Live state of every part of the gateway: /status. Questions: abc@advancebitcoin.org.