Stable Tape · developer reference
Stable Tape for developers
Stable Tape publishes the current ODS reading, dated records, market-structure data, an embed, and a research oracle. For each interface, this reference states what it returns and whether Stable Tape supports it for public use.
Public endpoints need no key, account, or registration. They are read-only and accept no request body. npm run test:developer-api-docs checks the documented behaviour against the implementation.
register
Choose the right interface
The interfaces below fall into three groups. Each label is written beside the row so it remains clear in print, greyscale, or a cropped screenshot.
- Public. A supported read-only contract. CORS-open to any origin, documented here, and covered by the documentation test. Field names and units are not changed without changing this page.
- Research. Real and readable, but published for research. The on-chain oracle is a second, independent publication of the same reading; it is not a citable record and it carries no availability commitment.
- Internal. Reachable, but not offered as a public contract. These routes accept only first-party origins and appear here so developers who see them in a network log do not mistake them for public APIs.
| Route | Method | Returns | Freshness | Register |
|---|---|---|---|---|
| /api/ratio | GET | Current ODS reading: unrounded and display share, 1-in-N, supply, U.S. M2, M2 observation date, sources | Recomputed per request, behind a 5-minute shared cache | Public |
| /api/tape?window=daily | GET | One frozen canonical Daily Tape record, in full | Daily, cut at 12:00 UTC | Public |
| /api/tape?window=daily-history | GET | Newest-first page of reduced Daily Tape records, plus declared gaps | Daily, appends | Public |
| /api/issuers | GET | Issuer supply and share snapshot behind /issuers | Recomputed per request, behind a 5-minute shared cache | Public |
| /api/chains | GET | Per-chain supply and 24h change behind /chains | Recomputed per request, behind a 5-minute shared cache | Public |
| /api/currencies-supply | GET | Supply by peg currency behind /currencies | Recomputed per request, behind a 5-minute shared cache | Public |
| /widget | GET | Embeddable HTML document that reads /api/ratio | Reads on every load | Public |
| /api/v1/oracle/summary | GET | Decoded on-chain summary account, big integers as strings | Follows the on-chain write cycle | Research |
| /api/v1/oracle/snapshot | GET | Summary plus the chain, issuer and token books, with a consistent flag |
Follows the on-chain write cycle | Research |
| /api/jupiter-solana-route | GET | Read-only Jupiter quote proxy. Not an oracle read, not versioned | Per request | Internal |
/api/tape?window=daily, /api/tape?window=daily-history, or the dated /tape/YYYY-MM-DD permalink. Those return frozen canonical records. /api/ratio is a current reading, not a record, and the on-chain oracle is a research publication of the same reading. Neither should be cited as a record.
what the benchmark is for
A maintained benchmark, not another raw feed
Stablecoin supply and U.S. M2 are public data. ODS makes their comparison consistent through fixed sources, calculation, rounding, and dated records that do not change when upstream feeds do.
These endpoints let a product, article, dashboard, or agent use the published reading without rebuilding those choices. Methodology documents the sources and caveats.
public · benchmark and records
Benchmark and record API
Three public read-only endpoints carry the benchmark itself. All are CORS-open to any origin, take no authentication, and return application/json.
GET/api/ratio
Current ODS reading
Recomputed on each request from the same two sources the benchmark always uses, then served behind Cache-Control: public, s-maxage=300, stale-while-revalidate=600. It takes no parameters. It is a current reading, not a frozen record, so successive calls can differ. New integrations should read odsPctRaw and odsPctDisplay.
$ curl -sS 'https://stabletape.com/api/ratio'
// Illustrative values. Every field the endpoint returns is shown. { "odsPctRaw": 1.3308184918420052, // preferred; unrounded PERCENT, not a 0-1 ratio "odsPctDisplay": "1.33", // preferred; string, fixed 2dp "dddPctRaw": 1.3308184918420052, // legacy compatibility alias "dddPctDisplay": "1.33", // legacy compatibility alias "oneInN": 75, // reciprocal denominator; round(100 / odsPctRaw) "stablecoinSupplyUsd": 308153683423, // whole USD "usM2Usd": 23155200000000, // whole USD "m2Date": "2026-06-01", // FRED M2SL observation date, always a month start "m2DateLabel": "JUN 2026", "updatedAt": "2026-08-06T12:04:11.318Z", "sources": { "stablecoins": "DeFiLlama", "m2": "FRED M2SL" } }
"status": "fallback-m2", sources.m2 becomes "bot/fallback.json" instead of "FRED M2SL", and updatedAt reports the stored snapshot's timestamp rather than the time of the request. Treat the presence of status as the signal that M2 is not fresh. If the stablecoin supply fetch fails instead, no reading is invented: the endpoint returns 502 with an error field.
/api/ddd, dddPctRaw and dddPctDisplay remain aliases for the same Onchain Dollar Share series so existing integrations do not break. Prefer /api/ratio and the odsPct* fields for new work. The oneInN machine field is unchanged; express it publicly as “$1 of stablecoins for every $N of US M2.” See the terminology history for the naming boundary.
GET/api/tape?window=daily
One frozen Daily Tape record
Returns the most recent frozen canonical Daily Tape record in full. Add &date=YYYY-MM-DD to fetch a specific date. A record carries the record date and window, the benchmark block, the primary market-level dailyAggregateChange, the separate constituent-resolved movement object, provenance and integrity fields, the selected headline and its evidence line under display.signal, and the record's canonical status. dailyAggregateChange is derived from accepted market totals on consecutive calendar-day records; it is not written into or hashed as part of the frozen record. The existing flattened movement fields remain for compatibility. Records frozen under ODS also carry benchmarkIdentity: { "seriesId": "usd-stablecoin-supply-relative-to-us-m2", "terminology": "ods-v1" }; older metadata-free records retain their publication-era DDD vocabulary.
$ curl -sS 'https://stabletape.com/api/tape?window=daily' $ curl -sS 'https://stabletape.com/api/tape?window=daily&date=2026-06-25'
canonical: true can be cited. Each dated record also has a share card at /daily-tape-card.png?date=YYYY-MM-DD, square variant at /daily-tape-card-square.png?date=YYYY-MM-DD. The canonical Daily Tape series starts 2026-06-20. Records describe snapshot-to-snapshot supply movement, not transaction-level mint or burn events.
dailyAggregateChange.deltaUsd: current accepted market total minus the prior calendar-day accepted market total. deltaPct is that delta divided by the prior total and expressed as a percentage. The separate movement object remains the constituent-resolved quantity and is also projected through the historical flattened fields such as netIssuerMovementUsd. A LEVEL_ONLY record may carry available Movement while movement.status is "unavailable". Never use the aggregate field as issuer, chain, currency, mint or burn attribution. If either market endpoint is invalid, withdrawn, missing or non-consecutive, only dailyAggregateChange becomes unavailable with null deltas and a reasonCode.
GET/api/tape?window=daily-history
Recent frozen Daily Tape records
Returns the newest frozen Daily Tape records in newest-first order. The default response contains up to 7 records. A caller that needs a longer run of history asks for a larger bounded page with ?limit=N, up to a maximum of 90. The archive decides the rest: if fewer eligible records are stored than the number requested, fewer are returned, so limit=90 returns 90 records only once 90 records exist.
Each entry is a reduced projection of the frozen record, not the complete stored record. It carries the record date, the snapshot window, supply before and after, primary dailyAggregateChange, a reduced constituent movement object, the existing flattened constituent fields, the largest expansion, contraction and absolute supply delta, the tracked row count, the selected headline, the benchmark block, and the record's canonical status. Full per-row movement and frozen-record integrity fields stay on the dated record at /api/tape?window=daily&date=YYYY-MM-DD; the daily aggregate block itself carries the two dated record hashes needed to reproduce it.
| Parameter | Default | Accepted | Behaviour |
|---|---|---|---|
| limit | 7 | 1–90 | Maximum number of records in the response. A value above 90 is clamped to 90, so the response stays bounded however long the archive grows. Any value that is not a plain positive integer, for example an empty, negative, fractional, exponential or non-numeric value, normalises to the default of 7 and still returns HTTP 200 rather than an error. A repeated limit uses the first occurrence. |
Omitting limit is the long-standing behaviour and is unchanged: an existing caller that sends no parameter keeps the same seven-record response it received before the parameter existed. Responses are CORS-open and sent with Cache-Control: public, max-age=300, stale-while-revalidate=600.
$ curl -sS 'https://stabletape.com/api/tape?window=daily-history'
$ curl -sS 'https://stabletape.com/api/tape?window=daily-history&limit=90'
// illustrative values; each record is a reduced projection, newest first { "status": "ok", "count": 41, // whole stored archive, not the returned page "records": [ { "date": "2026-07-29", "from": "2026-07-28T12:00:00.000Z", "to": "2026-07-29T12:00:00.000Z", "movement": { "status": "available", "netDeltaUsd": -250060285, "netDeltaPct": -0.08111668888574356, "expandedUsd": 426210093, "contractedUsd": 676270378 }, "netIssuerMovementUsd": -250060285, "expandedUsd": 426210093, "contractedUsd": 676270378, "dailyAggregateChange": { "status": "available", "methodVersion": "daily-aggregate-change-v1", "deltaUsd": -250019746, "deltaPct": -0.08110354030153145, "provenance": { "priorRecord": { "date": "2026-07-28", "recordHash": "0b442c3b...", "totalSupplyUsd": 308272296216 }, "currentRecord": { "date": "2026-07-29", "recordHash": "1b60c4d0...", "totalSupplyUsd": 308022276470 } } }, "benchmark": { "totalSupplyUsd": 308022276470, "m2Used": 23155200000000, "m2AsOf": "2026-06-01", // FRED M2SL reference month "sharePctRaw": 1.33025098, "sharePct": 1.33, "oneInN": 75 }, "canonical": true, "recordStatus": "canonical" } ], "gaps": [ { "date": "2026-07-30", "type": "gap", "reason": "noon_anchor_unavailable" } ] }
gaps with a reason, and is never fabricated into a record. Never assume a missing day equals zero movement. count reports the whole stored archive, so it can be larger than the number of records returned; the returned page is always the newest slice of the same ordering, and a smaller limit gives a strict prefix of a larger one.
benchmark block carries the inputs to the published calculation: total stablecoin supply, the U.S. M2 figure used, the M2 reference month, the raw and display-rounded share, and the 1-in-N value. Every canonical record therefore resolves back to the division it came from. The window does not provide M2 release dates, M2 vintage history, or revision history; the M2 figure is the monthly observation held constant until the next reference month. Early records stored before the canonical series began are still returned, flagged canonical: false with a recordStatus, rather than silently dropped.
public · structure
Structure endpoints
Three further public endpoints provide the structure data documented in Methodology. Each one backs the corresponding Structure route, is CORS-open, and is served with Cache-Control: public, s-maxage=300, stale-while-revalidate=600. Like /api/ratio, they are current snapshots rather than frozen records, so they should not be cited as records.
| Route | Backs | Top-level keys |
|---|---|---|
| GET /api/issuers | /issuers | The issuer snapshot object, including issuers and history. Each issuer row carries its symbol, supply, share and the static metadata Stable Tape maintains for it. |
| GET /api/chains | /chains | Per-chain totals summed across every USD-pegged asset, with the previous-day figure alongside the current one so a 24h change can be derived. |
| GET /api/currencies-supply | /currencies | currencies, totals, summary, trackingStartedAt, annualAvailable, annualNote, generatedAt. Note that annualAvailable is false: there is no 1Y currency series behind this endpoint yet, and the response says so rather than implying one. |
error field rather than a stale or partial figure.
citation
Attribution guidance
Please attribute Stable Tape and link to the relevant dated Daily Tape record or to the methodology page when citing the benchmark.
- Benchmark: cite Onchain Dollar Share (ODS) with a link to stabletape.com/methodology. ODS measures USD stablecoin supply as a share of U.S. M2. It is a supply-scale benchmark, not a complete adoption metric.
- Daily Tape records: dated, frozen records can be cited and shared, and each carries a selected headline and its own dated card. Link to stabletape.com/tape and include the record date. Current readings from
/api/ratio, from the widget or from the homepage are not citable until frozen as a dated record. - Movement language: use expanded supply, contracted supply and snapshot-to-snapshot movement. Daily Tape is not a transaction-level mint or burn tracker unless on-chain event evidence is shown.
- Sources and caveats: stablecoin supply from DeFiLlama; U.S. M2 from FRED M2SL. M2 observations are revised by the Federal Reserve, and ODS uses the latest available. Data can be delayed or revised upstream. No SLA or uptime guarantee is implied.
public · embed
Embed widget
The widget is a single self-contained HTML document served at /widget. Drop it into any page as an iframe. It loads no script bundle, no web font and no stylesheet from anywhere, and it makes exactly one network request of its own: a read of /api/ratio when it loads. It is free to use and needs no key.
research · solana mainnet
The on-chain oracle
The ODS reading is also written to four accounts on Solana mainnet, so the number can be verified and consumed without trusting this site's HTTP API. This is a research publication. It is a second publication of the same reading, not a separate measurement, and it carries no availability commitment: an on-chain read can be mid-cycle, and the accounts are updated by a single authority.
Reads are permissionless. No wallet, no transaction and no fee is involved; a single RPC getAccountInfo or getMultipleAccounts call is enough.
| Account | Seed | Role | Address |
|---|---|---|---|
| program | — | Oracle program ID | XJjnewyPHcfb2ogMN1uAZGyt25XbKN2DWnm1GfAwddd |
| summary | "oracle" | ODS value, totals, concentration and metadata | 8DAncbbsEkmsCCakHpNK5zf49XLcWQrryTBYdjgcLWxA |
| chains | "chains" | Supply breakdown by chain, up to 20 rows | 97g36T1PV3anxCwxV6ue5MmF2A9HQHMsLqevckrkqjzK |
| issuers | "issuers" | Supply breakdown by issuer, up to 25 rows | GznG6pxbLPrqeYDd5aFXJcXK5SQ4wXifBtqjep34DNe |
| tokens | "tokens" | Supply breakdown by token, up to 25 rows | 7FRjdiN489qMU2R8G2RumBPxRjanY9683gnXttKP4Fhv |
Each account is a program-derived address from its seed and the program ID, so any of them can be re-derived rather than trusted from this page.
sequence when they are written together. If a book's sequence does not equal the summary's, the read landed mid-cycle or one book write failed. Compare them and retry rather than mixing figures from two cycles.
Node.js · @snowplow1337/ddd-oracle-sdk
SDK
The SDK handles PDA derivation and account deserialisation. It has a peer dependency on @solana/web3.js and is published to GitHub Packages, so an install needs a registry entry for the @snowplow1337 scope.
// Read the ODS summary account from Solana mainnet. import('@snowplow1337/ddd-oracle-sdk').then(async ({ readDDD }) => { const { Connection } = await import('@solana/web3.js'); const conn = new Connection('https://api.mainnet-beta.solana.com'); const s = await readDDD(conn); // dddPercent is precomputed; totalStablesUsd is a bigint in whole USD. console.log('ODS = ', s.dddPercent.toFixed(4) + '%'); console.log('Stables = $' + (Number(s.totalStablesUsd) / 1e9).toFixed(2) + 'B'); });
node --input-type=module or use dynamic import() as shown above.
readDDD(connection) fetches the summary account only. readAll(connection) fetches the summary and all three books in one getMultipleAccounts call and adds a consistent flag; readAllConsistent(connection) is the same read with a built-in retry when that flag is false. readChains, readIssuers and readTokens fetch one book each.
| Field | Type | Meaning |
|---|---|---|
| admin | PublicKey | Admin key; can rotate the authority and the admin. |
| authority | PublicKey | Key that signs updates. |
| bump | number | Bump seed for the summary PDA. |
| ddd | bigint | Raw on-chain value, (stables / m2) × 106. |
| totalStablesUsd | bigint | Total US-stablecoin circulating supply, in whole USD. Divide by 1e9 for billions. |
| m2Usd | bigint | U.S. M2 money stock, in whole USD. Divide by 1e12 for trillions. |
| timestamp | bigint | Unix seconds for the underlying data point. |
| slot | bigint | Solana slot when the account was last written. |
| sequence | bigint | Monotonic write counter. Books copy it each cycle; compare to detect a mid-cycle read. |
| chainCount | number | Valid rows in the chain book. |
| issuerCount | number | Valid rows in the issuer book. |
| tokenCount | number | Valid rows in the token book. |
| issuerHhi | number | Issuer Herfindahl-Hirschman index on a 0–10,000 scale: the sum of squared market-share percentages. |
| effectiveIssuerScaled | number | Effective issuer count × 106. 2.5 issuers is 2500000. |
| topIssuerShareScaled | number | Largest issuer share as percent × 106, the same encoding book rows use. |
| dddRatio | number | Convenience: ODS as a decimal ratio, ddd / 1e6. 1.4138% is 0.014138. |
| dddPercent | number | Convenience: ODS as a percentage, dddRatio × 100. Ready to display. |
| effectiveIssuerCount | number | Convenience: effectiveIssuerScaled / 1e6. |
| topIssuerSharePercent | number | Convenience: topIssuerShareScaled / 1e6, e.g. 58.97. |
Python 3 · standard library only
Decoding without an SDK
No packages required. This queries the Solana JSON-RPC and decodes the binary account layout directly, using the offsets documented below.
import urllib.request, json, base64, struct from datetime import datetime, timezone # Base58 encoder (no deps) ALPH = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" def b58(b): n = int.from_bytes(b, 'big'); s = '' while n: n, r = divmod(n, 58); s = ALPH[r] + s return '1' * (len(b) - len(b.lstrip(b'\x00'))) + s # Fetch all 4 oracle accounts in one RPC call req = urllib.request.Request( 'https://api.mainnet-beta.solana.com', data=json.dumps({ "jsonrpc": "2.0", "id": 1, "method": "getMultipleAccounts", "params": [[ "8DAncbbsEkmsCCakHpNK5zf49XLcWQrryTBYdjgcLWxA", # summary "97g36T1PV3anxCwxV6ue5MmF2A9HQHMsLqevckrkqjzK", # chains "GznG6pxbLPrqeYDd5aFXJcXK5SQ4wXifBtqjep34DNe", # issuers "7FRjdiN489qMU2R8G2RumBPxRjanY9683gnXttKP4Fhv", # tokens ], {"encoding": "base64"}], }).encode(), headers={'Content-Type': 'application/json'}, ) r = json.loads(urllib.request.urlopen(req).read())['result']['value'] s, c, ib, t = (base64.b64decode(a['data'][0]) for a in r) ts_utc = lambda u: datetime.fromtimestamp(u, tz=timezone.utc).isoformat() # Summary account, offset 73 onward: ddd, total_stables_usd, m2_usd, timestamp, slot, sequence ddd, stb, m2, ts, slot, seq = struct.unpack_from('<QQQqQQ', s, 73) # Concentration fields follow the three u8 counts at 121/122/123. hhi, eff, top = struct.unpack_from('<III', s, 124) print(f"ODS ratio : {ddd/1e4:.4f}%") # raw / 1e4 == percent print(f"Stables : ${stb/1e9:,.4f}B") # whole USD, so /1e9 == billions print(f"U.S. M2 : ${m2/1e12:,.4f}T") print(f"Issuer HHI: {hhi} (effective {eff/1e6:.2f}, top {top/1e6:.2f}%)") print(f"Updated : {ts_utc(ts)} (slot {slot:,})") print(f"Sequence : {seq}") print(f"Admin : {b58(s[8:40])}") # Book accounts: count at byte 25, then fixed 29-byte entries from byte 26. def read_book(name, d): cnt = d[25] print(f"\n--- {name} (count={cnt}) ---") for k in range(cnt): o = 26 + k*29 nm = d[o+1:o+1+d[o]].decode('utf-8', errors='replace') sup, pct = struct.unpack_from('<QI', d, o+17) print(f" {k+1:<3} {nm:<16} ${sup/1e9:>10,.2f}B {pct/1e6:.4f}%") read_book('CHAINS', c) read_book('ISSUERS', ib) read_book('TOKENS', t)
# Static example. This page runs no browser-side RPC call of its own: # third-party RPC endpoints set their own CORS and CSP rules, so a # documentation page is the wrong place to read from one. Run the # snippet above from a server or a shell for real values. ODS ratio : 1.3500% Stables : $305.0000B U.S. M2 : $22.5900T Issuer HHI: 3120 (effective 3.21, top 61.40%) Updated : 2026-08-06T12:00:00+00:00 (slot ...) Sequence : ...
Any language · Solana JSON-RPC
Direct RPC call
No SDK? Fetch the summary account directly with a standard Solana JSON-RPC call. This works with any HTTP client in any language.
$ curl -sS https://api.mainnet-beta.solana.com \ -X POST \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "getAccountInfo", "params": [ "8DAncbbsEkmsCCakHpNK5zf49XLcWQrryTBYdjgcLWxA", { "encoding": "base64", "commitment": "confirmed" } ] }'
The response is not meant to be readable at a glance. result.value.data[0] is the base64-encoded raw account. Solana does not turn that into ODS = 1.33% for you: decode the base64, then read little-endian fields at the offsets in the layout table below. The long string that looks like noise is the expected result.
If a python3 -c "..." one-liner printed nothing, the cause is usually quoting: double quotes let the shell expand $ inside Python f-strings before Python runs. Use single-quoted -c '...' as below so stdin from curl still reaches Python. Do not use curl | python3 <<'PY'; that replaces stdin and breaks the pipe.
$ curl -sS 'https://api.mainnet-beta.solana.com' -X POST \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"getAccountInfo","params":["8DAncbbsEkmsCCakHpNK5zf49XLcWQrryTBYdjgcLWxA",{"encoding":"base64","commitment":"confirmed"}]}' \ | python3 -c ' import sys, json, base64, struct from datetime import datetime, timezone j = json.load(sys.stdin) raw = base64.b64decode(j["result"]["value"]["data"][0]) ddd, stb, m2, ts, slot, seq = struct.unpack_from("<QQQqQQ", raw, 73) print("ODS % ", round(ddd / 1e4, 4)) print("Stables ", "${:.2f}B".format(stb / 1e9)) print("U.S. M2 ", "${:.2f}T".format(m2 / 1e12)) print("Updated ", datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M UTC")) print("Slot ", slot) print("Sequence ", seq) '
"commitment": "confirmed" in the params object; many setups need it. If nothing comes back: try the URL without a trailing slash, check whether the provider enforces an IP allowlist, and add -v to see HTTP errors. On Windows, run the command in Git Bash or WSL, as PowerShell handles multiline single-quoted JSON differently.
Binary layout · OracleState
Summary account layout
The summary account is a packed Anchor struct of exactly 252 bytes. Every integer is little-endian and there is no padding between fields, so each offset below is the sum of the widths before it. A read that returns any other length is not this account.
| Field | Offset | Type | Meaning |
|---|---|---|---|
| discriminator | 0 | [u8; 8] | Anchor account discriminator |
| admin | 8 | [u8; 32] | Admin pubkey; base58-encode to display |
| authority | 40 | [u8; 32] | Update authority pubkey |
| bump | 72 | u8 | Bump seed for this PDA |
| ddd | 73 | u64 LE | ODS ratio × 106. Divide by 10,000 for percentage. e.g. 13900 → 1.3900% |
| total_stables_usd | 81 | u64 LE | Total US-stablecoin circulating supply, in whole USD. Not cents, and not scaled. e.g. 319000000000 → $319B |
| m2_usd | 89 | u64 LE | U.S. M2 money stock, in whole USD. e.g. 21700000000000 → $21.7T |
| timestamp | 97 | i64 LE | Unix seconds (UTC) of the underlying data point. Signed, not unsigned. |
| slot | 105 | u64 LE | Solana slot of the last write |
| sequence | 113 | u64 LE | Monotonic write counter. Books copy it; compare to detect a mid-cycle read. |
| chain_count | 121 | u8 | Valid rows in the chain book |
| issuer_count | 122 | u8 | Valid rows in the issuer book |
| token_count | 123 | u8 | Valid rows in the token book |
| issuer_hhi | 124 | u32 LE | Issuer HHI on a 0–10,000 scale: the sum of squared market-share percentages. e.g. a 56% issuer contributes 3136 |
| effective_issuer_scaled | 128 | u32 LE | Effective issuer count × 106. e.g. 2500000 → 2.5 issuers |
| top_issuer_share_scaled | 132 | u32 LE | Largest issuer share, percent × 106. e.g. 58970000 → 58.97% |
| reserved | 136 | [u8; 116] | Reserved for future fields. Do not read; do not assume it stays zero. |
ddd is the decimal ratio × 106, so percent is ddd / 10,000. The *_scaled concentration fields and every book row's pct_scaled are a percentage × 106, so percent is value / 1,000,000. Both use 106; they do not mean the same thing.
Book account layout
The three book accounts share one layout. Only the row capacity, and therefore the total size, differs: chains holds up to 20 rows (638 bytes), issuers and tokens up to 25 (783 bytes each). Rows past count are present in the account but not meaningful.
| Field | Offset | Type | Meaning |
|---|---|---|---|
| discriminator | 0 | [u8; 8] | Anchor account discriminator |
| bump | 8 | u8 | Bump seed for this PDA |
| sequence | 9 | u64 LE | Sequence stamped when this book was written. Must equal the summary's for an atomic read. |
| timestamp | 17 | i64 LE | Unix seconds of the cycle this book belongs to |
| count | 25 | u8 | Number of valid rows |
| entries[i] | 26 + 29i | 29 bytes | Fixed-width row: name_len u8, name [u8; 16], supply_usd u64 LE at +17 (whole USD), pct_scaled u32 LE at +25 (percent × 106). |
| reserved | 26 + 29·capacity | [u8; 32] | Reserved for future fields |
Research · /api/v1/oracle/*
Oracle over HTTP
If you want the on-chain values without running an RPC client, two versioned read-only endpoints return the decoded accounts as JSON. They read the same four accounts the SDK reads, so they are a convenience over the oracle, not a separate source. They have the same research status as the oracle itself.
GET /api/v1/oracle/summary— the decoded summary account only. The cheapest read.GET /api/v1/oracle/snapshot— the summary plus the chain, issuer and token books from one multi-account read, with aconsistentboolean. Whenconsistentisfalse, anotefield explains that the read landed mid-cycle; retry shortly rather than using the books.
Both wrap their payload in apiVersion, kind, programId and cluster, and both return every 64-bit value as a string so no precision is lost in JSON. Alongside the raw fields they include the same convenience values the SDK computes: dddRatio, dddPercent, effectiveIssuerCount, topIssuerSharePercent, plus updatedAt as an ISO timestamp derived from timestamp.
Both are CORS-open, answer OPTIONS, and reject any method other than GET with 405. Each response carries an ETag keyed on the oracle sequence and Cache-Control: public, max-age=15, s-maxage=45, stale-while-revalidate=120. Send If-None-Match with the previous ETag to get 304 Not Modified while the sequence is unchanged. If the account cannot be decoded, both return 503 with error: "oracle_unavailable"; any other failure is 500. Adding ?debug=1 appends a small non-sensitive debug block with the fetch time and the RPC hostname.
GET /api/jupiter-solana-route also exists and may appear in a network log. It is a read-only proxy for a Jupiter swap quote — not an oracle read, not versioned, and not part of the benchmark. Its CORS policy admits only Stable Tape's own origins, so a cross-origin browser call from another site will fail. It is listed here so it is explained, not so it is built on.
agent workflows
Using ODS with agents
The endpoints above give agents machine-readable benchmark, issuer-concentration, and chain-distribution data. Those figures can inform allocation, monitoring, payment-planning, and research workflows.
- Treasury
Check issuer concentration — HHI, effective issuer count, top issuer share — before recommending a stablecoin allocation.
- Risk monitoring
Watch for concentration rising: the same three fields, sampled over time from
/api/v1/oracle/summaryor/api/issuers. - Payment planning
Compare chain distribution when evaluating where a stablecoin already has depth. Velocity data is research-stage and is not exposed here.
- Research
Read the dated records directly from
/api/tapeinstead of scraping a dashboard that may re-render.
limits and guarantees
What is and is not promised
- No SLA. These endpoints are published without an uptime, latency or availability guarantee. Cache what you need and degrade gracefully. Every public endpoint here is CDN-cached, so treat the cache window as the real freshness bound.
- Upstream revision. DeFiLlama restates supply and the Federal Reserve revises M2. A current reading can change under you; a frozen dated record cannot. That is the reason to cite records rather than readings.
- Update cadence. The Daily Tape record is cut at 12:00 UTC.
/api/ratioand the structure endpoints recompute per request behind a five-minute cache. The oracle follows its own write cycle and stampstimestamp,slotandsequenceso you can see exactly how old a read is. - No fees on chain. Reading Solana accounts costs nothing. You only pay lamports to write, which reading the ratio never requires.
- RPC endpoints. Every snippet here uses the public cluster
api.mainnet-beta.solana.com, which is rate-limited and not intended for production. Use your own provider. This page itself makes no RPC call: the example output above is a static illustration, not a live read. - Change policy. If a documented field name, unit or scale changes, this page changes with it, and
npm run test:developer-api-docsfails until it does. Interfaces in the research and internal registers carry no such commitment.
Questions? @stabletape
Weekly email
The weekly email.
The week’s stablecoin supply movement and the current benchmark level. One list, at most one email a week.