Methodology

How Stabledesk measures stablecoin activity on the Arc testnet. ← Back to the terminal

Stabledesk indexes on-chain Transfer events for the Arc testnet stablecoins and turns them into time series, supply metrics and flow analytics — with a deliberate focus on real economic volume, not raw event counts.

Data source

Everything is read directly from the chain — no third-party data provider. Blocks and logs come from the Arc testnet public RPC (with fallback endpoints), chain ID 5042002. Arc is an EVM chain where gas is paid in USDC and blocks are ~0.5s with no reorgs.

Two Transfer logs, one movement. Arc implements EIP-7708, so a native value movement emits its own Transfer log from the system address 0xff…fe, in addition to any ERC-20 Transfer the token contract emits. Because gas on Arc is paid in USDC, every transaction on the chain produces one — on Arc testnet, measured in August 2026, that address emitted more Transfer logs than the USDC contract does. Counting both would report the same money twice. We read logs filtered by token contract address, so a native log is never seen and never counted; the discovery probe that looks for assets we do not track excludes this address by name and says so on every run.

Supply is reported per denomination, and never summed across them. Arc carries stablecoins in more than one currency — USDC, USYC and USDT in dollars, EURC in euros, with cNGN, QCAD, MXNB and ZARU first seen on Arc testnet in August 2026. Adding their face values together produces a figure that is a quantity of nothing: measured here, that sum was 86% euros, so the headline number of a dollar-denominated chain was mostly not dollars, and nothing said so. Converting between currencies would require an exchange rate, and there is no price feed and no oracle anywhere in this project — the same rule that makes the fee figures directly measured rather than estimated. So each denomination is published on its own, a token's share is computed against its own currency, and a tracked asset whose denomination we have not declared is reported separately rather than folded into a total. The same applies to volume: until August 2026 the 24-hour figure was a sum of transferred amounts across currencies, so it answered no question, and every token's share was measured against that mixed total. Volume is grouped the same way now, and each share is a share of the volume moved in its own currency.

We track the Arc testnet stablecoins over their ERC-20 interface: USDC (native gas), EURC (euro), USYC (yield / MMF, 2 contracts), USDT (stablecoin), QCAD (Canadian dollar), MXNB (Mexican peso), cNGN (Nigerian naira), ZARU (South African rand). That list is generated from the running configuration rather than written here, so it cannot drift from what is actually indexed — the full set of contract addresses is in /openapi.json and on /v1/status. An asset carried by more than one contract is measured across all of them; see What is counted as a stablecoin below. A historical indexer backfills recent history on first run, then keeps indexing forward and persists aggregates in SQLite.

Three volume measures

Naïve volume badly overstates real activity, for two independent reasons. We correct each with its own filter, and publish all three figures so you can see exactly what every step removes.

1 · Raw volume — every Transfer event, unfiltered.

2 · Real volume — a transaction-level filter. A single user action often emits several Transfer events (routing hops, contract internals), so within each transaction we keep only the largest transfer per token and discard the rest. Per token, not per transaction: a swap moves two different assets in one transaction, and collapsing it to a single leg would erase the smaller asset's volume entirely.

3 · Adjusted volume — real volume, plus an address-level filter. Some addresses are infrastructure rather than economic actors: market-making bots, faucets, sequencers, test harnesses. Any address whose activity rate exceeds 1,000 transfers or $10M of volume per month is flagged. A transfer is dropped when both of its ends are flagged — infrastructure talking to infrastructure. The thresholds come from the Visa / Allium onchain analytics methodology.

Why "both ends", not "either end". Visa drops a transfer if either end is flagged. That is right for retail-shaped chains, where most transfers run user-to-user and a flagged counterparty is a strong signal of noise. Arc testnet is hub-and-spoke — almost every transfer touches a faucet, router or sequencer — so the "either end" rule deletes genuine payments along with the churn — measured on Arc testnet in July 2026, it removed 99.9% of volume and stopped describing anything. A bot paying a user, or a user funding an exchange, is real value delivered to a real party, so we keep it. Only bot-to-bot movement is discarded.

How flags are applied. The monthly thresholds are converted to a daily rate, and each address is measured against its own observation window: the first block we saw it in, to the last, converted to days at the measured block time. So an address that moves $2M in a day is flagged and an address that moves the same $2M across a month is not, which is what makes the rule a rate rather than a total. Each window is floored at one full day — an address first seen inside a single block has a near-zero span, and without the floor any activity at all would be an infinite rate. The flag set is recomputed every minute, and the terminal lists the currently flagged addresses with the window and the two limits each one was judged against, so the arithmetic can be redone rather than trusted.

