Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions book/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ No options. Outputs the mnemonic and address.

## `airnode address`

Derive and display the airnode address from the `AIRNODE_PRIVATE_KEY` environment variable. Useful for verifying which
address your node will use without starting it.
Derive and display the Airnode address from `AIRNODE_MNEMONIC` or `AIRNODE_PRIVATE_KEY`. This lets you check the address
without starting the server.

```bash
airnode address
```

No options. Requires `AIRNODE_PRIVATE_KEY` in the environment.
No options. At least one signing credential is required. If both are set, the mnemonic takes precedence.

## `airnode identity show`

Expand All @@ -89,7 +89,7 @@ airnode identity show --domain <domain> [options]
| `--domain <domain>` | `-d` | -- | Domain name to associate |
| `--chain-id <id>` | | `1` | Chain ID for the TXT record |

Requires `AIRNODE_PRIVATE_KEY` in the environment.
Requires `AIRNODE_MNEMONIC` or `AIRNODE_PRIVATE_KEY`. If both are set, the mnemonic takes precedence.

```bash
airnode identity show --domain api.coingecko.com
Expand Down
6 changes: 3 additions & 3 deletions book/docs/concepts/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ giving plugins the ability to observe, filter, or modify data at each stage.
11. **Plugin: onBeforeSign** -- plugins can modify the encoded (or, for `encrypt` endpoints, encrypted) data before
signing.
12. **Sign** -- EIP-191 personal sign over `keccak256(encodePacked(endpointId, timestamp, data))`. The signature proves
the data came from this airnode at this time for this endpoint.
the Airnode key signed this payload and timestamp for this endpoint.
13. **TLS proof** -- if proof is enabled in settings and the endpoint has `responseMatches`, request a TLS proof from
the proof gateway. Proof failures are non-fatal -- the response is returned without a proof. See
[TLS Proofs](/docs/concepts/proofs).
Expand Down Expand Up @@ -96,8 +96,8 @@ raw-response hashing rules.

When Airnode starts:

1. Load and validate `config.yaml` against the Zod schema.
2. Interpolate environment variables (`${VAR}` references in the config).
1. Load `config.yaml` and interpolate environment variables (`${VAR}` references).
2. Validate the resolved config against the Zod schema.
3. Derive the airnode address from `AIRNODE_PRIVATE_KEY` or `AIRNODE_MNEMONIC`.
4. Build the endpoint map: compute each endpoint ID and register it.
5. Load plugins from their `source` paths.
Expand Down
258 changes: 82 additions & 176 deletions book/docs/concepts/endpoint-ids.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions book/docs/concepts/fhe-encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ signing:
[@zama-fhe/relayer-sdk](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer) SDK, producing an
encrypted-input handle and a zero-knowledge proof.
3. It replaces the `data` field with `abi.encode(bytes32 handle, bytes inputProof)`.
4. Airnode signs the ciphertext — the EIP-191 signature proves the encrypted data is authentically from the API
provider. (`onBeforeSign` plugins also see the ciphertext, not the plaintext.)
4. Airnode signs the ciphertext. The EIP-191 signature identifies the signing key; provider identity must be established
separately. (`onBeforeSign` plugins also see the ciphertext, not the plaintext.)
5. The client submits the signed response to `AirnodeVerifier` on-chain (the existing contract — no changes needed).
6. `AirnodeVerifier` verifies the signature and forwards the bytes to the consumer's callback, which unpacks the data,
registers the FHE handle via `FHE.fromExternal(handle, inputProof)`, and manages its own decryption access control.
Expand Down Expand Up @@ -165,7 +165,7 @@ euint256 price = FHE.fromExternal(handle, inputProof);
## Example consumer contract

An example `ConfidentialPriceFeed` contract is provided in
[`contracts/src/examples/`](https://github.com/api3dao/airnode-v2/tree/main/contracts/src/examples). It demonstrates:
[`contracts/src/examples/`](https://github.com/andreogle/airnode-v2/tree/main/contracts/src/examples). It demonstrates:

- Receiving encrypted price data via the `AirnodeVerifier` callback pattern
- Registering FHE handles with the coprocessor
Expand Down
3 changes: 2 additions & 1 deletion book/docs/concepts/proofs.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,6 @@ https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd

## Example config

See [`examples/configs/reclaim-proof/`](https://github.com/api3dao/airnode-v2/tree/main/examples/configs/reclaim-proof)
See
[`examples/configs/reclaim-proof/`](https://github.com/andreogle/airnode-v2/tree/main/examples/configs/reclaim-proof)
for a minimal working configuration with TLS proofs enabled.
6 changes: 3 additions & 3 deletions book/docs/concepts/request-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ The `data` field is the ABI-encoded value extracted from the API response at the
`times` if specified, and encoded as the configured `type` (e.g., `int256`).

The `proof` field is present only when [TLS proofs](/docs/concepts/proofs) are enabled and the endpoint has
`responseMatches` configured. It contains the attestor's cryptographic attestation that the data came from the claimed
API over TLS.
`responseMatches` configured. It attests that the gateway's separate HTTPS response matched configured patterns. It does
not prove that Airnode signed the same response.

## Response (raw)

Expand All @@ -61,7 +61,7 @@ covers the keccak256 hash of the JSON:
```

