Skip to content

Turning token data into a 3D aquarium

How Blocktronics draws a token's holders as a tank of fish, rule by rule, and how to build your own with your AI agent and the Blocktronics MCP.

Blocktronics6 min read
The Blocktronics aquarium for DEGEN: shoals of small fish, a few large ones, anemones on the floor and charts painted on the rocks, in dark teal water.
DEGEN on Base in the aquarium, drawn on 14 Oct 2026: holder bands settled through 13 Oct 2026, live transfers from the chain (last 2 h 14 min).

Every fish is a group of real wallets. Pick a token and watch its holders swim in.

—DEGEN holders of $5+ on —
—No consistent holder floors stored
—the settled day the shoals are drawn from
  1. 1Pick a token
  2. 2Press Play
  3. 3Explore, then tap a fish

Aquarium

Reading DEGEN…

Bands become fish

Each day the stored read counts a token's wallets at four floors: $5, $1k, $10k and $100k. Take each floor from the one below and you get four bands. Tap one.

DEGEN's holders, band by band

Reading DEGEN…

A big band would flood the tank, so one fish stands for a round number of wallets. The tank tries 1, 2, 5, 10, 25, 50, 100 and up, and keeps the first number whose fish fit under the band's cap.

The fish rule
Band
  1. 1—
  2. 2—
  3. 5—
  4. 10—
  5. 25—
  6. 50—
  7. 100—
  8. 250—
  9. 500—
  10. 1,000—
  11. 2,500—
  12. 5,000—
  13. 10K—
  14. 25K—
  15. 50K—
  16. 100K—
0fish, one per — wallets · cap 12your number

The caps and the round numbers live in one file, so the tank and this page read the same ones.

src/lib/worlds/aquarium/rules.ts
export const FISH_CAPS = { shrimp: 12, lobster: 9, dolphin: 6, whale: 4 } as const;
/** The share of each cap the lite budget keeps (whales keep at least WHALE_MIN_LITE). */
export const LITE_SHARE = 0.6;
export const WHALE_MIN_LITE = 2;

/** Round numbers of wallets one fish may stand for; the smallest that keeps a cohort under its cap is used. */
export const FISH_UNITS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10_000, 25_000, 50_000, 100_000] as const;
Prompt to paste into your agentAsk your agent for the bands

Use the Blocktronics MCP. Call token_series for DEGEN on Base with metrics ["cohorts"], days 7 and format "rows". Give me the latest day where all four bands are present, and the wallets in each.

Species

Bigger bands get bigger animals. A humpback in the whale band is about 5 times the length of a neon tetra in the shrimp band.

Shrimp

$5–1k

A harlequin rasbora, neon tetra or fancy guppy.

Lobsters

$1k–10k

A zebra danio, sunset platy or pearl gourami.

Dolphins

$10k–100k

An angelfish, discus or silver arowana.

Whales

$100k+

A bottlenose dolphin, orca or humpback whale.

Shrimp $5–1k
Harlequin rasbora · Neon tetra · Fancy guppy
Lobster $1k–10k
Zebra danio · Sunset platy · Pearl gourami
Dolphin $10k–100k
Angelfish · Discus · Silver arowana
Whale $100k+
Bottlenose dolphin · Orca · Humpback whale
Every species in the tank, drawn to the lengths the aquarium gives them

Each fish is a little bigger or smaller than the next, so a shoal never looks stamped out. Small fish school together and big ones cruise alone.

Colour carries no data in the shoals. The one colour that means something is a cyan rim: a real wallet from the live chain.

The live chain

Press Play and the site reads the chain itself, over a public node. On Base the first read reaches back 4,000 blocks, then the tank checks for new ones every 5 seconds.

  1. Pools become anemones

    A contract that traded with 6 wallets or more, or moved the token 12 times, counts as a pool. The 10 largest stand on the floor.

  2. Big wallets get a cyan rim

    Up to 14 of the largest holders swim in, each sized by its estimated dollar value.

  3. New holders swim in

    A wallet joins once it holds $50 or more, at most one every 8 seconds. A wallet that sells out swims away.

  4. Transfers become light

    A comet streaks from one wallet to the other.

  5. Trades stream colour

    Buys run teal from the pool into the fish. Sells run coral back.

  6. Swaps become crabs

    The latest 4 buys gather on the left and sells on the right, around the giant clam that stands for the pool.

Prompt to paste into your agentAsk your agent what moved today

Use the Blocktronics MCP. Call today for DEGEN on Base. Tell me the strongest buying and selling hours so far, and the time the day runs to.

The decor