What this replaced, and why it mattered. Until August 2026 the limits were pro-rated to our retention window instead — a rolling ~7 days — while the per-address totals they were compared against accumulate from the first time an address is seen. Those are different spans, and the second one keeps growing. An address active for two months was measured over two months and judged against seven days, so the same behaviour was flagged or not depending on how long this deployment had been running, and adjusted volume drifted downwards over a deployment's lifetime with nothing on the page saying so. The rule described here is now the rule the code applies; every flagged row carries the window it was actually measured over.

One transitional caveat. Addresses first seen before this change have no recorded start block, so they fall back to the one-day floor — which over-flags them, and makes adjusted volume a lower bound rather than an overstatement. It corrects itself as each address is seen again and its span starts accumulating, so the effect fades within about a week of the change. A database that starts fresh is never affected.

Flags are derived from the same history they filter, so on a cold start the initial backfill is indexed before any address has been flagged. Rather than ship a figure we know is wrong, the indexer re-reads that range once the first pass has revealed the high-frequency addresses and rewrites the adjusted numbers.

The flag set is capped at 20,000 addresses, ordered by volume, so a pathological window cannot load an unbounded set into memory. This is a threshold like any other and it belongs here: when more than 20,000 addresses meet the rate limits above, which addresses get flagged stops being decided by those limits and starts being decided by the cap and the ordering. Adjusted volume is then a lower bound on what the published rule would exclude — the lowest-volume qualifying addresses fall outside the set and their bot-to-bot transfers stay counted. Raw and real volume are unaffected. Whether the cap is currently binding is published on every response: /v1/addresses/filtered reports qualifying (selected by the thresholds), flagged (actually used), cap and atCap, and carries an explicit warning while the two differ.

Bridged liquidity vs. issuance

The problem. Circle Gateway holds one USDC balance that is spendable across every chain it supports. Instead of pre-positioning liquidity chain by chain, an operator draws it onto a chain on demand — Circle reports replenishment in under 500 ms, and roughly $410M of crosschain volume settled through Gateway between March and June 2026. On-chain, that arrives as a mint. A naïve supply tracker reads that mint as new issuance and reports that demand for USDC on this chain grew. It didn't: the same balance moved here, and it can move away again just as fast.

What we do. Gateway's contracts are in our registry, and every block range is scanned for logs they emit. Any mint, burn or transfer in a transaction that Gateway touched is attributed to it and reported separately. Attribution is per transaction, not per counterparty: the Transfer event of a Gateway mint names the end recipient rather than the minter, so matching on addresses alone would miss it entirely.

Two figures, always both. netIssuance24h counts every mint minus every burn — unchanged, and what you get if you scan the chain yourself, so our numbers stay reconcilable against yours. organicNetIssuance24h takes Gateway's net out. Where they diverge, the gap is the bridged liquidity. Gateway's share of real volume is published the same way, because a deposit into Gateway is a genuine transfer — it just isn't anyone paying anyone.

Where this applies. Arc testnet is a Gateway chain and is measured as described: Gateway's contracts are 0x0077777d7eba4688bdef3e311b846f25870a19b9 and 0x0022222abe238cc2c7bb1f21003f0a260052475b. Where Gateway is not deployed, every bridge-adjusted figure is returned as null rather than 0: nothing was measured, so nothing is claimed — a zero here would read as "we checked and there was none".

What is counted as a stablecoin, and what isn't

Tracked assets are listed by contract, not by symbol, because a symbol can be deployed more than once on the same chain. Arc testnet carried, as of August 2026, two independent USYC contracts — one an EIP-1967 proxy holding 1.38M and essentially dormant, one a standalone contract holding 12.2M with all of the activity (722 transfers against 1, over the same six-day sample). Nothing on-chain says which is canonical, and we will not guess: both are measured, and the chain's USYC supply is reported as the sum. Both addresses appear in the API's token list. If Circle publishes a canonical address, the other is removed and this paragraph goes with it.

Wrapped USDC is deliberately not counted as issuance. Its supply is exactly the USDC it custodies — 68,920,819.86 on both sides when measured — so reporting it as a separate stablecoin would count the same dollars twice, once under USDC and once under WUSDC. It is in the registry instead, where the USDC it holds is measured as value locked in a contract. Same reasoning as Circle Gateway above: a wrapper moves value, it does not create it.

Native USDC transfers are a known blind spot. USDC is Arc's gas asset, so it exists twice: as the ERC-20 contract we index, and as the chain's native balance, whose movements the protocol surfaces at the reserved address 0xffff…fffe at 18-decimal scale. Measured over a 44-hour sample, 87% of native transfer events are exact duplicates of an ERC-20 event we already count — same transaction, same parties, same amount — so indexing that address would roughly double every USDC figure on this site. The remainder is real and uncounted: 24% of native transfer events, but 4.1% of native volume, and inspection of a sample showed most of it is msg.value being forwarded between contracts inside one transaction, which is exactly what the real-volume filter removes anyway. Plain wallet-to-wallet native payments exist and are not counted. We would rather state the gap than close it with a number that double-counts.

