Skip to content

05 — Payments

Markdown

Pay on Stellar

An agent can pay Inferit per request with x402 on Stellar: one transfer of ITC to Inferit's treasury account, signed by the agent, with the network fee paid by Inferit. No account with Inferit and no API key.

Network

x402 names the network stellar:testnet (a CAIP-2 id). Every payment is signed for its passphrase. The 402 lists one entry per network Inferit takes payments on: Whitechain (eip155:1874) and Stellar. A client that registers only the Stellar scheme pays with the Stellar entry.

WhatValue
Network idstellar:testnet
PassphraseTest SDF Network ; September 2015
Soroban RPChttps://soroban-testnet.stellar.org
Horizonhttps://horizon-testnet.stellar.org
Explorerstellar.expert/explorer/testnet
Free XLM for a new accounthttps://friendbot.stellar.org?addr=G… (friendbot)

The asset

Payments on Stellar are in ITC, the same credit Inferit prices in on Whitechain: one ITC is one dollar of price. On Stellar, ITC is a classic asset issued by Inferit's issuing account, and x402 moves it through its Stellar Asset Contract (the asset's contract address, C…). The contract is derived from the asset itself: new Asset(code, issuer).contractId(passphrase) in stellar-sdk. So a client can check the contract it is asked to pay against the code and issuer it pinned.

ITC on Stellar has 7 decimals; Inferit's API amounts are whole millionths of a dollar (6 decimals). So the API asks for 10 base units per millionth of a dollar: the minimum payment, $0.01 (10,000 millionths), is "100000" on Stellar. What arrives is credited as the base units divided by 10, rounded down, so an amount below 10 base units is never credited. One ITC is 10,000,000 base units.

Live values

Read from GET /v1/rails (the rail with "id": "stellar"), so this always matches the API. Each address links to the explorer.

A payer account

  1. A key. Any ed25519 Stellar key: its public key (G…) is the payer. Keypair.random() in stellar-sdk makes one, and createEd25519Signer from @x402/stellar signs with it. Stellar addresses are case-sensitive: never lower-case one.
  2. Free XLM. Friendbot creates the account with free XLM: https://friendbot.stellar.org?addr=G…. The XLM pays the account's reserve and the trustline below. x402 payments cost the agent no XLM: Inferit pays their network fee.
  3. A trustline to ITC. A Stellar account holds an asset only after it opts in: one changeTrust operation for ITC, with the code and issuer from GET /v1/rails.
  4. ITC from Inferit's faucet. POST /v1/faucet/stellar with {"address":"G…"} sends ITC to the address, once per address per 24 h, at no charge. Without the trustline the faucet answers with an error that says how to add it. It sends to G… accounts only, the accounts x402 payments here come from.

How a payment works

  1. Call POST /v1/chat/completions with no Authorization header. It answers 402 Payment Required; the PAYMENT-REQUIRED header (base64 JSON) and the body carry the same document. Its Stellar entry in accepts: scheme exact, network stellar:testnet, asset = the ITC contract (C…), payTo = Inferit's treasury account (G…), amount in base units, maxTimeoutSeconds, and extra.areFeesSponsored: true.
  2. The amount is the worst-case cost of this exact request body, as on Whitechain: the input estimate plus the output limit, all-in, and never less than $0.01; then times 10 for the 7 decimals. The quote is pinned for 120 s. The transfer must be exactly this amount.
  3. The client builds the contract call transfer(from, payTo, amount) on the ITC contract, simulates it, and signs only its authorization entry. The signature is valid until a ledger number: the current ledger plus maxTimeoutSeconds divided by about 5 seconds a ledger. It resends the same body with the transaction in the PAYMENT-SIGNATURE header.
  4. Inferit's API checks the payment with the official x402 facilitator code (@x402/stellar@2.28.0): one transfer, of ITC, from the signer to the treasury, of exactly the amount, on this network, before its ledger expiry. It then wraps the transaction in one from its own fee-payer account, pays the fee in XLM and submits it. If the payment does not verify, the API answers 402 again, with the reason in error.
  5. Once the transfer is in a ledger, it is credited to the payer's balance with Inferit: base units divided by 10, rounded down. Each payment is credited once; a replayed payment is refused.
  6. The request is served (streaming included) and charged its exact cost against that balance, never more than this one payment. The response carries PAYMENT-RESPONSE (base64: the Stellar transaction hash, the network and the payer) and x-inferit-buyer-cost-micro, the cost in millionths of a dollar.
  7. Every call without a key gets its own 402 and its own payment, and is charged against that payment only: what a payment does not spend adds to your balance (spending it).

