Skip to content

Commit 7946425

Browse files
committed
docs: audience-first sidebar, close consumer journey, expand troubleshooting
Phase 3 of the technical-writer audit — structural improvements. - book/sidebars.ts: reorder so Operators and Consumers appear immediately after Introduction, before Concepts. Reframes the IA as audience-first rather than topic-first. Renamed "Airnode Operators" to "Operators" for parity with "Consumers", "Config" to "Config Reference". Moved v1-comparison from the top to the appendix-style tail. - consumers/getting-started.md: add a "You're done (if you only need the value off-chain)" step before the on-chain section. Closes the off-chain consumer journey explicitly — Step 7 (on-chain) is now framed as optional rather than the implied conclusion. - operators/publishing-endpoints.md (new): the operator's checklist for handing endpoint IDs, airnode address, and request contracts to consumers. Covers the parts of the operator-day-one workflow that weren't in operators/index.md (running the server) or operators/deployment.md (production runtime). - operators/index.md "Next steps": link to the new publishing page. - troubleshooting.md: expanded from 8 entries to 16. Added 429 (rateLimit), 401 x402 verification flood, 402 payment-required, 503 busy (maxConcurrentApiCalls), FHE encryption failures, plugin budget exhaustion, plugin cache-window non-firing, async store 404, async store 503, and CORS rejection — all of which were silently undocumented before.
1 parent c22922c commit 7946425

5 files changed

Lines changed: 239 additions & 22 deletions

File tree

