X1ID is live on X1 mainnet
X1ID / Developer platform

Developer platform

Mainnet

The X1ID developer platform is a hosted JSON API under https://api.x1id.io/v1. It packages the things you'd otherwise build yourself on top of the @x1id/resolve SDK — sign-in, access gating, agent discovery, cross-namespace resolution — behind one key.

Two surfaces, one host. The Resolve API (api.x1id.io/resolve/…) is public and keyless — resolution is a read anyone can do. Everything on this page lives under the /v1 prefix and is keyed: a key identifies which app is calling, so the platform can rate-limit, meter, and (for Connect) bind a session to your app.

Base URL and authentication

https://api.x1id.io/v1

Send your key in the X-API-Key header on every /v1 request:

terminal
curl https://api.x1id.io/v1/resolve/x1 \
  -H "X-API-Key: $X1ID_API_KEY"

API keys are server-only. Never put an X1ID API key in browser code, a mobile bundle, or anything a user can download. Call /v1 from your own backend and attach X-API-Key there. For browser sign-in, proxy the /v1/auth/* calls through your backend, as shown in Sign in with X1ID.

Every non-2xx response is a uniform { code, message } body, so you can branch on code instead of parsing prose:

{ "code": "API_KEY_REQUIRED", "message": "provide an X-API-Key header. Request a key at https://developers.x1id.io (or contact [email protected])." }

API_KEY_REQUIRED is what a keyless call to a metered endpoint gets where x402 is not enabled. Where x402 is enabled, that same keyless call instead returns HTTP 402 Payment Required (describing what to pay), not API_KEY_REQUIRED — the pay-per-call path, no key needed.

The one exception to { code, message }: the 402 challenge body is an x402 payment-requirements object — { x402Version, accepts: [ … ] }, where each accepts[] entry carries the scheme, the CAIP-2 network (on X1 mainnet, solana:4SvBP3omtvcCVWdxq1zBY5cDp4wndjsT), asset, payTo, amount (smallest units, as a decimal string), and maxTimeoutSeconds; the X1-native entry also carries extra (with X-Payer: feePayer, the request-bound reference and requestHash). A USDC-via-CDP entry, where offered, uses the x402 v1 field name maxAmountRequired instead of amount. It is the x402 wire format, not an error envelope — detect a 402 by its status, then read accepts[], not a code.

Get a key

Keys are self-serve from the Developer Portal at developers.x1id.io. The portal signs you in with X1ID Connect — your wallet is the identity, no password — and then lets you create, list, and revoke keys for your account, and watch your usage.

A key's secret is shown once, at creation. Store it somewhere safe the moment it's revealed; the portal keeps only a hash and can't show it again. Lost a secret? Revoke the key and create a new one.

The portal is backed by four endpoints, all authenticated by your Connect bearer session (not an X-API-Key):

EndpointDoes
POST /v1/keysCreate a key — the one-time secret is in the response.
GET /v1/keysList your keys, each with its tier and recent usage.
DELETE /v1/keys/:idRevoke one of your keys.
GET /v1/usageYour request counts, tier, and current limits.

Tiers and rate limits

Every key carries a tier. Both windows are per key, shared across every /v1 endpoint the key calls.

TierBurstDailyFor
free300 / min15,000 / dayEvaluating and building. Self-serve.
partner1,000 / min100,000 / dayProduction integrations. Issued after an agreement — contact [email protected].

Each response carries your live budget in headers:

X-RateLimit-Limit            300
X-RateLimit-Remaining        299
X-RateLimit-Daily-Limit      15000
X-RateLimit-Daily-Remaining  14999

Over the limit returns 429 { "code": "RATE_LIMITED", … }. A keyed request that's over its window is not silently charged per call and does not fall through to x402: it gets the 429 until the window resets. x402 is a separate path for requests that send no key (see below), not an overflow for a key that hit its limit. To get more headroom, raise your tier.

Pay-per-call with x402

You don't strictly need a key. Where it's enabled, the metered read endpoints also accept an x402 payment: call with no key, get a 402 Payment Required describing what to pay, pay on-chain, sign the challenge's redeemMessage with the wallet that paid, and retry with a PAYMENT-SIGNATURE header carrying both signatures (the v1 name X-PAYMENT is still accepted). On success the response carries a PAYMENT-RESPONSE header (and the v1 X-PAYMENT-RESPONSE) confirming settlement. Send X-Payer with your wallet address on both requests: it is required for the X1-native rail (see below).

The order a request is tried is fixed: a valid X-API-Key is served (or rate-limited with 429); a key that's present but invalid gets 401 INVALID_KEY; only a request with no key reaches x402. The Connect /v1/auth/* endpoints and Gate policy management are keyed-only and not x402-metered.

This is the no-signup, no-contract path — ideal for autonomous agents. The X1-native rail is self-sponsored XNT on X1 (the buyer pays their own gas); a USDC-via-CDP rail can also be offered. Default prices are a fraction of a cent per call (resolve/reverse cheaper than verify/gate) and are tunable per deployment.

Breaking (2026-10-11): redemption now needs the payer's signature. api.x1id.io refuses a payment header without payload.payerSig (REDEEM_SIGNATURE_REQUIRED), so a client that only sends payload.signature stops working. Add the redeem signature step, and upgrade to @x1id/resolve 0.22.0 for x402RedeemMessage. The request-bound reference is the only payment shape: a payment transaction with any other instruction, including a Memo, is refused.

x402 is only live on a deployment that has a pay-to address configured. Check GET /v1/health — its x402 field tells you whether pay-per-call is accepted, and rails lists which payment rails are on.

x402 headers (v2 names)

api.x1id.io speaks the x402 v2 HTTP header names and still accepts the v1 ones:

Directionv2 header (preferred)v1 header (still accepted)Contents
Request: your paymentPAYMENT-SIGNATUREX-PAYMENTbase64 JSON payment payload. On the X1-native rail: { x402Version, scheme: "exact", network, payload: { signature, payerSig } } (the v2 accepted: { scheme, network } form works too)
Response: the 402 challengePAYMENT-REQUIRED(body only)base64 of the same { x402Version, accepts: [...] } object that is also the 402 body
Response: settled 200PAYMENT-RESPONSEX-PAYMENT-RESPONSEbase64 settlement receipt (both headers carry the same value)
Request: who is payingX-Payer (or ?payer=)your wallet's base58 address. See below

Send one payment header. If PAYMENT-SIGNATURE and X-PAYMENT are both present and differ, the request is rejected.

Bind your payer with X-Payer on the first request

X-Payer is required. Send X-Payer: <your wallet> (or ?payer=) on the request that gets the 402. Only then does the X1-native challenge carry a request-bound reference (extra.reference). Without it the 402 has "code": "PAYER_REQUIRED" and no reference.

With X-Payer, the X1-native accepts[] entry's extra holds:

  • feePayer: your wallet. You pay your own fee; there is no sponsor.
  • reference: a base58 address you add to your payment (see below). It binds the payment to this exact request (method, path, query and body), to the challenge, and to your wallet.
  • requestHash: hex SHA-256 of the canonical request the reference was issued for. Optional to check.
  • redeemMessage: the text your wallet signs to redeem the payment (see below). It is x402RedeemMessage(reference).

A live challenge, requested with X-Payer:

{
  "x402Version": 2,
  "accepts": [{
    "scheme": "exact",
    "network": "solana:4SvBP3omtvcCVWdxq1zBY5cDp4wndjsT",
    "amount": "100000",
    "asset": "So11111111111111111111111111111111111111112",
    "payTo": "E2cELMtby3YYYbDdxamuGKWCyte3kwLywv19pEk2iUq6",
    "maxTimeoutSeconds": 60,
    "extra": {
      "feePayer": "H8FsKYhMyCN9brknY7tL4JXRbKZEjVh7XgrVmWcX1HiA",
      "reference": "S7k9Y5brdu1MBfV8WykKzu8KBkFi97ri5JUp5H4cTTC",
      "requestHash": "e96b43f9ae0ea38a788a748548527d328a67cb16271f64a99eb97e630f4a82bc",
      "redeemMessage": "x1id-x402-redeem:v1|api.x1id.io/dev-api\nreference:S7k9Y5brdu1MBfV8WykKzu8KBkFi97ri5JUp5H4cTTC"
    }
  }]
}

Redemption is bound to the wallet you named: only a payment whose TransferChecked authority is that wallet, which that wallet signs, and whose redemption carries that wallet's signature over redeemMessage, can redeem it. A later X-Payer cannot re-point it. A malformed X-Payer (not a base58 address) is silently ignored, as if you had not sent it, so double-check the value if you see PAYER_REQUIRED.

Retry the identical request. A payment for GET /v1/resolve/alice cannot be redeemed on /v1/resolve/bob, and a POST /v1/gate/check payment only works with the same body bytes. Path case matters: @Alice and @alice are different requests.

Always set a compute-unit limit

X1 charges the transaction fee by the compute-unit limit you request, not the compute units you use: about 10 lamports per requested CU plus a small base fee. A transaction with no SetComputeUnitLimit defaults to 200,000 CU per instruction, at least 0.002 XNT, which is many times the price of the call. Always set the limit explicitly, and size it to what your transaction actually consumes.

What a payment really costs, measured on a live X1 mainnet payment (2026-10-11, wrapped-native asset):

Payment transactionCU consumedLimit requestedFee (lamports)
CU limit + CU price + TransferChecked + reference account6,7197,71980,191

The reference account adds about 180 CU, which keeps the network fee below the 0.0001 XNT resolve price.

Simulate, then size the limit to what was consumed plus about 1,000 CU of headroom (about 7,700 for the reference shape). Do not guess low: a limit below what the transaction consumes makes it fail on chain, and you are still charged the fee. Keep it at or under the server's cap of 40,000 CU.

The payment transaction

The X1-native rail verifies a fixed shape:

  1. ComputeBudget::SetComputeUnitLimit: sized from simulation. It must be first, and anything above 40,000 is rejected (compute-limit-too-high).
  2. ComputeBudget::SetComputeUnitPrice: a small price (x1id's builders use 1 micro-lamport/CU; the cap is 5,000,000).
  3. One SPL TransferChecked of at least the quoted amount of the quoted asset to payTo's associated token account, signed by your wallet, with extra.reference appended as one extra account, read-only and not a signer. SPL Token ignores extra accounts on a single-owner transfer. Nobody signs with the reference; it is a hash, not a key.

Explorers may mislabel the reference. Because the TransferChecked carries one account more than a plain transfer, explorers and jsonParsed RPC output can decode it as a multisig transfer: the reference shows up as a multisig "signer" and your wallet as multisigAuthority. Neither is what happens on-chain. The reference is a read-only, non-signer account (the standard Solana Pay reference pattern), and the transfer authority is your ordinary wallet, approved by its normal transaction signature. A mainnet payment in this shape, for comparison: 2UUMMij6…9UdRo.

Allowed instructions. Only ComputeBudget and SPL Token instructions are allowed (a Memo instruction gets PAYMENT_LAYOUT_INVALID), exactly one transfer may pay payTo, the reference must appear exactly once and only in the transfer, and Address Lookup Tables are refused. A multisig token authority is not supported.

The amount is checked twice: the TransferChecked amount, and the balance actually credited to payTo's token account, which must also cover the price. A Token-2022 mint whose transfer fee leaves payTo short is refused, and so is a Token-2022 payment whose credited amount the RPC can't report.

Wrap native XNT in a separate, earlier transaction. The default asset is the wrapped-native mint (So11111111111111111111111111111111111111112). It has 9 decimals, like XNT. If your wrapped balance is short, top it up first (create the ATA if needed, transfer XNT into it, SyncNative) and wait for it to confirm. Those instructions cannot go in the payment transaction: instruction 0 must be the compute-unit limit, and the wrap has to happen before the transfer.

import {
  ComputeBudgetProgram, Connection, PublicKey, TransactionMessage, VersionedTransaction,
} from "@solana/web3.js";
import { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from "@solana/spl-token";
import { encodeBase58, parseX402Response, x402RedeemMessage } from "@x1id/resolve"; // 0.22.0+

const API = "https://api.x1id.io/v1/resolve/alice";
const connection = new Connection("https://rpc.mainnet.x1.xyz");
const payer = wallet.publicKey; // your wallet adapter / keypair

// 1. Ask for the challenge. X-Payer is required to get a reference.
const challenge = await fetch(API, { headers: { "X-Payer": payer.toBase58() } });
const parsed = parseX402Response(await challenge.json()); // status 402
const X1_MAINNET = "solana:4SvBP3omtvcCVWdxq1zBY5cDp4wndjsT"; // match exactly: a CDP rail is also "solana:..."
const req = parsed?.accepts.find((a) => a.network === X1_MAINNET);
if (!req?.extra.reference) throw new Error("no reference: send X-Payer"); // PAYER_REQUIRED
const mint = new PublicKey(req.asset);

// 2. Build the payment: [CU limit, CU price, TransferChecked + reference].
//    (Wrap XNT -> wSOL in an EARLIER transaction if your balance is short.)
const transfer = createTransferCheckedInstruction(
  getAssociatedTokenAddressSync(mint, payer),
  mint,
  getAssociatedTokenAddressSync(mint, new PublicKey(req.payTo), true), // payTo may be a PDA
  payer,
  BigInt(req.amount),
  9, // wrapped-native XNT has 9 decimals (read the mint for any other asset)
);
transfer.keys.push({ pubkey: new PublicKey(req.extra.reference), isSigner: false, isWritable: false });
const build = (units: number) => [
  ComputeBudgetProgram.setComputeUnitLimit({ units }),
  ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1 }),
  transfer,
];
const { blockhash } = await connection.getLatestBlockhash("finalized");
const compile = (units: number) => new VersionedTransaction(
  new TransactionMessage({ payerKey: payer, recentBlockhash: blockhash, instructions: build(units) }).compileToV0Message(),
);

// Simulate at the 40,000 cap, then request what it used + ~1,000 headroom
// (~7,700). Too low = failed tx, fee still charged.
const sim = await connection.simulateTransaction(compile(40_000), { sigVerify: false, replaceRecentBlockhash: true });
if (sim.value.err) throw new Error(`payment would fail: ${JSON.stringify(sim.value.err)}`);
const tx = compile(Math.min(40_000, sim.value.unitsConsumed! + 1_000));
const signature = await connection.sendTransaction(await wallet.signTransaction(tx));
await connection.confirmTransaction(signature, "finalized");

// 3. Sign the redemption with the SAME wallet that paid. The 402 carries the
//    message verbatim; the SDK rebuilds it so you can check it first.
const redeemMessage = x402RedeemMessage(req.extra.reference);
if (req.extra.redeemMessage !== redeemMessage) throw new Error("unexpected redeemMessage");
const payerSig = encodeBase58(await wallet.signMessage(new TextEncoder().encode(redeemMessage)));

// 4. Claim: the IDENTICAL request, both signatures in PAYMENT-SIGNATURE.
//    ({ reference: req.extra.reference, payerSig } instead of
//    { signature, payerSig } also works: the server finds the payment by its reference.)
const payment = btoa(JSON.stringify({
  x402Version: 2, scheme: "exact", network: req.network, payload: { signature, payerSig },
}));
const res = await fetch(API, { headers: { "X-Payer": payer.toBase58(), "PAYMENT-SIGNATURE": payment } });
// 200 + PAYMENT-RESPONSE header on success

The server only accepts a finalized payment, so wait for finality before you claim. A 402 with PAYMENT_NOT_FINALIZED means retry the same payment header shortly. With { reference }, the server checks every finalized transaction that lists your reference and skips any that only mention it without being a valid payment, so another transaction naming your reference can't block you; if none is valid yet you get the same retryable PAYMENT_NOT_FINALIZED.

Sign the redemption with the paying wallet

Your payment is public once it lands, signature and all. So the claim proves the caller is the payer: payload.payerSig is the base58 ed25519 signature, by the wallet bound with X-Payer, over extra.redeemMessage:

redeemMessage = "x1id-x402-redeem:v1|api.x1id.io/dev-api" "\n" "reference:" reference
  • Sign it with your wallet's signMessage. A raw signature over the UTF-8 bytes and one over a wallet's Solana off-chain message envelope (SIMD-0048) are both accepted.
  • @x1id/resolve 0.22.0 exports x402RedeemMessage(reference) and the domain string X402_REDEEM_DOMAIN, so you can rebuild the message and compare it with the one in the 402.
  • The reference is single-use and already bound to the request and the payer, so a payerSig can't be reused on another challenge.
  • A missing payerSig gets REDEEM_SIGNATURE_REQUIRED, and one that doesn't verify gets REDEEM_SIGNATURE_INVALID. Neither consumes the payment: sign and resend the same request with the same payment.

How the reference is derived

You do not need to compute it, since the 402 gives it to you. To check it, or to confirm you are paying for the request you will send, the @x1id/resolve SDK exports canonicalX402Request, x402RequestHash and deriveX402Reference:

canonical   = METHOD "\n" path "\n" query "\n" hex(SHA-256(body))
requestHash = hex(SHA-256(canonical))
reference   = base58(SHA-256("x1id-x402-ref:v1|api.x1id.io/dev-api" "\n" canonical "\n" nonce "\n" payer))
  • METHOD is uppercased. Scheme, host and fragment are left out.
  • path: each /-separated segment is percent-decoded, then re-encoded per RFC 3986 (letters, digits and - . _ ~ stay literal; every other byte becomes %XX, uppercase). Trailing slashes are kept and case is preserved.
  • query: split on &, + means a space, each key and value decoded and re-encoded, pairs sorted by key then value, duplicates kept, a bare k becomes k=.
  • body: the exact bytes sent. GET, HEAD and empty bodies use SHA-256 of the empty string (e3b0c442…7852b855).
  • nonce is the challenge's server-side nonce. It is not sent to you: the server computes the reference and puts it in extra.reference.

Worked example:

RequestGET https://api.x1id.io/v1/resolve/@Alice?fresh=1&chain=x1
canonicalGET\n/v1/resolve/%40Alice\nchain=x1&fresh=1\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
requestHash9a408b21042932cda138cd88c8421e27cc23b699ab6780084dfa22269edfb6ce
nonce00112233445566778899aabbccddeeff
payerH8FsKYhMyCN9brknY7tL4JXRbKZEjVh7XgrVmWcX1HiA
referenceGxfSof9wYBDnnLUXR3ZqePRUoNN2etHiJYkkaUShVseQ

x402 error codes

A rejected payment gets another 402 carrying a fresh challenge, with the rejection's code in both error and code and an explanation in message, so you can branch on error and pay again from the same response:

codeMeaning
PAYER_REQUIREDThe challenge was requested without X-Payer, so it has no reference.
PAYMENT_PAYLOAD_INVALIDThe payment header is not base64 JSON of an x402 payment payload with payload.signature or payload.reference.
REDEEM_SIGNATURE_REQUIREDNo payload.payerSig. Sign extra.redeemMessage with the paying wallet and resend; the payment was not consumed.
REDEEM_SIGNATURE_INVALIDpayload.payerSig is not the paying wallet's signature over extra.redeemMessage. The payment was not consumed.
PAYMENT_NOT_FINALIZEDThe payment is not finalized yet. Retry the identical request with the same payment header shortly.
PAYMENT_ALREADY_USEDThis payment already bought a successful response. A replay of the same signature gets this code. Pay again against a fresh challenge.
REFERENCE_MISSINGNo issued reference in the TransferChecked, or it is writable, a signer, repeated, or also in another instruction.
REFERENCE_MISMATCHThe request changed (method, path, query, body or endpoint), or the payment carries two references.
REFERENCE_REUSEDA different payment carries a reference that has already paid for a successful call. Request a new challenge.
REFERENCE_EXPIREDPast the 600-second redeem window. Request a new challenge.
MULTISIG_AUTHORITY_UNSUPPORTEDThe transfer authority is a multisig. Pay from a single-signer wallet.
PAYMENT_LAYOUT_INVALIDThe transaction has an instruction outside the fixed shape (for example a Memo), a second transfer, or compute-budget values out of bounds.

The error reference lists every code the gateway returns, with its HTTP status and what to do.

A call whose handler fails (for example a 404) does not consume your payment, so you can retry the same request with the same payment.

Health

GET/v1/health

Never gated. Reports liveness, whether x402 is enabled, and which RPC host and program the deployment reads.

curl https://api.x1id.io/v1/health
# → { "status": "ok", "service": "x1id-dev-api", "x402": true,
#     "rails": ["x1-native"], "rpc": "rpc.mainnet.x1.xyz",
#     "program": "8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P" }

The platform endpoints

Browse the docs

Developer platform