Appearance
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
dataas calldata, and eachProjectedJsonread parses all ofdataagain. 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.
ProjectedJsonreads 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
400and 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
ProjectedJsonreverts 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. ProjectedJsonreads only top-level numbers, strings and booleans. Project each value that you need as its own field. To read inside a projected object, useJSONParserLib.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"]]]forPOST /.pnpm request-hashalways adds the starter's projection, so compute this hash yourself. datais 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 withFieldNotFound, becausevalueis 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.