blockyard 0.0.1 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +679 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +172 -4
- package/SECURITY.md +38 -0
- package/bin/blockyard.js +40 -0
- package/config/pool-map.json +2620 -0
- package/docs/API.md +1575 -0
- package/docs/ARCHITECTURE.md +1307 -0
- package/docs/AUTO-UPDATE.md +269 -0
- package/docs/CONFIGURATION.md +840 -0
- package/docs/DEFECTS.md +813 -0
- package/docs/EFFECTS-AGENTS.md +448 -0
- package/docs/GETTING-STARTED.md +202 -0
- package/docs/INSTALL.md +490 -0
- package/docs/MEASUREMENTS.md +1254 -0
- package/docs/PRIVATE-LEADERBOARD.md +230 -0
- package/docs/RULES.md +681 -0
- package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
- package/docs/SECURITY-AUDIT.md +258 -0
- package/docs/SECURITY.md +195 -0
- package/docs/STATE-2026-09-09.md +200 -0
- package/docs/TROUBLESHOOTING.md +298 -0
- package/docs/USER-GUIDE.md +1022 -0
- package/package.json +53 -5
- package/public/404.html +9 -0
- package/public/css/app.css +1785 -0
- package/public/index.html +893 -0
- package/public/js/about.js +112 -0
- package/public/js/agents.js +964 -0
- package/public/js/app.js +1312 -0
- package/public/js/arkanoid.js +806 -0
- package/public/js/blockanoid.js +347 -0
- package/public/js/blockout.js +347 -0
- package/public/js/blockpack.js +428 -0
- package/public/js/blockscene3d.js +2678 -0
- package/public/js/breakout.js +224 -0
- package/public/js/charts.js +635 -0
- package/public/js/depthchart.js +311 -0
- package/public/js/details3d.js +2957 -0
- package/public/js/explorer.js +405 -0
- package/public/js/feepalette.js +149 -0
- package/public/js/fmt.js +162 -0
- package/public/js/goggles.js +886 -0
- package/public/js/kiosk.js +41 -0
- package/public/js/login.js +83 -0
- package/public/js/markets.js +357 -0
- package/public/js/mining.js +1138 -0
- package/public/js/panels.js +966 -0
- package/public/js/pricechart.js +188 -0
- package/public/js/settings.js +1014 -0
- package/public/js/tetris.js +226 -0
- package/public/js/tetrust.js +356 -0
- package/public/js/tetsound.js +175 -0
- package/public/login.html +33 -0
- package/scripts/blockfile-measure.js +156 -0
- package/scripts/browser-check.mjs +286 -0
- package/scripts/check.js +173 -0
- package/scripts/decode-check.js +81 -0
- package/scripts/doc-counts.js +109 -0
- package/scripts/donate-qr.py +20 -0
- package/scripts/fake-node.js +534 -0
- package/scripts/index-bench.js +216 -0
- package/scripts/index-benchmark.js +117 -0
- package/scripts/index-build.js +40 -0
- package/scripts/live-render-check.mjs +89 -0
- package/scripts/manage-users.js +132 -0
- package/scripts/motion-check.mjs +138 -0
- package/scripts/pool-map.js +157 -0
- package/scripts/setup.js +410 -0
- package/scripts/shots.mjs +272 -0
- package/scripts/smoke.sh +327 -0
- package/scripts/ui.js +174 -0
- package/server/auth/sessions.js +221 -0
- package/server/auth/users.js +243 -0
- package/server/chain/blockfile.js +234 -0
- package/server/chain/index/build.js +193 -0
- package/server/chain/index/heights.js +36 -0
- package/server/chain/index/live.js +276 -0
- package/server/chain/index/rows.js +145 -0
- package/server/chain/index/store.js +154 -0
- package/server/chain/index/worker.js +109 -0
- package/server/chain/tx.js +310 -0
- package/server/collect/gbt.js +229 -0
- package/server/collect/logparse.js +765 -0
- package/server/collect/logtail.js +189 -0
- package/server/collect/markets.js +333 -0
- package/server/collect/mining.js +333 -0
- package/server/collect/monitor.js +2516 -0
- package/server/collect/nextblock.js +275 -0
- package/server/collect/sync.js +386 -0
- package/server/config.js +620 -0
- package/server/http/api.js +1275 -0
- package/server/http/explorer.js +418 -0
- package/server/http/server.js +412 -0
- package/server/http/sse.js +176 -0
- package/server/http/static.js +212 -0
- package/server/main.js +628 -0
- package/server/netinfo.js +253 -0
- package/server/rpc/allowlist.js +130 -0
- package/server/rpc/client.js +414 -0
- package/server/store/audit.js +148 -0
- package/server/store/history.js +220 -0
- package/server/store/ledger.js +290 -0
- package/server/store/ring.js +173 -0
- package/server/util/fmt.js +29 -0
- package/systemd/blockyard.service +100 -0
package/docs/API.md
ADDED
|
@@ -0,0 +1,1575 @@
|
|
|
1
|
+
# BlockYard HTTP API
|
|
2
|
+
|
|
3
|
+
BlockYard serves its browser UI and a JSON API from the same port. Everything the dashboard draws comes from the endpoints documented here, so any script can read the same data. This reference is derived from `server/http/api.js` (the route table), `server/http/server.js` (routing, auth, limits), `server/http/sse.js` (the event stream), `server/http/explorer.js`, `server/collect/markets.js`, `server/collect/monitor.js` and `server/rpc/allowlist.js`.
|
|
4
|
+
|
|
5
|
+
Contents
|
|
6
|
+
|
|
7
|
+
1. [Conventions](#1-conventions)
|
|
8
|
+
2. [Endpoint index](#2-endpoint-index)
|
|
9
|
+
3. [Health and build](#3-health-and-build)
|
|
10
|
+
4. [State, sync and nodes](#4-state-sync-and-nodes)
|
|
11
|
+
5. [Chart history: /api/series](#5-chart-history-apiseries)
|
|
12
|
+
6. [Mempool](#6-mempool)
|
|
13
|
+
7. [Blocks and transactions (drill-down)](#7-blocks-and-transactions-drill-down)
|
|
14
|
+
8. [Explorer: /api/x/*](#8-explorer-apix)
|
|
15
|
+
9. [Mining](#9-mining)
|
|
16
|
+
10. [Peers and network](#10-peers-and-network)
|
|
17
|
+
11. [Events](#11-events)
|
|
18
|
+
12. [Markets](#12-markets)
|
|
19
|
+
13. [RPC console and the allowlist](#13-rpc-console-and-the-allowlist)
|
|
20
|
+
14. [Node actions](#14-node-actions)
|
|
21
|
+
15. [Accounts, sessions, users and audit](#15-accounts-sessions-users-and-audit)
|
|
22
|
+
16. [Telemetry and configuration](#16-telemetry-and-configuration)
|
|
23
|
+
17. [The event stream: /api/stream](#17-the-event-stream-apistream)
|
|
24
|
+
18. [End-to-end examples](#18-end-to-end-examples)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Conventions
|
|
29
|
+
|
|
30
|
+
### Base URL
|
|
31
|
+
|
|
32
|
+
The server listens on port `21000` by default (`BLOCKYARD_PORT`), on the addresses in `server.host` / `BLOCKYARD_BIND`. The examples below use:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
http://127.0.0.1:21000
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
When TLS is configured (`BLOCKYARD_TLS_CERT` + `BLOCKYARD_TLS_KEY`), every listener speaks HTTPS and the scheme becomes `https`. `/api/build` and `/api/health` report which one is in effect (`scheme`, `tls`).
|
|
39
|
+
|
|
40
|
+
### JSON everywhere
|
|
41
|
+
|
|
42
|
+
- Every `/api/*` response is `application/json; charset=utf-8` with `Cache-Control: no-store`. The only exception is `/api/stream`, which is `text/event-stream`.
|
|
43
|
+
- Request bodies (POST) can be JSON or `application/x-www-form-urlencoded`, and are capped at 1 MB. A larger body is refused with `413`, and a body that is not valid JSON with `400` (`kind: "body"`).
|
|
44
|
+
- Timestamps named `at`, `t`, `ts`, `*At` are Unix milliseconds. Fields named `time`, `mediantime`, `blocktime` are node-supplied Unix seconds.
|
|
45
|
+
- A figure the node does not report is `null`, never `0`. Several endpoints also carry `notReported` / `unavailable` / `note` strings saying why.
|
|
46
|
+
|
|
47
|
+
### Multi-node: the `node` parameter
|
|
48
|
+
|
|
49
|
+
A monitor can watch several nodes. Every per-node endpoint accepts `?node=<id>` (`?nodeId=` is an alias, except on `/api/stream`). When it is omitted, the first configured node (the "primary") answers. `/api/nodes` lists the configured ids. The built-in default id is `main`, and the examples use it.
|
|
50
|
+
|
|
51
|
+
An unknown id is a `404`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{ "error": { "message": "no node \"nope\"; known: main", "kind": "api", "code": null } }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
These endpoints are **not** per-node: `/api/health`, `/api/build`, `/api/nodes`, `/api/telemetry`, `/api/config`, `/api/config/node*` (they always address `nodes[0]`), `/api/settings`, `/api/events` (rows carry a `node` field but there is no node filter), `/api/markets*`, and the account/admin routes.
|
|
58
|
+
|
|
59
|
+
### Errors and status codes
|
|
60
|
+
|
|
61
|
+
Transport-level failures use one envelope:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{ "error": { "message": "human-readable sentence", "kind": "api", "code": "rpc_denied" } }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`code` is present only on errors raised by a handler (`kind: "api"`). Some errors add sibling fields: `login` (on 401) or `accounts` (on admin refusals in open mode).
|
|
68
|
+
|
|
69
|
+
| `kind` | Status | Meaning |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `api` | 400 / 401 / 403 / 404 / 429 / 503 | A handler refused the request. See `code`. |
|
|
72
|
+
| `auth` | 401 | Accounts are on and there is no valid session. The body includes `"login": "/login"`. |
|
|
73
|
+
| `forbidden` | 403 | Address outside the CIDR gate, disabled account, or an admin route without the admin role. |
|
|
74
|
+
| `csrf` | 403 | `X-CSRF-Token` missing or wrong (accounts on only). |
|
|
75
|
+
| `ratelimited` | 429 | The token bucket is empty. The message says how many seconds to wait (no `Retry-After` header is sent). |
|
|
76
|
+
| `not_found` | 404 | No such `/api/...` path. |
|
|
77
|
+
| `method` | 405 | The path exists with another method. The `Allow` header lists the valid ones. |
|
|
78
|
+
| `body` | 400 / 413 | The body is unreadable, not JSON, or larger than 1 MB. |
|
|
79
|
+
| `unknown_node` | 404 | `/api/stream?node=` named a node that does not exist. |
|
|
80
|
+
| `internal` | 500 | Unexpected server error. Details go to the server log only. |
|
|
81
|
+
|
|
82
|
+
Handler `code` values: `accounts_disabled`, `throttled`, `locked`, `bad_credentials`, `rpc_denied`, `action_denied`, `confirm_required`, or `null`.
|
|
83
|
+
|
|
84
|
+
**Node errors are not HTTP errors.** When the node itself refuses or fails, the answer is `200` with `ok: false` and the node's own message. This applies to `/api/rpc`, `/api/action`, `/api/block`, `/api/tx` and all of `/api/x/*`. Always check `ok`, not just the status.
|
|
85
|
+
|
|
86
|
+
Other statuses: `204` for `OPTIONS` on any path (`Allow: GET,POST,DELETE,HEAD`). `HEAD` works for static files only. On `/api/*` a `HEAD` gets `405`.
|
|
87
|
+
|
|
88
|
+
### Access modes and auth levels
|
|
89
|
+
|
|
90
|
+
Each route in the table has one of three auth levels:
|
|
91
|
+
|
|
92
|
+
| Level | Accounts off (default) | Accounts on (`BLOCKYARD_AUTH=1`) |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `none` | Open. | Open. No session needed. |
|
|
95
|
+
| `any` | Served as the built-in anonymous user (`role: "viewer"`). | Any valid, non-disabled session. Without one: `401`. |
|
|
96
|
+
| `admin` | Always `403` with `"accounts": false`: nothing in open mode has the admin role. | Session with role `admin`, else `403`. |
|
|
97
|
+
|
|
98
|
+
The roles are `viewer` < `operator` < `admin`. No route in the table requires `viewer` or `operator` by itself. Those two roles matter in two places only: each node action names a minimum role (section 14), and `POST /api/password` needs `admin` to change *another* user's password.
|
|
99
|
+
|
|
100
|
+
**Open mode (the default, `auth.enabled=false`).** No sign-in. Anyone who can reach the port can read everything (state, charts, events, peers, mempool, explorer, markets, the read-only RPC console) as `viewer`. The ceiling is fixed and nothing can raise it. As a result:
|
|
101
|
+
|
|
102
|
+
- user administration, `/api/audit` and password changes are closed;
|
|
103
|
+
- node writes (`/api/action`) are refused unless `BLOCKYARD_ALLOW_WRITES_WITHOUT_AUTH=1` was set deliberately;
|
|
104
|
+
- `/api/login` answers `403 accounts_disabled`;
|
|
105
|
+
- `GET /login` redirects (`302`) to `/`.
|
|
106
|
+
|
|
107
|
+
**Accounts mode (`BLOCKYARD_AUTH=1`).** Sign-in, roles, sessions, CSRF protection and a per-user audit trail. On first start with no users, an `admin` account is created. Its password comes from `BLOCKYARD_ADMIN_PASSWORD`, or is generated and printed once to the server log.
|
|
108
|
+
|
|
109
|
+
### Session cookie
|
|
110
|
+
|
|
111
|
+
`POST /api/login` sets two cookies:
|
|
112
|
+
|
|
113
|
+
| Cookie | Attributes | Purpose |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `blockyard_sid` (configurable as `auth.cookieName`) | `HttpOnly; SameSite=Strict; Path=/; Max-Age=259200` (+ `Secure` over TLS or with `BLOCKYARD_SECURE_COOKIE=1`) | The session token. Only its SHA-256 is stored server-side. |
|
|
116
|
+
| `blockyard_csrf` | Same, but **not** `HttpOnly` | The CSRF double-submit value, readable by page JavaScript. |
|
|
117
|
+
|
|
118
|
+
Sessions expire 72 hours after creation (`auth.sessionTtlMs`), or after 8 hours without a request (`auth.idleTtlMs`). A role change or a disable takes effect on the next request, not at next login.
|
|
119
|
+
|
|
120
|
+
### CSRF: the `X-CSRF-Token` header
|
|
121
|
+
|
|
122
|
+
With accounts on, every mutating route (`POST /api/logout`, `/api/logout-all`, `/api/rpc`, `/api/action`, `/api/password`, `/api/config/node/test`, `/api/config/node`, `/api/settings`, and all `POST /api/users*`) needs the session's CSRF token. Send it in the `X-CSRF-Token` header, or as a `csrf` field in the body. The cookie alone is **not** accepted, because a cross-site request would carry it too. The value is returned by `/api/login` as `csrf` and is also in the `blockyard_csrf` cookie.
|
|
123
|
+
|
|
124
|
+
`/api/login` itself is exempt. In open mode there is no session, so no CSRF check runs.
|
|
125
|
+
|
|
126
|
+
### Rate limits
|
|
127
|
+
|
|
128
|
+
| Bucket | Key | Capacity / refill | Applies to |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| General | `anon:<ip>` in open mode, the user id with accounts | 120 requests, refilled at 40/s | Every `auth: any` / `admin` route. |
|
|
131
|
+
| Stream | `sse:anon:<ip>` or `sse:<userId>` | 120 / 40 per s | Opening `/api/stream`. |
|
|
132
|
+
| Login | `login:<ip>` | 10 attempts, refilled at 1 per 2 s | `POST /api/login` (answers `429 throttled`). |
|
|
133
|
+
| Login lockout | per username **and** per IP | 8 failures in 5 minutes locks for 10 minutes | `POST /api/login` (answers `429 locked`). |
|
|
134
|
+
|
|
135
|
+
`auth: none` routes (`/api/health`, `/api/build`) and static files are not rate limited.
|
|
136
|
+
|
|
137
|
+
### The CIDR gate
|
|
138
|
+
|
|
139
|
+
When `server.allowCidrs` / `BLOCKYARD_ALLOW_CIDRS` is set (comma-separated IPv4/IPv6 CIDRs, for example `192.0.2.0/24,127.0.0.1/32`), every request is checked first: API, static files and the stream, before any KDF or session work. An address outside the list gets:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{ "error": { "message": "this address is not permitted", "kind": "forbidden" } }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The reason is logged server-side only. The client IP is the socket peer, unless `server.trustProxy` / `BLOCKYARD_TRUST_PROXY=1` is set, in which case the first `X-Forwarded-For` entry is used. Enable that only behind a proxy you control.
|
|
146
|
+
|
|
147
|
+
### Security headers
|
|
148
|
+
|
|
149
|
+
Every JSON, static and error response carries:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
X-Content-Type-Options: nosniff
|
|
153
|
+
X-Frame-Options: DENY
|
|
154
|
+
Referrer-Policy: no-referrer
|
|
155
|
+
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()
|
|
156
|
+
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:;
|
|
157
|
+
font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
HTML pages get a per-response script nonce (`script-src 'self' 'nonce-...'`). `Strict-Transport-Security` is added only when TLS is on and `server.tls.hstsMs > 0`. Static files are `Cache-Control: no-cache` with a weak ETag. Asset URLs in pages carry `?v=<build>`.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 2. Endpoint index
|
|
165
|
+
|
|
166
|
+
| Method | Path | Auth | Section |
|
|
167
|
+
|---|---|---|---|
|
|
168
|
+
| GET | `/api/health` | none | [3](#3-health-and-build) |
|
|
169
|
+
| GET | `/api/about` | any | [3](#3-health-and-build) |
|
|
170
|
+
| GET | `/api/build` | none | [3](#3-health-and-build) |
|
|
171
|
+
| GET | `/api/state` | any | [4](#4-state-sync-and-nodes) |
|
|
172
|
+
| GET | `/api/sync` | any | [4](#4-state-sync-and-nodes) |
|
|
173
|
+
| GET | `/api/nodes` | any | [4](#4-state-sync-and-nodes) |
|
|
174
|
+
| GET | `/api/series` | any | [5](#5-chart-history-apiseries) |
|
|
175
|
+
| GET | `/api/mempool` | any | [6](#6-mempool) |
|
|
176
|
+
| GET | `/api/mempool/dense` | any | [6](#6-mempool) |
|
|
177
|
+
| GET | `/api/blocks` | any | [7](#7-blocks-and-transactions-drill-down) |
|
|
178
|
+
| GET | `/api/block` | any | [7](#7-blocks-and-transactions-drill-down) |
|
|
179
|
+
| GET | `/api/tx` | any | [7](#7-blocks-and-transactions-drill-down) |
|
|
180
|
+
| GET | `/api/x/search` | any | [8](#8-explorer-apix) |
|
|
181
|
+
| GET | `/api/x/tx` | any | [8](#8-explorer-apix) |
|
|
182
|
+
| GET | `/api/x/block` | any | [8](#8-explorer-apix) |
|
|
183
|
+
| GET | `/api/x/address` | any | [8](#8-explorer-apix) |
|
|
184
|
+
| GET | `/api/mining` | any | [9](#9-mining) |
|
|
185
|
+
| GET | `/api/nextblock` | any | [9](#9-mining) |
|
|
186
|
+
| GET | `/api/peers` | any | [10](#10-peers-and-network) |
|
|
187
|
+
| GET | `/api/net` | any | [10](#10-peers-and-network) |
|
|
188
|
+
| GET | `/api/events` | any | [11](#11-events) |
|
|
189
|
+
| GET | `/api/markets` | any | [12](#12-markets) |
|
|
190
|
+
| GET | `/api/markets/depth` | any | [12](#12-markets) |
|
|
191
|
+
| POST | `/api/rpc` | any + CSRF | [13](#13-rpc-console-and-the-allowlist) |
|
|
192
|
+
| GET | `/api/actions` | any | [14](#14-node-actions) |
|
|
193
|
+
| POST | `/api/action` | any + CSRF (+ action role) | [14](#14-node-actions) |
|
|
194
|
+
| POST | `/api/login` | none | [15](#15-accounts-sessions-users-and-audit) |
|
|
195
|
+
| POST | `/api/logout` | any + CSRF | [15](#15-accounts-sessions-users-and-audit) |
|
|
196
|
+
| POST | `/api/logout-all` | any + CSRF | [15](#15-accounts-sessions-users-and-audit) |
|
|
197
|
+
| GET | `/api/me` | any | [15](#15-accounts-sessions-users-and-audit) |
|
|
198
|
+
| POST | `/api/password` | any + CSRF | [15](#15-accounts-sessions-users-and-audit) |
|
|
199
|
+
| GET | `/api/users` | admin | [15](#15-accounts-sessions-users-and-audit) |
|
|
200
|
+
| POST | `/api/users` | admin + CSRF | [15](#15-accounts-sessions-users-and-audit) |
|
|
201
|
+
| POST | `/api/users/generate` | admin + CSRF | [15](#15-accounts-sessions-users-and-audit) |
|
|
202
|
+
| POST | `/api/users/:username/role` | admin + CSRF | [15](#15-accounts-sessions-users-and-audit) |
|
|
203
|
+
| POST | `/api/users/:username/disabled` | admin + CSRF | [15](#15-accounts-sessions-users-and-audit) |
|
|
204
|
+
| GET | `/api/audit` | admin | [15](#15-accounts-sessions-users-and-audit) |
|
|
205
|
+
| GET | `/api/telemetry` | any | [16](#16-telemetry-and-configuration) |
|
|
206
|
+
| GET | `/api/config` | any | [16](#16-telemetry-and-configuration) |
|
|
207
|
+
| POST | `/api/config/node/test` | any + CSRF (admin with accounts on) | [16](#16-telemetry-and-configuration) |
|
|
208
|
+
| POST | `/api/config/node` | any + CSRF (admin with accounts on) | [16](#16-telemetry-and-configuration) |
|
|
209
|
+
| GET | `/api/settings` | any | [16](#16-telemetry-and-configuration) |
|
|
210
|
+
| POST | `/api/settings` | any + CSRF (admin with accounts on) | [16](#16-telemetry-and-configuration) |
|
|
211
|
+
| GET | `/api/stream` | any (checked in `server.js`) | [17](#17-the-event-stream-apistream) |
|
|
212
|
+
|
|
213
|
+
Any other path that is not an `/api/*` path is served from `public/` as a static file (`/` is `index.html`, `/login` is `login.html`). A missing page gets `404.html` with status 404.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 3. Health and build
|
|
218
|
+
|
|
219
|
+
### `GET /api/health`
|
|
220
|
+
|
|
221
|
+
Auth `none`, not rate limited. Built for uptime probes. `ok` is `true` when at least one node is configured and every **non-optional** node's RPC is online. An optional node that is down shows up in `degraded` but does not turn `ok` false.
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"ok": true,
|
|
226
|
+
"degraded": [],
|
|
227
|
+
"version": "0.0.9",
|
|
228
|
+
"build": "0.0.9-fd620adc52",
|
|
229
|
+
"scheme": "http",
|
|
230
|
+
"tls": false,
|
|
231
|
+
"uptimeSec": 1519,
|
|
232
|
+
"authRequired": false,
|
|
233
|
+
"nodes": [
|
|
234
|
+
{ "id": "main", "label": "Bitcoin Core", "online": true, "optional": false,
|
|
235
|
+
"chain": "main", "tip": 966546, "lastError": null }
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `GET /api/about`
|
|
241
|
+
|
|
242
|
+
What the About page shows: the monitor's version and live build, and the **shape** of the machine it runs on.
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{ "version": "0.0.9", "build": "0.0.9-a6ecedff3c", "platform": "linux", "release": "7.0.0-31-generic",
|
|
246
|
+
"arch": "x64", "cpus": 32, "cpuModel": "AMD Ryzen 9 9950X3D 16-Core Processor",
|
|
247
|
+
"totalMemGb": 132.3, "node": "v22.23.2", "uptimeSec": 2355 }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`auth: any` rather than `none`, unlike `/api/health` and `/api/build`: a version string answers "is my tab running current code", which a login page legitimately needs, whereas the host's processor and memory should not be readable before sign-in when accounts are on.
|
|
251
|
+
|
|
252
|
+
It reports **no hostname, no username, no network addresses and no environment**. This monitor is open-access by default, so everything here is readable by anyone who can reach the port; the operating system and processor describe a machine's shape, not its owner. A test pins those absences.
|
|
253
|
+
|
|
254
|
+
### `GET /api/build`
|
|
255
|
+
|
|
256
|
+
Auth `none`. Answers "is the code in my tab the code on disk?" The build id is `<version>-<10 hex>`, a digest of the sizes and mtimes of the files under `public/`.
|
|
257
|
+
|
|
258
|
+
| Query | Type | Meaning |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| `build` | string | Optional. The build id the page was served with. |
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"version": "0.0.9",
|
|
265
|
+
"build": "0.0.9-5223f98d84",
|
|
266
|
+
"bootBuild": "0.0.9-fd620adc52",
|
|
267
|
+
"matchesClient": null,
|
|
268
|
+
"scheme": "http",
|
|
269
|
+
"tls": false,
|
|
270
|
+
"uptimeSec": 1571
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`build` is computed live, `bootBuild` at process start. `matchesClient` is `true`/`false` when `?build=` was sent, otherwise `null`.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 4. State, sync and nodes
|
|
279
|
+
|
|
280
|
+
### `GET /api/state`
|
|
281
|
+
|
|
282
|
+
The whole read model for one node: the same object the stream pushes as `snapshot` (section 17), plus `app`, `user` and `seq`.
|
|
283
|
+
|
|
284
|
+
| Query | Type | Default | Meaning |
|
|
285
|
+
|---|---|---|---|
|
|
286
|
+
| `node` | string | primary | Node id. |
|
|
287
|
+
| `series` | `none` | — | `series=none` returns `"series": {}`: the cheap poll. |
|
|
288
|
+
| `range` | duration | `1h` | Range of the "hour" chart windows. |
|
|
289
|
+
| `range6` | duration | `6h` | Range of the "hours6" chart windows. |
|
|
290
|
+
|
|
291
|
+
A duration is a number with an optional unit `s` (default), `m`, `h` or `d`, for example `15m`, `24h`, `7d`. It is clamped to 1 s through 31 d. Anything unparseable falls back to the default.
|
|
292
|
+
|
|
293
|
+
Top-level keys:
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
id label color online chain ibd tip sync progress warnings difficulty hashrateEstEh hashrateNote
|
|
297
|
+
avgBlockGapSec sizeOnDisk pruned chainwork uptimeSec network mempool peers net attribution blocks
|
|
298
|
+
fees mining utxo chaintxstats indexes tips deployments rpcInfo log health series app user seq
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Trimmed example:
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"id": "main",
|
|
306
|
+
"label": "Bitcoin Core",
|
|
307
|
+
"color": "#f7931a",
|
|
308
|
+
"online": true,
|
|
309
|
+
"chain": "main",
|
|
310
|
+
"ibd": false,
|
|
311
|
+
"tip": { "height": 966546, "headers": 966546,
|
|
312
|
+
"hash": "0000000000000000000019f6fb9d8e1e43e9d72916b2ed8aef080c778685a4aa",
|
|
313
|
+
"time": 1789154285, "mediantime": 1789151883, "ageSec": 1403, "behindHeaders": 0 },
|
|
314
|
+
"sync": { "state": "synced", "height": 966546, "headers": 966546, "behind": 0, "pct": 100,
|
|
315
|
+
"verificationProgress": 100, "etaSec": null, "eta": null, "rateTrend": "stalled",
|
|
316
|
+
"node": "main", "nodeLabel": "Bitcoin Core", "endpoint": "http://127.0.0.1:8332",
|
|
317
|
+
"strip": [ { "label": "chain", "...": "..." } ] },
|
|
318
|
+
"difficulty": 127450789715843.1,
|
|
319
|
+
"hashrateEstEh": 200.1,
|
|
320
|
+
"hashrateNote": null,
|
|
321
|
+
"mempool": { "count": 21398, "bytes": 8123456, "usage": 41234567, "maxUsage": 300000000,
|
|
322
|
+
"usagePct": 13.7, "totalFee": 0.123, "minFee": 0.00001, "minRelayFee": 0.00001,
|
|
323
|
+
"rejects": null, "dist": { "count": 21398, "p50Feerate": 1.2, "scatterPoints": 1500, "...": "..." } },
|
|
324
|
+
"peers": { "connections": 11, "in": 0, "out": 11, "identitySource": "getpeerinfo", "...": "..." },
|
|
325
|
+
"net": { "inBps": null, "downloadMeasured": false, "outBps": null, "uploadMeasured": false, "...": "..." },
|
|
326
|
+
"attribution": { "recent": [], "pools": [], "...": "same shape as /api/mining" },
|
|
327
|
+
"blocks": { "count": 31, "recent": [ { "height": 966546, "totalfee": 1929031, "...": "..." } ],
|
|
328
|
+
"backfilled": true, "reorgs": 0 },
|
|
329
|
+
"fees": { "f1": 3.1, "f2": 2.0, "f6": 1.1, "f24": 1.0, "f144": 1.0 },
|
|
330
|
+
"log": { "exists": false, "source": "disabled", "...": "..." },
|
|
331
|
+
"health": { "rpc": { "online": true, "...": "..." }, "cadence": { "fast": { "configuredMs": 4000,
|
|
332
|
+
"effectiveMs": 4000, "stretched": false, "lastRunMs": 120 } }, "quality": [] },
|
|
333
|
+
"series": { "mempool": { "hour": [ { "t": 1789154820000, "v": 18780 } ], "...": "..." } },
|
|
334
|
+
"app": { "version": "0.0.9", "build": "0.0.9-fd620adc52", "scheme": "http", "uptimeSec": 1536,
|
|
335
|
+
"sseClients": 5, "self": { "rssMb": 175.6, "heapMb": 42.8, "cpuPct": 2.71 }, "serverTime": 1789155688322 },
|
|
336
|
+
"user": { "username": "anonymous", "role": "viewer", "id": "anonymous", "disabled": false, "lastLoginAt": null },
|
|
337
|
+
"seq": 406
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Notes:
|
|
342
|
+
|
|
343
|
+
- `sync` always carries the node's own identity (`node`, `nodeLabel`, `endpoint`), and `strip` is the ordered list of facts the sync bar draws. `sync.state` is one of `unknown`, `ibd`, `catching_up`, `synced`, `stalled` or `reorg` (see `server/collect/sync.js`). `stalled` is claimed only when the connected peers report a tip above the node's (`computeSync` takes `peerBestHeight`, the highest `synced_headers` — `startingheight` as a fallback — over `getpeerinfo`); a long gap with the peers agreeing is `synced` with a caveat, and with no peer heights at all the state becomes `stalled` after 7200 s without a block.
|
|
344
|
+
- `utxo` is filled only from a node whose `getindexinfo` reports a synced `coinstatsindex`; the slow tier asks `getindexinfo` first, on its own, and sends `gettxoutsetinfo` only to such a node. Otherwise `utxo` is `null` and `health.quality` carries `utxo-unindexed`.
|
|
345
|
+
- `health.quality` is the list of named gaps the Overview shows. While the server is building the address index it holds `address-index-building` (phase, done of total, rows, time left, `paused while the node's RPC is slow`); a failed build leaves `address-index-build-failed`. `rpc-slow` and `rpc-timeouts` describe the measurement and name the index build when one is running rather than asserting a cause.
|
|
346
|
+
- `hashrateEstEh` is the network hash rate in EH/s, estimated as difficulty x 2^32 divided by the observed mean block gap -- a difficulty-1 target expects 2^32 hashes, so leaving that factor out understates the rate by 4.29 billion (it did until 2026-09-12, and the figure reached the page as `0.0 EH/s`). The estimate agrees with the node's own `getnetworkhashps` to about one per cent. During IBD, or more than 6 blocks behind headers, it is `null` and `hashrateNote` says why.
|
|
347
|
+
- `mempool.dist` omits the scatter points (`scatterPoints` is their count). Fetch them from `/api/mempool`.
|
|
348
|
+
- `blocks.recent` holds the newest 40 blocks, newest first.
|
|
349
|
+
- `series` has the chart windows listed under [/api/series](#snapshot-series-windows).
|
|
350
|
+
|
|
351
|
+
### `GET /api/sync`
|
|
352
|
+
|
|
353
|
+
Only the sync bar's data: small enough to poll hard from a status widget.
|
|
354
|
+
|
|
355
|
+
| Query | Meaning |
|
|
356
|
+
|---|---|
|
|
357
|
+
| `node` | Node id. |
|
|
358
|
+
|
|
359
|
+
```json
|
|
360
|
+
{
|
|
361
|
+
"node": "main",
|
|
362
|
+
"sync": { "state": "synced", "chain": "main", "height": 966546, "headers": 966546, "behind": 0,
|
|
363
|
+
"pct": 100, "verificationProgress": 100, "headersMayLag": false, "tipAgeSec": 1408,
|
|
364
|
+
"etaSec": null, "etaBest": null, "etaWorst": null, "blocksPerMin": null,
|
|
365
|
+
"avgBlockGapSec": 636.7, "sizeOnDisk": 767996069971, "peers": 11, "reorgs": 0,
|
|
366
|
+
"warnings": [], "reason": null, "caveats": [], "node": "main", "strip": [] },
|
|
367
|
+
"tip": { "height": 966546, "headers": 966546, "hash": "0000...a4aa", "ageSec": 1408, "behindHeaders": 0 },
|
|
368
|
+
"chain": "main",
|
|
369
|
+
"ibd": false,
|
|
370
|
+
"health": { "rpc": { "online": true, "lastError": null } }
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### `GET /api/nodes`
|
|
375
|
+
|
|
376
|
+
Every configured node with its sync state.
|
|
377
|
+
|
|
378
|
+
```json
|
|
379
|
+
{
|
|
380
|
+
"nodes": [
|
|
381
|
+
{ "id": "main", "label": "Bitcoin Core", "rpcUrl": "http://127.0.0.1:8332", "chain": "main",
|
|
382
|
+
"online": true, "optional": false, "syncState": "synced", "pct": 100,
|
|
383
|
+
"height": 966546, "headers": 966546, "syncing": false }
|
|
384
|
+
],
|
|
385
|
+
"primary": "main",
|
|
386
|
+
"attention": []
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`attention` lists node ids whose sync state is not `synced` (including `unknown`). `primary` is the node that answers when `?node=` is omitted.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 5. Chart history: `/api/series`
|
|
395
|
+
|
|
396
|
+
### `GET /api/series`
|
|
397
|
+
|
|
398
|
+
Bucketed time series from the in-memory history rings. The rings are persisted to disk and kept for `store.retentionHours`.
|
|
399
|
+
|
|
400
|
+
| Query | Type | Default | Meaning |
|
|
401
|
+
|---|---|---|---|
|
|
402
|
+
| `node` | string | primary | Node id. |
|
|
403
|
+
| `name` | csv | `mempool` | One or more series names (table below). |
|
|
404
|
+
| `field` | csv | all fields of each series | Fields to return. Every field must exist in every named series. |
|
|
405
|
+
| `range` (alias `since`) | duration | `1h` | How far back. Clamped to 1 s through 31 d. |
|
|
406
|
+
| `points` | int | `240` | Target number of buckets, 20–2000. `bucketMs = round(range / points)` to whole seconds, at least 1 s. |
|
|
407
|
+
| `agg` | enum | `last` | Per-bucket aggregate: `last`, `first`, `avg`, `min`, `max`, `sum`, `delta` (the sum of positive steps). |
|
|
408
|
+
|
|
409
|
+
An unknown name or field is a `400` that lists the valid ones.
|
|
410
|
+
|
|
411
|
+
```json
|
|
412
|
+
{
|
|
413
|
+
"node": "main",
|
|
414
|
+
"rangeMs": 900000,
|
|
415
|
+
"bucketMs": 45000,
|
|
416
|
+
"series": {
|
|
417
|
+
"mempool": {
|
|
418
|
+
"count": [ { "t": 1789154820000, "v": 18780 }, { "t": 1789154865000, "v": 18927 } ]
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Each point is `{ t, v }`. `t` is the **end** of the bucket, and empty buckets are left out.
|
|
425
|
+
|
|
426
|
+
| Series | Fields |
|
|
427
|
+
|---|---|
|
|
428
|
+
| `node` | `blocks headers progress difficulty sizeOnDisk ibd connections peersIn peersOut uptimeMs txRate chainTxCount txouts totalAmount muhash` |
|
|
429
|
+
| `mempool` | `count bytes usage maxUsage totalFee minFee minRelayFee unbroadcast ingestRate acceptedDelta rejectMissing rejectPolicy rejectInvalid confirmedDrain pendingAncestors replaceable avgFee avgVsize` |
|
|
430
|
+
| `net` | `inBps outBps diskWriteBps inTotal outTotal diskTotal avgRecvBps avgWriteBps floorBps poolMedianBps` |
|
|
431
|
+
| `fees` | `f1 f2 f6 f24 f144 mempoolmin priority estimatorOk` |
|
|
432
|
+
| `peers` | `connections in out relayPeers servedBlocks txRelayPeers wanted banned rankingLive rankingAnswered rankingMedianKbps` |
|
|
433
|
+
| `blocks` | `height time mediantime totalfee txs size weight medianTxSize avgTxSize swtotalSize swtxs avgFeerate subsidy utxoIncrease ins outs avgfee medianfee maxfee p0 p1 p2 p3 p4 gapSec viaPeer source` |
|
|
434
|
+
| `txflow` | `accepted relayAccepted rejectMissing rejectPolicy rejectInvalid alreadyConfirmed orphansHeld orphansParked orphansResolved orphansDropped inFlight oneP1C oneP1CFailed windowSec` |
|
|
435
|
+
| `rpc` | `latencyMs avgLatencyMs ratePerSec queued errors breakerTrips busyMsPerSec` |
|
|
436
|
+
| `self` | `rssMb heapMb sseClients usersActive cpuPct eventRate` |
|
|
437
|
+
|
|
438
|
+
Many fields only fill from a node log source. **That source does not currently support Bitcoin Core** (the parsers target an experimental node's log grammar), so against Core these stay empty -- empty, never zero. RPC-only is the default and supported mode.
|
|
439
|
+
|
|
440
|
+
#### Snapshot series windows
|
|
441
|
+
|
|
442
|
+
The `series` object inside `/api/state`, the `snapshot` event and the `series` event uses fixed windows, keyed `series.<name>.<window>`:
|
|
443
|
+
|
|
444
|
+
| Series | Windows (field, range) |
|
|
445
|
+
|---|---|
|
|
446
|
+
| `mempool` | `hour`/`hours6`/`day` (count over `range` / `range6` / 24 h), `bytesHour`, `usageHour`, `feeHour`, `ingestHour` |
|
|
447
|
+
| `net` | `inHour`, `outHour`, `diskHour`, `inDay` |
|
|
448
|
+
| `fees` | `f1`, `f2`, `f6`, `f24`, `f144`, `min` (24 h) |
|
|
449
|
+
| `blocks` | `fee`, `size`, `txs`, `gap`, `p1`, `p2`, `p3` (24 h, 300 points) |
|
|
450
|
+
| `peers` | `connections`, `in`, `out`, `relay` (24 h) |
|
|
451
|
+
| `node` | `tip`, `difficulty` (7 d avg), `txRate`, `txouts` (7 d avg), `disk` |
|
|
452
|
+
| `txflow` | `accepted`, `rejectPolicy` (1 h avg), `orphansHeld`, `orphansParked`, `inFlight` |
|
|
453
|
+
| `rpc` | `latency`, `rate` (1 h) |
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## 6. Mempool
|
|
458
|
+
|
|
459
|
+
### `GET /api/mempool`
|
|
460
|
+
|
|
461
|
+
The mempool panel's data, including the scatter points and treemap cells that the snapshot leaves out. The data comes from `getrawmempool` (verbose) on the 20 s pool tier: it is a poll, not a stream, and `feed` says so.
|
|
462
|
+
|
|
463
|
+
```json
|
|
464
|
+
{
|
|
465
|
+
"node": "main",
|
|
466
|
+
"feed": { "kind": "poll", "cadenceSec": 60, "streamAvailable": false,
|
|
467
|
+
"why": "the node refuses zmqpubsequence: ...",
|
|
468
|
+
"source": "getrawmempool verbose on the 20 s pool tier + [tx_accept]/[txrelay] log lines" },
|
|
469
|
+
"info": { "loaded": true, "count": 21398, "bytes": 8123456, "usage": 41234567, "maxUsage": 300000000,
|
|
470
|
+
"usagePct": 13.7, "totalFee": 0.123, "minFee": 0.00001, "minRelayFee": 0.00001,
|
|
471
|
+
"incrementalRelayFee": 0.00001, "unbroadcast": 0, "maxDataCarrier": 83,
|
|
472
|
+
"permitBareMultisig": true, "ingestRate": null, "acceptWindow": null, "rejects": null,
|
|
473
|
+
"lastDrain": null, "dist": { "...": "as in /api/state" } },
|
|
474
|
+
"dist": {
|
|
475
|
+
"count": 21398, "totalVsize": 5200000, "totalFeeSat": 12345678,
|
|
476
|
+
"avgFeerate": 2.4, "p50Feerate": 1.2, "p90Feerate": 4.1, "maxFeerate": 350.5,
|
|
477
|
+
"avgVsize": 243, "oldestSec": 86000, "ageUnknown": 0,
|
|
478
|
+
"hist": [ "40 log-spaced feerate buckets, 0.5-2000 sat/vB" ],
|
|
479
|
+
"ageHist": [ "30 linear age buckets" ],
|
|
480
|
+
"scatter": [ [ 120, 1.25, 141 ] ],
|
|
481
|
+
"cells": [ { "vbytes": 141, "rate": 120.0, "txid": "9891f72b..." } ],
|
|
482
|
+
"cellCount": 21398,
|
|
483
|
+
"pendingAncestors": null, "replaceable": null,
|
|
484
|
+
"projected": {
|
|
485
|
+
"blockVsize": 1000000,
|
|
486
|
+
"skipped": { "n": 4107, "vsize": 999930, "feeSat": 1100000, "maxRate": 350.5, "minRate": 1.01, "medianRate": 1.5 },
|
|
487
|
+
"blocks": [ { "n": 5200, "vsize": 999800, "feeSat": 1000000, "maxRate": 1.01, "minRate": 1.0, "medianRate": 1.0 } ],
|
|
488
|
+
"rest": { "n": 900, "vsize": 200000, "feeSat": 20000, "maxRate": 1.0, "minRate": 0.1, "blocks": 1 }
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
"notReported": [ "pendingancestors", "replaceable (BIP125) flag", "fees.prioritiserved", "modifiedfees",
|
|
492
|
+
"ancestorcount/ancestorfees", "withdrawreason", "replaced-by" ],
|
|
493
|
+
"log": { "lastDrain": null, "orphans": null, "orphanDetail": null, "accept": null, "relayRate": null },
|
|
494
|
+
"fees": { "f1": 3.1, "f2": 2.0, "f6": 1.1, "f24": 1.0, "f144": 1.0 },
|
|
495
|
+
"history": { "count": [ { "t": 1789155600000, "v": 21398 } ], "usage": [ { "t": 1789155600000, "v": 41234567 } ] }
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
- `scatter` rows are `[ageSec, feerate sat/vB, vsize]`, sampled to at most 1500 points. Entries with an unknown age (`time` 0) or no fee are left out, and `ageUnknown` counts the unknown ages.
|
|
500
|
+
- `cells` holds the richest (up to) 400 transactions, then one aggregate cell for the rest.
|
|
501
|
+
The aggregate carries `strata`: the tail grouped by feerate, richest first, at most 32
|
|
502
|
+
groups of `{ "vbytes", "rate", "n" }`; the 3D board colours the tail's pieces from them.
|
|
503
|
+
- `projected` cuts the pool, richest feerate first, into 1,000,000 vB blocks. `skipped` is the first block's worth, which the node is assembling itself (see `/api/nextblock`). `blocks` holds the next six, and `rest` sums everything beyond. This is an estimate: the node reports no ancestor data, so a CPFP child is placed at its own feerate.
|
|
504
|
+
- `history` holds the last 60 raw ring rows for count and usage.
|
|
505
|
+
|
|
506
|
+
### `GET /api/mempool/dense`
|
|
507
|
+
|
|
508
|
+
Every transaction in the next block's worth of the pool, richest feerate first: roughly 3–5k entries and about 100 KB. This feeds the dense block view and is kept out of the snapshot. It is refreshed on the pool tier.
|
|
509
|
+
|
|
510
|
+
```json
|
|
511
|
+
{
|
|
512
|
+
"node": "main",
|
|
513
|
+
"at": 1789155648684,
|
|
514
|
+
"blockVsize": 1000000,
|
|
515
|
+
"n": 4107,
|
|
516
|
+
"vsize": 999930,
|
|
517
|
+
"poolCount": 21398,
|
|
518
|
+
"unranked": 0,
|
|
519
|
+
"v": [ 189, 189, 110 ],
|
|
520
|
+
"r": [ 350.5, 120.0, 88.2 ],
|
|
521
|
+
"id": [ "9891f72bea4bc27b", "..." ]
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
`v`, `r` and `id` are parallel arrays: vsize, feerate (sat/vB, 2 dp), and a 16-hex txid prefix. `unranked` counts entries without a reported fee, which cannot be ranked. Before the first pool poll the answer is `{ "at": null, "n": 0, "v": [], "r": [], "id": [] }`.
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## 7. Blocks and transactions (drill-down)
|
|
530
|
+
|
|
531
|
+
These go through the same serialized RPC lane as the collectors. They never request `getblock` verbosity 2, and never return transaction hex. For richer pages use the explorer (section 8).
|
|
532
|
+
|
|
533
|
+
### `GET /api/blocks`
|
|
534
|
+
|
|
535
|
+
Recent blocks with per-block statistics from `getblockstats`.
|
|
536
|
+
|
|
537
|
+
| Query | Type | Default | Meaning |
|
|
538
|
+
|---|---|---|---|
|
|
539
|
+
| `node` | string | primary | Node id. |
|
|
540
|
+
| `limit` | int | `90` | 1–400. The source list holds at most the newest 40 blocks, so larger values return 40. |
|
|
541
|
+
|
|
542
|
+
```json
|
|
543
|
+
{
|
|
544
|
+
"node": "main",
|
|
545
|
+
"blocks": [
|
|
546
|
+
{ "t": 1789154285000, "height": 966546,
|
|
547
|
+
"hash": "0000000000000000000019f6fb9d8e1e43e9d72916b2ed8aef080c778685a4aa",
|
|
548
|
+
"time": 1789154285, "mediantime": 1789151883, "totalfee": 1929031, "txs": 3441,
|
|
549
|
+
"size": 2224872, "weight": 3991722,
|
|
550
|
+
"sizeBasis": "sum of transaction sizes (getblockstats total_size); excludes the 80-byte header and the txid-count varint",
|
|
551
|
+
"sizeMissing": null, "medianTxSize": 222, "avgTxSize": 646, "avgFeerate": 1,
|
|
552
|
+
"swtotalSize": 2121556, "swtxs": 2833, "subsidy": 312500000, "utxoIncrease": 1857,
|
|
553
|
+
"ins": 6038, "outs": 7895, "avgfee": 560, "medianfee": 178, "maxfee": 51899,
|
|
554
|
+
"p": [0, 1, 1, 2, 3], "viaPeer": null, "source": "getblockstats", "gapSec": 438 }
|
|
555
|
+
],
|
|
556
|
+
"stats": { "count": 31, "spanSec": 17766, "totalFeesSat": 103184764, "avgFeesSat": 3328541,
|
|
557
|
+
"avgSize": 1773700, "maxSize": 3142799, "avgTxs": 3585, "avgGapSec": 636.7,
|
|
558
|
+
"medGapSec": 438, "etaCadence": "00:00:10:37" }
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
`p` holds the feerate percentiles (10/25/50/75/90) in sat/vB. `gapSec` is the time since the previous block. Gaps of 7200 s or more are left out of `stats`.
|
|
563
|
+
|
|
564
|
+
### `GET /api/block`
|
|
565
|
+
|
|
566
|
+
One block: header, statistics and the first 50 txids.
|
|
567
|
+
|
|
568
|
+
| Query | Type | Meaning |
|
|
569
|
+
|---|---|---|
|
|
570
|
+
| `hash` | 64 hex | Block hash. |
|
|
571
|
+
| `height` | int | Block height (up to 12 digits). |
|
|
572
|
+
| `node` | string | Node id. |
|
|
573
|
+
|
|
574
|
+
With neither `hash` nor `height`, the current tip is returned. A malformed hash or height is a `400`. If the node has not answered `getblockchaininfo` yet, the answer is `503`.
|
|
575
|
+
|
|
576
|
+
```json
|
|
577
|
+
{
|
|
578
|
+
"ok": true,
|
|
579
|
+
"node": "main",
|
|
580
|
+
"requested": { "hash": null, "height": "966000",
|
|
581
|
+
"resolvedHash": "0000000000000000000013b8a367391f68a9891808c636ced0a399cdc1e0d5ab",
|
|
582
|
+
"resolvedHeight": 966000 },
|
|
583
|
+
"ms": null,
|
|
584
|
+
"header": { "hash": "0000000000000000000013b8a367391f68a9891808c636ced0a399cdc1e0d5ab",
|
|
585
|
+
"confirmations": 547, "height": 966000, "version": 551559168, "size": 1661742,
|
|
586
|
+
"weight": 3993924, "time": 1788831647, "mediantime": 1788829927,
|
|
587
|
+
"merkleRoot": "7d9d386ed407ee2c8bca86fef5c5c28a21163a5220f060447f1e5a17dbd8f140",
|
|
588
|
+
"txCount": 5997, "nTx": 5997,
|
|
589
|
+
"previousblockhash": "00000000000000000000dbb4d1e55ad22ed5b5a7d81d4c0fe992fceb8a5302d0",
|
|
590
|
+
"nextblockhash": "00000000000000000000032a2840c7dfc576808660ddff94773d6db984ff870c",
|
|
591
|
+
"bits": "1702355e", "difficulty": 127450789715843.1,
|
|
592
|
+
"chainTrust": "000000000000000000000000000000000000000145ab4333c85f0902722fea5e" },
|
|
593
|
+
"stats": { "totalfee": 0, "txs": 5997, "feerate_percentiles": [1, 1, 2, 3, 5], "...": "getblockstats reply" },
|
|
594
|
+
"statsError": null,
|
|
595
|
+
"txids": [ "9445363a2e1715ba61f24d068e7a3aec62a88aad458dee12284d9f89c33d883a",
|
|
596
|
+
"9891f72bea4bc27b8dd3990295be331e5e9ae69bc152c5ad31862efbb624ae69" ],
|
|
597
|
+
"txidsShown": 50,
|
|
598
|
+
"txidsTotal": 5997,
|
|
599
|
+
"truncated": true,
|
|
600
|
+
"notes": [ "header + txids only: ...", "5947 further txid(s) not listed; ..." ]
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
When the node refuses (for example, a pruned block), the answer is `200` with:
|
|
605
|
+
|
|
606
|
+
```json
|
|
607
|
+
{ "ok": false, "node": "main", "query": { "hash": null, "height": "12" },
|
|
608
|
+
"error": { "message": "Block not available (pruned data)", "code": -1, "kind": "rpc" },
|
|
609
|
+
"hint": "the node does not have this block stored (pruned, ...)" }
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### `GET /api/tx`
|
|
613
|
+
|
|
614
|
+
One transaction, decoded by the node (`getrawtransaction` verbosity 1).
|
|
615
|
+
|
|
616
|
+
| Query | Type | Meaning |
|
|
617
|
+
|---|---|---|
|
|
618
|
+
| `txid` | 64 hex | Required. |
|
|
619
|
+
| `block` | 64 hex | Optional block hash. Some nodes need it to find a confirmed transaction. |
|
|
620
|
+
| `node` | string | Node id. |
|
|
621
|
+
|
|
622
|
+
```json
|
|
623
|
+
{
|
|
624
|
+
"ok": true,
|
|
625
|
+
"node": "main",
|
|
626
|
+
"requested": { "txid": "9891f72bea4bc27b8dd3990295be331e5e9ae69bc152c5ad31862efbb624ae69", "block": null },
|
|
627
|
+
"txid": "9891f72bea4bc27b8dd3990295be331e5e9ae69bc152c5ad31862efbb624ae69",
|
|
628
|
+
"size": 222, "vsize": 141, "weight": 561, "version": 2, "locktime": 0,
|
|
629
|
+
"inMempool": false,
|
|
630
|
+
"blockHash": "0000000000000000000013b8a367391f68a9891808c636ced0a399cdc1e0d5ab",
|
|
631
|
+
"blockHeight": null, "confirmations": 547, "blockTime": 1788831647, "time": 1788831647,
|
|
632
|
+
"inputs": [ { "txid": "e990507273c34ad15db3c8491158b976940dfedfa9c42fc0a9adc2204c51376b", "vout": 1,
|
|
633
|
+
"sequence": 4294967295, "scriptSigAsm": "", "scriptSigType": null,
|
|
634
|
+
"witness": [ "304402205160e150b09b2632eab91797…" ], "value": null, "address": null } ],
|
|
635
|
+
"inputsTotal": 1,
|
|
636
|
+
"outputs": [ { "n": 0, "value": 0.0022176, "scriptPubKeyType": "witness_v0_keyhash",
|
|
637
|
+
"address": "bc1qqe2mj05z2q4zrqly789r59q5k53rhtgn8hznl0",
|
|
638
|
+
"scriptPubKeyAsm": "0 0655b93e82502a2183e4f1ca3a1414b5223bad13", "spent": null } ],
|
|
639
|
+
"outputsTotal": 2,
|
|
640
|
+
"totalOutSat": 12116383,
|
|
641
|
+
"notReported": [ "fee / feerate (needs every input's prevout, ...)" ]
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
- At most 40 inputs and 40 outputs are listed. Scripts are truncated to 96 characters and witness items to 32.
|
|
646
|
+
- Output `value` is in **BTC**, as the node reports it. `totalOutSat` is in satoshis.
|
|
647
|
+
- If the node answers raw hex instead, the result is `{ ok: true, decoded: false, sizeHex, notes }`.
|
|
648
|
+
- A malformed `txid` is a `400`. An unknown transaction is `ok: false` with a `hint`.
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## 8. Explorer: `/api/x/*`
|
|
653
|
+
|
|
654
|
+
Block, transaction and address pages, built on this node's own RPC. Each page is one batched RPC request (sometimes two). It is queued at priority 3 on the RPC lane and may wait up to 45 s. Decoded **confirmed** transactions are cached in memory (LRU, 3000 entries).
|
|
655
|
+
|
|
656
|
+
Conventions for all four:
|
|
657
|
+
|
|
658
|
+
- **Bad input is not an HTTP error.** A malformed or unknown query answers `200` with `{ "ok": false, "error": { "message": "..." }, "hint": "..." | null }`.
|
|
659
|
+
- Amounts are in **satoshis**. `feerate` is sat/vB, to 2 dp.
|
|
660
|
+
- **`usd`**: `/api/x/tx`, `/api/x/block` and `/api/x/address` add `"usd": <number|null>` to every `ok: true` answer when market data is enabled. The price is the median of fresh exchange tickers if the Markets tab is polling. Otherwise it is one Coinbase and one Kraken ticker read, cached for 60 s. The server waits at most 1.5 s for a price; if none arrives, `usd` is `null`. The field is absent when markets are disabled. `/api/x/search` never carries it.
|
|
661
|
+
|
|
662
|
+
### `GET /api/x/search`
|
|
663
|
+
|
|
664
|
+
| Query | Meaning |
|
|
665
|
+
|---|---|
|
|
666
|
+
| `q` | A height, block hash, txid or address. |
|
|
667
|
+
|
|
668
|
+
Classification:
|
|
669
|
+
|
|
670
|
+
- 1–9 digits: a block (no node call).
|
|
671
|
+
- 64 hex: `getblockheader` decides between `block` and `tx`.
|
|
672
|
+
- 14–100 alphanumerics: `validateaddress`.
|
|
673
|
+
|
|
674
|
+
```json
|
|
675
|
+
{ "ok": true, "type": "block", "id": "966000" }
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
```json
|
|
679
|
+
{ "ok": false, "error": { "message": "nothing on this node matches \"zzz\"" },
|
|
680
|
+
"hint": "a height is digits; a block hash or txid is 64 hex characters; an address starts 1, 3 or bc1" }
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
`type` is `block`, `tx` or `address`. A hex `id` is returned lowercased.
|
|
684
|
+
|
|
685
|
+
### `GET /api/x/tx`
|
|
686
|
+
|
|
687
|
+
| Query | Meaning |
|
|
688
|
+
|---|---|
|
|
689
|
+
| `txid` | 64 hex. |
|
|
690
|
+
|
|
691
|
+
Uses `getrawtransaction` verbosity 2, which carries the fee and every prevout. Then, in a second batch, `getblockheader` (for the height) and `gettxspendingprevout` (spent-by, for up to 500 outputs).
|
|
692
|
+
|
|
693
|
+
```json
|
|
694
|
+
{
|
|
695
|
+
"ok": true,
|
|
696
|
+
"node": "main",
|
|
697
|
+
"tx": {
|
|
698
|
+
"txid": "9891f72bea4bc27b8dd3990295be331e5e9ae69bc152c5ad31862efbb624ae69",
|
|
699
|
+
"hash": "00267bf1d6de9f1a811665fe9cbe3f8f551ead73703e41e4b7f0ecb097669c8e",
|
|
700
|
+
"version": 2, "locktime": 0, "size": 222, "vsize": 141, "weight": 561,
|
|
701
|
+
"fee": 16920, "feerate": 120, "coinbase": false,
|
|
702
|
+
"inSat": 12133303, "outSat": 12116383,
|
|
703
|
+
"vin": [ { "txid": "e990507273c34ad15db3c8491158b976940dfedfa9c42fc0a9adc2204c51376b", "vout": 1,
|
|
704
|
+
"sequence": 4294967295, "value": 12133303,
|
|
705
|
+
"address": "bc1qgwmsxwp6wj0fla53kwtx3h7eeyh3hsuamynp9z",
|
|
706
|
+
"type": "witness_v0_keyhash", "height": 965976, "witness": true } ],
|
|
707
|
+
"vout": [
|
|
708
|
+
{ "n": 0, "value": 221760, "address": "bc1qqe2mj05z2q4zrqly789r59q5k53rhtgn8hznl0",
|
|
709
|
+
"type": "witness_v0_keyhash", "spentBy": null },
|
|
710
|
+
{ "n": 1, "value": 11894623, "address": "bc1qgwmsxwp6wj0fla53kwtx3h7eeyh3hsuamynp9z",
|
|
711
|
+
"type": "witness_v0_keyhash",
|
|
712
|
+
"spentBy": { "txid": "6e556dd8ec13181e7f92ecdc6e2172ffa2f9c98ebece6f4245d5cc46eab88ff7",
|
|
713
|
+
"blockhash": "0000000000000000000125c3df20202df66378286a3bcd84142877bfae84e466" } }
|
|
714
|
+
],
|
|
715
|
+
"features": [ "segwit" ],
|
|
716
|
+
"blockhash": "0000000000000000000013b8a367391f68a9891808c636ced0a399cdc1e0d5ab",
|
|
717
|
+
"confirmations": 547, "time": 1788831647, "height": 966000
|
|
718
|
+
},
|
|
719
|
+
"tip": 966546,
|
|
720
|
+
"outputsShown": 2,
|
|
721
|
+
"usd": 77267.47
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
- A coinbase input is `{ "coinbase": true, "sequence": ... }`, and a coinbase transaction has `fee: 0`.
|
|
726
|
+
- `features` can contain `segwit`, `taproot`, `rbf` (some input sequence below `0xfffffffe`), `consolidation` (5 or more inputs, 2 or fewer outputs) and `op_return`.
|
|
727
|
+
- `confirmations: 0` means the transaction is in the mempool.
|
|
728
|
+
|
|
729
|
+
### `GET /api/x/block`
|
|
730
|
+
|
|
731
|
+
| Query | Type | Meaning |
|
|
732
|
+
|---|---|---|
|
|
733
|
+
| `id` | height or 64 hex | Required. |
|
|
734
|
+
| `page` | int ≥ 0 | 25 transactions per page. Default 0. |
|
|
735
|
+
|
|
736
|
+
```json
|
|
737
|
+
{
|
|
738
|
+
"ok": true,
|
|
739
|
+
"node": "main",
|
|
740
|
+
"block": { "hash": "0000000000000000000013b8a367391f68a9891808c636ced0a399cdc1e0d5ab",
|
|
741
|
+
"height": 966000, "confirmations": 547, "time": 1788831647, "mediantime": 1788829927,
|
|
742
|
+
"size": 1661742, "strippedsize": 777394, "weight": 3993924, "version": 551559168,
|
|
743
|
+
"versionHex": "20e02000",
|
|
744
|
+
"merkleroot": "7d9d386ed407ee2c8bca86fef5c5c28a21163a5220f060447f1e5a17dbd8f140",
|
|
745
|
+
"bits": "1702355e", "nonce": 545538887, "difficulty": 127450789715843.1,
|
|
746
|
+
"chainwork": "000000000000000000000000000000000000000145ab4333c85f0902722fea5e",
|
|
747
|
+
"nTx": 5997,
|
|
748
|
+
"previousblockhash": "00000000000000000000dbb4d1e55ad22ed5b5a7d81d4c0fe992fceb8a5302d0",
|
|
749
|
+
"nextblockhash": "00000000000000000000032a2840c7dfc576808660ddff94773d6db984ff870c" },
|
|
750
|
+
"stats": { "avgfee": 0, "feerate_percentiles": [1, 1, 2, 3, 5], "total_size": 1661600, "...": "full getblockstats reply" },
|
|
751
|
+
"pool": { "label": "ViaBTC", "tag": "/ViaBTC/" },
|
|
752
|
+
"page": 0,
|
|
753
|
+
"pages": 240,
|
|
754
|
+
"txs": [
|
|
755
|
+
{ "txid": "9445363a2e1715ba61f24d068e7a3aec62a88aad458dee12284d9f89c33d883a", "fee": 0, "feerate": 0,
|
|
756
|
+
"vsize": 419, "outSat": 313349306, "coinbase": true, "inCount": 1, "outCount": 6,
|
|
757
|
+
"features": [ "op_return" ], "height": 966000, "time": 1788831647, "confirmations": 547 },
|
|
758
|
+
{ "txid": "9891f72bea4bc27b8dd3990295be331e5e9ae69bc152c5ad31862efbb624ae69", "fee": 16920, "feerate": 120,
|
|
759
|
+
"vsize": 141, "outSat": 12116383, "coinbase": false, "inCount": 1, "outCount": 2,
|
|
760
|
+
"features": [ "segwit" ], "height": 966000, "time": 1788831647, "confirmations": 547 }
|
|
761
|
+
],
|
|
762
|
+
"tip": 966546,
|
|
763
|
+
"usd": 77280.04
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
`pool` is set only when mining attribution has a row for that height. A transaction the node could not return appears as `{ "txid": "...", "missing": true }`.
|
|
768
|
+
|
|
769
|
+
### `GET /api/x/address`
|
|
770
|
+
|
|
771
|
+
| Query | Type | Meaning |
|
|
772
|
+
|---|---|---|
|
|
773
|
+
| `addr` | string | 14–100 alphanumerics. |
|
|
774
|
+
| `page` | int ≥ 0 | 25 transactions per page, newest first. |
|
|
775
|
+
|
|
776
|
+
**Bitcoin Core has no address index** — `getaddressbalance` and `getaddresstxids` are insight-style
|
|
777
|
+
extensions, and a stock Core node answers `Method not found` (measured 2026-09-13 against both
|
|
778
|
+
configured nodes, both Bitcoin Core). So a node configured with `addressIndex` answers
|
|
779
|
+
from **BlockYard's own index**, built from the node's block files and kept current by a follower
|
|
780
|
+
(see [Building the address index](INSTALL.md#building-the-address-index)); the reply says so in
|
|
781
|
+
`source` and describes the index in `index`.
|
|
782
|
+
|
|
783
|
+
```json
|
|
784
|
+
{
|
|
785
|
+
"ok": true,
|
|
786
|
+
"node": "main",
|
|
787
|
+
"dataNode": "main",
|
|
788
|
+
"address": "bc1qqe2mj05z2q4zrqly789r59q5k53rhtgn8hznl0",
|
|
789
|
+
"type": "witness v0",
|
|
790
|
+
"scriptType": null,
|
|
791
|
+
"indexed": true,
|
|
792
|
+
"source": "local-index",
|
|
793
|
+
"balance": { "balance": 221760, "received": 221760, "utxos": 1 },
|
|
794
|
+
"utxos": [ { "txid": "9891f72b...", "n": 0, "value": 221760, "height": 966000 } ],
|
|
795
|
+
"utxoNote": null,
|
|
796
|
+
"txCount": 1,
|
|
797
|
+
"index": { "tip": 966963, "behind": 0, "builtAt": "2026-09-14T06:54:06.609Z", "following": true, "stale": null, "postTip": 0 },
|
|
798
|
+
"page": 0,
|
|
799
|
+
"pages": 1,
|
|
800
|
+
"txs": [ { "txid": "9891f72b...", "fee": 16920, "feerate": 120, "vsize": 141, "outSat": 12116383,
|
|
801
|
+
"coinbase": false, "inCount": 1, "outCount": 2, "features": [ "segwit" ],
|
|
802
|
+
"height": 966000, "time": 1788831647, "confirmations": 547, "delta": 221760 } ],
|
|
803
|
+
"tip": 966546,
|
|
804
|
+
"usd": 77280.04
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
Without an index the page still answers, but it answers *honestly*: `indexed` is `false`,
|
|
809
|
+
`txCount` is **`null` rather than `0`**, `balance` is `null`, `txs` is empty and the page says the
|
|
810
|
+
node keeps no address index. A count nobody can answer is not zero. `validateaddress` works
|
|
811
|
+
everywhere (script parsing, no index), so the address is still confirmed and typed. Transaction and
|
|
812
|
+
block lookups are unaffected — those use `txindex`.
|
|
813
|
+
|
|
814
|
+
**While the server is building the index** (a configured `addressIndex` directory with no finished
|
|
815
|
+
index in it), the answer is the same `indexed: false` shape plus `indexBuilding`, so the page can say
|
|
816
|
+
how far the build has got:
|
|
817
|
+
|
|
818
|
+
```json
|
|
819
|
+
{
|
|
820
|
+
"ok": true, "node": "main", "address": "bc1qqe2mj05z2q4zrqly789r59q5k53rhtgn8hznl0", "type": "witness v0",
|
|
821
|
+
"indexed": false, "txCount": null, "balance": null, "txs": [], "page": 0, "pages": 1,
|
|
822
|
+
"indexError": "Method not found", "balanceError": "Method not found", "localIndexError": null,
|
|
823
|
+
"indexBuilding": { "dir": "/var/lib/blockyard/index", "node": "main", "phase": "scan",
|
|
824
|
+
"done": 1812, "total": 4870, "rows": 2190345120, "eta": "19 min",
|
|
825
|
+
"startedAt": 1789154285000, "error": null, "paused": false },
|
|
826
|
+
"tip": 966546, "usd": 77280.04
|
|
827
|
+
}
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
`indexBuilding` is `null` whenever no build is running. `phase` is `heights` (block hashes over
|
|
831
|
+
RPC; `done`/`total` are heights), `scan` (block files on the worker pool; `done`/`total` are files)
|
|
832
|
+
or `sort` (buckets); `rows` is the count so far, `eta` a short duration string or `null`, and
|
|
833
|
+
`paused` is `true` while the build is held because the node's RPC is failing or slow. On finish the
|
|
834
|
+
follower starts and the next request answers `indexed: true`.
|
|
835
|
+
|
|
836
|
+
- `type` is `witness v<N>`, `script` or `legacy`.
|
|
837
|
+
- `indexed` says whether this node can answer address history at all. When `false`, `txCount` is
|
|
838
|
+
`null`, `balance` is `null`, and nothing on the page is derived from the refusal; `indexError` and
|
|
839
|
+
`balanceError` carry the node's own message (`Method not found` on Core), and `localIndexError`
|
|
840
|
+
says why a configured `addressIndex` could not be opened, so a broken index does not look like an
|
|
841
|
+
unconfigured one.
|
|
842
|
+
- `source` is `local-index` when the answer comes from BlockYard's index; `dataNode` is the node
|
|
843
|
+
whose RPC the follower reads (one index serves every node on the same chain).
|
|
844
|
+
- `balance.balance` and `balance.received` are sums of each transaction's **net** for the address
|
|
845
|
+
(a transaction that both paid and spent it counts once, by its net). `utxos` lists the address's
|
|
846
|
+
unspent outputs — every transaction the index says touched it, each output paying it checked with
|
|
847
|
+
`gettxout` (the UTXO set, less what the mempool already spends), `height` from the index's own row
|
|
848
|
+
— and `balance.utxos` is their count. The walk is the whole history, so it is made for an address
|
|
849
|
+
with at most 100 transactions; a longer one gets `utxos: null` and a `utxoNote` saying so.
|
|
850
|
+
- `index.tip` is the highest block the index covers (base, folded layers and the follower's live
|
|
851
|
+
tail together); `behind` is how many blocks the node is ahead of it; `builtAt` is the manifest's
|
|
852
|
+
build time; `following` says a follower is running; `stale` is a message when it has stopped (a
|
|
853
|
+
reorganisation deeper than its tail, asking for a rebuild); `postTip` counts rows above the node's
|
|
854
|
+
own tip, which are excluded from the balance and the history rather than shown.
|
|
855
|
+
- `delta` is the net change to this address in satoshis: outputs to it minus inputs from it. A
|
|
856
|
+
transaction the node could not return is `{ "txid": ..., "missing": true, "height": ..., "delta": ... }`
|
|
857
|
+
— the height and amount are the index's own.
|
|
858
|
+
- Pages are 25 rows, newest first, and a deep page costs no more than the first: the index keeps
|
|
859
|
+
only `skip + limit` rows in a ring while it sums the balance.
|
|
860
|
+
|
|
861
|
+
---
|
|
862
|
+
|
|
863
|
+
## 9. Mining
|
|
864
|
+
|
|
865
|
+
### `GET /api/mining`
|
|
866
|
+
|
|
867
|
+
Pool attribution over the observed window of recent blocks, plus the cached block template. Attribution costs two cheap reads per block and can be turned off with `BLOCKYARD_MINING=0`.
|
|
868
|
+
|
|
869
|
+
```json
|
|
870
|
+
{
|
|
871
|
+
"node": "main",
|
|
872
|
+
"recent": [
|
|
873
|
+
{ "height": 966546, "hash": "0000...a4aa", "at": 1789154285000, "poolKey": "viabtc",
|
|
874
|
+
"tagText": "/ViaBTC/", "tagSource": "push", "tagParseable": true, "tagHeightMatch": true,
|
|
875
|
+
"weight": 3993366, "size": 2225310, "strippedSize": 589352, "txs": 3441, "totalfee": 1929031,
|
|
876
|
+
"avgFeerate": 1, "p50": 1, "p75": 1, "p99": 3, "extraNonce": null, "commitment": "fabe6d6d",
|
|
877
|
+
"rawCoinbase": "0392bf0e...", "poolLabel": "ViaBTC", "poolLabelKey": "viabtc",
|
|
878
|
+
"matchedTag": "/ViaBTC/", "seenAt": 1789154316094, "poolName": null }
|
|
879
|
+
],
|
|
880
|
+
"pools": [
|
|
881
|
+
{ "poolKey": "viabtc", "label": "ViaBTC", "tagText": "/ViaBTC/", "tags": [ "/ViaBTC/" ],
|
|
882
|
+
"blocks": 7, "sharePct": 22.6, "txs": 21361, "avgWeight": 3993511, "medianSize": 1791590,
|
|
883
|
+
"avgFeeRate": 2.14, "medianFeeRate": 1, "totalFeesSat": 20814877,
|
|
884
|
+
"firstHeight": 966519, "lastHeight": 966546, "name": null }
|
|
885
|
+
],
|
|
886
|
+
"byPool": [ { "poolKey": "foundry usa", "label": "Foundry USA", "blocks": 8, "sharePct": 25.8,
|
|
887
|
+
"name": "Foundry USA", "labelled": true, "...": "same fields as pools" } ],
|
|
888
|
+
"labelSource": null,
|
|
889
|
+
"nextBlock": { "...": "the last /api/nextblock result, or null" },
|
|
890
|
+
"nextBlockAgeMs": 12000,
|
|
891
|
+
"windowBlocks": 31,
|
|
892
|
+
"windowHeights": { "from": 966516, "to": 966546 },
|
|
893
|
+
"fetched": 31, "at": 1789154316094, "lastError": null, "skippedIbd": 0,
|
|
894
|
+
"aliasesLoaded": false, "enabled": true,
|
|
895
|
+
"note": "shares are of the observed window only, ..."
|
|
896
|
+
}
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
- `recent` holds the newest 40 attributed blocks.
|
|
900
|
+
- `pools` groups blocks by coinbase tag. `byPool` groups by pool label, when a pool map is loaded (`labelSource` then names its source and hash).
|
|
901
|
+
- `poolName` / `name` come from `data/pool-aliases.json`. When a block has no alias, the coinbase text is shown as the pool wrote it.
|
|
902
|
+
|
|
903
|
+
### `GET /api/nextblock`
|
|
904
|
+
|
|
905
|
+
The block being built right now, **assembled by this server from the mempool** — it makes no RPC call of its own. Core publishes `depends`, the ancestor sizes and fees, and `fees.chunk`/`chunkweight` in `getrawmempool(true)`, which the monitor already reads every 20 s for the mempool view; `server/collect/gbt.js` selects greedily over the node's own chunk feerate, taking each transaction with its unselected ancestors. Assembly costs this process ~50–70 ms and your node nothing. The reply carries `assembledLocally: true`, `source: "getrawmempool"`, `poolSize` and `poolAgeMs` so a caller can see how fresh its input was. Measured against the node's own `getblocktemplate` on the same pool: 0.03% apart on total fees. It is a reconstruction of what a miner would choose, not the node's template — sigop limits and policy the mempool does not publish are not modelled.
|
|
906
|
+
|
|
907
|
+
| Query | Type | Default | Meaning |
|
|
908
|
+
|---|---|---|---|
|
|
909
|
+
| `node` | string | primary | Node id. |
|
|
910
|
+
| `stale` | ms | `15000` | Serve the cached template if it is younger than this. |
|
|
911
|
+
| `refresh` | `1` | — | Force a new call. |
|
|
912
|
+
|
|
913
|
+
```json
|
|
914
|
+
{
|
|
915
|
+
"node": "main",
|
|
916
|
+
"height": 966547,
|
|
917
|
+
"previous": "0000000000000000000019f6fb9d8e1e43e9d72916b2ed8aef080c778685a4aa",
|
|
918
|
+
"txCount": 1186,
|
|
919
|
+
"weight": 2027641, "weightLimit": 4000000, "weightPct": 50.7,
|
|
920
|
+
"totalFeesSat": 1087392, "coinbaseSat": 313587392,
|
|
921
|
+
"feeRate": { "...": "summary" },
|
|
922
|
+
"feeRateHistogram": [ { "lo": 0, "hi": 0.1, "n": 57, "weight": 30528 } ],
|
|
923
|
+
"mintime": 1789151884, "bits": "1702355e", "sigopLimit": 80000, "sizeLimit": 4000000,
|
|
924
|
+
"version": 536870912, "signal": null,
|
|
925
|
+
"packages": { "total": 1100, "multiTx": 40, "txsInPackages": 126, "largest": 5,
|
|
926
|
+
"cpfpCandidates": 12, "sizeHistogram": [], "top": [] },
|
|
927
|
+
"economy": { "remainingWeight": 1972359, "remainingPct": 49.3, "marginal": null, "spillCount": 0,
|
|
928
|
+
"spillWeight": 0, "poolBytes": 8123456, "poolFitsNextPct": 100, "backlogBlocks": 0,
|
|
929
|
+
"blockBytesEstimate": 1661742, "ahead": [], "note": "..." },
|
|
930
|
+
"visual": { "cells": [], "totalVbytes": 506910, "tailCount": 786 },
|
|
931
|
+
"at": 1789155693548,
|
|
932
|
+
"ms": 72,
|
|
933
|
+
"note": "the block being built is assembled here from getrawmempool(true) ...",
|
|
934
|
+
"assembledLocally": true,
|
|
935
|
+
"source": "getrawmempool",
|
|
936
|
+
"poolSize": 30768,
|
|
937
|
+
"poolAgeMs": 2276
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
When no template is available, the answer is `{ "node": "...", "unavailable": "<reason>" }`. The reasons are: disabled with `BLOCKYARD_MINING_TEMPLATE=0`, the node is in initial download, or the node's error. If a refresh fails after an earlier success, the last template is returned with a `lastError` field.
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
945
|
+
## 10. Peers and network
|
|
946
|
+
|
|
947
|
+
### `GET /api/peers`
|
|
948
|
+
|
|
949
|
+
```json
|
|
950
|
+
{
|
|
951
|
+
"node": "main",
|
|
952
|
+
"counts": { "connections": 11, "in": 0, "out": 11, "wanted": null, "budget": null,
|
|
953
|
+
"banned": null, "bannedOf": null },
|
|
954
|
+
"ranking": null,
|
|
955
|
+
"identitySource": "getpeerinfo",
|
|
956
|
+
"identity": null,
|
|
957
|
+
"rpcRows": 11,
|
|
958
|
+
"rpcUpdatedAt": 1789155718231,
|
|
959
|
+
"rpcPeers": [ { "id": 3, "addr": "192.0.2.10:8333", "subver": "/Satoshi:27.0.0/", "...": "getpeerinfo row, verbatim" } ],
|
|
960
|
+
"activity": [],
|
|
961
|
+
"recentEvents": [],
|
|
962
|
+
"network": { "version": 10000, "subversion": "/Satoshi:29.0.0/", "protocol": 70016,
|
|
963
|
+
"services": "0000000000000409", "servicesNames": [ "NETWORK", "WITNESS" ],
|
|
964
|
+
"networkactive": true, "relayfee": 0.00001, "incrementalfee": 0.00001,
|
|
965
|
+
"networks": [], "localaddresses": [] }
|
|
966
|
+
}
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
- `rpcPeers` is the node's `getpeerinfo` reply, passed through as-is.
|
|
970
|
+
- `activity`, `identity`, `ranking`, `wanted`, `budget` and `banned` come from the node log, and stay empty or `null` unless `BLOCKYARD_LOG_SOURCE=1`.
|
|
971
|
+
- `identitySource` names the source in use.
|
|
972
|
+
|
|
973
|
+
### `GET /api/net`
|
|
974
|
+
|
|
975
|
+
Bandwidth and disk, with every absent figure listed in words.
|
|
976
|
+
|
|
977
|
+
```json
|
|
978
|
+
{
|
|
979
|
+
"node": "main",
|
|
980
|
+
"measured": { "inBps": null, "diskWriteBps": null, "netTotalLog": null, "diskTotal": null,
|
|
981
|
+
"avgRecv": null, "avgWrite": null, "floor": null, "poolMedian": null,
|
|
982
|
+
"source": "getnettotals delta rate (RPC) — reported only while that counter moves; ...",
|
|
983
|
+
"downloadMeasured": false },
|
|
984
|
+
"rpc": { "totalRecv": 0, "totalSent": 0, "uploadtarget": { "...": "getnettotals uploadtarget" }, "uploadMeasured": false },
|
|
985
|
+
"unavailable": [ "outbound bytes / upload rate: ...", "inbound bytes / download rate: ...", "log-only figures (...)" ],
|
|
986
|
+
"peers": { "connections": 11, "in": 0, "out": 11, "wanted": null },
|
|
987
|
+
"series": null,
|
|
988
|
+
"formatted": { "inBps": null, "disk": null }
|
|
989
|
+
}
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
`inBps` / `outBps` are `null` (never `0`) while the node's byte counter reads zero.
|
|
993
|
+
|
|
994
|
+
---
|
|
995
|
+
|
|
996
|
+
## 11. Events
|
|
997
|
+
|
|
998
|
+
### `GET /api/events`
|
|
999
|
+
|
|
1000
|
+
The monitor's own event feed: collector errors, quality flags, new tips, reorgs and so on. It is newest first and shared by all nodes; each row names its `node`. Rows of kind `index` mark the start, the finish and a failure of an address index build the server runs; the browser shows those three as notifications.
|
|
1001
|
+
|
|
1002
|
+
| Query | Type | Default | Meaning |
|
|
1003
|
+
|---|---|---|---|
|
|
1004
|
+
| `since` | int | `0` | Return only rows with `seq` greater than this. |
|
|
1005
|
+
| `limit` | int | `200` | 1–1000. |
|
|
1006
|
+
| `source` | csv | `monitor` | `monitor`, `log`, …, or `all`. Node-log lines are excluded unless asked for. |
|
|
1007
|
+
| `severity` | csv | — | For example `warn,error`. |
|
|
1008
|
+
| `kind` | csv | — | For example `collector_error`. |
|
|
1009
|
+
| `q` | string | — | Case-insensitive substring over `text`, `tag` and `kind`. |
|
|
1010
|
+
|
|
1011
|
+
```json
|
|
1012
|
+
{
|
|
1013
|
+
"events": [
|
|
1014
|
+
{ "seq": 194639, "source": "monitor", "kind": "collector_error", "severity": "warn",
|
|
1015
|
+
"tag": "collector", "ts": 1789107616900,
|
|
1016
|
+
"text": "getrawmempool verbose failed: RPC circuit breaker open; retry in 22s (...)",
|
|
1017
|
+
"node": "main" }
|
|
1018
|
+
],
|
|
1019
|
+
"maxSeq": 194639,
|
|
1020
|
+
"count": 8
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
To poll incrementally, keep `maxSeq` and pass it back as `since`. The filters are applied to a window of `limit × 4` rows, and `count` is the number of matches in that window.
|
|
1025
|
+
|
|
1026
|
+
---
|
|
1027
|
+
|
|
1028
|
+
## 12. Markets
|
|
1029
|
+
|
|
1030
|
+
Exchange prices from the public REST APIs of Coinbase, Kraken, Bitstamp, Bitfinex and OKX. **This is the only outbound connection BlockYard makes that is not to the node.** It runs server-side, because the page's CSP allows `connect-src 'self'` only.
|
|
1031
|
+
|
|
1032
|
+
- **On demand.** Nothing is fetched until someone calls `/api/markets` or `/api/markets/depth`. Each call "touches" the feed. The dashboard's Overview makes that call by default (Display settings → Markets & Price → Price line on Overview), so a monitor with anyone on its landing page is polling. While touched, it polls tickers every 15 s, hourly candles every 5 min and order books every 30 s. It stops 10 minutes after the last touch.
|
|
1033
|
+
- `BLOCKYARD_MARKETS=0` (or `markets.enabled=false`) turns it off. Both endpoints then answer `{ "ok": true, "enabled": false, "note": "market data is off on this monitor (...)" }`.
|
|
1034
|
+
- An exchange that fails keeps its last data and reports `error`. It is never dropped or zero-filled.
|
|
1035
|
+
|
|
1036
|
+
### `GET /api/markets`
|
|
1037
|
+
|
|
1038
|
+
```json
|
|
1039
|
+
{
|
|
1040
|
+
"ok": true, "enabled": true, "at": 1789155671855, "running": true,
|
|
1041
|
+
"interval": "1h", "tickerMs": 15000, "warming": false,
|
|
1042
|
+
"summary": { "median": 77282.55, "spread": 14.17, "vol24": 23204.09, "reporting": 4 },
|
|
1043
|
+
"exchanges": [
|
|
1044
|
+
{ "id": "coinbase", "name": "Coinbase", "pair": "BTC-USD", "quote": "USD",
|
|
1045
|
+
"last": 77286.14, "bid": 77286.13, "ask": 77286.14, "spread": 0.01, "vol24": 7289.43,
|
|
1046
|
+
"change24": 0.00205, "high24": 79852.22, "low24": 76030,
|
|
1047
|
+
"at": 1789155664470, "stale": false, "error": null,
|
|
1048
|
+
"candles": [ { "t": 1788552000000, "o": 79808.55, "h": 79890, "l": 79680.01, "c": 79718.63, "v": 181.98 } ],
|
|
1049
|
+
"candlesAt": 1789155409559 }
|
|
1050
|
+
]
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
- `summary.median` and `summary.spread` use USD-quoted exchanges only (OKX quotes USDT). Stale tickers are excluded. `reporting` is how many exchanges contributed.
|
|
1055
|
+
- `stale` is `true` when the ticker is older than 3 × `tickerMs`.
|
|
1056
|
+
- `change24` is a fraction, relative to the open of the first hourly candle within the last 24 h.
|
|
1057
|
+
- `candles` are hourly, oldest first, up to 168 (7 days). `v` is in BTC.
|
|
1058
|
+
- `warming` is `true` until any exchange has reported a price. The first call after an idle period usually returns `warming: true`.
|
|
1059
|
+
|
|
1060
|
+
### `GET /api/markets/depth`
|
|
1061
|
+
|
|
1062
|
+
The order books as cumulative depth on a fixed price grid, plus the snapshot from `ago` seconds earlier for comparison.
|
|
1063
|
+
|
|
1064
|
+
| Query | Type | Default | Meaning |
|
|
1065
|
+
|---|---|---|---|
|
|
1066
|
+
| `ago` | seconds | `600` | One of `60`, `300`, `600`, `1800`, `3600`. Other values become 600. |
|
|
1067
|
+
|
|
1068
|
+
```json
|
|
1069
|
+
{
|
|
1070
|
+
"ok": true, "enabled": true, "running": true, "bookMs": 30000,
|
|
1071
|
+
"at": 1789155680845, "thenAt": 1789155380703, "firstAt": 1789154211190, "ago": 300,
|
|
1072
|
+
"mid": 77276.995, "p0": 68000, "n": 372, "step": 50,
|
|
1073
|
+
"exchanges": [
|
|
1074
|
+
{ "id": "coinbase", "name": "Coinbase", "quote": "USD",
|
|
1075
|
+
"bids": [ 1127.511, 1087.61, 1087.313, "..." ],
|
|
1076
|
+
"asks": [ null, null, null, "..." ],
|
|
1077
|
+
"thenBids": [ 1137.445, 1097.559, 1097.261, "..." ],
|
|
1078
|
+
"thenAsks": [ null, null, null, "..." ],
|
|
1079
|
+
"error": null, "bookAt": 1789155680841 }
|
|
1080
|
+
]
|
|
1081
|
+
}
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
- The grid is `price(i) = p0 + i × step` for `i` from 0 to `n - 1`. `step` is $50, and the grid spans ±12 % around the median mid across exchanges.
|
|
1085
|
+
- `bids[i]` is the total BTC bid at or above `price(i)`. `asks[i]` is the total BTC asked at or below it.
|
|
1086
|
+
- A value is `null` where that side has nothing to say: above the best bid, below the best ask, or past the end of a book the exchange cut short.
|
|
1087
|
+
- `thenBids` / `thenAsks` are the older snapshot, re-aligned onto the current grid. They are `null` until an old enough snapshot exists. History is kept for about an hour.
|
|
1088
|
+
- Before the first book poll completes, the answer is `{ ok, enabled, running, bookMs, "warming": true, "exchanges": [ { "id", "name", "error" } ] }`.
|
|
1089
|
+
|
|
1090
|
+
---
|
|
1091
|
+
|
|
1092
|
+
## 13. RPC console and the allowlist
|
|
1093
|
+
|
|
1094
|
+
### `POST /api/rpc`
|
|
1095
|
+
|
|
1096
|
+
Auth `any`, with CSRF when accounts are on. Runs one **read-only** RPC method against the node, through the same serialized lane as the collectors.
|
|
1097
|
+
|
|
1098
|
+
| Body field | Type | Meaning |
|
|
1099
|
+
|---|---|---|
|
|
1100
|
+
| `method` | string | RPC method name. |
|
|
1101
|
+
| `params` | array | Positional parameters. Anything that is not an array is replaced with `[]`. |
|
|
1102
|
+
|
|
1103
|
+
| Query | Meaning |
|
|
1104
|
+
|---|---|
|
|
1105
|
+
| `node` | Node id. |
|
|
1106
|
+
|
|
1107
|
+
Success:
|
|
1108
|
+
|
|
1109
|
+
```json
|
|
1110
|
+
{ "ok": true, "node": "main", "method": "getblockcount", "ms": 314, "result": 966546, "note": null }
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
The node returned an error (still HTTP 200):
|
|
1114
|
+
|
|
1115
|
+
```json
|
|
1116
|
+
{ "ok": false, "node": "main", "method": "getblock", "ms": 12,
|
|
1117
|
+
"error": { "message": "Block not found", "code": -5, "kind": "rpc" } }
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
The allowlist refused the method (HTTP 403):
|
|
1121
|
+
|
|
1122
|
+
```json
|
|
1123
|
+
{ "error": { "message": "stop is not callable from the web UI: this method mutates state or monopolises the node's single-threaded RPC server",
|
|
1124
|
+
"kind": "api", "code": "rpc_denied" } }
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
`note` is set for methods the node itself documents as refused or worker-owned, so an error there is expected: `loadtxoutset`, `getopenrpcinfo`, `rpc.discover`, `exportasmap`, `enumeratesigners`, `walletdisplayaddress`, `getmempoolcluster`, `getblockfrompeer`, `preciousblock`, `pruneblockchain`, `submitheader`, `getblockfilter`.
|
|
1128
|
+
|
|
1129
|
+
Every call, allowed or denied, is written to the audit log (method name, node, duration, and the error if any). Parameters are not recorded.
|
|
1130
|
+
|
|
1131
|
+
### How a method is classified (`server/rpc/allowlist.js`)
|
|
1132
|
+
|
|
1133
|
+
The default is **deny**. The rules are applied in this order, and the first match wins:
|
|
1134
|
+
|
|
1135
|
+
1. **Not a non-empty string**: denied.
|
|
1136
|
+
2. **Exact deny list**: denied, even when the name looks like a read. The list:
|
|
1137
|
+
|
|
1138
|
+
| Group | Methods |
|
|
1139
|
+
|---|---|
|
|
1140
|
+
| Wallet key material | `getnewaddress`, `getrawchangeaddress`, `keypoolrefill`, `addhdkey` |
|
|
1141
|
+
| Spends and broadcasts | `sendtoaddress`, `sendmany`, `send`, `sendall`, `sendrawtransaction`, `submitpackage`, `walletcreatefundedpsbt`, `fundrawtransaction`, `bumpfee`, `psbtbumpfee`, `signrawtransactionwithwallet` |
|
|
1142
|
+
| Wallet state | `walletpassphrase`, `walletlock`, `walletpassphrasechange`, `encryptwallet`, `setlabel`, `importprivkey`, `importaddress`, `importpublickey`, `importdescriptors`, `importmempool`, `importmulti`, `importprunedfunds`, `removeprunedfunds`, `createwallet`, `loadwallet`, `unloadwallet`, `restorewallet`, `migratewallet`, `setwalletflag`, `backupwallet`, `exportwatchonlywallet`, `abandontransaction`, `lockunspent`, `sethdseed`, `settxfee`, `signmessage`, `signrawtransactionwithkey`, `walletprocesspsbt`, `descriptorprocesspsbt` |
|
|
1143
|
+
| Peer / network control | `addnode`, `removeaddednode`, `disconnectnode`, `setban`, `clearbanned`, `setnetworkactive`, `ping` |
|
|
1144
|
+
| Chain / storage | `invalidateblock`, `reconsiderblock`, `preciousblock`, `pruneblockchain`, `submitblock`, `submitheader`, `stop`, `savemempool`, `dumptxoutset`, `loadtxoutset`, `simulateutxo` |
|
|
1145
|
+
| Heavy reads that monopolise the single-threaded RPC server | `rescanblockchain`, `scanblocks`, `scantxoutset`, `getdescriptoractivity`, `verifychain` |
|
|
1146
|
+
| Other | `logging` |
|
|
1147
|
+
|
|
1148
|
+
3. **Deny prefixes**: denied. The prefixes are `generate`, `invalidate`, `reconsider`, `import`, `send`, `set`, `unload`, `load`, `sign`. A node that adds vendor-prefixed commands of its own beyond Core's RPC set has the mutating shapes of those denied too, by the same rule.
|
|
1149
|
+
4. `help` and `uptime` are allowed. `stop` is denied.
|
|
1150
|
+
5. **Allow prefixes**: allowed. The prefixes are `get`, `list`, `estimate`, `verify`, `help`, `uptime`, `decoderaw`, `decodescript`, `createraw`, `analyzepsbt`, `decodepsbt`, `convertbits`, `getrpcinfo`. Where a node adds vendor-prefixed commands, its **read** verbs are admitted by name (its `get`/`list`/`estimate`/`verify` forms) and the bare vendor prefix is **not**, so a future vendor-prefixed `setban` cannot be pre-authorised.
|
|
1151
|
+
6. **Anything else**: denied, with the message "not recognised as a read-only method; add it to server/rpc/allowlist.js if this is wrong".
|
|
1152
|
+
|
|
1153
|
+
Consequences worth knowing:
|
|
1154
|
+
|
|
1155
|
+
- The list is fixed in code, and no setting widens it.
|
|
1156
|
+
- `get*` and `list*` wallet reads (for example `getbalance`, `listunspent`) are allowed if the node has a wallet loaded.
|
|
1157
|
+
- `createrawtransaction` is allowed: it only builds an unsigned transaction and changes nothing.
|
|
1158
|
+
- `/api/config` → `allowlist` publishes a summary: the prefixes, the size of the deny list and the default decision.
|
|
1159
|
+
|
|
1160
|
+
Node writes never go through this endpoint. They are separate, opt-in actions (next section).
|
|
1161
|
+
|
|
1162
|
+
---
|
|
1163
|
+
|
|
1164
|
+
## 14. Node actions
|
|
1165
|
+
|
|
1166
|
+
A small, fixed set of node writes. They are off by default, each one must be listed explicitly, each is role-gated, needs a typed confirmation, and is audited.
|
|
1167
|
+
|
|
1168
|
+
| Action | RPC method | Minimum role | Args (body `args` keys) |
|
|
1169
|
+
|---|---|---|---|
|
|
1170
|
+
| `broadcast` | `sendrawtransaction` | operator | `hexstring`, `maxfeerate?` |
|
|
1171
|
+
| `savemempool` | `savemempool` | operator | — |
|
|
1172
|
+
| `testmempoolaccept` | `testmempoolaccept` | viewer | `rawtxs`, `maxfeerate?` |
|
|
1173
|
+
| `verifychain_l1` | `verifychain` | admin | fixed to `[2, 6]` (checklevel 2, depth 6) |
|
|
1174
|
+
|
|
1175
|
+
To enable them: `BLOCKYARD_ENABLE_ACTIONS=1` and `BLOCKYARD_ACTIONS=broadcast,testmempoolaccept` (or `actions.enabled` / `actions.allow`). With accounts off they also need `BLOCKYARD_ALLOW_WRITES_WITHOUT_AUTH=1`. Enabling actions in open mode without that setting is a fatal configuration error at startup.
|
|
1176
|
+
|
|
1177
|
+
### `GET /api/actions`
|
|
1178
|
+
|
|
1179
|
+
```json
|
|
1180
|
+
{
|
|
1181
|
+
"enabled": false,
|
|
1182
|
+
"allowed": [],
|
|
1183
|
+
"actions": [
|
|
1184
|
+
{ "name": "broadcast", "label": "Broadcast a raw transaction", "note": "Pushes a signed transaction ...",
|
|
1185
|
+
"args": [ "hexstring", "maxfeerate?" ], "requiredRole": "operator",
|
|
1186
|
+
"enabled": false, "permittedForYou": false, "method": "sendrawtransaction" }
|
|
1187
|
+
]
|
|
1188
|
+
}
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
### `POST /api/action`
|
|
1192
|
+
|
|
1193
|
+
Auth `any` + CSRF.
|
|
1194
|
+
|
|
1195
|
+
| Body field | Type | Meaning |
|
|
1196
|
+
|---|---|---|
|
|
1197
|
+
| `action` | string | Action name. |
|
|
1198
|
+
| `confirm` | string | Must equal `action` exactly. |
|
|
1199
|
+
| `args` | object | Named arguments from the table, for example `{ "hexstring": "0200..." }`. Names ending in `?` are optional. |
|
|
1200
|
+
|
|
1201
|
+
| Query | Meaning |
|
|
1202
|
+
|---|---|
|
|
1203
|
+
| `node` | Node id. |
|
|
1204
|
+
|
|
1205
|
+
The checks run in this order, each a `403 action_denied` unless noted:
|
|
1206
|
+
|
|
1207
|
+
1. unknown action;
|
|
1208
|
+
2. actions disabled;
|
|
1209
|
+
3. accounts off without the writes override;
|
|
1210
|
+
4. action not in the allow list;
|
|
1211
|
+
5. caller's role below the action's role;
|
|
1212
|
+
6. `confirm` missing or different: `400 confirm_required`.
|
|
1213
|
+
|
|
1214
|
+
```json
|
|
1215
|
+
{ "ok": true, "action": "testmempoolaccept", "method": "testmempoolaccept",
|
|
1216
|
+
"result": [ { "txid": "aaaa...", "allowed": true } ] }
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
If the node refuses, the answer is `200` with `{ "ok": false, "action", "method", "error": { "message", "code" } }`.
|
|
1220
|
+
|
|
1221
|
+
---
|
|
1222
|
+
|
|
1223
|
+
## 15. Accounts, sessions, users and audit
|
|
1224
|
+
|
|
1225
|
+
These routes are meaningful only with `BLOCKYARD_AUTH=1`. In open mode they answer as noted.
|
|
1226
|
+
|
|
1227
|
+
The user object returned by these routes (`publicUser`):
|
|
1228
|
+
|
|
1229
|
+
```json
|
|
1230
|
+
{ "username": "alice", "role": "operator", "id": "3f9a1c0b7d2e4a61", "disabled": false, "lastLoginAt": 1789150000000 }
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
### `POST /api/login`
|
|
1234
|
+
|
|
1235
|
+
Auth `none`, no CSRF. Body: `{ "username": "...", "password": "..." }` (JSON or form-encoded).
|
|
1236
|
+
|
|
1237
|
+
```json
|
|
1238
|
+
{ "ok": true, "user": { "username": "alice", "role": "operator", "id": "3f9a1c0b7d2e4a61", "disabled": false, "lastLoginAt": 1789150000000 },
|
|
1239
|
+
"csrf": "Zk2c...base64url" }
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
It also sets the `blockyard_sid` and `blockyard_csrf` cookies. Errors:
|
|
1243
|
+
|
|
1244
|
+
| Status | `code` | Meaning |
|
|
1245
|
+
|---|---|---|
|
|
1246
|
+
| 400 | — | Username or password missing. |
|
|
1247
|
+
| 401 | `bad_credentials` | Wrong password, unknown user or disabled account. The message is the same in all three cases. |
|
|
1248
|
+
| 403 | `accounts_disabled` | Open mode. |
|
|
1249
|
+
| 429 | `throttled` | The per-IP login bucket is empty. |
|
|
1250
|
+
| 429 | `locked` | Too many failures for this username or address. |
|
|
1251
|
+
|
|
1252
|
+
### `POST /api/logout`
|
|
1253
|
+
|
|
1254
|
+
Auth `any` + CSRF. Ends the current session and clears both cookies: `{ "ok": true }`. In open mode it answers `{ "ok": true, "accounts": false, "note": "accounts are off, so there is no session to end" }`.
|
|
1255
|
+
|
|
1256
|
+
### `POST /api/logout-all`
|
|
1257
|
+
|
|
1258
|
+
Auth `any` + CSRF. Revokes every session of the calling user: `{ "ok": true, "revoked": 3 }`. In open mode: `403`.
|
|
1259
|
+
|
|
1260
|
+
### `GET /api/me`
|
|
1261
|
+
|
|
1262
|
+
Auth `any`. Returns who you are, your sessions, and what you may do. With accounts on:
|
|
1263
|
+
|
|
1264
|
+
```json
|
|
1265
|
+
{
|
|
1266
|
+
"user": { "username": "alice", "role": "operator", "id": "3f9a1c0b7d2e4a61", "disabled": false, "lastLoginAt": 1789150000000 },
|
|
1267
|
+
"accounts": true,
|
|
1268
|
+
"sessions": [ { "createdAt": 1789150000000, "lastSeenAt": 1789155000000, "ip": "192.0.2.20",
|
|
1269
|
+
"userAgent": "Mozilla/5.0 ...", "current": true } ],
|
|
1270
|
+
"actions": [ "... as in /api/actions ..." ],
|
|
1271
|
+
"capabilities": { "canCallRpc": true, "canAct": false, "actionsEnabled": false,
|
|
1272
|
+
"allowedActions": [], "ceiling": "operator" }
|
|
1273
|
+
}
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
In open mode: `"accounts": false`, `"sessions": []`, a `note`, and `capabilities` with `"ceiling": "viewer"`, `"canAct": false` and `"writesRequireAccounts": true|false`.
|
|
1277
|
+
|
|
1278
|
+
### `POST /api/password`
|
|
1279
|
+
|
|
1280
|
+
Auth `any` + CSRF.
|
|
1281
|
+
|
|
1282
|
+
| Body field | Meaning |
|
|
1283
|
+
|---|---|
|
|
1284
|
+
| `current` | Your current password. Required when changing your own. |
|
|
1285
|
+
| `password` | The new password. |
|
|
1286
|
+
| `username` | Admins only: change another user's password (no `current` needed). |
|
|
1287
|
+
|
|
1288
|
+
Password rules: at least 12 characters (`auth.minPasswordChars`), must not contain the username, must not start with a common breach-corpus word, and must not be a single repeated character.
|
|
1289
|
+
|
|
1290
|
+
On success, every session of the target user is revoked:
|
|
1291
|
+
|
|
1292
|
+
```json
|
|
1293
|
+
{ "ok": true, "signedOut": true, "note": "sessions revoked; sign in again with the new password" }
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
A wrong `current` is `403`. A rule violation is `400`. In open mode: `403`.
|
|
1297
|
+
|
|
1298
|
+
### `GET /api/users`
|
|
1299
|
+
|
|
1300
|
+
Auth `admin`.
|
|
1301
|
+
|
|
1302
|
+
```json
|
|
1303
|
+
{
|
|
1304
|
+
"users": [ { "id": "3f9a1c0b7d2e4a61", "username": "alice", "role": "operator",
|
|
1305
|
+
"createdAt": 1789000000000, "updatedAt": 1789000000000, "lastLoginAt": 1789150000000,
|
|
1306
|
+
"disabled": false, "scheme": "scrypt", "kdf": { "N": 16384, "r": 8, "p": 1, "keylen": 32 } } ],
|
|
1307
|
+
"roles": [ "viewer", "operator", "admin" ]
|
|
1308
|
+
}
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
### `POST /api/users`
|
|
1312
|
+
|
|
1313
|
+
Auth `admin` + CSRF. Body: `{ "username", "password", "role"? }`. `role` defaults to `viewer`. Usernames are 2–31 characters of lowercase letters, digits, `.`, `-` and `_`.
|
|
1314
|
+
|
|
1315
|
+
```json
|
|
1316
|
+
{ "ok": true, "user": { "id": "3f9a1c0b7d2e4a61", "username": "alice", "role": "operator" } }
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
A validation failure is a `400` with the reason.
|
|
1320
|
+
|
|
1321
|
+
### `POST /api/users/generate`
|
|
1322
|
+
|
|
1323
|
+
Auth `admin` + CSRF. Body: `{ "username", "role"? }`. Creates the user with a generated 18-character password that is **shown exactly once**:
|
|
1324
|
+
|
|
1325
|
+
```json
|
|
1326
|
+
{ "ok": true, "user": { "id": "...", "username": "bob", "role": "viewer" },
|
|
1327
|
+
"password": "generated-once", "warning": "this password is shown once and is not stored in recoverable form" }
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
### `POST /api/users/:username/role`
|
|
1331
|
+
|
|
1332
|
+
Auth `admin` + CSRF. Body: `{ "role": "viewer" | "operator" | "admin" }`. Revokes the user's sessions.
|
|
1333
|
+
|
|
1334
|
+
```json
|
|
1335
|
+
{ "ok": true, "user": { "id": "...", "username": "bob", "role": "operator" } }
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
Demoting the last enabled admin is refused with `400`.
|
|
1339
|
+
|
|
1340
|
+
### `POST /api/users/:username/disabled`
|
|
1341
|
+
|
|
1342
|
+
Auth `admin` + CSRF. Body: `{ "disabled": true | false }`. Disabling revokes the user's sessions.
|
|
1343
|
+
|
|
1344
|
+
```json
|
|
1345
|
+
{ "ok": true, "user": { "id": "...", "username": "bob", "disabled": true } }
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
Disabling the last enabled admin is refused with `400`.
|
|
1349
|
+
|
|
1350
|
+
### `GET /api/audit`
|
|
1351
|
+
|
|
1352
|
+
Auth `admin`.
|
|
1353
|
+
|
|
1354
|
+
| Query | Default | Meaning |
|
|
1355
|
+
|---|---|---|
|
|
1356
|
+
| `limit` | `100` | 1–500. |
|
|
1357
|
+
|
|
1358
|
+
```json
|
|
1359
|
+
{
|
|
1360
|
+
"entries": [ { "at": 1789155000000, "type": "rpc", "ok": true, "username": "alice", "node": "main",
|
|
1361
|
+
"method": "getblockcount", "ms": 12, "ip": "192.0.2.20" } ],
|
|
1362
|
+
"limit": 100,
|
|
1363
|
+
"log": { "file": "audit.jsonl", "files": 1, "totalBytes": 20480, "currentBytes": 20480,
|
|
1364
|
+
"maxBytes": 8388608, "keep": 5, "rotations": 0, "rotationError": null, "headroomBytes": 8368128 }
|
|
1365
|
+
}
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
The file rotates at 8 MB and keeps 5 files (`store.auditMaxBytes`, `store.auditKeep`). Entry `type` values: `login`, `login-throttled`, `logout`, `logout-all`, `kdf-upgrade`, `rpc`, `rpc-denied`, `action`, `action-denied`, `user-create`, `user-role`, `user-disabled`, `password-change`, `csrf-rejected`, `config-node`. Passwords are never recorded.
|
|
1369
|
+
|
|
1370
|
+
---
|
|
1371
|
+
|
|
1372
|
+
## 16. Telemetry and configuration
|
|
1373
|
+
|
|
1374
|
+
### `GET /api/telemetry`
|
|
1375
|
+
|
|
1376
|
+
The monitor's own health: process, RPC client, log tail, stream clients and the audit file.
|
|
1377
|
+
|
|
1378
|
+
```json
|
|
1379
|
+
{
|
|
1380
|
+
"self": { "t": 1789155700000, "rssMb": 175.6, "heapMb": 42.8, "sseClients": 5, "usersActive": 0,
|
|
1381
|
+
"cpuPct": 2.71, "eventRate": 0, "build": "0.0.9-fd620adc52" },
|
|
1382
|
+
"nodes": [
|
|
1383
|
+
{ "id": "main",
|
|
1384
|
+
"rpc": { "nodeId": "main", "url": "http://127.0.0.1:8332", "cookieSource": "...", "online": true,
|
|
1385
|
+
"lastGoodAt": 1789155699000, "lastError": null, "breakerOpen": false, "breaker": {},
|
|
1386
|
+
"queued": 0, "calls": 12345, "batches": 2345, "methods": {}, "errors": 3, "timeouts": 0,
|
|
1387
|
+
"authRetries": 0, "breakerTrips": 1, "lastLatencyMs": 8, "avgLatencyMs": 40,
|
|
1388
|
+
"maxLatencyMs": 4200, "ratePerSec": 1.8, "busyMsPerSec": 90, "staleDropped": 0 },
|
|
1389
|
+
"log": { "exists": false },
|
|
1390
|
+
"tiers": { "fast": 1789155699000 },
|
|
1391
|
+
"lastTier": {},
|
|
1392
|
+
"history": { "mempool": { "points": 5000, "firstAt": 1789000000000, "lastAt": 1789155699000,
|
|
1393
|
+
"unattributed": 0, "nodes": [ "main" ] } } }
|
|
1394
|
+
],
|
|
1395
|
+
"sse": { "clients": 1, "perClient": [ { "id": 1, "user": "anonymous", "seconds": 1666, "kb": 243531,
|
|
1396
|
+
"dropped": { "snapshot": 0, "series": 0, "events": 0 }, "node": "main" } ] },
|
|
1397
|
+
"audit": { "file": "audit.jsonl", "files": 1, "totalBytes": 20480, "...": "as in /api/audit log" }
|
|
1398
|
+
}
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
### `GET /api/config`
|
|
1402
|
+
|
|
1403
|
+
The effective, non-secret configuration and the access posture.
|
|
1404
|
+
|
|
1405
|
+
```json
|
|
1406
|
+
{
|
|
1407
|
+
"poll": { "fastMs": 4000, "midMs": 15000, "poolMs": 20000, "slowMs": 60000, "rareMs": 900000, "blockBackfill": 30 },
|
|
1408
|
+
"rpc": { "maxInFlight": 1, "minIntervalMs": 250, "maxRatePerSec": 4, "timeoutMs": 90000 },
|
|
1409
|
+
"allowlist": { "denyExactCount": 68, "allowPrefixes": [ "analyzepsbt", "convertbits", "..." ],
|
|
1410
|
+
"denyPrefixes": [ "generate", "..." ], "defaultDecision": "deny" },
|
|
1411
|
+
"actions": { "enabled": false, "allow": [] },
|
|
1412
|
+
"retention": { "hours": 72, "ringCapacity": 20000, "events": 5000 },
|
|
1413
|
+
"access": { "mode": "open", "anonymous": true, "role": "viewer", "writesAllowed": false },
|
|
1414
|
+
"log": { "enabled": false },
|
|
1415
|
+
"sources": [ { "panel": "sync bar", "source": "getblockchaininfo blocks/headers + verificationprogress",
|
|
1416
|
+
"note": "kept as two separate figures (rule 9)" } ]
|
|
1417
|
+
}
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
With accounts on, `access` is `{ "mode": "accounts", "anonymous": false }`. `sources` states which data source backs each dashboard panel in the current mode (log tail on or off). The numbers above are the shipped defaults.
|
|
1421
|
+
|
|
1422
|
+
### `POST /api/config/node/test` and `POST /api/config/node`
|
|
1423
|
+
|
|
1424
|
+
The Node connection form on Node & RPC. Auth `any` + CSRF; with accounts on, both need the `admin` role. Both take the same body and accept **only four fields** — `rpcUrl` (required, `http(s)://host:port`), `datadir`, `chainHint` (default: the current node's, else `main`) and `label`. `rpcUser`, `rpcPassword` and `cookieFile` are refused: authentication is the datadir cookie, and a password is not taken over an endpoint that is open by default. Without a `datadir` (given or already configured) and with no `cookieFile` configured, the answer is `400 need_datadir`; a bad URL is `400 bad_rpc_url`.
|
|
1425
|
+
|
|
1426
|
+
`/test` writes nothing. It probes `getblockchaininfo` with a throwaway client on its own lane (timeout at most 8 s). **Credentials go to one endpoint only**: the cookie is sent when `rpcUrl` equals the endpoint this monitor is already configured for, and to any other address the probe carries no `Authorization` header at all — a `401` from a new endpoint is reported as `ok: true, reachable: true, authenticated: false` with a `note`, since it proves an RPC server answered.
|
|
1427
|
+
|
|
1428
|
+
```json
|
|
1429
|
+
{ "ok": true, "ms": 41, "authenticated": true, "chain": "main", "blocks": 966546, "ibd": false }
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
A failed probe is `200` with `{ "ok": false, "ms", "authenticated", "error": { "message", "kind", "code" } }`.
|
|
1433
|
+
|
|
1434
|
+
`/api/config/node` needs `"confirm": "save"` in the body (`400 confirm_required` otherwise) and merges the four fields onto `nodes[0]` of the configuration file, leaving every other key as it was; the file is written `0600` by temporary file, fsync and rename. A process started with `BLOCKYARD_CONFIG=none` has nowhere to save and answers `409 no_config_file`. The write is audited as `config-node`.
|
|
1435
|
+
|
|
1436
|
+
```json
|
|
1437
|
+
{ "ok": true, "file": "/path/to/config/local.json", "restartRequired": true,
|
|
1438
|
+
"envOverrides": [ "BLOCKYARD_NODE_URL" ],
|
|
1439
|
+
"note": "saved, but this process takes its node from BLOCKYARD_NODE_URL, which the environment sets and which beats the file — ..." }
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
`envOverrides` lists the node environment variables currently set, because the environment is applied after the file and a restart would keep the old endpoint.
|
|
1443
|
+
|
|
1444
|
+
### `GET /api/settings` and `POST /api/settings`
|
|
1445
|
+
|
|
1446
|
+
The Display settings, kept on the server in `config/blockyard.json` (beside the configuration file) so every browser sees the same choices. The server stores the blob and nothing else: it has no schema, and `public/js/settings.js` clamps every value on the way in.
|
|
1447
|
+
|
|
1448
|
+
`GET` is auth `any`. Nothing saved yet is the normal first-run answer, not an error:
|
|
1449
|
+
|
|
1450
|
+
```json
|
|
1451
|
+
{ "settings": { "version": 3, "space": { "...": "..." }, "...": "..." }, "file": "/path/to/config/blockyard.json", "stored": true }
|
|
1452
|
+
```
|
|
1453
|
+
|
|
1454
|
+
With no file: `{ "settings": null, "file": "...", "stored": false }`; an unreadable file adds `"error": "unreadable: ..."`.
|
|
1455
|
+
|
|
1456
|
+
`POST` is auth `any` + CSRF, `admin` with accounts on. Body: `{ "settings": { ... } }` (an object; anything else is `400 bad_settings`). The serialised text is capped at 256 KB (`413 too_large`), and the file is written `0600` by temporary file, fsync and rename. Reply: `{ "ok": true, "file": "...", "bytes": 2431 }`.
|
|
1457
|
+
|
|
1458
|
+
---
|
|
1459
|
+
|
|
1460
|
+
## 17. The event stream: `/api/stream`
|
|
1461
|
+
|
|
1462
|
+
Server-Sent Events over plain HTTP: one-way, node to browser, with no WebSocket and no dependency. A browser `EventSource` reconnects on its own.
|
|
1463
|
+
|
|
1464
|
+
### Connecting
|
|
1465
|
+
|
|
1466
|
+
```
|
|
1467
|
+
GET /api/stream?node=main
|
|
1468
|
+
Accept: text/event-stream
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
- **Auth.** Open mode: anyone. Accounts on: a valid `blockyard_sid` cookie, otherwise `401` `{ "error": { "message": "authentication required", "kind": "auth" } }`.
|
|
1472
|
+
- **Limit.** Each connection attempt spends one token from the stream bucket (section 1). An empty bucket answers `429` `{ "error": { "message": "too many streams", "kind": "ratelimited" } }`.
|
|
1473
|
+
- **Node.** `?node=<id>` scopes the stream to one node. An unknown id is refused with `404` (`kind: "unknown_node"`), so a client pointed at a removed node gets a visible failure instead of a silent stream. Only `node` works here, not `nodeId`.
|
|
1474
|
+
- **Headers.** `Content-Type: text/event-stream; charset=utf-8`, `Cache-Control: no-cache, no-store, must-revalidate`, `Connection: keep-alive`, `X-Accel-Buffering: no` (so nginx does not buffer the stream).
|
|
1475
|
+
|
|
1476
|
+
**Always pass `?node=`.** `series` and `events` frames are delivered only to streams scoped to the node they belong to. A stream opened without `?node=` receives `snapshot` frames for every node, but no `series` or `events` frames.
|
|
1477
|
+
|
|
1478
|
+
### Frames
|
|
1479
|
+
|
|
1480
|
+
The stream opens with the comment line `: stream open`. After that:
|
|
1481
|
+
|
|
1482
|
+
| Event | `id:` | Cadence | `data` |
|
|
1483
|
+
|---|---|---|---|
|
|
1484
|
+
| `snapshot` | the global `seq` | at most 1/s per node, sent when a collector tier finishes | The same object as `GET /api/state` without `app` and `user`, and with the default series windows included. It is large: hundreds of KB with series. |
|
|
1485
|
+
| `series` | — | every 20 s while any client is connected | `{ "node": "main", "series": { "mempool": { "hour": [ {"t","v"} ], "...": "..." }, "...": "..." } }` (the windows in section 5) |
|
|
1486
|
+
| `events` | — | batched as they occur | An array of event rows, same shape as `/api/events` rows. Routine `raw`/`info` chatter is filtered out. |
|
|
1487
|
+
| comment `: ping` | — | every 15 s | Heartbeat. There is no payload. |
|
|
1488
|
+
|
|
1489
|
+
Example wire format:
|
|
1490
|
+
|
|
1491
|
+
```
|
|
1492
|
+
: stream open
|
|
1493
|
+
|
|
1494
|
+
id: 406
|
|
1495
|
+
event: snapshot
|
|
1496
|
+
data: {"id":"main","label":"Bitcoin Core","online":true,"chain":"main","tip":{"height":966546,...},"seq":406,...}
|
|
1497
|
+
|
|
1498
|
+
event: events
|
|
1499
|
+
data: [{"seq":194640,"source":"monitor","kind":"tip_jump","severity":"info","ts":1789155700000,"text":"...","node":"main"}]
|
|
1500
|
+
|
|
1501
|
+
: ping
|
|
1502
|
+
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
### Backpressure and reconnects
|
|
1506
|
+
|
|
1507
|
+
- **Backpressure.** Each client has at most one pending `snapshot` and one pending `series`, and the newest replaces an unsent older one. The event batch is capped: past 800 queued rows, the oldest 400 are dropped. `/api/telemetry` → `sse.perClient[].dropped` counts what was skipped. A slow client sees a coarser stream, not a growing queue.
|
|
1508
|
+
- **Reconnects.** The server sends no `retry:` field, so clients use their default delay (about 3 s in browsers). `Last-Event-ID` is not replayed. After a reconnect, the next `snapshot` is the complete current state, and `/api/events?since=<last seq>` fills any gap in the event feed.
|
|
1509
|
+
- **Dead connections.** A client socket that has closed is removed on the next write or heartbeat.
|
|
1510
|
+
|
|
1511
|
+
---
|
|
1512
|
+
|
|
1513
|
+
## 18. End-to-end examples
|
|
1514
|
+
|
|
1515
|
+
Get the current state of the primary node, without chart series:
|
|
1516
|
+
|
|
1517
|
+
```sh
|
|
1518
|
+
curl -s 'http://127.0.0.1:21000/api/state?series=none' | jq '{height: .tip.height, sync: .sync.state, mempool: .mempool.count}'
|
|
1519
|
+
```
|
|
1520
|
+
|
|
1521
|
+
Look up a transaction, first through search, then as a full page:
|
|
1522
|
+
|
|
1523
|
+
```sh
|
|
1524
|
+
curl -s 'http://127.0.0.1:21000/api/x/search?q=9891f72bea4bc27b8dd3990295be331e5e9ae69bc152c5ad31862efbb624ae69'
|
|
1525
|
+
# {"ok":true,"type":"tx","id":"9891f72b..."}
|
|
1526
|
+
|
|
1527
|
+
curl -s 'http://127.0.0.1:21000/api/x/tx?txid=9891f72bea4bc27b8dd3990295be331e5e9ae69bc152c5ad31862efbb624ae69' \
|
|
1528
|
+
| jq '{ok, fee: .tx.fee, feerate: .tx.feerate, height: .tx.height, usd}'
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
Stream live updates (`-N` turns off curl's buffering):
|
|
1532
|
+
|
|
1533
|
+
```sh
|
|
1534
|
+
curl -sN 'http://127.0.0.1:21000/api/stream?node=main' | grep --line-buffered '^event:'
|
|
1535
|
+
```
|
|
1536
|
+
|
|
1537
|
+
Query market depth. The first call starts exchange polling, and books appear within about 30 s:
|
|
1538
|
+
|
|
1539
|
+
```sh
|
|
1540
|
+
curl -s 'http://127.0.0.1:21000/api/markets' > /dev/null
|
|
1541
|
+
sleep 35
|
|
1542
|
+
curl -s 'http://127.0.0.1:21000/api/markets/depth?ago=300' \
|
|
1543
|
+
| jq '{mid, p0, step, n, coinbase: (.exchanges[] | select(.id=="coinbase") | {bestBidDepth: (.bids | map(select(. != null)) | last), error})}'
|
|
1544
|
+
```
|
|
1545
|
+
|
|
1546
|
+
Call a read-only RPC method (open mode):
|
|
1547
|
+
|
|
1548
|
+
```sh
|
|
1549
|
+
curl -s -X POST -H 'Content-Type: application/json' \
|
|
1550
|
+
-d '{"method":"getblockchaininfo"}' http://127.0.0.1:21000/api/rpc | jq '.result.blocks'
|
|
1551
|
+
```
|
|
1552
|
+
|
|
1553
|
+
With accounts on: sign in, keep the cookie, and send the CSRF token on mutating calls:
|
|
1554
|
+
|
|
1555
|
+
```sh
|
|
1556
|
+
B=http://127.0.0.1:21000
|
|
1557
|
+
CSRF=$(curl -s -c jar.txt -H 'Content-Type: application/json' \
|
|
1558
|
+
-d '{"username":"alice","password":"a long passphrase here"}' $B/api/login | jq -r .csrf)
|
|
1559
|
+
|
|
1560
|
+
curl -s -b jar.txt "$B/api/me" | jq .user
|
|
1561
|
+
curl -s -b jar.txt -X POST -H 'Content-Type: application/json' -H "X-CSRF-Token: $CSRF" \
|
|
1562
|
+
-d '{"method":"getmempoolinfo"}' "$B/api/rpc" | jq .result.size
|
|
1563
|
+
curl -sN -b jar.txt "$B/api/stream?node=main"
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
Poll the event feed incrementally:
|
|
1567
|
+
|
|
1568
|
+
```sh
|
|
1569
|
+
SEQ=0
|
|
1570
|
+
while sleep 10; do
|
|
1571
|
+
R=$(curl -s "http://127.0.0.1:21000/api/events?since=$SEQ&severity=warn,error")
|
|
1572
|
+
echo "$R" | jq -r '.events[] | "\(.ts) \(.severity) \(.text)"'
|
|
1573
|
+
SEQ=$(echo "$R" | jq .maxSeq)
|
|
1574
|
+
done
|
|
1575
|
+
```
|