Marketplace Partner-Onboarding Workflow#
This handbook covers the end-to-end workflow for bringing a partner onto the Bits marketplace: from initial application through deal authoring, audience scoping, redemption monitoring, and payout settlement.
Who this is for: anyone with the platform admin role and the marketplace.curate capability. The user-facing marketplace guide is at documentation/role-guides/alumni/marketplace.md.
Source of truth: backend/internal/marketplace/. RPC contract: protos/bits/marketplace/v1/marketplace.proto. Admin webapp: webapp/src/routes/admin/AdminMarketplace*.tsx.
1. Partner application (out-of-platform)#
V2 does not provide a self-service partner-application form. The application is handled out-of-platform — typically email, contract, then a Bits admin manually mints the partner record. This is intentional: marketplace is a curated surface and uncurated partner sign-ups would dilute trust.
A typical pre-platform application captures:
- Partner legal name, brand name.
- Logo (PNG/SVG, square aspect for the deal card).
- Website + a primary contact email.
- Category fit (food, travel, gear, services, etc.).
- Deal type they want to offer (
couponorlink). - Validity window (start / end dates).
- Redemption cap if any (e.g. first 100 redemptions).
Run partner due diligence offline. Once approved by the BITS marketplace owner, proceed to step 2.
Note: in-platform partner applications are out of scope for V2; they’re a Phase 3 candidate.
2. Creating the partner record#
/admin/marketplace/partners → New partner. Fields:
- Name (≤ 255 chars).
- Logo URL — point at a CDN-hosted asset; the marketplace UI does not transcode.
- Website URL.
- Contact email — used for partner-side correspondence (e.g. payout notifications).
Once you save, the partner row exists with vetted_at = NULL. A separate Mark vetted action (or directly setting vetted_at via a deal-creation gate) records who vetted the partner and when (Partner.VettedBy, Partner.VettedAt).
You can delete a partner only if they have zero deals attached (ErrPartnerHasDeals → 422). For partners going off-platform, paused-state all their deals first, then delete.
3. Authoring a deal#
/admin/marketplace/deals → New deal. Server-validated rules (backend/internal/marketplace/service.go:429):
| Field | Rule |
|---|---|
partner_id | required; must reference an existing partner |
title | required, ≤ 255 chars |
description | ≤ 4 000 chars |
deal_kind | required: coupon or link |
coupon_code | required when kind=coupon, ≤ 64 chars |
redemption_url | required when kind=link, ≤ 2 048 chars |
categories | free-form text[] |
audience_scope | required: `public |
audience_scope_value | required when scope is batch or chapter, otherwise empty |
valid_from / valid_until | both required; valid_until strictly after valid_from |
redemption_cap | optional positive int |
state | defaults to draft if not set |
State machine (backend/internal/marketplace/types.go:46):
draft → active (admin flips when ready to publish)
→ paused (admin pause, e.g. partner request)
active → paused (reversible)
→ expired (auto, on `valid_until` lapse, by the periodic job)Only active deals are listable, redeemable, and indexed in search. Flipping to or from active triggers the search reconciler to upsert / delete the deal’s search_document row (Service.reconcileDeal / Service.deleteFromIndex).
4. Audience scoping#
Five scope kinds. Choose the narrowest one that fits the partner’s targeting.
| Scope | Visible to | When to use |
|---|---|---|
public | everyone with a Bits login (incl. parents) | broad consumer offers |
institute | verified institute users | campus-life targeted (food, transit) |
alumni | role = alumni | alumni-only perks |
batch | alumni in a specific batch_year | reunion-year offers |
chapter | members of a specific chapter group | location-bound chapter perks |
Scoping is enforced server-side at every read (canSeeDeal in service.go), at redemption (RecordRedemption rejects with NotFound if the caller can’t see the deal), and in search hits (the reconciler emits the audience tag set on the indexed document).
5. Redemption caps#
Two cap modes:
redemption_cap = 1— one-shot per user. The store enforces a unique(deal_id, user_id)index in this mode, so a secondRecordRedemptionfrom the same user returns 409AlreadyExistswith the originalredeemed_atembedded in the error message. This is the right shape for high-value coupons.redemption_cap = N— first N total redemptions. The decrement is row-locked at the database (SELECT … FOR UPDATE), so two simultaneous clients on the 100th of 100 cap will not double-redeem. Beyond N, the API returns 429ResourceExhausted(ErrRedemptionCapReached).
Leaving the cap unset means unlimited redemptions for the validity window. In that case repeat redemptions from the same user are allowed and recorded individually — useful for ongoing discounts.
6. Monitoring redemptions#
/admin/marketplace/deals shows the live redemption count per deal. For deeper analysis, the redemptions list is at /admin/marketplace/deals/<id> (or via the ListRedemptions admin RPC, service.go:709) with filters by date range and CSV export.
A redemption row records:
deal_id,user_id.redeemed_at.source—copy_code(kind=coupon) oroutbound_click(kind=link).
Audit rows are written for every successful redemption (bits.marketplace.v1.MarketplaceService/RecordRedemption) so investigation can use the existing /admin/audit-logs query.
What to watch#
- Sudden spike from one user — possible coupon-sharing across accounts. Investigate via the audit timeline; consider tightening the cap or adding a per-user cap (Phase 3).
- High
outbound_clickcount, low confirmed conversions on the partner side — partner reports back manually; reconcile by comparing redemption counts with the partner’s tally. - Cap exhausted in minutes after publish — usually means the cap was set too low for the audience size. Pause + re-publish with a higher cap.
7. Payout settlement (off-platform)#
V2 does not run any payment rails. Settlement with partners is handled out-of-platform on whatever cadence the partnership contract specified (typically monthly):
- Pull the redemption export for the period (
/admin/marketplace/deals→ CSV export, or the admin redemption RPC). - Reconcile against the partner’s tally.
- Process payment through your finance team’s existing channel.
- Record the settlement in your finance system (Bits does not store this).
If the partner offered a flat fee instead of per-redemption commission, the count is informational only.
8. Off-boarding a partner#
When a partnership ends:
- Pause every active deal for the partner (
UpdateDeal→state=paused). - Wait for the validity windows to lapse, or manually flip to
expired. The expiry job (backend/internal/marketplace/jobs.go) will eventually do this on its own oncevalid_untilpasses. - Once all deals are non-active, delete the partner row (or leave it for audit — partners are not auto-cleaned).
9. Performance budget (Stage 23 §8.3)#
- p95
ListDeals< 150 ms. - Admin export of 10k redemptions < 5 s.
- Cap contention: 1k concurrent on a cap=100 deal — exactly 100 succeed.
10. Common operational issues#
- “A partner wants to change their coupon code mid-campaign” —
UpdateDealaccepts a newcoupon_code. Existing redemptions retain the old code in their audit row. Communicate the change to redeemers via a partner email; no in-platform broadcast. - “Partner deleted before deal expired” —
ErrPartnerHasDealsblocks the partner delete. Pause and expire the deals first, then delete. - “A deal won’t show in search” — only
active-state deals are indexed. Confirm the state, and confirm the document was reconciled (audit log shows thereconcileDealcall site implicitly via the create/update audit row). - “User reports a redemption they didn’t make” — pull the audit log entry for the redemption, check the
user_id. If it’s genuinely fraudulent (cookie theft, etc.), terminate the user’s session and reset their account; bits does not have a redemption-rollback RPC in V2.