From 56dbdc711d6d32ed5b974e8a44cce0d520bc4edd Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Mon, 5 Jan 2026 12:12:27 -0500 Subject: [PATCH 1/3] cache getAcceptsAddresses --- apps/scan/src/services/db/resources/accepts.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/scan/src/services/db/resources/accepts.ts b/apps/scan/src/services/db/resources/accepts.ts index e595ad1be..7c7dbfa2a 100644 --- a/apps/scan/src/services/db/resources/accepts.ts +++ b/apps/scan/src/services/db/resources/accepts.ts @@ -1,6 +1,7 @@ import { scanDb } from '@x402scan/scan-db'; import { mixedAddressSchema } from '@/lib/schemas'; +import { createCachedQuery, createStandardCacheKey } from '@/lib/cache'; import type { Chain } from '@/types/chain'; import type { AcceptsNetwork, ResourceOrigin } from '@x402scan/scan-db'; @@ -10,7 +11,7 @@ interface GetAcceptsAddressesInput { tags?: string[]; } -export const getAcceptsAddresses = async (input: GetAcceptsAddressesInput) => { +const getAcceptsAddressesUncached = async (input: GetAcceptsAddressesInput) => { const { chain, tags } = input; const accepts = await scanDb.accepts.findMany({ include: { @@ -58,3 +59,16 @@ export const getAcceptsAddresses = async (input: GetAcceptsAddressesInput) => { {} as Record> ); }; + +/** + * Get accepts addresses grouped by origin (cached) + * This is used to determine which addresses are "bazaar" sellers + */ +export const getAcceptsAddresses = createCachedQuery({ + queryFn: getAcceptsAddressesUncached, + cacheKeyPrefix: 'accepts:addresses', + createCacheKey: input => + createStandardCacheKey(input as Record), + dateFields: [], + tags: ['accepts', 'bazaar'], +}); From e180046956874dd5016c50351d2a76cc6438ff84 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Mon, 5 Jan 2026 12:24:07 -0500 Subject: [PATCH 2/3] build recipient filter first --- .../src/services/transfers/sellers/stats/bucketed.ts | 8 ++++++++ .../scan/src/services/transfers/sellers/stats/overall.ts | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/apps/scan/src/services/transfers/sellers/stats/bucketed.ts b/apps/scan/src/services/transfers/sellers/stats/bucketed.ts index 041aa5573..56cdec9ff 100644 --- a/apps/scan/src/services/transfers/sellers/stats/bucketed.ts +++ b/apps/scan/src/services/transfers/sellers/stats/bucketed.ts @@ -36,6 +36,13 @@ const getBucketedSellerStatisticsUncached = async ( Math.floor(timeRangeMs / numBuckets / 1000) ); + // Build recipient filter for the seller_first_transactions CTE + // This is critical for performance when querying bazaar stats with 1000+ addresses + const recipientFilter = + input.recipients?.include && input.recipients.include.length > 0 + ? Prisma.sql`WHERE recipient = ANY(${input.recipients.include})` + : Prisma.empty; + const sql = Prisma.sql` WITH all_buckets AS ( SELECT generate_series( @@ -51,6 +58,7 @@ const getBucketedSellerStatisticsUncached = async ( recipient, MIN(block_timestamp) AS first_transaction_date FROM "TransferEvent" + ${recipientFilter} GROUP BY recipient ), bucket_stats AS ( diff --git a/apps/scan/src/services/transfers/sellers/stats/overall.ts b/apps/scan/src/services/transfers/sellers/stats/overall.ts index c205c851d..2b7bb460b 100644 --- a/apps/scan/src/services/transfers/sellers/stats/overall.ts +++ b/apps/scan/src/services/transfers/sellers/stats/overall.ts @@ -13,12 +13,21 @@ const getOverallSellerStatisticsUncached = async ( input: z.infer ) => { const { startDate, endDate } = getTimeRangeFromTimeframe(input.timeframe); + + // Build recipient filter for the seller_first_transactions CTE + // This is critical for performance when querying bazaar stats with 1000+ addresses + const recipientFilter = + input.recipients?.include && input.recipients.include.length > 0 + ? Prisma.sql`WHERE recipient = ANY(${input.recipients.include})` + : Prisma.empty; + const sql = Prisma.sql` WITH seller_first_transactions AS ( SELECT recipient, MIN(block_timestamp) AS first_transaction_date FROM "TransferEvent" + ${recipientFilter} GROUP BY recipient ), filtered_transfers AS ( From 2eb749c58b8fd783ace665ed48fca1ad52ac908c Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Mon, 5 Jan 2026 13:17:42 -0500 Subject: [PATCH 3/3] use MV for first txs --- .../transfers/sellers/stats/bucketed.ts | 21 +++------------- .../transfers/sellers/stats/overall.ts | 25 +++++-------------- 2 files changed, 10 insertions(+), 36 deletions(-) diff --git a/apps/scan/src/services/transfers/sellers/stats/bucketed.ts b/apps/scan/src/services/transfers/sellers/stats/bucketed.ts index 56cdec9ff..bfc0e0dce 100644 --- a/apps/scan/src/services/transfers/sellers/stats/bucketed.ts +++ b/apps/scan/src/services/transfers/sellers/stats/bucketed.ts @@ -36,13 +36,8 @@ const getBucketedSellerStatisticsUncached = async ( Math.floor(timeRangeMs / numBuckets / 1000) ); - // Build recipient filter for the seller_first_transactions CTE - // This is critical for performance when querying bazaar stats with 1000+ addresses - const recipientFilter = - input.recipients?.include && input.recipients.include.length > 0 - ? Prisma.sql`WHERE recipient = ANY(${input.recipients.include})` - : Prisma.empty; - + // Use the recipient_first_transaction materialized view for fast lookups + // This avoids scanning all hypertable chunks to compute MIN(block_timestamp) const sql = Prisma.sql` WITH all_buckets AS ( SELECT generate_series( @@ -53,14 +48,6 @@ const getBucketedSellerStatisticsUncached = async ( (${bucketSizeSeconds} || ' seconds')::interval ) AS bucket_start ), - seller_first_transactions AS ( - SELECT - recipient, - MIN(block_timestamp) AS first_transaction_date - FROM "TransferEvent" - ${recipientFilter} - GROUP BY recipient - ), bucket_stats AS ( SELECT to_timestamp( @@ -69,14 +56,14 @@ const getBucketedSellerStatisticsUncached = async ( COUNT(DISTINCT t.recipient)::int AS total_sellers, COUNT(DISTINCT CASE WHEN to_timestamp( - floor(extract(epoch from sft.first_transaction_date) / ${bucketSizeSeconds}) * ${bucketSizeSeconds} + floor(extract(epoch from rft.first_transaction_date) / ${bucketSizeSeconds}) * ${bucketSizeSeconds} ) = to_timestamp( floor(extract(epoch from t.block_timestamp) / ${bucketSizeSeconds}) * ${bucketSizeSeconds} ) THEN t.recipient END)::int AS new_sellers FROM "TransferEvent" t - LEFT JOIN seller_first_transactions sft ON t.recipient = sft.recipient + LEFT JOIN recipient_first_transaction rft ON t.recipient = rft.recipient ${transfersWhereClause(input)} GROUP BY bucket_start ) diff --git a/apps/scan/src/services/transfers/sellers/stats/overall.ts b/apps/scan/src/services/transfers/sellers/stats/overall.ts index 2b7bb460b..fe28fec36 100644 --- a/apps/scan/src/services/transfers/sellers/stats/overall.ts +++ b/apps/scan/src/services/transfers/sellers/stats/overall.ts @@ -14,23 +14,10 @@ const getOverallSellerStatisticsUncached = async ( ) => { const { startDate, endDate } = getTimeRangeFromTimeframe(input.timeframe); - // Build recipient filter for the seller_first_transactions CTE - // This is critical for performance when querying bazaar stats with 1000+ addresses - const recipientFilter = - input.recipients?.include && input.recipients.include.length > 0 - ? Prisma.sql`WHERE recipient = ANY(${input.recipients.include})` - : Prisma.empty; - + // Use the recipient_first_transaction materialized view for fast lookups + // This avoids scanning all hypertable chunks to compute MIN(block_timestamp) const sql = Prisma.sql` - WITH seller_first_transactions AS ( - SELECT - recipient, - MIN(block_timestamp) AS first_transaction_date - FROM "TransferEvent" - ${recipientFilter} - GROUP BY recipient - ), - filtered_transfers AS ( + WITH filtered_transfers AS ( SELECT DISTINCT t.recipient FROM "TransferEvent" t ${transfersWhereClause(input)} @@ -38,12 +25,12 @@ const getOverallSellerStatisticsUncached = async ( SELECT COUNT(DISTINCT ft.recipient)::int AS total_sellers, COUNT(DISTINCT CASE - WHEN sft.first_transaction_date >= ${startDate ?? Prisma.sql`'1970-01-01'::timestamp`} - AND sft.first_transaction_date <= ${endDate ?? Prisma.sql`NOW()`} + WHEN rft.first_transaction_date >= ${startDate ?? Prisma.sql`'1970-01-01'::timestamp`} + AND rft.first_transaction_date <= ${endDate ?? Prisma.sql`NOW()`} THEN ft.recipient END)::int AS new_sellers FROM filtered_transfers ft - LEFT JOIN seller_first_transactions sft ON ft.recipient = sft.recipient + LEFT JOIN recipient_first_transaction rft ON ft.recipient = rft.recipient `; const result = await queryRaw(