book/docs/consumers/getting-started.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,24 @@ Raw responses need no decoding. Access the JSON directly:
139139
const ethPrice = rawData.ethereum.usd; // 3842.17
140140
```
141141

142+
## You're done (if you only need the value off-chain)
143+
144+
At this point you have:
145+
146+
- A verified `airnode` address (proves who signed it).
147+
- A decoded `value` (the price, the temperature, whatever the endpoint serves).
148+
- A `timestamp` (so you can decide if the value is fresh enough for your use case).
149+
150+
That's the full off-chain consumer loop. Use the value in your app, run your own staleness check
151+
(`Date.now() / 1000 - timestamp < maxAgeSeconds`), and store or forward the signed payload if you want to prove
152+
provenance to a downstream system later — `(airnode, endpointId, timestamp, data, signature)` is a self-contained
153+
attestation that anyone can re-verify.
154+
142155
## Step 7: Submit on-chain (optional)
143156

144-
Pass the signed data to an on-chain verifier contract. See [On-Chain Integration](/docs/consumers/on-chain) for contract
145-
examples using AirnodeVerifier.
157+
If you instead want a smart contract to consume the data, hand `(airnode, endpointId, timestamp, data, signature)` to
158+
`AirnodeVerifier.verifyAndFulfill(...)`. The verifier checks the signature and forwards to your callback. See
159+
[On-Chain Integration](/docs/consumers/on-chain) for contract examples.
146160

147161
## Choosing encoding at request time
148162

book/docs/operators/index.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ Bun automatically loads `.env` files from the working directory.
125125

126126
## Next steps
127127

128-
- [Deployment](/docs/operators/deployment) -- run in production with systemd, Docker, or Docker Compose
129-
- [Config Reference](/docs/config) -- full configuration documentation
130-
- [Plugins](/docs/plugins) -- extend Airnode with custom hooks
128+
- [Publishing Endpoints](/docs/operators/publishing-endpoints) — what to share with consumers so they can call your
129+
airnode
130+
- [Deployment](/docs/operators/deployment) — run in production with systemd, Docker, or Docker Compose
131+
- [Config Reference](/docs/config) — full configuration documentation
132+
- [Plugins](/docs/plugins) — extend Airnode with custom hooks
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
slug: /operators/publishing-endpoints
3+
sidebar_position: 3
4+
---
5+
6+
# Publishing Endpoints
7+
8+
Once your airnode is running, consumers need three things to use it:
9+
10+
1. The airnode's **base URL** (where to send `POST /endpoints/{endpointId}`).
11+
2. The **endpoint ID** for each endpoint they want to call.
12+
3. The **airnode address**, so they can verify signatures recover correctly.
13+
14+
This page is the operator-side checklist for handing those out.
15+
16+
## What an airnode address looks like
17+
18+
The address is derived from your `AIRNODE_PRIVATE_KEY` (or `AIRNODE_MNEMONIC`). You can read it back at any time:
19+
20+
```bash
21+
airnode address
22+
```
23+
24+
```
25+
0xd1e98F3Ac20DA5e4da874723517c914a31b0e857
26+
```
27+
28+
You can also fetch it from a running server:
29+
30+
```bash
31+
curl http://localhost:3000/health
32+
```
33+
34+
```json
35+
{
36+
"status": "ok",
37+
"airnode": "0xd1e98F3Ac20DA5e4da874723517c914a31b0e857"
38+
}
39+
```
40+
41+
This address is what consumers pin as a trusted signer. Never let it drift — rotating the key means re-deriving every
42+
endpoint a consumer trusts.
43+
44+
## Listing endpoint IDs
45+
46+
On startup, the airnode logs every registered endpoint with its derived ID:
47+
48+
```
49+
Plugin loaded: heartbeat (budget: 5000ms)
50+
Loaded 3 endpoint(s):
51+
- CoinGecko/coinPrice 0x1c3e0fa5ac82e5514e0e9abac98e0b8e6c58b7bea12ae0393e4e4abe64ab9620
52+
- CoinGecko/coinPriceRaw 0x9e1354c4ede00d01d99b610bfe6873ede803e0d598aeff820b51cc9d81a3568d
53+
- WeatherAPI/currentTemp 0xa1b2...
54+
```
55+
56+
The endpoint ID is a deterministic hash of the endpoint specification — change the path, method, parameters, or
57+
encoding and the ID changes too. See [Endpoint IDs](/docs/concepts/endpoint-ids) for what the hash commits to and why
58+
that matters for consumers.
59+
60+
## Proving you're the API provider (optional)
61+
62+
For a consumer to know that `airnode.example.com` is actually run by the operator of `api.example.com`, publish a
63+
[DNS identity record](/docs/security/identity-verification) (`_airnode.api.example.com` TXT). Consumers can verify the
64+
DNS record matches the address `/health` returned before pinning the address as trusted.
65+
66+
## What to share with consumers
67+
68+
A complete handoff covers, per endpoint:
69+
70+
| Field | Example | Why the consumer needs it |
71+
| -------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------ |
72+
| Endpoint URL | `https://airnode.example.com/endpoints/{endpointId}` | Where to POST. |
73+
| Endpoint ID | `0x1c3e0fa5ac82e5514e0e9abac98e0b8e6c58b7bea12ae0393e4e4abe64ab9620` | Pinned in their off-chain client or consumer contract. |
74+
| Airnode address | `0xd1e98F3Ac20DA5e4da874723517c914a31b0e857` | Recovers from the signature. |
75+
| Parameter contract | List of required + optional request parameters | So they can build a valid request body. |
76+
| Encoding shape | e.g. `int256 × 1e18` for `$.ethereum.usd` | So they can `abi.decode` the result. |
77+
| Wildcard fields | Which encoding fields are `'*'`, if any | They must supply matching `_type`/`_path`/`_times` per call. |
78+
| Auth | `free`, `apiKey` (and key value out-of-band), or `x402` payment | So they can authenticate the request. |
79+
| Freshness expectation| e.g. "data is refreshed every 30s; reject >120s old" | Sets their on-chain or off-chain staleness threshold. |
80+
| Cache TTL | If `apis[].cache.maxAge` is set, what window | They should expect identical responses inside the window. |
81+
82+
The endpoint ID changes if you change the spec. Tell consumers up front whether the endpoint is considered
83+
stable — if you reserve the right to change `encoding.times` from `1e18` to `1e6`, say so. Any change invalidates the
84+
old ID and breaks consumers that hard-coded it.
85+
86+
## Operator-side checklist
87+
88+
Before announcing an endpoint:
89+
90+
- [ ] The endpoint loads (`airnode start` prints its ID without errors).
91+
- [ ] The upstream API call works end-to-end (`curl` the airnode and inspect the signed response).
92+
- [ ] The signature recovers to the address you intend to publish.
93+
- [ ] If `auth` is `apiKey`, the key delivery channel is in place.
94+
- [ ] If `auth` is `x402`, the payment recipient address and chain are correct.
95+
- [ ] If consumers will read on-chain, a deployed `AirnodeVerifier` exists on their target chain
96+
(the same one across chains is fine — there's no per-airnode registration).
97+
- [ ] If you're claiming ownership of the upstream API's domain, the
98+
[DNS identity record](/docs/security/identity-verification) is published.

book/docs/troubleshooting.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,41 @@ value wins. If the endpoint has no `encoding` block at all, you'll get raw JSON
127127
**Fix:** Reduce the request payload size. If you need to send large bodies, check whether the parameters can be
128128
simplified.
129129

130+
### `Too Many Requests` (429)
131+
132+
**Cause:** You've exceeded `server.rateLimit.max` requests per IP within `server.rateLimit.window`. Each upstream API
133+
call costs the operator (metered API quotas), so the airnode caps per-IP throughput.
134+
135+
**Fix:** Throttle your client. If you're behind a NAT or shared IP, ask the operator whether they trust an
136+
`X-Forwarded-For` header from your environment — if so, they can enable `rateLimit.trustForwardedFor` and the limit
137+
applies per real client.
138+
139+
### `Too many x402 verification attempts — slow down` (401)
140+
141+
**Cause:** You're an x402 client and you're submitting payment proofs faster than `server.rateLimit.x402.max` per
142+
`window`. This is a separate, stricter bucket from the global rate limit — each submitted proof triggers several
143+
chain-RPC reads, so unauthenticated flooders are throttled hard.
144+
145+
**Fix:** Slow down proof submission. Only submit proofs for transactions you actually intend the airnode to verify;
146+
don't speculatively spam unverified `txHash` values.
147+
148+
### `Payment required` (402)
149+
150+
**Cause:** The endpoint has `auth.type: 'x402'` and you haven't supplied a valid payment proof.
151+
152+
**Fix:** The 402 response body includes `airnode`, `endpointId`, `amount`, `token`, `network`, `recipient`, and
153+
`expiresAt`. Send the on-chain transfer, then retry with an `X-Payment-Proof` header containing
154+
`{ "txHash": "0x…", "expiresAt": <unix-seconds>, "signature": "0x…" }`. The signature is over
155+
`keccak256(encodePacked(airnode, endpointId, uint64(expiresAt)))` from the payer's EOA.
156+
157+
### `Server busy` (503)
158+
159+
**Cause:** The airnode is already running `settings.maxConcurrentApiCalls` upstream requests and your request waited
160+
its full timeout for a slot without getting one.
161+
162+
**Fix:** Operator-side: raise `maxConcurrentApiCalls` if the upstream can handle it, or front the airnode with a CDN
163+
that caches frequent endpoints. Client-side: slow your request rate or add jitter.
164+
130165
## Upstream API errors
131166

132167
### `API call failed` (502)
@@ -152,6 +187,75 @@ response.
152187
**Fix:** Inspect the actual upstream response and adjust the `path` in the endpoint's encoding config. Common causes:
153188
the API changed its response format, a nested field was renamed, or the path uses the wrong separator.
154189

190+
## FHE encryption errors
191+
192+
### `FHE encryption failed` (502)
193+
194+
**Cause:** The endpoint has an `encrypt` block, but the relayer rejected the encryption attempt. Common subcauses
195+
appear in the server log: a negative integer for an unsigned ciphertext (`euint*` types are unsigned), a value that
196+
overflows the chosen ciphertext type, or the relayer being unreachable.
197+
198+
**Fix:** Check the airnode logs for the specific error. Common fixes: choose a larger `encrypt.type` (e.g.
199+
`euint256` instead of `euint64`); pin a non-negative encoding (`uint256` instead of `int256`); or verify
200+
`settings.fhe.rpcUrl` and `settings.fhe.apiKey` are correct.
201+
202+
### `Endpoint requires FHE encryption but settings.fhe is not configured`
203+
204+
**Cause:** An endpoint has `encrypt: { ... }` but `settings.fhe` is `'none'`.
205+
206+
**Fix:** Either remove `encrypt` from the endpoint or set `settings.fhe` to a configured relayer block.
207+
Config-validation catches this at startup, so seeing it at runtime usually means the config was edited without
208+
restarting.
209+
210+
## Plugin errors
211+
212+
### `Plugin "X" budget exhausted` (request dropped, 403)
213+
214+
**Cause:** A plugin defining a mutation hook (`onHttpRequest`, `onBeforeApiCall`, `onAfterApiCall`, `onBeforeSign`) has
215+
spent its full `timeout` budget on previous hook invocations within the same request. Mutation hooks are fail-closed —
216+
once the budget is gone, the request is dropped rather than being processed without the plugin's intervention.
217+
218+
**Fix:** Operator-side: raise the plugin's `timeout` in `settings.plugins[].timeout`. Plugin-author side: pass each
219+
hook's `signal` to your `fetch` calls so cancellation actually propagates, and avoid unnecessary work in earlier hooks.
220+
221+
### Plugin runs only on the first request in a cache window
222+
223+
**Cause:** Not an error — by design. Cached responses bypass the upstream API call, which also bypasses the
224+
`onBeforeApiCall`, `onAfterApiCall`, and `onBeforeSign` hooks. Only `onHttpRequest`, `onResponseSent`, and `onError`
225+
fire on every request.
226+
227+
**Fix:** If you need per-request signal, use `onResponseSent` or `onHttpRequest`. See
228+
[Plugins → Caching interaction](/docs/config/plugins#caching-interaction).
229+
230+
## Async endpoint errors
231+
232+
### `Request not found` (404) on `GET /requests/{requestId}`
233+
234+
**Cause:** The request ID is wrong, or it's older than the async store's retention window (10 minutes for in-flight
235+
requests, 1 minute for completed/failed results). Finished results are evicted promptly so an unrelated request can
236+
take its slot.
237+
238+
**Fix:** Poll within the retention window. If you waited longer, the result is gone — re-submit the request.
239+
240+
### `Service Unavailable` (503) from a `mode: async` endpoint
241+
242+
**Cause:** The async store is at its 100-entry cap and no slot can be safely evicted (every entry is still in-flight
243+
within its TTL or holds an unread result within its retention window).
244+
245+
**Fix:** This indicates sustained submission rate exceeding the airnode's async capacity. Wait, retry, or have the
246+
operator review whether `mode: async` is appropriate for that workload.
247+
248+
## CORS errors
249+
250+
### Browser rejects response: `CORS policy: No 'Access-Control-Allow-Origin' header`
251+
252+
**Cause:** The airnode's `server.cors` is configured with an `origins` allow-list that doesn't include your origin.
253+
Non-matching origins receive `Access-Control-Allow-Origin: null`, which browsers refuse.
254+
255+
**Fix:** Operator-side: add your origin to `server.cors.origins`, or remove the `cors` block entirely (defaults to
256+
allowing every origin). Verify with `curl -i -H 'Origin: https://your-app.example' …` — the airnode echoes the matched
257+
origin back in the response header.
258+
155259
## General debugging
156260

157261
**Check the logs.** Airnode logs every request with its endpoint ID, response status, and processing time. Set

book/sidebars.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
22

3+
// Audience-first navigation: a reader landing on the docs picks "I'm an
4+
// operator" or "I'm a consumer" first, then drills into concepts /
5+
// reference / security as needed. Concepts come after the audience tracks
6+
// because they make more sense once you know what you're building.
37
const sidebars: SidebarsConfig = {
48
docs: [
59
'introduction',
6-
'v1-comparison',
10+
{
11+
type: 'category',
12+
label: 'Operators',
13+
items: ['operators/index', 'operators/deployment', 'operators/publishing-endpoints'],
14+
},
15+
{
16+
type: 'category',
17+
label: 'Consumers',
18+
items: ['consumers/getting-started', 'consumers/http-client', 'consumers/on-chain'],
19+
},
720
{
821
type: 'category',
922
label: 'Concepts',
@@ -18,24 +31,9 @@ const sidebars: SidebarsConfig = {
1831
},
1932
{
2033
type: 'category',
21-
label: 'Guides',
22-
items: ['guides/system-overview'],
23-
},
24-
{
25-
type: 'category',
26-
label: 'Config',
34+
label: 'Config Reference',
2735
items: ['config/index', 'config/server', 'config/settings', 'config/apis', 'config/plugins'],
2836
},
29-
{
30-
type: 'category',
31-
label: 'Airnode Operators',
32-
items: ['operators/index', 'operators/deployment'],
33-
},
34-
{
35-
type: 'category',
36-
label: 'Consumers',
37-
items: ['consumers/getting-started', 'consumers/http-client', 'consumers/on-chain'],
38-
},
3937
'plugins',
4038
{
4139
type: 'category',
@@ -49,6 +47,7 @@ const sidebars: SidebarsConfig = {
4947
},
5048
'troubleshooting',
5149
'cli',
50+
'v1-comparison',
5251
'roadmap',
5352
],
5453
};

0 commit comments

Comments
 (0)