Pay with x402 (USDC on Solana) — for AI agents
Get an API key without signing up: read the 402, register your Solana wallet with a Sign-In-With-Solana signature, send USDC from it, sign again and receive an sk-pv key. Exact requests, responses, a TypeScript sample and error codes.
An AI agent can get an API key without a sign-up form, an e-mail or a card. It registers its Solana wallet by signing a short text message, pays USDC from that wallet, signs once more and receives an ordinary sk-pv-… API key. From then on it is a normal client: the same https://getvibeapi.com/v1 endpoints, the same catalog prices, the same limits.
Here x402 is used as a prepaid deposit, not per-request settlement. You fund a balance once (1 USDC = $1), and each request is debited from it at the published price. The 402 response follows x402 v2: a base64 PAYMENT-REQUIRED header with accepts and the sign-in-with-x (SIWX / Sign-In-With-Solana) extension.
/api/x402/verify before the transfer (step 2). A transfer from a wallet that was not registered first — or a withdrawal from an exchange or any custodial wallet, which cannot sign — is not credited automatically; it waits for a manual review.At a glance
| Parámetro | Tipo | Descripción |
|---|---|---|
Network | CAIP-2 | Solana mainnet — solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
Asset | SPL mint | USDC, mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v (6 decimals). Only this mint is credited; a token that merely calls itself "USDC" is ignored. |
Minimum | USD | $1 (accepts[0].amount = "1000000" atomic units). Send more to prefund; any amount at or above the minimum is credited in full. |
payTo | address | Read it from the 402 response every time. Never hard-code it or take it from a third party. |
Credit time | minutes | Usually 2–4 minutes: the transfer must be finalized on-chain, and deposits are scanned every 2 minutes. |
Fees | — | We add none. The sender pays the Solana network fee (a fraction of a cent). |
Refunds | manual | Not automatic. Contact [email protected] with the transaction signature. |
The flow, step by step
- Call any
/v1endpoint without a key and read the402. - Register the wallet: ask for a challenge, sign its
messagewith the wallet you will pay from, and post it to/api/x402/verify. It answers402withcause: "x402_wallet_registered_awaiting_deposit"— the wallet is now registered. - Send at least the minimum USDC on Solana to
payTofrom that same wallet. - Wait for the deposit to be credited (about 2–4 minutes).
- Sign a fresh challenge and post it to
/api/x402/verifyagain: now it returns an API key. - Call
/v1withAuthorization: Bearer sk-pv-….
1. Read the 402
Any /v1 request without a credential answers 402 Payment Required. The body is our usual error envelope with a compact error.x402 summary:
curl -si https://getvibeapi.com/v1/modelsHTTP/2 402
content-type: application/json
payment-required: eyJ4NDAyVmVyc2lvbiI6Mi… (base64 JSON, below)
{
"error": {
"message": "Top up with USDC on Solana: register your wallet first (sign the challenge, POST it to /api/x402/verify), then send at least $1 USDC to 9N5KTgRBfGarFVrJJi1KQg8qoD393rayWBjf1ejXRrkc. Register your wallet BEFORE paying: sign the sign-in-with-x challenge with the Solana wallet you will pay from and POST it to /api/x402/verify. Then send USDC on Solana from that same wallet (you must control its private key), and POST a freshly signed challenge to /api/x402/verify again to receive your API key. Deposits from a wallet that was not registered first, or from an exchange or any other custodial wallet, are not credited automatically and require manual operator review.",
"type": "insufficient_quota",
"code": 402,
"cause": "missing_key",
"x402": {
"payTo": "9N5KTgRBfGarFVrJJi1KQg8qoD393rayWBjf1ejXRrkc",
"asset": "USDC",
"network": "solana",
"minDepositUsd": 1,
"docs": "https://getvibeapi.com/docs/x402"
},
"request_id": "…"
}
}The PAYMENT-REQUIRED header, base64-decoded, is the machine-readable x402 object:
curl -s -D - -o /dev/null https://getvibeapi.com/v1/models \
| awk 'tolower($1)=="payment-required:"{print $2}' | tr -d '\r' | base64 -d | jq{
"x402Version": 2,
"error": "Payment required. First sign the sign-in-with-x challenge with your wallet and POST it to /api/x402/verify (registers the wallet), then send at least $1 USDC (mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v) on Solana from that wallet to the payTo address, then sign in again for your API key.",
"resource": { "url": "https://getvibeapi.com/v1/models", "description": "…", "mimeType": "application/json" },
"accepts": [{
"scheme": "exact",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"amount": "1000000",
"asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"payTo": "9N5KTgRBfGarFVrJJi1KQg8qoD393rayWBjf1ejXRrkc",
"maxTimeoutSeconds": 600,
"extra": { "name": "USDC", "decimals": 6, "depositWarning": "…", "docs": "https://getvibeapi.com/docs/x402" }
}],
"extensions": {
"sign-in-with-x": {
"info": {
"domain": "getvibeapi.com",
"uri": "https://getvibeapi.com/v1/models",
"version": "1",
"nonce": "…",
"issuedAt": "…",
"expirationTime": "…",
"statement": "Sign in to register this Solana wallet with your GetVibe API account. Only continue if you control this wallet. USDC is credited automatically only when it is sent from a wallet registered before the payment; deposits from an exchange or other custodial wallet cannot be linked to an account.",
"resources": ["https://getvibeapi.com/v1/models"]
},
"supportedChains": [{ "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "type": "ed25519", "signatureScheme": "siws" }]
}
}
}| Parámetro | Tipo | Descripción |
|---|---|---|
accepts[0].payTo | string | The Solana address to pay. Take it from here, not from memory. |
accepts[0].asset | string | The USDC mint. Check it equals EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v before you pay. |
accepts[0].amount | string | Minimum deposit in atomic units (divide by 10^6). |
accepts[0].network | string | CAIP-2 chain id. Solana mainnet only — USDC on any other network cannot be credited. |
extensions["sign-in-with-x"] | object | A signable challenge. Signing it and posting it to /api/x402/verify BEFORE paying registers your wallet (step 2); a fresh one from /api/x402/challenge works the same way. |
2. Register the wallet: get a challenge, sign it, post it
curl -s https://getvibeapi.com/api/x402/challenge \
-H "Content-Type: application/json" \
-d '{"wallet":"9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"}'{
"ok": true,
"wallet": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"message": "getvibeapi.com wants you to sign in with your Solana account:\n9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM\n\nSign in to register this Solana wallet with your GetVibe API account. Only continue if you control this wallet. USDC is credited automatically only when it is sent from a wallet registered before the payment; deposits from an exchange or other custodial wallet cannot be linked to an account.\n\nURI: https://getvibeapi.com/v1\nVersion: 1\nChain ID: 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\nNonce: 684c31b4bc2b7709b95d61dcb5a03fea\nIssued At: 2026-09-27T13:57:32.210Z\nExpiration Time: 2026-09-27T14:02:32.210Z\nResources:\n- https://getvibeapi.com/v1",
"signInWithX": {
"info": {
"domain": "getvibeapi.com",
"uri": "https://getvibeapi.com/v1",
"version": "1",
"nonce": "684c31b4bc2b7709b95d61dcb5a03fea",
"issuedAt": "2026-09-27T13:57:32.210Z",
"expirationTime": "2026-09-27T14:02:32.210Z",
"statement": "Sign in to register this Solana wallet with your GetVibe API account. Only continue if you control this wallet. USDC is credited automatically only when it is sent from a wallet registered before the payment; deposits from an exchange or other custodial wallet cannot be linked to an account.",
"resources": ["https://getvibeapi.com/v1"]
},
"supportedChains": [{ "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "type": "ed25519", "signatureScheme": "siws" }]
},
"warning": "Register your wallet BEFORE paying: sign the sign-in-with-x challenge with the Solana wallet you will pay from and POST it to /api/x402/verify. Then send USDC on Solana from that same wallet (you must control its private key), and POST a freshly signed challenge to /api/x402/verify again to receive your API key. Deposits from a wallet that was not registered first, or from an exchange or any other custodial wallet, are not credited automatically and require manual operator review.",
"expiresAt": "2026-09-27T14:02:32.210Z"
}Sign the message string exactly as returned (UTF-8 bytes, every newline included) with the wallet's ed25519 key — the same operation as a wallet's signMessage. A challenge lives 5 minutes and can be used once; ask for a new one for every attempt. Then post the proof to /api/x402/verify exactly as in step 5. Before your first deposit it answers 402 with cause: "x402_wallet_registered_awaiting_deposit": the wallet is registered, and USDC it sends from now on is credited to it. A transfer sent before this step is not credited automatically.
Registering does not attach the wallet to a dashboard account you may be signed in to: its deposits credit the wallet's own account, and the key from step 5 belongs to that account. To credit an existing dashboard account instead, link the wallet in the dashboard top-up page. One wallet belongs to one account; once it does, verify from any other signed-in account answers 409 and changes nothing.
3. Pay from the registered wallet
Send a USDC SPL transfer (transferChecked) of at least 1000000 atomic units to payTo, signed by the wallet you registered in step 2. No memo is needed for this path: the registered sender address identifies you. Keep the transaction signature — it is your receipt.
4. Wait for the credit
Only finalized transactions are credited. Deposits are scanned every 2 minutes, so plan for 2–4 minutes. You do not need to poll the chain yourself: repeat step 5 every 30–60 seconds; until the credit lands, verify answers 402 with cause: "x402_wallet_registered_awaiting_deposit". Give up and contact support after about 15 minutes.
5. Sign again and receive the key
Get a fresh challenge (step 2) and post its SIWX fields plus your address, the chain fields and the signature (base58 or base64, 64 bytes). The same JSON may instead be sent base64-encoded in a SIGN-IN-WITH-X header.
curl -s https://getvibeapi.com/api/x402/verify \
-H "Content-Type: application/json" \
-d '{
"domain": "getvibeapi.com",
"address": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"uri": "https://getvibeapi.com/v1",
"version": "1",
"chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"type": "ed25519",
"signatureScheme": "siws",
"nonce": "684c31b4bc2b7709b95d61dcb5a03fea",
"issuedAt": "2026-09-27T13:57:32.210Z",
"expirationTime": "2026-09-27T14:02:32.210Z",
"statement": "Sign in to register this Solana wallet with your GetVibe API account. Only continue if you control this wallet. USDC is credited automatically only when it is sent from a wallet registered before the payment; deposits from an exchange or other custodial wallet cannot be linked to an account.",
"signature": "<base58 ed25519 signature of message>"
}'{
"ok": true,
"wallet": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"key": "sk-pv-…",
"type": "payg",
"note": "Store this key securely. Every subsequent request uses it like any other API key."
}6. Use the API
curl https://getvibeapi.com/v1/chat/completions \
-H "Authorization: Bearer $GETVIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-6.1-sol","messages":[{"role":"user","content":"Hello"}]}'
curl https://getvibeapi.com/v1/balance -H "Authorization: Bearer $GETVIBE_API_KEY"
# {"balance": 1, "currency": "USD", ...}To top up later, send more USDC from the same wallet to the same payTo. The wallet is already registered, so it is credited to the same account and your key keeps working — no new sign-in needed. When the balance runs out, /v1 answers 402 with a fresh PAYMENT-REQUIRED header.
TypeScript: the whole flow
Node 20+, npm i @solana/web3.js @solana/spl-token tweetnacl bs58. The wallet secret is read from the environment and never leaves the process.
import { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction } from '@solana/web3.js';
import { createTransferCheckedInstruction, getOrCreateAssociatedTokenAccount } from '@solana/spl-token';
import nacl from 'tweetnacl';
import bs58 from 'bs58';
const API = 'https://getvibeapi.com';
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const wallet = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_SECRET_KEY!));
const connection = new Connection(process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com', 'confirmed');
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
// Sign a fresh challenge with the wallet and post it to /api/x402/verify.
async function signAndVerify(): Promise<Response> {
const address = wallet.publicKey.toBase58();
const challenge = await fetch(`${API}/api/x402/challenge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ wallet: address }),
}).then((r) => r.json());
const signature = bs58.encode(nacl.sign.detached(new TextEncoder().encode(challenge.message), wallet.secretKey));
const chain = challenge.signInWithX.supportedChains[0];
return fetch(`${API}/api/x402/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...challenge.signInWithX.info,
address,
chainId: chain.chainId,
type: chain.type,
signatureScheme: chain.signatureScheme,
signature,
}),
});
}
// 1. Read the payment requirements from the 402.
const probe = await fetch(`${API}/v1/models`);
if (probe.status !== 402) throw new Error(`expected 402, got ${probe.status}`);
const required = JSON.parse(Buffer.from(probe.headers.get('payment-required')!, 'base64').toString('utf8'));
const accept = required.accepts[0];
if (accept.asset !== USDC_MINT || !accept.network.startsWith('solana:')) throw new Error('unexpected asset/network');
// 2. Register the wallet BEFORE paying: a verified signature marks it as an expected payer.
const registered = await signAndVerify();
const reg = await registered.json();
if (registered.status !== 402 || reg.error?.cause !== 'x402_wallet_registered_awaiting_deposit') {
throw new Error(`registration failed: ${registered.status} ${JSON.stringify(reg)}`);
}
// 3. Pay at least the minimum (here: exactly the minimum) from the SAME wallet.
const mint = new PublicKey(accept.asset);
const amount = BigInt(accept.amount);
const from = await getOrCreateAssociatedTokenAccount(connection, wallet, mint, wallet.publicKey);
const to = await getOrCreateAssociatedTokenAccount(connection, wallet, mint, new PublicKey(accept.payTo));
const tx = new Transaction().add(
createTransferCheckedInstruction(from.address, mint, to.address, wallet.publicKey, amount, accept.extra.decimals),
);
const txSignature = await sendAndConfirmTransaction(connection, tx, [wallet], { commitment: 'finalized' });
console.log('paid', txSignature);
// 4–5. Sign a fresh challenge until the deposit is credited, then keep the key.
async function getApiKey(): Promise<string> {
for (let attempt = 0; attempt < 30; attempt++) {
const res = await signAndVerify();
if (res.ok) return (await res.json()).key;
if (res.status !== 402) throw new Error(`verify failed: ${res.status} ${await res.text()}`);
await sleep(30_000); // deposit not credited yet
}
throw new Error(`deposit ${txSignature} not credited after 15 minutes — contact [email protected]`);
}
const apiKey = await getApiKey(); // store it securely: it is shown once
// 6. A normal OpenAI-compatible call.
const reply = await fetch(`${API}/v1/chat/completions`, {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'gpt-6.1-sol', messages: [{ role: 'user', content: 'Hello' }] }),
}).then((r) => r.json());
console.log(reply.choices[0].message.content);Using a browser wallet instead of a raw key: sign each challenge with wallet.signMessage(new TextEncoder().encode(challenge.message)) (through bs58.encode) — once before the transfer, once after — and send the transfer through the wallet in between.
Already have a dashboard account?
The flow above creates a new account keyed by your wallet. To add USDC to an account you already use, open Dashboard → Billing → Top up and choose USDC (Solana). The dialog shows the address, a Solana Pay QR code and your personal memo code (GV + 8 characters):
- Wallet: scan the QR. It carries your account's reference and memo, so the deposit lands on your existing account.
- Exchange withdrawal: withdraw USDC on the Solana network to the address and paste the memo code into the withdrawal's memo field. This is the only way an exchange withdrawal can be credited automatically.
- Link a wallet: the dialog can link a self-custody wallet by signature. After that, USDC from that wallet credits your account even without the memo. A wallet can belong to one account only.
Caveats
- Unregistered senders. A transfer from a wallet that did not complete step 2 before paying is held for manual review, not credited. Dashboard clients can submit its transaction signature in the USDC tab; otherwise contact support with it.
- Exchanges and custodial wallets. They cannot sign, so they cannot register. Without a memo code a withdrawal is held for review. Contact support with the transaction signature.
- Below the minimum. A transfer under $1 is recorded but not credited automatically. Contact support with the signature.
- Wrong token or network. Only genuine USDC on Solana mainnet is credited. Other tokens and USDC on other networks cannot be credited and may be lost.
- Refunds are not automatic. There is no self-service refund for USDC; write to [email protected].
- No key without funds. Verify issues a key only once the wallet has a credited, positive balance.
Limits
A wallet account is an ordinary pay-as-you-go account: catalog prices, and a throughput tier set by the current balance. See Errors & limits.
| Parámetro | Tipo | Descripción |
|---|---|---|
balance ≥ $0 | tier | 300 requests/min, 10 concurrent requests. |
balance ≥ $1 | tier | 1100 requests/min, 50 concurrent requests. |
balance ≥ $2 | tier | 6600 requests/min, 300 concurrent requests. |
balance ≥ $4 | tier | 19800 requests/min, 900 concurrent requests. |
balance ≥ $12 | tier | 44000 requests/min, 2000 concurrent requests. |
Errores
| Parámetro | Tipo | Descripción |
|---|---|---|
402 on /v1 | missing_key | No credential. Read the PAYMENT-REQUIRED header and pay. |
402 on /v1 (with a key) | balance | Balance exhausted. Send more USDC from the same wallet. |
402 on /api/x402/verify | x402_wallet_registered_awaiting_deposit | Signature is valid and the wallet is registered, but no credited deposit yet. Pay from this wallet (if you have not), wait and retry with a fresh challenge. |
409 on /api/x402/verify | wallet_linked_to_other_account | You are signed in to a dashboard account, and this wallet already belongs to a different account. Nothing is registered, linked or issued. Sign out (agents send no cookie), or use the account that owns the wallet. |
409 on /api/x402/verify | wallet_linked_to_account | The wallet is linked to a regular e-mail account. A wallet signature cannot mint keys on it: create keys for that account in the dashboard. |
invalid_siwx_signature | 400 | The signature does not match the address. Sign the exact message bytes with the paying wallet. |
invalid_siwx_nonce | 400 | Unknown, already-used, or issued for another wallet. Request a new challenge. |
invalid_siwx_expired | 400 | The 5-minute challenge window passed. Request a new challenge. |
invalid_siwx_domain_mismatch / uri_mismatch | 400 | domain must be getvibeapi.com and uri must start with https://getvibeapi.com. Echo the challenge fields unchanged. |
invalid_siwx_unsupported_chain | 400 | chainId must be solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp and type must be ed25519. |
invalid_siwx_malformed_signature | 400 | The address is not a Solana address, or the signature is not 64 bytes in base58/base64. |
invalid_request / invalid_wallet | 400 | The challenge body must be JSON with a valid Solana wallet address. |