The indexer's holders resource serves one row per HOLDING. On an ERC-20 that is one row per holder; on a collection it is one row per held item, so Lux Genesis came back as three rows for two addresses and the Uniswap position manager as fifty rows that are all the same address. The site printed "3 holders" and "73 holders" and drew one address fifty times. explorer.holders() folds the rows by address, which is what makes the answer countable. The collection page counts the folded list and reads 2 for Genesis, which agrees with ownerOf. The resource pages at 50 and reports no next page on the page that fills, so a full page says "at least" rather than a flat count. holders_count arrives as Token.holdings, named for what it counts, and only a screen that knows the standard prints it. Nothing is ranked any more. Collection cards carried a position, 1 through 10, over the order the indexer happens to answer in — which is by holder count — on a site whose own launches page says a default that puts one project on top is a recommendation it is not entitled to make. Collections arrive by name. Also read against the chain and corrected: /launches said the indexer reports decimals as 0 for Zoo tokens that carry 18. Those contracts return 0 from decimals() themselves, so the claim was false and the reason for omitting supply is the real one. /support/projects claimed collection-level data was complete, including holder count and full transfer history; it is a page of transfers and a holdings count, and now says so. An application no longer requires a session to compose. A filled-in form produced nothing at all without Hanzo IAM, directly under a paragraph saying signing in was not a condition of applying. Signed in it names the applicant; signed out the record says the reply goes to the address it arrives from. /item threw during render on ?id=abc and took the page to a blank screen. Holdings drew a chain whose read had FAILED as no holdings at all. usd() rounded a pool holding forty cents to $0, the same thing an empty pool prints. The header lit both Launches and Apply on /launches, on a shared prefix. The launches table read the indexer itself, with its own token type and its own fetch; it uses lib/explorer like every other screen. trailingSlash, because the export wrote support.html beside a support/ directory and a host that prefers the directory served a listing where the page belongs. Every route is now its own index.html and needs no rewrite rules. Removed from public/: reservoir's logos, 80 chain icons for chains Lux does not run, ten foreign chain logos, and og-image.png — a Reservoir Explorer marketing card advertising Bored Ape floor prices, sitting in the social-preview slot of a site whose whole point is that it quotes no prices. The two radix packages whose only consumers were the deleted listing dialogs go with them. Co-authored-by: Hanzo Dev <[email protected]>
9.1 KiB
Lux Market
@luxfi/market — Next 15 static export, @luxfi/ui 7.x, wagmi + viem.
lux.market reads the Lux chains and shows what is on them. It holds no assets,
takes no custody and settles nothing.
What is actually on the chains
Measured 2026-08-29, and worth re-measuring before relying on any of it.
- One collectible NFT contract exists on any Lux chain. Lux Genesis,
0x9e04fc57c20b2ee45627c4aa280eb471f2ca6ea5on 96369, three tokens. The other ERC-721 is Uniswap's V3 position manager, which is an LP receipt. - No marketplace contract is deployed anywhere.
Market.sollives in~/work/lux/standard/contracts/nft/Market.sol; every address the old deployments file recorded returns0xfrometh_getCode, and the Genesis collection's ownmarket()reads the zero address. Nothing has ever been listed, bid on or sold. - The AMM is the deep data. Lux has 32 pools and Zoo 20, with both sides of every pair, fee tier, locked value, volume and transaction count.
The three reads, and which questions each answers
| module | source | answers |
|---|---|---|
lib/registry.ts |
/v1/explorer/admin/chains |
which chains exist, their ids, slugs, coins, RPCs, AMM contracts |
lib/explorer.ts |
/v1/indexer/<slug>/… |
tokens, collections, holders, transfers, holdings, stats |
lib/amm.ts |
/v1/graph/<slug>/amm/graphql |
factory totals and pools |
lib/logs.ts |
the chain's own RPC | the token id the indexer drops |
hooks/queries.ts wraps all four in react-query. Nothing else fetches.
The registry is the chain list
There is no array of chains in this app. readChains() reads the indexer's
registry and everything downstream carries a whole Chain record — slug for the
indexer, id for a contract, coin for a label, explorer for a link out. The array
this replaced had drifted twice: it missed Osage, and it offered SPC, whose RPC
404s and which the registry has never served.
Two things the registry does not hand over cleanly:
rpcis the indexer's own route, not a browser's. For Lux it is an in-cluster name over plain HTTP; the public route arrives aspublic_rpc. Takepublic_rpcfirst, thenrpcif it is already https, otherwise admit there is none.- Its order is a Go map's, so it changes between requests. The default chain leads and the rest are sorted by name, or the chain switcher reshuffles on every load.
The explorer host is derived — api.X → explore.X, verified 200 on all five —
rather than kept in a second list that can disagree with the first.
The indexer drops the token id, and it is one field
Every ERC-721 transfer row arrives with the ERC-20 shape: total.value "1",
no id, and a token object carrying only address_hash and type. The chain
still has it — eth_getLogs on the Transfer topic returns it in topics[3] —
and the indexer's log_index is the EVM log index, so (block, logIndex)
joins a row to its log exactly. lib/logs.ts does that join in bounded windows;
rows outside the budget say the id was not read rather than printing #?.
Fixing this upstream is a small change in ~/work/lux/indexer (write
topics[3] into token_id, populate instances, carry name/symbol on the
transfer's token). Do not apply migrations/004_nft_marketplace.sql — it is
a Postgres marketplace schema nothing writes to, and its companion
evm/defi/nft.go holds event hashes that do not match cast keccak.
Also true, and the reason there is no item resource: /tokens/{addr}/instances
returns {"items":[]} for every collection, including the 149-token one.
The holders resource counts holdings, not holders
/tokens/{addr}/holders serves one row per HOLDING. On an ERC-20 that is one
row per holder and the rows are distinct — LZOO returns 11 rows for 11
addresses. On a collection it is one row per held item: Lux Genesis returns
three rows for two addresses, and the Uniswap position manager returns fifty
rows that are all 0xd0ebbdcd…cee8. holders_count on /tokens and
token_holders_count on /counters count the same rows, so both read 3 for
Genesis and 73 for the position manager, and neither is a holder count on a
collection.
explorer.holders() folds the rows by address, which is what makes the answer
countable: the collection page counts the folded list rather than printing the
indexer's figure, and reads "2 holders" for Genesis where the field says 3. The
resource also pages at 50 and reports next_page_params: null on the page that
fills, so a full page cannot be told from a complete one — whole is false
there and the screen says "at least".
Token.holdings carries the raw field under the name of what it counts. Only a
screen that knows the standard may print it: the launches table shows it for a
fungible token and says "counted per item" on a collection.
The one place per-item metadata exists
GenesisNFTs.sol keeps mapping(uint256 => TokenMeta) public tokenMeta and its
generated getter answers. Each token has a name (#0 "Genesis Validator #51",
#1 "Genesis Validator #100", #2 "The Terminator"), a tier, and the LUX bonded to
it. components/Genesis.tsx reads it. It is contract-specific on purpose; no
other collection publishes anything per item.
Two things that will bite
The gui engine and the Lux token set share the name --background.
@hanzo/gui injects :root.t_dark { --background: … ; --color: … } and
body { background: var(--background); color: var(--color) } at runtime, later
in the cascade and more specific than @luxfi/ui/tokens.css. The page ground
came from the wrong system and card text measured 1.03:1 against its own card.
globals.css reads the ground from --color-bg-body, which only the Lux token
set defines and which tracks the same rungs. Every other token is uncontested.
TypeScript must stay on 6.x. Next 15 drives the JavaScript compiler API for
tsconfig paths and typechecking; TS 7's native compiler does not expose it, so
next dev refuses to start, @/* stops resolving in next build, and
next.config.ts cannot load at all (hence next.config.mjs).
Routes
/ /collections /collection /item /activity /pools /genesis
/portfolio — the read surface.
/launch /launches /support (+ buyers, projects, terms, privacy) — applying
and help.
output: 'export' renders every route ahead of time, so a path segment cannot
hold a contract address deployed after the build. /collection and /item take
the chain and address in the query instead; lib/links.ts builds those URLs. A
query value is whatever someone typed, so /item checks the id is a whole
number before it reaches BigInt — ?id=abc used to throw during render and
take the page down to a blank screen.
Rules this surface is held to
- No invented figures. No floor price nothing computed, no holder count nothing counted, no activity row nothing observed, no volume nothing summed. Chain totals come from the factory entity, not a sum over one page of pools. Where the indexer's own figure disagrees with the chain, the chain wins and the screen counts for itself.
- Unavailable is never drawn as empty or as zero. A failed read throws in
lib/explorer.tsso a screen can tell "there are none" from "we could not ask", and says which it is — including one chain among several, which is why holdings draws a sentence per chain that did not answer rather than nothing. A value that rounds to zero is not zero either:usd()prints<$1. - Nothing is ranked. Collections arrive by name, the launches table sorts by name until a reader picks a column, and no card carries a position. The indexer answers in holder order and printing that unlabelled would rank one project above another on a figure nobody chose.
- Every screen names the chain it read (
components/Source.tsx). - Colour through tokens only. Monochrome carries state by weight and label, never by hue.
- Nothing signs or broadcasts here. The user's own wallet signs, and today nothing asks it to.
- No transactional UI against a contract that is not deployed. When
Market.sollands somewhere, the ABI and the forms come back with it.
Build
pnpm install && pnpm build # static export to out/
pnpm dev # port 3100
trailingSlash: true, so every route exports as its own index.html and any
static host resolves it without rewrite rules. Without it the export wrote
support.html beside a support/ directory and a host that prefers the
directory served a listing where the page belongs.
Applying
/launch composes the application and hands it over; there is no server here to
receive one and no intake endpoint was found on api.lux.financial or
api.hanzo.ai, so the page says so rather than accepting a submission it would
drop. Signing in with Hanzo IAM names the applicant and is not a condition of
applying — the composed record says the reply goes to the address it arrives
from. The contract address is read live with eth_getCode and labelled
"observed", which is the one line on the application nobody typed.
A real intake would be one constant and one handler. A queue that holds applications is what would make this a pipeline rather than a mailbox.