Raw mode is useful when the consumer needs the full JSON structure or when multiple values from the same response are
needed. The signature still proves the data came from this airnode, but the data itself is not ABI-encoded for on-chain
needed. The signature identifies the Airnode key that signed the response, but the data is not ABI-encoded for on-chain
use. The `proof` field appears when [TLS proofs](/docs/concepts/proofs) are enabled for the endpoint.

## Response (empty)
Expand Down
10 changes: 5 additions & 5 deletions book/docs/concepts/signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ sidebar_position: 4

# Signing and Verification

Every response from Airnode is signed with the operator's private key using EIP-191. The signature proves that a
specific airnode produced specific data for a specific endpoint at a specific time. Consumers can verify the signature
off-chain or submit it to an on-chain contract for verification.
Every response from Airnode is signed with the operator's private key using EIP-191. The signature proves that the key
holder signed specific data for a specific endpoint and timestamp. It does not prove who controls the key or where the
data came from. Consumers can verify the signature off-chain or submit it to an on-chain contract.

## Signature Format

Expand All @@ -29,8 +29,8 @@ The three fields are ABI-packed with their types:
The `endpointId`, `timestamp`, and `data` are packed as separate fields -- not nested inside another hash. This is a
deliberate design choice:

- **On-chain contracts** can decode the packed data and inspect each field independently. A freshness check can reject
data with a stale timestamp. An endpoint filter can reject data for an unexpected endpoint.
- **On-chain contracts** receive each field separately and can inspect it before using the data. A consumer can reject a
stale timestamp or an unexpected endpoint ID.
- **TLS proof verification** can match the endpoint ID against the observed HTTP request without needing to reconstruct
a nested hash structure.
- **Simplicity** -- the signed message is a single `keccak256(encodePacked(...))`, which maps directly to how Solidity
Expand Down
34 changes: 16 additions & 18 deletions book/docs/config/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,8 @@ uniqueness key — it can be redeemed exactly once.

`expiresAt` must be a future unix-seconds timestamp no further ahead than 10 minutes; longer-lived proofs are rejected.

Submitted proofs are additionally rate-limited **per client IP** before the airnode touches the chain RPC — currently 30
attempts per minute per IP. The unpaid 402 challenge path is unaffected. This is independent of `server.rateLimit` and
keeps an unauthenticated flooder from draining the operator's RPC quota by spamming bogus proofs.
Submitted proofs use the separate `server.rateLimit.x402` per-IP limit before Airnode calls the chain RPC. The unpaid
402 challenge path does not use this bucket.

### Multiple auth methods

Expand Down Expand Up @@ -155,10 +154,9 @@ caller (and every on-chain submission) gets the identical signed payload.

:::warning On-chain race within the cache window

Because every caller in a TTL window receives the **byte-identical** signed response, only the first on-chain submission
through `AirnodeVerifier` succeeds. The verifier's `fulfilled[]` mapping treats subsequent submissions of the same
`(endpointId, timestamp, data)` as replays and reverts with `"Already fulfilled"` — the second and third callers pay gas
for a failed transaction.
Because every caller in a TTL window receives the **byte-identical** signed response, only the first delivery to a given
callback and selector succeeds. Repeating the same delivery reverts with `"Already fulfilled"`, so later callers may pay
gas for a failed transaction.

