Build
Developer hub: start selling on Solana mainnet
From a new account to your first paid API call on Solana mainnet. This is real money in USDC: a Solana transaction cannot be undone on the blockchain. You create the account yourself; nobody sets it up for you.
Real money. New accounts start with small limits, set by us.
Your first call
You need your API key from start.payzilla.ai. Then ask the API what it accepts:
More on your first call
curl https://api-mainnet.payzilla.ai/supported \ -H "x-api-key: <YOUR_KEY>"
The same in Node
// first-call.mjs. Node 20.18 or newer. Run: PAYZILLA_API_KEY=<YOUR_KEY> node first-call.mjs
const res = await fetch("https://api-mainnet.payzilla.ai/supported", { headers: { "x-api-key": process.env.PAYZILLA_API_KEY } });
console.log(res.status, await res.text());You should see: HTTP 200 and a body like this (captured on 15 October 2026; your key is never in the answer):
{"kinds":[{"x402Version":2,"scheme":"exact","network":"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp","extra":{"feePayer":"CE9TKZjh9yQY1NtdZ3MJUByaN41E49NRt1QE4DQW8M1f"}}],"extensions":[],"signers":{"solana:*":["CE9TKZjh9yQY1NtdZ3MJUByaN41E49NRt1QE4DQW8M1f"]}}The network is Solana mainnet in CAIP-2 form. The feePayer is the address that pays the network fee, so the buyer needs no SOL. If you see 401, the key is wrong; send it exactly as shown on the start page. Nothing is charged for this call. A client library link is coming; until then the first paid call is step 0, the demo.
Step 0: see one paid call first
Before you build anything, watch the whole exchange once. The demo agent asks a demo seller for weather data, gets the price (HTTP 402), pays, and gets the data (HTTP 200). You see the payment settle and appear in the ledger. Two ways:
More on see one paid call first
- The real-money demo: about one US cent in USDC from your own wallet. It cannot be undone on the blockchain, and Payzilla does not refund it.
Then come back here and do steps 1 to 5 with your own account.
Before you start
Before your first payment, send 0.01 USDC to your receiving wallet from another wallet so it can receive USDC; payments are refused until then.
More on before you start
Four things, all yours:
- An account. Go to start.payzilla.ai, sign in with your email (a one-time code), give the name buyers will see, and prove you control your receiving wallet by signing a message. That first wallet is your main wallet: it cannot be changed, and you can add more wallets later, up to 5 in all. Signing sends no transaction and moves no money. Use a wallet you control, not an exchange deposit address.
- An API key. It is shown once, on the start page, and we keep only a fingerprint, so we cannot show it again. It starts with
afk_live_. Put it in your server’s secrets, never in a web page, an app or a repository. If it may have leaked, use “Stop this key” on the start page and make a new one. - The base address:
https://api-mainnet.payzilla.ai. - A way to pay as a buyer, to test: a second Solana wallet you control that holds a few cents of USDC, and a client that signs
exact-scheme payments (an x402 client library). Step 4 says why it must be a different wallet.
You also need Node 20.18 or newer to run the examples. Never send anyone a private key or seed phrase. None of this needs one.
New accounts start with small limits that we set. A payment above a limit is refused with a plain error and no money moves. To change your limits, write to [email protected].
The flow
- A buyer calls your API without paying. Your server asks Payzilla for a payment challenge for one of your services and answers HTTP 402 with it. The price is the one set on the service.
- The buyer signs a payment and calls again with a
PAYMENT-SIGNATUREheader. - Your server asks Payzilla to verify the payment, then to settle it.
- Only when settlement succeeds does your server send the paid response. Verify alone is not a payment.
- You see the payment in the ledger and on the Solana explorer.
More on the flow
Today the money goes from the buyer’s wallet to yours. Payzilla’s fee payer pays the Solana network fee, so neither wallet needs SOL for that.
Step 1: Check your key
curl https://api-mainnet.payzilla.ai/supported \ -H "x-api-key: <YOUR_KEY>"
More on check your key
Every call to the API needs your key. Send it in the x-api-key header, not as Authorization: Bearer. Without it the answer is unauthorized. A successful call to /supported lists the scheme and network this API accepts. A 401 means the key is wrong: send it exactly as shown on the start page, starting afk_live_, with no quotes.
You should see: HTTP 200 with the list described above.
Step 2: Create a service
A service is what you sell, with its own name and price. Your code sends only the service id; Payzilla takes the price from the service.
More on create a service
curl -X POST https://api-mainnet.payzilla.ai/v1/services \
-H "x-api-key: <YOUR_KEY>" \
-H "content-type: application/json" \
-d '{"name": "Weather lookup", "price": "2000"}'The same in Node
// create-service.mjs. Node 20.18 or newer. Run: PAYZILLA_API_KEY=<YOUR_KEY> node create-service.mjs
const res = await fetch("https://api-mainnet.payzilla.ai/v1/services", {
method: "POST",
headers: { "x-api-key": process.env.PAYZILLA_API_KEY, "content-type": "application/json" },
body: JSON.stringify({ name: "Weather lookup", price: "2000" }),
});
console.log(res.status, await res.text());You should see: HTTP 201 and a body with your new service, including its id. Keep that id for step 3.
The answer is 201 with the service, including its id. The price is a string of atomic units with six decimals, so "2000" is 0.002 USDC. A decimal such as "0.002" is refused with price_invalid. The smallest price is 0.001 USDC ("1000"); a lower price is refused with 400 price_below_minimum. In the start app the smallest price is 0.01 USDC. GET /v1/services lists your services. A service can be paused with PATCH /v1/services/:id and {"status": "paused"}. A service is never deleted, and its changes are kept in its history. In the start app you can also list, add, edit and pause your services (up to 20 for now; none can be deleted). Anyone who holds your key can create, reprice or pause services, so keep it secret.
Step 3: A small seller server
This is the whole seller side for one route. It uses no libraries. It is reference code, not a package: it keeps state in memory, serves one route and one process, and handles only GET. Use it to understand the flow, then adapt it. It is the same code as in the devnet quickstart, which we ran against our staging environment; we have not published a run of it on mainnet, so make your first payment small and read the result yourself.
More on a small seller server
Read the code: seller.mjs (82 lines)
Run it with PAYZILLA_API_KEY=<YOUR_KEY> PAYZILLA_SERVICE_ID=<SERVICE_ID> node seller.mjs. Calling it with no payment header returns a 402 with the challenge in the body and, base64-encoded, in a PAYMENT-REQUIRED header.
You should see: HTTP 402 when you call it without a payment, then HTTP 200 once a buyer has paid and Payzilla has settled the payment. A plain call with no payment is not a failure: it leaves a payment row with the status Created.
Settlement is idempotent on our side, but serving is your job: the in_flight state is set before the first await so that two requests carrying the same signature at the same moment cannot both be served. If settlement fails or is still pending, the state goes back to issued. The reason Payzilla gives for a refused verify is for your own logs; the buyer gets a fixed payment_invalid. The issued map lives in memory, so after a restart a buyer who has already paid gets unknown_intent and must not be asked to pay again; a real seller keeps this record in its own store. That is the concrete reason this is reference code.
Step 4: Pay as a buyer
The buyer reads the challenge, checks the price and the recipient against its own limits, signs an exact-scheme payment for USDC on Solana mainnet, and repeats the request with PAYMENT-SIGNATURE. The paid response carries a PAYMENT-RESPONSE header, which holds the transaction signature.
More on pay as a buyer
- Use a different wallet from your receiving wallet. A wallet that is registered as a seller’s receiving wallet cannot pay through Payzilla. This protects against a wallet paying itself.
- It is real USDC. Fund the buyer wallet with a few cents. The smallest payment is 0.001 USDC.
- No SOL needed for the network fee: Payzilla’s fee payer pays it.
- Check before you sign. A careful buyer refuses the wrong network, a token other than USDC, an amount above its cap and a scheme other than
exact. - To see what a payment looks like first, use the real-money demo (about one US cent).
You should see: HTTP 200 with your paid response, and a PAYMENT-RESPONSE header that holds the transaction signature.
Unpaid requests leave rows. Every request your server makes for a payment challenge creates a payment intent, so a plain curl or any other unpaid call leaves a row in your payments with the status Created. They are not failures.
Step 5: See the payment
You should see: the payment in GET /v1/payments with the status Final, and the same transaction on the Solana explorer.
More on see the payment
- Explorer: paste the transaction signature into the Solana explorer, with the cluster on mainnet. Check the amount and that it is finalized.
- Ledger:
GET /v1/paymentslists your payments, newest first, andGET /v1/payments/:idshows one with its timeline and ledger entries.GET /v1/payments?serviceId=<id>lists the payments of one service. - Your account: the start page shows your name, receiving wallet and key fingerprint. In the start app, your Account page shows your own payments (read-only): time, amount, status, service, payer and a Solscan link. You can make a new key, stop a key, change the receiving wallet (it needs a signature from the current wallet and one from the new wallet, and it takes effect after a delay) and close the account.
Try a payment against your own service
A small script that sends ONE payment to your own listed service, so you can see that it works. It is a helper we wrote and tested; please read it before you run it: pay-service.mjs (about 11 KB, no key is ever printed or sent). SHA-256: 99f107a67645338254cf314de3b7ed948c5d0936ce8117407fd052e7da6efe1d. Nothing here is financial advice. If you would rather not set anything up, the real-money demo needs no setup.
More on try a payment against your own service
Before you start
- Try devnet first (the test network, no real money). Mainnet moves real money, a few cents.
- You need a second wallet, different from your receiving wallet. The platform refuses a payment to yourself. Do not use your receiving wallet.
- The paying wallet needs a little USDC (on devnet, staging’s own test token). The facilitator named in the challenge is set as the fee payer, so the paying wallet needs no SOL (checked in one devnet run on 15 October 2026, with a stand-in service; not yet shown through Payzilla’s own path on staging or on mainnet).
- The receiving wallet needs a USDC token account before the first payment. It is created when the wallet first receives USDC, and our service does not create it for you. If your receiving wallet has never held USDC, send it a small amount of USDC from another wallet first, otherwise the payment is refused (HTTP 402).
- A key file in Solana CLI format (a JSON array of 64 numbers). It is read on your computer only and never printed or sent. Do not paste it anywhere.
- Node 20.18 or newer, and three packages next to the script (no
package.jsonis needed):npm install @solana/kit@^5.1.0 @x402/core@~2.28.0 @x402/svm@~2.28.0
Step A: look first (signs nothing, opens no file)
node pay-service.mjs <your-service-url>
If you run the example seller server from step 3 on your own machine, give the script http://localhost:PORT, not http://127.0.0.1:PORT: the script refuses a challenge whose host differs from the URL you typed (different host), and the example server puts localhost in the challenge.
You should see: what your service asks for: the service host, the amount in USDC, the token, the network, your receiving wallet (payTo) and the paying wallet, then Dry run: nothing was signed. Nothing is signed.
Step B: a devnet payment (the default network)
node pay-service.mjs <your-service-url> --keypair payer.json
You are asked to type the word pay. Anything else stops with nothing signed (Aborted. Nothing was signed.). The limit is 0.005 USDC on devnet; --max <minor units> changes it.
You should see (from our tests with a stand-in service; the names and amounts are examples):
Service: 127.0.0.1:50038
Amount: 0.002 USDC (2000 minor units)
Token: USDC (devnet)
Network: devnet
Receiving wallet (payTo): 9xQe...VFin
Paying wallet: GpMH...voDkaD
This is devnet (test network).
Type the word pay to send this payment:
HTTP status: 200
Body: {...your service's answer...}
Payment response: {"success":true,"transaction":"<id>"}
Transaction: <id>
View on Solscan: https://solscan.io/tx/<id>?cluster=devnetThe script exits with success only on HTTP 200.
Step C: a mainnet payment (real money)
node pay-service.mjs <your-service-url> --keypair payer.json --network mainnet
The network must be written out. The limit is 0.1 USDC on mainnet. Above 0.05 USDC you are asked a second time (type yes-large). The output has the same lines; the Solscan link has no ?cluster=devnet.
When it refuses (nothing is signed)
chose mainnetornetwork ... but you chose devnet: the service is on the other network than the one you asked for.not an accepted USDC: the token is not USDC on that network.above your cap: the amount is more than your limit; raise it only if you mean it.The paying wallet is the same as the seller's receiving wallet: use a different wallet to pay.different host: the challenge is about another host than the URL you gave.redirect: the service tried to send you elsewhere; the script does not follow.not Solana devnetornot Solana mainnet: your--rpcendpoint is on the other network.HTTP status: 402after you typedpay, with aHint:line: the service refused the payment. The usual cause is the receiving wallet having no USDC token account yet (see above); others are too little USDC in the paying wallet, or a challenge that ran out of time. Nothing was lost.expected HTTP 402,version 2,only "exact",plain whole numberorpayTo: the service's answer is not a valid payment request.
What is tested, and what is not
Tested, with a stand-in service and a fake RPC: everything before signing, the confirmations, every refusal above, the printing after payment (with a stub in place of the signer), and that the key never appears in any output. Tested on devnet: one real run on 15 October 2026 (stand-in service, test token, a paying wallet with no SOL) passed: HTTP 200, a transaction and its explorer link; a repeat of the same signed request was refused (409). Not shown by that run: Payzilla’s own path on staging, and any mainnet run. Please do the devnet run first and tell us what you saw at [email protected].
Test network (devnet)
Devnet: test tokens, by invitation
The devnet preview uses test tokens that have no value and a private staging environment that we set up for invited partners: the base address, your key and your seller wallet come from us by email. The steps are the same as above; only the address, the key and the token differ.
Open the test network quickstart, or ask for a devnet invite.
Common mistakes
More on common mistakes
| What happens | Why, and what to do |
|---|---|
| A price or amount is rejected | Prices and amounts are strings of digits in atomic units: "2000", not "0.002" (a decimal price is refused with price_invalid). If you send an amount with a serviceId, it must equal the service price. |
401 from every call | Send the key exactly as shown, starting afk_live_, in the x-api-key header, with no quotes. If you lost it, make a new key on the start page. |
409 when creating an intent | Read the code in the answer. idempotency_conflict: you reused an Idempotency-Key with a different amount; use a new key for each new intent. service_paused: resume the service. service_price_mismatch or service_asset_mismatch: the request does not match the service. |
400 price_below_minimum or amount_below_minimum | The platform minimum is 0.001 USDC ("1000"). Raise the price. |
| A payment is refused for a limit | New accounts start with small limits. No money moved. Write to [email protected] to ask for more. |
| Every payment is refused (402) | Your receiving wallet has no USDC token account yet. Send 0.01 USDC to it from another wallet once, then try again. Nothing was charged. |
| The buyer cannot pay | A wallet registered as a seller’s receiving wallet cannot pay through Payzilla. Pay from a different wallet. |
404 service_not_found | The service id is wrong, or it belongs to another seller. List yours with GET /v1/services. |
Something else? Write to [email protected] with the transaction link. We read every message.
List your first service.
Up to 20 services for now. Start selling opens start.payzilla.ai.
Read the Terms and the Privacy note before you pay or sell.