Network fees & economics

Gas on Arc is paid in USDC. That makes fee metrics unusually direct: a fee figure is already a dollar figure, with no token price, no exchange rate and no oracle in the path. We read fees from transaction receiptsgasUsed × effectiveGasPrice, summed per block — which captures the priority tip as well as the base fee.

Sampling. Receipts only exist per transaction, and at ~0.5s blocks fetching every one would mean ~172,000 requests a day against a rate-limited public RPC. So we sample a few blocks per poll, at a randomised offset behind the head. Each sampled block is exact; the per-day and per-window figures derived from them are extrapolations. The terminal and the API always report how many blocks a fee number rests on — an estimate is never presented as a measured total.

Cost to move $1M divides total network fees by real volume, not adjusted volume. The fee total in the numerator includes every transaction, high-frequency addresses included; dividing it by only the non-bot volume would price the whole network's cost against a fraction of its throughput and badly overstate it.

TVL & the protocol registry

TVL is measured as stablecoin balances held by contractsbalanceOf on each indexed asset, for every address we have established has bytecode. On a chain where value is denominated in USDC this is what TVL means: there is no volatile base asset to price and no LP share maths, so no per-protocol adapter is needed. On Ethereum the same method would be worthless; here it is the direct measurement.

Only contracts count. Contract-ness comes from eth_getCode, never from a heuristic. A balance sitting in a plain wallet is someone's money, not value locked in a protocol, and is excluded.

Unattributed value is reported, never hidden. A contract holding a balance that no registry entry claims is counted in the chain total and listed separately as unattributed. We publish the share of TVL we can actually name, next to the total — an ecosystem page that quietly drops what it cannot label is understating the chain, and one that assigns it to a plausible protocol is inventing data.

Registry provenance. Each protocol entry records how we know its contracts: canonical (a deterministic address identical on every EVM chain), team (the operator confirmed it), or observed (we found and classified it ourselves — which is not a claim about who operates it). Only the first two are marked verified. Two entries may never claim the same contract, because that would double-count its balance.

How many contracts are measured, and which. One pass reads balanceOf for every tracked asset against every target, so the number of targets per pass is capped — at 600 — to keep a pass inside the public RPC's rate limit. That budget is split rather than spent on the same contracts every time. Registry addresses are always read, because a listed protocol with no measured balance is indistinguishable from one nobody looked at. The 300 highest-value contracts are re-read every pass, because they decide the total and a stale reading there moves the headline figure. The remaining budget rotates through every other known contract in address order, in two lanes. The fast lane carries contracts that hold a balance, have never been scanned, or moved a tracked token within the retention week — the rows the total is made of, plus the ones that could change it. The slow lane re-confirms contracts already verified empty and quiet, on a longer published cycle; any activity promotes a contract back to the fast lane on the next pass. The split exists because discovery admits every contract seen emitting a Transfer, and on a test network that universe grew tenfold in four days — a single flat rotation was spending most of its budget re-reading zeros while actual holders queued behind them. Contracts enter that universe from two sources: addresses seen moving a tracked stablecoin, and contracts seen emitting any Transfer on chain — the second because the first is kept to a rolling week, so a contract that received a stablecoin and then sat still would otherwise never be measured at all. That is the wrong thing to miss: holding value without moving it is what TVL is. A few slots per pass are reserved for the stalest readings still counted in the total, whatever their provenance — so oldestReadingMs is bounded by construction, not by the selection policy staying consistent with every balance the table has ever recorded.

What this replaced. Until August 2026 the same top 600 by balance were scanned on every pass, and the rest were never scanned at all — 600 of 2,105 known contracts, so the published total covered 29% of them. That is a systematic blind spot rather than a sampling one: the same contracts were missing every time, and waiting did not fix it. Simply raising the ceiling is not the remedy either — a pass already spends 90 seconds in deliberate inter-batch delay inside a five-minute cycle, and tripling the targets would make a pass outlast its own schedule.

The cost, stated. Rotating means some balances in the total were read a few cycles ago. TVL is a level and moves slowly, so this buys complete coverage for very little accuracy — but a figure that mixes fresh and older readings has to say so. /v1/tvl publishes coverage.cycleLength (how many passes cover everything), coverage.oldestReadingMs (the oldest reading still counted), and coverage.atCap. Note that atCap now means "one pass does not cover everything", not "the rest is never read" — different claims about the same number.

Known limits. Value held in assets other than the indexed stablecoins is invisible to this method. A protocol's own accounting may therefore be higher than ours, and where we know that to be the case the entry says so. TVL history begins the day measurement began — nothing is back-filled or estimated. Registry submissions and corrections: PROTOCOLS.md.

