Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Default owner for everything in the repo, unless overridden below.
* @0xSHKWON
69 changes: 50 additions & 19 deletions README.ko.md
Original file line number Diff line number Diff line change
@@ -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` 커밋에 남아 있습니다.

## 빠른 시작

Expand All @@ -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)

Expand All @@ -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`를 다시 돌려서
Expand All @@ -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회 제한. 우리는
Expand Down Expand Up @@ -92,6 +117,11 @@ curl https://<app>.fly.dev/healthz # 2분 안에 degraded:false
fly logs # litestream "replicating to" 줄
```

`degraded`는 프리미엄 시계열의 **양쪽 다리**를 다 봅니다. `collectorDegraded`
(5분간 성공한 DexScreener 폴 없음) 또는 `anchorsDegraded`(주식 사이클 3번 동안
기초 주가 틱이 안 써짐 — 예: Yahoo가 차단하기 시작)면 켜집니다. 알림은 이 값에
거세요. 엔드포인트 자체는 프로세스가 살아 있는 한 항상 200을 돌려줍니다.

Litestream 시크릿이 없어도 entrypoint는 수집기를 그대로 돌립니다(백업 복제는 건너뛰고
로그에 크게 남깁니다). 그래도 시크릿은 최대한 빨리 넣으세요.

Expand All @@ -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 저장소 안에서만 돌아갑니다.
`<tokens-repo>/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 저장소
안에서만 돌아갑니다. `<tokens-repo>/apps/preipo/data/`로 복사한 뒤 거기서 bun으로
돌리고, 갱신된 JSON을 여기로 가져온 다음 `bun scripts/verify-seed.ts`로 앵커
티커 전수 검증(분할 괴리 포함)을 통과시키고 커밋하세요.
- **정리**는 자동입니다 (90일 지난 raw_pairs, 하루 한 번).

## 알려진 한계
Expand Down
75 changes: 55 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -95,6 +124,11 @@ curl https://<app>.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.

Expand All @@ -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 `<tokens-repo>/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 `<tokens-repo>/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
Expand Down
Loading
Loading