The client code

TypeScript, with the official x402 fetch client

A brand-new key works: friendbot funds the account, the faucet gives it ITC, and the agent never pays a network fee for a payment. Pinned versions: @x402/fetch@2.28.0, @x402/stellar@2.28.0 and @stellar/stellar-sdk@16.4.1.

agent.mts
// npm i @x402/fetch@2.28.0 @x402/stellar@2.28.0 @stellar/stellar-sdk@16.4.1
// then: npx tsx agent.mts    (Node 22+)
import { Asset, BASE_FEE, Horizon, Keypair, Networks, Operation, TransactionBuilder } from '@stellar/stellar-sdk';
import { decodePaymentResponseHeader, wrapFetchWithPayment, x402Client } from '@x402/fetch';
import { createEd25519Signer } from '@x402/stellar';
import { ExactStellarScheme } from '@x402/stellar/exact/client';

const API = 'https://api-production-c74b9.up.railway.app';
const NETWORK = 'stellar:testnet'; // pinned: the one network this agent pays on
const horizon = new Horizon.Server('https://horizon-testnet.stellar.org');
// A brand-new key works: friendbot gives it free XLM, and Inferit pays the network fee of every payment.
// Your balance with Inferit belongs to the key that pays: set STELLAR_SECRET_KEY to keep using one (the key you sign in
// with on Inferit, to see and spend that balance).
const keypair = process.env.STELLAR_SECRET_KEY ? Keypair.fromSecret(process.env.STELLAR_SECRET_KEY) : Keypair.random();
const address = keypair.publicKey(); // case-sensitive: never lower-case a Stellar address
if (!process.env.STELLAR_SECRET_KEY) console.log('new key', address, '- keep it to reuse its balance: STELLAR_SECRET_KEY=' + keypair.secret());

// 1. The asset: code and issuer from the API, its contract checked against them. Pin both in real code.
const rails = await (await fetch(`${API}/v1/rails`)).json();
const d = rails.rails.find((r: { id: string }) => r.id === 'stellar').details;
const [code, issuer] = d.asset.split(':');
const ITC = new Asset(code, issuer);
const SAC = ITC.contractId(Networks.TESTNET);
if (d.network !== NETWORK || d.assetContract !== SAC) throw new Error('unexpected Stellar network or asset');

// 2. Create the account with free XLM, then opt in to ITC with a trustline.
await fetch(`https://friendbot.stellar.org?addr=${address}`);
const trust = new TransactionBuilder(await horizon.loadAccount(address), { fee: BASE_FEE, networkPassphrase: Networks.TESTNET })
  .addOperation(Operation.changeTrust({ asset: ITC }))
  .setTimeout(60)
  .build();
trust.sign(keypair);
await horizon.submitTransaction(trust);

// 3. ITC from Inferit's faucet, once per address per 24 h.
const faucet = await fetch(`${API}/v1/faucet/stellar`, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ address }),
});
console.log('faucet', faucet.status, await faucet.text());

// 4. The standard x402 v2 client with a Stellar signer. It pays only in ITC, and at most 1 ITC
//    (10000000 base units, 7 decimals) for one request, whatever the server quotes.
const client = new x402Client()
  .register(NETWORK, new ExactStellarScheme(createEd25519Signer(keypair.secret(), NETWORK)))
  .setSpendControls({ allowedAssets: [{ network: NETWORK, asset: SAC, maxAmountPerPayment: '10000000' }] });
const fetchWithPayment = wrapFetchWithPayment(fetch, client);

