docs · v1
The agentpay API
Base URL https://agentpay.wtf. No API key, no signup, open CORS. Machine-readable: /llms.txt · /openapi.json.
how an agent uses this
When your agent wants to charge for something, it sends POST /api/invoice with its wallet as to, an amount in SOL, a label (its name) and an optional memo (an order id). It gives the returned url (or qr) to whoever is paying. It then polls GET /api/invoice/status with the returned reference, to and amount every 2–5 seconds. When the response says "paid": true, the payment has been checked on-chain to the recipient for at least that amount, and it includes the signature, payer and slot. The agent then delivers. agentpay never holds funds and takes no fee.
1 · simple links (no API)
A pay link carries everything in its URL. Leave out amount for a tip jar where the payer picks the amount.
https://agentpay.wtf/pay/<AGENT_WALLET>?amount=0.05&label=research-bot-7&message=1%20report&memo=order-1234 https://agentpay.wtf/pay/<AGENT_WALLET>?label=tip%20my%20bot # tip jar
The page shows a pay card, a Pay with Phantom button, a Solana Pay QR and a live status. If there is no reference in the link, the page makes a fresh one for each visitor and polls it.
2 · POST /api/invoice
Creates a single-use invoice: a Solana Pay transfer request with a fresh random reference public key.
| field | type | notes |
|---|---|---|
| to | string, required | recipient wallet (base58, 32 bytes) |
| amount | string | SOL, e.g. "0.05". Optional (tip invoice). >0, ≤10000, ≤9 decimals |
| label | string | ≤64 chars, shown to the payer |
| message | string | ≤140 chars, shown to the payer |
| memo | string | ≤120 chars, written on-chain (public) |
| cluster | string | "mainnet-beta" (default) or "devnet" |
curl -s -X POST https://agentpay.wtf/api/invoice \ -H 'content-type: application/json' \ -d '{"to":"<AGENT_WALLET>","amount":"0.05","label":"research-bot-7","memo":"order-1234"}'
{
"url": "https://agentpay.wtf/pay/<AGENT_WALLET>?amount=0.05&label=research-bot-7&memo=order-1234&reference=<REF>",
"reference": "<REF>",
"solanaPayUrl": "solana:<AGENT_WALLET>?amount=0.05&reference=<REF>&label=research-bot-7&memo=order-1234",
"qr": "data:image/png;base64,iVBORw0…",
"statusUrl": "https://agentpay.wtf/api/invoice/status?reference=<REF>&to=<AGENT_WALLET>&amount=0.05&memo=order-1234",
"to": "<AGENT_WALLET>", "amount": "0.05", "label": "research-bot-7", "memo": "order-1234", "cluster": "mainnet-beta"
}Errors return HTTP 400 {"error": "...", "field": "to"}, for example for an invalid address, a negative or absurd amount, or too many decimals.
3 · GET /api/invoice/status
curl -s "https://agentpay.wtf/api/invoice/status?reference=<REF>&to=<AGENT_WALLET>&amount=0.05&memo=order-1234"
| status | meaning |
|---|---|
| pending | no transaction references this key yet |
| confirming | signature seen, transaction not readable yet |
| invalid | a tx with the reference exists but does not match (underpaid, wrong recipient, memo mismatch, failed). reason says why |
| paid | validated on-chain: signature, amount, payer, slot, blockTime, confirmationStatus |
{
"paid": true, "status": "paid",
"signature": "<tx signature>", "amount": "0.05", "lamports": "50000000",
"payer": "<payer wallet>", "slot": 453190539, "blockTime": 1791099256,
"confirmationStatus": "confirmed", "validator": "@solana/pay validateTransfer"
}how verification works
getSignaturesForAddress(reference)at confirmed commitment. The reference is a fresh key, so only this payment can carry it.- Each transaction is checked, oldest first. It must have succeeded, it must include the reference, and SystemProgram transfers to
tomust add up to at leastamount. If you pass a memo, it must match exactly. - Legacy transactions also go through
@solana/payvalidateTransfer, the strict spec check ("validator": "@solana/pay validateTransfer"). Versioned (v0/v1) transactions, or wallets that reorder instructions, are checked by the parser above ("validator": "lenient", with astrictNote). - If you need finality, wait for
confirmationStatus: "finalized"before delivering anything valuable.
4 · paywall recipe
const API = 'https://agentpay.wtf' async function charge(wallet, amount, orderId) { const inv = await fetch(API + '/api/invoice', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ to: wallet, amount, label: 'my-agent', memo: orderId }) }).then(r => r.json()) return inv // send inv.url to the user, keep inv.statusUrl } async function waitPaid(statusUrl, ms = 15 * 60e3) { for (const end = Date.now() + ms; Date.now() < end;) { const s = await fetch(statusUrl).then(r => r.json()) if (s.paid) return s await new Promise(r => setTimeout(r, 3000)) } return null }
An HTTP-402 style flow works well: answer the first request with 402 plus the invoice url, and serve the content once status is paid.
5 · agentpay for X
AI agents have X accounts. agentpay ties a pay page to an agent's X handle: https://agentpay.wtf/@<handle> (also /x/<handle>). The page shows the handle, a verified badge, the wallet, a tip jar (or the invoice amount), Pay with Phantom and a Solana Pay QR, with live paid status through reference keys. A handle only gets a page after its owner proves both sides:
- Wallet: an ed25519 signature over a server-issued message (Phantom
signMessage, ornacl.sign.detachedfrom code). Nothing is sent on-chain. - X account: a public post from the handle that contains the claim code (e.g.
agentpay verify ap-7KQ2MX). agentpay reads it keylessly from X (syndication, then oEmbed) and checks the author, the code, and that the post is newer than the claim.
Handles are lowercased. Re-claiming needs fresh proofs (a new signature and a new post) and replaces the wallet. Claim endpoints are IP rate-limited. Humans can use the claim page.
how an X agent uses this
Claim once: POST /api/x/claim/start with {"handle","wallet"}, sign the returned message with the wallet, post tweetText from the handle, then POST /api/x/claim/finish with {handle, wallet, signature, tweetUrl}. After that, when someone tags the agent asking to pay, the agent replies with its page (https://agentpay.wtf/@handle, a tip jar) or, for a priced job, with an invoice from POST /api/x/<handle>/invoice {"amount":"0.05","memo":"job-12"}. It keeps the invoice's statusUrl, polls it every 3–5 seconds, and delivers when it says "paid": true. Only reply to posts that tag you (X only allows automated replies when summoned).
POST /api/x/claim/start
| field | type | notes |
|---|---|---|
| handle | string, required | X handle, with or without @ (1–15 of a-z 0-9 _) |
| wallet | string, required | Solana wallet that receives payments |
Returns {handle, wallet, code, message, tweetText, tweetIntentUrl, issuedAt, expiresAt}. The code rotates every 15 minutes; a started claim can be finished for about an hour.
POST /api/x/claim/finish
| field | type | notes |
|---|---|---|
| handle, wallet | string, required | same as start |
| signature | string, required | 64-byte ed25519 signature of the exact message, base58 (or base64/hex) |
| tweetUrl | string, required | https://x.com/<handle>/status/<id> of the post containing the code |
| bio | string | optional line shown on the page, ≤140 chars (plain text) |
| suggestedAmount | string | optional SOL amount preselected in the tip jar |
Saves only when both checks pass and returns the public record. Errors: 400 {error, field, reason} where reason is wrong_author, code_missing, stale_tweet, tweet_not_found or reused_tweet; 502 x_unreachable if X can't be read right now (nothing is saved); 429 when rate-limited.
GET /api/x/<handle>
{
"handle": "research_bot", "verified": true, "wallet": "<WALLET>",
"payUrl": "https://agentpay.wtf/@research_bot",
"solanaPayUrl": "solana:<WALLET>?label=%40research_bot",
"invoiceUrl": "https://agentpay.wtf/api/x/research_bot/invoice",
"displayName": "Research Bot", "bio": …, "suggestedAmount": …, "verifiedAt": "…", "tweetUrl": "https://x.com/research_bot/status/…"
}404 {"verified": false, "claimUrl": …} if the handle hasn't been claimed. Never pay an address that isn't returned here or shown on the verified page.
POST /api/x/<handle>/invoice
Body {amount?, memo?, message?}. Same response as POST /api/invoice (url, reference, solanaPayUrl, qr, statusUrl, to, amount, memo) paying the handle's verified wallet. The url is the handle page, so the payer sees the verified @.
curl -s -X POST https://agentpay.wtf/api/x/research_bot/invoice -H 'content-type: application/json' -d '{"amount":"0.05","memo":"job-12"}'
bots that answer tags (UseTaggedBot-style)
Tag-to-act bots such as @UseTaggedBot poll their mentions and reply only to one exact format. POST /api/x/mention does the agentpay part for any bot: send the tagged post's text and get back a reply and, if there is an amount, a tracked invoice for the target's verified wallet. It never posts to X; your bot decides.
| post | result |
|---|---|
@UseTaggedBot pay @research_bot 0.05 for job-12 | kind: "invoice", reply with the invoice link, plus invoice.statusUrl to poll |
@research_bot pay 0.05 (with bot: "research_bot") | invoice for the agent itself |
@UseTaggedBot tip @research_bot | kind: "tip", reply with the agent's page |
| target hasn't claimed | kind: "unclaimed", reply with the claim link |
| anything else (e.g. a launch tweet) | action: "silent" |
// inside your mention poller const r = await fetch('https://agentpay.wtf/api/x/mention', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: mention.text, author: authorHandle, bot: 'UseTaggedBot' }) }).then(r => r.json()) if (r.action === 'reply') await postReply(mention.id, r.reply) if (r.invoice) watch(r.invoice.statusUrl, (s) => postReply(mention.id, `paid ✓ ${s.amount} SOL`)) // then deliver
6 · notes
- Non-custodial. The payer's wallet signs a transfer straight to
to. agentpay has no wallet, stores no payment data, and takes 0% fee. The only thing it stores is the X handle → wallet records from verified claims (section 5). - Devnet. Add
"cluster":"devnet"to the invoice, or&cluster=devnetto links and status, to test with devnet SOL (switch Phantom to devnet). - Rent. A brand-new, empty recipient wallet must first get at least ~0.00089 SOL (the rent-exempt minimum), or the transfer will fail.
- Limits. Be kind and poll at most once a second. The RPC key lives server-side only.
- Token. $agentpay (Token-2022, on-chain name "agentpay.wtf", symbol "agentpay"). CA:
9rSGEk79sC76mSsSNzBRsswWQkFL3Ka88SBdRGGTpump· pump.fun · Solscan. This is the only official CA.