From c8a807319924f82c6d69727f1a77b3822151151d Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:42:34 -0700 Subject: [PATCH] fix(jobs): count token accounts, not distinct owners, for on-chain holder metric MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CoinStatsOnchainJob computed holder = COUNT(DISTINCT owner), which undercounts: the claimable-tokens program authority owns one token account per user (the user-bank mechanism), so many real holders collapse into a single program owner. Verified on-chain — e.g. MONIST has 145 non-zero token accounts across only 7 distinct owners, because one program authority holds 139 of them. Count token accounts (COUNT(*)) to match Birdeye's holder metric and reflect real distinct-user holdings. Co-Authored-By: Claude Opus 4.8 --- jobs/coin_stats_onchain.go | 6 +++++- jobs/coin_stats_onchain_test.go | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/jobs/coin_stats_onchain.go b/jobs/coin_stats_onchain.go index 8812992e..d5f758bf 100644 --- a/jobs/coin_stats_onchain.go +++ b/jobs/coin_stats_onchain.go @@ -239,7 +239,11 @@ func (j *CoinStatsOnchainJob) queryAggregates(ctx context.Context, audioPrice fl FROM artist_coin_prices ), holders AS ( - SELECT mint, COUNT(DISTINCT owner) AS holder + -- Count token accounts (not distinct owners) to match Birdeye's holder + -- metric. Distinct-owner would undercount: the claimable-tokens program + -- authority owns one account per user (the user-bank mechanism), so many + -- real holders share a single program owner. + SELECT mint, COUNT(*) AS holder FROM sol_token_account_balances WHERE balance > 0 GROUP BY mint diff --git a/jobs/coin_stats_onchain_test.go b/jobs/coin_stats_onchain_test.go index e237db8f..506d94a7 100644 --- a/jobs/coin_stats_onchain_test.go +++ b/jobs/coin_stats_onchain_test.go @@ -69,11 +69,14 @@ func TestCoinStatsOnchainJob(t *testing.T) { "artist_coin_pools": { {"address": "pool1", "base_mint": coinMint, "price_usd": 4.0}, }, - // Holders: 3 distinct owners with balance > 0, one with 0 (excluded). + // Holders = token accounts with balance > 0 (matches Birdeye; NOT distinct + // owners). acct1 and acct5 share owner1 (e.g. the claimable-tokens program + // authority holding for two users), so this counts 4, not 3 distinct owners. "sol_token_account_balances": { {"account": "acct1", "mint": coinMint, "owner": "owner1", "balance": 10, "slot": 1}, {"account": "acct2", "mint": coinMint, "owner": "owner2", "balance": 5, "slot": 1}, {"account": "acct3", "mint": coinMint, "owner": "owner3", "balance": 1, "slot": 1}, + {"account": "acct5", "mint": coinMint, "owner": "owner1", "balance": 3, "slot": 1}, {"account": "acct4", "mint": coinMint, "owner": "owner4", "balance": 0, "slot": 1}, }, // Volume through the DBC quote vault (AUDIO leg): |1e8| + |-2e8| = 3e8 -> 3 AUDIO. @@ -130,7 +133,7 @@ func TestCoinStatsOnchainJob(t *testing.T) { require.NoError(t, err) assert.InDelta(t, 4.0, price, 1e-9, "price from pools_price_usd") - assert.Equal(t, 3, holder, "distinct owners with balance > 0") + assert.Equal(t, 4, holder, "token accounts with balance > 0 (owner1 has two)") assert.InDelta(t, 5000.0, liquidity, 1e-6, "TVL = base_usd + quote_usd") assert.InDelta(t, 1000.0, totalSupply, 1e-9, "supply from RPC") assert.InDelta(t, 4000.0, marketCap, 1e-6, "price * supply")