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 (coupon or link).
  • 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/partnersNew 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/dealsNew deal. Server-validated rules (backend/internal/marketplace/service.go:429):

FieldRule
partner_idrequired; must reference an existing partner
titlerequired, ≤ 255 chars
description≤ 4 000 chars
deal_kindrequired: coupon or link
coupon_coderequired when kind=coupon, ≤ 64 chars
redemption_urlrequired when kind=link, ≤ 2 048 chars
categoriesfree-form text[]
audience_scoperequired: `public
audience_scope_valuerequired when scope is batch or chapter, otherwise empty
valid_from / valid_untilboth required; valid_until strictly after valid_from
redemption_capoptional positive int
statedefaults 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.

ScopeVisible toWhen to use
publiceveryone with a Bits login (incl. parents)broad consumer offers
instituteverified institute userscampus-life targeted (food, transit)
alumnirole = alumnialumni-only perks
batchalumni in a specific batch_yearreunion-year offers
chaptermembers of a specific chapter grouplocation-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 second RecordRedemption from the same user returns 409 AlreadyExists with the original redeemed_at embedded 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 429 ResourceExhausted (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.
  • sourcecopy_code (kind=coupon) or outbound_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_click count, 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):

  1. Pull the redemption export for the period (/admin/marketplace/deals → CSV export, or the admin redemption RPC).
  2. Reconcile against the partner’s tally.
  3. Process payment through your finance team’s existing channel.
  4. 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:

  1. Pause every active deal for the partner (UpdateDealstate=paused).
  2. 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 once valid_until passes.
  3. 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”UpdateDeal accepts a new coupon_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”ErrPartnerHasDeals blocks 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 the reconcileDeal call 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.