{"id":51587847,"url":"https://github.com/dapperlabs/fandom-graph","last_synced_at":"2026-07-11T12:30:24.366Z","repository":{"id":367702266,"uuid":"1223125663","full_name":"dapperlabs/fandom-graph","owner":"dapperlabs","description":"3D visualization of every NBA Top Shot Moment for a player + every collector who owns one. Real public-API data, static site, MIT.","archived":false,"fork":false,"pushed_at":"2026-06-27T08:19:35.000Z","size":7484,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-27T08:20:08.997Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dapperlabs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-28T03:07:25.000Z","updated_at":"2026-06-27T08:19:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dapperlabs/fandom-graph","commit_stats":null,"previous_names":["dapperlabs/fandom-graph"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/dapperlabs/fandom-graph","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dapperlabs%2Ffandom-graph","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dapperlabs%2Ffandom-graph/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dapperlabs%2Ffandom-graph/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dapperlabs%2Ffandom-graph/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dapperlabs","download_url":"https://codeload.github.com/dapperlabs/fandom-graph/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dapperlabs%2Ffandom-graph/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35362871,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-11T02:00:05.354Z","response_time":104,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-11T12:30:23.565Z","updated_at":"2026-07-11T12:30:24.356Z","avatar_url":"https://github.com/dapperlabs.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fandom Graph\n\n**A 3D pride engine for NBA Top Shot collectors. Pick a player, see their universe of fans.**\n\n[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![Live Demo](https://img.shields.io/badge/demo-topshot.world%2Ffandom-success)](https://topshot.world/fandom)\n[![Vercel](https://img.shields.io/badge/deploy-Vercel-black)](https://vercel.com)\n\n![Fandom Graph](docs/screenshot.png)\n\nFandom Graph renders every minted NBA Top Shot Moment for a given player, plus every Flow wallet that owns one, as a living 3D constellation. The player sits at the center; their Moments orbit them; the collectors who hold those Moments orbit the Moments. Every node is a real Moment. Every edge is a real Flow wallet. Collectors are ranked by **locked score** — the summed average sale price of their locked moments — matching the Top Shot app's leaderboard. The data is drawn from public Flow Network ownership records and the atlas-api locked-score leaderboard.\n\n## What it is\n\nFandom Graph is a static, client-side visualization built to make the Top Shot collection graph legible at a glance. For any given player, it pulls a single JSON file that contains every edition of that player's Moments and every owner of every edition, then lays them out as a force-directed 3D graph: the player node at the center, edition nodes in a ring around them, and collector nodes clustered around the editions they hold. Collectors are ranked by their **locked score** (summed ASP of locked moments), fetched in real-time from the atlas-api leaderboard via a Vercel Edge proxy. Bloom post-processing and a film-grain pass give the scene a cinematic feel; click any collector to spotlight their locked-score rank.\n\nThe headline number (\"LeBron James · 288,890 Moments\") is the sum of every edition's `circulationCount` — the canonical on-chain supply. Heuristic filters remove Top Shot's pack-distribution wallet and any unnamed wallet holding more than 1,500 Moments of a single player (exchange/treasury accounts, not fans). See [`methodology.html`](./methodology.html) for the full data-flow and filtering write-up.\n\nThis is a Dapper Labs prototype, open-sourced as a reference for building rich, data-driven 3D visualizations on top of public Flow Network data.\n\n## Live demo\n\n**\u003chttps://topshot.world/fandom\u003e**\n\n## Quick start\n\nNo build step. Serve the directory with any static server:\n\n```bash\ngit clone https://github.com/dapperlabs/fandom-graph.git\ncd fandom-graph\npython3 -m http.server 8000\n```\n\nThen open **\u003chttp://localhost:8000/fandom?player=LeBron+James\u003e** (omit `?player=` to land on the picker).\n\n## Data format\n\nEach player is described by a single JSON file at `/data/{playerId}.json`, where `{playerId}` is the NBA player ID. The file shape:\n\n```jsonc\n{\n  \"playerId\": \"2544\",\n  \"name\": \"LeBron James\",\n  \"team\": \"Los Angeles Lakers\",\n  \"teamSlug\": \"lakers\",\n  \"teamColors\": [\"#552583\", \"#FDB927\"],\n  \"totalMintedMomentCount\": 288890,        // sum of edition.circulationCount\n  \"editions\": [\n    {\n      \"editionKey\": \"245-c8fb4597-...-0\",  // {setFlowId}-{playId}-{parallelID}\n      \"set\": { \"flowId\": 245, \"flowName\": \"Top Shot This: Playoffs Edition\", ... },\n      \"play\": { \"id\": \"...\", \"flowID\": \"8426\", \"description\": \"...\", \"stats\": { ... } },\n      \"edition\": { \"id\": \"...\", \"circulationCount\": 3148, \"tier\": \"MOMENT_TIER_FANDOM\", \"parallelID\": 0 },\n      \"tier\": \"MOMENT_TIER_FANDOM\",\n      \"serialsSampled\": [                   // sample of minted serials in this edition\n        { \"flowId\": \"51740308\", \"serial\": 1961, \"ownerFlowAddress\": \"ac9b7b3363bba175\" }\n      ],\n      \"totalSerialsFetched\": 3148\n    }\n  ],\n  \"owners\": [                              // every collector who holds at least one Moment\n    {\n      \"type\": \"user\",                      // \"user\" | \"nc\" (non-custodial / unnamed)\n      \"flowAddress\": \"214fdf1a68530b98\",   // Flow Network account address\n      \"dapperID\": \"auth0|5f4917c3...\",     // Dapper auth subject (null for nc wallets)\n      \"username\": \"MasterCollector\",       // display name (null for nc wallets)\n      \"profileImageUrl\": \"https://...\",\n      \"topshotScore\": null,                // Top Shot collector score, when available\n      \"holdings\": 374                       // count of this player's Moments owned\n    }\n  ],\n  \"systemSerialsDropped\": 1700,            // serials removed by the system-wallet filter\n  \"partial\": true                          // true when serials were sampled, not exhaustively fetched\n}\n```\n\nThe `owners[]` array is the collector graph; the `editions[]` array is the edition graph. `serialsSampled[]` links the two: each sampled serial carries an `ownerFlowAddress` that joins back to `owners[].flowAddress`.\n\nThe data pipeline that produces these files is specified in **[SPEC-002 — Fandom Ownership Backend](https://github.com/dapperlabs/nbats-data-auth-portal/blob/main/specs/002-fandom-ownership-backend/spec.md)**.\n\n### For external users\n\nThe included `/data/sample/` directory contains 3 sample player files (**LeBron James**, **Stephen Curry**, **Karl-Anthony Towns**) so the site runs out-of-the-box with no extra setup. To use production data, supply your own `/data/{playerId}.json` files matching the shape documented above.\n\n## Tech stack\n\n- **Three.js r149** + **3d-force-graph 1.70.19** + **GSAP 3.12.5**, loaded via [unpkg](https://unpkg.com) UMD tags in `fandom.html`.\n- **Vercel Edge function** (`api/leaderboard.js`) — proxies the atlas-api locked-score leaderboard, bypassing Cloudflare via the Edge runtime's TLS stack.\n- **Vercel static deploy** — `vercel.json` sets clean URLs, the `/` → `/fandom` rewrite, and cache headers for `/data/*` and hashed assets.\n\n## Project structure\n\n```\nfandom-graph/\n├── fandom.html          # Entry point — picker + 3D graph\n├── fandom.js            # Graph engine (~3,500 lines, no build step)\n├── data-layer.js        # Player roster + per-player data loader + locked leaderboard fetcher\n├── router.js            # URL ↔ state router (?player=\u0026spotlight=)\n├── styles.css           # All styles\n├── methodology.html     # Data methodology page\n├── vercel.json          # Vercel config (cache headers, clean URLs)\n├── api/\n│   └── leaderboard.js   # Vercel Edge function — atlas-api locked-score proxy\n├── data/                # Per-player JSON (index.json + sample files)\n│   ├── 2544.json        # LeBron James (sample)\n│   ├── index.json       # 100-player roster metadata\n│   └── sample/          # 3 sample files for OSS consumers\n├── scripts/             # CI + smoke tests\n│   ├── smoke.mjs        # Playwright smoke (G3, G5, G9)\n│   └── privacy-smoke.mjs # Analytics privacy assertions\n├── docs/                # Documentation\n│   ├── perf-spike.md    # THREE.Points integration spike report\n│   └── screenshot.png   # Screenshot for README\n└── .github/             # GitHub config\n    └── workflows/ci.yml # CI: node --check + privacy smoke + Playwright smoke\n```\n\n## Curated roster\n\nThe picker ships with 100 players. `playerId` is the NBA player ID and the filename of the data file. The picker's `data/index.json` is the canonical 100-player list (generated by SPEC-002's BigQuery pipeline). Per-player `{id}.json` files are fetched on-demand from the CDN at `fandom-v3.vercel.app/data/`.\n\n## What's filtered\n\n- Top Shot's pack-distribution wallet (`b6f2481eba4df97b`) is excluded — it holds tens of thousands of Moments waiting for sale, not as a fan.\n- Heuristic: any unnamed wallet holding more than 1,500 Moments of a single player is treated as a system account (exchange, treasury) and filtered out.\n\n## Testing\n\nSmoke tests use [Playwright](https://playwright.dev). Install dev deps first:\n\n```bash\nnpm install\nnpx playwright install --with-deps chromium\n```\n\nRun the privacy smoke (no browser needed):\n\n```bash\nnode scripts/privacy-smoke.mjs\n```\n\nRun the Playwright smoke (needs a static server on `:8000`):\n\n```bash\npython3 -m http.server 8000 \u0026\nnode scripts/smoke.mjs\n```\n\nCI (`.github/workflows/ci.yml`) runs `node --check` on every JS file, the privacy smoke, and the Playwright smoke on every push and PR.\n\n## Locked scores\n\nThe leaderboard ranks collectors by **locked score** — the summed average sale price (in USD cents) of their locked moments for that player. This matches the Top Shot app's player leaderboard.\n\nLocked scores are fetched in real-time from the [atlas-api](https://github.com/dapperlabs/atlas-api) `LeaderboardService` via a Vercel Edge function at `/api/leaderboard?playerId={id}`. The Edge runtime bypasses Cloudflare's WAF (which blocks Node serverless). The response is cached for 5 minutes at the edge.\n\nIf the atlas API is unavailable, the graph falls back to ownership-count ranking (no crash).\n\n## Contributing\n\nPRs welcome for bug fixes and visualization improvements. See [**CONTRIBUTING.md**](./CONTRIBUTING.md) for local setup, code style, and submission guidelines.\n\n## License\n\nMIT — see [LICENSE](./LICENSE). Use it, fork it, change the players, swap the data source, do whatever.\n\n## Acknowledgments\n\n- [NBA Top Shot](https://nbatopshot.com) for shipping a public GraphQL API and not gating it.\n- The [Flow Network](https://flow.com) — every wallet here is a real Flow account.\n- [3d-force-graph](https://github.com/vasturiano/3d-force-graph) by Vasco Asturiano.\n- [Three.js](https://threejs.org) and [GSAP](https://greensock.com/gsap/) for tweening.\n- [Dapper Labs](https://dapperlabs.com) for open-sourcing this prototype.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdapperlabs%2Ffandom-graph","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdapperlabs%2Ffandom-graph","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdapperlabs%2Ffandom-graph/lists"}