railHint · draft-railhint-01 · August 2026 (supersedes draft-railhint-00: carrier key is now rail-hint, object shape is now {info, schema})
Category: x402 community extension (advisory) · Requires: x402 v2
Schema: railhint.schema.json · Reference implementation: github.com/feeless402/feeless402
Status: DRAFT — seeking community review

railHint: in-protocol rail negotiation for x402 payment-required responses

Abstract

x402 lets a server offer multiple payment options in accepts, but gives a machine client no structured way to learn why one rail is cheaper or how to onboard onto a rail it does not yet hold. This document defines extensions["rail-hint"], an advisory object a server MAY attach to an HTTP 402 response, carrying a cost rationale and an onboarding path for the offer it considers cheapest for the payer. Clients that do not understand the extension ignore it; the accepts array remains the only binding payment terms. “railHint” remains the extension's name in prose; the wire key follows the x402 SDK's kebab-case convention.

Design principle: discovery, not execution

A railHint tells a client where to look, never what to run. accepts is the sole binding term of the payment; a client that ignores the hint loses information and nothing else. Fields naming commands or endpoints (bootstrap, topup, faucets) are untrusted remote input, exactly as a URL in an HTTP body is: a client MUST NOT act on them blindly, and a server MUST NOT depend on their execution — a hint that only works when its command is run is out of spec.

The same boundary applies to numbers: railHint is not a price oracle or fee-comparison matrix. cheapest and why annotate the server's own offers — claims a client can verify against the amounts already binding in accepts — never live market data or fee breakdowns for third-party networks. A client SHOULD treat any cost claim it cannot check against accepts as advertising and do its own arithmetic.

Concretely, prices move through the handshake in four steps, and no step queries an external price source:

  1. The server prices its own offers. Each entry in accepts is a binding quote denominated in that rail's asset, set by the server before the 402 is sent. How the server converts between currencies to arrive at those quotes — an exchange feed, a fixed peg, manual pricing — is its own business, out of band and out of scope for this specification.
  2. The server annotates its own menu. A rail-hint names which existing accepts entry the server considers lowest total-cost for the payer (cheapest) and explains why (why), using only its own quotes and the fee structure of its own offers — e.g. “this rail's quote is the whole cost; that rail adds a per-transaction fee floor.”
  3. The client verifies against the same response. cheapest MUST match an entry in accepts; every cost claim in why is checkable against the amounts binding three lines up in the same HTTP body. Nothing requires trusting the server about market data, because none is present.
  4. Cross-currency comparison stays with the client. If a client wants to know what a quote is worth in a currency the server does not quote in accepts, it uses its own price source — exactly as it would for any x402 response with no hint at all. No rail-hint field carries an exchange rate, and this specification defines no mechanism for obtaining one.

This is deliberate. Rail negotiation belongs inside the existing 402 handshake, and a payment-required response must never become a remote-code channel. The obligations this places on clients and servers are detailed in Security considerations.

Motivation

Autonomous clients select payment rails by arithmetic, but today the information needed to switch rails — comparative cost, client software, asset acquisition — lives in human documentation outside the protocol. The result is rail inertia: clients pay on whichever rail they already hold, even when both parties would benefit from another offer in the same accepts menu (for example, a rail without per-transaction fees or minimum-price floors). railHint moves that negotiation into the 402 response itself — the one message every paying client is guaranteed to parse, at the exact moment of payment choice.

The railHint object

The extension is carried at extensions["rail-hint"] as an object with exactly two members, following the convention every extension in the x402 SDK uses: info holds the hint's fields, and schema holds the JSON Schema (Draft 2020-12) that validates info, shipped inline so a client can validate the declaration without a network fetch.

{
  "x402Version": 2,
  "accepts": [
    { "scheme": "exact", "network": "base",         "asset": "USDC", "amount": "1000",  "payTo": "0x..." },
    { "scheme": "exact", "network": "nano:mainnet", "asset": "XNO",  "amount": "6690000000000000000000000", "payTo": "nano_..." }
  ],
  "extensions": {
    "rail-hint": {
      "info": {
        "cheapest": "nano:mainnet",
        "why": "The base offer is floored at 0.001 USDC; the nano:mainnet offer is the metered price ($0.0000027) with zero network fees and sub-second finality.",
        "docs": "https://example.com/docs",
        "agentReadme": "https://example.com/llms.txt",
        "bootstrap": "pip install feeless402 && nano-pay init",
        "topup": "any instant-swap service: ~$5 of a held asset into the hinted asset",
        "faucets": ["https://example.com/faucet"],
        "spec": "x402 exact scheme on nano:mainnet"
      },
      "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema",
                  "$id": "https://railhint.com/railhint.schema.json",
                  "…": "canonical copy at railhint.com/railhint.schema.json" }
    }
  }
}

