Loading...
Loading...
Most attribution tools stop at the checkout page. Proova had to answer a harder question: how do you attribute revenue when the entire transaction happens off-platform?
Field Note — Proova
---
Revenue attribution is a solved problem — until the transaction happens in a WhatsApp chat, a bank transfer, or a cash handoff at a pop-up event.
That's the problem Proova was built to solve. Influencer marketing in emerging markets doesn't follow the clean funnel of Western e-commerce. A creator posts a product on Instagram. A follower DMs them. They negotiate on WhatsApp. The buyer pays via bank transfer or cash. The product ships. At no point in this chain does a tracking pixel fire, a checkout page load, or a cookie get set.
Traditional attribution tools — Google Analytics, Facebook Pixel, even most first-party analytics — are invisible to this flow. They see the click, maybe the landing page, and then darkness. The revenue exists. The attribution doesn't.
This is the architectural story of how we built a system that bridges that gap.
---
The fundamental challenge with off-platform attribution is that you don't control the transaction surface. You can't inject JavaScript into a WhatsApp conversation. You can't read a bank transfer confirmation. You can't hook into a POS terminal you don't own.
What you can do is instrument the edges: the influencer's link, the confirmation message, the manual reconciliation step. Proova's approach is to make the influencer the attribution carrier — every influencer gets a unique tracking identity that follows the customer through whatever channel they choose.
---
We chose Next.js with server-side rendering for the dashboard and reporting surfaces. The reasoning was straightforward: dashboards are only useful if they load fast, and SEO doesn't matter for an internal analytics tool, but first paint does.
SSR gives us two things. First, the initial dashboard state is rendered on the server, so the user sees data immediately rather than a loading spinner while client-side JavaScript fetches. Second, it lets us do server-side data fetching against the Django API with internal network calls — no CORS, no token exposure to the browser, no JWT in localStorage.
The tradeoff is complexity. Server-side data fetching in Next.js App Router is powerful but finicky. You have to think about caching boundaries, revalidation strategies, and the boundary between what runs on the server and what hydrates on the client. We accepted this because the performance gain for dashboard users was substantial — time-to-first-meaningful-paint dropped from ~2.1s to ~680ms on average.
The backend is Django REST Framework with PostgreSQL. Django is a deliberate choice here, not a default. Revenue attribution involves financial data — amounts, commissions, payouts — and Django's ORM maturity, migration system, and admin interface are genuinely useful for financial models that evolve over time.
PostgreSQL was chosen for three reasons:
1. ACID compliance at the transaction level. When you record that an influencer drove ₦50,000 in revenue, that record has to be exactly right. No eventual consistency, no "maybe it'll sync later."
2. JSONB for flexible metadata. Attribution events carry wildly different metadata depending on the channel — WhatsApp messages have sender IDs, bank transfers have reference numbers, cash payments have location tags. JSONB lets us store this heterogeneity without schema migrations for every new channel.
3. Window functions for time-series analytics. Revenue attribution is fundamentally a time-series problem — "how much did this influencer drive this week versus last week?" PostgreSQL's window functions and date_trunc make these queries readable and performant.
Attribution isn't a synchronous operation. When a transaction is confirmed — whether via webhook from a payment processor, a manual upload of a bank statement, or a WhatsApp message parsed by NLP — several things need to happen:
1. The raw event is ingested and normalized.
2. The attribution engine matches the event to an influencer and a campaign.
3. The influencer's balance is updated.
4. Commission calculations run.
5. Real-time dashboards get invalidated.
6. Notifications fire.
Doing all of this synchronously in the request path would make the API unresponsive. We use Celery with Redis as the broker to handle this pipeline asynchronously. Redis also serves as a cache layer for frequently accessed attribution summaries — "total revenue this month" is a cache hit, not a SUM query, for 95% of requests.
This is the decision I'm most proud of from the Proova architecture. We chose a double-entry ledger over a simple balance-update model.
In a simple balance model, when an influencer earns a commission, you run:
UPDATE influencer_balances SET balance = balance + 5000 WHERE id = 123;This is fast, simple, and wrong for financial data. If that UPDATE runs twice — retry logic, race condition, bug — the balance is permanently wrong. There's no record of what happened, no way to reconstruct the correct state.
In a double-entry model, every commission creates two rows:
INSERT INTO ledger_entries (account_id, type, amount, running_balance, ...)
VALUES
(influencer_revenue_account, 'CREDIT', 5000, ...),
(platform_commission_account, 'DEBIT', 5000, ...);The running balance is computed, not stored as a mutable value. The ledger is append-only. A duplicate insert is visible as two rows with the same reference ID — detectable, reversible, auditable. The tradeoff is write complexity: every commission, refund, or adjustment requires careful transaction wrapping and idempotency keys. But the guarantee — that the financial record is always reconstructible and tamper-evident — is worth it.
This decision was made before we had any compliance requirements. We made it because we knew that once money moves through a system, "we'll add auditing later" is a promise that never gets kept.
Proova integrates with multiple payment processors — Paystack, Flutterwave, and manual bank reconciliation. Each has a different webhook format, retry strategy, and idempotency model.
We built a generic webhook receiver that normalizes every incoming event into an internal PaymentEvent schema before it hits the attribution engine. The receiver handles:
We chose Redis Streams over a dedicated message queue like RabbitMQ or Kafka. The reasoning was operational: Redis was already in the stack for caching and Celery. Adding a separate message broker would mean another service to monitor, another failure mode, another set of credentials to rotate.
Redis Streams gives us ordered, replayable event streams with consumer groups. The tradeoff is weaker delivery guarantees — Redis Streams doesn't have the same durability promises as Kafka. A Redis restart can lose unacknowledged messages. We mitigated this by keeping the stream as a notification channel, not the source of truth. The ledger is the source of truth. The stream just tells the dashboard to refresh.
---
The hardest technical problem in Proova is attributing a transaction that has no digital fingerprint. A customer sees an influencer's post, sends a WhatsApp message, and pays via bank transfer. The bank transfer has a reference number, but that reference number doesn't contain the influencer's ID.
Our solution is a multi-factor matching engine:
1. Link-level attribution: Every influencer gets a unique link (proova.app/r/abc123). If the customer clicks the link before making contact, we cookie them and associate the eventual transaction with that influencer. This catches ~30% of cases.
2. Code-level attribution: Influencers distribute unique discount codes. When a customer mentions the code in a WhatsApp conversation or includes it in a bank transfer reference, we match it. This catches ~45% of cases.
3. Manual reconciliation: For the remaining ~25%, the business owner manually uploads transaction records (bank statements, WhatsApp export logs) and our reconciliation engine suggests matches based on amount, time proximity, and fuzzy text matching on descriptions.
The reconciliation engine uses a weighted confidence score: amount match (40%), time proximity (30%), description similarity (20%), and channel pattern (10%). Matches above 85% confidence are auto-attributed. Matches between 60-85% are flagged for human review. Below 60% is unmatched, tracked separately.
The analytics pipeline processes thousands of attribution events per second during peak campaign periods. We needed sub-second latency for dashboard updates without melting the database.
Our approach is a Lambda architecture lite:
This gives us "real-time enough" for dashboard users while keeping the PostgreSQL load manageable. The nightly reconciliation job has caught edge cases the speed layer missed — timezone handling in date_trunc, off-by-one errors in window functions, duplicate events that slipped through idempotency checks.
---
Offline attribution requires creative solutions beyond traditional web analytics. The tools built for e-commerce checkout flows are useless for WhatsApp commerce. You have to design for the channel your customers actually use, not the channel you wish they used.
Financial data demands rigorous validation and audit trails. Every peso, naira, or dollar that moves through your system needs a paper trail. Not because regulators ask for it today, but because they will tomorrow, and because your own debugging depends on it.
Real-time systems require careful consideration of eventual consistency. The speed layer lies. It lies small, and it lies rarely, but it lies. You need a batch layer to tell you when and by how much. Trusting the speed layer without reconciliation is how you end up paying influencers the wrong amount.
---
Proova is live at [proova.app](https://proova.app)