Live miner leaderboard and network hashrate for the Quantus
blockchain and its Planck testnet, at https://blackbeard.observer
(blackbeard.internal from the mesh).
The point of it: a miner can find their own row, see what share of the network they are actually winning, and watch it move — against everyone else, in real time. Mining is a competition that normally gives its participants no scoreboard. This is the scoreboard.
Every block header carries its author's 32-byte wormhole reward preimage in a
PreRuntime digest stamped with the engine id pow_. The node writes it there so
the mining-rewards pallet can read it back and derive the payout address
on-chain — and as a side effect, authorship for every miner on the network is
derivable from headers alone.
That is the whole mechanism, and its consequences are worth stating plainly:
lair/quantus's doc/wormhole-rewards.md has the full
analysis.Difficulty comes from the QPoWApi_get_difficulty runtime call. Difficulty is
the expected number of hashes to win a block, so network hashrate is difficulty
divided by the block interval — and a miner's implied hashrate is their share of
recent blocks times that.
The mechanics were first worked out in lair/quantus's Prometheus arena
exporter (asset/arena/quantus-arena-exporter.py), which remains the reference
for anyone checking this implementation against a second one.
The chain gives us preimages; a board of 64-character hex strings is honest and unreadable. substrate-telemetry closes the gap indirectly.
The feed reports block imports with a propagation time, stamping the first
reporter of a hash with 0. A node imports its own block before announcing it,
so the author — if it is on telemetry at all — is nearly always first. One block
proves nothing (measured on Planck, an author's lead over the second reporter is
50–620 ms, which a well-peered bystander can produce), so the observer votes
over a rolling window and only shows a name once enough blocks agree.
An attributed name is therefore an inference, never a claim the miner made and never an identity check. The UI marks it as such, carries the confidence in a tooltip, and falls back to the abbreviated preimage rather than guessing. Where two miners resolve to the same node name — one operator with several preimages, or two people who never renamed their node — every member of the colliding group gets a preimage suffix, so no row is silently privileged as "the real one".
Cargo workspace plus a Vite frontend, per ~/git/architecture/generic.md §1.
crates/
blackbeard-entities/ domain types + the browser wire protocol; ts-rs exports
the TypeScript the frontend consumes. No I/O.
blackbeard-core/ header/SCALE decoding, hashrate maths, the rolling
window, telemetry attribution voting. Pure — no I/O,
no clock, no sockets, exercised entirely by unit tests.
blackbeard-data/ chain JSON-RPC, the telemetry feed, Postgres.
blackbeard-api/ the daemon: ingest tasks, REST, WebSocket fanout.
blackbeard-cli/ operator tools: probe, backfill, standings.
web/ Vite + React + SWC + TS. Static build, served by nginx.
asset/ systemd, firewalld, nginx, config template, bootstrap SQL.
script/infra-setup.sh one-time host provisioning, operator-run.
The frontend's types are generated from the Rust. cargo test -p blackbeard-entities writes web/src/api/generated/; CI fails the build if the
committed output is stale. Edit the Rust DTO, never the TypeScript — otherwise
the two compile cleanly and disagree at runtime.
One WebSocket per browser tab. On subscribe the client gets a full snapshot;
after that only deltas — a block on every block, a summary when the headline
numbers move, a leaderboard when the standings change. The page never polls and
never refetches.
The head stream is itself a push: chain_subscribeNewHeads over the node's
WebSocket, so the observer learns of a block the moment the node imports it. The
only polling anywhere is difficulty and sync state, on a four-second timer.
RxJS was considered and not used. This needs one stream, one reducer and one
subscriber list; useSyncExternalStore is React's own contract for exactly that,
gets concurrent rendering right, and costs no dependency and no idiom in every
component. The store is web/src/api/socket.ts — about 200 lines including the
reconnect and resubscribe logic.
Server-side, messages are serialised once per broadcast and every socket
writes the same Arc<str>; leaderboards are recomputed only for windows that
actually have a subscriber.
Every window on the site is a block count — 600, 3 600, 14 400, 100 800 —
and since those are what is windowed on, they are also what the selector says
and what the URL carries: /quantus/3600, not /quantus/six_hours.
The duration names came off because they were a promise the site could not keep.
A window is a fixed number of blocks, so how far back it reaches is whatever the
chain's rate makes it. When Planck's miners left for mainnet its rate fell
ninefold, and /planck/six_hours went on saying six hours over a window that
spanned twenty-nine — with a miner that had not touched the chain in a day
sitting in the standings looking current. The name in the address bar was the
reason a reader believed it.
The duration is still shown, next to the count, but it is now measured: the
authored-time span of exactly the blocks tallied, which is the same figure that
is already the denominator of every per-miner hashrate. It carries no "",
because nothing is being estimated. Only the windows that are not being
displayed still show an estimate from the current interval, and those keep the
"". The old URLs still resolve — the router rewrites them to the block count —
so links already shared keep working.
This is not pedantry. A duration is meaningless while a node is catching up: it imports historical blocks at disk speed, so "the last hour" can contain half a million blocks. The same reasoning is why a measured block interval is only used when the node is at the tip; while syncing, the configured target is used and every hashrate on the page is labelled nominal.
The sparklines under the headline tiles hold the same line. They are bucketed by height, not by time: every point is the same number of blocks, so every point carries the same statistical weight and a stretch where nothing was recorded stays the same width on the axis as one where everything was. A bucket is the window divided by fifty, which is why every window is a multiple of it — a full window is then exactly fifty buckets, and the rolling miner count at the last point is the same number printed in the tile above it.
Their block interval comes from the author's own timestamp inherent rather than from when this observer saw the block, for the same reason the measured interval only counts tip observations: a batch of blocks fetched from history all carry very nearly the same observation time, and dividing by that reads as a chain producing hundreds of blocks a second. Each point is a moving average over three buckets, because the mean of one bucket's gaps still carries enough Poisson noise to draw a chain that looks like it is changing size every few minutes.
/quantus/block/1069799 is a position. Because block is keyed on
(chain, height) and a reorg overwrites, a link to a height quietly comes to mean
a different block the moment the chain forks there. So opening one resolves it
and rewrites the address to /quantus/block/0x…, which names one block for good
— including after it has lost, which is the only way an orphan is linkable at
all.
The page is assembled from both sources because neither is sufficient. The node
holds the block and forgets it: blocks-pruning defaults to
archive-canonical, so a losing fork's body goes once finality passes it, and
difficulty is a state read behind a 256-block window. The observer holds what
the node never had — when the block was seen, and whether that sighting was at
the tip — and what it has since forgotten, the difficulty recorded while the
state still existed. Anything neither can answer says so in words rather than
going blank, because a gap on a block page reads as a zero.
/quantus/runtime/152 is every pallet, call, event, error, storage entry and
constant the runtime declares — with the constants' values, decoded against
their own declared types. Nothing on that page is transcribed from a source
tree, which is the distinction worth having: a source tree says what the chain
should be running, and metadata says what it ran, at a height, possibly months
after it was upgraded past.
The set of runtimes is found rather than waited for. Decoding caches a runtime
the moment it needs one, so left alone the cache is "whatever the backfill has
walked past" — days, on a chain a million blocks deep. spec_version only
increases, so the upgrade boundaries are a sorted sequence and bisection finds
each in log₂(height) probes. On Heisenberg that turned up six runtimes and
the block each took over at: v126 from 1, v128 from 132, v131 from 342,813,
v136 from 669,129, v144 from 812,055, v148 from 977,079.
It runs as its own task, not in the poll loop, and the reason is worth
recording: state_getRuntimeVersion at an old block makes the node load and
instantiate the runtime WASM out of that block's state, which measured four
seconds against Heisenberg's public endpoint against a quarter of a second at
the tip. A hundred of those inside a four-second poll loop would stall
difficulty, the summary broadcast and the telemetry attach for minutes on every
start.
Reading the two ends of Heisenberg's history side by side is the case for the
page existing. Between v126 and v148 the chain dropped Referenda,
ConvictionVoting, Recovery, Assets and AssetsHolder, added Vesting and
Origins, gained a WeightReclaim signed extension and moved
ChargeTransactionPayment after the two Quantus-specific ones. Every one of
those is a change that breaks a decoder written against the other version, and
none of them is discoverable from a block.
Two renderings are not what the registry literally says, both for the same
reason and both identified by registry path rather than by shape:
AccountId32 becomes 0x hex rather than thirty-two numbers, and U256/U512
become one decimal rather than four or eight little-endian limbs. Difficulty as
[1189189, 0, 0, 0, 0, 0, 0, 0] is a correct description of the bytes and tells
a reader nothing.
/quantus/reversible shows scheduled transfers that have not yet landed,
counting down to the block they execute at. ReversibleTransfers is the most
distinctive thing this chain does — a transfer waits out a delay, default 7,200
blocks, during which its sender or a nominated guardian can call it back — and
no other explorer can show it, because no other chain has the pallet.
It is the only view here built from both halves of the observer, each authoritative about a different thing:
PendingTransfers — says what is still pending. A cancelled or
executed transfer is simply gone from the map, and that absence is more
trustworthy than replaying every event since genesis and reconstructing the
set, which fails silently when it fails.TransactionScheduled
carries execute_at and the stored struct does not. A transfer scheduled
before the index reaches shows as pending with an unknown deadline rather
than being dropped for lacking half its story.execute_at is a DispatchTime<BlockNumber, Moment> — an enum, so it names a
block on one arm and a timestamp on another, and reading the wrong arm would
present a millisecond timestamp as a height. Time remaining is an estimate from
a block interval that moves; the block count is the fact, and the page says
which is which.
Enumerating a map needs state_getKeysPaged on the entry's prefix — there is no
list, only keys derived from the things in it — and recovering each key from its
storage key needs the hasher to have kept it. Blake2_128Concat and
Twox64Concat do; Twox128 and the rest do not, so StorageMap::key_offset is
None there rather than a guess, and such a map can be counted but not
attributed.
The block page lists its extrinsics with each one's own events beneath it,
and the block's own events in their phases. An event's phase says whether it
belongs to an extrinsic or to the block, and extrinsic_index says which — that
join is why both columns are stored, and it turns a block from two lists into
one account of what it did.
Block 1 on this chain is the argument for it. Showing only extrinsics, it is a
single Timestamp::set and nothing else. Its events are the whole opening
distribution:
EXTRINSICS (1)
0 Timestamp::set 1788943917807
42 Vesting::LaunchMomentSet 1788943917807
INITIALIZATION (21)
1 Wormhole::NativeTransferred 3 QTC → qzmtKfCX…z6HW
…
35 Wormhole::NativeTransferred 5,669,940 QTC → qzmviwoP…nxW7
…
FINALIZATION (6)
45 System::NewAccount · Balances::Endowed · Balances::Minted
49 Wormhole::NativeTransferred 0.3 QTC
50 MiningRewards::MinerRewarded 0.3 QTC
51 ZkTree::TreeGrew 3
Two things only visible this way. The chain's opening distribution is
twenty-one wormhole transfers in Initialization, owned by no extrinsic at
all and sent from the minting account — not block zero, and not anything a
transaction did. And Vesting::LaunchMomentSet fires here, so vesting has been
exercised even though the call index shows zero Vesting::* dispatched: a
pallet the runtime uses looks unused from the call side alone.
Some accounts are special and nothing about the address says so. The treasury looks like any other empty account; a minting sentinel looks like a wallet; the accounts endowed at genesis look like they earned it.
Nothing here is curated. The chain names them itself, in three places that are three different claims and are deliberately not flattened into one badge:
spec_version.
MiningRewards::MintingAccount and Wormhole::MintingAccount, both
0x0101…0101.TreasuryPallet::TreasuryAccount.Every label is derived from the chain's own name — TreasuryPallet:: TreasuryAccount becomes Treasury — so a pallet added next year gets its
accounts labelled without an edit. The chip is the claim and the citation
beneath it is the evidence, because "named by a constant" and "named by state"
mean different things about how permanent the arrangement is.
Two rules hold this together. An account is told from a hash by registry path,
never by shape: after normalise both are 0x and sixty-four hex characters,
and filtering by shape claims System::ParentHash names an account. And an
entry must be an account, not merely contain one — System::Events is full
of them, none named by it, and treating "contains" as a role labelled half the
chain's active addresses Events on the first attempt.
A chip states what the chain says and does not endorse it. MintingAccount has
no System::Account entry at all; labelling it as what the runtime calls it is
a fact, and implying it holds funds would be the site vouching for an address.
Endowments have no extrinsic and no event. They are balances the chain was born
holding, invisible to anyone who has not read the chainspec — so /:chain/block/0
carries them, and a Genesis link sits in the section nav to give somebody who
would never think to look an unmissable way to.
Mainnet started with 21 accounts and 5,670,000 QTC, of which one account holds 5,669,940 and the other twenty got 3 each. The page shows the endowment beside what each holds now, so what a founding account did with its stake is one row.
Genesis state lives only on an archive node. When it cannot be read the page says so rather than showing an empty table, because missing data and a chain that endowed nobody are not the same claim.
Everything else here is history — headers, events, extrinsics, a record of what
happened. /quantus/state is the other half, and it exists because of a
question history could not answer.
Which address is this chain's treasury? TreasuryPallet::set_treasury_account
reads never on the call index and TreasuryAccountUpdated reads never on
the event index. Both are true: the address was set at genesis, so no extrinsic
ever carried it and no event ever announced it. It exists only in state, and
until the observer could read state the honest answer was that we did not know.
It is qzjsuLN7Nhu4bjvmUbjSTr2ZTeZ7oRxXpQP9fdv6PcHUCRrVR, and it has never
been funded — no System::Account entry at all.
A storage key is twox128(pallet prefix) ++ twox128(item) ++ hashed keys, and
which hash each key uses is declared per entry: System::Account is
Blake2_128Concat, another map is Twox64Concat. Picking wrong yields a key
that reads as absent rather than as an error, which is why
Runtime::storage_key computes every part of it from the runtime's own
description — including the pallet's storage prefix, which is usually the pallet
name and is not required to be — and why its test pins two keys against ones
read off the live chain by hand.
Absent is not zero, and the metadata is what knows the difference. An optional
entry holding nothing means nothing; a default entry holding nothing means the
value the runtime supplies, which the page marks as default rather than
passing off as something the chain wrote. The same distinction is why an
unfunded account shows — and "no account on chain" rather than a balance of
zero: Substrate reaps an empty account, so a missing entry means it does not
exist, and a zero would say it had been funded and spent.
Read live on every request. State is what is true now, and a cached copy of now is a copy of some earlier now wearing the same face.
/quantus/event is its other half, and the one a chain analyst reads for
narrative: 19 of 107 event kinds have ever fired. A dispatchable says what
somebody can ask for; an event says what the runtime does, including the parts
nobody dispatches directly — a vesting schedule ending, an account reaped, a
proof verified. Vesting::VestingCompleted at zero and System::CodeUpdated at
zero are two different statements about a chain's life, and neither is visible
on an explorer that lists only what happened.
Which is why nothing is excluded from the index for being unused. The event
interest list is a denylist, so a pallet the chain grows next year is indexed
the day it ships rather than the day somebody remembers to add a line. The rule
for exclusion is narrower than volume and narrower than usefulness: an event
carrying no account can never answer "everything involving this account",
which is what the index is for, so the three that both name no account and fire
every block are skipped — ZkTree::LeafInserted, QPoW::DifficultyAdjusted,
System::ExtrinsicSuccess. They show as not indexed rather than as a zero,
because a zero would read as disuse.
Balances::Minted looked like a fourth candidate — it fired exactly as often as
MiningRewards::MinerRewarded across a 120-block sample, and for the same
reason. It is kept: it names an account, minting is not only for miners, and
"who received newly issued tokens" is worth being able to ask even in a month
where the answer is only the miners.
That widening took the index from ~1.2 to ~3.4 rows per block measured on live mainnet, and bought 739 accounts' worth of wormhole transfers that were previously invisible.
/quantus/call is the one page here that does not exist elsewhere. Every
explorer shows activity; this shows activity against the declared surface,
because the runtime describes all 58 of its dispatchables in the same metadata
the decoder already reads. The join is free and the gap is the interesting half:
quantus v152 6 of 58 calls used
heisenberg v148 6 of 58 — and a different six
ReversibleTransfers, TechReferenda, Vesting and Preimage are shipped,
documented, and untouched. "This chain has governance nobody has used" is a
different statement from "this chain has no governance", and only one of them is
visible on an explorer that lists what happened. The unused rows keep their
signature and their documentation, both straight from the chain rather than from
a source tree.
Two chains diverging on the same runtime family is the other thing it shows: mainnet has exercised the wormhole batch verifiers and nothing else, while Heisenberg has a multisig lifecycle — create, propose, approve, execute — and no wormhole traffic at all.
Nothing on the page is written per call. /quantus/call/Utility/batch_all feeds
from the same generic index and renders its arguments through the same
Payload, so a pallet added next year has a working page the moment somebody
uses it — and a row on the index the moment the runtime declares it.
An event is the chain's account of what happened. An extrinsic is the request that caused it, and it carries what no event does: who signed, what they actually called, the nonce and tip they set, and whether it worked. A history built from events alone silently omits every failed attempt and never names a submitter.
The same oracle does both. The metadata's extrinsic type carries four
parameters and the registry names all of them — Address is a MultiAddress,
Call is RuntimeCall, Extra is the twelve signed extensions, and
Signature is qp_dilithium_crypto::types::DilithiumSignatureScheme. That last
one is the part that would otherwise need hand-written post-quantum knowledge,
and it does not: the chain describes its own signature scheme, so
decode_extrinsic reads a Quantus transaction without a line of code that knows
Quantus exists.
Two things about this chain's extrinsics are worth knowing before touching them.
The first byte is not the version — the top two bits are a type tag, and
mainnet carries 0x84 (signed, v4) and 0x05 (bare, v5) in the same block while
the metadata declares version 4. And a signature is 5.3 KiB, two orders of
magnitude larger than the call it authorises; it is decoded to find where the
call begins and then thrown away, with only the scheme's name and the byte count
kept.
Both halves of a block are indexed in one pass, because both come from calls
already being made: the events blob for chain_event, the body for
chain_extrinsic and the block's own timestamp. Whether a dispatch succeeded
comes from the System::ExtrinsicSuccess and ExtrinsicFailed events in that
same decoded list — read, used, and not stored, because two rows per block
forever for one boolean each is not a trade worth making.
An account's page merges the two into one history ordered by block, with the
extrinsic above the events it caused. source is 1 for an extrinsic and 0 for an
event precisely so every part of the sort key descends, which is what lets the
paging cursor be a plain three-part row comparison rather than a
mixed-direction one Postgres cannot express.
Nested calls are the reason this is worth doing generically. A
Utility::batch_all carries whole calls in its arguments, so a transfer's
recipient can be three levels down — and because the decoder collects account
ids wherever it finds them, that batched transfer appears on the recipient's page
tagged received even though they signed nothing.
/quantus/account/qzp2AxZw… answers what the official explorer does not: what
an address has actually been paid, every reward, back as far as the index has
read. The address is converted to a raw account id at the edge, so a typo is
rejected as malformed rather than answered with an empty history — which reads
exactly like a real account that has never been paid — and a Polkadot address is
rejected too: same thirty-two bytes, different network, and answering for it
would show someone activity that is not theirs.
The activity table has no case per event type. Every chain_event row carries
the accounts it mentions in a column the decoder filled — it knows which
values are AccountId32 by their registry path, which is also how it avoids
rendering a block hash as an address — so accounts @> array[…] finds
everything, including from pallets that do not exist yet. The frontend labels
and formats what it recognises and shows anything else as its own key and value,
so a new event type appears as itself rather than not at all.
The preimage and the address are the two ends of one identity, and each page links to the other: the miner page counts blocks, the account page counts tokens, and the derivation joins them. Going the other way — address to preimage — is a lookup among preimages already seen, not a calculation, because the Poseidon2 derivation runs one way only.
Every route here was reachable only by already knowing an address: a block hash,
a preimage, an SS58 string, a spec_version. That is fine for a link somebody
sent you and useless for finding out the pages exist. A kind with nothing after
it is now the index of that kind — /quantus/block, /quantus/account,
/quantus/runtime — and a section nav under the chain chips names all of them.
There is deliberately no /quantus/miner. The standings are the index of
miners, and a second page listing the same rows under a different address would
be two answers to one question; that path redirects there, as does anything else
that resolves to the front page but is not spelled like it.
The three indexes are not the same page three times:
authored_at, the
chain's own clock, not from observed_at: a gap fill writes a whole batch
within the same second, and the first cut of this page reported three
milliseconds between blocks on a chain targeting twelve seconds.pearl-prover with nothing to tell them apart.Paths, not a hash: /quantus/day, /quantus/block/13160,
/quantus/miner/0x…. A fragment is never sent to a server, so a hash URL cannot
be given a title, cannot be crawled, and cannot be resolved by anything but the
page itself — which was a fine trade while the whole surface was a chain and a
window, and stopped being one once a block and a miner each became a page worth
sending someone. Both vhosts already answer any path with index.html, so
nothing outside the client changed. lib/routes holds the grammar, and
fromLegacyHash rewrites the old form on load, so every #/… link already
published still lands where it meant to.
Route segments are named — /quantus/block/13160 rather than
/quantus/13160 — rather than told apart by shape. Shape works right up until
two kinds of thing can look alike, and an SS58 address is arbitrary base58 with
no rule keeping it clear of a window name forever.
A miner's page carries the window too: /quantus/miner/0x…/day. Its rank and
hashrate are of a window, so a link without one means something different from
what the sender was looking at — and until it was in the path, changing the
window on a miner's page could only navigate back to the standings, because
there was no URL for "this miner, over a day". The window control appears on
exactly the two pages where it changes something, the standings and a miner;
a block happened once and a balance has no span, and a control that does nothing
where it sits teaches a reader to distrust it everywhere else.
Everything navigable is an <a>, including the chain chips and the window
control. That is what "real URLs" has to mean in practice: middle-click opens
the 24-hour view of another chain in a tab, right-click copies a link to a
miner, and the back button walks the panels the way it walks pages. The one
deliberate exception is a height resolving to its hash, which replaces rather
than pushes — it is the same block under a better name, not a second place the
reader has been.
A [[chains]] entry takes rpc_urls and ws_urls, tried in order. rpc_url
and ws_url still work as a one-element list, because a config naming one
endpoint is still a valid config.
They are stuck to, not balanced across, and that is the whole design. A storage read at an old block hash needs a node that still holds that block's state; nodes prune on their own schedules, so alternating between them would return a mixture of answers and absences that reads as sparse data rather than as a configuration problem. Requests stay on one endpoint until it fails, and the cursor is shared across every task on the chain so a failover one of them discovers is not rediscovered by the rest.
Failing over on the wrong thing is the trap. A JSON-RPC error is the node
answering — count exceeds maximum value is a caller's mistake, and moving on
it would hide that behind a second node making the same complaint. A pruned
block is a success: state_getStorage returns {"result": null}, and treating
that as a failure would walk every endpoint asking a question none of them can
answer. Only transport failures and malformed responses move.
The WebSocket rotates at reconnect, where the loop already was. Holding
subscriptions to every endpoint and deduplicating heads buys nothing: heads are
a liveness signal and ingest fills gaps against chain_getBlockHash anyway, so
a few seconds on the next endpoint costs a reconnect rather than data.
Verified against a chain configured with a dead endpoint first: Planck stayed
full at its live height, and the failover logged once — from, to, and how many
endpoints the chain has, because "the chain is unreachable" and "one of three
endpoints is unreachable" are different operational facts.
Three chains walking their history at once — mainnet, Heisenberg and Planck's
million blocks — costs about 5% of one core, holds five of eight Postgres
connections with one active, and does not move API latency: summary at 7 ms
p95, block at 13 ms. The walk is bound by RPC round trips, not by anything
here, which is why it needs no deployable of its own.
It is also separable if it ever does. backfill only reads in-memory state —
the tip height and the current runtime — and writes exclusively to Postgres, so
it contends with nothing the API serves. Live ingest is the half that could
not move: it feeds the window, the ticker and the attributions that the API
answers from.
Progress survives a restart because event_scan is a table rather than a field.
That is load-bearing rather than tidy: blackbeard-api-cert.path restarts the
service several times a day when the host certificate rotates, and a cursor in
memory would mean Planck never finishing.
Postgres on magrathea.kosherinata.internal, mTLS and passwordless — the host's
own certificate is the credential and pg_ident.conf maps its CN to the role.
There is no password anywhere in this repo and nowhere in the config to put one.
Blocks are keyed on (chain, height), not on the block hash, deliberately: this
is a proof-of-work chain and it reorgs. An upsert on that key means a block
replacing another at the same height overwrites it, so the standings reflect the
canonical chain rather than the union of every fork the observer witnessed.
What that overwrites is copied into block_displacement on the way past, so a
contested height leaves a trace of who lost it. The node will not answer for the
losing block for long — it keeps only canonical bodies once finality passes
them, and difficulty is a state read behind a 256-block window — so the row
written at the moment of the reorg is the only record there will be. It is not
an orphan store: the block itself is gone, and only what the observer had
already seen of it survives.
The database is what lets the site keep promises a rolling window cannot: "how did I do last week", a hashrate line older than the process, and a leaderboard that survives a deploy. On start the daemon replays the window and the held attribution names out of it, so a restart does not serve an empty site.
A header tells you who mined a block. It does not tell you what they were paid:
the amount is remaining supply over an emission divisor plus collected fees,
quantized with the dust carried into the next block, so it is only knowable from
the MiningRewards::MinerRewarded event the runtime emits. Reading that means
decoding SCALE against the type registry of the runtime that produced the block
— and this chain's encodings differ from vanilla Substrate, because the
post-quantum signature scheme changes the shapes.
Rather than transcribe those shapes into Rust and re-transcribe them after every
upgrade, the observer asks the chain to describe itself. state_getMetadata at a
block hash makes the node execute Metadata_metadata against the runtime WASM in
that block's state, which returns metadata v14: every pallet, every event, every
field, as a portable scale-info registry. blackbeard-core::runtime decodes
against that registry and knows nothing about mining, rewards or transfers —
INDEXED_EVENTS in ingest.rs is the only place a name appears, and adding
transfers or multisig or whatever the chain grows next is one line there and a
query, not a decoder.
Metadata is cached per spec_version in runtime_metadata, because a node that
starts pruning would otherwise make every historical block undecodable. Blocks
are decoded against the tip's runtime first — which costs no extra RPC call and
is right for all but a handful of blocks — and decode_events refuses a partial
read, so a block from before an upgrade fails rather than decoding into
plausible nonsense. That failure is what triggers asking which runtime actually
produced it. It is the property the whole approach rests on.
Events land in one table, chain_event, in the runtime's own vocabulary:
pallet, variant, and the fields as jsonb. Account ids are normalised to 0x
hex on the way in, and integers past 2^53 to strings — balances here are u128 and
a JSON number would be silently rounded by the browser. A jsonb_path_ops GIN
index over the fields is what makes "everything involving this account" a query
rather than a table per event type.
ingest::backfill walks the chain outward from the tip: to the tip first, then
toward genesis, one contiguous interval recorded in event_scan. Live indexing
alone is not enough for an account page — a restart, a brief RPC failure or a gap
fill that ran before the metadata was cached each leave a hole that nothing would
return to. Progress is a cursor rather than max(height) from chain_event,
because a block with no indexed event and a block never looked at are otherwise
indistinguishable, and the first is the common case.
A miner's pow_ preimage and their reward event agree without a lookup table:
wormhole::reward_address runs the same Poseidon2 derivation the chain does, and
its output is exactly the miner field of MinerRewarded. Verified on mainnet
block 13160 — preimage 0x02dacf59…, address qzowWAgb…, account
0xc6801725…, which is the event's own field.
# A throwaway Postgres for the dev loop and for the sqlx query cache.
podman run -d --rm --name bb-pg -e POSTGRES_PASSWORD=dev \
-e POSTGRES_DB=blackbeard -p 55432:5432 docker.io/library/postgres:18-alpine
export DATABASE_URL='postgres://postgres:dev@127.0.0.1:55432/blackbeard'
export TEST_DATABASE_URL="$DATABASE_URL"
cargo fmt --all && cargo clippy --all-targets -- -D warnings && cargo test --all
# The daemon, against a real node. BLACKBEARD_DEV_DATABASE_URL is the
# development-only path that skips mTLS; the systemd unit never sets it.
BLACKBEARD_DEV_DATABASE_URL="$DATABASE_URL" \
cargo run -p blackbeard-api -- --config dev-config.toml
cd web && pnpm install && pnpm dev # proxies /v1 to 127.0.0.1:25864
cargo test skips the database integration tests when TEST_DATABASE_URL is
unset, so it stays useful on a machine with no Postgres.
Before adding a chain, probe it. Every failure the probe reports is one the
deployed daemon would hit silently — an RPC without the QPoW runtime API gives a
site with no hashrate, and headers without a pow_ digest give an empty
leaderboard, both of which look like a working deployment:
cargo run -p blackbeard-cli -- probe --rpc-url http://bob.hanzalova.internal:9944
CI-driven (architecture/deployment-gitea-actions.md): push to main, or run
the workflow from the Actions UI. Hosts, ports and paths live in
.gitea/workflows/deploy.yaml and nowhere else.
One-time host provisioning is operator-run, from a workstation with full sudo:
./script/infra-setup.sh --pubkey ~/.ssh/id_gitea_ci.pub
Re-run it whenever the deploy gains a new file to ship — each deploy job preflights the target's sudoers against the grants in that script and fails up front naming what is missing, rather than dying partway through an rsync.
Two things it deliberately does not automate, both because they touch shared
infrastructure: the public Let's Encrypt certificate (external-tls.md) and the
split-horizon blackbeard.internal record, which must be added to both site
routers' Unbound or it NXDOMAINs everywhere but one site.
| Port | What | Where |
|---|---|---|
25864 | blackbeard-api REST + WebSocket | bob (hanzalova), mesh address only, plain HTTP behind nginx |
The edge proxy is oolon (kosherinata) and the API is on bob
(hanzalova), so /v1 is a cross-site hop over the mesh. That is deliberate —
the API has to sit beside the node whose loopback RPC it reads — and it is the
one hop a loopback health probe cannot see, so the deploy checks it explicitly.
Registered in architecture/port-allocations.md §5; derived from the service
name per §3.
Quantus mainnet is not yet live. It is a commented-out [[chains]] block in
asset/config/config.toml.tmpl, waiting on a published chain spec and an
endpoint.
A chain may be configured before it launches: the observer reports it as
awaiting, the UI says so plainly, and it comes alive on its own the moment the
node starts answering. No redeploy, no restart. Adding it is a config change and
a probe run.
runs-on: infra for the deploy jobs, not fedora-43. The targets are
mesh-only .internal names and the fedora runners have no route to the mesh.
Same reasoning as lair/quantus and lair/mail.generic.md says nothing about themes, but the dataviz
conventions expect a selected dark mode alongside light. This site commits to
one look: it is a scoreboard for a proof-of-work chain, and a light variant
would be a second palette to validate for a context this content does not have.BLACKBEARD_DEV_DATABASE_URL bypasses the mandated mTLS Postgres
connection. It exists so cargo run works on a workstation with no
pg_ident mapping on the fleet cluster. It is an environment variable rather
than a config key precisely so it cannot be reached by editing a deployed
config, and taking it logs a warning loud enough to spot in the journal.lair/quantus issue #1 tracks the same gap from the exporter side.lair/quantus — the node and miner deployment, the arena exporter these
mechanics come from, and the measured hashrate numbers behind the fleet.~/git/architecture — the house conventions this project follows.263 activities
c36d08b feat(sponsor): on, as blackbeard-observer.near, 30 bps to itself, 300 tx a day0b8fb05 fix(sponsor): a creation deposit that covers the account's storageafa2845 docs: shard 13's instance is synced, through the 24569 forward003a13e feat: the NEAR sponsor for blackbeard wallets, off until configuredce347c1 feat(infra): a second neard on frankie for shard 13, behind s13.near36bea6b feat(infra): our NEAR nodes public by shard, s<id>.near.blackbeard.observerWindow: NEAR blocks 218,963,708 → 219,043,107 (2026-10-07 17:00 UTC → 2026-10-08 ~05:50 UTC). Read from frankie (shard 7) and gargravarr (shard 8).
dry: true, inside Intents)No liquidity available on every probe, every ~5 min, all night (qtc_watch journal).No liquidity available at 05:48 UTC. A continuous buy-side probe started at ~06:30 UTC and has logged No liquidity available since.No liquidity available.3,498 QTC-related execute_intents transactions.
Pool swaps through Intents: all by solver-ref-2.near, all memo: "rebalance":
No token_diff trades, as at launch. Delivery to buyers is by transfer:
solver-ref-2.near → per-order accounts 58,036.20 QTC
per-order accounts → 51e8f94d…7cbe (near.com's pooled account) 60,017.10 QTC
51e8f94d…7cbe → other accounts 8,014.76 QTC
→ of which withdrawn to Quantus (WITHDRAW_TO) 6,793.82 QTC
This is the same flow as the launch window: orders filled by the Ref solver and credited to the account that near.com purchases land in.
The pool:
| | 2026-10-07 17:05 UTC | 2026-10-08 05:50 UTC | |---|---|---| | Price | $101.59 | $157.83 | | USDC | $3.53M | $11.17M | | QTC left | 97,457 | 37,263 (all in the $157.83–$219.63 band) | | Cumulative | | USDC in 14.45M / out 3.39M; QTC in 31,872 / out 144,608 |
Supply: the team's holder qzozZqZW…jFEAc moved another 30,000 QTC (43,840 → 13,840). Bridge custody is at 180,689 QTC against NEAR supply 180,672.6, still fully backed.
Buy orders reaching the Ref solver through near.com were filled all night, about 58k QTC worth, while:
rebalance).With only buyers served and the solver restocking from a sell-only ladder, the pool price could only rise, and did, by 55%.
This is the pattern the issue's probe and pause log are for: per-direction availability, per channel where we can test one, against on-chain fills.
thursday, october 8, 2026 — 05:55:12 utcref_pool_settings began recording at 14:59 UTC today, so it can say only what the whitelist has been since. Its history before that was read off the archive by bisecting the pool's own get_pool view:
| Block | When (UTC) | What |
|---|---|---|
| #218,316,831 | 2026-10-03 ~07:00 | Pool 17208628…\|qtc.omft.near\|2000 created on dclv2.ref-labs.near, whitelist empty |
| proposal #926 | 2026-10-03 07:18:47 | refchef.near proposes to ref-finance.sputnik-dao.near: "Set pool whitelist for …|qtc.omft.near|2000" |
| #218,321,052 | 2026-10-03 07:45:07 | Proposal executes on the approving vote of nicktrust.near (tx 3ZPb9G5h…, with approvals from nicktrust.near, refchef.near and wannamakeit.near). set_pool_whitelist sets all six at once, logging dcl.ref pool_whitelist_updated with the DAO as operator. |
| #218,636,504 | 2026-10-06 | First QTC minted on NEAR. The whitelist is already in force. |
| now | | Unchanged at every height checked since #218,321,052 |
The six accounts: intents.near, staging-intents.near, staging-ref-solver.near, solver-ref.near, solver-ref-2.near, liq.selfevident.near.
What this establishes:
intents.near, and only Ref's own solvers and one liquidity account may call it directly.dclv2.ref-labs.near (seven of them) has an empty whitelist, so anyone may swap in those, but they hold little liquidity.Method, for next time: a pool setting's history can always be bisected from the archive by reading get_pool at past heights. About 30 calls found both the creation and the change here, against a block-by-block scan of shard 8.
Twenty-odd dry quotes, keyless, paced, with nothing committed. They varied one thing at a time from the probe's request:
nep141:qtc.omft.near, blockchain qtc, to the site's donation address); QTC delivered to a NEAR account (1cs_v1:near:nep141:qtc.omft.near).EXACT_INPUT, EXACT_OUTPUT and FLEX_INPUT.All refused: "No liquidity available". A USDC→wNEAR control in each request shape was quoted. Two things about the requests themselves:
0x0…0 or 0x…dEaD is rejected as refundTo is not valid: burn addresses are refused by name. A refund inside Intents (refundType: INTENTS) is accepted, and that is what the Ethereum-deposit variants used.nep141:qtc.omft.near (blockchain qtc) and 1cs_v1:near:nep141:qtc.omft.near (blockchain near).So there was no keyless buy request that worked at that moment. Buys were filled at 14:18 and 14:57, so quoting either comes and goes over time or depends on who asks, and a single round of requests cannot tell which.
The body says selling QTC "was never quoted to the public" on launch evening. The fill record says someone's sells were quoted:
solver-ref-2.near. 45 of them fell between 18:45 and 19:41 UTC on 2026-10-06, at 100–460 QTC each, about one a minute at 6–8 seconds past it. That is regular enough to look automated, but it could be one seller or many.51e8f94d… (11,760.6 QTC in total), the shape a 1Click sell settles in. None was funded by a bridge mint directly, so the chain does not say whose QTC it was.For the rest of the fills: the 87 launch buys (15:00–17:59 UTC) coincide with the price climbing from about $86 to $102, the overnight buys with a recovery from $91 to $100, and today's two buys with a flat market around $101.
solver-ref-2.near restocks from the pool to fill a buy, and that restocking is what moves the pool's price. The record cannot separate cause from effect here without many more events.1cs_v1 asset id. The first minute any variant is quoted while the baseline is refused names the routing difference. If none is ever quoted while fills keep happening, that points to authenticated requests. The quote record ought to have been kept this way from the start.0378d0e fix(web): say the probe's request was refused, not that nothing was quoted0378d0e fix(web): say the probe's request was refused, not that nothing was quotedShipped today: the pause log, asymmetry and daily summary (ab2b59d), and on-chain context per pause (8892f0a), which needed a new record, ref_pool_settings, for the pool's state and whitelist. The first context it computed contradicts an assumption in this issue.
During the pause that began when the record did (11:01 UTC) and was still running at 15:00:
solver-ref-2.near's QTC holding rose from 2,070 to 2,460.8EiqtANy…, 14:18:03 UTC: solver-ref-2.near signed a transfer of 233.31 QTC to a one-off account, which signed it on to 51e8f94d… with an opaque 1Click memo.CZNrw5kF…, 14:57:02 UTC: the same shape, 114.81 QTC.That is how a 1Click buy settles on NEAR. At the pool price these were roughly $23.7k and $11.6k.
Every buy probe in that hour was "No liquidity available": $100 every minute, and $1k, $10k and $100k at 14:15, 14:30 and 14:45, against both native and Ethereum USDC.
The claim "1Click quoted nobody" is wrong. It quoted somebody: two buys were filled while our probes were refused. What the probe establishes is narrower: a keyless, dry, INTENTS-deposit quote for USDC→QTC was refused while someone else's request was filled.
The difference could be any of:
ORIGIN_CHAIN, paying from the buyer's own chain, where the probe deposits from inside Intents.NEAR does not show which, because 1Click takes payment on the buyer's own chain. Nothing here says the refusal was selective on purpose. It says the probe as configured does not see what those buyers saw.
depositType: ORIGIN_CHAIN with USDC on Ethereum as origin, and USDT and wNEAR as origin assets. The same cadence rules apply, a few more requests per quarter hour inside the published limit. If one variant is quoted while the INTENTS probe is refused, that is the finding, and it is a fact about routing, not about intent.5880ad…) is not counted, which is correct.8892f0a feat: on-chain context for each pause in quoting, and pool settings kept8892f0a feat: on-chain context for each pause in quoting, and pool settings keptab2b59d feat: pause log, asymmetry and daily summary for 1Click quote probesab2b59d feat: pause log, asymmetry and daily summary for 1Click quote probes918d730 fix(api): read a transaction's fee whether the decoder wrote a number or a string464bb49 feat: search box in the chain header918d730 fix(api): read a transaction's fee whether the decoder wrote a number or a string464bb49 feat: search box in the chain header4fbb4db feat: search box in the chain header7c02840 feat: Quantus transaction page, addressed by hash, with history hashed7c02840 feat: Quantus transaction page, addressed by hash, with history hashedDecision on the earlier comment: (a). Only our own observations are kept. A quote becomes markup_bps against the pool when it arrives, and raw responses are pruned after 24 hours (1ClickSwap API Terms §4.1(e)).
Probes. Keyless dry quotes on QTC pairs only, both directions, against native USDC and Ethereum USDC. The smallest size goes every minute (1 QTC, or 100 USDC); the full grid goes every 15 minutes (1/10/100/1,000 QTC and $100/$1k/$10k/$100k). That averages about 4 requests a minute against the published limit of 1,200.
Alongside each probe, read from our own nodes at the same moment:
quote for the same input;A solver counts as able when it alone held at least what the pool would pay, because solvers do not pool inventory.
What it does not claim. A probe that gets no answer is failed, never no_liquidity, so throttling or blocking cannot read as a solver pause.
| Trade | Pool would pay | Largest single holding of what was needed | Solvers able |
|---|---|---|---|
| sell 1 QTC → USDC (NEAR) | 101.37 USDC | solver-multichain-asset.near: 1,629,892 USDC | 10 |
| sell 1 QTC → USDC (Ethereum) | 101.37 USDC (native pool as reference) | solver-priv-liq.near: 1,062,583 USDC.e | 16 |
| buy QTC with 100 USDC (NEAR) | 0.9825 QTC | solver-ref-2.near: 2,070.28 QTC | 1 |
| buy QTC with 100 USDC (Ethereum) | 0.9825 QTC | solver-ref-2.near: 2,070.28 QTC | 1 |
Sell side: ten solvers each held enough native USDC, and sixteen enough Ethereum USDC, to pay a seller of 1 QTC on their own, and none quoted.
Buy side: solver-ref-2.near is the only solver holding any QTC, and its holding fell from 2,308 at 10:50 to 2,070 at 11:01 while no quote was being given.
These figures are what the page shows, with the benign explanations stated beside them. Which solvers 1Click asked, and what they answered, stays private to them.
6b3bf76 feat: probe whether 1Click will quote QTC, against what could have filled it6b3bf76 feat: probe whether 1Click will quote QTC, against what could have filled itTwo dry quotes (dry: true, depositType/recipientType INTENTS, no key) sent to https://1click.chaindefuser.com/v0/quote at 10:24 UTC, User-Agent naming this issue:
| Request | Result |
|---|---|
| 1,000 QTC → USDC (native, 17208628…) | 400 No liquidity available, correlationId 7937dbb1… |
| 100 USDC → QTC | 400 No liquidity available, correlationId dcc8be23… |
| Control: 100 USDC → wNEAR, identical request shape | 201, 19.6836 wNEAR, minAmountOut 19.4867 |
The control shows the request is well formed, so the QTC answer is real: neither direction was quoted at that moment, matching what the wallet and near.com show.
Every response carries the unauthenticated limit: x-ratelimit-limit-unauth: 1200, x-ratelimit-reset-unauth: 60. That is 1,200 requests a minute without a key. The reduced schedule proposed earlier (the smallest size each way every minute, the full grid every 10 to 15 minutes) averages about 3 a minute, a quarter of one percent of the published allowance.
1Click is governed by the 1ClickSwap API Terms of Service (Intents Technology Limited, BVI, effective 2026-08-28), not by the near.com platform terms. Using the API without registering is contemplated: an Unregistered Developer is defined and given a fee tier, 20 bps on swap volume, which never applies to dry quotes.
Read against the probe:
amountOut, minAmountOut and correlationId indefinitely, as the issue body proposes, would archive API data well past 24 hours.The probe itself is fine. The archive needs a decision:
(a) Store only this observer's own observations, long term:
Raw responses (amounts, correlationIds) are kept for 24 hours for debugging and then deleted. The derived markup is our measurement, not their data, though a strict reader could argue it is "API data" in another form.
(b) Store everything, as the issue proposes, and accept the §4.1(e) exposure.
(a) keeps every figure the issue's derived section needs: availability, asymmetry, markup and pauses. It gives up being able to show a specific historical quote's exact numbers.
wednesday, october 7, 2026 — 10:25:18 utc6fa8cf3 feat: NEAR calls and events read as sentences, JSON as the fallback6fa8cf3 feat: NEAR calls and events read as sentences, JSON as the fallback21ea353 feat: NEAR transaction and Intents account pages21ea353 feat: NEAR transaction and Intents account pagesPart 3 is live and verified: 313 of 314 deposits are linked, and all 328 withdrawals are matched at a 0.002 QTC fee. #46 recovered three history mints that had been dropped. The one unlinked deposit (1.348 QTC, Quantus #180,567) has no mint on NEAR at all.
A whole transaction can be assembled from our own nodes, which contradicts the plan above ("from the archive … assembly from our own nodes once completeness can be checked"). Completeness turns out to be checkable, and the missing receipts are fetchable:
EXPERIMENTAL_tx_status answers only on a node tracking the signer's shard, and only when sender_account_id is an account on that shard. gargravarr returned 10 of 11 receipts for piPTXyoq…. frankie and quadbrat returned final_execution_status: NONE with nothing. A wrong-shard sender gets UNKNOWN_TRANSACTION or NONE even from the right node. The sender only routes; any account on the signer's shard works.receipt_ids list (the transaction's own, and each outcome's) must have an outcome. For piPTXyoq… the missing one was 5ecjV2FP…, the intents.near ft_on_transfer that logs mt_mint to 42d72e44…, which runs on shard 7.EXPERIMENTAL_receipt returns the receipt (predecessor, actions), and EXPERIMENTAL_light_client_proof with type: receipt returns its outcome (logs, status, spawned receipts). frankie answered both for 5ecjV2FP…, so the tree is completed by following spawned receipts across our three nodes.tx_status for any sender with all outcomes (12 for piPTXyoq…). It is the fallback for a transaction older than our nodes' history, or one touching a shard we don't track (zec, btc and so on).So the plan is: our nodes first, public only for what they can't serve, and the page says which source it was. A transaction is stored permanently once complete and every block it touched is final. It is stored raw and decoded on read, so a decoder fix applies to everything already stored.
Where the signer comes from. The page is addressed by hash alone. The API looks up an account on the signer's shard from what it indexed: the configured minter for a bridge mint, and intents.near for anything read from shard 7, which is the intents scan's trades, transfers and withdrawals. A hash it has never seen goes straight to the public node.
URLs are chain-scoped, /quantus/near/tx/<hash> and /quantus/near/intents/<account>, not the /near/… in the issue body. The site's grammar is /:chain/:kind/…, so /near would parse as a chain named "near". Both pages sit in the Markets section, which already covers the token on NEAR.
The account page's history is what this observer indexes, which is QTC only: trades, transfers, bridge deposits and bridge withdrawals. It is not every dip4/nep245 event the account was ever in. The page says so beside the list. Balances are complete for all tokens, read live from intents.near on frankie (mt_tokens_for_owner, mt_batch_balance_of, public_keys_of).
Decision, 2026-10-07: closing without an Origin check.
From the 2026-09-29 report, covering a week of the #24 log: 1,843 WebSocket upgrades from https://blackbeard.observer (92 IPs) and one with no Origin. No third-party page opened the socket.
An Origin check would therefore stop nothing that happens today. A non-browser client can send any Origin it likes, so the check would only ever affect embedding by another site's visitors, and nobody is doing that. Not adding it keeps one less thing to keep in step with the domains the site serves on.
Reopen if a foreign origin shows up on /v1/ws upgrades. The jq query in the issue body still finds them.
Decision, 2026-10-07: closing. The deny list worked, and nothing moved.
From the 2026-09-29 report:
37.187.143.115 kept polling into the 403 at an unchanged ~1,234 a day for the whole week. Nobody is watching it, so it has neither noticed nor moved.135.181.130.74 (stardust-devops) stopped after 2026-09-23. It gave up.curl from Cogent), apart from our own checks.The 403 isn't teaching anyone to move, so the escalations considered here (return 444, an SNI blackhole) aren't needed. Reopen this if a fingerprint turns up behind the list.
94472f1 fix(api): bridge history treats a failed archive read as unresolved94472f1 fix(api): bridge history treats a failed archive read as unresolvede183fda fix(api): bridge mint scan reads its layout from the first height that existse183fda fix(api): bridge mint scan reads its layout from the first height that exists5f30000 fix(api): give NEAR backfills every other turn instead of none5f30000 fix(api): give NEAR backfills every other turn instead of none084fd5b feat: link QTC bridge deposits and withdrawals across both chains084fd5b feat: link QTC bridge deposits and withdrawals across both chainsFacts:
bridge-mng.near is on shard 8, which gargravarr tracks, and the direct-call scan already reads those chunks. gargravarr serves piPTXyoq… whole, and its ft_deposit arguments carry the Intents account (msg.receiver_id), the amount and BRIDGED_FROM.txHash. So recording a deposit needs no receipt fetch and no public endpoint.0xc4444afc…bf8d. chain_extrinsic stores no hash, so it's computed for the bridge's deposit extrinsics, and stored for every extrinsic from now on.qtc.omft.near's ft_total_supply on the archive, then reading only the shard-8 chunks around each change. A block-by-block scan of the gap would cost about a day of public calls; this costs a few per mint.Order:
ft_withdraw / WITHDRAW_TO from the intents scan, matched to the custody payout on Quantus by destination, amount less the 0.002 QTC fee, and time. Marked as inferred, with the rule shown; anything that doesn't match is listed as unmatched, never forced.E4ysnVht… (failure plus refund) is the test fixture.69050a4 feat: index QTC's 1Click fills, which settle as transfers, not token_diff69050a4 feat: index QTC's 1Click fills, which settle as transfers, not token_difftoken_diff tradesThis issue, and the NEAR Intents market built on it (#30, #33), assumed a QTC price would come from the ratio inside an executed token_diff. Since swaps went live on 2026-10-06, none has carried QTC.
1Click settles a QTC buy as a solver transferring QTC to a one-off 1Click account, which passes it on. The user's payment happens on their own chain and never appears on NEAR. So the NEAR Intents page's empty price and settlement panels were accurate for what they measured, but measured the wrong event.
The fills are being indexed in #43. The on-chain price is the solver's restock in the Ref pool (#37).
tuesday, october 6, 2026 — 17:26:41 utc| # | Item | Shipped | Verified |
|---|---|---|---|
| 1 | Supply reconciliation | unaccounted in the listing: supply less intents.near, the DCL pool contract, every pool whitelist account and Ref's classic exchange. Withheld until every balance is read. | 0.269 QTC, matching the hand count in this issue. A first-pass bug read 0.3695 by missing the whitelist; it was fixed the same day. |
| 2 | Ref v1 discovery | All classic pools listed every 10 min on quadbrat (shard 12), shown on the Ref market page. | 8,916 pools scanned; QTC is only in #8900, the empty QTC/QTEST test pool. |
| 3 | Bridge custody | /bridge: custody against minted, with deposits and withdrawals. The alarm fires only when the NEAR supply was read after the last custody movement. | Locked 155,994.701 against minted 155,994.698 at the time; deposit addresses are reused and funding amounts vary (comment above). |
| 4 | Replaced: QTC into and out of NEAR Intents | intents.near's balance read at every block on gargravarr (shard 4), with each change recorded. | Matches direct reads (36,044.097 QTC at #218,805,000; 72,671.542 at #218,811,064); in 1,044 rows each balance less its change equals the previous one. |
| 5 | Shards 4 and 8 | gargravarr tracks both (quadbrat has 10 and 12). Direct pool calls about QTC are scanned on 4 and 8; pool and token views come from gargravarr while it is at the tip. | Queues real direct calls; the first cut matched every call to the DCL contract, and was fixed by migration 0016 plus a re-read. |
| 6 | reads names Confidential Intents | In the price response. | — |
Found on the way, fixed separately:
025116f feat(api): log the pool's non-swap events, to learn whether it logs orders025116f feat(api): log the pool's non-swap events, to learn whether it logs orders6988368 fix(api): fetch relay swaps first, and never idle while the queue has work6988368 fix(api): fetch relay swaps first, and never idle while the queue has workb3dc4ef feat: list Ref's classic exchange for pools holding the assetb3dc4ef feat: list Ref's classic exchange for pools holding the asset6152ba8 fix(api): queue only direct pool calls about the asset, and re-read the rest6116954 feat: QTC into and out of NEAR Intents, block by block6116954 feat: QTC into and out of NEAR Intents, block by blockAs written, item 4 can't measure what it set out to. It proposed checking intents.near's QTC balance (ft_balance_of) against what the indexed events account for, to measure the trades arriving as cross-contract receipts that the scan doesn't read. But:
qtc.omft.near (ft_on_transfer), never inside the relay-signed transactions the scan reads.Replacement: a block-by-block series of QTC moving into and out of NEAR Intents.
ft_balance_of on qtc.omft.near at every NEAR block, from gargravarr, which tracks the token's shard (4) and so answers at any block it holds.Shown on the NEAR Intents market page as flows in and out, beside the supply reconciliation (item 1).
tuesday, october 6, 2026 — 15:48:13 utc0851b24 fix(api): read the pools already known while discovery is rate-limited5361dec feat: record the Ref pool's bid side over time1d25689 feat(data): ask our own NEAR node for view calls while it is at the tip5361dec feat: record the Ref pool's bid side over time1d25689 feat(data): ask our own NEAR node for view calls while it is at the tip26dfdc9 fix: pace public NEAR view calls, and never publish "no pools" from a failed read1afb995 fix(bridge): alarm on a shortfall only when both figures describe one moment8eb812a feat: read the NEAR bridge from the Quantus side — custody against minted9becf26 fix(data): count each visible transfer once, not once per pallet26dfdc9 fix: pace public NEAR view calls, and never publish "no pools" from a failed read1afb995 fix(bridge): alarm on a shortfall only when both figures describe one moment8eb812a feat: read the NEAR bridge from the Quantus side — custody against minted9becf26 fix(data): count each visible transfer once, not once per palletThe issue body says CLAUDE.md quotes some of the doubled counts. It doesn't. Recomputed on production with the fix applied, over blocks ≤ 42,400, Utility::batch_all gives 15,530 visible transfers. The table says 15,303. A doubled figure would be about twice the true one, not slightly below it. The table must have been measured from Wormhole::NativeTransferred alone, most likely before Balances::Transfer was indexed, so it is left as it stands.
The fix itself is checked on production data for the bridge custody account. The old query gives received 372,003.992 over 30 receipts; the fixed one 186,001.996 over 15, exactly half. Custody has received 30,000 QTC more since the figure in the issue body was taken.
b55d323 fix: no unaccounted figure until the whitelists are known; no mining row on market pagesb55d323 fix: no unaccounted figure until the whitelists are known; no mining row on market pages9ceed05 feat: read the NEAR bridge from the Quantus side — custody against mintedBuilding item 3 against every transfer on custody and the fee payer, as production has them indexed up to block ~176,178.
Correction 1: deposit addresses are not one-time. qzkz3nBAawgVa7oYWMcsx5GorCn4CtcFKvjyViQQFMWW9AS7i deposited three times: 0.2 QTC at 174,731, 1 at 174,760 and 5,990 at 174,829. The fee payer funded it before each sweep, at 174,730, 174,759 and 174,828. A rule expecting one sweep per address would have dropped two of them. 13 deposits come from 11 addresses.
Correction 2: the funding amount is not fixed. It is usually 0.0010166025 QTC, but it was 0.0010169025 before the 150,000 QTC sweep (173,222) and 0.0010168025 before the 5,990 (174,828). Presumably it covers the sweep's own fee, which depends on the transaction. Matching on the amount would miss the two largest deposits on the chain. The classifier matches on who and when: a transfer into custody from an address the fee payer funded at most max_sweep_lag blocks earlier (default 100; every deposit so far was funded exactly one block before).
Figures re-derived now (the issue's were from before the 5,990 deposit):
| | QTC |
|---|---|
| custody balance (chain state, read from bob) | 155,999.701002692 |
| other inflows (the 5 QTC seed, from qzozZq…, which was never funded and itself funded the fee payer at 169,160) | 5 |
| locked = balance − other inflows | 155,994.701 |
| deposited / withdrawn | 155,996.996 / 2.292 |
| net deposited | 155,994.704 |
| minted (qtc.omft.near ft_total_supply) | 155,994.698 |
| locked − minted | +0.003 |
The 0.003 between net deposited and locked is custody's own fees on its three withdrawals.
Finding, outside this item: the account route double-counts ordinary transfers. Every ordinary transfer emits both Balances::Transfer and Wormhole::NativeTransferred, with the same sender, recipient and amount: custody's 17 transfers are 17 of each. Store::account_flows sums both, so /accounts/qzmzw3… reports received = 312,003.992 QTC and receipts = 28, which is exactly double the real 156,001.996 and 14. The distribution table's queries read the same pair of event types, so they very likely double-count visible transfers in the same way; I haven't verified that one. The bridge query takes Balances::Transfer once, plus NativeTransferred only where the sender is the hidden sentinel (an exit, which has no Balances::Transfer).
35c7a2c fix: show the Ref pool's limit-order bids in its ladder35c7a2c fix: show the Ref pool's limit-order bids in its ladder/v1/chains/quantus/ref/pools reports counter_held 102,003.62 USDC, up from 45,000 at 16:51. The ladder on /quantus/market/ref-dcl shows only ~1,001 USDC of bids: 0.011 + 988.30 in two liquidity ranges, plus 13.29 at the current point.
The cause: the ladder is built from get_liquidity_range alone, so no limit order appears in it. About 101k USDC of posted bids is invisible, and a seller reading the ladder sees almost no bid side. The "USDC in the pool" field is right; the ladder under it is wrong by omission. Fixing that is the first part of this issue, ahead of the series.
Link to #39 item 5: the direct-call scan shipped in e78e627 already queues every transaction that calls the pool from shards 4 and 8, which includes order adds and cancels by whitelisted accounts. refdcl::fetch reads each whole but parses only swap events. If dcl.ref logs order events, parsing them there gives exact add and cancel times, and the posting account, with no new scan. Orders placed from other shards would still be missed, the same limit as for swaps.
016c042 feat: reconcile QTC's NEAR supply against every holder we name3078cb0 fix(ops): leave a still-syncing node out of 'blocks behind'fa5f844 feat(ops): a Grafana dashboard for the three NEAR nodese78e627 feat(api): read direct calls to the asset's pools on the shards they are signed one78e627 feat(api): read direct calls to the asset's pools on the shards they are signed onc483d51 fix(web): mark a section's nav link on its subjects' pages, not only its index/intents/inventory published a false zero for two minutesBetween about 12:59 and 13:01 EEST, /intents/inventory reported QTC held: "0" with solvers: 30. Before and after, it was 5,991.605 QTC (solver-ref-2.near) with solvers: 34. On-chain the balance never moved: mt_balance_of(solver-ref-2.near, nep141:qtc.omft.near) on intents.near reads 5,991.605328583476 at every height sampled from 218,774,276 to 218,776,876.
Cause, from crates/blackbeard-api/src/intents.rs: inventory() builds whitelisted from chain.ref_pools, and falls back to an empty list when that is None (unwrap_or_default()). Until the pool loop has read its pools (e.g. right after a restart), the whitelist accounts aren't asked. solver-ref-2.near has filled no token_diff trade, so intents_solvers() doesn't name it either. read_inventory then returns Ok with a total of 0, which replaces the good reading.
That breaks the rule IntentsListing follows: "no verdict rather than a false all-clear." A partial read is presented as a complete one, and a client watching inventory saw the market maker's entire QTC vanish and reappear.
Suggested fix (any one of these works):
ref_pools has been read at least once. Keep the previous Some(inv) meanwhile.partial when it asked fewer accounts than the last complete one.qtc_watch now ignores a reading that asked fewer solvers than the previous one, for up to about 20 minutes.
7cd2197), closing/quantus/market/ref-dcl has a Pool swap price panel, from /v1/chains/{chain}/ref/series?counter=nep141:….
RefPool::counter_token was documented as the counter token's contract, but it carries the nep141: prefix. The first cut of the chart added the prefix again and drew an empty frame. Caught by rendering the chart, and the doc comment is fixed.7cd2197 feat: chart the Ref DCL pool's swap price on its own market page7cd2197 feat: chart the Ref DCL pool's swap price on its own market pagef6b1e8a test(web): assert dates and hours, not one ICU build's word order21ec54b fix(web): label local times with their zone and keep UTC days on their own datef6b1e8a test(web): assert dates and hours, not one ICU build's word order21ec54b fix(web): label local times with their zone and keep UTC days on their own dateb85e74f) and verified on production| NEAR height | Side | Solver | Price | |---|---|---|---| | 218,751,935 | buy | solver-ref-2.near | $20.1288 | | 218,752,703 | buy | solver-ref-2.near | $20.1288 | | 218,754,997 | sell | solver-ref-2.near | $20.0483 | | 218,757,893 | sell | solver-ref-2.near | $20.0483 |
solver-ref-2.near holding QTC inside intents.near./quantus/market/ref-dcl. /quantus/market/near-intents carries a note pointing to it.Remaining, so left open: a price chart for the pool's swaps on ref-dcl (the near-intents chart stays intents-only, never mixed), and tracking shard 8 (option B) on the new hardware, which would close the direct-call gap.
b85e74f feat: index QTC's Ref DCL pool — its swaps, its ladder, and how much we seeb85e74f feat: index QTC's Ref DCL pool — its swaps, its ladder, and how much we seeVerified against the chain:
qtc.omft.near supply is 150,003.2.Corrections:
get_liquidity_range returns amount_l, not amount. It also returns two ranges the issue didn't list:
108159..108160, the current point's own slot, which the swaps have partly consumed and which holds both tokens108160..108200 with L 60,863: about 0.01 USDC above the current point, the only bidsvolume_x_in 3.994494 USDC and volume_y_out 0.198447 QTC, more than the two listed swaps sum to (3.984494 / 0.197949).Where things run (shard layout V3): intents.near is on shard 7, dclv2.ref-labs.near on shard 8, qtc.omft.near on shard 4. Our node tracks shard 7 only, so the dcl.ref swap event is logged where our node cannot see it. In tx BJEtX…, the relay's first intents.near receipt logs ft_withdraw (receiver dclv2.ref-labs.near, amount, the solver as account_id), intents_executed and mt_burn. Those are on shard 7 and visible.
Approach: option A, chosen over tracking shard 8 (which would mean about 5 h to add in place, roughly double the load on frankie, and a migration next week anyway):
intents.near outcome logs ft_withdraw to a tracked pool contract. The solver in that event is the trader.dcl.ref swap events for pools containing the asset. Pending transactions are persisted, because the process restarts several times a day.blackbeard-core, with the numbers above as tests. The current point's slot is reported from total_x/total_y less the other ranges, rather than split from L./:chain/market/ref-dcl, so a user's buy (a token_diff fill) and a solver's restock (a pool swap) can never be summed into one volume.d6654ac feat(api): flag QTC on NEAR that exceeds what exists outside the vesting pota13a6ef fix(api): pace the intents backfill only for someone else's endpointa07a3bc fix(api): sort solver inventory with sort_by_key2d80325 feat(api): solver inventory per side of each pair, read from our nodea07a3bc fix(api): sort solver inventory with sort_by_keyfb7148c feat(ui): a price chart on the market page, and its placeholder5d22137 feat(ui): a market route — /:chain/market and /:chain/market/near-intentsef2afa8 fix(data): a skipped NEAR height no longer stalls the intents scanef2afa8 fix(data): a skipped NEAR height no longer stalls the intents scan40bc2f6 fix(core): one swap per taker leg, and solver hops are never trades40bc2f6 fix(core): one swap per taker leg, and solver hops are never tradesec615ca ci: gate every branch, and make a "validate" dispatch ship nothingec615ca ci: gate every branch, and make a "validate" dispatch ship nothingce1884a fix(core): one swap per taker leg, and solver hops are never tradesad79d74 probe: do not merge (rust=false skip path, composite failure propagation)I looked for a "two takers, same pair" outcome to use as a test fixture. The first one, tx GTH3v6ff… at NEAR 218,371,635, is not two users. It is one account (1click-e2e_tests) making a round trip, wNEAR → Solana USDC → wNEAR, and the solvers routed both legs through USDT on Ethereum:
a5e8dc1b… (1click-e2e_tests) sol-USDC +1,335,270 wNEAR −287,513.95…
a5e8dc1b… (1click-e2e_tests) sol-USDC −322,478 wNEAR +68,591.62…
solver-priv-liq.near eth-USDT −1,335,408 wNEAR +287,513.66…
solver-multichain-asset.near eth-USDT +1,335,406 sol-USDC −1,335,272
solver-priv-liq.near eth-USDT +322,444 wNEAR −68,591.69…
solver-multichain-asset-escrow eth-USDT −322,445 sol-USDC +322,477
With wNEAR as the tracked asset, the current code does three wrong things:
solver-priv-liq.near's wNEAR/eth-USDT legs as a second trade, against a token the user never touched. They have no referral, so they fall into the pooled path.How common. Of 347 outcomes with a taker, 156 (45%) contain solver legs on a pair no taker traded: 377 such hop legs. The tokens in them are eth-USDT 284, eth-USDC 120, wNEAR 65, ETH 46, SOL 43, ZEC 17, … A QTC swap routed QTC → USDT → USDC would show a phantom QTC/USDT trade, priced from the solvers' internal leg.
Change to the plan. The fallback is decided per outcome, not per pair. If an outcome has any taker leg, every other leg in it is a fill or a hop and is never a trade. The pooled trade with kind: unattributed applies only to outcomes with no referral leg at all (solver to solver, 3 of 350). Both cases go in as fixtures.
ec615ca ci: gate every branch, and make a "validate" dispatch ship nothingb05b208 feat(config): follow QTC on NEAR Intents through our own nodecf227bf fix(data): read tx outcomes at INCLUDED_FINAL, not EXECUTED100d508 feat(ws): push each swap as it settles on NEAR Intents0d5ebd1 feat(ui): a market panel on the network page, from settled swaps onlyd40e17b docs(config): point the intents example at near.internalb05b208 feat(config): follow QTC on NEAR Intents through our own nodecf227bf fix(data): read tx outcomes at INCLUDED_FINAL, not EXECUTED100d508 feat(ws): push each swap as it settles on NEAR Intents0d5ebd1 feat(ui): a market panel on the network page, from settled swaps onlyd40e17b docs(config): point the intents example at near.internalNode. neard 2.13.4 on frankie tracks {"Accounts": ["intents.near"]}. Mesh clients reach it at https://near.internal; the public reaches it at https://near.blackbeard.observer (#29).
State sync needs inbound P2P. Peers deliver state by connecting back on TCP 24567, and the node was stuck until the office WAN forwarded that port to frankie. Once forwarded, it synced like this:
tier3_public_addr was not needed; STUN found the right address. The Postgres standby on the same box caught up fully meanwhile (from 32 days behind to 0.07 s).
Probe from bob against near.internal, at the tip:
| Check | Result |
|---|---|
| status | syncing: false, at FastNEAR's final height |
| chunk on shard 7 at a recent height | ok, 0.01 s |
| view calls to qtc.omft.near and zec.omft.near | UNAVAILABLE_SHARD, as expected. This is why views go to view_urls (FastNEAR), commit 6279b8f |
| EXPERIMENTAL_tx_status on a cross-shard swap (BBSuQgGz…, which withdrew via omni.bridge.near) | see below |
| wait_until | ours | token_diff events |
|---|---|---|
| NONE / INCLUDED_FINAL | 0.01–0.02 s | 5 of 5, plus intents_executed |
| EXECUTED_OPTIMISTIC / EXECUTED | 408 after 10 s | — |
The code used EXECUTED, so it would have stalled forever on the first swap that withdraws to another chain. Fixed in 1f921eb, which uses INCLUDED_FINAL.
End to end. I ran the indexer locally against near.internal, configured for wNEAR. It followed the tip with no errors, read 176 blocks in about a minute, recorded trades, and pushed one live on /v1/ws.
An observation about side inference. That pushed trade's taker leg had referral: 1click-protocol-fees. The leg carrying the referral can be a fee leg, not the user. side is already labelled inferred; this shows how weak the inference is, and it needs a better rule before anything leans on it.
Ready to enable. Merge intents-swaps, uncomment [chains.intents] with rpc_urls = ["https://near.internal"], and delete the gate assertion in config.rs.
1f921eb fix(data): read tx outcomes at INCLUDED_FINAL, not EXECUTEDdc68fe3 infra: publish the NEAR node as near.blackbeard.observerd5a0697 feat(ws): push each swap as it settles on NEAR Intents7aae351 feat(ui): a market panel on the network page, from settled swaps only7baa1f2 docs(config): point the intents example at near.internal6279b8f fix(data): send NEAR view calls to their own endpoints49f4d4b feat(api): index swaps settled by NEAR Intents, off by defaultThe free RPC is throttled too. FastNEAR's rpc.mainnet.fastnear.com:
-429 "Rate limits exceeded" after about 20 calls in a burst, and recovers in about 2 sFollowing one shard at the tip needs about 1.7/s before any transaction lookups, so there is no headroom. We'll run our own NEAR RPC node with TrackedShardsConfig::Accounts(["intents.near"]), which nearcore 2.13.4 supports. The archive is still needed for history before the node starts.
The swap in the sample was not ZEC→TRON. The counter tron-d28a265909efecdcee7c5028585214ea0b96f015.omft.near reports ft_metadata.symbol = "USDT": it's USDT on Tron. Tx 85uy9uCF… swapped 9,036.29 USDT for 7.0259 ZEC, about 1,286 USDT per ZEC. Reading the contract name gave the wrong token, which is exactly why stablecoins must be identified by contract id.
Implementation (branch intents-swaps, commit 49f4d4b, not pushed):
GET /v1/chains/{chain}/intents/swaps?after=<near_height>:<trade_index>&limit= returns trades oldest first, with indexed_from/indexed_to.GET /v1/chains/{chain}/intents/price?trades=N returns, per counter token:
listing block: ft_total_supply, the intents.near balance, whether 1Click lists the tokenreads, which states the blind spot in words[chains.intents], which asset/config/config.toml.tmpl leaves commented out. A test renders the template and fails if the block is enabled; I confirmed it fails when the block is uncommented. With the gate off, deploying adds three empty tables (migration 0009) and nothing else.intents.near, and listed on 1Click.Still to do: the frontend tile, and the ws push.
friday, october 2, 2026 — 23:02:49 utcPoint 2 above ("neardata as the only source") is wrong. Its free tier is rate-limited, and I measured the limit by hitting it:
HTTP/2 429
{"error": "rate-limit exceeded of 30 blocks per minute", "message": "Please get a plan from https://fastnear.com for higher rate limits"}
30 blocks a minute is 0.5/s. NEAR produces 1.69/s, so the free tier falls behind the tip immediately and could never backfill. The "skipped heights" I saw while probing were this limit, not gaps in the chain.
What does work on free endpoints, measured the same evening:
rpc.mainnet.fastnear.com: 60 sequential chunk {shard_id: 7} calls in 6.0 s, all HTTP 200. That is about 10/s against a need of 1.69/s.archival-rpc.mainnet.fastnear.com and archival-rpc.mainnet.near.org both answer chunk at 217,640,747 (qtc.omft.near's deploy height). That covers what the ~21 h retention of the non-archival RPC cannot.So the issue's original Option A is back as the live path: chunk on intents.near's shard per block, then EXPERIMENTAL_tx_status only for execute_intents transactions. The archival RPC handles backfill and any gap. Its known blind spot stands as the issue described it: intents arriving as cross-contract receipts rather than as transactions signed by intents.near are not read. The indexer should count those receipts where it sees them, so that the gap is measured rather than assumed to be empty.
One thing found while fetching the fixture: a failed receipt keeps its logs. NEAR rolls back state on a failed receipt but still reports the outcome's logs. So a token_diff counts only when the receipt outcome is a success and its intent_hash appears in intents_executed. Either check alone is not enough.
A paid FastNEAR plan (neardata at full rate) or NEAR Lake on S3 would remove the blind spot. Both cost money, so that is a decision for later, and the block source sits behind one interface so it can be swapped.
friday, october 2, 2026 — 22:41:31 utc/v0/quote with the wallet's own request shape (referral: quantus, no API key, which is what SwapService() sends) returns tokenIn is not valid / tokenOut is not valid. This is blocker #1 in the wallet's own mobile-app/docs/near-intents-swaps.md.supported_tokens (240).qtc.omft.near was deployed by omft.near at NEAR 217,640,747. Its only activity since is one storage_deposit by staging-ref-solver.near at 217,918,989. ft_total_supply is still "0" at 218,262,584.fba843df. In that commit the swap flow is a demo:
getUsdPrice hard-codes QTC at 50.0. The screen's "1 QTC = 0.00059146 BTC" is exactly 50 / 84,539, BTC's price on 1Click.getQuote never calls 1Click. It multiplies by that rate and adds a flat 0.5%.createSwap makes the deposit address 0x plus 40 random hex characters, even for a BTC deposit.onTap: () {}. That matches what is observed on the phone.0f3848e0, fd45f10d, 47e99a10). No release carries them: the last wallet tag is wallet-v1.7.0+142.enableSwap remote flag, so this demo is switched on for 144 users. A user who ignores the warning and sends ETH/USDC to the random 0x… address loses it. This is worth telling the wallet team.No QTC price exists anywhere today. The $50 is a constant in a demo, and the observer must never show it.
status.sync_info.earliest_block_height was 218,135,889 at 01:28Z. Option A alone cannot fill a gap longer than that, and this service restarts several times a day. The archival feed is required, not optional for gap repair.tx_status round trips. Measured on a quiet block: about 17 KB compressed (68 KB raw), against 0.9 KB for one chunk call. That is roughly 2.5 GB/day at 1.69 blocks/s, a fair price for one code path.intents.near. A transaction lands in its signer's shard. I verified it for 85uy9u… (signer intents.near). Any other relayer would be missed silently.token_diff, not per pair. Each diff already carries both legs, and one user intent can be filled by several solvers. Telling taker from maker is a heuristic: referral is optional and solvers can be implicit accounts. Volume is counted once per execution, as the sum of positive QTC across diffs, and the side is labelled as inferred. The wallet sends referral: quantus, so wallet-originated trades should be recognisable. That stays an expectation until the first one settles.ft_metadata.symbol. Anyone can deploy a token called USDC.ft_metadata exists only for nep141. nep245 and nep171 counters are excluded until handled.?window=: windows on this site are counts, not durations. Buckets use the NEAR block timestamp, the counterpart of authored_at. Every figure carries its trade count, because a "last price" from one trade hours ago needs to say so.near_intents indexer with the asset from config, plus /intents/swaps and /intents/price with prices in counter-token units and indexed_from/indexed_to. It also includes a listing-status block (supply, the intents.near balance, whether 1Click lists it) so the page says "no market yet" with evidence. It starts at the contract's deployment height (217,640,747), and it is tested end to end against nep141:zec.omft.near because QTC has nothing to index.Automated report for this issue, from /var/log/nginx/blackbeard.observer.access.log* on oolon (the #24 JSON log). It gives the numbers only. The decision is still open.
Span: 2026-09-22T12:03:25+00:00 → 2026-09-29T06:00:43+00:00, 23557 requests
135.181.130.74: 2026-09-22 73 (73 denied), 2026-09-23 42 (42 denied)
37.187.143.115: 2026-09-22 614 (614 denied), 2026-09-23 1235 (1235 denied), 2026-09-24 1234 (1234 denied), 2026-09-25 1237 (1237 denied), 2026-09-26 1235 (1235 denied), 2026-09-27 1234 (1234 denied), 2026-09-28 1234 (1234 denied), 2026-09-29 310 (310 denied)
Fingerprints: an empty UA polling summary?window=day, or a UA containing stardust.
No request matching either fingerprint got through.
No Referer, and an empty or non-browser User-Agent. Our own checks (127.0.0.1, and bob at 37.63.38.146) will appear here and are not candidates. Only hosting ASNs belong on the list.
| ip | ASN | requests | per day | UA | top path |
|---|---|---|---|---|---|
| 38.244.50.119 | AS174 COGENT-174 - Cogent Communications, LLC | 10 | 1 | curl/8.14.1 | /v1/leaderboard |
| 127.0.0.1 | unknown | 3 | 0 | curl/8.15.0 | /v1/chains |
| 103.175.215.228 | AS136841 MTMBALI-AS-ID - PT MITRA TELEMEDIA MANUNGGAL | 2 | 0 | curl/8.7.1 | /v1/chains/quantus/state |
| 37.63.38.146 | AS29580 A1BG_RSG A1 Bulgaria EAD | 2 | 0 | curl/8.18.0 | /v1/ws |
4 denied requests with a browser User-Agent (details on #25).
Posted by ~/.local/bin/bb-origin-report via a systemd user timer.
Automated report for this issue, from /var/log/nginx/blackbeard.observer.access.log* on oolon (the #24 JSON log). It gives the numbers only. The decision is still open.
Span: 2026-09-22T12:03:25+00:00 → 2026-09-29T06:00:43+00:00, 23557 requests
/v1/ws upgrades by Origin (not denied)| origin | upgrades | 101 | distinct IPs |
|---|---|---|---|
| https://blackbeard.observer | 1843 | 1594 | 92 |
| (none) | 1 | 1 | 1 |
Read against the issue: https://blackbeard.observer is our UI, (none) is non-browser clients that an Origin check would not affect, and localhost is dev. Anything else is a third-party page.
/v1/* by Origin, for comparison| origin | requests |
|---|---|
| (none) | 10418 |
| https://blackbeard.observer | 5 |
8452 denied requests, 4 with a browser User-Agent.
| ip | requests | first UA |
|---|---|---|
| 37.187.143.115 | 8333 | `` |
| 135.181.130.74 | 115 | stardust-devops |
| 167.233.143.69 | 1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, l |
| 5.135.137.201 | 1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, l |
| 51.178.179.51 | 1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, l |
| 95.217.114.145 | 1 | Mozilla/5.0 (compatible; SERankingBacklinksBot/1.0; +https://seranking |
Browser User-Agents among the denials would be VPN users caught by the Hetzner/OVH list.
Posted by ~/.local/bin/bb-origin-report via a systemd user timer.
0de203c fix(ui): link the repositories at their new paths6c79718 docs: issues live at blackbeard/observerConsidered and rejected, 2026-09-22: an OPNsense drop rule in place of the nginx 403.
The appeal was a silent timeout instead of a denial, so an operator could not tell they had been blocked. It was rejected because OPNsense filters by IP and port and cannot see SNI, and 87.227.159.178 fronts every site oolon serves: about 70 enabled names, including git.lair.cafe, rpm.lair.cafe, mail.lair.cafe (plus the Stalwart passthrough), the quantus./planck. node endpoints, and several unrelated sites. A drop keyed on Hetzner and OVH would silently break package installs, git and mail for every client on those networks, and the evidence would leave the nginx log that this issue's check reads.
Two scoped alternatives were discussed and not adopted: return 444 in the vhost, and a blackhole route in oolon's SNI stream router keyed on SNI plus the deny list. Either stays open if the report here shows the 403 is teaching the pollers to move.
Scheduled together with #25: bb-origin-report.timer on grenade's workstation (2026-09-29 09:00 Europe/Sofia, Persistent=true) posts a comment here covering the four checks above:
It posts numbers only. The decision stays with a human.
tuesday, september 22, 2026 — 12:20:14 utcScheduled: a systemd user timer on grenade's workstation (bb-origin-report.timer, 2026-09-29 09:00 Europe/Sofia, Persistent=true) runs ~/.local/bin/bb-origin-report. The script reads the #24 JSON log over ssh and posts the numbers here as a comment: WebSocket upgrades by Origin, REST by Origin, and denials with browser User-Agents. It posts numbers only. The decision stays with a human.
It is not a cloud routine because a cloud agent can reach neither oolon nor this Gitea.
tuesday, september 22, 2026 — 12:17:51 utc6d8c3fe infra: a dedicated access log for the public vhost, and deny hosting networks501a9e6 fix(data): re-read event history under the current keep policy82a4ac9 feat(api): wormhole deposits by recipient82a4ac9 feat(api): wormhole deposits by recipient54db950 infra: public node endpoints quantus. and planck.blackbeard.observerefaf487 feat(ui): a caution mark where the tagline's bullet wasb04a45f feat(ui): a face on the about page6ed9a6c feat: an about route, and somewhere to say thanks