Skip to content

The HTTP interface

An Airnode is two endpoints, and it describes both itself. This is the pull model: a consumer asks and the Airnode answers, and a polling module that pushes on a timer does not exist yet.

Every response allows any origin (Access-Control-Allow-Origin: *). An Airnode holds no cookies or caller identity a cross-origin read could leak, and a browser running an agent is a consumer like any other.

GET / describes the Airnode

GET / returns an OpenAPI 3.1 document for the Airnode itself, so an agent that already speaks OpenAPI needs nothing bespoke to call it. Discovery is free and machine-readable. The document is titled after the API it wraps, lists the Airnode's own URL under servers, and covers both endpoints.

Every operation the Airnode serves appears as one accepted shape of the POST / request body, as a oneOf of variants. A variant pins the operation field to one operationId with a const and declares that operation's parameters as a strict object: the required ones are listed and unknown properties are refused. Each parameter is published exactly as the upstream's document declared it, nesting and all, so a caller can tell which parameters go with which operation without a separate lookup.

The shape every response takes is published under components.schemas.Attestation, and everything a verifier needs sits under the x-airnode extension:

json
"x-airnode": {
  "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  "attestation": {
    "scheme": "eip191-secp256k1",
    "digest": "keccak256(abi.encodePacked(bytes32 requestHash, uint256 timestamp, bytes dataBytes))",
    "dataBytes": "data itself when data is a string, JSON.stringify(data) otherwise, as UTF-8",
    "requestHash": "keccak256(utf8(JSON.stringify([operation, parameterEntriesSortedByName]))), where an object value is itself entries sorted by name, at any depth, and an array keeps its order",
    "verify": "Recover the signer of the signature over the digest and check it equals the address above."
  }
}

What the document leaves out matters as much as what it contains: the upstream URL and the way parameters map onto it never leave the process. Publishing them would let a consumer skip the Airnode and call the provider's API directly.

POST / performs an operation

The body names an operation and its parameters:

json
{ "operation": "coinPrice", "parameters": { "ids": "ethereum", "vs_currencies": "usd" } }

The response is the upstream data plus the attestation over it. data carries the upstream body as JSON when the upstream answered JSON, and as text otherwise:

json
{
  "airnode": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  "requestHash": "0x…",
  "timestamp": "1752580000",
  "data": { "ethereum": { "usd": 1920.51 } },
  "signature": "0x…"
}

An unknown operation is a 404, a missing, unknown or mistyped parameter is a 400, and an upstream failure is a 502. Unknown parameters are rejected rather than dropped, so a caller mistyping a name hears about it instead of silently receiving an attestation over a request they did not mean to make.

Payments

An operation can be priced with x402, which fits the free GET / and paid POST / split exactly: discovery is free, the resource is paid. Pricing is opt-in per deployment. An Airnode that names no price for an operation serves it free, exactly as before, so a free Airnode is unaffected by any of this.

A priced operation answers an unpaid POST / with 402 and a body of payment requirements:

json
{
  "x402Version": 1,
  "error": "Payment required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "1000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0x…",
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ]
}

The caller signs an EIP-3009 transferWithAuthorization matching one entry, base64-encodes the signed payload, and retries with it in the X-PAYMENT header. The Airnode has a facilitator verify the authorization, performs the operation, then has the facilitator settle it on-chain, and returns the attestation with the settlement receipt in the X-PAYMENT-RESPONSE header. The Airnode never touches the chain itself: an x402 facilitator does the verifying and submitting and pays the gas. Settlement is USDC on Base.

Verification happens before the upstream is called and settlement only after it succeeds, so a call that fails upstream is never charged, and a caller who is charged always gets the data. Prices are per operation: an unpaid POST / reads the requested operation from the body and returns requirements for that operation's price, so the single shared POST / URL is not a limitation. Every price an Airnode charges is also published under x-airnode.payment in GET /, so an agent learns what a call costs from the same document that tells it how to make one.

The one deployment detail that matters: a facilitator that settles on Base has to be named, such as Coinbase's CDP facilitator. There is no default, so an Airnode is never left charging callers through a facilitator the operator did not choose.