Skip to content

Projections ​

A projection maps field names that you choose to JSON Pointers into the API response. The Airnode then signs only those fields, and the request hash covers the projection:

json
"responseProjection": { "price": "/ETH~1USD/value", "updatedAt": "/ETH~1USD/timestamp" }
json
"data": "{\"price\":2443.95,\"updatedAt\":1792403240691}"

Through the passthrough, the same projection goes in the query, once for each field: x-airnode-project=price:/ETH~1USD/value&x-airnode-project=updatedAt:/ETH~1USD/timestamp.

Why projections ​

Use a projection for every request that a contract consumes:

  • Gas. The transaction pays for every byte of data as calldata, and each ProjectedJson read parses all of data again. A read costs about 165,000 gas for each KB. A read of the projected Nodary price (43 bytes) costs about 16,000 gas. A read from a 4.4 KB response costs about 720,000 gas.
  • Simple reads. A projection puts each value in a top-level field. ProjectedJson reads only top-level fields. Without a projection, Nodary's price is at {"ETH/USD":{"value":…}}, and you need your own nested reader.
  • Failure before gas. If the response does not have a projected field, the Airnode answers 400 and signs nothing. Without a projection, the Airnode signs the response, and the contract reverts only after someone pays gas to submit it.
  • Smaller responses for agents. An agent pays for every token that it reads.

Why the hash covers the projection ​

You choose the field names, and the signed data does not contain the pointers. If a contract examined only the signer and the field names, anyone could request {"price": "/ETH~1USD/timestamp"}. The Airnode signs {"price":1792403240691}, and the contract reads a timestamp as a price. Only the stored request hash shows which request the data answers. With that hash, a projected response is as safe as the whole response. Only the exact request passes lists which changes to a request change its hash.

Projected values ​

  • Each value is copied as the API wrote it. An integer of any size, such as an amount in wei, keeps every digit. A number has exponent notation only if the API writes it that way, and ProjectedJson reverts on exponent notation.
  • The fields come in the byte order of their names, whatever order the request used. Thus a request hash always comes with the same field order, such as {"price":…,"updatedAt":…}.
  • At most 20 fields, each with its own pointer that starts with /. For the whole response, send no projection.
  • A pointer can go to any depth. Its value can be a number, a string, a boolean, null, an object or an array.
  • ProjectedJson reads only top-level numbers, strings and booleans. Project each value that you need as its own field. To read inside a projected object, use JSONParserLib.parse(string(data)).at('"feed"').at('"value"').
  • A pointer reaches an array element only by index, such as /flights/0/status. It cannot select an element by a property. Make sure that the API keeps the order of the array stable.
  • A missing field gives 400. You cannot get a signed proof that a field is missing.

Without a projection ​

Leave out the projection only when the contract needs the whole response. verify and the timestamp check work the same, but:

  • The request hash leaves out the projection, such as ["latestFeeds",[["name","ETH/USD"]]] for POST /. pnpm request-hash always adds the starter's projection, so compute this hash yourself.
  • data is the whole response, exactly as the API sent it, with the field names of the API, often nested. Nodary's ETH/USD response is {"ETH/USD":{"value":…,"timestamp":…,"category":"crypto"}}. ProjectedJson.readUint(data, "value", 18) reverts with FieldNotFound, because value is one level down, so you need your own nested reader.
  • Every cost in Why projections applies. The contract pays to parse all of data, and a response without a field is signed anyway and fails only on-chain.