Three pieces of furniture carry charts of the stored read's last 30 days. They are the same cards you can put on any Blocktronics dashboard.

Stone tablet

Active holders

Loading
Active holders · DEGEN on Base · last 30 days · live Blocktronics dataWhat it measuresOpen as a dashboard
Sunken crate

DEX netflow

Loading
DEX netflow · DEGEN on Base · last 30 days · live Blocktronics dataWhat it measuresOpen as a dashboard
River boulder

Holder cohorts

Loading
Holder cohorts · DEGEN on Base · last 30 days · live Blocktronics dataWhat it measuresOpen as a dashboard

A phone held upright leaves the boulder out, where the floor is narrow.

Prompt to paste into your agentAsk your agent for the crate's chart

Use the Blocktronics MCP. Call render_chart for DEGEN on Base with card netflow and show me the image.

What you're looking at

The Blocktronics aquarium for DEGEN: shoals of small fish, a few large ones, anemones on the floor and charts painted on the rocks, in dark teal water.
DEGEN on Base in the aquarium, drawn on 14 Oct 2026: holder bands settled through 13 Oct 2026, live transfers from the chain (last 2 h 14 min). The numbers sit where the build found each thing in this still

If it isn't in the data, it isn't in the tank.

Build your own

Your agent can fetch the same bands through the Blocktronics MCP and draw a tank of its own. You don't need an account or a key.

Connect the MCP

  1. 1On claude.ai or in the Claude desktop app, go to Customize → Connectors, choose + Add, then Add custom connector.
  2. 2Name it Blocktronics, paste the URL below and choose Continue.
  3. 3Under Authentication pick Sign in when needed, then Add.
  4. 4In a chat, open + → Connectors and turn Blocktronics on. When a tool needs your account, Claude asks you to sign in with your wallet on Blocktronics, then carries on.
Connector URL
https://blocktronics.xyz/api/mcp/account

Connectors added on the web or the desktop app work in the Claude mobile apps too. The Free plan holds one custom connector. On Team and Enterprise an owner adds it under Organization settings → Connectors, then each member chooses Connect.

Cursor, VS Code, Codex, Grok and the OpenAI API have their own tabs on the MCP page.

One call, one answer

token_seriesMCP tool · Token daily seriesTidy daily series for one token, or up to 5 at once (a list in `token`: each token is its own pull, 2 credits each), over the days Blocktronics stores for it (UTC; 60 today): the most recent `days`, or a span from `from` to `to` (ISO days) inside them.
{
  "token": "DEGEN",
  "chain": "base",
  "metrics": [
    "cohorts"
  ],
  "days": 7,
  "format": "rows"
}
What comes backanswered 31 Oct 2026
{}
The answer as your agent gets it.

Each row is one settled day. data_freshness names the last one, and a band can be null on a day its floors didn't line up. Skip that day rather than draw it as zero.

Four steps to a tank

Each step is one prompt, or one file to save.

