Background
This is two phases, as scoped by the request: (1) indexer handlers that keep analytics data current in Postgres, and (2) admin-facing endpoints that read it. MerchantAnalytics and TokenAnalytics already exist in the schema (per-token live counters) but nothing writes to them yet applyInvoicePayment updates Invoice/Transaction only. That's a gap this issue closes, not a new problem it introduces.
Events split into three buckets for dashboard purposes:
| Bucket |
Events |
Feeds |
| Volume-moving |
InvoicePaidEvent, SubscriptionChargedEvent, TicketPurchasedEvent, TicketResoldEvent (royalty only — no platform fee on resale) |
MerchantAnalytics, TokenAnalytics, PlatformDailyStats |
| Growth-counting |
MerchantRegisteredEvent, InvoiceCreatedEvent, SubscriptionPlanCreatedEvent, SubscribedEvent, EventCreatedEvent |
PlatformDailyStats "new X today" counters (and are also directly queryable via count() at request time for point-in-time totals — no new schema needed for that) |
| Refund adjustments |
InvoiceRefundedEvent, InvoicePartiallyRefundedEvent |
Tracked as separate refund totals, not netted against volume — verify against the contract before assuming refunds reduce total_volume on-chain; don't guess |
Proposed Steps
Phase 1 — Indexer
- Retrofit
applyInvoicePayment (invoice.services.ts) to also, inside the same $transaction: upsert MerchantAnalytics (by [merchantId, token], incrementing totalVolume/totalFees/transactionCount) and TokenAnalytics (by token, same increments, plus uniqueMerchants — only bump this the first time a given merchant appears for that token), and increment today's PlatformDailyStats row (totalVolume, totalFees, transactionCount).
- New handler for
SubscriptionChargedEvent → same shape of writes as above, keyed off subscription_id/plan_id (resolve merchant via the Subscription's merchantId, not the event ). This handler is also exactly what "subscription payments" endpoint depends on — implementing it here satisfies both.
- New handler for
TicketPurchasedEvent and TicketResoldEvent → same shape of writes (out of scope if event ticketing isn't implemented yet elsewhere in the backend; if Event/Ticket models don't exist yet, log-and-skip with a clear comment rather than blocking this issue on building ticketing from scratch).
- New handlers for the growth-counting events → increment the relevant
PlatformDailyStats "new X" counter for today's row.
PlatformDailyStats rows are upserted by UTC calendar day (date truncated to midnight UTC) — upsert on each event, not a scheduled batch job, consistent with how MerchantAnalytics/TokenAnalytics already work.
Phase 2 — Endpoints (mounted at src/routes/admin/analytics.routes.ts, protected by authenticateAdmin)
GET /admin/analytics/summary — protocol-wide current totals: sum of TokenAnalytics across tokens, plus live count()s (Merchant, Invoice grouped by status, Subscription grouped by status).
GET /admin/analytics/timeseries?from=&to= — reads PlatformDailyStats rows in the given date range, ordered by date. No granularity param in this issue — daily only; weekly/monthly rollups are a follow-up if needed.
GET /admin/analytics/tokens — top tokens by volume, reading straight from TokenAnalytics ordered by totalVolume desc (mirrors the contract's get_top_tokens_by_volume, served from Postgres instead of a live contract call — much cheaper for a dashboard that gets refreshed often).
Schema Changes
PlatformDailyStats (new model)
id String (uuid, PK)
date DateTime (unique, truncated to UTC midnight)
totalVolume BigInt (default 0)
totalFees BigInt (default 0)
transactionCount BigInt (default 0)
newInvoices Int (default 0)
newMerchants Int (default 0)
newSubscriptions Int (default 0)
newTickets Int (default 0)
updatedAt DateTime (@updatedAt)
Per-token daily breakdown (e.g. "USDC volume trend over time" as opposed to protocol-wide) is explicitly out of scope here — TokenAnalytics already gives current per-token totals; a PlatformDailyTokenStats table is a reasonable future enhancement, not part of this issue.
Acceptance Criteria
Background
This is two phases, as scoped by the request: (1) indexer handlers that keep analytics data current in Postgres, and (2) admin-facing endpoints that read it.
MerchantAnalyticsandTokenAnalyticsalready exist in the schema (per-token live counters) but nothing writes to them yetapplyInvoicePaymentupdatesInvoice/Transactiononly. That's a gap this issue closes, not a new problem it introduces.Events split into three buckets for dashboard purposes:
InvoicePaidEvent,SubscriptionChargedEvent,TicketPurchasedEvent,TicketResoldEvent(royalty only — no platform fee on resale)MerchantAnalytics,TokenAnalytics,PlatformDailyStatsMerchantRegisteredEvent,InvoiceCreatedEvent,SubscriptionPlanCreatedEvent,SubscribedEvent,EventCreatedEventPlatformDailyStats"new X today" counters (and are also directly queryable viacount()at request time for point-in-time totals — no new schema needed for that)InvoiceRefundedEvent,InvoicePartiallyRefundedEventtotal_volumeon-chain; don't guessProposed Steps
Phase 1 — Indexer
applyInvoicePayment(invoice.services.ts) to also, inside the same$transaction:upsertMerchantAnalytics(by[merchantId, token], incrementingtotalVolume/totalFees/transactionCount) andTokenAnalytics(bytoken, same increments, plusuniqueMerchants— only bump this the first time a given merchant appears for that token), and increment today'sPlatformDailyStatsrow (totalVolume,totalFees,transactionCount).SubscriptionChargedEvent→ same shape of writes as above, keyed offsubscription_id/plan_id(resolve merchant via theSubscription'smerchantId, not the event ). This handler is also exactly what "subscription payments" endpoint depends on — implementing it here satisfies both.TicketPurchasedEventandTicketResoldEvent→ same shape of writes (out of scope if event ticketing isn't implemented yet elsewhere in the backend; ifEvent/Ticketmodels don't exist yet, log-and-skip with a clear comment rather than blocking this issue on building ticketing from scratch).PlatformDailyStats"new X" counter for today's row.PlatformDailyStatsrows are upserted by UTC calendar day (datetruncated to midnight UTC) —upserton each event, not a scheduled batch job, consistent with howMerchantAnalytics/TokenAnalyticsalready work.Phase 2 — Endpoints (mounted at
src/routes/admin/analytics.routes.ts, protected byauthenticateAdmin)GET /admin/analytics/summary— protocol-wide current totals: sum ofTokenAnalyticsacross tokens, plus livecount()s (Merchant,Invoicegrouped by status,Subscriptiongrouped by status).GET /admin/analytics/timeseries?from=&to=— readsPlatformDailyStatsrows in the given date range, ordered by date. Nogranularityparam in this issue — daily only; weekly/monthly rollups are a follow-up if needed.GET /admin/analytics/tokens— top tokens by volume, reading straight fromTokenAnalyticsordered bytotalVolume desc(mirrors the contract'sget_top_tokens_by_volume, served from Postgres instead of a live contract call — much cheaper for a dashboard that gets refreshed often).Schema Changes
PlatformDailyStats (new model)
Per-token daily breakdown (e.g. "USDC volume trend over time" as opposed to protocol-wide) is explicitly out of scope here —
TokenAnalyticsalready gives current per-token totals; aPlatformDailyTokenStatstable is a reasonable future enhancement, not part of this issue.Acceptance Criteria
PlatformDailyStatsmodel added;prisma migrate devruns cleanlyInvoicePaidEventhandling now also updatesMerchantAnalytics,TokenAnalytics, and today'sPlatformDailyStatsrow, in the same transaction as the existingInvoice/TransactionwritesSubscriptionChargedEventhas a working handler that produces the same category of writesGET /admin/analytics/summaryreturns protocol totals matching what's actually inTokenAnalytics/MerchantAnalytics/live countsGET /admin/analytics/timeseriesreturnsPlatformDailyStatsrows for the requested range, empty array (not an error) for a range with no activityGET /admin/analytics/tokensis ordered by volume descendingauthenticateAdmin(norequireSuperAdminneeded — read-only)