A marketplace creates value only when demand and supply can find each other and complete a satisfactory transaction. Buyer growth without relevant supply can worsen the product; supplier growth without demand can reduce earnings and retention.
Keep orders, payments, inventory, identity verification, and balances in their authoritative services. Send immutable lifecycle facts and governed snapshots to GraphJSON for analysis.
Define every grain#
Typical entities:
| Entity | Examples |
|---|---|
| Demand actor | buyer, requester, rider, employer |
| Supply actor | seller, provider, driver, candidate |
| Supply unit | listing, available hour, inventory item |
| Demand unit | search, request, job, booking intent |
| Match | candidate pair or accepted offer |
| Transaction | order, booking, completed job |
| Monetary component | customer payment, supplier payout, fee, refund |
Do not use user_id for both sides without a role or marketplace-side field.
One person may act on both sides.
Instrument supply availability#
A listing-created event does not prove active supply. Model published, available, paused, exhausted, and removed states:
{
"event": "supply_availability_changed",
"supplier_id": "sup_42",
"listing_id": "lst_91",
"market_key": "designers:sf",
"previous_status": "paused",
"status": "available",
"capacity_units": 4,
"effective_at": "2026-07-25T16:00:00Z",
"reason": "supplier_resumed"
}
For inventory or capacity at a point in time, publish snapshots. Do not sum hourly availability snapshots as if they were unique listings.
Instrument demand and matching#
{
"event": "marketplace_request_created",
"request_id": "req_81",
"buyer_id": "buy_7",
"market_key": "designers:sf",
"requested_units": 1,
"constraints_version": 3
}
{
"event": "marketplace_match_accepted",
"match_id": "mat_61",
"request_id": "req_81",
"buyer_id": "buy_7",
"supplier_id": "sup_42",
"listing_id": "lst_91",
"match_rank": 2,
"seconds_to_match": 840,
"matching_version": 8
}
Preserve one request through candidate generation, offer, acceptance, expiry, and withdrawal. Do not count every candidate as a completed match.
Define liquidity by market#
Liquidity is the probability and speed with which relevant demand and supply complete a match. It is local to a market:
market = geography × category × time window × required constraints
Useful measures:
- request fill rate
- time to first qualified offer
- time to accepted match
- supply utilization
- unfulfilled demand
- available supply with no relevant impressions
- match-to-transaction conversion
Always show denominators and maturity. A request still open has not yet failed.
Query mature request fill rate#
Assume requests have a 24-hour matching window:
WITH requests AS (
SELECT
JSONExtractString(json, 'request_id') AS request_id,
argMin(JSONExtractString(json, 'market_key'), timestamp) AS market_key,
min(toDateTime(timestamp)) AS requested_at
FROM marketplace_events
WHERE JSONExtractString(json, 'event') = 'marketplace_request_created'
GROUP BY request_id
),
matches AS (
SELECT
JSONExtractString(json, 'request_id') AS request_id,
min(toDateTime(timestamp)) AS matched_at
FROM marketplace_events
WHERE JSONExtractString(json, 'event') = 'marketplace_match_accepted'
GROUP BY request_id
)
SELECT
requests.market_key,
count() AS mature_requests,
countIf(
matches.matched_at >= requests.requested_at
AND matches.matched_at < requests.requested_at + INTERVAL 24 HOUR
) AS filled_requests,
filled_requests / nullIf(mature_requests, 0) AS fill_rate
FROM requests
LEFT JOIN matches USING request_id
WHERE requests.requested_at < now() - INTERVAL 24 HOUR
GROUP BY requests.market_key
ORDER BY mature_requests DESC
Use a product-specific eligibility rule. Requests canceled by the buyer may be separate from marketplace failures.
Keep transaction and money facts separate#
{
"event": "marketplace_transaction_completed",
"transaction_id": "txn_51",
"request_id": "req_81",
"match_id": "mat_61",
"buyer_id": "buy_7",
"supplier_id": "sup_42",
"market_key": "designers:sf",
"gross_amount_minor": 12500,
"currency": "usd"
}
Payout and platform fee may settle later:
{
"event": "marketplace_transaction_settled",
"transaction_id": "txn_51",
"gross_amount_minor": 12500,
"supplier_payout_minor": 10000,
"platform_fee_minor": 2500,
"currency": "usd",
"settlement_status": "settled"
}
Define:
GMV = eligible gross transaction value
take rate = eligible platform revenue or fee / compatible GMV
Do not add buyer payment and supplier payout; they are two views of the same economic transaction. Keep currencies separate or normalize under a governed FX policy.
Model cancellations, refunds, and disputes#
Emit each transition with actor and controlled reason:
- buyer canceled before match
- supplier rejected
- matched transaction canceled
- service not delivered
- full or partial refund
- dispute opened and resolved
Gross, completed, canceled, refunded, and net transaction value answer different questions. Do not mutate a completed event into a canceled one.
Separate marketplace quality from policy outcomes. A fraud block and a supply shortage may both prevent a transaction, but require different action.
Measure both sides of retention#
Demand retention might mean another qualified request or completed transaction. Supply retention might mean renewed availability and another completed service. Define each independently.
Use acquisition cohorts and maturity windows:
- buyers by first eligible request or first transaction
- suppliers by approved onboarding or first available date
- markets by the date they reached minimum viable supply
Do not infer supplier retention from login events if earning opportunity is the core value.
Cross-side effects need careful interpretation. More suppliers may improve buyer fill rate but reduce earnings per supplier. Keep guardrails for both sides.
Prevent double counting#
Common errors:
- counting one transaction once for the buyer and once for the supplier
- summing daily listing snapshots
- counting every candidate offer as a match
- counting retries as new requests
- treating a partial refund as a fully refunded order
- adding multiple payout installments without grouping to transaction
- counting users instead of durable buyer or supplier entities
Write the grain beside every dashboard tile.
Build the marketplace dashboard#
Show by meaningful market:
- eligible demand and available supply
- mature fill rate and time to match
- unfulfilled requests and idle supply
- match-to-transaction conversion
- completed, canceled, refunded, and net value
- GMV, platform revenue, and take rate by currency
- buyer and supplier retention
- supplier utilization or earnings distribution
- matching version and instrumentation health
Avoid global averages that hide thin markets. Apply minimum-volume rules and show distributions, not only totals.
Launch checklist#
- Buyer, supplier, listing, request, match, and transaction grains are distinct.
- Availability is modeled as state or snapshots, not listing creation alone.
- Fill-rate denominators are mature and eligible.
- Candidate, accepted match, and completed transaction are separate facts.
- Buyer payment and supplier payout are not double counted.
- Cancellations, refunds, disputes, and failed delivery remain explicit.
- Both marketplace sides have value and retention measures.
- Thin markets show denominators and suppression thresholds.
- Money remains separated by currency and economic meaning.
- Transaction, payment, and balance authorities reconcile with GraphJSON.
For detailed order, payment, fulfillment, and refund modeling, continue with Commerce and order-lifecycle analytics.