Pay per request with x402
Call Inodra's Sui data APIs with only a Sui wallet. You do not need an account, an email address, or a card. You pay for each request with USDC on Sui mainnet, using the open x402 protocol.
Who It Is For
- AI agents that need Sui data and hold their own wallet
- Quick scripts and one-off jobs where a sign-up is too much work
- Developers who want to try Inodra before they create an account
If you make many requests every month, a plan is usually cheaper. You can also register your wallet to get a normal API key that bills the same prepaid balance.
How a Paid Request Works
- You send a request without an API key.
- Inodra answers
402 Payment Required. ThePAYMENT-REQUIREDheader holds a challenge. The challenge tells you the amount, the asset, and the address to pay. - You build a Sui transaction that pays that amount to that address. You sign it with your wallet. You do not execute it.
- You send the same request again. Add the signed transaction in the
PAYMENT-SIGNATUREheader. - Inodra verifies the signature and simulates the transaction. It checks that the deposit address receives at least the accepted amount.
- Inodra executes the transaction through its own fullnodes and credits your wallet.
- Inodra charges the request and returns the response. The
PAYMENT-RESPONSEheader confirms the payment.
All of this happens in one round trip. Inodra never holds your key and never signs for you.
Prices and Limits
| Item | Value |
|---|---|
| Price | $10 per 1,000,000 credits ($0.00001 per credit) |
| Standard read | 1 credit (see Credits & Billing) |
| Minimum payment | $0.01 (1,000 credits) |
| Accepted asset | USDC on Sui mainnet (6 decimals) |
| Settlement network | sui:mainnet only |
| Unused amount | Kept as prepaid balance for the same wallet |
| Balance expiry | Never |
| Refunds | None. The balance is not refundable. |
| 4xx responses | Billed like any other request |
| 5xx responses | Refunded |
The USDC coin type is:
0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDCWe may add more stablecoins. The accepts list in each challenge always shows what Inodra accepts now.
Note: Payments always settle on Sui mainnet. This is also true when you call the testnet API hostname.
Quick Start
1. Read the Challenge
Send a request without an API key:
curl -i https://mainnet-api.inodra.com/v1/jsonrpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"sui_getLatestCheckpointSequenceNumber","params":[]}'Inodra answers 402 Payment Required. The PAYMENT-REQUIRED header holds the challenge as base64 JSON. The JSON body holds the same object in the paymentRequired field:
{
"x402Version": 2,
"resource": {
"url": "https://mainnet-api.inodra.com/v1/jsonrpc",
"description": "...",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "sui:mainnet",
"amount": "10000",
"asset": "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
"payTo": "0x...",
"maxTimeoutSeconds": 60,
"extra": {
"symbol": "USDC",
"decimals": 6,
"credits": "1000",
"pricePerCreditMicroUsd": 10,
"minPaymentMicroUsd": 10000
}
}
],
"extensions": { "bazaar": {} }
}amountis in atomic units of the asset. For USDC,10000is $0.01.payTois Inodra's deposit address.extra.creditsis the number of credits that the amount buys.
2. Pay and Resend (TypeScript)
This example uses @mysten/sui v2. Any client that can build a transaction works.
import { SuiGrpcClient } from '@mysten/sui/grpc'
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'
import { Transaction, coinWithBalance } from '@mysten/sui/transactions'
import { toBase64 } from '@mysten/sui/utils'
const url = 'https://mainnet-api.inodra.com/v1/jsonrpc'
const body = JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'sui_getLatestCheckpointSequenceNumber',
params: []
})
const client = new SuiGrpcClient({
network: 'mainnet',
baseUrl: 'https://fullnode.mainnet.sui.io:443'
})
const keypair = Ed25519Keypair.fromSecretKey(process.env.SUI_PRIVATE_KEY!)
const address = keypair.toSuiAddress()
// 1. Get the challenge
const first = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
})
const challenge = JSON.parse(atob(first.headers.get('PAYMENT-REQUIRED')!))
// Send `accepted` back unchanged. It names the path this payment is for,
// so a payment for one endpoint does not settle at another.
const accepted = challenge.accepts[0]
const { amount, asset, payTo } = accepted
// 2. Build and sign the payment. Do not execute it.
const tx = new Transaction()
tx.setSender(address)
const coin = tx.add(coinWithBalance({ type: asset, balance: BigInt(amount) }))
tx.transferObjects([coin], payTo)
const bytes = await tx.build({ client })
const { signature } = await keypair.signTransaction(bytes)
const header = btoa(
JSON.stringify({
x402Version: 2,
accepted,
payload: { signature, transaction: toBase64(bytes) }
})
)
// 3. Resend with the payment
const paid = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'PAYMENT-SIGNATURE': header },
body
})
console.log(await paid.json())
console.log(JSON.parse(atob(paid.headers.get('PAYMENT-RESPONSE')!)))The PAYMENT-RESPONSE header holds base64 JSON:
{
"success": true,
"payer": "0x...",
"transaction": "<transaction digest>",
"network": "sui:mainnet",
"amount": "10000"
}On failure, the object also has an errorReason field.
Rules That Protect Your Funds
- Pay from the wallet that should own the credits. Credits go to the transaction sender, not to a sponsor.
- Do not send a plain transfer to the deposit address. Only a payment attached to a request is credited.
- A payment is not bound to one request. Send it only over HTTPS to an Inodra host.
- Treat the
PAYMENT-SIGNATUREheader like a secret. Do not log it, cache it or send it through a proxy you do not control. Anyone who holds it before Inodra does can spend that payment on their own request. - Credits are spend-only. You cannot withdraw them.
- An organization on a monthly or annual plan cannot receive deposits (
ORGANIZATION_ON_PLAN). Plans and prepaid credits do not mix yet.
Prepaid Balance and Top-Ups
A payment can be larger than the cost of the request. Inodra keeps the rest as prepaid balance for your wallet. Each keyless request still needs its own payment, because nothing but the payment identifies the wallet. The balance is spent through an API key (see below) and counts toward the registration minimum. Keyless payments are limited to 10 per minute per IP address.
To add balance without making a data request, attach a payment to POST /v1/wallet/topup. The full amount becomes balance:
curl -X POST https://mainnet-api.inodra.com/v1/wallet/topup \
-H "PAYMENT-SIGNATURE: <base64 payment>"{
"data": {
"payment": {
"payer": "0x...",
"digest": "...",
"asset": "0xdba3...::usdc::USDC",
"amountAtomic": "5000000",
"creditsGranted": "500000"
},
"balance": { "credits": "500000", "usd": "5.000000" }
}
}To check a balance, call GET /v1/wallet/balance with the wallet's API key. Balances are not public: before you hold a key, the top-up and register responses carry your balance.
curl https://mainnet-api.inodra.com/v1/wallet/balance -H "x-api-key: indr_pat_..."{
"data": {
"address": "0x...",
"balance": { "credits": "500000", "usd": "5.000000" },
"registered": false
}
}The wallet endpoints need no API key. The payment is the authentication.
Get an API Key
A wallet with a balance of at least $5 can register for a normal API key. The wallet proves control by signing a one-time message. A payment alone never mints a key.
- Get the message to sign. It is valid once, for 5 minutes.
curl "https://mainnet-api.inodra.com/v1/wallet/register/nonce?address=0x..."- Sign the exact
messagetext withsignPersonalMessage, then register:
curl -X POST https://mainnet-api.inodra.com/v1/wallet/register \
-H "Content-Type: application/json" \
-d '{ "address": "0x...", "nonce": "<nonce>", "signature": "<serialized signature>" }'To top up in the same call, attach a PAYMENT-SIGNATURE header. The payment must come from the same wallet. zkLogin and passkey wallets cannot register yet.
Each nonce is valid once, and any failed register spends it, including a bad signature. If register fails for any reason, request a new nonce and sign again. A PAYMENT-SIGNATURE header from the failed attempt can be sent again as it is: a payment that already landed is credited to your balance, not charged twice.
On success, Inodra answers 201:
{
"data": {
"apiKey": "indr_pat_...",
"expiresAt": "...",
"scopes": ["data", "streams", "manage"],
"organizationId": "...",
"projectId": "...",
"payment": { "...": "..." },
"balance": { "credits": "500000", "usd": "5.000000" }
}
}- The key is valid for 30 days. It has the
data,streamsandmanagescopes. See API Key Scopes. - If the balance is below $5, Inodra answers
402with the codeINSUFFICIENT_BALANCE. An attached payment is credited. The challenge asks for the difference. - To rotate the key, sign a new message and register again. Inodra issues a new key and revokes the old ones.
Use the key in the x-api-key header, as described in API Keys. Usage bills the same prepaid balance at the normal credit costs.
The key belongs to a "wallet" tier organization:
| Limit | Value |
|---|---|
| Included credits | None (prepaid only) |
| Rate limit | 25 requests per second |
| Projects | One, on mainnet |
| Webhooks | Not available |
| Warp streams | Not available |
gRPC
x402 also works on the gRPC gateway at mainnet-grpc.inodra.com:443. It uses metadata in place of headers:
- Call a method with no API key and no payment. The call fails with
UNAUTHENTICATED. Thepayment-requiredmetadata holds the same base64 challenge. - Build and sign the payment as in the Quick Start.
- Call the method again. Add the payment as
payment-signaturemetadata. - The
payment-responsetrailer confirms the payment.
If your balance is too low, the call fails with RESOURCE_EXHAUSTED. The payment-required metadata then asks for the difference.
grpcurl -H "payment-signature: <base64 payment>" -d '{}' \
mainnet-grpc.inodra.com:443 sui.rpc.v2.LedgerService/GetServiceInfoError Codes and Retry Rules
Errors return a JSON body with a code field.
| Code | Status | Meaning | What to do |
|---|---|---|---|
PAYMENT_REQUIRED | 402 | No payment header | Pay the challenge |
INVALID_PAYMENT_HEADER | 402 | The header is not valid base64 JSON | Fix the header |
UNSUPPORTED_SCHEME | 402 | The scheme is not exact | Use an entry from accepts |
UNSUPPORTED_NETWORK | 402 | The network is not sui:mainnet | Use an entry from accepts |
UNSUPPORTED_ASSET | 402 | Inodra does not accept this asset | Use an entry from accepts |
WRONG_PAY_TO | 402 | The payment goes to the wrong address | Pay to payTo |
PAYMENT_WRONG_RESOURCE | 402 | The payment was issued for another path | Send accepted unchanged from the challenge |
NONCE_INVALID | 401 | No valid nonce for this wallet | Call register/nonce, then sign and register |
INVALID_SIGNATURE | 401 | The signature does not match the message and address | Sign the exact message text with the wallet |
PAYER_MISMATCH | 400 | The payment comes from another wallet | Pay from the wallet that registers |
AMOUNT_BELOW_MINIMUM | 402 | The amount is below $0.01 | Pay at least the minimum |
INVALID_TRANSACTION | 402 | The transaction bytes are not valid | Build the transaction again |
INVALID_SIGNATURE | 402 | The signature does not match the transaction | Sign again with the sender's key |
PAYMENT_ALREADY_EXECUTED | 402 | The transaction was already executed | Sign a new transaction |
SIMULATION_FAILED | 402 | The transaction fails in simulation | Check the coin balance and gas, then sign again |
PAYMENT_AMOUNT_MISMATCH | 402 | The deposit address receives less than amount | Pay the full amount |
EXECUTION_FAILED | 402 | The transaction failed on chain. Nothing is credited | Sign a new transaction |
INSUFFICIENT_BALANCE | 402 | The payment is credited, but it is not enough | Pay the difference in the new challenge |
PAYMENT_IN_PROGRESS | 409 | Inodra is still processing this payment | Wait, then retry with the same header |
SETTLE_RATE_LIMITED | 429 | Too many payments | Wait, then retry |
CREDIT_WRITE_FAILED | 500 | The funds moved, but the credit was not written | Retry with the identical header. Do not pay again |
BROADCAST_UNCONFIRMED | 502 | Inodra cannot confirm the transaction yet | Retry with the identical header |
SETTLE_BUSY | 503 | Settlement is busy | Retry with the same header |
SETTLE_UNAVAILABLE | 503 | Settlement is not available | Retry with the same header |
Important: For
BROADCAST_UNCONFIRMEDandCREDIT_WRITE_FAILED, do not sign a new transaction. Retry with the identicalPAYMENT-SIGNATUREheader. Inodra credits the funds on the retry.
Replay rules:
- Inodra credits each transaction digest once.
- A replayed or already-executed transaction credits its funds to the payer once. It never serves a request.
Discovery
Agents and tools can find the payment details at /.well-known/x402.json on the API host:
curl https://mainnet-api.inodra.com/.well-known/x402.jsonFAQ
Can I pay on testnet? No. Payments settle on Sui mainnet only. You can call https://testnet-api.inodra.com and pay with mainnet USDC.
Can I get a refund? No. The prepaid balance is not refundable. It never expires. Requests that fail with a 5xx status are refunded to your balance.
What if my payment transaction fails? If it fails in simulation or on chain, Inodra credits nothing and serves nothing. Sign a new transaction and try again. If you get BROADCAST_UNCONFIRMED or CREDIT_WRITE_FAILED, retry with the identical header. Do not pay again.
Does Inodra hold my private key? No. You sign the transaction in your own wallet. Inodra only executes the signed transaction that you send.