Commit Graph
18 Commits
Author SHA1 Message Date
grenadeandClaude Opus 5.5 6b3bf76b7e feat: probe whether 1Click will quote QTC, against what could have filled it
ci / gate (push) Successful in 4m40s
deploy / validate (push) Skipped
deploy / build (push) Successful in 9m23s
deploy / deploy-web (push) Successful in 10s
deploy / deploy-api (push) Successful in 24s
On NEAR Intents a price exists only when a solver chooses to quote, and
nothing on-chain records a refusal. Every minute the observer now asks
1Click for a keyless dry quote on QTC pairs only, both ways, against
native USDC and Ethereum USDC: the smallest size each minute and the
full grid each quarter hour, averaging about 4 requests a minute against
a published limit of 1,200. At the same moment it reads two things from
its own nodes: the Ref pool's quote for the same input, and every
solver's balance of the output token inside intents.near.

So each "No liquidity available" is stored with whether one solver
alone held enough to settle the trade. A probe that got no answer is
recorded as `failed`, never as a refusal. Only our own observations are
kept: the 1ClickSwap API Terms (s4.1(e)) forbid keeping API data past 24
hours, so a quote becomes a markup against the pool on arrival and raw
responses are pruned hourly.

The first live round, 2026-10-07: "No liquidity available" both ways.
Selling 1 QTC, the pool would have paid 101.53 USDC and three solvers
each held enough. Buying with 100 USDC, solver-ref-2.near held 2,308
QTC.

The near-intents market page shows a strip per direction and counter
token. It is an emphasis chart: the accent marks only "no quote while a
solver could fill", the other states are quiet ink told apart by
height, and the neutral token is validated against the accent in both
themes. Beside it are a summary table and a list of the latest
refusals. The pause log and the price-move statistics in #45 come later,
once there is history to support them.

Refs #45

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tyzzRVjH9AdwGtYwsH5VC
2026-10-07 13:39:52 +03:00
grenadeandClaude Opus 5.5 b3dc4ef4f6 feat: list Ref's classic exchange for pools holding the asset
ci / gate (push) Successful in 4m7s
deploy / validate (push) Skipped
deploy / build (push) Failing after 11m44s
deploy / deploy-api (push) Skipped
deploy / deploy-web (push) Skipped
#39 item 2. Every pool on v2.ref-finance.near is listed every ten
minutes — nine thousand-pool pages, three seconds on quadbrat, which
tracks the exchange's shard and is asked first while at the tip — and
each pool naming the asset is reported on the Ref market page, empty or
not. Today that is pool 8900, the empty QTC/QTEST test pool #39 found by
hand. A listing that fails part way publishes nothing.

The exchange's own balance of the asset joins the supply reconciliation
as a named holder, so a venue opening there is never "unaccounted".

Refs #39

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-06 19:13:08 +03:00
grenadeandClaude Opus 5.5 1d25689e3d feat(data): ask our own NEAR node for view calls while it is at the tip
FastNEAR kept answering 429 after the pacer shipped: its limit is per
address, and every host behind the office IP shares it. gargravarr tracks
the pool's and the token's shards, so `pool_rpc_urls` now also serves
view calls first — pool state, order book, quotes, supply, balances — but
only while its newest block is under two minutes old and it is not
syncing, re-checked every 30 s. Behind the tip it would answer `final`
from its own final block: on 2026-10-06, 11,536 blocks behind, it gave
qtc.omft.near's supply as of three hours earlier, which would have
replaced "unreadable" with a figure that looked current and was not. Any
call it cannot answer falls back to the public endpoint.

Refs #39

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-06 18:17:11 +03:00
grenadeandClaude Opus 5.5 8eb812a19a feat: read the NEAR bridge from the Quantus side — custody against minted
`/v1/chains/{chain}/bridge` sets the bridge's custody account against
qtc.omft.near's supply on NEAR, and lists every movement through custody,
newest first and paged. The bridge's two accounts, custody and the fee payer
that funds each deposit, are named in `[chains.bridge]` with the evidence
beside them and validated as addresses at startup; neither is inferred.

