diff --git a/.env.example b/.env.example index 5fa093a..fc18609 100644 --- a/.env.example +++ b/.env.example @@ -2,5 +2,9 @@ PREIPO_DB_PATH=./dev.db # Poll interval in milliseconds (default 60000; cycles align to this boundary) PREIPO_POLL_INTERVAL_MS=60000 +# Public-equity mints are fetched every Nth DexScreener cycle (default 5) +PREIPO_PUBLIC_EVERY_N_CYCLES=5 +# Underlying stock quote poll interval in milliseconds (default 300000) +PREIPO_STOCK_POLL_INTERVAL_MS=300000 # HTTP port for the board + API PREIPO_PORT=8080 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..20d7fc9 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# Default owner for everything in the repo, unless overridden below. +* @0xSHKWON diff --git a/README.ko.md b/README.ko.md index cf28611..0a75f89 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,27 +1,36 @@ -# pretape — 온체인 프리 IPO 테이프 +# pretape — 온체인 주식 테이프 *[English](README.md) · 한국어* -솔라나에 토큰화된 프리 IPO 주식의 온체인 가격을 수집해서 보여줍니다(SpaceX, OpenAI, -Anthropic, Anduril, Polymarket, Kalshi — 6개 자산 / 11개 민트. 발행사는 PreStocks, -Ondo, Backpack Securities, xStock/Backed, Tessera/RWA.xyz). 핵심은 이겁니다. 같은 -회사인데 발행사마다 가격이 눈에 띄게 다릅니다(예: OpenAI의 PreStocks와 Tessera는 -약 34% 벌어져 있습니다, 2026-08-13). 그리고 이걸 한자리에 모아 보여주는 곳이 없습니다. +솔라나에 토큰화된 주식의 온체인 가격을 두 섹션으로 수집해서 보여줍니다. -Bun 프로세스 하나가 전부 처리합니다. 60초마다 민트별로 DexScreener를 폴링해서 -원본 페어와 유동성 가중 집계 틱을 SQLite에 쓰고, Hono로 JSON API와 라이브 보드를 -띄웁니다. +- **프리 IPO** (6개 자산 / 11개 민트: SpaceX, OpenAI, Anthropic, Anduril, + Polymarket, Kalshi): 같은 회사인데 발행사마다 가격이 눈에 띄게 다릅니다(예: + OpenAI의 PreStocks와 Tessera는 약 34% 벌어져 있습니다, 2026-08-13). 공개시장 + 기준가가 없고, 이걸 한자리에 모아 보여주는 곳도 없습니다. +- **토큰화 주식 vs 거래소** (75개 자산 / 156개 민트, tokens.xyz 레지스트리의 + 멀티 발행사 공개주식 전부: AAPL, TSLA, NVDA…): 각 토큰을 기초 주식의 실제 + 거래소 가격(Yahoo, 앵커 티커 74개)과 비교해 **프리미엄/디스카운트 시계열**을 + 만듭니다. 토큰은 24시간 거래되고 거래소는 아닙니다 — 장외 시간의 프리미엄은 + 어떤 거래소 피드에도 없는 온체인 가격발견 신호입니다. + +Bun 프로세스 하나가 전부 처리합니다. 민트별 DexScreener 폴링(프리 IPO는 60초마다, +공개주식은 5사이클마다), 앵커 티커별 Yahoo 폴링(5분마다), 원본 페어 + 유동성 가중 +집계 틱 + 기초 주가 틱을 SQLite에 쓰고, Hono로 JSON API와 라이브 보드를 띄웁니다. **시계열 자체가 제품입니다.** DexScreener에는 히스토리 API가 없습니다. 수집기가 멈춰 -있는 1분은 영원히 복구할 수 없는 데이터입니다. 다듬는 건 나중에, 배포부터 하세요. +있는 1분은 영원히 복구할 수 없는 데이터입니다. 프리미엄 시계열은 두 다리를 같은 +순간에 잡아야 해서 더더욱 그렇습니다. 다듬는 건 나중에, 배포부터 하세요. ## 출처 [tokens.xyz](https://tokens.xyz)(MIT, Solana Foundation) 포크에서 출발했습니다. -`data/preipo-seed.json`은 tokens.xyz가 손으로 정리한 자산 레지스트리에서 프리 IPO -부분만 떼어낸 것입니다. 민트↔발행사↔상환 가능 여부를 잇는 이 신원 그래프가 있어서 -발행사 간 비교가 가능합니다. 서드파티 고지는 LICENSE를 보세요. 원래 빌드 히스토리는 -포크의 `apps/preipo` 커밋에 남아 있습니다. +`data/preipo-seed.json`(프리 IPO)과 `data/equity-seed.json`(멀티 발행사 공개주식)은 +tokens.xyz가 손으로 정리한 자산 레지스트리에서 떼어낸 것입니다. 민트↔발행사↔상환 +가능 여부를 잇는 이 신원 그래프가 있어서 발행사 간 비교가 가능합니다. 기초 주식 +티커와 액면분할 배율은 이 저장소에서 큐레이션하고 `scripts/verify-seed.ts`가 라이브로 +검증합니다. 서드파티 고지는 LICENSE를 보세요. 원래 빌드 히스토리는 포크의 +`apps/preipo` 커밋에 남아 있습니다. ## 빠른 시작 @@ -34,7 +43,7 @@ bun test src # 단위 + 픽스처 + 통합 테스트 ``` 환경 변수(`.env.example` 참고): `PREIPO_DB_PATH`, `PREIPO_POLL_INTERVAL_MS`, -`PREIPO_PORT`. +`PREIPO_PUBLIC_EVERY_N_CYCLES`, `PREIPO_STOCK_POLL_INTERVAL_MS`, `PREIPO_PORT`. ## 데이터 모델 (SQLite, WAL) @@ -44,6 +53,7 @@ bun test src # 단위 + 픽스처 + 통합 테스트 | `raw_pairs` | DexScreener가 돌려준 모든 페어의 전체 수치 필드 — 나중에 어떤 집계 방식으로든 다시 돌릴 수 있을 만큼 | 90일 (매일 밤 정리) | | `raw_responses` | 15사이클마다 `{민트: 원본 응답}` 맵을 gzip으로 — 스키마가 바뀔 경우를 대비한 보험 | 영구 | | `variant_ticks` | 사이클마다 variant별 집계 가격, `algo_version` 라벨이 붙음 | 영구 | +| `underlying_ticks` | 주식 사이클마다 티커별 기초 거래소 가격 (Yahoo v8 chart) | 영구 | 사이클 하나는 트랜잭션 하나로 씁니다. 집계 임계값을 건드리면 `src/aggregate/aggregate.ts`의 `ALGO_VERSION`을 올리세요. 기존 틱은 예전 라벨을 유지하고, `raw_pairs`를 다시 돌려서 @@ -58,6 +68,21 @@ quote 쪽에 있는 밈코인 페어를 쳐냅니다) → 가격이 유한하고 base 페어가 하나도 없으면 `no_pairs`(오늘 기준 Ondo SPCXon이 그렇습니다), 전부 걸러지면 `all_filtered`입니다. +## 기초 주식 대비 프리미엄 (공개주식만) + +`프리미엄 = 토큰 가격 / (앵커 가격 × anchor_scale) − 1`. 저장된 두 시계열에서 +읽기 시점에 계산하고 따로 저장하지 않습니다 — 양쪽 다리가 그대로 남아 있어야 +나중에 어떤 방식으로든 다시 계산할 수 있습니다. `anchor_scale`은 액면분할 괴리를 +처리합니다(넷플릭스는 2025년 11월 10:1 분할했는데 토큰은 여전히 분할 전 1주를 +표현합니다 — NFLX ×10으로 앵커링). 미국 장이 닫혀 있으면 앵커는 종가이고, +프리미엄은 "장이 닫힌 동안 체인이 생각하는 주가"가 됩니다. `sk-hynix`는 USD +앵커가 없어(KRX 상장) 스프레드만 추적합니다. + +앵커 소스: Yahoo v8 chart API. 키 불필요, 주식 사이클마다 티커당 1회, 브라우저 +UA 필수. 티커 맵과 분할 배율은 `data/extract-equity-seed.ts`에서 큐레이션하고 +`bun scripts/verify-seed.ts`로 검증합니다(앵커는 USD로 해석돼야 하고, +|프리미엄| > 20%면 잘못된 매핑으로 플래그). + ## DexScreener에 관해 알아낸 것 (2026-08-13 확인, 다시 삽질하지 마세요) - `GET /latest/dex/tokens/{mint}` — 무료, 키 불필요, 분당 약 300회 제한. 우리는 @@ -92,6 +117,11 @@ curl https://.fly.dev/healthz # 2분 안에 degraded:false fly logs # litestream "replicating to" 줄 ``` +`degraded`는 프리미엄 시계열의 **양쪽 다리**를 다 봅니다. `collectorDegraded` +(5분간 성공한 DexScreener 폴 없음) 또는 `anchorsDegraded`(주식 사이클 3번 동안 +기초 주가 틱이 안 써짐 — 예: Yahoo가 차단하기 시작)면 켜집니다. 알림은 이 값에 +거세요. 엔드포인트 자체는 프로세스가 살아 있는 한 항상 200을 돌려줍니다. + Litestream 시크릿이 없어도 entrypoint는 수집기를 그대로 돌립니다(백업 복제는 건너뛰고 로그에 크게 남깁니다). 그래도 시크릿은 최대한 빨리 넣으세요. @@ -111,10 +141,11 @@ sqlite3 /tmp/restored.db "SELECT count(*), max(ts_ms) FROM variant_ticks" ## 관리 작업 -- **시드 갱신** (수동, 가끔): 상위 tokens.xyz 레지스트리에 새 프리 IPO 민트가 - 추가됩니다. `data/extract-preipo-seed.ts`는 tokens.xyz 저장소 안에서만 돌아갑니다. - `/apps/preipo/data/`로 복사한 뒤 거기서 `bun apps/preipo/data/extract-preipo-seed.ts`를 - 돌리고, 갱신된 JSON을 여기로 가져와 검토하고 커밋하세요. +- **시드 갱신** (수동, 가끔): 상위 tokens.xyz 레지스트리에 새 민트가 추가됩니다. + `data/extract-preipo-seed.ts`와 `data/extract-equity-seed.ts`는 tokens.xyz 저장소 + 안에서만 돌아갑니다. `/apps/preipo/data/`로 복사한 뒤 거기서 bun으로 + 돌리고, 갱신된 JSON을 여기로 가져온 다음 `bun scripts/verify-seed.ts`로 앵커 + 티커 전수 검증(분할 괴리 포함)을 통과시키고 커밋하세요. - **정리**는 자동입니다 (90일 지난 raw_pairs, 하루 한 번). ## 알려진 한계 diff --git a/README.md b/README.md index a6ef35a..dfbccb6 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,41 @@ -# pretape — the on-chain pre-IPO tape +# pretape — the on-chain equity tape *English · [한국어](README.ko.md)* -Collects and serves on-chain prices of tokenized pre-IPO equities on Solana -(SpaceX, OpenAI, Anthropic, Anduril, Polymarket, Kalshi — 6 assets / 11 mints -across issuers: PreStocks, Ondo, Backpack Securities, xStock/Backed, -Tessera/RWA.xyz). The point: the same company trades at materially different -prices across issuers (e.g. OpenAI PreStocks vs Tessera ≈ 34% spread, -2026-08-13), and nobody else shows that in one place. - -One Bun process does everything: polls DexScreener per mint every 60s, writes -raw pairs + liquidity-weighted aggregate ticks to SQLite, serves the JSON API -and the live board over Hono. +Collects and serves on-chain prices of tokenized equities on Solana, in two +sections: + +- **Pre-IPO** (6 assets / 11 mints: SpaceX, OpenAI, Anthropic, Anduril, + Polymarket, Kalshi): the same company trades at materially different prices + across issuers (e.g. OpenAI PreStocks vs Tessera ≈ 34% spread, 2026-08-13), + no public reference price exists, and nobody else shows that in one place. +- **Tokenized stocks vs exchange** (75 assets / 156 mints, every multi-issuer + public equity in the tokens.xyz registry: AAPL, TSLA, NVDA…): each token is + measured against the real exchange price of its underlying stock (Yahoo, 74 + anchor tickers), producing a **premium/discount series**. Tokens trade 24/7 + while exchanges don't — the off-hours premium is on-chain price discovery + that no exchange feed contains. + +One Bun process does everything: polls DexScreener per mint (pre-IPO every +60s, public equities every 5th cycle), polls Yahoo per anchor ticker every +5 min, writes raw pairs + liquidity-weighted aggregate ticks + underlying +ticks to SQLite, serves the JSON API and the live board over Hono. **The time series is the product.** DexScreener has no historical API — every -minute the collector is down is data that can never be recovered. Deploy +minute the collector is down is data that can never be recovered. The premium +series doubly so: it needs both legs captured at the same moment. Deploy first, polish later. ## Provenance Built out of a fork of [tokens.xyz](https://tokens.xyz) (MIT, Solana -Foundation): `data/preipo-seed.json` is the pre-IPO slice of its hand-curated -asset registry — the mint↔issuer↔redeemability identity graph that makes the -cross-issuer comparison possible. See LICENSE for third-party notices. The -original build history lives in the fork's `apps/preipo` commits. +Foundation): `data/preipo-seed.json` (pre-IPO) and `data/equity-seed.json` +(multi-issuer public equities) are slices of its hand-curated asset registry — +the mint↔issuer↔redeemability identity graph that makes the cross-issuer +comparison possible. Underlying tickers and split scales in the equity seed +are curated here and live-verified by `scripts/verify-seed.ts`. See LICENSE +for third-party notices. The original build history lives in the fork's +`apps/preipo` commits. ## Quick start @@ -36,6 +48,7 @@ bun test src # unit + fixture + integration tests ``` Env vars (see `.env.example`): `PREIPO_DB_PATH`, `PREIPO_POLL_INTERVAL_MS`, +`PREIPO_PUBLIC_EVERY_N_CYCLES`, `PREIPO_STOCK_POLL_INTERVAL_MS`, `PREIPO_PORT`. ## Data model (SQLite, WAL) @@ -46,6 +59,7 @@ Env vars (see `.env.example`): `PREIPO_DB_PATH`, `PREIPO_POLL_INTERVAL_MS`, | `raw_pairs` | every pair DexScreener returned, all numeric fields — enough to re-run any future aggregation | 90 days (nightly prune) | | `raw_responses` | gzip'd `{mint: raw body}` map every 15th cycle — schema-drift insurance | forever | | `variant_ticks` | aggregated price per variant per cycle, labeled with `algo_version` | forever | +| `underlying_ticks` | anchor exchange price per ticker per stock cycle (Yahoo v8 chart) | forever | A whole cycle is written in one transaction. Changing any aggregation threshold ⇒ bump `ALGO_VERSION` in `src/aggregate/aggregate.ts`; old ticks @@ -60,6 +74,21 @@ median** (kills broken pools like the $0.56 SPACEX pool) → price = liquidity-weighted mean of survivors. Zero base pairs → `no_pairs` (true for Ondo SPCXon today); everything filtered → `all_filtered`. +## Premium vs the underlying (public equities only) + +`premium = token_price / (anchor_price × anchor_scale) − 1`, computed at read +time from the two stored series — never persisted, so both legs stay +replayable. `anchor_scale` covers split drift (Netflix split 10:1 in Nov 2025; +the tokens still represent one pre-split share, so NFLX anchors at ×10). +Outside US market hours the anchor is the last close and the premium reads as +"what the chain thinks the stock is worth while the exchange is shut." +`sk-hynix` has no USD anchor (KRX listing) and is tracked spread-only. + +Anchor source: Yahoo v8 chart API, no key, one request per ticker per stock +cycle with a browser UA. Ticker map + split scales are curated in +`data/extract-equity-seed.ts` and checked by `bun scripts/verify-seed.ts` +(anchors must resolve in USD; |premium| > 20% flags a wrong mapping). + ## DexScreener facts (verified 2026-08-13, do not re-learn the hard way) - `GET /latest/dex/tokens/{mint}` — free, no key, ~300 req/min limit. We use @@ -95,6 +124,11 @@ curl https://.fly.dev/healthz # degraded:false within ~2 min fly logs # litestream "replicating to" line ``` +`degraded` covers **both legs** of the premium series: `collectorDegraded` +(no successful DexScreener poll in 5 min) OR `anchorsDegraded` (no underlying +tick written in 3 stock cycles — e.g. Yahoo started blocking). Alert on it; +the endpoint itself always answers 200 while the process lives. + Without the Litestream secrets the entrypoint still runs the collector (backup replication is skipped with a loud log line) — set them ASAP. @@ -115,10 +149,11 @@ If the volume dies, the entrypoint auto-restores from R2 on next boot ## Chores - **Seed refresh** (manual, occasionally): the upstream tokens.xyz registry - gains new pre-IPO mints. `data/extract-preipo-seed.ts` only runs inside a - tokens.xyz repo checkout — copy it to `/apps/preipo/data/`, run - `bun apps/preipo/data/extract-preipo-seed.ts` there, and bring the refreshed - JSON back here for review and commit. + gains new mints. `data/extract-preipo-seed.ts` and + `data/extract-equity-seed.ts` only run inside a tokens.xyz repo checkout — + copy them to `/apps/preipo/data/`, run them there with bun, and + bring the refreshed JSON back here. Then run `bun scripts/verify-seed.ts` + (live-checks every anchor ticker and flags split drift) before committing. - **Prune** runs automatically (raw_pairs > 90 days, once per day). ## Known limits diff --git a/data/equity-seed.json b/data/equity-seed.json new file mode 100644 index 0000000..d179184 --- /dev/null +++ b/data/equity-seed.json @@ -0,0 +1,2713 @@ +{ + "generatedFrom": "tokens.xyz asset-registry (MIT)", + "equities": [ + { + "assetId": "ishares-core-msci-eafe-etf", + "name": "iShares Core MSCI EAFE ETF", + "symbol": "IEFA", + "underlyingTicker": "IEFA", + "anchorScale": 1, + "variants": [ + { + "variantId": "ishares-core-msci-eafe-etf:C9J9vZ8N79GzzxFoRkPWCkGtMKU8akg4FhUk4r9ondo", + "mint": "C9J9vZ8N79GzzxFoRkPWCkGtMKU8akg4FhUk4r9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": null, + "stockVariantTier": null, + "tags": [ + "curated:etfs" + ] + }, + { + "variantId": "ishares-core-msci-eafe-etf:cdVNL7wK8mf1UCDqM6zdrziRv4hmvqWhXeTcck2ondo", + "mint": "cdVNL7wK8mf1UCDqM6zdrziRv4hmvqWhXeTcck2ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": null, + "stockVariantTier": null, + "tags": [ + "curated:etfs" + ] + } + ] + }, + { + "assetId": "abbott", + "name": "Abbott", + "symbol": "ABT", + "underlyingTicker": "ABT", + "anchorScale": 1, + "variants": [ + { + "variantId": "abbott:xStock", + "mint": "XsHtf5RpxsQ7jeJ9ivNewouZKJHbPxhPoEy6yYvULr7", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "abbott:Ondo", + "mint": "129gRoHKhVg7CvPMrqVsEB4uYZo6zV4yDZX6NBg9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "abbvie", + "name": "AbbVie", + "symbol": "ABBV", + "underlyingTicker": "ABBV", + "anchorScale": 1, + "variants": [ + { + "variantId": "abbvie:xStock", + "mint": "XswbinNKyPmzTa5CskMbCPvMW6G5CMnZXZEeQSSQoie", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "abbvie:Ondo", + "mint": "MFerpBVGKZh2jXN7cbJdXRXQTp6j6pbSnSZrfWrondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "accenture", + "name": "Accenture", + "symbol": "ACN", + "underlyingTicker": "ACN", + "anchorScale": 1, + "variants": [ + { + "variantId": "accenture:xStock", + "mint": "Xs5UJzmCRQ8DWZjskExdSQDnbE6iLkRu2jjrRAB1JSU", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "accenture:Ondo", + "mint": "12LxMMJYVSf4LoeqjFE47BQQNRciaH9E3nbDfjH4ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "adobe", + "name": "Adobe", + "symbol": "ADBE", + "underlyingTicker": "ADBE", + "anchorScale": 1, + "variants": [ + { + "variantId": "adobe:xStock", + "mint": "XsDZMGEU8zadWFCkTtPBoPWYcUX3JHVmghnwf2Mve2q", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "adobe:Ondo", + "mint": "12Rh6JhfW4X5fKP16bbUdb4pcVCKDHFB48x8GG33ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "alphabet", + "name": "Alphabet", + "symbol": "GOOGL", + "underlyingTicker": "GOOGL", + "anchorScale": 1, + "variants": [ + { + "variantId": "alphabet:xStock", + "mint": "XsCPL9dNWBMvFtTmwcCA5v3xWPSMEBCszbQdiLLq6aN", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "alphabet:Ondo", + "mint": "bbahNA5vT9WJeYft8tALrH1LXWffjwqVoUbqYa1ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "amazon", + "name": "Amazon", + "symbol": "AMZN", + "underlyingTicker": "AMZN", + "anchorScale": 1, + "variants": [ + { + "variantId": "amazon:xStock", + "mint": "Xs3eBt7uRfJX8QUs4suhyU8p2M6DoUDrJyWBa8LLZsg", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "amazon:Ondo", + "mint": "14Tqdo8V1FhzKsE3W2pFsZCzYPQxxupXRcqw9jv6ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "amd", + "name": "AMD", + "symbol": "AMD", + "underlyingTicker": "AMD", + "anchorScale": 1, + "variants": [ + { + "variantId": "amd:xStock", + "mint": "XsXcJ6GZ9kVnjqGsjBnktRcuwMBmvKWh8S93RefZ1rF", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "amd:Ondo", + "mint": "14diAn5z8kjrKwSC8WLqvBqqe5YmihJhjxRxd8Z6ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "american-express", + "name": "American Express", + "symbol": "AXP", + "underlyingTicker": "AXP", + "anchorScale": 1, + "variants": [ + { + "variantId": "american-express:xStock", + "mint": "XstYR6KGqZFchAuhHoEe4mL8W2kL9aqGW1EDBKRrmGB", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "american-express:Ondo", + "mint": "1WxT6NdK7uqpfXuKpALxL2n3f7Rq61XXeHA8UM4ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "apple", + "name": "Apple", + "symbol": "AAPL", + "underlyingTicker": "AAPL", + "anchorScale": 1, + "variants": [ + { + "variantId": "apple:xStock", + "mint": "XsbEhLAtcf6HdfpFZ5xEMdqW8nfAvcsP5bdudRLJzJp", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "apple:Ondo", + "mint": "123mYEnRLM2LLYsJW3K6oyYh8uP1fngj732iG638ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "applovin", + "name": "AppLovin", + "symbol": "APP", + "underlyingTicker": "APP", + "anchorScale": 1, + "variants": [ + { + "variantId": "applovin:xStock", + "mint": "XsPdAVBi8Zc1xvv53k4JcMrQaEDTgkGqKYeh7AYgPHV", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "applovin:Ondo", + "mint": "14Z8rQQe2Aza33YgEUmj3g3QGNz8DXLiFPuCnsD1ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "asml", + "name": "ASML", + "symbol": "ASML", + "underlyingTicker": "ASML", + "anchorScale": 1, + "variants": [ + { + "variantId": "asml:xStock", + "mint": "XshuHQ6o6SVpUNawvnnTMxsZ4tacZsNgVCLorv7TkFq", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "asml:Ondo", + "mint": "1eLZPRsn8bAKmoxsqDMH9Q2m2k7GMNp6RLSQGm8ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "atandt", + "name": "AT&T", + "symbol": "T", + "underlyingTicker": "T", + "anchorScale": 1, + "variants": [ + { + "variantId": "atandt:xStock", + "mint": "XsqML71RLbyUM3CmY4EYN88zE6BJkpvJACnUKBdGD3x", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "atandt:Ondo", + "mint": "WKMZummev5UcXz5nNKQZvTD6QjNSM2X58uwmDReondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "bank-of-america", + "name": "Bank of America", + "symbol": "BAC", + "underlyingTicker": "BAC", + "anchorScale": 1, + "variants": [ + { + "variantId": "bank-of-america:xStock", + "mint": "XswsQk4duEQmCbGzfqUUWYmi7pV7xpJ9eEmLHXCaEQP", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "bank-of-america:Ondo", + "mint": "Wk8gC6iTNp8dqd4ghkJ3h1giiUnyhykwHh7tYWjondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "bitmine", + "name": "Bitmine", + "symbol": "BMNR", + "underlyingTicker": "BMNR", + "anchorScale": 1, + "variants": [ + { + "variantId": "bitmine:xStock", + "mint": "XsrBCwaH8c46xiqXBChzobgufRKxQxAWUWbndgBNzFn", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "bitmine:Ondo", + "mint": "MYXqkDYbzr7vjXAz2BapR4AiYRXzoikGirrLoRzondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "blackrock", + "name": "BlackRock", + "symbol": "BLK", + "underlyingTicker": "BLK", + "anchorScale": 1, + "variants": [ + { + "variantId": "blackrock:xStock", + "mint": "Xs5ywCCaayf3TFdxFxuJCr25XMoqY6vdfJKkSHE4D6r", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "blackrock:Ondo", + "mint": "5H1VpMzRuoNtRbPTRCz35ETtEUtnkt8hJuQb9v7ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "broadcom", + "name": "Broadcom", + "symbol": "AVGO", + "underlyingTicker": "AVGO", + "anchorScale": 1, + "variants": [ + { + "variantId": "broadcom:xStock", + "mint": "XsgSaSvNSqLTtFuyWPBhK9196Xb9Bbdyjj4fH3cPJGo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "broadcom:Ondo", + "mint": "1FWZtdWN7y38BSXGzbs8D6Shk88oL9atDNgbVz9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "bullish", + "name": "Bullish", + "symbol": "BLSH", + "underlyingTicker": "BLSH", + "anchorScale": 1, + "variants": [ + { + "variantId": "bullish:xStock", + "mint": "XsRiRZg9NGkL3WEZZmqnxvxEoyeHG8LhgzMQF2HRE6F", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "bullish:Ondo", + "mint": "A9PFmw9Hu8zzxDUoU351pio1E1XWBWBfWnjT9qoondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "chevron", + "name": "Chevron", + "symbol": "CVX", + "underlyingTicker": "CVX", + "anchorScale": 1, + "variants": [ + { + "variantId": "chevron:xStock", + "mint": "XsNNMt7WTNA2sV3jrb1NNfNgapxRF5i4i6GcnTRRHts", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "chevron:Ondo", + "mint": "7tgKziACteG26VjV5xKufojKxwTgCFyTwmWUmz5ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "circle", + "name": "Circle", + "symbol": "CRCL", + "underlyingTicker": "CRCL", + "anchorScale": 1, + "variants": [ + { + "variantId": "circle:xStock", + "mint": "XsueG8BtpquVJX9LVLLEGuViXUungE6WmK5YZ3p3bd1", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "circle:Ondo", + "mint": "6xHEyem9hmkGtVq6XGCiQUGpPsHBaoYuYdFNZa5ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "cisco", + "name": "Cisco", + "symbol": "CSCO", + "underlyingTicker": "CSCO", + "anchorScale": 1, + "variants": [ + { + "variantId": "cisco:xStock", + "mint": "Xsr3pdLQyXvDJBFgpR5nexCEZwXvigb8wbPYp4YoNFf", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "cisco:Ondo", + "mint": "7DWcZE1uVc8m2mf9pV8KNov28ET7HsvHkhrhgr9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "coca-cola", + "name": "Coca-Cola", + "symbol": "KO", + "underlyingTicker": "KO", + "anchorScale": 1, + "variants": [ + { + "variantId": "coca-cola:xStock", + "mint": "XsaBXg8dU5cPM6ehmVctMkVqoiRG2ZjMo1cyBJ3AykQ", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "coca-cola:Ondo", + "mint": "e6G4pfFcrdKxJuZ4YXixRFfMbpMvgXG2Mjcus71ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "coinbase", + "name": "Coinbase", + "symbol": "COIN", + "underlyingTicker": "COIN", + "anchorScale": 1, + "variants": [ + { + "variantId": "coinbase:xStock", + "mint": "Xs7ZdzSHLU9ftNJsii5fCeJhoRWSC32SQGzGQtePxNu", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "coinbase:Ondo", + "mint": "5u6KDiNJXxX4rGMfYT4BApZQC5CuDNrG6MHkwp1ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "costco", + "name": "Costco", + "symbol": "COST", + "underlyingTicker": "COST", + "anchorScale": 1, + "variants": [ + { + "variantId": "costco:xStock", + "mint": "XsiRaD8qZm1P3KViowDYFS8rZ8BYT2g7ZEeyztthRye", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "costco:Ondo", + "mint": "6btaz134wjHkR8sqhAYrtSM6tavftfxnRvnyMd8ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "crowdstrike", + "name": "CrowdStrike", + "symbol": "CRWD", + "underlyingTicker": "CRWD", + "anchorScale": 1, + "variants": [ + { + "variantId": "crowdstrike:xStock", + "mint": "Xs7xXqkcK7K8urEqGg52SECi79dRp2cEKKuYjUePYDw", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "crowdstrike:Ondo", + "mint": "cdKfoNjbXgnSuxvoajhtH3uixfZhq1YXhQsS1Rwondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "eli-lilly", + "name": "Eli Lilly", + "symbol": "LLY", + "underlyingTicker": "LLY", + "anchorScale": 1, + "variants": [ + { + "variantId": "eli-lilly:xStock", + "mint": "Xsnuv4omNoHozR6EEW5mXkw8Nrny5rB3jVfLqi6gKMH", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "eli-lilly:Ondo", + "mint": "eGGxZwNSfuNKRqQLKaz2hc4QkA2mau7skyxPdj7ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "exxon-mobil", + "name": "Exxon Mobil", + "symbol": "XOM", + "underlyingTicker": "XOM", + "anchorScale": 1, + "variants": [ + { + "variantId": "exxon-mobil:xStock", + "mint": "XsaHND8sHyfMfsWPj6kSdd5VwvCayZvjYgKmmcNL5qh", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "exxon-mobil:Ondo", + "mint": "qCYD74QnXzd9pzv6pGHQKJVwoibL6sNcPQDnpDiondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "figma", + "name": "Figma", + "symbol": "FIG", + "underlyingTicker": "FIG", + "anchorScale": 1, + "variants": [ + { + "variantId": "figma:xStock", + "mint": "XssfYT48PeXG2EmNdEaVtQcveNhxYnM42KTqdjmiBS9", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "figma:Ondo", + "mint": "aLDdFsr3VTUQaHFK6yNvQxztvxQ8nxW4AMuSGC7ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "gamestop", + "name": "GameStop", + "symbol": "GME", + "underlyingTicker": "GME", + "anchorScale": 1, + "variants": [ + { + "variantId": "gamestop:xStock", + "mint": "Xsf9mBktVB9BSU5kf4nHxPq5hCBJ2j2ui3ecFGxPRGc", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "gamestop:Ondo", + "mint": "aznKt8v32CwYMEcTcB4bGTv8DXWStCpHrcCtyy7ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "goldman-sachs", + "name": "Goldman Sachs", + "symbol": "GS", + "underlyingTicker": "GS", + "anchorScale": 1, + "variants": [ + { + "variantId": "goldman-sachs:xStock", + "mint": "XsgaUyp4jd1fNBCxgtTKkW64xnnhQcvgaxzsbAq5ZD1", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "goldman-sachs:Ondo", + "mint": "BchJRy2snmhJZf3rQ9LJ3ePs2BGfYgfvQNo31d2ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "home-depot", + "name": "Home Depot", + "symbol": "HD", + "underlyingTicker": "HD", + "anchorScale": 1, + "variants": [ + { + "variantId": "home-depot:xStock", + "mint": "XszjVtyhowGjSC5odCqBpW1CtXXwXjYokymrk7fGKD3", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "home-depot:Ondo", + "mint": "MtEXKVN3Pcggy8MPA3eJr15H6SK3RXheScqj9qtondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "ibm", + "name": "IBM", + "symbol": "IBM", + "underlyingTicker": "IBM", + "anchorScale": 1, + "variants": [ + { + "variantId": "ibm:xStock", + "mint": "XspwhyYPdWVM8XBHZnpS9hgyag9MKjLRyE3tVfmCbSr", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "ibm:Ondo", + "mint": "C8bZkgSxXkyT1RgxByp2teJ24hgimPLoyEYoNa9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "intel", + "name": "Intel", + "symbol": "INTC", + "underlyingTicker": "INTC", + "anchorScale": 1, + "variants": [ + { + "variantId": "intel:xStock", + "mint": "XshPgPdXFRWB8tP1j82rebb2Q9rPgGX37RuqzohmArM", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "intel:Ondo", + "mint": "cJpUMp5R7rZ6fGeLHbHhrRuJzK9mkyKDjZqNpT3ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + }, + { + "variantId": "intel:Backpack_Securities", + "mint": "iNTCy1qTsUEZQe3DSocLz1ZXXai34Gdw8THQh5rxFaF", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Backpack Securities", + "stockVariantTier": "share_redeemable", + "tags": [ + "Backpack Securities" + ] + } + ] + }, + { + "assetId": "johnson-and-johnson", + "name": "Johnson & Johnson", + "symbol": "JNJ", + "underlyingTicker": "JNJ", + "anchorScale": 1, + "variants": [ + { + "variantId": "johnson-and-johnson:xStock", + "mint": "XsGVi5eo1Dh2zUpic4qACcjuWGjNv8GCt3dm5XcX6Dn", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "johnson-and-johnson:Ondo", + "mint": "KUXt7LzHWSQXp5eyqMZRxWjAP6yM8BUh4LRHwiwondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "jpmorgan-chase", + "name": "JPMorgan Chase", + "symbol": "JPM", + "underlyingTicker": "JPM", + "anchorScale": 1, + "variants": [ + { + "variantId": "jpmorgan-chase:xStock", + "mint": "XsMAqkcKsUewDrzVkait4e5u4y8REgtyS7jWgCpLV2C", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "jpmorgan-chase:Ondo", + "mint": "E5Gczsavxcomqf6Cw1sGCKLabL1xYD2FzKxVoB4ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "linde", + "name": "Linde", + "symbol": "LIN", + "underlyingTicker": "LIN", + "anchorScale": 1, + "variants": [ + { + "variantId": "linde:xStock", + "mint": "XsSr8anD1hkvNMu8XQiVcmiaTP7XGvYu7Q58LdmtE8Z", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "linde:Ondo", + "mint": "Edik9MoFp8LAXS9HNu2gRFyihwYqDqv4ZmNmVT9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "mara", + "name": "MARA", + "symbol": "MARA", + "underlyingTicker": "MARA", + "anchorScale": 1, + "variants": [ + { + "variantId": "mara:xStock", + "mint": "XsguBZPkM9BDmxspmWe29EmrYZBv21ENcC27Pqh7grB", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "mara:Ondo", + "mint": "ETCJUmuhs5aY62xgEVWCZ5JR8KPdeXUaJz3LuC5ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "marvell", + "name": "Marvell", + "symbol": "MRVL", + "underlyingTicker": "MRVL", + "anchorScale": 1, + "variants": [ + { + "variantId": "marvell:xStock", + "mint": "XsuxRGDzbLjnJ72v74b7p9VY6N66uYgTCyfwwRjVCJA", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "marvell:Ondo", + "mint": "FovBwhoV5KQjZCdhoM6jgXYwXLX3F8vgAfvmLH7ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "mastercard", + "name": "Mastercard", + "symbol": "MA", + "underlyingTicker": "MA", + "anchorScale": 1, + "variants": [ + { + "variantId": "mastercard:xStock", + "mint": "XsApJFV9MAktqnAc6jqzsHVujxkGm9xcSUffaBoYLKC", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "mastercard:Ondo", + "mint": "EsVHcyRxXFJCLMiuYLWhoDygrNe1BJGpYeZ17X7ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "mcdonald-s", + "name": "McDonald's", + "symbol": "MCD", + "underlyingTicker": "MCD", + "anchorScale": 1, + "variants": [ + { + "variantId": "mcdonald-s:xStock", + "mint": "XsqE9cRRpzxcGKDXj1BJ7Xmg4GRhZoyY1KpmGSxAWT2", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "mcdonald-s:Ondo", + "mint": "EUbJjmDt8JA222M91bVLZs211siZ2jzbFArH9N3ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "merck", + "name": "Merck", + "symbol": "MRK", + "underlyingTicker": "MRK", + "anchorScale": 1, + "variants": [ + { + "variantId": "merck:xStock", + "mint": "XsnQnU7AdbRZYe2akqqpibDdXjkieGFfSkbkjX1Sd1X", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "merck:Ondo", + "mint": "bn1fb8dwzafGePqNPrM8m8cbAKQiFqeEPuZkPySondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "meta", + "name": "Meta", + "symbol": "META", + "underlyingTicker": "META", + "anchorScale": 1, + "variants": [ + { + "variantId": "meta:xStock", + "mint": "Xsa62P5mvPszXL1krVUnU5ar38bBSVcWAB6fmPCo5Zu", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "meta:Ondo", + "mint": "fDxs5y12E7x7jBwCKBXGqt71uJmCWsAQ3Srkte6ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "micron", + "name": "Micron Technology", + "symbol": "MU", + "underlyingTicker": "MU", + "anchorScale": 1, + "variants": [ + { + "variantId": "micron:xStock", + "mint": "XsQLZycSZ7QnBBdBXQaTbQdiUcbRqjNJgyBGAMzhHav", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "micron:Ondo", + "mint": "Fz9edBpaURPPzpKVRR1A8PENYDEgHqwx5D5th28ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + }, + { + "variantId": "micron:Backpack_Securities", + "mint": "MUxEsUKSMACyw5fZf68wxf5FLnZVhtU9CwH8uNNGay1", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Backpack Securities", + "stockVariantTier": "share_redeemable", + "tags": [ + "Backpack Securities" + ] + } + ] + }, + { + "assetId": "microsoft", + "name": "Microsoft", + "symbol": "MSFT", + "underlyingTicker": "MSFT", + "anchorScale": 1, + "variants": [ + { + "variantId": "microsoft:xStock", + "mint": "XspzcW1PRtgf6Wj92HCiZdjzKCyFekVD8P5Ueh3dRMX", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "microsoft:Ondo", + "mint": "FRmH6iRkMr33DLG6zVLR7EM4LojBFAuq6NtFzG6ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "microstrategy", + "name": "MicroStrategy", + "symbol": "MSTR", + "underlyingTicker": "MSTR", + "anchorScale": 1, + "variants": [ + { + "variantId": "microstrategy:xStock", + "mint": "XsP7xzNPvEHS1m6qfanPUGjNmdnmsLKEoNAnHjdxxyZ", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "microstrategy:Ondo", + "mint": "FSz4ouiqXpHuGPcpacZfTzbMjScoj5FfzHkiyu2ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + }, + { + "variantId": "microstrategy:Backpack_Securities", + "mint": "MSTRdWXMeZxdE8osAQy3fA4rvTY5rgummDSMEx6U7Nz", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Backpack Securities", + "stockVariantTier": "share_redeemable", + "tags": [ + "Backpack Securities" + ] + } + ] + }, + { + "assetId": "netflix", + "name": "Netflix", + "symbol": "NFLX", + "underlyingTicker": "NFLX", + "anchorScale": 10, + "variants": [ + { + "variantId": "netflix:xStock", + "mint": "XsEH7wWfJJu2ZT3UCFeVfALnVA6CP5ur7Ee11KmzVpL", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "netflix:Ondo", + "mint": "g4KnPrxPLeeKkwvDmZFMtYQPM64eHeShbD55vK6ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "novo-nordisk", + "name": "Novo Nordisk", + "symbol": "NVO", + "underlyingTicker": "NVO", + "anchorScale": 1, + "variants": [ + { + "variantId": "novo-nordisk:xStock", + "mint": "XsfAzPzYrYjd4Dpa9BU3cusBsvWfVB9gBcyGC87S57n", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "novo-nordisk:Ondo", + "mint": "GeV7S8vjP8qdYZpdGv2Xi6e7MUMCk8NAAp2z7g5ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "nvidia", + "name": "NVIDIA", + "symbol": "NVDA", + "underlyingTicker": "NVDA", + "anchorScale": 1, + "variants": [ + { + "variantId": "nvidia:xStock", + "mint": "Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "nvidia:Ondo", + "mint": "gEGtLTPNQ7jcg25zTetkbmF7teoDLcrfTnQfmn2ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "oklo", + "name": "Oklo", + "symbol": "OKLO", + "underlyingTicker": "OKLO", + "anchorScale": 1, + "variants": [ + { + "variantId": "oklo:xStock", + "mint": "XsJb1p4Ks6VFggq68JyMz7S3kdRfkqh4wA6EMyxi4DD", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "oklo:Ondo", + "mint": "m6oDLvJT7rY7M1TxuLWP3pWmAPg2cCWDQR1NKiEondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "open", + "name": "OPEN", + "symbol": "OPEN", + "underlyingTicker": "OPEN", + "anchorScale": 1, + "variants": [ + { + "variantId": "open:xStock", + "mint": "XsGtpmjhmC8kyjVSWL4VicGu36ceq9u55PTgF8bhGv6", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "open:Ondo", + "mint": "ou1uE526v7zmUYP2qCb2LJgfXAyWAtWS9SETtr8ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "oracle", + "name": "Oracle", + "symbol": "ORCL", + "underlyingTicker": "ORCL", + "anchorScale": 1, + "variants": [ + { + "variantId": "oracle:xStock", + "mint": "XsjFwUPiLofddX5cWFHW35GCbXcSu1BCUGfxoQAQjeL", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "oracle:Ondo", + "mint": "GmDADFpfwjfzZq9MfCafMDTS69MgVjtzD7Fd9a4ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "palantir", + "name": "Palantir", + "symbol": "PLTR", + "underlyingTicker": "PLTR", + "anchorScale": 1, + "variants": [ + { + "variantId": "palantir:xStock", + "mint": "XsoBhf2ufR8fTyNSjqfU71DYGaE6Z3SUGAidpzriAA4", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "palantir:Ondo", + "mint": "HfsnTS5qtdStwec9DfBrunRqnAMYMMz1kjv9Hu9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "palo-alto-networks", + "name": "Palo Alto Networks", + "symbol": "PANW", + "underlyingTicker": "PANW", + "anchorScale": 1, + "variants": [ + { + "variantId": "palo-alto-networks:xStock", + "mint": "XsQ6NfzzLH8nspjrB9X8R2K64Zz7Tnxqu12juDiMPMW", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "palo-alto-networks:Ondo", + "mint": "M7hVQomhw4Q2D2op3HvBrZjHu9SryjNvD5haEZ1ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "paypal", + "name": "PayPal", + "symbol": "PYPL", + "underlyingTicker": "PYPL", + "anchorScale": 1, + "variants": [ + { + "variantId": "paypal:xStock", + "mint": "XshWQWYVp5ff8CrAEsGmLVKD47nBWi3Ygn5v8wXK27G", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "paypal:Ondo", + "mint": "hM7B3UQTTR81mS27SxDDPzBbjejmo8fnpFjzgv9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "pepsico", + "name": "PepsiCo", + "symbol": "PEP", + "underlyingTicker": "PEP", + "anchorScale": 1, + "variants": [ + { + "variantId": "pepsico:xStock", + "mint": "Xsv99frTRUeornyvCfvhnDesQDWuvns1M852Pez91vF", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "pepsico:Ondo", + "mint": "gud6b3fYekjhMG5F818BALwbg2vt4JKoow59Md9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "pfizer", + "name": "Pfizer", + "symbol": "PFE", + "underlyingTicker": "PFE", + "anchorScale": 1, + "variants": [ + { + "variantId": "pfizer:xStock", + "mint": "XsAtbqkAP1HJxy7hFDeq7ok6yM43DQ9mQ1Rh861X8rw", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "pfizer:Ondo", + "mint": "Gwh9fPsX1qWATXy63vNaJnAFfwebWQtZaVmPko6ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "procter-and-gamble", + "name": "Procter & Gamble", + "symbol": "PG", + "underlyingTicker": "PG", + "anchorScale": 1, + "variants": [ + { + "variantId": "procter-and-gamble:xStock", + "mint": "XsYdjDjNUygZ7yGKfQaB6TxLh2gC6RRjzLtLAGJrhzV", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "procter-and-gamble:Ondo", + "mint": "GZ8v4NdSG7CTRZqHMgNsTPRULeVi8CpdWd9wZY8ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "riot-platforms", + "name": "Riot Platforms", + "symbol": "RIOT", + "underlyingTicker": "RIOT", + "anchorScale": 1, + "variants": [ + { + "variantId": "riot-platforms:xStock", + "mint": "Xs31mE5EiqjSHEaiX9QDKCN6NvSGCqpJ6f1FNq2wri5", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "riot-platforms:Ondo", + "mint": "i6f3DvZBuLpnGSqS8x6WPeStJ7jNe5KewD6afD5ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "robinhood", + "name": "Robinhood", + "symbol": "HOOD", + "underlyingTicker": "HOOD", + "anchorScale": 1, + "variants": [ + { + "variantId": "robinhood:xStock", + "mint": "XsvNBAYkrDRNhA7wPHQfX3ZUXZyZLdnCQDfHZ56bzpg", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "robinhood:Ondo", + "mint": "BVdXGvmgi6A9oAiwWvBvP76fyTqcCNRJMM7zMN6ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + }, + { + "variantId": "robinhood:Backpack_Securities", + "mint": "HooDYv5RewLRiMLnEVq3VJqdqxhuE6c5eYvqejMC3e9A", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Backpack Securities", + "stockVariantTier": "share_redeemable", + "tags": [ + "Backpack Securities" + ] + } + ] + }, + { + "assetId": "roundhill-memory-etf", + "name": "Roundhill Memory ETF", + "symbol": "DRAM", + "underlyingTicker": "DRAM", + "anchorScale": 1, + "variants": [ + { + "variantId": "roundhill-memory-etf:ondo", + "mint": "oXeD5ZesXfJQ3mxtuZdMaccUsWrE8r1SnpYRP2Bondo", + "symbol": "DRAMon", + "name": "Roundhill Memory ETF (Ondo Tokenized)", + "issuer": "Ondo", + "issuerUrl": "https://app.ondo.finance/", + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "curated:etfs", + "Ondo" + ] + }, + { + "variantId": "roundhill-memory-etf:backpack", + "mint": "DRAMjSWR7HRfJKjRkvQWYL2bcaejaVhuxEcjf4pAY4Cw", + "symbol": "DRAM", + "name": "Roundhill Memory ETF - Backpack Securities", + "issuer": "Backpack Securities", + "issuerUrl": "https://backpack.exchange/", + "label": "Backpack Securities", + "stockVariantTier": "share_redeemable", + "tags": [ + "curated:etfs", + "Backpack Securities" + ] + } + ] + }, + { + "assetId": "salesforce", + "name": "Salesforce", + "symbol": "CRM", + "underlyingTicker": "CRM", + "anchorScale": 1, + "variants": [ + { + "variantId": "salesforce:xStock", + "mint": "XsczbcQ3zfcgAEt9qHQES8pxKAVG5rujPSHQEXi4kaN", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "salesforce:Ondo", + "mint": "7D7ukbcnUNYt7Et5vtsDZhAy28MKu9pkHka1Hp9ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "sandisk", + "name": "SanDisk", + "symbol": "SNDK", + "underlyingTicker": "SNDK", + "anchorScale": 1, + "variants": [ + { + "variantId": "sandisk:xStock", + "mint": "Xswbpc8UqU6e1j9QZEWCjBMjyvz4twqD7PCy6j2e7jj", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "sandisk:Ondo", + "mint": "EJmUVvDqAdfH5zEohkdS4234bi3c6iunqEMobjmondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + }, + { + "variantId": "sandisk:Backpack_Securities", + "mint": "SNDKbwMUQvZhnLnxLduradgLHG5KrPuKwpnrkkGRhfH", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Backpack Securities", + "stockVariantTier": "share_redeemable", + "tags": [ + "Backpack Securities" + ] + } + ] + }, + { + "assetId": "sandp-global", + "name": "S&P Global", + "symbol": "SPGI", + "underlyingTicker": "SPGI", + "anchorScale": 1, + "variants": [ + { + "variantId": "sandp-global:xStock", + "mint": "XsTuW2zaiPbzgrnQ7b4Z99zeULMM7gPxppE61CtpyDj", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "sandp-global:Ondo", + "mint": "JrTYw7A9jihX5TwpRStYviEbsYf2X2VJpZ13719ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "sharplink-gaming", + "name": "SharpLink Gaming", + "symbol": "SBET", + "underlyingTicker": "SBET", + "anchorScale": 1, + "variants": [ + { + "variantId": "sharplink-gaming:xStock", + "mint": "XsEoih2x6nZuUjFwzGoba6MFmtzCkzW2c4YAm6baQbq", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "sharplink-gaming:Ondo", + "mint": "iLDu2jjp2i3Uqc2Vm7K7GLiUj3hR4Un49MtD7c4ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "t-mobile", + "name": "T-Mobile", + "symbol": "TMUS", + "underlyingTicker": "TMUS", + "anchorScale": 1, + "variants": [ + { + "variantId": "t-mobile:xStock", + "mint": "XswCi2U1G6Ppbw1QhG45yKb8UKuR1FKLJrquv2FZSD4", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "t-mobile:Ondo", + "mint": "pDY4GPJfZcNETPG7myXeafQfgJqqVkn81bMYDyfondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "terawulf", + "name": "TeraWulf", + "symbol": "WULF", + "underlyingTicker": "WULF", + "anchorScale": 1, + "variants": [ + { + "variantId": "terawulf:xStock", + "mint": "XsuwUbQSzCJN2wZabD1Gxf1MER2Ypa7hMzVMYB2WawJ", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "terawulf:Ondo", + "mint": "exYfSJt6Fgfhfnp3bAD4roYy97hLF9npjYaLyEXondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "tesla", + "name": "Tesla", + "symbol": "TSLA", + "underlyingTicker": "TSLA", + "anchorScale": 1, + "variants": [ + { + "variantId": "tesla:xStock", + "mint": "XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "tesla:Ondo", + "mint": "KeGv7bsfR4MheC1CkmnAVceoApjrkvBhHYjWb67ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "thermo-fisher", + "name": "Thermo Fisher", + "symbol": "TMO", + "underlyingTicker": "TMO", + "anchorScale": 1, + "variants": [ + { + "variantId": "thermo-fisher:xStock", + "mint": "Xs8drBWy3Sd5QY3aifG9kt9KFs2K3PGZmx7jWrsrk57", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "thermo-fisher:Ondo", + "mint": "T699bgtXQw4CJ59rQ4VzLsupVQUzoL5RmuhHnKrondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "tsmc", + "name": "TSMC", + "symbol": "TSM", + "underlyingTicker": "TSM", + "anchorScale": 1, + "variants": [ + { + "variantId": "tsmc:xStock", + "mint": "XsafvsGtzFqqHgTnA3aPC83EAMkacU5mcGtcSayhpVV", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "tsmc:Ondo", + "mint": "keybg184d4vyXeQdFqs4o99YsMg7xBthxTJ6Ky3ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "uber", + "name": "Uber", + "symbol": "UBER", + "underlyingTicker": "UBER", + "anchorScale": 1, + "variants": [ + { + "variantId": "uber:xStock", + "mint": "XsAsZLF4MmsvS1sDxRMrUz7REjHfwbC9UAMXSRBqgEB", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "uber:Ondo", + "mint": "KJNeFW3kk3ycPjXpC6cbuyckjeYHacc2ekhtAi5ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "unitedhealth", + "name": "UnitedHealth", + "symbol": "UNH", + "underlyingTicker": "UNH", + "anchorScale": 1, + "variants": [ + { + "variantId": "unitedhealth:xStock", + "mint": "XszvaiXGPwvk2nwb3o9C1CX4K6zH8sez11E6uyup6fe", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "unitedhealth:Ondo", + "mint": "kPBGL8vAwKN3UGmr9cjkM2dU79SC3nzTC9yu7F8ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "visa", + "name": "Visa", + "symbol": "V", + "underlyingTicker": "V", + "anchorScale": 1, + "variants": [ + { + "variantId": "visa:xStock", + "mint": "XsqgsbXwWogGJsNcVZ3TyVouy2MbTkfCFhCGGGcQZ2p", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "visa:Ondo", + "mint": "kxEW4oJL75K37VeXaZF1ynbHQATQwhECQKN1374ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "vistra", + "name": "Vistra", + "symbol": "VST", + "underlyingTicker": "VST", + "anchorScale": 1, + "variants": [ + { + "variantId": "vistra:xStock", + "mint": "Xs936JPaWKEXmpu54DZ149GETV7PmsnTqnMWxgcPNAj", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "vistra:Ondo", + "mint": "h6MW8GFpfzxFa1JNn6hZNnBF3t4fj9SHAXKy6LXondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "walmart", + "name": "Walmart", + "symbol": "WMT", + "underlyingTicker": "WMT", + "anchorScale": 1, + "variants": [ + { + "variantId": "walmart:xStock", + "mint": "Xs151QeqTCiuKtinzfRATnUESM2xTU6V9Wy8Vy538ci", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "xStock" + ] + }, + { + "variantId": "walmart:Ondo", + "mint": "LZddqAqKqJW9oMZSjTxCUmbmzBRQtv9gMkD9hZ3ondo", + "symbol": null, + "name": null, + "issuer": null, + "issuerUrl": null, + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "Ondo" + ] + } + ] + }, + { + "assetId": "sk-hynix", + "name": "SK Hynix", + "symbol": "SKHY", + "underlyingTicker": null, + "anchorScale": 1, + "variants": [ + { + "variantId": "sk-hynix:ondo", + "mint": "Huyb2fyDDjSuDKCRWsN9ci2rmcgPo6NFiLbx9ZDondo", + "symbol": "SKHYon", + "name": "SK Hynix (Ondo Tokenized)", + "issuer": "Ondo", + "issuerUrl": "https://app.ondo.finance/", + "label": "Ondo", + "stockVariantTier": "cash_redeemable", + "tags": [ + "curated:stocks", + "Ondo" + ] + }, + { + "variantId": "sk-hynix:backpack", + "mint": "SKHYhSjuRWHgikq8eRKbtBbpABgJSkd7ytQV14i9EQ3", + "symbol": "SKHY", + "name": "SK Hynix - Backpack Securities", + "issuer": "Backpack Securities", + "issuerUrl": "https://backpack.exchange/stocks/SKHY", + "label": "Backpack Securities", + "stockVariantTier": "share_redeemable", + "tags": [ + "curated:stocks", + "Backpack Securities" + ] + }, + { + "variantId": "sk-hynix:xstock", + "mint": "XsnhgGRQwhExfS2bmWzR6EYddKGPRGDEjeJsatkmKqU", + "symbol": "SKHYx", + "name": "SK Hynix xStock", + "issuer": "Backed", + "issuerUrl": "https://backed.fi/", + "label": "xStock", + "stockVariantTier": "cash_redeemable", + "tags": [ + "curated:stocks", + "xStock" + ] + } + ] + } + ] +} diff --git a/data/extract-equity-seed.ts b/data/extract-equity-seed.ts new file mode 100644 index 0000000..89637dc --- /dev/null +++ b/data/extract-equity-seed.ts @@ -0,0 +1,161 @@ +// Extracts every multi-issuer PUBLIC tokenized equity from the tokens.xyz +// asset registry into data/equity-seed.json, tagged with the underlying +// exchange ticker that anchors premium/discount computation. +// +// Does NOT run inside this repo — it imports the registry source directly, so +// it must live in a tokens.xyz repo checkout. To refresh the seed: copy this +// file to /apps/preipo/data/, run +// bun apps/preipo/data/extract-equity-seed.ts +// there, and copy the regenerated JSON back into this repo's data/. +import { listAssets } from '../../../packages/asset-registry/src/registry'; + +// Pre-IPO assets live in preipo-seed.json (no public-market anchor exists). +const PRE_IPO_ASSET_IDS = new Set(['spacex', 'openai']); + +// Underlying exchange ticker per assetId, hand-curated 2026-08-14 and verified +// live against the Yahoo v8 chart API (see scripts/verify-seed.ts). `null` = +// tracked spread-only, no premium anchor: +// - sk-hynix trades in KRW on KRX (000660.KS); its US OTC ADR (HXSCL) is too +// illiquid to be an honest anchor. +// Non-US companies use their USD ADR (TSM, NVO, ASML act as primary US listings). +const UNDERLYING_TICKER_BY_ASSET_ID: Record = { + abbott: 'ABT', + abbvie: 'ABBV', + accenture: 'ACN', + adobe: 'ADBE', + alphabet: 'GOOGL', + amazon: 'AMZN', + amd: 'AMD', + 'american-express': 'AXP', + apple: 'AAPL', + applovin: 'APP', + asml: 'ASML', + atandt: 'T', + 'bank-of-america': 'BAC', + bitmine: 'BMNR', + blackrock: 'BLK', + broadcom: 'AVGO', + bullish: 'BLSH', + chevron: 'CVX', + circle: 'CRCL', + cisco: 'CSCO', + 'coca-cola': 'KO', + coinbase: 'COIN', + costco: 'COST', + crowdstrike: 'CRWD', + 'eli-lilly': 'LLY', + 'exxon-mobil': 'XOM', + figma: 'FIG', + gamestop: 'GME', + 'goldman-sachs': 'GS', + 'home-depot': 'HD', + ibm: 'IBM', + intel: 'INTC', + 'ishares-core-msci-eafe-etf': 'IEFA', + 'johnson-and-johnson': 'JNJ', + 'jpmorgan-chase': 'JPM', + linde: 'LIN', + mara: 'MARA', + marvell: 'MRVL', + mastercard: 'MA', + 'mcdonald-s': 'MCD', + merck: 'MRK', + meta: 'META', + micron: 'MU', + microsoft: 'MSFT', + microstrategy: 'MSTR', + netflix: 'NFLX', + 'novo-nordisk': 'NVO', + nvidia: 'NVDA', + oklo: 'OKLO', + open: 'OPEN', + oracle: 'ORCL', + palantir: 'PLTR', + 'palo-alto-networks': 'PANW', + paypal: 'PYPL', + pepsico: 'PEP', + pfizer: 'PFE', + 'procter-and-gamble': 'PG', + 'riot-platforms': 'RIOT', + robinhood: 'HOOD', + 'roundhill-memory-etf': 'DRAM', + salesforce: 'CRM', + sandisk: 'SNDK', + 'sandp-global': 'SPGI', + 'sharplink-gaming': 'SBET', + 'sk-hynix': null, // KRX-listed (000660.KS, KRW); US OTC ADR too illiquid to anchor + 't-mobile': 'TMUS', + terawulf: 'WULF', + tesla: 'TSLA', + 'thermo-fisher': 'TMO', + tsmc: 'TSM', + uber: 'UBER', + unitedhealth: 'UNH', + visa: 'V', + vistra: 'VST', + walmart: 'WMT', +}; + +// Token units per underlying share, when they diverge from 1:1. +// netflix: NFLX split 10-for-1 in Nov 2025; the tokens still represent one +// pre-split share (verified live 2026-08-14: token $767.91 vs NFLX $77.09, +// ratio 9.96). If issuers rebase later, verify-seed.ts flags it as SUSPECT. +const ANCHOR_SCALE_BY_ASSET_ID: Record = { + netflix: 10, +}; + +interface OutVariant { + variantId: string; + mint: string; + symbol: string | null; + name: string | null; + issuer: string | null; + issuerUrl: string | null; + label: string | null; + stockVariantTier: string | null; + tags: string[]; +} + +const assets = (listAssets() as any[]).filter( + a => + (a.category === 'equity' || a.category === 'etf') && + a.variants.length >= 2 && + !PRE_IPO_ASSET_IDS.has(a.assetId), +); + +const missing = assets.filter(a => !(a.assetId in UNDERLYING_TICKER_BY_ASSET_ID)); +if (missing.length > 0) { + console.error( + `Registry has ${missing.length} multi-variant equities without a ticker mapping:\n` + + missing.map(a => ` ${a.assetId}`).join('\n'), + ); + process.exit(1); +} + +const out = { + generatedFrom: 'tokens.xyz asset-registry (MIT)', + equities: assets.map(a => ({ + assetId: a.assetId, + name: a.name ?? a.assetId, + symbol: a.symbol ?? UNDERLYING_TICKER_BY_ASSET_ID[a.assetId], + underlyingTicker: UNDERLYING_TICKER_BY_ASSET_ID[a.assetId], + anchorScale: ANCHOR_SCALE_BY_ASSET_ID[a.assetId] ?? 1, + variants: a.variants.map( + (v: any): OutVariant => ({ + variantId: v.variantId, + mint: v.mint, + symbol: v.symbol ?? null, + name: v.name ?? null, + issuer: v.issuer ?? null, + issuerUrl: v.issuerUrl ?? null, + label: v.label ?? null, + stockVariantTier: v.stockVariantTier ?? null, + tags: v.tags ?? [], + }), + ), + })), +}; + +const dest = new URL('./equity-seed.json', import.meta.url).pathname; +await Bun.write(dest, `${JSON.stringify(out, null, 2)}\n`); +console.log(`wrote ${dest}: ${out.equities.length} assets / ${out.equities.reduce((n, a) => n + a.variants.length, 0)} variants`); diff --git a/scripts/verify-seed.ts b/scripts/verify-seed.ts new file mode 100644 index 0000000..1674ee1 --- /dev/null +++ b/scripts/verify-seed.ts @@ -0,0 +1,83 @@ +// Sanity-checks data/equity-seed.json against live sources. Run after every +// seed regeneration: +// bun scripts/verify-seed.ts +// +// 1. Every underlyingTicker must resolve on the Yahoo v8 chart API, in USD. +// 2. One variant mint per asset is priced via DexScreener and compared to the +// anchor: |premium| > 20% almost always means the ticker map is wrong. +import seed from '../data/equity-seed.json'; +import { aggregatePairs } from '../src/aggregate/aggregate'; +import type { DexPair } from '../src/dexscreener/types'; + +const YAHOO_UA = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'; + +interface YahooQuote { + price: number; + currency: string; + asOfMs: number; +} + +async function fetchYahoo(ticker: string): Promise { + const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?interval=1d&range=1d`; + const res = await fetch(url, { headers: { 'user-agent': YAHOO_UA } }); + if (!res.ok) return `http ${res.status}`; + const body = (await res.json()) as { + chart?: { result?: { meta?: { regularMarketPrice?: unknown; currency?: unknown; regularMarketTime?: unknown } }[] }; + }; + const meta = body.chart?.result?.[0]?.meta ?? {}; + const price = meta.regularMarketPrice; + if (typeof price !== 'number' || !Number.isFinite(price) || price <= 0) return 'no price in response'; + const asOfSec = typeof meta.regularMarketTime === 'number' ? meta.regularMarketTime : 0; + return { price, currency: String(meta.currency ?? '?'), asOfMs: asOfSec * 1000 }; +} + +async function fetchDexPrice(mint: string): Promise { + const res = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${mint}`); + if (!res.ok) return null; + const body = (await res.json()) as { pairs?: DexPair[] }; + const agg = aggregatePairs(mint, body.pairs ?? []); + return agg.priceUsd; +} + +let failures = 0; +const premiums: { assetId: string; ticker: string; premiumPct: number }[] = []; + +for (const asset of seed.equities) { + const ticker = asset.underlyingTicker; + if (ticker === null) { + console.log(`SKIP ${asset.assetId} (spread-only, no anchor)`); + continue; + } + const quote = await fetchYahoo(ticker); + if (typeof quote === 'string') { + console.log(`FAIL ${asset.assetId} → ${ticker}: ${quote}`); + failures++; + continue; + } + if (quote.currency !== 'USD') { + console.log(`FAIL ${asset.assetId} → ${ticker}: currency ${quote.currency}, need USD`); + failures++; + continue; + } + + const ageMin = quote.asOfMs > 0 ? Math.round((Date.now() - quote.asOfMs) / 60_000) : -1; + const mint = asset.variants[0]!.mint; + const tokenPrice = await fetchDexPrice(mint); + const anchor = quote.price * (asset.anchorScale ?? 1); + if (tokenPrice === null) { + console.log(`WARN ${asset.assetId} → ${ticker} $${quote.price} (${ageMin}m old) | no DEX price for ${mint.slice(0, 8)}…`); + } else { + const premiumPct = (tokenPrice / anchor - 1) * 100; + premiums.push({ assetId: asset.assetId, ticker, premiumPct }); + const flag = Math.abs(premiumPct) > 20 ? 'SUSPECT' : 'ok'; + console.log( + `${flag.padEnd(7)} ${asset.assetId} → ${ticker} $${quote.price}×${asset.anchorScale ?? 1} (${ageMin}m old) | token $${tokenPrice.toFixed(2)} | premium ${premiumPct.toFixed(2)}%`, + ); + if (flag === 'SUSPECT') failures++; + } + await Bun.sleep(300); +} + +console.log(`\n${failures === 0 ? 'ALL OK' : `${failures} FAILURES`} — ${premiums.length} anchored assets checked`); +process.exit(failures === 0 ? 0 : 1); diff --git a/src/board.ts b/src/board.ts index 657d7bc..c1b3604 100644 --- a/src/board.ts +++ b/src/board.ts @@ -1,7 +1,7 @@ import type { Database } from 'bun:sqlite'; -import type { RegistryAsset, StockVariantTier } from './registry'; +import type { AssetGroup, RegistryAsset, StockVariantTier } from './registry'; import { ALGO_VERSION } from './aggregate/aggregate'; -import { latestTicks, priceAt, sparklineSeries, type SeriesPoint } from './db/reads'; +import { latestTicks, latestUnderlying, priceAt, sparklineSeries, type SeriesPoint } from './db/reads'; export interface BoardVariant { variantId: string; @@ -14,9 +14,22 @@ export interface BoardVariant { poolCountTotal: number; status: 'ok' | 'no_pairs' | 'all_filtered' | 'no_data'; change24hPct: number | null; + /** Token price vs the underlying exchange price (× anchorScale); null without an anchor. */ + premiumPct: number | null; tsMs: number | null; } +export interface BoardAnchor { + ticker: string; + /** Raw exchange price (per post-split share). */ + priceUsd: number; + /** Token units per underlying share; premium compares token price to priceUsd × scale. */ + scale: number; + /** Exchange trade time; goes stale while the market is closed. */ + asOfMs: number | null; + fetchedAtMs: number; +} + export interface BoardSpread { minVariantId: string; maxVariantId: string; @@ -27,6 +40,8 @@ export interface BoardAsset { assetId: string; name: string; symbol: string | null; + group: AssetGroup; + anchor: BoardAnchor | null; variants: BoardVariant[]; spread: BoardSpread | null; } @@ -41,7 +56,19 @@ const DAY_MS = 24 * 60 * 60 * 1000; export function buildBoardData(db: Database, assets: RegistryAsset[], now = Date.now()): BoardData { const latest = latestTicks(db, ALGO_VERSION); + const anchors = latestUnderlying(db); const boardAssets: BoardAsset[] = assets.map(asset => { + const under = asset.underlyingTicker !== null ? anchors.get(asset.underlyingTicker) : undefined; + const anchor: BoardAnchor | null = under + ? { + ticker: under.ticker, + priceUsd: under.priceUsd, + scale: asset.anchorScale, + asOfMs: under.asOfMs, + fetchedAtMs: under.tsMs, + } + : null; + const anchorPrice = anchor !== null ? anchor.priceUsd * anchor.scale : null; const variants: BoardVariant[] = asset.variants.map(v => { const tick = latest.get(v.variantId); if (!tick) { @@ -56,13 +83,18 @@ export function buildBoardData(db: Database, assets: RegistryAsset[], now = Date poolCountTotal: 0, status: 'no_data', change24hPct: null, + premiumPct: null, tsMs: null, }; } let change24hPct: number | null = null; + let premiumPct: number | null = null; if (tick.status === 'ok' && tick.priceUsd !== null) { const base = priceAt(db, v.variantId, tick.tsMs - DAY_MS, ALGO_VERSION); if (base !== null && base > 0) change24hPct = (tick.priceUsd / base - 1) * 100; + if (anchorPrice !== null && anchorPrice > 0) { + premiumPct = (tick.priceUsd / anchorPrice - 1) * 100; + } } return { variantId: v.variantId, @@ -75,6 +107,7 @@ export function buildBoardData(db: Database, assets: RegistryAsset[], now = Date poolCountTotal: tick.poolCountTotal, status: tick.status, change24hPct, + premiumPct, tsMs: tick.tsMs, }; }); @@ -90,7 +123,15 @@ export function buildBoardData(db: Database, assets: RegistryAsset[], now = Date spreadPct: (max.priceUsd! / min.priceUsd! - 1) * 100, }; } - return { assetId: asset.assetId, name: asset.name, symbol: asset.symbol, variants, spread }; + return { + assetId: asset.assetId, + name: asset.name, + symbol: asset.symbol, + group: asset.group, + anchor, + variants, + spread, + }; }); return { generatedAt: now, algoVersion: ALGO_VERSION, assets: boardAssets }; } diff --git a/src/config.ts b/src/config.ts index b60992a..e41343c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,9 @@ export interface Config { dbPath: string; pollIntervalMs: number; + /** Public-equity mints are fetched every Nth DexScreener cycle (~156 mints; keeps us far under the ~300 req/min limit). */ + publicEveryNCycles: number; + stockPollIntervalMs: number; port: number; } @@ -14,6 +17,8 @@ export function loadConfig(env: Record = process.env return { dbPath: env.PREIPO_DB_PATH ?? './dev.db', pollIntervalMs: intFromEnv(env.PREIPO_POLL_INTERVAL_MS, 60_000, 10_000, 3_600_000), + publicEveryNCycles: intFromEnv(env.PREIPO_PUBLIC_EVERY_N_CYCLES, 5, 1, 60), + stockPollIntervalMs: intFromEnv(env.PREIPO_STOCK_POLL_INTERVAL_MS, 300_000, 60_000, 3_600_000), // PORT is what most PaaS hosts (Railway, Render, Heroku) inject. Explicit // PREIPO_PORT still wins so Fly's fly.toml [env] keeps working. port: intFromEnv(env.PREIPO_PORT ?? env.PORT, 8080, 1, 65_535), diff --git a/src/db/db.test.ts b/src/db/db.test.ts index ee2a0f4..95b08e6 100644 --- a/src/db/db.test.ts +++ b/src/db/db.test.ts @@ -115,6 +115,9 @@ describe('healthStats', () => { expect(stats.lastSuccessfulPollTsMs).toBeNull(); expect(stats.tickCount).toBe(0); expect(stats.dbSizeBytes).toBeGreaterThan(0); + // Anchor health is only evaluated when explicitly requested. + expect(stats.anchorsDegraded).toBeNull(); + expect(healthStats(db, Date.now(), { staleAfterMs: 1000 }).anchorsDegraded).toBe(true); }); test('healthy right after a successful cycle, degraded 6 minutes later', () => { diff --git a/src/db/migrations.ts b/src/db/migrations.ts index bf3ef58..147aadf 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -74,4 +74,22 @@ export const MIGRATIONS: readonly (readonly string[])[] = [ `CREATE UNIQUE INDEX idx_ticks_variant_ts_algo ON variant_ticks(variant_id, ts_ms, algo_version)`, `CREATE INDEX idx_ticks_ts ON variant_ticks(ts_ms)`, ], + [ + // Underlying exchange prices (the premium anchor), one row per ticker + // per stock-poll cycle. ts_ms is our fetch time; as_of_ms is the + // exchange's regularMarketTime — when the market is closed the same + // as_of_ms repeats and readers treat the anchor as "at close". + // Kept forever: token price alone can't reconstruct the premium series. + `CREATE TABLE underlying_ticks ( + id INTEGER PRIMARY KEY, + ts_ms INTEGER NOT NULL, + ticker TEXT NOT NULL, + price_usd REAL NOT NULL, + currency TEXT NOT NULL, + as_of_ms INTEGER, + source TEXT NOT NULL + )`, + `CREATE INDEX idx_underlying_ticker_ts ON underlying_ticks(ticker, ts_ms)`, + `CREATE INDEX idx_underlying_ts ON underlying_ticks(ts_ms)`, + ], ]; diff --git a/src/db/reads.ts b/src/db/reads.ts index 03f025e..5499b27 100644 --- a/src/db/reads.ts +++ b/src/db/reads.ts @@ -101,16 +101,54 @@ export function sparklineSeries( return series; } +export interface LatestUnderlying { + ticker: string; + tsMs: number; + priceUsd: number; + asOfMs: number | null; +} + +/** Latest anchor price per ticker. */ +export function latestUnderlying(db: Database): Map { + const rows = db + .query( + `SELECT u.ticker, u.ts_ms, u.price_usd, u.as_of_ms + FROM underlying_ticks u + JOIN ( + SELECT ticker, max(ts_ms) AS max_ts FROM underlying_ticks GROUP BY ticker + ) latest ON latest.ticker = u.ticker AND latest.max_ts = u.ts_ms`, + ) + .all() as { ticker: string; ts_ms: number; price_usd: number; as_of_ms: number | null }[]; + return new Map( + rows.map(r => [ + r.ticker, + { ticker: r.ticker, tsMs: r.ts_ms, priceUsd: r.price_usd, asOfMs: r.as_of_ms }, + ]), + ); +} + +export interface AnchorHealthOptions { + /** Anchors count as stale after this many ms without a written underlying tick. */ + staleAfterMs: number; +} + export interface HealthStats { + /** True when either leg is stale — the premium series needs both. */ degraded: boolean; + collectorDegraded: boolean; + /** null when anchor health is not evaluated (no anchored assets configured). */ + anchorsDegraded: boolean | null; lastSuccessfulPollTsMs: number | null; lastSuccessfulPollAgoMs: number | null; lastPollError: string | null; + lastUnderlyingTickTsMs: number | null; + lastUnderlyingTickAgoMs: number | null; tickCount: number; + underlyingTickCount: number; dbSizeBytes: number; } -export function healthStats(db: Database, now = Date.now()): HealthStats { +export function healthStats(db: Database, now = Date.now(), anchors?: AnchorHealthOptions | null): HealthStats { const lastOk = db.query('SELECT ts_ms FROM polls WHERE ok = 1 ORDER BY id DESC LIMIT 1').get() as | { ts_ms: number } | null; @@ -118,15 +156,31 @@ export function healthStats(db: Database, now = Date.now()): HealthStats { | { error: string | null } | null; const ticks = db.query('SELECT count(*) AS n FROM variant_ticks').get() as { n: number }; + const lastUnderlying = db.query('SELECT max(ts_ms) AS ts_ms FROM underlying_ticks').get() as { + ts_ms: number | null; + }; + const underlyingCount = db.query('SELECT count(*) AS n FROM underlying_ticks').get() as { n: number }; const pageCount = db.query('PRAGMA page_count').get() as { page_count: number }; const pageSize = db.query('PRAGMA page_size').get() as { page_size: number }; + const agoMs = lastOk ? now - lastOk.ts_ms : null; + const collectorDegraded = agoMs === null || agoMs > DEGRADED_AFTER_MS; + + const underlyingAgoMs = lastUnderlying.ts_ms !== null ? now - lastUnderlying.ts_ms : null; + const anchorsDegraded = + anchors == null ? null : underlyingAgoMs === null || underlyingAgoMs > anchors.staleAfterMs; + return { - degraded: agoMs === null || agoMs > DEGRADED_AFTER_MS, + degraded: collectorDegraded || anchorsDegraded === true, + collectorDegraded, + anchorsDegraded, lastSuccessfulPollTsMs: lastOk?.ts_ms ?? null, lastSuccessfulPollAgoMs: agoMs, lastPollError: lastErr?.error ?? null, + lastUnderlyingTickTsMs: lastUnderlying.ts_ms, + lastUnderlyingTickAgoMs: underlyingAgoMs, tickCount: ticks.n, + underlyingTickCount: underlyingCount.n, dbSizeBytes: pageCount.page_count * pageSize.page_size, }; } diff --git a/src/db/repo.ts b/src/db/repo.ts index a1b4ed4..44ee212 100644 --- a/src/db/repo.ts +++ b/src/db/repo.ts @@ -168,3 +168,23 @@ export function pruneRawPairs(db: Database, olderThanTsMs: number): number { const res = db.query('DELETE FROM raw_pairs WHERE ts_ms < ?').run(olderThanTsMs); return res.changes; } + +export interface UnderlyingTickRow { + ticker: string; + priceUsd: number; + currency: string; + asOfMs: number | null; + source: string; +} + +/** Writes one stock-poll cycle atomically. */ +export function insertUnderlyingTicks(db: Database, tsMs: number, rows: UnderlyingTickRow[]): void { + if (rows.length === 0) return; + db.transaction(() => { + const stmt = db.query( + `INSERT INTO underlying_ticks (ts_ms, ticker, price_usd, currency, as_of_ms, source) + VALUES (?, ?, ?, ?, ?, ?)`, + ); + for (const r of rows) stmt.run(tsMs, r.ticker, r.priceUsd, r.currency, r.asOfMs, r.source); + })(); +} diff --git a/src/index.ts b/src/index.ts index 108e5a9..3ac2f70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,8 @@ import { openDb } from './db/db'; import { loadRegistry, allVariants } from './registry'; import { makeDexScreenerClient } from './dexscreener/client'; import { startPoller } from './poller'; +import { makeYahooClient } from './stocks/yahoo'; +import { startStockPoller, anchorTickers } from './stocks/poller'; import { createServer } from './server'; const config = loadConfig(); @@ -10,12 +12,19 @@ const db = openDb(config.dbPath); const assets = loadRegistry(); const client = makeDexScreenerClient(); const poller = startPoller({ db, config, assets, client }); -const app = createServer({ db, assets }); +const stockPoller = startStockPoller({ + db, + assets, + client: makeYahooClient(), + intervalMs: config.stockPollIntervalMs, +}); +const app = createServer({ db, assets, anchorStaleAfterMs: config.stockPollIntervalMs * 3 }); const server = Bun.serve({ port: config.port, fetch: app.fetch }); console.log( `[preipo] listening on :${config.port} | db=${config.dbPath} | ` + - `${assets.length} assets / ${allVariants(assets).length} mints @ ${config.pollIntervalMs}ms`, + `${assets.length} assets / ${allVariants(assets).length} mints @ ${config.pollIntervalMs}ms | ` + + `${anchorTickers(assets).length} anchors @ ${config.stockPollIntervalMs}ms`, ); let shuttingDown = false; @@ -23,7 +32,7 @@ async function shutdown(signal: string): Promise { if (shuttingDown) return; shuttingDown = true; console.log(`[preipo] ${signal} received — flushing cycle and closing`); - await poller.stop(); + await Promise.all([poller.stop(), stockPoller.stop()]); await server.stop(); db.close(); process.exit(0); diff --git a/src/poller.ts b/src/poller.ts index f477c30..53f39aa 100644 --- a/src/poller.ts +++ b/src/poller.ts @@ -21,6 +21,8 @@ export interface PollerDeps { assets: RegistryAsset[]; client: DexScreenerClient; log?: (message: string) => void; + /** Pacing between mint fetches; tests set 0. */ + interFetchDelayMs?: number; } export interface Poller { @@ -31,12 +33,15 @@ export interface Poller { export function startPoller(deps: PollerDeps): Poller { const { db, config, client } = deps; const log = deps.log ?? ((m: string) => console.log(m)); - const variants = allVariants(deps.assets); + const fastVariants = allVariants(deps.assets.filter(a => a.group === 'preipo')); + const slowVariants = allVariants(deps.assets.filter(a => a.group !== 'preipo')); const interval = config.pollIntervalMs; + const interFetchDelayMs = deps.interFetchDelayMs ?? INTER_FETCH_DELAY_MS; let stopping = false; let timer: ReturnType | null = null; let inFlight: Promise | null = null; + let cycleIndex = 0; let successfulCycles = 0; let consecutiveTotalFailures = 0; let pendingRetryAfterMs: number | null = null; @@ -44,6 +49,11 @@ export function startPoller(deps: PollerDeps): Poller { async function runCycle(): Promise { const tsMs = Date.now(); + // Cycle 0 includes everything so a fresh deploy snapshots the full + // universe immediately. + const variants = + cycleIndex % config.publicEveryNCycles === 0 ? [...fastVariants, ...slowVariants] : fastVariants; + cycleIndex++; const rawPairs: RawPairRow[] = []; const ticks: TickRow[] = []; const errors: Record = {}; @@ -84,7 +94,7 @@ export function startPoller(deps: PollerDeps): Poller { break; } } - if (!stopping) await Bun.sleep(INTER_FETCH_DELAY_MS); + if (!stopping) await Bun.sleep(interFetchDelayMs); } const ok = fetchCount > 0 && fetchFailCount < fetchCount; diff --git a/src/registry.ts b/src/registry.ts index 98097a8..7e2eac0 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -1,7 +1,14 @@ -import seedJson from '../data/preipo-seed.json'; +import preIpoSeedJson from '../data/preipo-seed.json'; +import equitySeedJson from '../data/equity-seed.json'; export type StockVariantTier = 'share_redeemable' | 'cash_redeemable' | 'not_redeemable'; +/** + * preipo: no public-market anchor exists; polled every DexScreener cycle. + * public: anchored to the underlying exchange price; polled every Nth cycle. + */ +export type AssetGroup = 'preipo' | 'public'; + export interface RegistryVariant { variantId: string; mint: string; @@ -19,6 +26,11 @@ export interface RegistryAsset { assetId: string; name: string; symbol: string | null; + group: AssetGroup; + /** Underlying exchange ticker anchoring premium computation; null = spread-only. */ + underlyingTicker: string | null; + /** Token units per underlying share (e.g. 10 where the underlying split but the token didn't). */ + anchorScale: number; variants: RegistryVariant[]; } @@ -38,7 +50,9 @@ interface SeedAsset { assetId: string; name: string | null; symbol: string | null; - aliases: string[]; + aliases?: string[]; + underlyingTicker?: string | null; + anchorScale?: number; variants: SeedVariant[]; } @@ -51,12 +65,14 @@ function displayLabel(v: SeedVariant): string { return v.tags.find(t => t !== 'curated:stocks') ?? 'PreStocks'; } -export function loadRegistry(): RegistryAsset[] { - const seed = seedJson as unknown as { preIpo: SeedAsset[] }; - return seed.preIpo.map(a => ({ +function toRegistryAsset(a: SeedAsset, group: AssetGroup): RegistryAsset { + return { assetId: a.assetId, name: a.name ?? a.assetId, symbol: a.symbol, + group, + underlyingTicker: a.underlyingTicker ?? null, + anchorScale: a.anchorScale ?? 1, variants: a.variants.map(v => ({ variantId: v.variantId, mint: v.mint, @@ -70,7 +86,16 @@ export function loadRegistry(): RegistryAsset[] { : null, tags: v.tags, })), - })); + }; +} + +export function loadRegistry(): RegistryAsset[] { + const preIpo = (preIpoSeedJson as unknown as { preIpo: SeedAsset[] }).preIpo; + const equities = (equitySeedJson as unknown as { equities: SeedAsset[] }).equities; + return [ + ...preIpo.map(a => toRegistryAsset(a, 'preipo')), + ...equities.map(a => toRegistryAsset(a, 'public')), + ]; } export function allVariants(assets: RegistryAsset[]): RegistryVariant[] { diff --git a/src/server.test.ts b/src/server.test.ts index 424fced..4c30fef 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from 'bun:test'; import { Database } from 'bun:sqlite'; import { migrate } from './db/db'; -import { insertCycle } from './db/repo'; +import { insertCycle, insertUnderlyingTicks } from './db/repo'; import { createServer } from './server'; import { startPoller } from './poller'; import { loadRegistry, allVariants } from './registry'; @@ -29,7 +29,7 @@ describe('server', () => { expect(body.tickCount).toBe(0); }); - test('/healthz recovers after a successful cycle', async () => { + test('/healthz recovers only when BOTH legs are fresh', async () => { const { db, app } = emptyApp(); insertCycle(db, { tsMs: Date.now(), @@ -42,10 +42,54 @@ describe('server', () => { ticks: [], rawBodyGz: null, }); - const body = (await (await app.request('/healthz')).json()) as { degraded: boolean }; + // DEX leg fresh, anchor leg missing — the premium series is broken. + let body = (await (await app.request('/healthz')).json()) as { + degraded: boolean; + collectorDegraded: boolean; + anchorsDegraded: boolean | null; + }; + expect(body.collectorDegraded).toBe(false); + expect(body.anchorsDegraded).toBe(true); + expect(body.degraded).toBe(true); + + insertUnderlyingTicks(db, Date.now(), [ + { ticker: 'AAPL', priceUsd: 300, currency: 'USD', asOfMs: Date.now(), source: 'test' }, + ]); + body = (await (await app.request('/healthz')).json()) as typeof body; + expect(body.anchorsDegraded).toBe(false); expect(body.degraded).toBe(false); }); + test('/healthz flags stale anchors while the DEX leg is still fresh', async () => { + const { db, app } = emptyApp(); + const now = Date.now(); + insertCycle(db, { + tsMs: now, + ok: true, + fetchCount: 1, + fetchFailCount: 0, + latencyMs: 50, + error: null, + rawPairs: [], + ticks: [], + rawBodyGz: null, + }); + // Last anchor write was 16 minutes ago — beyond 3 × 5-minute stock cycles. + insertUnderlyingTicks(db, now - 16 * 60 * 1000, [ + { ticker: 'AAPL', priceUsd: 300, currency: 'USD', asOfMs: now, source: 'test' }, + ]); + const body = (await (await app.request('/healthz')).json()) as { + degraded: boolean; + collectorDegraded: boolean; + anchorsDegraded: boolean | null; + lastUnderlyingTickAgoMs: number | null; + }; + expect(body.collectorDegraded).toBe(false); + expect(body.anchorsDegraded).toBe(true); + expect(body.degraded).toBe(true); + expect(body.lastUnderlyingTickAgoMs!).toBeGreaterThan(15 * 60 * 1000); + }); + test('/api/latest lists every asset with no_data variants on an empty database', async () => { const { app } = emptyApp(); const res = await app.request('/api/latest'); @@ -117,10 +161,17 @@ describe('poller integration (fixture-backed fake client)', () => { }; const poller = startPoller({ db, - config: { dbPath: ':memory:', pollIntervalMs: 3_600_000, port: 0 }, + config: { + dbPath: ':memory:', + pollIntervalMs: 3_600_000, + publicEveryNCycles: 5, + stockPollIntervalMs: 300_000, + port: 0, + }, assets, client: fakeClient, log: () => {}, + interFetchDelayMs: 0, }); try { const variantCount = allVariants(assets).length; diff --git a/src/server.ts b/src/server.ts index 10117ec..326f2af 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,14 +3,19 @@ import type { Database } from 'bun:sqlite'; import type { RegistryAsset } from './registry'; import { allVariants } from './registry'; import { healthStats } from './db/reads'; +import { anchorTickers } from './stocks/poller'; import { buildBoardData, buildSparklines } from './board'; import { renderFragment, renderPage, type BoardProps } from './views/Board'; export interface ServerDeps { db: Database; assets: RegistryAsset[]; + /** Anchors count as stale after this long without an underlying tick (default 3 stock cycles). */ + anchorStaleAfterMs?: number; } +const DEFAULT_ANCHOR_STALE_AFTER_MS = 3 * 300_000; + const BOARD_HOURS = 24; const BOARD_POINTS = 96; // 15-minute buckets over 24h @@ -26,6 +31,12 @@ function clampInt(raw: string | undefined, fallback: number, min: number, max: n export function createServer(deps: ServerDeps): Hono { const app = new Hono(); const knownVariants = new Set(allVariants(deps.assets).map(v => v.variantId)); + // Only judge anchor freshness when the registry actually has anchors — + // a pre-IPO-only deployment must not report itself degraded forever. + const anchorHealth = + anchorTickers(deps.assets).length > 0 + ? { staleAfterMs: deps.anchorStaleAfterMs ?? DEFAULT_ANCHOR_STALE_AFTER_MS } + : null; function boardProps(): BoardProps { const data = buildBoardData(deps.db, deps.assets); @@ -34,9 +45,10 @@ export function createServer(deps: ServerDeps): Hono { } // Always 200 while the process is alive: restarting during a DexScreener - // outage is a restart loop that fixes nothing. Alert on `degraded`. + // outage is a restart loop that fixes nothing. Alert on `degraded` + // (either leg: DEX collector stalled OR anchor quotes stale). app.get('/healthz', c => { - return c.json({ ok: true, ...healthStats(deps.db) }); + return c.json({ ok: true, ...healthStats(deps.db, Date.now(), anchorHealth) }); }); app.get('/', c => c.html(renderPage(boardProps()))); diff --git a/src/stocks/poller.test.ts b/src/stocks/poller.test.ts new file mode 100644 index 0000000..f666cb4 --- /dev/null +++ b/src/stocks/poller.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, test } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { migrate } from '../db/db'; +import { insertCycle, insertUnderlyingTicks } from '../db/repo'; +import { latestUnderlying } from '../db/reads'; +import { buildBoardData } from '../board'; +import { loadRegistry } from '../registry'; +import { ALGO_VERSION } from '../aggregate/aggregate'; +import { anchorTickers, startStockPoller } from './poller'; +import type { StockQuoteClient } from './yahoo'; +import { STOCK_SOURCE } from './yahoo'; + +function freshDb(): Database { + const db = new Database(':memory:'); + migrate(db); + return db; +} + +describe('registry anchors', () => { + test('pre-IPO assets never carry an anchor; public equities mostly do', () => { + const assets = loadRegistry(); + const preIpo = assets.filter(a => a.group === 'preipo'); + expect(preIpo.length).toBeGreaterThan(0); + expect(preIpo.every(a => a.underlyingTicker === null)).toBe(true); + + const publicEq = assets.filter(a => a.group === 'public'); + expect(publicEq.length).toBeGreaterThanOrEqual(70); + const anchored = publicEq.filter(a => a.underlyingTicker !== null); + expect(anchored.length).toBeGreaterThanOrEqual(70); + + // The Netflix pre-split tokens compare against NFLX × 10. + const netflix = assets.find(a => a.assetId === 'netflix')!; + expect(netflix.anchorScale).toBe(10); + }); + + test('anchorTickers dedups and skips null', () => { + const assets = loadRegistry(); + const tickers = anchorTickers(assets); + expect(new Set(tickers).size).toBe(tickers.length); + expect(tickers).toContain('AAPL'); + expect(tickers.length).toBeGreaterThanOrEqual(70); + }); +}); + +describe('stock poller', () => { + test('one cycle writes an underlying tick per ticker; failures are skipped', async () => { + const db = freshDb(); + const assets = loadRegistry().filter(a => ['apple', 'tesla', 'netflix'].includes(a.assetId)); + const fake: StockQuoteClient = { + async fetchQuote(ticker) { + if (ticker === 'TSLA') { + return { + ok: false, + httpStatus: 500, + priceUsd: null, + currency: null, + asOfMs: null, + error: 'http_500', + retryAfterMs: null, + }; + } + return { + ok: true, + httpStatus: 200, + priceUsd: ticker === 'NFLX' ? 77.09 : 303.41, + currency: 'USD', + asOfMs: 1_700_000_000_000, + error: null, + retryAfterMs: null, + }; + }, + }; + const poller = startStockPoller({ + db, + assets, + client: fake, + intervalMs: 3_600_000, + log: () => {}, + interFetchDelayMs: 0, + retryDelayMs: 0, + }); + try { + const deadline = Date.now() + 15_000; + while (Date.now() < deadline) { + const n = (db.query('SELECT count(*) AS n FROM underlying_ticks').get() as { n: number }).n; + if (n >= 2) break; + await Bun.sleep(50); + } + } finally { + await poller.stop(); + } + const latest = latestUnderlying(db); + expect(latest.get('AAPL')?.priceUsd).toBe(303.41); + expect(latest.get('NFLX')?.priceUsd).toBe(77.09); + expect(latest.has('TSLA')).toBe(false); + const row = db.query('SELECT source, currency FROM underlying_ticks LIMIT 1').get() as { + source: string; + currency: string; + }; + expect(row.source).toBe(STOCK_SOURCE); + expect(row.currency).toBe('USD'); + }); +}); + +describe('premium computation', () => { + test('board joins token price against anchor × scale', () => { + const db = freshDb(); + const assets = loadRegistry().filter(a => ['apple', 'netflix', 'spacex'].includes(a.assetId)); + const now = Date.now(); + + const tickFor = (assetId: string, priceUsd: number) => { + const asset = assets.find(a => a.assetId === assetId)!; + const v = asset.variants[0]!; + return { + variantId: v.variantId, + mint: v.mint, + priceUsd, + liquidityUsd: 50_000, + poolCountTotal: 1, + poolCountUsed: 1, + status: 'ok' as const, + algoVersion: ALGO_VERSION, + }; + }; + insertCycle(db, { + tsMs: now, + ok: true, + fetchCount: 3, + fetchFailCount: 0, + latencyMs: 10, + error: null, + rawPairs: [], + ticks: [tickFor('apple', 306.44), tickFor('netflix', 767.91), tickFor('spacex', 200)], + rawBodyGz: null, + }); + insertUnderlyingTicks(db, now, [ + { ticker: 'AAPL', priceUsd: 303.41, currency: 'USD', asOfMs: now, source: STOCK_SOURCE }, + { ticker: 'NFLX', priceUsd: 77.09, currency: 'USD', asOfMs: now, source: STOCK_SOURCE }, + ]); + + const board = buildBoardData(db, assets, now); + + const apple = board.assets.find(a => a.assetId === 'apple')!; + expect(apple.anchor?.ticker).toBe('AAPL'); + expect(apple.anchor?.scale).toBe(1); + // 306.44 / 303.41 - 1 ≈ +1.0% + expect(apple.variants[0]!.premiumPct!).toBeCloseTo(0.9986, 2); + + // Netflix: token price is compared against NFLX × 10, not NFLX. + const netflix = board.assets.find(a => a.assetId === 'netflix')!; + expect(netflix.variants[0]!.premiumPct!).toBeCloseTo((767.91 / 770.9 - 1) * 100, 2); + + // Pre-IPO has no anchor and no premium, but still prices. + const spacex = board.assets.find(a => a.assetId === 'spacex')!; + expect(spacex.anchor).toBeNull(); + expect(spacex.variants[0]!.premiumPct).toBeNull(); + expect(spacex.variants[0]!.priceUsd).toBe(200); + }); +}); diff --git a/src/stocks/poller.ts b/src/stocks/poller.ts new file mode 100644 index 0000000..586ca8d --- /dev/null +++ b/src/stocks/poller.ts @@ -0,0 +1,124 @@ +import type { Database } from 'bun:sqlite'; +import type { RegistryAsset } from '../registry'; +import type { StockQuoteClient } from './yahoo'; +import { STOCK_SOURCE } from './yahoo'; +import { insertUnderlyingTicks, type UnderlyingTickRow } from '../db/repo'; + +const RETRY_DELAY_MS = 5_000; +const INTER_FETCH_DELAY_MS = 250; +const MAX_BACKOFF_MS = 30 * 60 * 1000; +const BACKOFF_AFTER_FAILURES = 3; + +export interface StockPollerDeps { + db: Database; + assets: RegistryAsset[]; + client: StockQuoteClient; + intervalMs: number; + log?: (message: string) => void; + /** Pacing between quote fetches; tests set 0. */ + interFetchDelayMs?: number; + /** Delay before the single same-cycle retry; tests set 0. */ + retryDelayMs?: number; +} + +export interface StockPoller { + stop(): Promise; +} + +/** Distinct anchor tickers across the registry, in seed order. */ +export function anchorTickers(assets: RegistryAsset[]): string[] { + const seen = new Set(); + for (const a of assets) { + if (a.underlyingTicker !== null) seen.add(a.underlyingTicker); + } + return [...seen]; +} + +export function startStockPoller(deps: StockPollerDeps): StockPoller { + const { db, client, intervalMs } = deps; + const log = deps.log ?? ((m: string) => console.log(m)); + const tickers = anchorTickers(deps.assets); + const interFetchDelayMs = deps.interFetchDelayMs ?? INTER_FETCH_DELAY_MS; + const retryDelayMs = deps.retryDelayMs ?? RETRY_DELAY_MS; + + let stopping = false; + let timer: ReturnType | null = null; + let inFlight: Promise | null = null; + let consecutiveTotalFailures = 0; + let pendingRetryAfterMs: number | null = null; + + async function runCycle(): Promise { + const tsMs = Date.now(); + const rows: UnderlyingTickRow[] = []; + const errors: string[] = []; + pendingRetryAfterMs = null; + + for (const ticker of tickers) { + if (stopping) break; + let result = await client.fetchQuote(ticker); + if (!result.ok && result.retryAfterMs === null && !stopping) { + await Bun.sleep(retryDelayMs); + result = await client.fetchQuote(ticker); + } + if (result.ok && result.priceUsd !== null) { + rows.push({ + ticker, + priceUsd: result.priceUsd, + currency: result.currency ?? 'USD', + asOfMs: result.asOfMs, + source: STOCK_SOURCE, + }); + } else { + errors.push(`${ticker}:${result.error ?? 'unknown'}`); + if (result.retryAfterMs !== null) { + pendingRetryAfterMs = result.retryAfterMs; + break; + } + } + if (!stopping) await Bun.sleep(interFetchDelayMs); + } + + try { + insertUnderlyingTicks(db, tsMs, rows); + } catch (err) { + log(`[stocks] cycle write failed: ${err instanceof Error ? err.message : String(err)}`); + return; + } + + consecutiveTotalFailures = rows.length === 0 && tickers.length > 0 ? consecutiveTotalFailures + 1 : 0; + if (errors.length > 0) { + log(`[stocks] cycle ${new Date(tsMs).toISOString()}: ${errors.length}/${tickers.length} quotes failed (${errors.slice(0, 3).join(', ')}${errors.length > 3 ? ', …' : ''})`); + } + } + + function nextDelayMs(): number { + if (pendingRetryAfterMs !== null) return Math.max(pendingRetryAfterMs, intervalMs); + if (consecutiveTotalFailures >= BACKOFF_AFTER_FAILURES) { + const doublings = consecutiveTotalFailures - BACKOFF_AFTER_FAILURES + 1; + return Math.min(intervalMs * 2 ** doublings, MAX_BACKOFF_MS); + } + return intervalMs; + } + + function tick(): void { + if (stopping) return; + inFlight = runCycle() + .catch(err => { + log(`[stocks] cycle crashed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`); + }) + .finally(() => { + inFlight = null; + if (!stopping) timer = setTimeout(tick, nextDelayMs()); + }); + } + + if (tickers.length > 0) tick(); + + return { + async stop() { + stopping = true; + if (timer !== null) clearTimeout(timer); + if (inFlight !== null) await inFlight; + }, + }; +} diff --git a/src/stocks/yahoo.ts b/src/stocks/yahoo.ts new file mode 100644 index 0000000..997fc71 --- /dev/null +++ b/src/stocks/yahoo.ts @@ -0,0 +1,81 @@ +export interface QuoteResult { + ok: boolean; + httpStatus: number | null; + priceUsd: number | null; + currency: string | null; + /** Exchange regularMarketTime in ms; repeats while the market is closed. */ + asOfMs: number | null; + error: string | null; + /** Set when the API answered 429 with a Retry-After header. */ + retryAfterMs: number | null; +} + +export interface StockQuoteClient { + fetchQuote(ticker: string): Promise; +} + +export const STOCK_SOURCE = 'yahoo-chart-v8'; + +const BASE_URL = 'https://query1.finance.yahoo.com/v8/finance/chart'; +// Yahoo serves 429/999 to clients without a browser UA. +const USER_AGENT = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'; + +const fail = (httpStatus: number | null, error: string, retryAfterMs: number | null = null): QuoteResult => ({ + ok: false, + httpStatus, + priceUsd: null, + currency: null, + asOfMs: null, + error, + retryAfterMs, +}); + +export function makeYahooClient(fetchImpl: typeof fetch = fetch, timeoutMs = 10_000): StockQuoteClient { + return { + async fetchQuote(ticker) { + try { + const res = await fetchImpl( + `${BASE_URL}/${encodeURIComponent(ticker)}?interval=1d&range=1d`, + { + signal: AbortSignal.timeout(timeoutMs), + headers: { accept: 'application/json', 'user-agent': USER_AGENT }, + }, + ); + if (!res.ok) { + let retryAfterMs: number | null = null; + if (res.status === 429) { + const header = Number(res.headers.get('retry-after')); + retryAfterMs = Number.isFinite(header) && header > 0 ? header * 1000 : 60_000; + } + return fail(res.status, `http_${res.status}`, retryAfterMs); + } + let meta: Record; + try { + const body = (await res.json()) as { + chart?: { result?: { meta?: Record }[] }; + }; + meta = body.chart?.result?.[0]?.meta ?? {}; + } catch { + return fail(res.status, 'invalid_json'); + } + const price = meta.regularMarketPrice; + if (typeof price !== 'number' || !Number.isFinite(price) || price <= 0) { + return fail(res.status, 'no_price'); + } + const asOfSec = meta.regularMarketTime; + return { + ok: true, + httpStatus: res.status, + priceUsd: price, + currency: typeof meta.currency === 'string' ? meta.currency : null, + asOfMs: typeof asOfSec === 'number' && Number.isFinite(asOfSec) ? asOfSec * 1000 : null, + error: null, + retryAfterMs: null, + }; + } catch (err) { + return fail(null, err instanceof Error ? `${err.name}: ${err.message}` : String(err)); + } + }, + }; +} diff --git a/src/views/Board.tsx b/src/views/Board.tsx index 049b5d9..1e97f31 100644 --- a/src/views/Board.tsx +++ b/src/views/Board.tsx @@ -29,6 +29,14 @@ function fmtSpread(pct: number): string { return pct >= 100 ? `${Math.round(pct)}%` : `${pct.toFixed(1)}%`; } +// An exchange print older than this is a closed/halted market. +const ANCHOR_LIVE_MAX_AGE_MS = 20 * 60 * 1000; + +function anchorAgeText(asOfMs: number | null, now: number): string { + if (asOfMs === null) return ''; + return now - asOfMs <= ANCHOR_LIVE_MAX_AGE_MS ? 'live' : 'at close'; +} + const TIER_TEXT: Record = { share_redeemable: 'share-redeemable', cash_redeemable: 'cash-redeemable', @@ -53,6 +61,18 @@ const Delta: FC<{ pct: number | null }> = ({ pct }) => { ); }; +const Premium: FC<{ pct: number | null }> = ({ pct }) => { + if (pct === null) return null; + const cls = pct >= 0 ? 'premium premium-up' : 'premium premium-down'; + const sign = pct >= 0 ? '+' : '−'; + return ( + + {sign} + {Math.abs(pct).toFixed(2)}% vs stock + + ); +}; + const VariantRow: FC<{ v: BoardVariant; points: SeriesPoint[]; gapMs: number }> = ({ v, points, gapMs }) => (
  • @@ -67,6 +87,8 @@ const VariantRow: FC<{ v: BoardVariant; points: SeriesPoint[]; gapMs: number }>
    + + {v.premiumPct !== null ? ' · ' : ''} {v.liquidityUsd !== null ? `${fmtCompactUsd(v.liquidityUsd)} liq` : ''} {' · '} {v.poolCountUsed}/{v.poolCountTotal} pools @@ -80,16 +102,32 @@ const VariantRow: FC<{ v: BoardVariant; points: SeriesPoint[]; gapMs: number }>
  • ); -const AssetCard: FC<{ asset: BoardAsset; sparklines: Record; gapMs: number }> = ({ - asset, - sparklines, - gapMs, -}) => ( +const AssetCard: FC<{ + asset: BoardAsset; + sparklines: Record; + gapMs: number; + now: number; +}> = ({ asset, sparklines, gapMs, now }) => (

    {asset.name}

    {asset.symbol ? {asset.symbol} : null} + {asset.anchor ? ( + + {asset.anchor.ticker} {fmtPrice(asset.anchor.priceUsd)} + {asset.anchor.scale !== 1 ? ` ×${asset.anchor.scale}` : ''} + {' · '} + {anchorAgeText(asset.anchor.asOfMs, now)} + + ) : null}
    {asset.spread ? ( ); -export const BoardFragment: FC = ({ data, sparklines, sparklineGapMs }) => ( -
    -
    - {data.assets.map(asset => ( - - ))} +function maxAbsPremium(asset: BoardAsset): number | null { + let max: number | null = null; + for (const v of asset.variants) { + if (v.premiumPct !== null && (max === null || Math.abs(v.premiumPct) > max)) { + max = Math.abs(v.premiumPct); + } + } + return max; +} + +function hasLivePrice(asset: BoardAsset): boolean { + return asset.variants.some(v => v.status === 'ok' && v.priceUsd !== null); +} + +/** Priced assets first, widest |premium| (then spread) on top; dataless last. */ +function sortPublic(assets: BoardAsset[]): BoardAsset[] { + return [...assets].sort((a, b) => { + const liveDiff = Number(hasLivePrice(b)) - Number(hasLivePrice(a)); + if (liveDiff !== 0) return liveDiff; + const pa = maxAbsPremium(a) ?? a.spread?.spreadPct ?? -1; + const pb = maxAbsPremium(b) ?? b.spread?.spreadPct ?? -1; + return pb - pa; + }); +} + +export const BoardFragment: FC = ({ data, sparklines, sparklineGapMs }) => { + const preIpo = data.assets.filter(a => a.group === 'preipo'); + const publicEq = sortPublic(data.assets.filter(a => a.group === 'public')); + return ( +
    +

    + Pre-IPO no public reference price exists +

    +
    + {preIpo.map(asset => ( + + ))} +
    +

    + Tokenized stocks vs exchange{' '} + on-chain premium over the underlying listed price +

    +
    + {publicEq.map(asset => ( + + ))} +
    -
    -); + ); +}; // JSX entry points for the plain-.ts server module. export function renderPage(props: BoardProps) { @@ -132,10 +211,10 @@ export const Page: FC = props => ( - pretape — Pre-IPO · On-Chain + pretape — the on-chain equity tape @@ -144,9 +223,12 @@ export const Page: FC = props => (