Handover · for knky engineering

knky Affiliates — dev handover

Two-sided marketplace + platform-promote (operator 2026-05-23). Cookie-first storage with two cookie-less fallbacks. Standalone mini-site at knky.money + first-class integration inside knky.co (settings panel, soft prompt, surfaced link).

Source: knky-affiliates-handover-brief.md

1. Outcomes

  1. Creator opt-in (merchant) — a knky creator enrols and picks a % (5–35) they're willing to pay anyone who refers fans/customers to them. The % is deducted from the creator's net earnings at payout time.
  2. Referrer marketplace — any knky user can browse opted-in creators (or paste a known handle), get a personalised link knky.co/r/<promoter>/<creator>, and earn the % that creator pre-defined. 30-day window → lifetime attribution on referred fans' spend with that creator.
  3. Platform-promote — separate enrolment for affiliates promoting knky itself. Flat 10% of net revenue from referred signups (fans AND creators). Two links per partner — knky.co/p/<handle>/fan and knky.co/p/<handle>/creator — same rate, tracked separately. Paid out of knky's platform fees, not from any creator.
  4. Creator-side integration on knky.co — creators can opt in, change % and pause from existing knky settings, see a one-time opt-in prompt elsewhere on the platform, and grab their referral link without leaving knky. The standalone mini-site stays the rich version (directory, stats, payouts).

Out of scope v1: per-tier creator bonuses, A/B-testable bounties, paid-search enforcement, native bank payouts.

2. Pages — UI is built; wire data 1:1

Standalone mini-site on knky.money (also reachable via /knky/affiliates on staging.folo.co):

Route on knky.moneyWhoPurpose
/anyoneLanding — three paths + FAQ
/creatorcreatorOpt-in: set %, get 'promote-me' link, see who's promoting + commission cost
/referreferrerDirectory + direct-handle entry → personalised link + earnings
/platformplatform partnerApply once → two separate links (fan / creator) at 10%
/devhandoverdev teamThis document

Creator-side integration on knky.co (knky devs to build inside the existing app — full spec in §6):

SurfacePurpose
Creator settings → 'Affiliate program' panelOpt-in toggle + commission % + 'view full dashboard' link to knky.money
Soft opt-in prompt (dashboard banner / toast)One-time nudge for creators not yet enrolled. Dismissible.
'Share your affiliate link' card on creator dashboardSurfaces knky.co/r/<creator> with copy/share. Only when opted in.
Optional badge on public profile'Has an affiliate program · X%' tag for promoters to spot.

3. Schema — minimal additions

3.1 creator_affiliate_settings (or fields on users)

{
  user_id: ObjectId,                  // FK users; PK
  status: 'inactive' | 'active' | 'paused',
  commission_pct: number,             // 5..35
  pause_new_referrals: boolean,
  terms_accepted_at: Date,
  // Soft-prompt suppression (§6.2) — set when a creator dismisses the prompt.
  prompt_dismissed_at: Date | null,
  created_at: Date,
  updated_at: Date,
}

Rate changes don't retroactively re-price: existing attributions keep their snapshotted % on every ledger row.

3.2 platform_partners

{
  _id: ObjectId,
  user_id: ObjectId | null,
  handle: string,                     // unique
  status: 'pending' | 'active' | 'paused' | 'rejected',
  legal_name: string,
  payout_email: string,
  promote_channel: string,
  audience_18plus_attested: boolean,
  terms_accepted_at: Date,
  fraud_review: boolean,
  created_at: Date,
  updated_at: Date,
}
// 10% rate stored as a constant: PLATFORM_AFFILIATE_PCT = 10

3.3 referral_attributions

{
  _id: ObjectId,
  paying_user_id: ObjectId,           // unique key
  referrer_kind: 'creator_promoter' | 'platform_partner',
  referrer_user_id: ObjectId | null,
  partner_id: ObjectId | null,
  promoted_creator_id: ObjectId | null,
  source_link: 'r' | 'p/fan' | 'p/creator',
  captured_at: Date,                  // when ref was first stored on this device/session
  signed_up_at: Date | null,
  first_paid_txn_id: ObjectId | null,
  first_paid_at: Date | null,
  is_self_referral: boolean,
  created_at: Date,
}

3.4 referral_ledger

{
  _id: ObjectId,
  attribution_id: ObjectId,
  transaction_id: ObjectId,
  gross_cents: number,                // net of platform + processing fees
  commission_pct: number,             // snapshotted at accrual
  commission_cents: number,
  pays_from: 'creator' | 'platform',  // drives finance routing
  state: 'accrued' | 'paid' | 'clawed_back' | 'voided',
  accrued_at: Date,
  paid_at: Date | null,
  payout_id: ObjectId | null,
  clawback_reason?: 'refund' | 'chargeback' | 'fraud',
  created_at: Date,
  updated_at: Date,
}
pays_from drives finance routing. 'creator' deducts from the creator's MassPay payout; 'platform' books against finance.platform_expenses.

