Error reference
Both the SDK (@x1id/resolve) and the REST API surface a small, fixed set of error conditions. Neither ever falls back to guessing a namespace or returning a plausible-but-wrong address — an unresolved or ambiguous query is always an error, never a best-effort answer.
ambiguous is a security property, not an edge case. When a query matches both the @handle shape and a known domain shape at once (e.g. @jack.x1), X1ID refuses to resolve it rather than picking one. @jack and jack.x1 can be owned by different people. A UI must surface this and ask the user to disambiguate — it must never retry with one namespace stripped, never "try handle first, then domain," and never resolve to whichever namespace happens to have a registered record.
SDK errors (ResolveError)
resolve(), reverse() and records() throw a ResolveError with a code field from a fixed union — never a bare string — so a UI can branch on it directly instead of parsing a message.
code | Meaning | What a UI should do |
|---|---|---|
unrecognized | From resolve(): the input isn't a handle or a known domain shape at all — probably a raw address or unrelated text. From reverse(): the input isn't a base58 32-byte address. | Don't show a resolution error. For resolve(), this is the expected result of skipping looksLikeName() against arbitrary input (e.g. a pasted address) — treat the input as a literal address. For reverse(), it's a caller bug — only pass addresses. |
ambiguous | The input matches both the @handle shape and a domain shape at once (e.g. @jack.x1). | Stop. Ask the user which namespace they mean (@jack, or jack.x1). Never auto-resolve either interpretation. |
invalid-handle | Right shape (@-prefixed), but the content fails handle validation (bad characters, length). | Show a normal "not a valid handle" validation message. |
invalid-domain | Right shape (dot + known TLD), but the label fails domain validation. | Show a normal "not a valid domain" validation message. |
not-found | The name is well-formed and in the correct namespace, but nothing is registered under it. May carry reason: 'nft-burned' — the handle is tokenized and its NFT was burned, so no one holds authority. | Show "not registered" — distinct from an invalid-input error. A legitimate terminal state, not a retry case. |
no-record-for-chain | The name resolves, but there is no live address record for the specific chain requested (e.g. asking for ETH on a handle that has set no ETH record). | Show "no address on file for this chain" rather than any address. This code exists precisely so a caller never receives a wrong address for a chain the handle hasn't set. |
rpc-error | Transport failure — the RPC endpoint was unreachable, returned a non-2xx status, or returned a malformed/short account that couldn't be parsed. | Treat as transient. Safe to retry with backoff; do not treat it as "not found." |
ResolveError also carries the original input string where available, plus an optional reason ('nft-burned' today) for a finer-grained explanation when the code alone isn't enough.
What is not an error: reverse() returns null — it does not throw — when an address has no primary, or when its primary pointer fails one of the three trust rules (stale after a release/re-register, or the address no longer holds the handle). "No primary" is a normal state, not a failure. See reverse.
REST API errors
The REST API returns the same underlying conditions as plain HTTP status codes with a JSON { "error": "...", "input": "..." } body. It is a read-only convenience over the same on-chain data the SDK reads.
GET /resolve/:name
| HTTP | error | Meaning | SDK equivalent |
|---|---|---|---|
409 | ambiguous: both a handle and a domain shape | Input matches both namespace shapes. | ambiguous — refuse to guess, never retryable by picking one side. |
400 | invalid handle | @-shaped (or bare-word) input fails handle validation. A bare word with no @ and no TLD — /resolve/nike — is treated as a handle and resolves (200); it is not a 400. (This differs from the SDK, where resolve('nike') throws unrecognized.) | invalid-handle |
400 | invalid domain | Domain-shaped input fails domain parsing. | invalid-domain |
404 | handle not registered | Well-formed @handle, nothing registered. | not-found |
404 | domain not registered | Well-formed domain, nothing at that account — including on a testnet-pointed instance, where X1NS (mainnet) domains never exist. | not-found |
502 | malformed account | The account was fetched but its data doesn't match the expected layout. | rpc-error — transient, safe to retry. |
GET /reverse/:address
| HTTP | error | Meaning | SDK equivalent |
|---|---|---|---|
400 | invalid address | :address isn't a valid base58 public key. | reverse() throwing unrecognized — a caller bug. |
404 | no primary set | No primary for this address (or the pointer failed a trust rule). | reverse() returns null. Show the bare address. |
502 | malformed handle | The handle account exists but couldn't be parsed. | Transient; safe to retry. |
Any route
| HTTP | error | Meaning |
|---|---|---|
404 | not found | Unknown route (no input field). A client/integration bug — check the path. |
405 | method not allowed | The HTTP method isn't supported on that route — the public /resolve and /reverse lookups are GET-only, so a POST to them is a 405. It does not mean the whole API is read-only: the public surface also has write routes (POST /gift/notify, POST /privy/wallet), and the keyed /v1 platform adds sign-in, gating, and more. A 405 is a client bug — check the method against the route. |
502 | rpc unreachable | /version couldn't reach the configured RPC. Transient; retry with backoff. |
x402 payments
These come from the keyed /v1 platform when a request with no X-API-Key pays per call. A rejected payment is answered with 402 and a body of { x402Version, accepts, error, code, message }: error and code both carry the code below, message explains it, and accepts is a fresh challenge you can pay straight away. The same object is base64-encoded in the PAYMENT-REQUIRED header. A 402 with no error field is a plain challenge, not a rejection.
| HTTP | code | Meaning | What to do |
|---|---|---|---|
402 | PAYER_REQUIRED | The challenge was requested without X-Payer (or with a malformed one), so it carries no extra.reference. | Re-request with X-Payer: <your wallet> (or ?payer=) and pay the reference that comes back. |
402 | PAYMENT_PAYLOAD_INVALID | The PAYMENT-SIGNATURE / X-PAYMENT header is not base64 JSON of a payment payload with payload.signature or payload.reference. | A client bug. Fix the encoding; the payment, if any, is not consumed. |
402 | REDEEM_SIGNATURE_REQUIRED | The payment header has no payload.payerSig. Since 2026-10-11 every redemption must be signed by the paying wallet. | Sign accepts[].extra.redeemMessage with the wallet that paid and send the base58 signature as payload.payerSig (how). The payment was not consumed: resend the same request with the same payment. |
402 | REDEEM_SIGNATURE_INVALID | payload.payerSig is not the paying wallet's signature over extra.redeemMessage (wrong wallet, wrong message, or not a base58 64-byte signature). | Sign the redeemMessage for this payment's reference with the wallet bound by X-Payer. The payment was not consumed. |
402 | PAYMENT_NETWORK_UNSUPPORTED | The payload names a network no rail serves. | Pay one of the accepts[] entries and copy its network. |
402 | PAYMENT_NOT_FINALIZED | The payment is not visible at finalized commitment yet. With payload.reference, no finalized transaction carrying the reference is a valid payment yet. | Retryable. Wait a few seconds and send the identical request with the same payment header. |
402 | PAYMENT_ALREADY_USED | This payment already bought a successful response. Replaying the same signature returns this. | Final for that payment. Pay again against the fresh challenge. |
402 | PAYMENT_FAILED | The payment transaction landed but failed on chain. | Pay again; nothing was transferred. |
402 | PAYMENT_STALE | The payment is older than the redeem window. | Pay again against the fresh challenge. |
402 | PAYMENT_WRONG_ASSET | The transfer moves a different mint than accepts[].asset. | Pay the advertised asset. |
402 | PAYMENT_WRONG_DESTINATION | The transfer is not to the associated token account of accepts[].payTo. | Pay the advertised payTo. |
402 | PAYMENT_AMOUNT_TOO_LOW | The transfer amount, or the balance actually credited to payTo, is below accepts[].amount. A Token-2022 transfer fee that leaves payTo short, or a Token-2022 payment whose credited amount can't be read, lands here. | Pay at least the advertised amount, net of any transfer fee. |
402 | PAYER_MISMATCH | The transfer was not signed by the wallet the challenge was bound to with X-Payer. | Pay from that wallet, or request a new challenge naming the wallet you pay from. |
402 | PAYMENT_LAYOUT_INVALID | The transaction's instructions don't match the x402 exact layout (extra instructions such as a Memo, a second transfer, compute-budget values out of bounds). | Rebuild the payment to the documented layout. |
402 | REFERENCE_MISSING | No issued, unexpired reference in the TransferChecked, or it is writable, a signer, repeated, or also in another instruction. | Append accepts[].extra.reference to the TransferChecked as one read-only, non-signer account. |
402 | REFERENCE_MISMATCH | The reference was issued for a different request (method, path, query, body or endpoint), or the payment carries more than one reference. | Retry the exact request the challenge was issued for, byte for byte. |
402 | REFERENCE_REUSED | A different payment carries a reference that already paid for a successful call. | Request a new challenge; each reference redeems once. |
402 | REFERENCE_EXPIRED | The reference's 600-second redeem window has passed. | Request a new challenge and pay its reference. |
402 | MULTISIG_AUTHORITY_UNSUPPORTED | The transfer authority is a token-program multisig account. | Pay from a single-signer wallet. |
402 | PAYMENT_UNVERIFIABLE | The payment could not be checked (for example a malformed transaction, or, on a deployment without a payment store, an RPC answer with no block time to bound the payment's age). | Check the signature you sent; pay again if the transaction is not a valid payment. |
402 | VERIFIER_UNAVAILABLE | A verification dependency (RPC lookup, payment store) is temporarily down. | Retryable. Your payment was not consumed; retry the same request shortly. |
402 | PAYMENT_INVALID | The payment does not satisfy the requirements, for a reason not covered above. message names it. | Read message, fix the payment, and pay again. |
409 | PAYMENT_ALREADY_USED / REFERENCE_REUSED | The payment was valid, but a concurrent request redeemed it first. | Final for that payment. Request a new challenge. |
503 | METERING_UNAVAILABLE | The payment store failed while recording your call, so the response is withheld. | Retryable. Your payment was not consumed; retry the identical request. |
400 | PAYMENT_HEADER_CONFLICT | Both PAYMENT-SIGNATURE and X-PAYMENT are present and differ. | Send one header (PAYMENT-SIGNATURE is the v2 name). |
429 | RATE_LIMITED | Too many unauthenticated payment requests from your address. | Back off, or use an X-API-Key. |
502 | X402_UNAVAILABLE | Pay-per-call is not available on this deployment right now. | Use an X-API-Key, or retry later. |
402 | SETTLEMENT_FAILED | USDC-via-CDP rail only: the facilitator refused to settle a payment it had verified. | Pay again against a fresh challenge. |
502 | X402_SETTLE_TIMEOUT / X402_FACILITATOR_ERROR | USDC-via-CDP rail only: the facilitator timed out or errored while settling. | Retry shortly. |
The USDC-via-CDP codes only apply where that rail is on; GET /v1/health lists the live rails (api.x1id.io currently runs x1-native only).
A call whose handler fails (for example a 404 for an unregistered name) returns that error untouched and does not consume your payment, so you can retry with the same payment.
On-chain program errors
The errors above come from the read surface (resolution). When you build and send a transaction — register, trade, make an offer, run an auction, manage a lease or recovery — the on-chain program can reject it with a custom error code. Wallets surface these as custom program error: 0x… (hex) or custom(…) (decimal). The common ones, with copy a UI can show:
| Code | Name | What it means | What to tell the user |
|---|---|---|---|
0x1781 (6017) | HandleIsTokenized | The name is a capability NFT. Offers, auctions and on-chain recovery (place_offer, start_auction, place_bid, set_recovery, initiate_recovery, cancel_recovery) are not available on a tokenized name. | "This name trades only as its NFT — sell it with a fixed-price listing or by sending the NFT; offers, auctions and recovery don't apply." |
0x93 (147) | pNFT frozen (listed) | The capability NFT is frozen because the name is listed. A plain send of a listed NFT fails with this. | "Cancel the listing before sending this name." |
0x0 | account already in use | A second offer from the same wallet on the same name — your first offer's PDA still exists. | "You already have an open offer on this name — cancel it first." |
0x1798 (6040) | PriceChanged | The listing or offer amount changed between when you loaded it and when you signed. | "The price changed — review the new amount and try again." |
0x1792 (6034) | BidTooLow | A bid below the reserve, or not at least 5% over the current high bid. | "Bid at least the reserve, or 5% above the current high bid." |
0x1799 (6041) | SellerCannotBid | The seller tried to bid in their own auction. | "You can't bid in your own auction." |
0x178f (6031) | AuctionNotEnded | Settling an auction that is still running. | "This auction hasn't ended yet." |
0x1790 (6032) | AuctionEnded | Bidding on an auction that has already ended. | "This auction has ended." |
0x1791 (6033) | AuctionHasBids | Cancelling an auction that already has bids, or moving the name out from under one: transfer, release_handle, initiate_recovery and complete_recovery all refuse while the name's auction carries a bid. | "This name has a live auction with bids, so it can't be cancelled, moved or recovered until the auction settles." |
0x1793 (6035) | BadDuration | An auction duration outside the allowed range (600 s to 2,592,000 s / 30 days). | "Pick a duration between 10 minutes and 30 days." |
0x17ab (6059) | VoucherNotExpired | Refunding a gift before its expiry. | "This gift hasn't expired yet — a refund is only possible after it expires." |
0x17dd (6109) | VoucherClaimNotRecipient | Claiming a recipient-bound gift through the name-only claim_voucher from a wallet other than the bound recipient. Relayers use claim_voucher_tokenized, which stays open to any signer. | "Only the person this gift was sent to can claim it this way." |
0x17df (6111) | AuctionNotStale | close_stale_auction on an auction whose seller still owns the untokenized name. | "This auction is still live; only the seller can cancel it." |
0x17e0 (6112) | VoucherExpiryTooFar | Creating a gift whose expiry is more than 366 days away. | "Pick a gift expiry within a year." |
0x17e1 (6113) | UnreceivableCreatorPayout | Creating a TLD whose creator payout is a sysvar or an executable account, which can never receive XNT. | "Use an ordinary wallet address for the creator payout." |
0x17e2 (6114) | NotTreasury | set_reserved_claim_approval signed by a key other than the treasury multisig. | "Only the treasury multisig can approve a reserved-name claim." |
0x17e3 (6115) | ReservedClaimNotApproved | A reserved-name claim grant for a domain the treasury multisig has not approved for that name (or approved before the name's current reservation). | "This name isn't approved for claiming by that domain." |
0x17e4 (6116) | NamespacePricingMismatch | register_under_namespace_v2 got a pricing account that isn't the TLD's canonical ["ns_pricing", label] PDA, isn't a valid price table, or is missing, including when a left-out optional placeholder shifts it into another slot. Also returned when the TLD is marked as priced but its price table account is absent. | "Something went wrong building this purchase. Refresh and try again." |
0x17e5 (6117) | NamespacePricingRequiresV2 | The original register_under_namespace on a TLD whose creator has set its own prices. Those names can only be registered with register_under_namespace_v2. | "This TLD sets its own prices. Update the app, or buy it somewhere that supports creator pricing." |
0x17e6 (6118) | PriceExceedsMax | register_under_namespace_v2 where the current price is above the max_price_lamports the buyer approved, for example because the creator raised prices after the quote. Nothing is charged except the network fee. | "The price went up since you got your quote. Review the new price and try again." |
0x17e7 (6119) | NamespaceTierBelowFloor | set_namespace_pricing with a price below the 1 XNT protocol floor (including 0). | "Each price must be at least 1 XNT." |
0x17e8 (6120) | NamespaceTierAboveMax | set_namespace_pricing with a price above the 100,000,000 XNT protocol ceiling. | "Each price must be at most 100,000,000 XNT." |
custom(6101) | lease first payment too small | The first lease-to-own payment is below the one-year minimum (0.025 XNT at divisor 4). | "The first lease payment must cover at least one year." |
0xbc4 (3012) | delegate PDA missing | A record-delegate action when no delegate is set for the name (Anchor AccountNotInitialized). | "No record delegate is set for this name." |
| — | reclaim_expired_lease on a permanent name | The name was registered permanently (or already paid off), so there's no lease to reclaim. | "This name is permanent — there's nothing to reclaim." |
These are the on-chain program's own codes. The exact hex/decimal pairs are stable for a given program version; branch on them where you can, but always keep a generic "the transaction was rejected" fallback for a code you don't recognise. The anti-snipe extension is end = max(end, now + 300s) — a late bid pushes the end to 5 minutes from that bid, it does not simply add 5 minutes each time.
General guidance
- Never render a bare address. Every successful result — from the SDK or the REST API — carries the namespace that produced it. Show it in any confirmation UI.
- Treat
ambiguousas blocking, not a fallback chain. Don't retry the same input against "just the handle" or "just the domain" — that reintroduces exactly the wrong-recipient risk the error exists to prevent. not-found,no-record-for-chainand "no primary" are not retryable. They're correct, final answers about the current state of the registry.- Transport errors (
rpc-error, HTTP502) are the only retryable class on the read surface. Back off and retry; everything else is a stable answer. For x402,PAYMENT_NOT_FINALIZED,VERIFIER_UNAVAILABLEandMETERING_UNAVAILABLEare the retryable codes; afterREDEEM_SIGNATURE_REQUIREDorREDEEM_SIGNATURE_INVALID, resend the same payment with a validpayerSig.
