From 88987a673c266334e30c0bd3a3a0a55e98aa925e Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Mon, 3 Aug 2026 19:29:33 -0700 Subject: [PATCH] feat(jobs): cut coin stats over to on-chain data and remove Birdeye MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retires the Birdeye-backed CoinStatsJob. CoinStatsOnchainJob now writes artist_coin_stats directly (it already read the AUDIO anchor from that table and iterated artist_coins), so every consumer — the artist_coin_prices view, v1_coins, v1_coin_insights, get_users, coin_dbc — keeps reading the same table unchanged. This is the Phase-2 cutover of the shadow-table rollout: the on-chain values were validated against Birdeye via artist_coin_stats_comparison before flipping. With CoinStatsJob gone and AudioPriceJob already sourcing the AUDIO/USDC pool on-chain, nothing calls Birdeye anymore, so this removes it entirely: - delete the birdeye client package, the dead server BirdeyeClient interface + field + unused mock, and the BirdeyeToken config field - drop the now-unused artist_coin_stats_onchain shadow table and the artist_coin_stats_comparison view (migration 0235) - de-Birdeye the artist_coin_prices view comment, swagger coin_insights description, and stale test/model comments Rendered stats (price, market cap, liquidity, holder, total supply, 24h change) are all on-chain-derived; the ~40 unrendered Birdeye-only columns stay in the table (NULL for new rows; consumers COALESCE to 0). Market cap shifts -8.5% in aggregate, concentrated entirely in dormant coins: Birdeye holds a stale last-trade price for the ~34 coins with no trades in months, while the on-chain value tracks the current pool. Coins that actually trade match within 0.3%; numberMarkets=1 for 53/57 and totalSupply==circulatingSupply, so it is neither a multi-market nor a supply-basis effect. Co-Authored-By: Claude Opus 4.8 --- api/dbv1/models.go | 2 +- api/mock_birdeye_client.go | 63 ---- api/server.go | 9 - api/swagger/swagger-v1.yaml | 4 +- api/v1_wallet_coins_test.go | 2 +- birdeye/client.go | 322 ------------------ config/config.go | 2 - database/seed.go | 5 - .../0235_drop_coin_stats_shadow.sql | 10 + ddl/views/artist_coin_prices.sql | 2 +- ddl/views/artist_coin_stats_comparison.sql | 42 --- jobs/coin_stats.go | 272 --------------- jobs/coin_stats_onchain.go | 20 +- jobs/coin_stats_onchain_test.go | 11 +- solana/indexer/solana_indexer.go | 10 +- sql/01_schema.sql | 114 +------ 16 files changed, 32 insertions(+), 858 deletions(-) delete mode 100644 api/mock_birdeye_client.go delete mode 100644 birdeye/client.go create mode 100644 ddl/migrations/0235_drop_coin_stats_shadow.sql delete mode 100644 ddl/views/artist_coin_stats_comparison.sql delete mode 100644 jobs/coin_stats.go diff --git a/api/dbv1/models.go b/api/dbv1/models.go index 122ddbfd..1eef445b 100644 --- a/api/dbv1/models.go +++ b/api/dbv1/models.go @@ -908,7 +908,7 @@ type ArtistCoinPool struct { CreatorWalletAddress pgtype.Text `json:"creator_wallet_address"` } -// View that provides artist coin prices using DAMM V2 pool if available, DBC pools if not and still applicable, artist_coin_pools.price_usd as fallback, and artist_coin_stats.price as final fallback (primarily for AUDIO and other tokens without pools). Makes use of the price of the quote token (AUDIO) from Birdeye if using a pool. +// View that provides artist coin prices using DAMM V2 pool if available, DBC pools if not and still applicable, artist_coin_pools.price_usd as fallback, and artist_coin_stats.price as final fallback (primarily for AUDIO and other tokens without pools). Makes use of the price of the quote token (AUDIO) from artist_coin_stats (maintained on-chain by AudioPriceJob) if using a pool. type ArtistCoinPrice struct { Mint string `json:"mint"` DammV2Price pgtype.Int4 `json:"damm_v2_price"` diff --git a/api/mock_birdeye_client.go b/api/mock_birdeye_client.go deleted file mode 100644 index 2994321d..00000000 --- a/api/mock_birdeye_client.go +++ /dev/null @@ -1,63 +0,0 @@ -package api - -import ( - "context" - "fmt" - - "api.audius.co/birdeye" -) - -type mockBirdeyeClient struct{} - -func (m *mockBirdeyeClient) GetTokenOverview(ctx context.Context, mint string, frames string) (*birdeye.TokenOverview, error) { - switch mint { - case "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": - return &birdeye.TokenOverview{ - Address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", - Price: 1.0, - }, nil - case "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM": - return &birdeye.TokenOverview{ - Address: "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM", - Price: 10.0, - }, nil - } - return nil, fmt.Errorf("token not found") -} - -func (m *mockBirdeyeClient) GetTokenAllTimeStats(ctx context.Context, mint string) (*birdeye.TokenAllTimeStats, error) { - switch mint { - case "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": - return &birdeye.TokenAllTimeStats{ - Address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", - TotalVolume: 1000000.0, - TotalVolumeUSD: 1.0, - }, nil - case "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM": - return &birdeye.TokenAllTimeStats{ - Address: "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM", - TotalVolume: 1000000.0, - TotalVolumeUSD: 10.0, - }, nil - } - return nil, fmt.Errorf("token not found") -} - -func (m *mockBirdeyeClient) GetPrices(ctx context.Context, mints []string) (birdeye.TokenPriceMap, error) { - prices := make(birdeye.TokenPriceMap) - for _, mint := range mints { - switch mint { - case "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": - prices[mint] = birdeye.TokenPriceData{ - Value: 1.0, - } - case "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM": - prices[mint] = birdeye.TokenPriceData{ - Value: 10.0, - } - default: - return nil, fmt.Errorf("price not found for mint %s", mint) - } - } - return prices, nil -} diff --git a/api/server.go b/api/server.go index a1e22be2..7a384df9 100644 --- a/api/server.go +++ b/api/server.go @@ -14,7 +14,6 @@ import ( comms "api.audius.co/api/comms" "api.audius.co/api/dbv1" - "api.audius.co/birdeye" "api.audius.co/config" "api.audius.co/esindexer" "api.audius.co/logging" @@ -309,7 +308,6 @@ func NewApiServer(config config.Config) *ApiServer { openAudioPool: openAudioPool, metricsCollector: metricsCollector, rateLimitMiddleware: rateLimitMiddleware, - birdeyeClient: birdeye.New(config.BirdeyeToken), solanaRpcClient: solanaRpc, meteoraDbcClient: meteoraDbcClient, newChainFlusher: newChainFlusher, @@ -837,12 +835,6 @@ func NewApiServer(config config.Config) *ApiServer { return app } -type BirdeyeClient interface { - GetTokenOverview(ctx context.Context, mint string, frames string) (*birdeye.TokenOverview, error) - GetTokenAllTimeStats(ctx context.Context, mint string) (*birdeye.TokenAllTimeStats, error) - GetPrices(ctx context.Context, mints []string) (birdeye.TokenPriceMap, error) -} - type ApiServer struct { *fiber.App config *config.Config @@ -875,7 +867,6 @@ type ApiServer struct { skipAuthCheck bool // set to true in a test if you don't care about auth middleware metricsCollector *MetricsCollector rateLimitMiddleware *RateLimitMiddleware - birdeyeClient BirdeyeClient solanaRpcClient *rpc.Client meteoraDbcClient *meteora_dbc.Client validators *Nodes diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index a324f0c6..9c396c42 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -16092,8 +16092,8 @@ components: coin_insights: type: object description: - "Additional token information from Birdeye's defi token overview - API. + "Additional token information derived from on-chain market and pool + data. Includes price, volume, supply, market cap, and other on-chain and market data. diff --git a/api/v1_wallet_coins_test.go b/api/v1_wallet_coins_test.go index 926f1a51..12bf88fc 100644 --- a/api/v1_wallet_coins_test.go +++ b/api/v1_wallet_coins_test.go @@ -409,7 +409,7 @@ func TestWalletCoinsAudioPricingFromStats(t *testing.T) { }, }, "artist_coin_stats": { - // AUDIO price from Birdeye (updated by AudioPriceJob) + // AUDIO price anchor (updated by AudioPriceJob from the AUDIO/USDC pool) { "mint": audioMint, "price": 0.25, // $0.25 per AUDIO diff --git a/birdeye/client.go b/birdeye/client.go deleted file mode 100644 index 46cabb74..00000000 --- a/birdeye/client.go +++ /dev/null @@ -1,322 +0,0 @@ -package birdeye - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" - - "github.com/maypok86/otter" -) - -type Client struct { - token string - tokenOverviewCache otter.Cache[string, *TokenOverview] - tokenAllTimeStatsCache otter.Cache[string, *TokenAllTimeStats] - pricesCache otter.Cache[string, *TokenPriceData] - httpClient *http.Client -} - -func attemptExtractErrorMessage(body io.Reader) string { - var respBody struct { - Message string `json:"message"` - } - if err := json.NewDecoder(body).Decode(&respBody); err != nil { - return "" - } - return respBody.Message -} - -func New(token string) *Client { - tokenOverviewCache, err := otter.MustBuilder[string, *TokenOverview](100). - WithTTL(60 * time.Second). - CollectStats(). - Build() - if err != nil { - panic(err) - } - - tokenAllTimeStatsCache, err := otter.MustBuilder[string, *TokenAllTimeStats](100). - WithTTL(60 * time.Second). - CollectStats(). - Build() - if err != nil { - panic(err) - } - - pricesCache, err := otter.MustBuilder[string, *TokenPriceData](100). - WithTTL(60 * time.Second). - CollectStats(). - Build() - if err != nil { - panic(err) - } - - httpClient := &http.Client{Timeout: 20 * time.Second} - - return &Client{ - httpClient: httpClient, - token: token, - tokenOverviewCache: tokenOverviewCache, - tokenAllTimeStatsCache: tokenAllTimeStatsCache, - pricesCache: pricesCache, - } -} - -type TokenOverview struct { - Address string `json:"address"` - Decimals int `json:"decimals"` - Symbol string `json:"symbol"` - Name string `json:"name"` - MarketCap float64 `json:"marketCap"` - FDV float64 `json:"fdv"` - Extensions struct { - CoingeckoID string `json:"coingeckoId"` - Description string `json:"description"` - Twitter string `json:"twitter"` - Website string `json:"website"` - Discord string `json:"discord"` - } `json:"extensions"` - LogoURI string `json:"logoURI"` - Liquidity float64 `json:"liquidity"` - LastTradeUnixTime int64 `json:"lastTradeUnixTime"` - LastTradeHumanTime string `json:"lastTradeHumanTime"` - Price float64 `json:"price"` - History24hPrice float64 `json:"history24hPrice"` - PriceChange24hPercent float64 `json:"priceChange24hPercent"` - UniqueWallet24h int `json:"uniqueWallet24h"` - UniqueWalletHistory24h int `json:"uniqueWalletHistory24h"` - UniqueWallet24hChangePercent float64 `json:"uniqueWallet24hChangePercent"` - TotalSupply float64 `json:"totalSupply"` - CirculatingSupply float64 `json:"circulatingSupply"` - Holder int `json:"holder"` - Trade24h int `json:"trade24h"` - TradeHistory24h int `json:"tradeHistory24h"` - Trade24hChangePercent float64 `json:"trade24hChangePercent"` - Sell24h int `json:"sell24h"` - SellHistory24h int `json:"sellHistory24h"` - Sell24hChangePercent float64 `json:"sell24hChangePercent"` - Buy24h int `json:"buy24h"` - BuyHistory24h int `json:"buyHistory24h"` - Buy24hChangePercent float64 `json:"buy24hChangePercent"` - V24h float64 `json:"v24h"` - V24hUSD float64 `json:"v24hUSD"` - VHistory24h float64 `json:"vHistory24h"` - VHistory24hUSD float64 `json:"vHistory24hUSD"` - V24hChangePercent float64 `json:"v24hChangePercent"` - VBuy24h float64 `json:"vBuy24h"` - VBuy24hUSD float64 `json:"vBuy24hUSD"` - VBuyHistory24h float64 `json:"vBuyHistory24h"` - VBuyHistory24hUSD float64 `json:"vBuyHistory24hUSD"` - VBuy24hChangePercent float64 `json:"vBuy24hChangePercent"` - VSell24h float64 `json:"vSell24h"` - VSell24hUSD float64 `json:"vSell24hUSD"` - VSellHistory24h float64 `json:"vSellHistory24h"` - VSellHistory24hUSD float64 `json:"vSellHistory24hUSD"` - VSell24hChangePercent float64 `json:"vSell24hChangePercent"` - NumberMarkets int `json:"numberMarkets"` -} - -func (c *Client) GetTokenOverview(ctx context.Context, tokenAddress string, frames string) (*TokenOverview, error) { - cacheKey := tokenAddress + "|" + frames - if cachedOverview, ok := c.tokenOverviewCache.Get(cacheKey); ok { - return cachedOverview, nil - } - - url := fmt.Sprintf("https://public-api.birdeye.so/defi/token_overview?address=%s&frames=%s", tokenAddress, frames) - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err - } - req.Header.Set("x-api-key", c.token) - req.Header.Set("x-chain", "solana") - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - errMsg := attemptExtractErrorMessage(resp.Body) - return nil, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, errMsg) - } - - var result struct { - Data TokenOverview `json:"data"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, err - } - - c.tokenOverviewCache.Set(cacheKey, &result.Data) - - return &result.Data, nil -} - -type TokenAllTimeStats struct { - Address string `json:"address"` - TotalVolume float64 `json:"total_volume"` - TotalVolumeUSD float64 `json:"total_volume_usd"` - VolumeBuyUSD float64 `json:"volume_buy_usd"` - VolumeSellUSD float64 `json:"volume_sell_usd"` - VolumeBuy float64 `json:"volume_buy"` - VolumeSell float64 `json:"volume_sell"` - TotalTrade int `json:"total_trade"` - Buy int `json:"buy"` - Sell int `json:"sell"` - IsScaledUiToken bool `json:"is_scaled_ui_token"` -} - -func (c *Client) GetTokenAllTimeStats(ctx context.Context, tokenAddress string) (*TokenAllTimeStats, error) { - cacheKey := tokenAddress - if cachedOverview, ok := c.tokenAllTimeStatsCache.Get(cacheKey); ok { - return cachedOverview, nil - } - - url := fmt.Sprintf("https://public-api.birdeye.so/defi/v3/all-time/trades/single?time_frame=alltime&address=%s", tokenAddress) - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err - } - req.Header.Set("x-api-key", c.token) - req.Header.Set("x-chain", "solana") - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - errMsg := attemptExtractErrorMessage(resp.Body) - return nil, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, errMsg) - } - - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var result struct { - Data []TokenAllTimeStats `json:"data"` - } - if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&result); err != nil { - return nil, err - } - - // Single all time stats endpoint returns an array with one item - if len(result.Data) == 0 { - return nil, fmt.Errorf("no data found for token address: %s", tokenAddress) - } - - c.tokenAllTimeStatsCache.Set(cacheKey, &result.Data[0]) - - return &result.Data[0], nil -} - -type TokenPriceData struct { - IsScaledUiToken bool `json:"isScaledUiToken"` - Value float64 `json:"value"` - UpdateUnixTime int64 `json:"updateUnixTime"` - UpdateHumanTime string `json:"updateHumanTime"` - PriceInNative float64 `json:"priceInNative"` - PriceChange24h float64 `json:"priceChange24h"` - Liquidity float64 `json:"liquidity"` -} - -// Map from mint to token price data -type TokenPriceMap map[string]TokenPriceData - -func (c *Client) GetPrices(ctx context.Context, mints []string) (TokenPriceMap, error) { - result := make(TokenPriceMap) - - mintsToFetch := make([]string, 0, len(mints)) - for _, mint := range mints { - if cachedPrices, ok := c.pricesCache.Get(mint); ok { - result[mint] = *cachedPrices - } else { - mintsToFetch = append(mintsToFetch, mint) - } - } - if len(mintsToFetch) == 0 { - return result, nil - } - - url := fmt.Sprintf("https://public-api.birdeye.so/defi/multi_price?list_address=%s", strings.Join(mintsToFetch, ",")) - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err - } - req.Header.Set("x-api-key", c.token) - req.Header.Set("x-chain", "solana") - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - errMsg := attemptExtractErrorMessage(resp.Body) - return nil, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, errMsg) - } - - var respBody struct { - Data TokenPriceMap `json:"data"` - } - if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil { - return nil, err - } - - for mint, priceData := range respBody.Data { - c.pricesCache.Set(mint, &priceData) - result[mint] = priceData - } - - return result, nil -} - -func (c *Client) GetPrice(ctx context.Context, mint string) (*TokenPriceData, error) { - if cachedPrices, ok := c.pricesCache.Get(mint); ok { - return cachedPrices, nil - } - - url := fmt.Sprintf("https://public-api.birdeye.so/defi/price?address=%s", mint) - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err - } - req.Header.Set("x-api-key", c.token) - req.Header.Set("x-chain", "solana") - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - errMsg := attemptExtractErrorMessage(resp.Body) - return nil, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, errMsg) - } - - var result struct { - Data TokenPriceData `json:"data"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, err - } - - c.pricesCache.Set(mint, &result.Data) - - return &result.Data, nil -} diff --git a/config/config.go b/config/config.go index b43c9f7e..ef2de655 100644 --- a/config/config.go +++ b/config/config.go @@ -38,7 +38,6 @@ type Config struct { CoreBlockStreamEnabled bool OpenAudioURLs []string ChainId string - BirdeyeToken string // HTTP(S) JSON-RPC endpoint for the Ethereum mainnet provider (e.g. an // Alchemy URL). Used by the eth-indexer for backfill `eth_getLogs` and // targeted `balanceOf` reads. If empty, the indexer is a no-op. @@ -114,7 +113,6 @@ var Cfg = Config{ AudiusdURL: os.Getenv("audiusdUrl"), CoreBlockStreamEnabled: os.Getenv("coreBlockStreamEnabled") == "true", OpenAudioURLs: []string{}, - BirdeyeToken: os.Getenv("birdeyeToken"), EthRpcUrl: os.Getenv("ethRpcUrl"), EthWsUrl: os.Getenv("ethWsUrl"), EthAudioContractAddress: os.Getenv("ethAudioContractAddress"), diff --git a/database/seed.go b/database/seed.go index b37704af..9c0c9b28 100644 --- a/database/seed.go +++ b/database/seed.go @@ -524,11 +524,6 @@ var ( "created_at": time.Now(), "updated_at": time.Now(), }, - "artist_coin_stats_onchain": { - "mint": nil, - "created_at": time.Now(), - "updated_at": time.Now(), - }, "artist_coin_price_history": { "mint": nil, "timestamp": nil, diff --git a/ddl/migrations/0235_drop_coin_stats_shadow.sql b/ddl/migrations/0235_drop_coin_stats_shadow.sql new file mode 100644 index 00000000..1fb7d5d0 --- /dev/null +++ b/ddl/migrations/0235_drop_coin_stats_shadow.sql @@ -0,0 +1,10 @@ +-- Retire the on-chain shadow of artist_coin_stats. CoinStatsOnchainJob now writes +-- artist_coin_stats directly (replacing the Birdeye-backed CoinStatsJob), so the +-- validation shadow table and its comparison view are no longer needed. +-- The comparison view depends on the table, so drop it first. +BEGIN; + +DROP VIEW IF EXISTS artist_coin_stats_comparison; +DROP TABLE IF EXISTS artist_coin_stats_onchain; + +COMMIT; diff --git a/ddl/views/artist_coin_prices.sql b/ddl/views/artist_coin_prices.sql index f8b5c0b4..3e3dd1e4 100644 --- a/ddl/views/artist_coin_prices.sql +++ b/ddl/views/artist_coin_prices.sql @@ -45,4 +45,4 @@ CREATE VIEW artist_coin_prices AS LEFT JOIN artist_coin_stats AS stats ON stats.mint = artist_coins.mint; -COMMENT ON VIEW artist_coin_prices IS 'View that provides artist coin prices using DAMM V2 pool if available, DBC pools if not and still applicable, artist_coin_pools.price_usd as fallback, and artist_coin_stats.price as final fallback (primarily for AUDIO and other tokens without pools). Makes use of the price of the quote token (AUDIO) from Birdeye if using a pool.'; \ No newline at end of file +COMMENT ON VIEW artist_coin_prices IS 'View that provides artist coin prices using DAMM V2 pool if available, DBC pools if not and still applicable, artist_coin_pools.price_usd as fallback, and artist_coin_stats.price as final fallback (primarily for AUDIO and other tokens without pools). Makes use of the price of the quote token (AUDIO) from artist_coin_stats (maintained on-chain by AudioPriceJob) if using a pool.'; \ No newline at end of file diff --git a/ddl/views/artist_coin_stats_comparison.sql b/ddl/views/artist_coin_stats_comparison.sql deleted file mode 100644 index 66578178..00000000 --- a/ddl/views/artist_coin_stats_comparison.sql +++ /dev/null @@ -1,42 +0,0 @@ --- Side-by-side comparison of the Birdeye-populated artist_coin_stats against the --- on-chain-derived artist_coin_stats_onchain, for validating CoinStatsOnchainJob --- before cutover. `*_pct_diff` = (onchain - birdeye) / birdeye * 100. --- A large diff (or NULL onchain where birdeye has a value) flags a missed pool, --- external market, or migration lag to investigate. -DROP VIEW IF EXISTS artist_coin_stats_comparison; -CREATE VIEW artist_coin_stats_comparison AS - SELECT - ac.mint, - ac.ticker, - - b.price AS birdeye_price, - o.price AS onchain_price, - ROUND((((o.price - b.price) / NULLIF(b.price, 0)) * 100)::numeric, 2) AS price_pct_diff, - - b.market_cap AS birdeye_market_cap, - o.market_cap AS onchain_market_cap, - ROUND((((o.market_cap - b.market_cap) / NULLIF(b.market_cap, 0)) * 100)::numeric, 2) AS market_cap_pct_diff, - - b.holder AS birdeye_holder, - o.holder AS onchain_holder, - (o.holder - b.holder) AS holder_diff, - - b.liquidity AS birdeye_liquidity, - o.liquidity AS onchain_liquidity, - ROUND((((o.liquidity - b.liquidity) / NULLIF(b.liquidity, 0)) * 100)::numeric, 2) AS liquidity_pct_diff, - - b.total_volume_usd AS birdeye_total_volume_usd, - o.total_volume_usd AS onchain_total_volume_usd, - ROUND((((o.total_volume_usd - b.total_volume_usd) / NULLIF(b.total_volume_usd, 0)) * 100)::numeric, 2) AS total_volume_usd_pct_diff, - - b.price_change_24h_percent AS birdeye_price_change_24h_percent, - o.price_change_24h_percent AS onchain_price_change_24h_percent, - - o.updated_at AS onchain_updated_at, - b.updated_at AS birdeye_updated_at - FROM artist_coins ac - LEFT JOIN artist_coin_stats b ON b.mint = ac.mint - LEFT JOIN artist_coin_stats_onchain o ON o.mint = ac.mint; - -COMMENT ON VIEW artist_coin_stats_comparison IS - 'Compares Birdeye artist_coin_stats vs on-chain artist_coin_stats_onchain per coin (values + % diff) to validate CoinStatsOnchainJob before cutover.'; diff --git a/jobs/coin_stats.go b/jobs/coin_stats.go deleted file mode 100644 index a4d59d61..00000000 --- a/jobs/coin_stats.go +++ /dev/null @@ -1,272 +0,0 @@ -package jobs - -import ( - "context" - "fmt" - "sync" - "time" - - "api.audius.co/birdeye" - "api.audius.co/config" - "api.audius.co/database" - "api.audius.co/logging" - "github.com/jackc/pgx/v5" - "go.uber.org/zap" - "golang.org/x/sync/errgroup" -) - -// Birdeye rate limit is 50 RPS -// We make two calls per token for this job, limiting to 40 per second to be safe. -const birdeyeDelay = 50 * time.Millisecond - -type CoinStatsJob struct { - birdeyeClient *birdeye.Client - pool database.DbPool - logger *zap.Logger - - mutex sync.Mutex - isRunning bool -} - -func NewCoinStatsJob(config config.Config, pool database.DbPool) *CoinStatsJob { - logger := logging.NewZapLogger(config).Named("CoinStatsJob") - birdeyeClient := birdeye.New(config.BirdeyeToken) - - return &CoinStatsJob{ - birdeyeClient: birdeyeClient, - logger: logger, - pool: pool, - } -} - -// ScheduleEvery runs the job every `duration` until the context is cancelled. -func (j *CoinStatsJob) ScheduleEvery(ctx context.Context, duration time.Duration) *CoinStatsJob { - go func() { - ticker := time.NewTicker(duration) - defer ticker.Stop() - for { - select { - case <-ticker.C: - j.logger.Info("Job started") - j.Run(ctx) - case <-ctx.Done(): - j.logger.Info("Job shutting down") - return - } - } - }() - return j -} - -// Run executes the job once -func (j *CoinStatsJob) Run(ctx context.Context) { - if err := j.run(ctx); err != nil { - j.logger.Error("Job run failed", zap.Error(err)) - } else { - j.logger.Info("Job completed successfully") - } -} - -// For each artist coin in the database, fetches the latest stats from Birdeye and -// updates the artist_coin_stats table. Ensures only one instance runs at a time. -func (j *CoinStatsJob) run(ctx context.Context) error { - j.mutex.Lock() - if j.isRunning { - j.mutex.Unlock() - return fmt.Errorf("job is already running") - } - j.isRunning = true - j.mutex.Unlock() - defer func() { - j.mutex.Lock() - j.isRunning = false - j.mutex.Unlock() - }() - - count, err := GetTokenCount(ctx, j.pool) - if err != nil { - return fmt.Errorf("error getting token count: %w", err) - } - - for offset := 0; offset < count; offset += tokenPageSize { - batch, err := GetTokenBatch(ctx, j.pool, tokenPageSize, offset) - if err != nil { - return fmt.Errorf("error getting token batch: %w", err) - } - - for _, coin := range batch { - err := j.updateStats(ctx, coin) - if err != nil { - j.logger.Error("error updating stats", zap.String("mint", coin.Mint), zap.Error(err)) - } - - // Prevent rate limiting on birdeye - time.Sleep(birdeyeDelay) - } - j.logger.Info("Processed batch", zap.Int("offset", offset), zap.Int("batch_size", len(batch))) - } - - return nil -} - -func (j *CoinStatsJob) updateStats(ctx context.Context, coin ArtistCoin) error { - var ( - overview *birdeye.TokenOverview - allTimeStats *birdeye.TokenAllTimeStats - ) - - g, gctx := errgroup.WithContext(ctx) - - g.Go(func() error { - ov, err := j.birdeyeClient.GetTokenOverview(gctx, coin.Mint, "24h") - if err != nil { - return fmt.Errorf("error getting token overview: %w", err) - } - overview = ov - return nil - }) - - g.Go(func() error { - at, err := j.birdeyeClient.GetTokenAllTimeStats(gctx, coin.Mint) - if err != nil { - return fmt.Errorf("error getting token all time stats: %w", err) - } - allTimeStats = at - return nil - }) - - if err := g.Wait(); err != nil { - return err - } - - err := j.insertArtistCoinStats(ctx, coin.Mint, overview, allTimeStats) - if err != nil { - return fmt.Errorf("error inserting artist coin stats: %w", err) - } - return nil -} - -func (j *CoinStatsJob) insertArtistCoinStats(ctx context.Context, mint string, overview *birdeye.TokenOverview, allTimeStats *birdeye.TokenAllTimeStats) error { - _, err := j.pool.Exec(ctx, ` - INSERT INTO artist_coin_stats ( - mint, market_cap, fdv, liquidity, last_trade_unix_time, last_trade_human_time, price, history_24h_price, - price_change_24h_percent, unique_wallet_24h, unique_wallet_history_24h, unique_wallet_24h_change_percent, - total_supply, circulating_supply, holder, trade_24h, trade_history_24h, trade_24h_change_percent, - sell_24h, sell_history_24h, sell_24h_change_percent, buy_24h, buy_history_24h, buy_24h_change_percent, - v_24h, v_24h_usd, v_history_24h, v_history_24h_usd, v_24h_change_percent, - v_buy_24h, v_buy_24h_usd, v_buy_history_24h, v_buy_history_24h_usd, v_buy_24h_change_percent, - v_sell_24h, v_sell_24h_usd, v_sell_history_24h, v_sell_history_24h_usd, v_sell_24h_change_percent, - number_markets, total_volume, total_volume_usd, volume_buy, volume_buy_usd, volume_sell, volume_sell_usd, buy, sell, total_trade, created_at, updated_at - ) VALUES ( - @mint, @market_cap, @fdv, @liquidity, @last_trade_unix_time, @last_trade_human_time, @price, @history_24h_price, - @price_change_24h_percent, @unique_wallet_24h, @unique_wallet_history_24h, @unique_wallet_24h_change_percent, - @total_supply, @circulating_supply, @holder, @trade_24h, @trade_history_24h, @trade_24h_change_percent, - @sell_24h, @sell_history_24h, @sell_24h_change_percent, @buy_24h, @buy_history_24h, @buy_24h_change_percent, - @v_24h, @v_24h_usd, @v_history_24h, @v_history_24h_usd, @v_24h_change_percent, - @v_buy_24h, @v_buy_24h_usd, @v_buy_history_24h, @v_buy_history_24h_usd, @v_buy_24h_change_percent, - @v_sell_24h, @v_sell_24h_usd, @v_sell_history_24h, @v_sell_history_24h_usd, @v_sell_24h_change_percent, - @number_markets, @total_volume, @total_volume_usd, @volume_buy, @volume_buy_usd, @volume_sell, @volume_sell_usd, @buy, @sell, @total_trade, NOW(), NOW() - ) - ON CONFLICT (mint) DO UPDATE SET - market_cap = EXCLUDED.market_cap, - fdv = EXCLUDED.fdv, - liquidity = EXCLUDED.liquidity, - last_trade_unix_time = EXCLUDED.last_trade_unix_time, - last_trade_human_time = EXCLUDED.last_trade_human_time, - price = EXCLUDED.price, - history_24h_price = EXCLUDED.history_24h_price, - price_change_24h_percent = EXCLUDED.price_change_24h_percent, - unique_wallet_24h = EXCLUDED.unique_wallet_24h, - unique_wallet_history_24h = EXCLUDED.unique_wallet_history_24h, - unique_wallet_24h_change_percent = EXCLUDED.unique_wallet_24h_change_percent, - total_supply = EXCLUDED.total_supply, - circulating_supply = EXCLUDED.circulating_supply, - holder = EXCLUDED.holder, - trade_24h = EXCLUDED.trade_24h, - trade_history_24h = EXCLUDED.trade_history_24h, - trade_24h_change_percent = EXCLUDED.trade_24h_change_percent, - sell_24h = EXCLUDED.sell_24h, - sell_history_24h = EXCLUDED.sell_history_24h, - sell_24h_change_percent = EXCLUDED.sell_24h_change_percent, - buy_24h = EXCLUDED.buy_24h, - buy_history_24h = EXCLUDED.buy_history_24h, - buy_24h_change_percent = EXCLUDED.buy_24h_change_percent, - v_24h = EXCLUDED.v_24h, - v_24h_usd = EXCLUDED.v_24h_usd, - v_history_24h = EXCLUDED.v_history_24h, - v_history_24h_usd = EXCLUDED.v_history_24h_usd, - v_24h_change_percent = EXCLUDED.v_24h_change_percent, - v_buy_24h = EXCLUDED.v_buy_24h, - v_buy_24h_usd = EXCLUDED.v_buy_24h_usd, - v_buy_history_24h = EXCLUDED.v_buy_history_24h, - v_buy_history_24h_usd = EXCLUDED.v_buy_history_24h_usd, - v_buy_24h_change_percent = EXCLUDED.v_buy_24h_change_percent, - v_sell_24h = EXCLUDED.v_sell_24h, - v_sell_24h_usd = EXCLUDED.v_sell_24h_usd, - v_sell_history_24h = EXCLUDED.v_sell_history_24h, - v_sell_history_24h_usd = EXCLUDED.v_sell_history_24h_usd, - v_sell_24h_change_percent = EXCLUDED.v_sell_24h_change_percent, - number_markets = EXCLUDED.number_markets, - total_volume = EXCLUDED.total_volume, - total_volume_usd = EXCLUDED.total_volume_usd, - volume_buy = EXCLUDED.volume_buy, - volume_buy_usd = EXCLUDED.volume_buy_usd, - volume_sell = EXCLUDED.volume_sell, - volume_sell_usd = EXCLUDED.volume_sell_usd, - buy = EXCLUDED.buy, - sell = EXCLUDED.sell, - total_trade = EXCLUDED.total_trade, - updated_at = NOW(); - `, pgx.NamedArgs{ - "mint": mint, - "market_cap": overview.MarketCap, - "fdv": overview.FDV, - "liquidity": overview.Liquidity, - "last_trade_unix_time": overview.LastTradeUnixTime, - "last_trade_human_time": overview.LastTradeHumanTime, - "price": overview.Price, - "history_24h_price": overview.History24hPrice, - "price_change_24h_percent": overview.PriceChange24hPercent, - "unique_wallet_24h": overview.UniqueWallet24h, - "unique_wallet_history_24h": overview.UniqueWalletHistory24h, - "unique_wallet_24h_change_percent": overview.UniqueWallet24hChangePercent, - "total_supply": overview.TotalSupply, - "circulating_supply": overview.CirculatingSupply, - "holder": overview.Holder, - "trade_24h": overview.Trade24h, - "trade_history_24h": overview.TradeHistory24h, - "trade_24h_change_percent": overview.Trade24hChangePercent, - "sell_24h": overview.Sell24h, - "sell_history_24h": overview.SellHistory24h, - "sell_24h_change_percent": overview.Sell24hChangePercent, - "buy_24h": overview.Buy24h, - "buy_history_24h": overview.BuyHistory24h, - "buy_24h_change_percent": overview.Buy24hChangePercent, - "v_24h": overview.V24h, - "v_24h_usd": overview.V24hUSD, - "v_history_24h": overview.VHistory24h, - "v_history_24h_usd": overview.VHistory24hUSD, - "v_24h_change_percent": overview.V24hChangePercent, - "v_buy_24h": overview.VBuy24h, - "v_buy_24h_usd": overview.VBuy24hUSD, - "v_buy_history_24h": overview.VBuyHistory24h, - "v_buy_history_24h_usd": overview.VBuyHistory24hUSD, - "v_buy_24h_change_percent": overview.VBuy24hChangePercent, - "v_sell_24h": overview.VSell24h, - "v_sell_24h_usd": overview.VSell24hUSD, - "v_sell_history_24h": overview.VSellHistory24h, - "v_sell_history_24h_usd": overview.VSellHistory24hUSD, - "v_sell_24h_change_percent": overview.VSell24hChangePercent, - "number_markets": overview.NumberMarkets, - "total_volume": allTimeStats.TotalVolume, - "total_volume_usd": allTimeStats.TotalVolumeUSD, - "volume_buy": allTimeStats.VolumeBuy, - "volume_buy_usd": allTimeStats.VolumeBuyUSD, - "volume_sell": allTimeStats.VolumeSell, - "volume_sell_usd": allTimeStats.VolumeSellUSD, - "buy": allTimeStats.Buy, - "sell": allTimeStats.Sell, - "total_trade": allTimeStats.TotalTrade, - }) - return err -} diff --git a/jobs/coin_stats_onchain.go b/jobs/coin_stats_onchain.go index 3deba88e..76ac68b2 100644 --- a/jobs/coin_stats_onchain.go +++ b/jobs/coin_stats_onchain.go @@ -31,9 +31,8 @@ type TokenSupplyFetcher interface { // CoinStatsOnchainJob derives artist-coin market stats from on-chain data // (Meteora DBC/DAMM v2 pools, token-account balances, mint supply) and writes -// them to the artist_coin_stats_onchain shadow table. It is the on-chain -// replacement for the Birdeye-backed CoinStatsJob; both run side by side until -// the on-chain values are validated (see artist_coin_stats_comparison). +// them to artist_coin_stats. It is the on-chain replacement for the former +// Birdeye-backed CoinStatsJob. type CoinStatsOnchainJob struct { pool database.DbPool rpcClient TokenSupplyFetcher @@ -409,7 +408,7 @@ func (j *CoinStatsOnchainJob) upsertStats( priceChange *float64, ) error { sql := ` - INSERT INTO artist_coin_stats_onchain ( + INSERT INTO artist_coin_stats ( mint, price, market_cap, liquidity, holder, total_supply, history_24h_price, price_change_24h_percent, created_at, updated_at @@ -419,14 +418,15 @@ func (j *CoinStatsOnchainJob) upsertStats( NOW(), NOW() ) ON CONFLICT (mint) DO UPDATE SET - -- Never overwrite a maintained price with NULL (e.g. AUDIO / pool-less coins). - price = COALESCE(EXCLUDED.price, artist_coin_stats_onchain.price), - market_cap = COALESCE(EXCLUDED.market_cap, artist_coin_stats_onchain.market_cap), + -- Never overwrite a maintained price with NULL. AUDIO's price is set by + -- AudioPriceJob (from the AUDIO/USDC pool); pool-less coins keep their last price. + price = COALESCE(EXCLUDED.price, artist_coin_stats.price), + market_cap = COALESCE(EXCLUDED.market_cap, artist_coin_stats.market_cap), liquidity = EXCLUDED.liquidity, holder = EXCLUDED.holder, - total_supply = COALESCE(EXCLUDED.total_supply, artist_coin_stats_onchain.total_supply), - history_24h_price = COALESCE(EXCLUDED.history_24h_price, artist_coin_stats_onchain.history_24h_price), - price_change_24h_percent = COALESCE(EXCLUDED.price_change_24h_percent, artist_coin_stats_onchain.price_change_24h_percent), + total_supply = COALESCE(EXCLUDED.total_supply, artist_coin_stats.total_supply), + history_24h_price = COALESCE(EXCLUDED.history_24h_price, artist_coin_stats.history_24h_price), + price_change_24h_percent = COALESCE(EXCLUDED.price_change_24h_percent, artist_coin_stats.price_change_24h_percent), updated_at = NOW() ` _, err := j.pool.Exec(ctx, sql, pgx.NamedArgs{ diff --git a/jobs/coin_stats_onchain_test.go b/jobs/coin_stats_onchain_test.go index 75277761..a512ae01 100644 --- a/jobs/coin_stats_onchain_test.go +++ b/jobs/coin_stats_onchain_test.go @@ -61,7 +61,8 @@ func TestCoinStatsOnchainJob(t *testing.T) { {"ticker": "COIN1", "decimals": 6, "user_id": 1, "mint": coinMint}, {"ticker": "AUDIO", "decimals": 8, "user_id": 1, "mint": audioMint}, }, - // AUDIO USD anchor (normally maintained by AudioPriceJob). + // AUDIO USD anchor (normally maintained by AudioPriceJob). Also the + // pre-existing row used to verify the job never clobbers AUDIO's price. "artist_coin_stats": { {"mint": audioMint, "price": 2.0}, }, @@ -85,10 +86,6 @@ func TestCoinStatsOnchainJob(t *testing.T) { {"account": "dbcpool1", "base_mint": coinMint, "quote_vault": quoteVault, "base_reserve": 1_000_000_000, "quote_reserve": 50_000_000_000, "is_migrated": 0}, }, - // Pre-existing on-chain AUDIO row to verify its price is not clobbered. - "artist_coin_stats_onchain": { - {"mint": audioMint, "price": 2.0}, - }, // A ~25h-old price snapshot so the 24h change is computable: (4.0-2.0)/2.0*100 = 100%. "artist_coin_price_history": { {"mint": coinMint, "timestamp": snapshot24hAgo, "price": 2.0}, @@ -118,7 +115,7 @@ func TestCoinStatsOnchainJob(t *testing.T) { err := pool.QueryRow(ctx, ` SELECT price, holder, liquidity, total_supply, market_cap, history_24h_price, price_change_24h_percent - FROM artist_coin_stats_onchain WHERE mint = $1`, coinMint). + FROM artist_coin_stats WHERE mint = $1`, coinMint). Scan(&price, &holder, &liquidity, &totalSupply, &marketCap, &history24h, &priceChange24) require.NoError(t, err) @@ -134,7 +131,7 @@ func TestCoinStatsOnchainJob(t *testing.T) { // AUDIO's maintained price must not be clobbered (on-chain price is NULL for it). var audioPrice float64 err = pool.QueryRow(ctx, - `SELECT price FROM artist_coin_stats_onchain WHERE mint = $1`, audioMint).Scan(&audioPrice) + `SELECT price FROM artist_coin_stats WHERE mint = $1`, audioMint).Scan(&audioPrice) require.NoError(t, err) assert.InDelta(t, 2.0, audioPrice, 1e-9, "AUDIO price preserved") } diff --git a/solana/indexer/solana_indexer.go b/solana/indexer/solana_indexer.go index 4df0cf56..1d0538d2 100644 --- a/solana/indexer/solana_indexer.go +++ b/solana/indexer/solana_indexer.go @@ -118,14 +118,8 @@ func New(config config.Config) *SolanaIndexer { func (s *SolanaIndexer) Start(ctx context.Context) error { go s.ScheduleProcessRetryQueue(ctx, s.config.SolanaIndexerRetryInterval) - statsJob := jobs.NewCoinStatsJob(s.config, s.pool) - statsCtx := context.WithoutCancel(ctx) - statsJob.ScheduleEvery(statsCtx, 15*time.Minute) - go statsJob.Run(statsCtx) - - // On-chain replacement for CoinStatsJob, running in shadow: it writes - // artist_coin_stats_onchain so its values can be validated against Birdeye - // (see artist_coin_stats_comparison) before cutover. + // Derives artist_coin_stats from on-chain data (Meteora DBC/DAMM v2 pools, + // token-account balances, mint supply). Replaced the Birdeye-backed CoinStatsJob. statsOnchainJob := jobs.NewCoinStatsOnchainJob(s.config, s.pool) statsOnchainCtx := context.WithoutCancel(ctx) statsOnchainJob.ScheduleEvery(statsOnchainCtx, 15*time.Minute) diff --git a/sql/01_schema.sql b/sql/01_schema.sql index 0b312200..b946a522 100644 --- a/sql/01_schema.sql +++ b/sql/01_schema.sql @@ -7628,111 +7628,7 @@ CREATE VIEW public.artist_coin_prices AS -- Name: VIEW artist_coin_prices; Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON VIEW public.artist_coin_prices IS 'View that provides artist coin prices using DAMM V2 pool if available, DBC pools if not and still applicable, artist_coin_pools.price_usd as fallback, and artist_coin_stats.price as final fallback (primarily for AUDIO and other tokens without pools). Makes use of the price of the quote token (AUDIO) from Birdeye if using a pool.'; - - --- --- Name: artist_coin_stats_onchain; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.artist_coin_stats_onchain ( - mint text NOT NULL, - market_cap double precision, - fdv double precision, - liquidity double precision, - last_trade_unix_time bigint, - last_trade_human_time text, - price double precision, - history_24h_price double precision, - price_change_24h_percent double precision, - unique_wallet_24h integer, - unique_wallet_history_24h integer, - unique_wallet_24h_change_percent double precision, - total_supply double precision, - circulating_supply double precision, - holder integer, - trade_24h integer, - trade_history_24h integer, - trade_24h_change_percent double precision, - sell_24h integer, - sell_history_24h integer, - sell_24h_change_percent double precision, - buy_24h integer, - buy_history_24h integer, - buy_24h_change_percent double precision, - v_24h double precision, - v_24h_usd double precision, - v_history_24h double precision, - v_history_24h_usd double precision, - v_24h_change_percent double precision, - v_buy_24h double precision, - v_buy_24h_usd double precision, - v_buy_history_24h double precision, - v_buy_history_24h_usd double precision, - v_buy_24h_change_percent double precision, - v_sell_24h double precision, - v_sell_24h_usd double precision, - v_sell_history_24h double precision, - v_sell_history_24h_usd double precision, - v_sell_24h_change_percent double precision, - number_markets integer, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - total_volume double precision, - total_volume_usd double precision, - volume_buy double precision, - volume_buy_usd double precision, - volume_sell double precision, - volume_sell_usd double precision, - buy integer, - sell integer, - total_trade integer -); - - --- --- Name: TABLE artist_coin_stats_onchain; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON TABLE public.artist_coin_stats_onchain IS 'On-chain-derived shadow of artist_coin_stats, written by CoinStatsOnchainJob. Used to validate against the Birdeye-populated artist_coin_stats before replacing it.'; - - --- --- Name: artist_coin_stats_comparison; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.artist_coin_stats_comparison AS - SELECT ac.mint, - ac.ticker, - b.price AS birdeye_price, - o.price AS onchain_price, - round(((((o.price - b.price) / NULLIF(b.price, (0)::double precision)) * (100)::double precision))::numeric, 2) AS price_pct_diff, - b.market_cap AS birdeye_market_cap, - o.market_cap AS onchain_market_cap, - round(((((o.market_cap - b.market_cap) / NULLIF(b.market_cap, (0)::double precision)) * (100)::double precision))::numeric, 2) AS market_cap_pct_diff, - b.holder AS birdeye_holder, - o.holder AS onchain_holder, - (o.holder - b.holder) AS holder_diff, - b.liquidity AS birdeye_liquidity, - o.liquidity AS onchain_liquidity, - round(((((o.liquidity - b.liquidity) / NULLIF(b.liquidity, (0)::double precision)) * (100)::double precision))::numeric, 2) AS liquidity_pct_diff, - b.total_volume_usd AS birdeye_total_volume_usd, - o.total_volume_usd AS onchain_total_volume_usd, - round(((((o.total_volume_usd - b.total_volume_usd) / NULLIF(b.total_volume_usd, (0)::double precision)) * (100)::double precision))::numeric, 2) AS total_volume_usd_pct_diff, - b.price_change_24h_percent AS birdeye_price_change_24h_percent, - o.price_change_24h_percent AS onchain_price_change_24h_percent, - o.updated_at AS onchain_updated_at, - b.updated_at AS birdeye_updated_at - FROM ((public.artist_coins ac - LEFT JOIN public.artist_coin_stats b ON ((b.mint = (ac.mint)::text))) - LEFT JOIN public.artist_coin_stats_onchain o ON ((o.mint = (ac.mint)::text))); - - --- --- Name: VIEW artist_coin_stats_comparison; Type: COMMENT; Schema: public; Owner: - --- - -COMMENT ON VIEW public.artist_coin_stats_comparison IS 'Compares Birdeye artist_coin_stats vs on-chain artist_coin_stats_onchain per coin (values + % diff) to validate CoinStatsOnchainJob before cutover.'; +COMMENT ON VIEW public.artist_coin_prices IS 'View that provides artist coin prices using DAMM V2 pool if available, DBC pools if not and still applicable, artist_coin_pools.price_usd as fallback, and artist_coin_stats.price as final fallback (primarily for AUDIO and other tokens without pools). Makes use of the price of the quote token (AUDIO) from artist_coin_stats (maintained on-chain by AudioPriceJob) if using a pool.'; -- @@ -11351,14 +11247,6 @@ ALTER TABLE ONLY public.artist_coin_price_history ADD CONSTRAINT artist_coin_price_history_pkey PRIMARY KEY (mint, "timestamp"); --- --- Name: artist_coin_stats_onchain artist_coin_stats_onchain_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.artist_coin_stats_onchain - ADD CONSTRAINT artist_coin_stats_onchain_pkey PRIMARY KEY (mint); - - -- -- Name: artist_coin_stats artist_coin_stats_pkey; Type: CONSTRAINT; Schema: public; Owner: - --