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
- 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.
- 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. - 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>/fanandknky.co/p/<handle>/creator— same rate, tracked separately. Paid out of knky's platform fees, not from any creator. - 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.money | Who | Purpose |
|---|---|---|
| / | anyone | Landing — three paths + FAQ |
| /creator | creator | Opt-in: set %, get 'promote-me' link, see who's promoting + commission cost |
| /refer | referrer | Directory + direct-handle entry → personalised link + earnings |
| /platform | platform partner | Apply once → two separate links (fan / creator) at 10% |
| /devhandover | dev team | This document |
Creator-side integration on knky.co (knky devs to build inside the existing app — full spec in §6):
| Surface | Purpose |
|---|---|
| Creator settings → 'Affiliate program' panel | Opt-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 dashboard | Surfaces 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 = 103.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 | null4. 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; SecureSignup endpoint reads req.cookies.knky_ref → parses → writes users.referrer_attribution_id + creates the referral_attributions row → clears the cookie.
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>');- Primary store: localStorage (same-origin only). Survives until user clears site data.
- Cross-subdomain fallback: the
?_aff=<id>URL parameter. Every internal redirect in the auth flow must preserve it (login → signup, multi-step onboarding, etc.) or attribution is lost across the subdomain crossing. - Signup form reads
_afffrom URL first, then localStorage; submits with the form payload. - Backend writes attribution from the form field.
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:
- Capture endpoint writes to Redis:
affiliate_pending[sessionId] = { affiliate_id, source_link, captured_at }with 30-day TTL. - Signup endpoint reads same
sessionIdfrom the request, looks up the pending attribution, materialises it asreferral_attributions, deletes the pending key.
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. Link shapes + attribution flow
5.1 Public capture URLs
- Creator-marketplace referral:
knky.co/r/<promoter>/<creator>→ storer:<referrer_user_id>:<creator_id>. - Creator's own "promote me" link:
knky.co/r/<creator>→ storer::<creator_id>. Single-segment marketing URL the opted-in creator shares. - Platform-promote, fans:
knky.co/p/<handle>/fan→ storep:<partner_id>:fan. - Platform-promote, creators:
knky.co/p/<handle>/creator→ storep:<partner_id>:creator.
Storage = whichever §4 strategy you picked. Last-click within window wins.
5.2 Signup
- Read the stored ref (cookie / localStorage / cached session).
- Validate the referenced rows are active.
- Create a
referral_attributionsrow keyed onpaying_user_id = new user._id. - Compute
is_self_referral; flag fraud if true. - 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:
- If
first_paid_txn_idnot yet set on the attribution, set it now. - For each completed paying transaction, write a
referral_ledgerrow.
| source_link | Rate source | pays_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 │
└──────────────────────────────────────────────────┘- Pre-fills from
creator_affiliate_settings(status, commission_pct). - Save writes via
POST /api/affiliates/creator/opt-in. - "Open full dashboard" deep-links to the mini-site for richer stats, payouts, marketing assets.
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:
- Creator dashboard top.
- Earnings / monetisation page.
- After first $100 in earnings (one-shot).
[ Set my rate ] [ Maybe later ]
Maybe later → POST /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:
- Creator dashboard: a "Share your affiliate link" card with copy + native share. Includes a small stat: "X promoters · $Y earned for them this month."
- Profile "share" modal/menu: if knky already has a "Share my profile" UI, add a tab: "Share my affiliate link" alongside.
- Optional public badge: a discreet "Affiliate program · X%" tag on the public creator profile so promoters can spot opted-in creators while browsing organically.
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:
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:
- On any creator profile, a small "Promote this creator → get my affiliate link" button (only shown if the creator is opted in).
- Clicking it: if the visitor is logged in → modal with
knky.co/r/<my-handle>/<this-creator>+ copy/share + their projected earnings. If not logged in → "Sign in or sign up to start earning" CTA. - For full browsing/discovery, link out to knky.money/refer (the mini-site directory).
7. Payouts — same MassPay rails, two finance lines
Monthly cron, 15th 09:00 UTC. Shared logic, different ledger source and finance booking:
| Recipient | Source rows | Funded from | Min payout |
|---|---|---|---|
| Referrer (creator-marketplace) | referral_ledger where pays_from='creator' | Deduction from the creator's net at the same MassPay batch | $20 |
| Platform partner | referral_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.
users.kyc_verified === true). Platform partners with no knky account need light KYC at $600+ lifetime payouts (US 1099-K).8. API endpoints
| Method | Path | Notes |
|---|---|---|
| GET | /r/:creator, /r/:promoter/:creator, /p/:handle/:type | Capture — 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-prompt | Sets prompt_dismissed_at (§6.2) |
| GET | /api/affiliates/creator/me | Settings + 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/me | Referrer dashboard data |
| POST | /api/affiliates/platform/apply | Partner application |
| GET | /api/affiliates/platform/me | Partner dashboard data |
| Admin | /api/admin/affiliates/* — approve/reject/pause/fraud_flag/ledger | Audited via existing admin-core |
9. Security & anti-abuse
- Self-referral block (§5.2).
- Co-attribution rule: last-click within window wins. A subsequent
/r/x/yclick before signup overwrites the stored ref. - Rate-limit partner application endpoint (3/min/IP, 10/day/IP) + CAPTCHA.
- Handle namespaces:
/r/<promoter>/<creator>uses the user's existing knky handle./p/<handle>uses its own namespace with reserved-handle blocklist. - IP / device co-occurrence heuristic via
logindeviceinfosflags suspicious clusters. - PII:
platform_partners.legal_namegated behind admin RBAC. - Cookie / storage privacy: if §4.1, the cookie is HttpOnly + first-party + 30-day → minimal footprint, but include it in your cookie policy. If you can't add cookies at all, §4.2 / §4.3 cover it.
10. What we need from your side
- 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.
- MassPay batch payout API shape + deduction-line format so the creator-marketplace deduction renders correctly on creators' statements.
- Confirm whether
transactions.total_amountis inclusive of tax (total_amount_with_taxalso on the doc) — commission wants net of fees and tax. centrochargebacks"finalised lost" field for clawback hook.- Any existing
?ref=campaigns to grandfather. - Admin-core RBAC entry-point + role names for the admin endpoints.
- 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
| Piece | Estimate |
|---|---|
| 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
- Self-promote via
/r/me/me— block by default (already covered by self-referral block). - Multiple promoters → same fan. Last-click wins, as above. Confirm OK.
- 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.
- Backfill. Existing organic referrals — backfill or prospective only? Suggest prospective.
- Rate change snapshotting. Old rate sticks on the ledger row; new attributions use new rate. Confirm.
- Soft-prompt fatigue (§6.2) — once dismissed, re-prompt after 30 days or never re-prompt? Suggest after 30 days, max twice ever.
- Public profile badge (§6.3) — opt-in by creator (default off), or always on when affiliate is active? Suggest opt-in.