`blackbeard_core::bridge::classify` tells a deposit by who and when: a
transfer into custody from an address the fee payer funded at most
`max_sweep_lag` blocks before (every mainnet deposit so far: one). Not by
amount — the funding varies (0.0010166025, 0.0010169025 before the 150k
sweep, 0.0010168025 before the 5,990) — and not by one-time addresses,
which #39 assumed: one address deposited three times. Anything else into
custody is an other inflow; locked is custody's balance less those, which
derives the 5 QTC seed instead of configuring it. Tested against every real
transfer on both accounts.

Each transfer is counted once. Every ordinary transfer emits both
`Balances::Transfer` and `Wormhole::NativeTransferred`; the query takes the
first, plus the second only for a hidden-sender exit.

Verified end to end against the live chain (custody balance read from bob)
and NEAR (the listing's supply): locked 155,994.701, minted 155,994.698,
locked − minted +0.003 QTC; 13 deposits from 11 addresses, 3 withdrawals.

The market page shows it beside the backing ceiling, reporting flows and
saying what they cannot establish: an address is not a person, and summed
flows are only the whole history if the index covers it.

Refs #39

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-06 17:49:05 +03:00
grenadeandClaude Opus 5.5 e78e627152 feat(api): read direct calls to the asset's pools on the shards they are signed on
ci / gate (push) Successful in 4m31s
deploy / validate (push) Skipped
deploy / build (push) Successful in 9m14s
deploy / deploy-api (push) Successful in 31s
deploy / deploy-web (push) Successful in 14s
The relay's pool swaps are found on intents.near's shard; a whitelisted
account calling the pool itself signs on its own shard, which nothing read —
#37's coverage figure measured the gap at 99.75% of buys. `poolscan` follows
the pools' and the token's shards on gargravarr (`pool_rpc_urls`), which
tracks shards 4 and 8, and queues every transaction that calls a pool or
`ft_transfer_call`s tokens into one. `refdcl::fetch` reads each whole, as it
does the relay's, so a swap is parsed one way and recorded once whichever
road it came by.

Reads only what the node holds; history before it synced stays the coverage
figure's to account for. Records the three-node shard split and the
Prometheus neard scrape config (installed on golgafrinchans) in the repo.

Refs #39

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-06 16:38:21 +03:00
grenadeandClaude Opus 5.5 b05b208dfa feat(config): follow QTC on NEAR Intents through our own node
deploy / build (push) Successful in 9m35s
deploy / deploy-web (push) Successful in 5s
deploy / deploy-api (push) Successful in 22s
Enables [chains.intents] for quantus: asset nep141:qtc.omft.near, read from
https://near.internal (neard on frankie, tracking intents.near's shard), with
view calls left on FastNEAR. start_height is the oldest block our node holds,
218,346,736 — a sound floor because qtc.omft.near held no supply at
218,369,028, so nothing can have traded before it, and it keeps the whole
backfill on our node rather than on a throttled archive.

The gate test that kept this off becomes a parse test that expects it on.

Closes #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-03 18:35:32 +03:00
grenadeandClaude Opus 5.5 d40e17bb1f docs(config): point the intents example at near.internal
Refs #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-03 18:35:32 +03:00
grenadeandClaude Opus 5.5 d2567e5584 fix(data): send NEAR view calls to their own endpoints
The node the scan will follow tracks only intents.near's shard, and the token
contracts it asks about — qtc.omft.near for supply, every counter token for
ft_metadata — live on other shards, which such a node cannot answer for.
Views are a few an hour, so they go to `view_urls`, FastNEAR by default, and
the per-block reads stay on `rpc_urls`.

Refs #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-03 18:35:32 +03:00
grenadeandClaude Opus 5.5 8128201ad9 feat(api): index swaps settled by NEAR Intents, off by default
The price of a token on NEAR Intents is the ratio inside a trade that
`intents.near` actually executed; there is no pool, oracle or stored price,
and a 1Click quote is an offer. This reads `execute_intents` transactions on
the contract's shard, keeps the `token_diff` legs whose receipt succeeded and
whose intent is in `intents_executed`, and records one trade per asset/counter
pair per outcome, counting each unit once whichever side the taker was on.

- core: event parsing, trade building, shard lookup, price and VWAP, tested
  against the logs of mainnet tx 85uy9uCF (USDT on Tron for ZEC).
- data: a NEAR client over the existing RpcClient, live then archival;
  migration 0009 (intents_trade, intents_scan, near_token).
- api: a contiguous-cursor scan and a supply/listing poll per chain, and
  GET /v1/chains/{chain}/intents/{swaps,price}. Prices are in counter-token
  units; no USD until a stablecoin is named by contract.

Gated on `[chains.intents]`, which the config template leaves commented out:
FastNEAR's free RPC sustains about two requests a second against ~1.7 needed
to follow the tip, and neardata allows 30 blocks a minute. It waits for our
own node. A test renders the template and fails if the block is enabled.

Verified end to end against mainnet with ZEC configured: the trade at NEAR
218,261,895 was recorded and served by both routes.

Refs #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-03 18:35:32 +03:00
grenadeandClaude Opus 5.5 dc68fe3be3 infra: publish the NEAR node as near.blackbeard.observer
Through the existing nodes role: a Cloudflare CNAME to the site indirection,
a Let's Encrypt cert by DNS-01 on oolon, and a vhost beside the Quantus and
Planck endpoints. The upstream is https://near.internal rather than frankie,
so next week's move to new hardware changes one internal vhost and nothing
here, and frankie's firewall still admits only the hanzalova proxy and bob.

Throttled harder than the Quantus endpoint, because the node is a four-core
stopgap and the swap indexer reads it too (via near.internal, outside these
limits): 5 r/s per client with a burst of 20, 8 connections per client, and a
30 r/s ceiling across all clients so many addresses together cannot starve
it. Only POST / and GET /status, /health pass; bodies over 64 KiB are refused.
Hetzner and OVH hosting networks get the site's 403, and the name has its own
JSON log.

Checked from outside after install: 405 for other methods and paths, 413 for
a 100 KB body, 32 of 40 parallel requests answered 429, and the log carries
the real client address. /health answers 503 because the node is not synced;
that is neard's own answer, passed through.

Closes #29

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
2026-10-03 12:34:35 +03:00
grenadeandClaude Opus 5 6d8c3fe3ea infra: a dedicated access log for the public vhost, and deny hosting networks
deploy / build (push) Successful in 8m30s
deploy / deploy-web (push) Successful in 7s
deploy / deploy-api (push) Successful in 18s
The proxy's shared access.log mixes every site on oolon and records neither
Host nor Origin. Origin is the only field that separates our UI's WebSocket
from another site's, because browsers send no Referer on the upgrade. The
public vhost now writes its own JSON-lines log carrying both.

Ten days of the shared log showed no third-party site using the API, and two
headless pollers: an OVH host polling summary once a minute (88% of all
no-referer REST) and a Hetzner host identifying as `stardust-devops`. The
public vhost now refuses the hosting networks of both providers, from the
prefixes RIPEstat reports as announced by AS24940, AS213230, AS212317 and
AS16276, generated by script/deny-networks.py and committed so the list is
reviewed. AS35540 is OVH's residential broadband and is left out. Against the
same ten days, the list matches the two pollers and no visitor.

Refusals are a plain-text 403 and are still logged with "denied":1, so a VPN
user caught by it shows up rather than disappearing.

Closes #24

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4Ldjix2ofMdQ7dzqgnYA8
2026-09-22 15:03:12 +03:00
grenadeandClaude Fable 5.1 54db950957 infra: public node endpoints quantus. and planck.blackbeard.observer
deploy / build (push) Successful in 10m25s
deploy / deploy-web (push) Failing after 3s
deploy / deploy-api (push) Failing after 5s
One name per chain for the blackbeard wallet (blackbeard/wallet #53), on
the same edge proxy as the site: a CNAME to the site indirection, a Let's
Encrypt certificate, and a vhost that proxies JSON-RPC over HTTP and
WebSocket to the chain's upstream. quantus_node is our node on bob across
the mesh (bob's firewalld now admits oolon by name, lair/quantus deploy);
planck_node is Quantus's two official testnet hosts over TLS with SNI and
verification, since we run no testnet node. Upstreams and the per-client
throttle zones (20 requests a second with a burst of 40, 16 concurrent
connections, 429 on excess) live in conf.d/blackbeard-nodes.conf so each
vhost stands alone.

script/infra-setup.sh gains a nodes role that does the three steps per
name, enables the vhosts only after nginx -t passes and disables them
again if it does not, and ends by asking each name for system_chain.
Run today: both names carry certificates to 2026-12-15 and Planck
answers; the quantus record was still propagating when the script asked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW
2026-09-16 09:11:11 +03:00
grenadeandClaude Opus 5 db9ee142bc config: give mainnet the two public archive nodes as fallbacks
deploy / build (push) Successful in 8m10s
deploy / deploy-web (push) Successful in 4s
deploy / deploy-api (push) Successful in 16s
Mainnet was the only chain left on a single endpoint, and that endpoint is our
own node — so a restart on bob took the flagship chain off the site while both
testnets, on two endpoints each, stayed up.

Our node stays first, and has to: loopback pays no network hop, and `127.0.0.1/32`
is the only address whitelisted against its `--rpc-rate-limit 300`. Behind it are
`rpc1-` and `rpc2-mainnet.quantus.com`, both of which answer `archive_v1_*`,
report the same genesis and the same runtime, and accept a `wss://` upgrade.

They took some finding, which is recorded in the template's comment so the next
person does not repeat the search: mainnet uses a different domain *and* a
different prefix from the testnets, its bootnodes serve only p2p on 30333, and
the official explorer talks to a GraphQL indexer rather than to any node at all.

This buys availability, not depth. Failover moves on a transport failure, and a
pruned node answers `{"result": null}` — a success, so a deep state read would
stop at it rather than fall through to a peer that has the answer. Filed as #9.

Closes #8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-10 11:38:10 +03:00
grenadeandClaude Opus 5 1a59b2d398 feat: several endpoints per chain, so one host cannot stop it
deploy / build (push) Successful in 8m19s
deploy / deploy-web (push) Successful in 4s
deploy / deploy-api (push) Successful in 16s
A chain entry held exactly one `rpc_url` and one `ws_url`, and when that host
went down everything for that chain stopped — headers, difficulty, backfill,
state reads, runtime discovery. The endpoints already existed in pairs:
`a2-planck` and `a2-heisenberg` both answer and neither was configured. We were
choosing single points of failure the network went to some trouble to avoid.

`rpc_urls` and `ws_urls` are lists now, and `rpc_url`/`ws_url` still work as a
one-element list — a config naming one endpoint is still a valid config, and
making every deployment rewrite its entry would be this feature breaking the
thing it exists to make reliable.

Endpoints are stuck to rather than balanced across, which is the design and not
laziness: a storage read at an old block hash needs a node that still holds that
block's state, and nodes prune on their own schedules, so alternating would
return a mixture of answers and absences that reads as sparse data rather than a
configuration problem. The cursor is shared across clones so a failover one task
finds is not rediscovered by every other task on the chain.

Failing over on the wrong thing was the trap worth avoiding. A JSON-RPC error is
the node answering — moving on `count exceeds maximum value` would hide a
caller's mistake behind a second node making the same complaint — so a new
`Malformed` variant separates "did not answer" from "answered, with an error".
A pruned block returns `{"result": null}`, a success, and never looks unhealthy.

The WebSocket rotates at reconnect, where the loop already was; racing
subscriptions across endpoints and deduplicating heads buys nothing, since heads
are a liveness signal and ingest fills gaps against `chain_getBlockHash` anyway.

Verified live with a dead endpoint configured first: Planck stayed `full` at its
real height, the RPC logged one `failed over` with from and to, and the head
subscription logged the loss with the endpoint count beside it — because "the
chain is unreachable" and "one of three endpoints is unreachable" are different
operational facts and used to look identical.

Closes #7

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-10 11:17:03 +03:00
grenadeandClaude Opus 5 27a4f30304 feat: index Planck
deploy / build (push) Waiting to run
deploy / deploy-api (push) Has been cancelled
deploy / deploy-web (push) Has been cancelled
`a1-planck.quantus.cat` answers, and has all along. The config comment claimed
Planck "publishes no RPC endpoint we can find" — true when written, and never
rechecked after the node we ran for it was switched to mainnet. A million blocks
of history and 2.38M zk-tree leaves were sitting there unasked for.

Measured before committing to it. Three chains backfilling at once cost 4.8% of
one core, five of eight Postgres connections with one active, and no movement in
API latency — summary 7 ms p95, block 13 ms, account 49 ms. The walk is bound by
RPC round trips rather than by anything local, so it needs no deployable of its
own; and `backfill` only reads in-memory state while writing solely to Postgres,
so it contends with nothing the API serves.

Unthrottled deliberately: ~46 hours at roughly 20 requests a second against a
public node. Leaning on public decentralised testnet infrastructure exercises
what it is there for.

Restart resilience confirmed by killing the process mid-walk — `event_scan.low`
stayed put rather than jumping back to the tip. That is load-bearing here rather
than tidy, because the cert-rotation path restarts this service several times a
day and a cursor in memory would mean Planck never finishing.

`a2-planck` answers too, and is noted for when a chain can hold more than one
endpoint.

Closes #6

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-10 11:10:03 +03:00
grenadeandClaude Opus 5 2cb8f1cf21 config: the node on bob is mainnet now, not Planck
deploy / build (push) Successful in 7m34s
deploy / deploy-web (push) Successful in 5s
deploy / deploy-api (push) Successful in 14s
Mainnet launched today and the node was switched to it. The observer reads
whatever the node is on, so the `[[chains]]` entry pointing at 127.0.0.1:9944
had to follow it — until now the daemon was filing mainnet's blocks under the id
`planck`, with Planck's genesis and PLK still attached to them.

Read from the node, not assumed: `system_chain` "Quantus", genesis
`0xfb5487c0…626fba`, token QTC. `mainnet = true` is asserted here rather than
inferred, which is the field's whole purpose — telemetry also carries a chain
calling itself "Quantus Staging Mainnet", and it is not this one.

The target block time is **12 s, not the 6 s Planck used**.
`TARGET_BLOCK_TIME_MS` is 12_000 in the runtime the node actually runs —
checked at commit b017e642, the `0.11.1-b017e6420aa` its `system_version`
reports, rather than at the tip of the chain repo. It is the denominator for
every hashrate published before twenty tip samples exist, which on a chain
hours old is all of them, so carrying 6.0 across would have put out a mainnet
hashrate at exactly twice its true value — labelled "nominal", not broken.

Planck keeps its 268 telemetry nodes and stays in the site's navigation as
`no_endpoint`: still listed, no longer navigable, because authorship comes from
block headers and we no longer run a node that serves Planck's. That is an
ordinary state here, not an outage.

Nothing about the node itself is touched by this; every command run against it
was a read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-09 12:07:21 +03:00
grenadeandClaude Opus 5 5f3aac2a13 feat(chains): discover every Quantus chain from telemetry; track the ones we can reach
deploy / build (push) Failing after 5s
deploy / deploy-api (push) Has been skipped
deploy / deploy-web (push) Has been skipped
The chain list is no longer config. substrate-telemetry announces every chain it
knows — with live node counts — before and independent of any subscription, so
one feed connection that never subscribes is a complete, self-maintaining index.
A chain that launches tomorrow appears in the nav on its own.

Config now contributes only RPC endpoints. Authorship lives in the `pow_` digest
of a block *header*, and telemetry publishes hashes and heights but never
headers — so a chain with no endpoint is listed with its node count and is not
navigable, and the nav says why rather than offering an empty board. The two
sets join on genesis hash, never on name.

Two chains are tracked in full: Planck (our own node on bob) and Heisenberg
(a1-heisenberg.quantus.cat, found via Quantus-Network/quantus-apps). Both decode
identically — same digest shape, same QPoW runtime API — which is the proof that
adding a chain is one [[chains]] entry and no new code. Quantus Staging Mainnet
and Quantus Dirac Testnet are listed and disabled: no endpoint exists under the
a1-/a2- pattern and none resolves under any plausible name.

Nav is ordered by node count descending, ties broken on id so it cannot reshuffle
between refreshes.

Telemetry now also yields chain-wide best height, best finalized height, average
block time and the client-version histogram (feed codes 1, 2, 12 and 22).

Infra moves to a single site: oolon fronts blackbeard.observer, reverse-proxying
cross-site over the mesh to the API beside the node on bob. That hop is the one
the loopback health probe cannot see, so the deploy now checks it explicitly —
a firewalld service scoped to bob's own /16 would leave a live site with a dead
/v1 and nothing would fail. infra-setup gains `dns` and `cert` roles that run on
oolon with the credential already there: the Cloudflare token is never copied off
the proxy, and it refuses to repoint an apex record that already exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi
2026-09-04 13:04:11 +03:00
grenadeandClaude Opus 5 110fbc3631 feat: blackbeard.observer — live Quantus mining leaderboard
Cargo workspace plus a Vite frontend, following ~/git/architecture/generic.md.

Every block header carries its author's wormhole reward preimage in a `pow_`
PreRuntime digest, so authorship for the whole network is derivable from headers
alone — no indexer, no registration, no way for a miner to be left out. That
decoding, the hashrate maths and the telemetry name attribution live in
blackbeard-core with no I/O at all, so the parts that are easy to get subtly
wrong are exercised by unit tests rather than only against a live chain.

The browser holds one WebSocket: snapshot on subscribe, deltas thereafter. The
head stream is itself a push (chain_subscribeNewHeads), so a block reaches the
page the moment the node imports it. Messages are serialised once per broadcast,
and leaderboards are recomputed only for windows a socket is actually watching.
No RxJS — useSyncExternalStore is React's own contract for this.

Verified against the live Planck testnet: 12/12 headers decoded, telemetry names
attributed (quanpool-planck, baba-gorchitsa, …), warm start restoring 84 blocks
and 5 held names across a restart.

Three findings worth recording, all in CLAUDE.md:

- substrate-telemetry sends its JSON in *binary* frames. A text-only client
  connects, subscribes, reports healthy and receives nothing at all — and a
  Python probe hides it, because json.loads accepts bytes.
- Difficulty is a little-endian U512; decoding it big-endian gives a number
  wrong by ~10^150 that still renders fine.
- Planck's real block interval is ~13-15s against a 6s target with enormous
  variance, so a measured interval needs 20 tip samples before it is publishable.

Deploy assets, the Gitea Actions workflow and script/infra-setup.sh are included;
port 25864 is registered in architecture/port-allocations.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSDYiibCtELsrjQq6KXnoi
2026-09-04 12:33:54 +03:00