4 steps0 of 4 done
  1. Step 1: Fetch the bands

    Prompt to paste into your agentFetch the bands

    Use the Blocktronics MCP. Call search_tokens for "DEGEN" and pick the token on Base. Then call token_series with metrics ["cohorts"], days 7 and format "rows". From the latest day with all four counts, tell me the day and each band's wallets. If a count is null, say so.

  2. Step 2: Count the fish

    Prompt to paste into your agentCount the fish

    For each band, pick the smallest number from this list that keeps its fish under the cap: 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000. Caps: 12 shrimp fish, 9 lobster fish, 6 dolphin fish and 4 whale fish. Show the band, its wallets, wallets per fish and the fish.

  3. Step 3: Draw the tank

    Save this as aquarium.html and open it. It asks the MCP from your browser, which works because the MCP's address lets any page call it. The public API doesn't, so to keep the page offline, have your agent paste its answer into DATA.

    aquarium.html
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <title>Holder aquarium</title>
      <style>
        html, body { margin: 0; height: 100%; overflow: hidden; background: #020814; color: #ffffff; font: 14px/1.45 system-ui, sans-serif; }
        canvas { display: block; }
        #label { position: fixed; left: 16px; bottom: 16px; max-width: calc(100% - 64px); padding: 10px 14px; border-radius: 12px; background: rgba(2, 8, 20, 0.78); }
      </style>
      <script type="importmap">
        { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js" } }
      </script>
    </head>
    <body>
      <div id="label">Reading the holder bands…</div>
      <script type="module">
        import * as THREE from "three";
    
        // 1. The data: your agent's token_series answer (metrics ["cohorts"], format "rows"), pasted here.
        //    Leave it null and the page asks the Blocktronics MCP itself, over HTTP.
        const DATA = null;
        const MCP_URL = "https://blocktronics.xyz/api/mcp/account";
        const TOKEN = { token: "DEGEN", chain: "base" };
    
        // 2. The rules, as the Blocktronics aquarium has them: one fish per round number of wallets, a cap per band.
        const BANDS = [
          { key: "cohort_shrimp", name: "Shrimp", range: "$5–1k", cap: 12, size: 0.34, color: "#1f6f7d" },
          { key: "cohort_lobster", name: "Lobster", range: "$1k–10k", cap: 9, size: 0.5, color: "#2a93a3" },
          { key: "cohort_dolphin", name: "Dolphin", range: "$10k–100k", cap: 6, size: 0.85, color: "#45b7c6" },
          { key: "cohort_whale", name: "Whale", range: "$100k+", cap: 4, size: 1.5, color: "#86dde6" },
        ];
        const UNITS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000];
        const unitFor = (wallets, cap) => UNITS.find((u) => Math.ceil(wallets / u) <= cap) ?? UNITS[UNITS.length - 1];
    
        async function load() {
          if (DATA) return DATA;
          const res = await fetch(MCP_URL, {
            method: "POST",
            headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
            body: JSON.stringify({
              jsonrpc: "2.0", id: 1, method: "tools/call",
              params: { name: "token_series", arguments: { ...TOKEN, metrics: ["cohorts"], days: 7, format: "rows" } },
            }),
          });
          const json = await res.json();
          if (!json.result || json.result.isError) throw new Error(json.error?.message ?? "The MCP answered with an error.");
          return json.result.structuredContent;
        }
    
        // The latest day with all four bands. A null is a gap: skip the day, never draw it as zero.
        function latestDay(answer) {
          const rows = answer.data ?? [];
          for (let i = rows.length - 1; i >= 0; i--) {
            if (BANDS.every((b) => Number.isFinite(rows[i][b.key]))) return rows[i];
          }
          return null;
        }
    
        // 3. The tank.
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
        renderer.setSize(innerWidth, innerHeight);
        document.body.appendChild(renderer.domElement);
        const scene = new THREE.Scene();
        scene.background = new THREE.Color("#020814");
        scene.fog = new THREE.Fog("#020814", 9, 22);
        const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
        camera.position.set(0, 0.4, 12);
        scene.add(new THREE.HemisphereLight("#ffffff", "#020814", 1.1));
        const sun = new THREE.DirectionalLight("#ffffff", 1.6);
        sun.position.set(2, 6, 4);
        scene.add(sun);
        const floor = new THREE.Mesh(new THREE.PlaneGeometry(40, 40), new THREE.MeshStandardMaterial({ color: "#0a5163", roughness: 1 }));
        floor.rotation.x = -Math.PI / 2;
        floor.position.y = -3.2;
        scene.add(floor);
    
        // A fish: a body and a tail fin, in its band's colour.
        function makeFish(band) {
          const mat = new THREE.MeshStandardMaterial({ color: band.color, roughness: 0.45, metalness: 0.1 });
          const fish = new THREE.Group();
          const body = new THREE.Mesh(new THREE.SphereGeometry(0.5, 20, 14), mat);
          body.scale.set(1, 0.55, 0.32);
          const tail = new THREE.Mesh(new THREE.ConeGeometry(0.28, 0.45, 24), mat);
          tail.rotation.z = -Math.PI / 2;
          tail.position.x = -0.62;
          fish.add(body, tail);
          fish.scale.setScalar(band.size);
          return fish;
        }
    
        const swimmers = [];
        function populate(day) {
          BANDS.forEach((band, b) => {
            const wallets = day[band.key];
            if (!wallets) return;
            band.unit = unitFor(wallets, band.cap);
            band.fish = Math.ceil(wallets / band.unit);
            for (let i = 0; i < band.fish; i++) {
              const fish = makeFish(band);
              // Small bands school near the surface, big fish cruise deeper, wider and slower.
              const depth = 1.7 - b * 1.1 + (Math.random() - 0.5) * 0.8;
              const radius = 2.2 + b * 0.7 + Math.random() * 3;
              const speed = (0.5 - b * 0.09) * (0.8 + Math.random() * 0.4);
              swimmers.push({ fish, depth, radius, speed, phase: Math.random() * Math.PI * 2 });
              scene.add(fish);
            }
          });
        }
    
        // Bubbles rising from the floor, for a little life.
        const bubbles = Array.from({ length: 40 }, () => {
          const m = new THREE.Mesh(new THREE.SphereGeometry(0.04 + Math.random() * 0.05, 8, 6), new THREE.MeshBasicMaterial({ color: "#86dde6", transparent: true, opacity: 0.35 }));
          m.position.set((Math.random() - 0.5) * 14, -3 + Math.random() * 7, (Math.random() - 0.5) * 6 - 1);
          scene.add(m);
          return { m, speed: 0.4 + Math.random() * 0.6 };
        });
    
        // Each fish swims an oval and faces the way it goes (its nose is +x).
        function place(t, dt) {
          for (const s of swimmers) {
            const a = s.phase + t * s.speed * 0.35;
            s.fish.position.set(Math.cos(a) * s.radius, s.depth + Math.sin(t * 1.3 + s.phase) * 0.15, Math.sin(a) * s.radius * 0.4 - 1.5);
            s.fish.rotation.y = -Math.atan2(Math.cos(a) * 0.4, -Math.sin(a));
          }
          for (const b of bubbles) {
            b.m.position.y += dt * b.speed;
            if (b.m.position.y > 4) b.m.position.y = -3;
          }
        }
    
        // Reduced motion: one still frame.
        const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
        const clock = new THREE.Clock();
        function frame() {
          const dt = Math.min(clock.getDelta(), 0.1);
          place(still ? 0 : clock.getElapsedTime(), still ? 0 : dt);
          renderer.render(scene, camera);
          if (!still) requestAnimationFrame(frame);
        }
    
        addEventListener("resize", () => {
          camera.aspect = innerWidth / innerHeight;
          camera.updateProjectionMatrix();
          renderer.setSize(innerWidth, innerHeight);
          if (still) frame();
        });
    
        // 4. The words: the token, the day and every count, from the answer.
        const label = document.getElementById("label");
        try {
          const answer = await load();
          const day = latestDay(answer);
          if (!day) throw new Error("No day in the answer has all four bands.");
          populate(day);
          label.textContent = "";
          const head = document.createElement("div");
          const name = document.createElement("b");
          name.textContent = answer.token?.symbol ?? TOKEN.token;
          head.append(name, ` holders on ${day.day}`);
          label.append(head);
          for (const band of BANDS) {
            const line = document.createElement("div");
            line.textContent = `${band.name} (${band.range}): ${day[band.key].toLocaleString("en-US")} wallets, ${band.fish ?? 0} fish, one per ${band.unit ?? "–"}`;
            label.append(line);
          }
          frame();
        } catch (err) {
          label.textContent = `No tank: ${err.message}`;
        }
      </script>
    </body>
    </html>
    The starter page: teal fish of four sizes swimming in dark water, with a label listing DEGEN's four holder bands and their fish.
    The starter page as copied, drawing DEGEN's holder bands settled through 13 Oct 2026, read through the MCP on 14 Oct 2026.
  4. Step 4: Check it

    Prompt to paste into your agentCheck the page

    Open aquarium.html and check it against the data. Every band's fish count matches the table from the last step, the day on the page is the day the counts are from, and no null band is drawn as zero fish. List anything that doesn't match.

Colours

Every data colour below passed the dataviz checks against its background. The starter uses the dark aquarium.

The dark aquarium
The chart palette
The colour-blind-safe pair
The brand

Variations

Another token

Change TOKEN to { token: "AERO", chain: "base" }. Every token Blocktronics covers has the same bands.

A fish for every whale

Whales are few, so raise their cap until each gets its own fish.

The week's flow on a rock

Add netflow_usd and paint seven days of bars on a stone, buying days up.

The real tank beside yours

Ask your agent for build_dashboard with the world-aquarium card. It returns a link to the live tank.

Every figure in this article is read live from Blocktronics' public data and carries its day; code numbers come from the code itself

Try it on live data

Every token Blocktronics covers works the same way, and none of it needs an account

Pass it on

More from the blog

The Outlook band for AERO: four tiles under the Outlook's wordmark, the median outcome, higher or lower, similar setups and the track record, stamped Sample outlook, not real.
OutlookDataExplainers

How our Outlook works

The Outlook shows how setups like a token's today played out before, at 7, 30 and 90 days. What it reads, what it leaves out, and why it is never a promise.

3 min readRead it
Smart money struck out for AERO, today's flagged wallets beside four empty outlines: bots, market makers, insiders and pro traders, not split yet.
Smart moneyDataExplainers

Smart money, broken down

"Smart money" lumps bots, market makers, insiders and pro traders into one word. What Blocktronics shows today, one flagged cohort, and the split that's coming.

3 min readRead it