For endpoints whose consumers race to submit on-chain (price feeds, single-fulfillment auctions), set a short `maxAge`
(e.g. 1000ms) or omit `cache` entirely so each caller gets a fresh signature. Caching is most useful for off-chain
Expand Down Expand Up @@ -265,15 +263,15 @@ In short: only `body` parameters may be nested; everything else should be a prim

### Parameter fields

| Field | Type | Required | Default | Description |
| ------------- | ----------------------------- | -------- | ------- | -------------------------------------------------------------- |
| `name` | `string` | Yes | -- | Parameter name as the upstream API expects it. |
| `in` | `string` | No | `query` | Where to send: `query`, `header`, `path`, `cookie`, or `body`. |
| `required` | `boolean` | No | `false` | If `true`, the client must provide this parameter. |
| `fixed` | `string \| number \| boolean` | No | -- | Hardcoded value. Always overrides the client's value. |
| `default` | `string \| number \| boolean` | No | -- | Fallback value when the client does not provide one. |
| `secret` | `boolean` | No | `false` | If `true`, excluded from endpoint ID derivation. |
| `description` | `string` | No | -- | Human-readable description. |
| Field | Type | Required | Default | Description |
| ------------- | ----------------------------- | -------- | ------- | ----------------------------------------------------------------------- |
| `name` | `string` | Yes | -- | Parameter name as the upstream API expects it. |
| `in` | `string` | No | `query` | Where to send: `query`, `header`, `path`, `cookie`, or `body`. |
| `required` | `boolean` | No | `false` | If `true`, the client must provide this parameter. |
| `fixed` | `string \| number \| boolean` | No | -- | Hardcoded value. Always overrides the client's value. |
| `default` | `string \| number \| boolean` | No | -- | Fallback value when the client does not provide one. |
| `secret` | `boolean` | No | `false` | Omits the parameter value, but not the parameter, from the endpoint ID. |
| `description` | `string` | No | -- | Human-readable description. |

### Fixed vs default

Expand All @@ -295,8 +293,8 @@ optional — there is always a fallback value. The schema validator rejects this

### Secret parameters

Parameters marked `secret: true` are excluded from endpoint ID derivation. This means changing a secret parameter does
not change the endpoint ID.
Parameters marked `secret: true` remain part of the endpoint specification, but their values are excluded. Changing the
secret value does not change the endpoint ID. Adding, removing, or renaming the parameter does.

Parameters with `fixed` values that use `${VAR}` interpolation are also treated as secret automatically.

Expand Down
6 changes: 4 additions & 2 deletions book/docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Airnode is configured with a single YAML file. JSON is also accepted. The file h
| ----------------------------------- | ------------------------------------------------------- |
| `version` | Must be `'1.0'`. Used for schema validation. |
| [`server`](/docs/config/server) | HTTP server port, host, CORS, and rate limiting. |
| [`settings`](/docs/config/settings) | Timeout, upstream concurrency, proof, FHE, and plugins. |
| [`settings`](/docs/config/settings) | Upstream concurrency, proof, FHE, and plugins. |
| [`apis`](/docs/config/apis) | Upstream API definitions with endpoints and parameters. |

## Minimal config
Expand All @@ -22,7 +22,9 @@ version: '1.0'

server:
port: 3000
host: '0.0.0.0'
rateLimit:
trustForwardedFor: false
window: 60000
max: 100
x402:
Expand Down Expand Up @@ -56,14 +58,14 @@ server:
origins:
- '*'
rateLimit:
trustForwardedFor: false
window: 60000
max: 100
x402:
window: 60000
max: 30

settings:
timeout: 15000
maxConcurrentApiCalls: 50
proof: none # or: { type: reclaim, gatewayUrl: 'http://localhost:5177/v1/prove' }
plugins:
Expand Down
2 changes: 1 addition & 1 deletion book/docs/config/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ hook from `onBeforeApiCall` onward:

- `onBeforeApiCall`, `onAfterApiCall`, `onBeforeSign` — **only fire on the first request in each cache window**. Cached
hits within the TTL never invoke them.
- `onHttpRequest`, `onResponseSent`, `onError` — fire on every request, cached or not.
- `onHttpRequest` and `onResponseSent` fire for cached and uncached requests. `onError` fires only when a request fails.

