6529-tdh-node

TDH — a specification

Version 0.1.1 — 4 August 2026. Complete enough to build from, and not yet validated by anything having been built.

0.1.1 adds scope only: xTDH and the upstream OpenAPI document are named and placed, below and under Sources. No claim about the calculation changed. Nothing here is authoritative in the sense of being blessed by 6529; this is one attempt to write down what their production code does, in a form another implementation can be built and checked against.

Two things would make it 1.0, and neither is more writing: an implementation that reproduces a published Merkle root using only this document, which is the only real test of a specification, and a reading by someone who did not write it — ideally from the 6529 team, on the points marked [stated] and [open].

Every claim carries a marker:

  meaning
[code] read in 6529seize-backend production source, path given
[live] confirmed against api.6529.io on the date given
[stated] said by a member of the 6529 team in a public channel; not confirmed in code
[open] not established — written down so it is not mistaken for settled

No implementation language is assumed anywhere in this document. Where an operation could be read two ways, the intent is that two implementations in two languages produce byte-identical output.

Scope, and two things deliberately outside it

This specification covers the daily TDH calculation: the modules under src/tdhLoop/, from the transfer set through to the Merkle root of §5. Two adjacent systems carry similar names and are not described here.

xTDH is a separate system, and ignoring it does not affect conformance. Production runs a second loop — src/xTdhLoop/, an SQS-driven lambda over the src/xtdh/ service, with src/xTdhGrantsReviewerLoop/ beside it. Holders grant an xtdh_rate to a target contract and token list on a target_chain, bounded by valid_from/valid_to, either revocable or is_irrevocable — the latter “must be covered with lock contracts”. It surfaces under /api/xtdh/*: grants, grantees, collections, tokens, per-token contributors, and global stats carrying a multiplier. [live] 2026-08-04

The direction of dependency is what matters here, and it is one-way: no module under src/tdhLoop/ references xTDH at all — checked across all eleven non-test modules — and the Merkle leaf is sha256("<consolidation_key>:<boosted_tdh>") and nothing else (§5). xTDH reads TDH; TDH does not read xTDH. An implementation that never hears of xTDH still produces the correct root. [code] src/tdhLoop/ at main, 2026-08-04

This is a statement about the calculation, not a judgement about importance. xTDH is live, it is a real extension of TDH beyond the three contracts of §2, and it deserves its own specification. This is not that document.

The wider REST API is specified upstream; the oracle routes are not part of it. 6529 publishes an OpenAPI 3.0.3 document at api.6529.io/openapi.json (and .yaml), covering 289 paths under the /api base. The ten /oracle/* routes of §6 do not appear in it — that surface is written down here because it is written down nowhere else. [live] 2026-08-04


0. The smallest thing worth building first

This document is longer than the first implementation needs to be. A spike that proves the whole approach needs five of the eight sections, and produces exactly one output: a Merkle root for a block, to compare against the one production published for that same block.

Needed for a first spike

  why
§1 — block selection you must resolve the calculation instant to the same block
§2 — contracts, ingestion the three addresses and how to build the transfer set
§4.1, §4.2, §4.3, §4.5 days held, hold rate, boost, composition
§5 — the Merkle commitment the output, and the only thing being compared
§8 — the reference range replay to block 25663469, whose correct root is known

Not needed, and safe to skip entirely

  why
§3 — identity enrichment handles and profiles are not derivable and not in the root
§4.4 — sale detection the calculation does not implement it; neither should you
§4.6 — ranks not in the tree; a rank mismatch is not a conformance failure
§6 — the ten routes a spike needs no HTTP surface at all — print the root

Smaller still: one wallet, no consolidation

Even the list above is more than the first useful milestone. Consolidation can be left out entirely, and the result is still exactly checkable against production.

For an address that belongs to no consolidation, its consolidation_key is the address itself and its TDH is its own. 94.2% of identities are in that position — 9,644 of the 10,239 in the tree at block 25663469; only 595 group two or three wallets. [live]

So the smallest thing that proves the calculation works is:

Take one address. Compute its TDH from chain data. Compare against GET /oracle/address/:address. Equal or not equal.

It is a single number, against a public endpoint, for any of thousands of addresses. When addresses in that response contains only the address you asked for, no consolidation is involved and the comparison is direct. [live] verified against 0xde112b77…c351, whose response lists exactly one address.

The token-date rules of §4.1 simplify accordingly: with a group of one, inbound transfers always create new dates and outbound always removes them, most recent first. The carry-over branch never fires.

Choosing something to test against

Do not start against an arbitrary identity. Pick one whose shape removes every rule you have not implemented yet, so a mismatch means one thing rather than five.

What to look for, in rough order of how much it simplifies:

property why it helps how to check
not consolidated no wallet grouping, no date carry-over addresses in /oracle/address/:address has exactly one entry
no outbound transfers the LIFO removal path of §4.1 never runs no row where from_address is the address itself
few transactions the whole history fits on one screen the count on the transactions query
no complete season set boost stays at 1, §4.3 is out of scope boost is 1 in the same response

An address holding a single card, acquired once and never moved, exercises the entire chain — read the transfer, resolve the instant to a block, count whole days, apply the hold rate, compose — while needing none of consolidation, removal, or boost.

Finding one is a short filter, not a hunt. Both endpoints are public and need no key:

  1. GET /oracle/tdh/above/0/entries → every identity, with its consolidation_key.
  2. Keep the keys containing no -. Those are single-wallet identities; there are thousands, and they are the overwhelming majority.
  3. For a candidate, GET /api/transactions?wallet=<address>&page_size=200 → its full transfer history in the shape of §2.
  4. Discard it if any row has from_address equal to the address (it has sold or moved something), or if count exceeds one page. Keep the ones with a handful of rows.
  5. Confirm with GET /oracle/address/<address>: addresses should list only that address, and boost should be 1.

Start from the low-TDH end of the entries list — a small TDH usually means few cards held for a short time, which is exactly the short history you want to trace by hand.

Read the expected value at the moment you compare it. Never hardcode one.

Two independent reasons, and both will bite:

  1. TDH grows every day. The same address holding the same card is worth more tomorrow — by exactly its hold rate. A number that was right on Monday is wrong on Tuesday, and that is the model in §1 working correctly, not a defect.
  2. The address belongs to someone. They can sell, buy or consolidate between two of your runs, with no warning. A test that suddenly fails may be reporting their activity rather than your bug — so re-check the shape (still one address in the list? still no outbound rows?) before assuming the implementation broke.

The comparison to make is therefore always: fetch the transfers, compute, fetch /oracle/address/:address at the same block, and check equality. The arithmetic to expect is just §4.5 with a group of one:

tdh = round( hold_rate × whole_days_held )      boost 1, single card, never moved

Later, this gets easier: a wallet created for the purpose, holding a fixed set of cards and never touched, would be a stable reference nobody else can move. Worth doing once the implementation is real; for now, arbitrary addresses plus a fresh read are enough.

A natural ladder falls out of this, each rung independently verifiable:

  milestone checked against
1 one unconsolidated address /oracle/address/:address — 94% of identities
2 consolidation the same route, for the remaining 6%
3 every identity, hashed the Merkle root — full conformance

Only rung 3 needs consolidation to be right, because the root covers everyone.

So the first milestone is not a node. It is a program that reads the chain, computes, prints one number, and is either right or wrong. Everything in §6 and §3 is packaging that comes after the number is correct.

1. The model

TDH is not an accumulator. Despite the name — Time-weighted Days Held — nothing is banked. It is a pure function of current ownership state evaluated over the complete transfer history, recomputed from scratch on a schedule. Yesterday’s TDH is not an input to today’s. [code] src/tdhLoop/index.ts, tdh.ts

This is the single most consequential sentence in this document. An implementer who assumes an accumulator will produce something that tracks production for a while and then diverges, in at least four independent ways:

  1. Consolidation rewrites history. Grouping wallets retroactively changes the holding dates of tokens already held. [code] getTokenDatesFromConsolidation
  2. Edition sizes change after the fact, and the HODL index derives from the set of all editions — so minting a new card reprices the entire collection. [code] tdh.tsMEMES_HODL_INDEX is the maximum over all calculation edition sizes
  3. Boosts multiply history made without them. Completing a set today changes the value of days held before you completed it. [code] calculateBoost
  4. A priced transfer may reclassify an old holding — publicly stated by the team, and not found in the code. See §4.4. [stated] [open]

A correct implementation may cache and may recompute incrementally, but only if it can reproduce the from-scratch result exactly. Any incremental design must therefore checkpoint the configuration in force (consolidation groupings, edition sizes, the HODL index) and not only the resulting state — because all three are inputs that change retroactively.

Schedule

Production recomputes at 00:00 UTC and the result is addressed by an Ethereum block number. [code] getLastTDH() returns today at 00:00:00 UTC, or yesterday if that instant is in the future.

Consequences visible to users, all downstream of this and all reported as confusing in the public archive: a card must be held for more than 24 hours before it contributes; a set-completion boost appears the day after completion; TDH can fall.

Block selection

The calculation instant is a timestamp; the result is addressed by a block. The mapping between them is deterministic, which is what makes conformance possible at all — two implementations that agree on the rule choose the same block without coordinating.

The block is the one with the greatest timestamp less than or equal to the calculation instant. [code] findLatestBlockBeforeTimestamp in tdh.ts

Production finds it by binary search over block timestamps, seeded by an estimate at 12 seconds per block. The search is an optimisation; the rule above is the specification. An implementation may find that block any way it likes.

Note this is a property worth checking rather than assuming: had the block instead been “whatever the indexer had ingested when the job ran”, two correct implementations would pick different blocks and could never agree at the same instant. It is not — the target timestamp fully determines the block.

Transactions are then filtered block <= target. [code] updateTDH


2. Inputs — what is read from the chain

Chain: Ethereum mainnet.

The three contracts

TDH is computed over exactly three contracts. [code] tdhContracts in updateTDH

collection address standard
The Memes 0x33FD426905F149f8376e227d0C9D3340AaD17aF1 ERC-1155
6529 Gradient 0x0c58ef43ff3032005e472cb5709f8908acb00205 ERC-721
NextGen Core 0x45882f9bc325E14FBb298a1Df930C43a874B83ae ERC-721

Meme Lab (0x4db52a61dc491e15a2f78f5ac001c14ffe3568cb) is not included. It is a 6529 collection and it does not contribute to TDH. An implementation that indexes it will produce numbers that do not reconcile. [code] absent from tdhContracts · [live] GET /oracle/nfts/0x4db52a61… returns an empty list, while the three above return their tokens with TDH — checked 2026-08-02

NextGen resolves per network; the address above is mainnet. [code] NEXTGEN_CORE_CONTRACT

Burn addresses

Transfers to either are burns, not holdings: [code] constants/index.ts

0x0000000000000000000000000000000000000000
0x000000000000000000000000000000000000dEaD

Edition sizes

Meme edition sizes derive from Manifold claim data, not from a raw supply count — see §4.2 for the floor that is applied. The Manifold contract is 0x3A3548e060Be10c2614d0a4Cb0c03CC9093fD799. [code]

One hardcoded correction exists. Meme card #8 carries an adjustment of −2588 editions, tied to a specific burn transaction 0xa6c27335d3c4f87064a938e987e36525885cc3d136ebb726f4c5d374c0d2d854. [code] MEME_8_EDITION_BURN_ADJUSTMENT

This is a historical fact about one card baked into the constants, not a rule that generalises. An implementation must reproduce it to match, and cannot derive it.

Building the transaction set

The calculation consumes a table of transfers. Producing it is the first thing an implementation must do, and nothing about it is exotic. [code] transactions-discovery.service.ts

Read ERC-721 and ERC-1155 transfer events for the three contracts, in ascending block order, from each contract’s first block up to the target block. Production uses Alchemy’s getAssetTransfers with category: [ERC721, ERC1155] filtered by contractAddresses; any equivalent source works, including plain eth_getLogs against the two Transfer topics. The provider is an implementation detail.

For a spike, the chain can wait. 6529 serves its own indexed transfers publicly: GET https://api.6529.io/api/transactions?wallet=<address> returns exactly the rows described below, already decoded and paginated. A first implementation can build against that, get the arithmetic right, and only then replace the source with a real chain read — which is required for independence, but is not what the first milestone is testing. [live] 2026-08-02

One row per (transaction, from_address, to_address, contract, token_id), carrying:

field note
transaction tx hash
block, transaction_date the block’s number and UTC timestamp
from_address, to_address lowercased
contract, token_id  
token_count units moved — always 1 for ERC-721; the amount for ERC-1155

An ERC-1155 batch transfer expands to one row per token id. Rows are de-duplicated on that same five-part key. [code] consolidateTransactions

Price is recorded but not used. Production also stores value, primary_proceeds and royalties per row, resolved by a separate 1,719-line module. The TDH calculation never reads them — see §4.4. An implementation targeting conformance does not need to compute transaction values at all.

Consolidation size

A consolidation holds at most 3 wallets. [code] CONSOLIDATIONS_LIMIT


3. Identity

Identity in this system is in two halves, and only one of them is derivable from public data. An implementation should be explicit about which half it is serving.

Derivable — wallet grouping. Consolidation and delegation are recorded in the on-chain nftdelegation contract. Registration is two-way: both wallets must register for the grouping to take effect. Any node can derive this from chain state alone. [code] Prenode and production both do it

Not derivable — the human-readable layer. Handles, profile pictures, level, rep and CIC live in 6529’s own database. There is no on-chain source. [code] absence in both trees; [live] they are served only from their API

GET /oracle/address/:address already resolves an address to its whole consolidation and returns every address in the group. [live] 2026-08-02

An implementation that enriches responses with the human-readable layer must label its source rather than present it as independently computed. Suggested shape:

{ "addresses": ["0x…", "0x…"],
  "handle": "eduardocruz",
  "handle_source": "api.6529.io" }

4. The calculation

4.1 Days held

For each token held, the contribution is the number of whole days between the token’s date for that owner and the calculation instant. [code] getDaysDiff

Token dates use LIFO accounting. Where an owner holds several units of the same ERC-1155 id and disposes of some, the units removed are the most recently acquired — the owner keeps the oldest dates. [code] removeDates splices from the end of the array.

This is undocumented anywhere upstream and it is not neutral: LIFO maximises reported TDH relative to FIFO. It exists because ERC-1155 units have no individual identity, so some convention was required; this is the convention.

4.2 Hold rate

hodlRate(nft) = MEMES_HODL_INDEX / calculationEditionSize(nft)      # Memes
              = nft.hodl_rate                                        # other contracts

MEMES_HODL_INDEX is the maximum calculation edition size across all Meme cards. [code] tdh.tsreduce((acc, size) => Math.max(acc, size), 0)

calculationEditionSize is not raw supply: it is supply floored at the Manifold totalMax, capped at 310. [code] memes-edition-size-floor

For Gradients and NextGen the rate is not derived in this loop at all: it is read from the NFT record as nft.hodl_rate, computed when the NFT was indexed. An implementation must reproduce those stored values, not recompute them from the Memes formula. [code] getTokenTdh call site

The rate is rounded to two decimals before it is used, not after: hodlRate = round(hodlRate * 100) / 100. [code] getTokenTdh

This is where the Prenode diverged. The Prenode computes HODL_INDEX / nft.edition_size — raw supply, and an index taken across Memes, Gradients and NextGen combined. Any card whose supply sits below its floor is valued higher there than in production. An implementation targeting production must use the floored form. [code] diff of the two tdhLoop trees, 2026-08-02

4.3 Boost

Boost starts at 1 and is rounded to two decimals at the end. Intermediate values round to six decimals. [code] calculateBoost, roundBoostValue

The seasons that count exclude the current one. Boostable seasons are those with id < max(id) and boost > 0 — the season in progress never contributes. [code] getBoostableSeasons

Two mutually exclusive categories. This is the part most likely to be implemented wrongly, because it reads as additive and is not: [code] calculateMemesBoosts

Gradients, in both categories: min(count * 0.02, 0.10).

Constants: ADDITIONAL_CARD_SET_BOOST = 0.05, ADDITIONAL_CARD_SET_RATIO = 0.6529, per-season boost 0.05. [code]

Boost has no dedicated vectors, and does not need them to be verified. The Merkle root already covers it transitively: boost feeds boosted_tdh, which is the value hashed into every leaf. An implementation with a wrong boost produces a wrong root and fails conformance immediately.

Dedicated vectors would still help localise a failure rather than merely detect it. Upstream has a real test to port — src/tests/calculate-boost.test.ts, 359 lines carrying the twelve seasons’ actual card ranges, present in both the backend and the Prenode. Worth doing; not a prerequisite for building.

4.4 Sale detection

Do not implement it. This is the one instruction in this document that tells an implementer to leave something out, so it is worth being precise about why.

A 6529 team member stated publicly in April 2026 that a transfer carrying a price is treated as a sale and resets TDH even between wallets inside the same consolidation. [stated]

No such rule exists in the calculation. Price is available — every transaction row carries value, primary_proceeds and royalties — and the calculation never reads any of them. Verified by searching all eleven non-test modules of src/tdhLoop/ for those fields, in both production and the Prenode:

tdh.ts  tdh_consolidation.ts  index.ts  tdh_merkle.ts
tdh_nft.ts  tdh_memes.ts  tdh_editions.ts          →  0 occurrences, both repos

So the rule an implementation must follow is the one in the code, and it is simple — this is what getTokenDatesFromConsolidation does: [code]

An implementation that adds sale detection will produce a different root and fail conformance.

[open] The contradiction between the statement and the code is left unresolved here rather than explained away. The remaining possibility not ruled out is that the rule once existed and was removed. It is worth asking the team directly — it is the only place in this document where public description and running code disagree.

4.5 Composition

tdh_raw(token) = sum of whole days held, per edition, counting only days > 0
tdh(token)     = round( round(hodlRate*100)/100 * tdh_raw * 1000 ) / 1000, then rounded to an integer
boosted_tdh    = round(memes * boost) + round(gradients * boost) + round(nextgen * boost)

Two rounding details that will not forgive being simplified: the hold rate is rounded to two decimals before multiplying, and the per-token result passes through three decimals before being rounded to an integer. Rounding once at the end gives a different number. [code] getTokenTdh

Note also that the boost is applied per component before summing, not once at the end. [code] tdh.ts

4.6 Ranks

Ranks are dense positions in a sorted list — index + 1, with no shared positions for ties. [code] calculateRanks

Overall wallet rank (tdh_rank) sorts by, in order: boosted_tdh desc, tdh desc, gradients_tdh desc, nextgen_tdh desc.

Per-collection ranks (Gradients, NextGen) sort by that collection’s tdh desc, then by token id ascending.

Per-card ranks (memes_ranks) sort by that card’s tdh desc, then by the holder’s boosted_tdh desc.

An implementation should reproduce these orderings, and should not treat a rank mismatch on an exact tie as a conformance failure — conformance is decided by the root, and ranks are not in the tree.

[open] — the overall sort has no final tie-breaker. Two wallets equal on all four values fall through to a constant, so their relative order depends on the order rows arrived from the database. This does not affect the Merkle root, which is sorted by (boosted_tdh desc, consolidation_key asc) and is fully determined — but it does mean two correct implementations can serve different tdh_rank values for a tie. [code] the else return -1 at the end of the comparator


5. The Merkle commitment

This section is complete and verified: the construction below was recovered from source and reproduced from public data, matching the root production published for block 25663469 on 2026-08-02. [code] src/tdhLoop/tdh_merkle.ts · [live]

  1. Take every consolidation with boosted_tdh > 0 at the block.
  2. Sort by boosted_tdh descending, then consolidation_key ascending.
  3. Leaf = sha256("<consolidation_key>:<boosted_tdh>"), lowercase hex.
  4. Parent = sha256(left || right) over the two children as hex strings, not as decoded bytes.
  5. On an odd level, the last node is promoted unchanged — not duplicated. (The comment in production source says “duplicate”; its code promotes. Follow the code.)
  6. Prefix the final value with 0x.

The root is global for the block. It is not a digest of whatever rows a query returned: /oracle/tdh/above/0 and /oracle/tdh/above/1000 return different counts and the same root. [live]

Note that /oracle/tdh/above/0/entries serves rows the tree does not contain: the route filters >= 0 while the tree is built over > 0. There were 71 such rows at block 25663469. An implementation that hashes what that route returns, verbatim, gets the wrong answer. [live]

Vectors: conformance/vectors/tree-shape.json (shape) and conformance/vectors/mainnet-25663469.json.gz (end to end).


6. Surface

Ten read-only GET routes, served with Access-Control-Allow-Origin: *. Every response carries block and merkle_root. [live] all ten checked 2026-08-02

/oracle/tdh/total                       /oracle/tdh/above/:value/:extra?
/oracle/tdh/percentile/:value           /oracle/tdh/cutoff/:value
/oracle/address/:address                /oracle/address/:address/breakdown
/oracle/address/:address/memes_seasons/:season?
/oracle/address/:address/:contract/:id
/oracle/nfts/memes_seasons/:season?     /oracle/nfts/:contract?/:id?

:extra on the above route takes the literal value entries to include the rows rather than only the count. [code] api.oracle.routes.ts


7. Conformance

An implementation conforms when both hold:

Correctness — identical, bit for bit. At a given block it produces the same merkle_root as production. This is not a tolerance; one hash decides. Every implementation in every language must produce the same value or one of them is wrong.

Vectors — reproduced exactly. Every case in conformance/vectors/ produces its expected_root.

Both are checked by conformance/verify.php and conformance/run-vectors.php. Those happen to be written in PHP; nothing about the criteria is. A conforming implementation in any language can be checked by the same vectors, and is encouraged to ship its own runner.


8. Operational requirements

Correctness is an equality. Operational behaviour is a bound — implementations will legitimately differ in speed, and are only required to clear the bar.

The window. Production recomputes at 00:00 UTC. An implementation must complete a full recomputation and publish the new root within the daily cycle, on hardware it declares. Missing the window is the failure mode that killed most of the existing prenode fleet: a node that cannot finish never becomes correct again.

The benchmark set. Numbers from different implementations are only comparable if they did the same work. As correctness is pinned by golden vectors, throughput must be pinned by a frozen block range — a defined start and end block over the tracked contracts, published in this repository, over which any implementation can replay and report.

A baseline exists and is observable from outside. Production’s own turnaround — how long after 00:00 UTC its published block and merkle_root advance — can be measured by polling the API, with no access to its infrastructure. Recording that daily gives the only reference point anyone has for what the workload costs in practice. [open] — proposed, not yet measured.

Any comparison drawn from it must be scoped honestly: production’s daily loop computes a great deal this specification does not cover, so wall-clock against wall- clock is not a like-for-like measure. The frozen block range exists precisely so that a fair comparison is possible — same work, same input, different implementations.

What to report, so results can be compared rather than asserted:

The reference range

Genesis of each of the three contracts, through block 25663469.

That end block is not arbitrary: it is the block frozen in conformance/vectors/mainnet-25663469.json.gz, whose correct root is already known and was published by production. So the benchmark run is also a correctness run — an implementation replays the full history, reports what it cost, and the root it arrives at either matches or does not. A fast wrong answer scores nothing.

The start is each contract’s own first transfer rather than a fixed number, because that is what “full history” means and it is what an implementation discovers on its first sync. Report the event count so the runs are comparable.

[open] — no measurement has been taken yet. The first run is the first task of the implementation phase, before anything is built on top. It may show the daily window is comfortable on any reasonable runtime, in which case this section becomes a formality — or it may not, in which case it should change the architecture rather than be discovered late.


Sources

Everything cited here is public and free to read without an account or a key.

Production — the system this specification describes

github.com/6529-Collections/6529seize-backend · TypeScript · Apache-2.0 · in continuous use, commits most days.

Every [code] marker above refers to a path in this repository unless it says otherwise. They are pinned to commit c8287bd (2 August 2026), which is what was read. Links to main would rot as the file moves underneath the quote; these will not.

cited as permalink
src/tdhLoop/tdh.ts — the calculation, 1,142 lines read it
src/tdhLoop/tdh_merkle.ts — the tree, 64 lines read it
src/tdhLoop/tdh_consolidation.ts — wallet grouping, token dates read it
src/tdhLoop/index.ts — the daily loop, getLastTDH() read it
src/api-serverless/src/oracle/api.oracle.routes.ts — the ten routes read it
src/api-serverless/src/oracle/api.oracle.db.ts — what each route queries read it
src/tests/calculate-boost.test.ts — their boost test, 359 lines read it

The Prenode — cited only where it differs

github.com/6529-Collections/6529-Prenode · pinned to d60f5bd, its last commit, 2 July 2025. It is 6529’s own self-hostable TDH provider, and it is not the target of this specification — see §4.2 for where it has drifted.

The live API

https://api.6529.io/oracle/* — unauthenticated reads, served with Access-Control-Allow-Origin: *. Every [live] marker was checked on 2026-08-02, except those dated 2026-08-04. The registry of running providers is at 6529.io/network/prenodes.

There is an official machine-readable spec for the rest of the API, which this document did not cite before v0.1.1: api.6529.io/openapi.json · .yaml · Swagger UI at /docs/ · terminology and authentication walkthrough at 6529.io/tools/api. OpenAPI 3.0.3, 289 paths, base /api.

It specifies request and response shapes; it says nothing about how TDH is computed, which is why it does not displace this document. It is worth reading anyway, because several routes under it answer questions an implementer of §2 and §4 will have:

route summary, as published
/api/blocks Get blocks and related timestamps — bears on the block selection of §1
/api/nfts Get NFTs
/api/memes_extended_data Get paginated Memes extended data — bears on edition sizes and seasons, §4.2 and §4.3
/api/transactions Get transactions — the transfer rows of §2
/api/tdh-editions/wallet/{wallet} Get TDH editions for a wallet
/api/tdh/consolidation/{identity} Get consolidated TDH for an identity

The exact semantics of these are [open] — they are listed here as leads, not as verified behaviour. Only their existence and published summaries have been checked. [live] 2026-08-04

Ignore the document’s authentication requirement for reads. It declares a global bearerAuth that every operation inherits, and describes a session-v2 wallet flow to obtain that bearer — /api/auth/session-nonce → sign → /api/auth/session-login/api/auth/session-refresh / session-logout, with the older nonce, login and redeem-refresh-token routes kept only for compatibility (walkthrough).

The declaration is broader than what is enforced. Every route in the table above answered 200 with no credentials of any kind: /api/blocks, /api/memes_extended_data, /api/nfts, /api/transactions and /api/tdh/consolidation/:identity, each checked directly. [live] 2026-08-04

So an implementation needs no wallet, no key and no account to read what it needs — which is what makes independent verification of this specification possible for anyone. Whether the gap is intentional or an artefact of how the document is generated is [open]; do not depend on it staying open.

Statements by the team

Anything marked [stated] comes from 6529’s public channels on 6529.io, not from code, and is repeated here with that caveat attached. It has not been confirmed against an implementation.


Corrections are the most useful thing you can send, particularly to anything marked [open] or [stated]. Open an issue.