Markets: every AP3X venue, live, with the proper marks

The Markets section listed three Base pairs from DexScreener and an
empty "Prime tokens" box. It now shows every venue where AP3X trades:

- Base: Aerodrome USDC (the reference price), QuickSwap cbBTC, the two
  Uniswap V3 pools DexScreener does not index and the Aerodrome cbADA
  pool it lists without a price - the last three read straight from
  the chain in one batched eth_call (publicnode, CORS-open; the
  out-of-range WETH pool is flagged and earns no TVL credit)
- Cardano: VyFi ADA/cstAP3X, ADA/cAP3X, cAP3X/cstAP3X from Koios
  address_info (reserves summed across UTxOs), Minswap V2 cAP3X/ADA and
  NIGHT/cAP3X from the pool datum found by the LP asset name, priced by
  the Minswap aggregator; DexHunter stands in when Koios is down
- LBank AP3X/USDT through the site-origin proxy, shown as what it is:
  no trades in days, spread, dead volume
- USDC.e on Nexus gets its own card (ApexScan: supply, holders, largest
  holder, all-time transfers, Stargate bridge link) in place of the
  Prime tokens box, which had nothing to list
- "One asset, four chains": AP3X, bAP3X, cAP3X, cstAP3X with the
  cstAP3X redemption rate computed live (vault / issued) so the
  receipt's premium is explained, not mispriced
- marks are the HAL 8 wallet's own PNGs plus each venue's mark, token
  image with a chain badge, embedded so the page stays one file
- feeds have deadlines, per-panel parsers, an error boundary, tiered
  refresh (quota'd feeds every 9 min, fast ones every 90 s), hidden-tab
  pause, a refresh cooldown, and honest fallbacks (ADA/USD from Minswap
  when CoinGecko is down, cAP3X/USD from reserves when the aggregator
  429s, placeholders when DexScreener is down)
- chain pulse now says mainnet; HeroLightning's ResizeObserver no longer
  throws after unmount; stake-pool saturation reads OSAM's field

Koios and LBank are reached via /api/koios/* and /api/lbank/* on this
origin (Caddy reverse proxies in the HAL 8 repo, PR #142) because
neither answers with a usable CORS header.

README documents the feeds; tools/bundle.py decodes and re-encodes the
bundled page byte-exactly so the next edit does not touch escaped text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPpFgu6wXiDfJv8ghoUwBJ
This commit is contained in:
2026-09-09 18:23:41 +00:00
parent 517d9a020a
commit b6d5920f54
3 changed files with 83 additions and 2 deletions

View File

@@ -37,7 +37,46 @@ can be repointed at HAL 8's own OpenAI-compatible API
(`https://hal8.odyssey-works.io/v1`) so conversations stay on
Odyssey Works infrastructure.
## Markets section — live feeds
Every number on the Markets page is fetched by the visitor's browser, so
only CORS-open sources can be called directly. Two are not, and the site
origin proxies them (Caddy, `deploy/Caddyfile` in the HAL 8 repo):
| Path on this origin | Upstream | Used for |
|---------------------|---------------------|-------------------------------------------------|
| `/api/koios/*` | `api.koios.rest` | VyFi pool reserves, cstAP3X vault + supply, Minswap V2 pool datum |
| `/api/lbank/*` | `api.lbkex.com` | AP3X/USDT ticker, depth, last trade |
Direct (CORS-open): CoinGecko, DexScreener, the Minswap aggregator
(`agg-api.minswap.org`, rate-limited to roughly five calls a minute per IP,
so the page makes one), Base JSON-RPC (`base-rpc.publicnode.com`; the two
Uniswap V3 pools and the Aerodrome cbADA pool are read from the chain
because DexScreener does not index them), ApexScan v1 on Nexus (USDC.e),
the Vector Koios instance, the Prime explorer, OSAM. DexHunter is the
fallback for Cardano reserves when Koios is down.
Fast feeds refresh every 90 s; the quota'd ones (Koios, LBank, Base RPC,
ApexScan) every third tick. Polling pauses in hidden tabs.
The page's own Cardano truth is a staking receipt, not a fourth asset:
`cstAP3X` is worth `vault cAP3X / cstAP3X issued` cAP3X each (about 1.07),
so it trades above cAP3X by design.
## Updating
Edit `index.html`, commit to `main`, and the host picks it up on its next
deploy tick. No build, no bundler, no CI.
deploy tick (every five minutes). No build, no bundler, no CI.
The page is stored inside `index.html` as a JSON-encoded template that the
bundler unpacks at load time, so do not edit the escaped text by hand.
`tools/bundle.py` decodes it to a plain HTML file and re-encodes it
byte-exactly:
```
python3 tools/bundle.py decode index.html page.html # edit page.html
python3 tools/bundle.py encode index.html page.html index.html
```
The Markets code lives in `page.html` between `const MK_CG` and
`const SECTION_COMPONENTS`.

File diff suppressed because one or more lines are too long

42
tools/bundle.py Normal file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Decode / re-encode the __bundler/template of the apex-fusion site.
python3 bundle.py decode index.html page.html # template -> editable HTML
python3 bundle.py encode index.html page.html out.html # editable HTML -> site file
The bundler stores the page as a JSON string with '</' written as '<\\u002F'
(so the inner page's </script> tags cannot terminate the outer tag). Encoding
mirrors that exactly, and `encode` proves it by asserting that an untouched
page round-trips byte-identical before writing.
"""
import json, re, sys
TAG = re.compile(r'(<script type="__bundler/template">)(.*?)(</script>)', re.S)
def decode(site):
return json.loads(TAG.search(site).group(2))
def encode_template(page):
e = json.dumps(page, ensure_ascii=False)
return '\n' + e.replace('</', '<\\u002F') + '\n '
def encode(site, page):
m = TAG.search(site)
return site[:m.start(2)] + encode_template(page) + site[m.end(2):]
if __name__ == '__main__':
cmd, site_path, page_path = sys.argv[1:4]
site = open(site_path, encoding='utf-8').read()
if cmd == 'decode':
open(page_path, 'w', encoding='utf-8').write(decode(site))
print('decoded', len(decode(site)), 'chars')
elif cmd == 'encode':
out_path = sys.argv[4]
# fidelity proof: the original template must re-encode to itself
orig_block = TAG.search(site).group(2)
assert encode_template(decode(site)) == orig_block, 'encoder no longer byte-exact for the untouched page'
page = open(page_path, encoding='utf-8').read()
out = encode(site, page)
assert decode(out) == page, 'round-trip mismatch after encode'
open(out_path, 'w', encoding='utf-8').write(out)
print('encoded', len(out), 'bytes ->', out_path)