3.5 users field addition

users.referrer_attribution_id: ObjectId | null

4. Storage strategy — cookie-first, two fallbacks

Affiliate attribution has to survive between landing on /r/... and signing up — could be minutes or weeks. Three implementations, in order of preference. Pick one — the rest of the spec works the same either way.

4.1 ✅ PREFERRED — one first-party cookie

Capture endpoint sets a single cookie:

Set-Cookie: knky_ref=r:<referrer_user_id>:<promoted_creator_id>;
            Domain=.knky.co; Path=/; Max-Age=2592000;
            HttpOnly; SameSite=Lax; Secure

Signup endpoint reads req.cookies.knky_ref → parses → writes users.referrer_attribution_id + creates the referral_attributions row → clears the cookie.

Why this is fine even though knky doesn't use cookies for auth. This cookie is purely additive, scoped to one purpose. Setting one first-party cookie doesn't change the app's "cookie-less auth" architecture — your auth keeps using JWT in localStorage / Authorization headers / whatever it uses. The affiliate cookie is single-purpose, HttpOnly (no JS reads it), and never touched by anything else. Same pattern every analytics/affiliate platform uses on cookieless-auth stacks.

Works across all .knky.co subdomains automatically (Domain=.knky.co). Survives tab close + browser restart. 30-day Max-Age = the attribution window.

4.2 Fallback A — localStorage + URL parameter

If there's a hard "no cookies anywhere" policy (consent, strict GDPR):

Capture endpoint serves a tiny inline HTML that:

localStorage.setItem('knky_ref', JSON.stringify({
  id: '<affiliate>',
  source_link: '<r|p/fan|p/creator>',
  exp: Date.now() + 30 * 86400000,
}));
window.location.replace('<dest>?_aff=<affiliate>');

Trade-offs vs. cookie: brittle on cross-subdomain (every redirect needs the param); localStorage cleared with site data; doesn't survive private-window changes. More integration work in the auth flow.

4.3 Fallback B — server-side session-id keyed cache

If knky already issues an ambient anonymous session-id to every visitor (in localStorage / a header) before signup:

Trade-off: depends on knky issuing the session-id BEFORE signup. Some apps issue lazily (only after auth). Confirm with the auth layer; if not, this option doesn't work without adding the anonymous session-id first.

5.1 Public capture URLs

Storage = whichever §4 strategy you picked. Last-click within window wins.

5.2 Signup

  1. Read the stored ref (cookie / localStorage / cached session).
  2. Validate the referenced rows are active.
  3. Create a referral_attributions row keyed on paying_user_id = new user._id.
  4. Compute is_self_referral; flag fraud if true.
  5. Set users.referrer_attribution_id. Clear the stored ref.

5.3 Paid transactions

On any transactions.status === 'Completed' for a user with referrer_attribution_id:

  1. If first_paid_txn_id not yet set on the attribution, set it now.
  2. For each completed paying transaction, write a referral_ledger row.
source_linkRate sourcepays_from
'r' (creator marketplace)creator_affiliate_settings.commission_pct of the promoted_creator'creator'
'p/fan'config PLATFORM_AFFILIATE_PCT (10%)'platform'
'p/creator'config PLATFORM_AFFILIATE_PCT (10%) — on platform's cut of referred creator's earnings'platform'

For 'r', only count revenue on transactions whose seller is promoted_creator_id.

Skip non-revenue categories: WalletRecharge, Withdraw, SavePaymentCard, PaymentCardSavedSuccessfullyAndRefunded, PaymentDetailUpdate, CreditFromKNKY, BalanceLocking, OfferAmountDeduction.

5.4 Refund / chargeback clawback

Within 60 days of accrued_at: void (still accrued) or claw back (already paid). Beyond 60 days: locked.

6. Creator-side integration on knky.co

The standalone mini-site at knky.money has the full experience. But creators shouldn't have to leave knky.co to opt in, change their %, pause, or grab their link. Build these surfaces inside the existing knky app.

6.1 Creator settings panel — "Affiliate program"

Add a section to the existing creator settings page (alongside payout, KYC, notifications, etc.):

┌──────────────────────────────────────────────────┐
│  Affiliate program                                │
│  ──────────────────────────────────────           │
│  Status: ● Active  ○ Paused  ○ Off                │
│  Commission to referrers:  [ 10% ] [15%] [20%]    │
│                            [25%] [Custom ▾]       │
│  Your promote-me link:                            │
│  knky.co/r/<your-handle>  [Copy]  [Share]         │
│                                                   │
│  Open full dashboard ↗  → knky.money/creator      │
└──────────────────────────────────────────────────┘

6.2 Soft opt-in prompt (one-time, dismissible)

For creators with no creator_affiliate_settings row (or status='inactive'), show a non-blocking prompt on relevant surfaces:

Earn more by enabling affiliate referrals. Set your commission %, and every fan or creator someone refers to you earns you net of their cut. We track + pay it all.