// 5. Call the API with no key: 402 -> sign -> paid retry, all inside fetchWithPayment.
const res = await fetchWithPayment(`${API}/v1/chat/completions`, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    model: 'qwen/qwen-2.5-14b-instruct',
    messages: [{ role: 'user', content: 'Say hello in five words.' }],
    max_tokens: 64, // the quote is the worst case for this body, so a small max_tokens keeps it small
  }),
});
const body = await res.json();
console.log(res.status, body.choices?.[0]?.message?.content ?? body);
console.log('model', res.headers.get('x-inferit-model'));
const receipt = res.headers.get('PAYMENT-RESPONSE');
if (receipt) console.log(decodePaymentResponseHeader(receipt)); // the Stellar transaction, the network and the payer
console.log('cost', res.headers.get('x-inferit-buyer-cost-micro'), 'millionths of a dollar; the rest stays your balance');
// A call without a key never draws on that balance: an API key made after signing in with this key spends it.

The setSpendControls line is configuration of the standard client, not a patch: the client only pays in tokens it knows, and the ITC contract is not on its built-in list. Its maxAmountPerPayment ("10000000", 1 ITC, $1) is the most the client signs for one request: a setting in your client, not a rule the API enforces.

A complete agent, with the account, the trustline, the faucet, checks on what it is asked to pay and a printed receipt: stellar-x402-agent in inferit-examples.

curl: see the Stellar entry without paying

the 402, without paying
curl -s https://api-production-c74b9.up.railway.app/v1/chat/completions \
  -H 'content-type: application/json' \
  -d '{"model":"qwen/qwen-2.5-14b-instruct","messages":[{"role":"user","content":"Hi"}],"max_tokens":64}' \
  | jq '.accepts[] | select(.network == "stellar:testnet")'
# scheme "exact", network, asset (the contract, C…), payTo (the treasury, G…), amount (7 decimals),
# maxTimeoutSeconds, extra.areFeesSponsored: true

Where the money is

  • The treasury. Every payment on Stellar goes to Inferit's treasury account, the payTo of the Stellar entry. Inferit holds what your requests have not spent, as your balance in its ledger, under your Stellar address.
  • Unlike Whitechain. On Whitechain a payment lands in the MarketEscrow contract, under limits the contract enforces (Whitechain escrow). On Stellar the exact scheme allows one plain transfer to payTo, so the balance is kept by Inferit's API, and the limits below are the API's.
  • Spending it. x402 payments add to your balance; a call without a key never draws on it. An API key does: sign in with the same Stellar key (POST /v1/auth/stellar/challenge, then /verify with the message signed as a SEP-53 message, or Buy with a Stellar wallet), make a key with POST /v1/keys, and each request with that key is charged its exact cost against the balance. Once a payment would take the balance past $25, x402 payments are refused (invalid_exact_stellar_balance_over_limit) until a key spends it down. Withdrawals from it are planned.
  • Sellers. A request paid on Stellar goes to the cheapest healthy seller of the model on Inferit, whichever network the seller registered on. What the seller is owed is recorded under the address it signed in with.

Limits

WhatDefaultWhy
Minimum payment$0.01 (0.01 ITC, "100000")Each payment is a transfer on the ledger whose fee Inferit pays
Most per payment$5 (5 ITC, "50000000")Inferit holds what a payment does not spend, so each payment stays small
Most a balance may reach$25 (25 ITC)A payment that would take your balance past it is refused before it settles: nothing is transferred. An API key from a Stellar sign-in spends the balance (Spending it)
AmountExactly the quoteThe exact scheme: a transfer of any other amount is refused
Signature validitymaxTimeoutSeconds (120 s), counted in ledgers of about 5 sThe authorization entry names its last valid ledger
Quote pin120 s, per request (path, routing headers and JSON body)The paid retry verifies against the quote it was shown; a changed request gets a new 402
SignerA Stellar account key (G…), x402 version 2createEd25519Signer signs the authorization entry; the facilitator pays the fee
x402 payments30 paid attempts a minute per paying address, 60 per IPPayments from one address are handled one at a time
Client ceiling (the code above)$1 per paymentYour own setting, in the client's spend controls

The API reports its own limits in GET /v1/rails (maxPaymentMicro, maxBalanceMicro; Live values). Agents should start at Agents or agents.md; the whole x402 exchange is in the API reference.

What is public

Each payment is a Stellar transaction: the payer's address, the treasury, the asset and the amount are public on the ledger, and Inferit's fee-payer account is the transaction's source. Your balance with Inferit and what each request cost are in Inferit's own ledger, not on chain: the response headers and your requests are the record.