If your plugin tracks per-request signal (heartbeats, counters, alerting), put that work in `onResponseSent` or
`onHttpRequest`. Don't rely on the mutation hooks to run once per HTTP request — they run once per upstream API call,
Expand Down
11 changes: 5 additions & 6 deletions book/docs/consumers/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,13 @@ const ethPrice = rawData.ethereum.usd; // 3842.17

At this point you have:

- A verified `airnode` address (proves who signed it).
- An Airnode address from a trusted provider-controlled source.
- A decoded `value` (the price, the temperature, whatever the endpoint serves).
- A `timestamp` (so you can decide if the value is fresh enough for your use case).

That's the full off-chain consumer loop. Use the value in your app, run your own staleness check
(`Date.now() / 1000 - timestamp < maxAgeSeconds`), and store or forward the signed payload if you want to prove
provenance to a downstream system later — `(airnode, endpointId, timestamp, data, signature)` is a self-contained
attestation that anyone can re-verify.
That's the full off-chain consumer loop. Use the value in your app and apply your own staleness check
(`Date.now() / 1000 - timestamp < maxAgeSeconds`). You can store the signed payload for later verification, but anyone
checking it still needs an independent reason to trust the signer and data source.

## Step 7: Submit on-chain (optional)

Expand Down Expand Up @@ -194,7 +193,7 @@ out of nothing.
| `400` | ``Endpoint requires `_type` request parameter`` | The operator marked `type: '*'`. Supply `_type` in `parameters`. |
| `400` | ``Endpoint requires `_path` request parameter`` | The operator marked `path: '*'`. Supply `_path` in `parameters`. |
| `400` | ``Endpoint requires `_times` request parameter`` | The operator marked `times: '*'`. Supply `_times` in `parameters`. |
| `401` | `Missing X-Api-Key header` | The endpoint requires authentication. Add `X-Api-Key: your-key` to the request. |
| `401` | `Missing X-Api-Key header` | The endpoint requires authentication. Add the `X-Api-Key` header. |
| `401` | `Invalid API key` | The key value is wrong. Check with the airnode operator. |
| `404` | `Endpoint not found` | The endpoint ID is incorrect. Verify the ID with the operator. |
| `413` | `Request body too large` | The request body exceeds 64KB. Reduce the payload size. |
Expand Down
2 changes: 1 addition & 1 deletion book/docs/consumers/on-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ AirnodeVerifier contract, which verifies the signature and forwards the data to
Your contract receives the callback. `verifyAndFulfill` is **permissionless** (anyone can submit any valid
Airnode-signed payload and point the callback anywhere) and signed payloads are **public**, so your `fulfill` must run
four checks before trusting the data. A documented reference is
[`AirnodePriceConsumer.sol`](https://github.com/api3dao/airnode-v2/blob/main/contracts/src/examples/AirnodePriceConsumer.sol);
[`AirnodePriceConsumer.sol`](https://github.com/andreogle/airnode-v2/blob/main/contracts/src/examples/AirnodePriceConsumer.sol);
the essentials:

```solidity
Expand Down
6 changes: 3 additions & 3 deletions book/docs/contracts/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The fields it commits to:

- **endpointId** -- a specification-bound hash committing to the API URL, path, method, parameters, and encoding rules.
Two independent airnodes serving the same API with the same config produce the same endpoint ID.
- **timestamp** -- unix timestamp (seconds) of when the data was produced.
- **timestamp** -- Unix timestamp (seconds) of when Airnode signed the data.
- **data** -- the signed payload: ABI-encoded value, an FHE ciphertext, or `keccak256` of the raw JSON. The contract
treats it as opaque `bytes` and forwards it to the callback unchanged. (Airnode caps the HTTP _request_ body at 64 KB,
but the contract imposes no size limit on `data`.)
Expand All @@ -57,5 +57,5 @@ is implemented inline for auditability and to minimize the attack surface.

### Flat architecture

A standalone contract with no shared state, no inheritance chain, and no external dependencies. It can be deployed and
used independently.
A standalone contract with no admin or configuration state, no inheritance chain, and no external dependencies. It keeps
only replay-delivery state and can be deployed independently.
Loading