Skip to content

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 ​

  1. You send a request without an API key.
  2. Inodra answers 402 Payment Required. The PAYMENT-REQUIRED header holds a challenge. The challenge tells you the amount, the asset, and the address to pay.
  3. You build a Sui transaction that pays that amount to that address. You sign it with your wallet. You do not execute it.
  4. You send the same request again. Add the signed transaction in the PAYMENT-SIGNATURE header.
  5. Inodra verifies the signature and simulates the transaction. It checks that the deposit address receives at least the accepted amount.
  6. Inodra executes the transaction through its own fullnodes and credits your wallet.
  7. Inodra charges the request and returns the response. The PAYMENT-RESPONSE header confirms the payment.

All of this happens in one round trip. Inodra never holds your key and never signs for you.

Prices and Limits ​

ItemValue
Price$10 per 1,000,000 credits ($0.00001 per credit)
Standard read1 credit (see Credits & Billing)
Minimum payment$0.01 (1,000 credits)
Accepted assetUSDC on Sui mainnet (6 decimals)
Settlement networksui:mainnet only
Unused amountKept as prepaid balance for the same wallet
Balance expiryNever
RefundsNone. The balance is not refundable.
4xx responsesBilled like any other request
5xx responsesRefunded

The USDC coin type is:

0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC

We 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:

bash
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:

json
{
  "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": {} }
}
  • amount is in atomic units of the asset. For USDC, 10000 is $0.01.
  • payTo is Inodra's deposit address.
  • extra.credits is 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.

typescript
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:

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-SIGNATURE header 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:

bash
curl -X POST https://mainnet-api.inodra.com/v1/wallet/topup \
  -H "PAYMENT-SIGNATURE: <base64 payment>"
json
{
  "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.

bash
curl https://mainnet-api.inodra.com/v1/wallet/balance -H "x-api-key: indr_pat_..."
json
{
  "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.

  1. Get the message to sign. It is valid once, for 5 minutes.
bash
curl "https://mainnet-api.inodra.com/v1/wallet/register/nonce?address=0x..."
  1. Sign the exact message text with signPersonalMessage, then register:
bash
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:

json
{
  "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, streams and manage scopes. See API Key Scopes.
  • If the balance is below $5, Inodra answers 402 with the code INSUFFICIENT_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:

LimitValue
Included creditsNone (prepaid only)
Rate limit25 requests per second
ProjectsOne, on mainnet
WebhooksNot available
Warp streamsNot available

gRPC ​

x402 also works on the gRPC gateway at mainnet-grpc.inodra.com:443. It uses metadata in place of headers:

  1. Call a method with no API key and no payment. The call fails with UNAUTHENTICATED. The payment-required metadata holds the same base64 challenge.
  2. Build and sign the payment as in the Quick Start.
  3. Call the method again. Add the payment as payment-signature metadata.
  4. The payment-response trailer confirms the payment.

If your balance is too low, the call fails with RESOURCE_EXHAUSTED. The payment-required metadata then asks for the difference.

bash
grpcurl -H "payment-signature: <base64 payment>" -d '{}' \
  mainnet-grpc.inodra.com:443 sui.rpc.v2.LedgerService/GetServiceInfo

Error Codes and Retry Rules ​

Errors return a JSON body with a code field.

CodeStatusMeaningWhat to do
PAYMENT_REQUIRED402No payment headerPay the challenge
INVALID_PAYMENT_HEADER402The header is not valid base64 JSONFix the header
UNSUPPORTED_SCHEME402The scheme is not exactUse an entry from accepts
UNSUPPORTED_NETWORK402The network is not sui:mainnetUse an entry from accepts
UNSUPPORTED_ASSET402Inodra does not accept this assetUse an entry from accepts
WRONG_PAY_TO402The payment goes to the wrong addressPay to payTo
PAYMENT_WRONG_RESOURCE402The payment was issued for another pathSend accepted unchanged from the challenge
NONCE_INVALID401No valid nonce for this walletCall register/nonce, then sign and register
INVALID_SIGNATURE401The signature does not match the message and addressSign the exact message text with the wallet
PAYER_MISMATCH400The payment comes from another walletPay from the wallet that registers
AMOUNT_BELOW_MINIMUM402The amount is below $0.01Pay at least the minimum
INVALID_TRANSACTION402The transaction bytes are not validBuild the transaction again
INVALID_SIGNATURE402The signature does not match the transactionSign again with the sender's key
PAYMENT_ALREADY_EXECUTED402The transaction was already executedSign a new transaction
SIMULATION_FAILED402The transaction fails in simulationCheck the coin balance and gas, then sign again
PAYMENT_AMOUNT_MISMATCH402The deposit address receives less than amountPay the full amount
EXECUTION_FAILED402The transaction failed on chain. Nothing is creditedSign a new transaction
INSUFFICIENT_BALANCE402The payment is credited, but it is not enoughPay the difference in the new challenge
PAYMENT_IN_PROGRESS409Inodra is still processing this paymentWait, then retry with the same header
SETTLE_RATE_LIMITED429Too many paymentsWait, then retry
CREDIT_WRITE_FAILED500The funds moved, but the credit was not writtenRetry with the identical header. Do not pay again
BROADCAST_UNCONFIRMED502Inodra cannot confirm the transaction yetRetry with the identical header
SETTLE_BUSY503Settlement is busyRetry with the same header
SETTLE_UNAVAILABLE503Settlement is not availableRetry with the same header

Important: For BROADCAST_UNCONFIRMED and CREDIT_WRITE_FAILED, do not sign a new transaction. Retry with the identical PAYMENT-SIGNATURE header. 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:

bash
curl https://mainnet-api.inodra.com/.well-known/x402.json

FAQ ​

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.

The full-stack Sui data layer.