Legacy key (deprecated). draft-railhint-00 carried the fields flat at extensions.railHint with no info wrapper and no inline schema. Servers built against that draft should migrate to rail-hint; during transition a server may emit both keys with identical field content. Clients should prefer rail-hint when both are present. The legacy key will be dropped from a future draft.

Field definitions

All fields below live inside info.

FieldRequirementSemantics
cheapestREQUIREDThe network value of the offer in accepts the server considers lowest total-cost for the payer. MUST match an entry in accepts.
whyREQUIREDRationale a reasoning client can evaluate. SHOULD contain concrete, comparable prices rather than slogans.
docsOPTIONALDocumentation URL for the hinted rail.
agentReadmeOPTIONALPlain-text (llms.txt-style) onboarding document.
bootstrapOPTIONALExecutable onboarding path. Untrusted input; see Security Considerations.
topupOPTIONALPath from commonly-held assets to the hinted asset. MUST NOT require a specific vendor.
faucetsOPTIONALEndpoints offering small starter grants (POST {"address": ...}). MAY require a proof-of-work challenge via GET <faucet>/challenge to resist sybil claims.
specOPTIONALReference to the scheme specification for the hinted rail.

Client behavior

A client that already holds the hinted asset proceeds exactly as without the extension: select an offer from accepts, pay, retry. A reasoning client may evaluate why, consult docs or agentReadme, acquire the asset via topup or faucets, and retry the original request on the cheaper rail. A client that does not recognize rail-hint ignores it with no loss of function.

Server behavior

Servers may attach railHint to any 402 response whose accepts contains more than zero offers. Servers must not place terms in railHint that contradict accepts; on any conflict, accepts is authoritative. Servers should keep why current with real prices — stale or exaggerated rationales are trivially falsified by clients comparing offers.

Security considerations

bootstrap and topup are untrusted remote input. A client must not execute them blindly; it should (a) act only on schemes it can verify against an independently obtained specification, (b) apply its own spending caps regardless of hinted amounts, and (c) prefer allowlisted package sources. The hint's role is discovery, not authority. Faucet endpoints should employ proof-of-work challenges or equivalent cost-imposing mechanisms so that sybil claims cost the claimant more than the grant is worth.

Rail neutrality

railHint is rail-agnostic. Any server may hint any rail — a Lightning merchant may hint bitcoin-lightning; a stablecoin merchant may hint an L2. The extension standardizes the negotiation, not a winner. Where genuinely fee-free rails exist, honest hints will tend to favor them; that is a property of arithmetic, not of this document.

IANA-style considerations

This extension registers the key rail-hint within the x402 v2 extensions namespace. The key railHint is reserved as a deprecated alias from draft-railhint-00 (see Legacy key). No other names are claimed. The JSON Schema at railhint.com/railhint.schema.json is versioned with this draft.

Acknowledgements

Thanks to u/xnoforge, who reviewed the reference implementation and argued that “discovery, not authority” belonged at the front of this document rather than in its security section — because commands riding inside a 402 are the first thing a reviewer will challenge. The Discovery, not execution section above is the result.

Thanks to noonoop of Subnano, who asked whether a single-rail server — one whose accepts will only ever name one rail, and so can never demonstrate rail negotiation — still belongs in this proposal. The answer sharpened this document's scope: discovery and onboarding are railHint's core use; negotiation is the special case. Subnano plans to upgrade its x402 v1 endpoint to canonical v2, using a Feeless402 client as its first independent interoperability test.

Reference implementation. Thanks to u/twaanman, who publicly audited the reference client before running it, performed the first independent end-to-end verification of the faucet-to-paid-call funnel, and surfaced two client bugs fixed in release 0.2.3. Specs are words; independent verification is what makes them checkable.