[ Set my rate ]  [ Maybe later ]

Maybe laterPOST /api/affiliates/creator/dismiss-prompt writes prompt_dismissed_at = now. Don't show again for 30 days. Set my rate → opens the settings panel from §6.1.

6.3 "Share your affiliate link" — surfaced on the platform

For OPTED-IN creators, surface knky.co/r/<creator> prominently:

6.4 Mini-site cross-link

Everywhere on knky.co that surfaces affiliate state — settings, the link card, the soft prompt — include a single deep-link out to the mini-site:

Open full affiliate dashboard ↗https://knky.money/creator (or https://knky.money for the landing)

The mini-site has the directory of other creators, full ledger, payouts table, marketing assets, and the partner-promote flow — heavier stuff that doesn't need to clutter the in-platform settings.

6.5 Fan-side: get a referral link without leaving knky.co

Optional but recommended for a complete loop on knky.co itself:

7. Payouts — same MassPay rails, two finance lines

Monthly cron, 15th 09:00 UTC. Shared logic, different ledger source and finance booking:

RecipientSource rowsFunded fromMin payout
Referrer (creator-marketplace)referral_ledger where pays_from='creator'Deduction from the creator's net at the same MassPay batch$20
Platform partnerreferral_ledger where pays_from='platform'knky platform fees$50

Creator's MassPay statement gets a line: "Promoter commission — $X.XX" so they see what they paid out.

Payouts only run for verified users (users.kyc_verified === true). Platform partners with no knky account need light KYC at $600+ lifetime payouts (US 1099-K).

8. API endpoints

MethodPathNotes
GET/r/:creator, /r/:promoter/:creator, /p/:handle/:typeCapture — apply §4 storage strategy + 302
POST/api/affiliates/creator/opt-in{ commission_pct }; auth; idempotent. Used by both mini-site + in-platform settings (§6.1)
POST/api/affiliates/creator/pause{ pause: boolean }
POST/api/affiliates/creator/dismiss-promptSets prompt_dismissed_at (§6.2)
GET/api/affiliates/creator/meSettings + stats — drives BOTH the in-platform card AND the mini-site dashboard
GET/api/affiliates/directory?vertical=&sort=Opted-in creators for /refer (public)
POST/api/affiliates/refer/link{ creator_handle } → personalised URL
GET/api/affiliates/refer/meReferrer dashboard data
POST/api/affiliates/platform/applyPartner application
GET/api/affiliates/platform/mePartner dashboard data
Admin/api/admin/affiliates/* — approve/reject/pause/fraud_flag/ledgerAudited via existing admin-core

9. Security & anti-abuse

10. What we need from your side

  1. Storage strategy choice (§4) — confirm you can add the one first-party cookie (4.1, preferred). If no, tell us which fallback (4.2 / 4.3) and we'll align.
  2. MassPay batch payout API shape + deduction-line format so the creator-marketplace deduction renders correctly on creators' statements.
  3. Confirm whether transactions.total_amount is inclusive of tax (total_amount_with_tax also on the doc) — commission wants net of fees and tax.
  4. centrochargebacks "finalised lost" field for clawback hook.
  5. Any existing ?ref= campaigns to grandfather.
  6. Admin-core RBAC entry-point + role names for the admin endpoints.
  7. Creator settings page location — where exactly we slot the §6.1 panel + the link to surface §6.3.

11. Estimate — ~8 dev-days for v1

PieceEstimate
Capture endpoints (3 URL shapes) + chosen storage strategy + signup hook~1 day
Schema + opt-in/apply endpoints + directory API~1 day
Attribution + ledger writers on transactions webhook~1 day
Refund / chargeback clawback~0.5 day
Monthly payout cron (split: creator deduction + platform expense)~1 day
Admin screens (list, approve, pause, ledger view) — both kinds~1.5 day
Standalone mini-site pages (built — wire data)~0.5 day
Creator-side integration on knky.co — settings panel + prompt + link surface (§6)~1.5 day
Total v1~8 dev-days

+ ~2 days polish + manual QA. Bigger than the previous estimate because of the in-platform creator surfaces (which we now want first-class, not just the standalone site).

12. Open questions

  1. Self-promote via /r/me/me — block by default (already covered by self-referral block).
  2. Multiple promoters → same fan. Last-click wins, as above. Confirm OK.
  3. Creator-promote-creator. Today the model is fan-attribution only — creator-to-creator referrals belong to the platform-promote flow. Confirm we don't conflate.
  4. Backfill. Existing organic referrals — backfill or prospective only? Suggest prospective.
  5. Rate change snapshotting. Old rate sticks on the ledger row; new attributions use new rate. Confirm.
  6. Soft-prompt fatigue (§6.2) — once dismissed, re-prompt after 30 days or never re-prompt? Suggest after 30 days, max twice ever.
  7. Public profile badge (§6.3) — opt-in by creator (default off), or always on when affiliate is active? Suggest opt-in.