Metric definitions

MetricDefinition
Raw volumeSum of every Transfer event value.
Real volumeSum of the largest transfer per transaction, per token (routing hops & contract internals removed).
Adjusted volumeReal volume, minus transfers where both ends are flagged high-frequency addresses.
TransfersCount of transfer events in the period.
Mint / BurnTransfers from / to the zero address 0x000…000 — supply created / destroyed on-chain.
Net issuanceMint − Burn over the period: net new supply minted on-chain.
SupplyERC-20 totalSupply() read live from each token contract.
DominanceA token's supply ÷ total tracked stablecoin supply.
VelocityReal transfer volume per day ÷ supply — how often the supply turns over.
Active addressesDistinct addresses that sent or received in the trailing window (e.g. 1h).
Size distributionHistogram of transfer amounts bucketed by magnitude, over the retained transfer window (published with the counts, since a row cap makes its span depend on throughput).
Avg fee / txTotal fees ÷ transaction count over sampled blocks, in USDC. Measured from receipts, not estimated from a gas assumption.
Fees / dayMean fee per sampled block × blocks per day — a run rate, not a cumulative total.
Cost to move $1MTotal network fees ÷ real volume, scaled to $1M. What the network charges to move a million dollars.
Flagged addressAn address exceeding 1,000 transfers or $10M volume per month, as a rate over its own observed span (floored at one day).
TVLSum of indexed stablecoin balances held by addresses with bytecode, read with balanceOf.
Attributed TVLThe part of TVL held by contracts a registry entry claims. The remainder is reported as unattributed.
Protocol "moved"Transfer volume across a protocol's contracts over the retained window (~7 days) — not 24 hours.
ObservedA listed protocol we have seen hold a balance or move value. Distinguishes a live entry from one only listed.

Timestamps & freshness

Arc never reorgs, so a transfer's timestamp is derived from its block number rather than by fetching a header for every block — which on a rate-limited public RPC is not affordable. Each indexed range carries the real timestamps of its own first and last block, read in the same request as the logs, and blocks in between are interpolated across them. Ranges are 500 blocks, so the error is bounded by however much the block time varies inside four minutes of chain.

What this replaced. Timestamps used to be extrapolated from a single rolling anchor at the average block time — and that average was initialised to an assumed 500 ms and only measured once the first live poll ran, which happens after the initial backfill. So a freshly-seeded history was timestamped at an assumption: on a chain running at 1s, the far end of a 20,000-block backfill landed nearly three hours from where it happened. Per-minute aggregates are additive, so that is not a figure that can be repaired later. The anchor still exists as a fallback for a range whose headers came back empty.

Which clock the windows use. Minute buckets are keyed by chain time. So every rolling window — 24h volume, the fee window — ends at the newest minute actually measured, not at our wall clock, and never at a time the data has not reached. On a healthy chain those are seconds apart. When they are not, windowEnd says which instant the window ends at and clockSkewSec says how far that is from the wall clock. Anchoring to the wall clock instead would publish "24h volume: 0" over a database holding days of transfers, which is the failure this makes visible rather than silent.

How much history, and of what

Three retentions, because the tables answer different questions:

Per-minute aggregates — rolling 7 days. A row per minute per token grows without bound, so it is a window.

Per-day aggregates — kept indefinitely. Before they are pruned, minutes are rolled into their day in the same transaction that deletes them, so every minute is counted exactly once and the two tables never overlap. A row per day per token is about a kilobyte a year, so there is no reason to throw it away. Ranges past 7 days are answered from this rollup, and every series response states which table answered (source) and how far back the record reaches (recordBegan) — a 90-day range drawn from a rollup that started last week is a one-week series wearing a 90-day label unless it says so.

Individual transfers — a 24-hour window, bounded by row count. This is what the size distribution, the per-token recent lists and an address's recent activity are read from, and the row cap means the span it covers is a function of how busy the chain is. So the span is measured and published rather than assumed. It was previously capped at 1,200 rows, which at the ~954k transfers a day Arc testnet was doing in July 2026 is 114 seconds — every one of those surfaces was describing the last two minutes while being labelled as something broader.

Largest transfers — top 100 per day per token, kept 180 days. Recorded separately for the same reason: "the largest transfer" read out of a row-capped table only ever means "the largest of the last few minutes". /v1/transfers/largest takes an explicit days and states the window it answered over.

Caveats

This is a testnet prototype (v0). Supply figures are inflated by faucet mints and carry no real value. Address labels are a small curated / heuristic set and will grow over time. Figures are best-effort from public RPC data, may lag or reset, and are provided for informational purposes only — not investment advice.