Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ ${options.body}
<a href="/privacy">Privacy</a>
<a href="/connect">Connect to Moshpit</a>
<a href="/build">Build an app</a>
<a href="/promote">Promoter program</a>
<a href="/ring">Web ring</a>
<a href="/pros">Find a pro</a>
<a href="/b">Claim your listing</a>
Expand Down
193 changes: 193 additions & 0 deletions src/promoters-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/**
* Routes for the pit.world promoter program (src/promoters.ts):
*
* /promote the offer (public, indexable); shows your link if you joined
* /r/:code a promoter's short share link -> /promote?ref=code
* POST /promote/join become a promoter (signed in)
* /promote/start start a free 30-day trial, attributing the referrer
* /promote/dashboard your link, referrals and accrued commission (signed in)
*
* Attribution rides the URL (?ref=code) through sign-in, so no tracking cookie
* is set — consistent with pit.world setting a cookie only on the account
* routes. These routes read the session cookie but never set one.
*/
import type { Context, Hono } from 'hono'
import { SESSION_COOKIE, type Account, type Accounts } from './accounts.js'
import { parseCookies } from './accounts-routes.js'
import { docPage, esc } from './html.js'
import { COMMISSION_RATE, type Promoters } from './promoters.js'

export type PromoteDeps = {
promoters: Promoters
accounts: Accounts
/** https://pit.world: where share links point. */
site: string
}

const PCT = `${Math.round(COMMISSION_RATE * 100)}%`
const money = (cents: number) => `$${(cents / 100).toFixed(2)}`
const refLink = (site: string, code: string) => `${site}/r/${code}`

export function mountPromote(app: Hono<any>, d: PromoteDeps) {
const { promoters, accounts, site } = d
const origin = new URL(site).origin

const current = (c: Context): Account | null => accounts.sessionAccount(parseCookies(c.req.header('cookie'))[SESSION_COOKIE])
const cleanRef = (raw: unknown): string | null => (promoters.byCode(String(raw ?? '')) ? String(raw) : null)
const sameOrigin = (c: Context) => {
const fetchSite = c.req.header('sec-fetch-site')
if (fetchSite) return fetchSite === 'same-origin'
try {
return new URL(c.req.header('origin') ?? c.req.header('referer') ?? '').origin === origin
} catch {
return false
}
}
const page = (c: Context, title: string, body: string, opts: { noindex?: boolean; status?: number } = {}) => {
c.header('cache-control', opts.noindex ? 'no-store' : 'public, max-age=300')
return c.html(docPage({ title: `${title} · pit.world`, description: `Bring people to the Mosh Pit and keep ${PCT} of everything they spend. Free 30-day trial, then $2 a month.`, path: '/promote', ...(opts.noindex ? { noindex: true } : {}), body }), (opts.status ?? 200) as 200)
}
const form = async (c: Context): Promise<Record<string, string>> => {
const body = await c.req.parseBody().catch(() => ({}))
return Object.fromEntries(Object.entries(body).map(([k, v]) => [k, typeof v === 'string' ? v : '']))
}

// ---- the offer (public) --------------------------------------------------------

app.get('/promote', (c) => {
const me = current(c)
const ref = cleanRef(c.req.query('ref'))
const mine = me ? promoters.byAccount(me.id) : null

const invited = ref
? `<p class="notice">You were invited by a promoter. Your 30-day trial is free. <a class="btn" href="/promote/start?ref=${esc(ref)}">Start free trial →</a></p>`
: ''

let cta: string
if (mine) {
cta = `<h2>Your promoter link</h2>
<p>Share this. Everyone who joins through it is yours, for as long as they stay.</p>
<pre><code>${esc(refLink(site, mine.code))}</code></pre>
<p><a class="btn" href="/promote/dashboard">Your dashboard →</a></p>`
} else if (me) {
cta = `<h2>Become a promoter</h2>
<form method="post" action="/promote/join"><button class="btn" type="submit">Get my promoter link →</button></form>
<p class="muted">You get a link, a dashboard, and ${PCT} of everything the people you bring spend.</p>`
} else {
cta = `<h2>Become a promoter</h2>
<p><a class="btn" href="/signin?next=${encodeURIComponent('/promote')}">Sign in to get your link →</a></p>
<p class="muted">Sign in with CoinPay. No card, no password.</p>`
}

return page(
c,
`Promoter program: keep ${PCT} of everything your people spend`,
`<p class="kicker">Promoter program</p>
<h1>Bring people to the Mosh Pit. Keep ${PCT} of everything they spend.</h1>
<p class="lede">pit.world charges a cover because you are not the product — no big-tech trackers, no profile, no selling your moves. Send people in and you keep ${PCT} of everything they spend here, for as long as they stay: builds, hosting, Moshpit names, memberships. $2 a month in, your best month out.</p>
${invited}
<div class="prices">
<div><b>Free</b><span>for 30 days, then $2/mo</span><p>Access the Mosh Pit as a business, promoter or influencer. Paid in crypto through CoinPay. No card.</p></div>
<div><b>${PCT}</b><span>of everything your people spend</span><p>Not ${PCT} of one sub — ${PCT} across the whole catalog, every month they stay. Your ceiling is how good you are.</p></div>
</div>
${cta}
<h2>How it works</h2>
<ol>
<li><b>Join.</b> Sign in with CoinPay and get your link.</li>
<li><b>Share it.</b> Anyone who joins through your link is attributed to you — recorded only when they join, never tracked click by click.</li>
<li><b>Earn.</b> ${PCT} of what they spend accrues on your dashboard and pays out monthly in crypto.</li>
</ol>
<h2>Questions</h2>
<h3>What does it cost?</h3>
<p>Nothing for 30 days, then $2 a month for Mosh Pit access. The promoter program itself is free.</p>
<h3>How is ${PCT} calculated?</h3>
<p>${PCT} of everything a person you referred spends across pit.world and the Moshpit catalog, for as long as they stay a paying member.</p>
<h3>How do I get paid?</h3>
<p>In crypto through <a href="https://coinpayportal.com">CoinPay</a>. Commission accrues on your dashboard and is paid monthly.</p>`,
)
})

// A promoter's short share link.
app.get('/r/:code', (c) => {
const code = c.req.param('code')
return c.redirect(promoters.byCode(code) ? `/promote?ref=${encodeURIComponent(code)}` : '/promote', 302)
})

// ---- become a promoter ---------------------------------------------------------

app.post('/promote/join', async (c) => {
const me = current(c)
if (!me) return c.redirect(`/signin?next=${encodeURIComponent('/promote')}`, 302)
if (!sameOrigin(c)) return c.text('Forbidden', 403)
promoters.join(me.id)
return c.redirect('/promote/dashboard', 302)
})

// ---- start a free trial (attributes the referrer) ------------------------------

app.get('/promote/start', (c) => {
const me = current(c)
const ref = cleanRef(c.req.query('ref'))
const nextTo = `/promote/start${ref ? `?ref=${encodeURIComponent(ref)}` : ''}`
if (!me) return c.redirect(`/signin?next=${encodeURIComponent(nextTo)}`, 302)
const existing = promoters.member(me.id)
if (existing) return c.redirect('/promote/dashboard', 302)
return page(
c,
'Start your free trial',
`<h1>Start your free 30-day trial</h1>
${ref ? `<p class="muted">Invited by a promoter.</p>` : ''}
<p>30 days free, then $2 a month for Mosh Pit access, paid in crypto through CoinPay. Cancel any time before the trial ends and you are never charged.</p>
<form method="post" action="/promote/start">${ref ? `<input type="hidden" name="ref" value="${esc(ref)}">` : ''}<button class="btn" type="submit">Start my free trial →</button></form>`,
{ noindex: true },
)
})

app.post('/promote/start', async (c) => {
const me = current(c)
if (!me) return c.redirect(`/signin?next=${encodeURIComponent('/promote/start')}`, 302)
if (!sameOrigin(c)) return c.text('Forbidden', 403)
const ref = cleanRef((await form(c)).ref)
promoters.startMembership(me.id, ref)
return c.redirect('/promote/dashboard', 302)
})

// ---- your dashboard ------------------------------------------------------------

app.get('/promote/dashboard', (c) => {
const me = current(c)
if (!me) return c.redirect(`/signin?next=${encodeURIComponent('/promote/dashboard')}`, 302)
const stats = promoters.stats(me.id)
if (!stats) {
return page(
c,
'Promoter dashboard',
`<h1>Become a promoter first</h1>
<p>You do not have a promoter link yet.</p>
<form method="post" action="/promote/join"><button class="btn" type="submit">Get my promoter link →</button></form>`,
{ noindex: true },
)
}
const trial =
stats.trial_end && stats.trial_end > Date.now()
? `Free trial — ${Math.ceil((stats.trial_end - Date.now()) / 86_400_000)} days left`
: stats.member_status
? 'Trial ended'
: 'Not a member yet'
return page(
c,
'Promoter dashboard',
`<h1>Your promoter dashboard</h1>
<h2>Your link</h2>
<pre><code>${esc(refLink(site, stats.code))}</code></pre>
<p class="muted">Share it anywhere. People who join through it are attributed to you.</p>
<table class="facts">
<tr><th>People referred</th><td>${stats.referred}</td></tr>
<tr><th>Commission accrued</th><td>${money(stats.commission_cents)} <span class="muted">(${PCT} of their spend, paid monthly in crypto)</span></td></tr>
<tr><th>Your membership</th><td>${esc(trial)}</td></tr>
</table>
${stats.member_status ? '' : `<p><a class="btn" href="/promote/start">Start your own free trial →</a></p>`}`,
{ noindex: true },
)
})
}
150 changes: 150 additions & 0 deletions src/promoters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* The pit.world promoter program: bring people to the Mosh Pit, keep 20% of
* everything they spend.
*
* It fits pit.world's no-tracking posture. Nothing is logged per click — a
* referral is recorded only at the moment a person *converts* (starts a
* membership), and the promoter code rides the URL through sign-in, so no
* tracking cookie is ever set. A promoter is just an account that joined; the
* code is its public, shareable id.
*
* Billing is staged: a membership starts as a free 30-day trial here. The $2/mo
* charge after the trial and the 20% payout execution are a separate CoinPay
* integration (none exists in this repo yet). Commission is accrued in a ledger
* the moment a referred account spends — recordCommission() — so the dashboard
* is honest from day one even before payouts are automated.
*/
import { Database } from 'bun:sqlite'
import { randomBytes } from 'node:crypto'
import { mkdirSync } from 'node:fs'
import { dirname } from 'node:path'

export type Promoter = { account_id: string; code: string; created_at: number }
export type Membership = { status: string; trial_end: number | null; referred_by: string | null }
export type PromoterStats = {
code: string
referred: number
commission_cents: number
trial_end: number | null
member_status: string | null
}

/** Trial length before the $2/mo membership would begin. */
export const TRIAL_MS = 30 * 24 * 60 * 60 * 1000
/** The house commission rate, applied to everything a referral spends. */
export const COMMISSION_RATE = 0.2

// No vowels and no ambiguous glyphs, so a code is safe to read aloud and type.
const CODE_ALPHABET = 'abcdefghjkmnpqrstuvwxyz23456789'
export const CODE_RE = /^[a-hj-km-np-z2-9]{8}$/

export class Promoters {
readonly db: Database

constructor(path = ':memory:', private now: () => number = Date.now) {
if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true })
this.db = new Database(path, { create: true })
this.db.run('PRAGMA journal_mode = WAL')
this.db.run(
`CREATE TABLE IF NOT EXISTS promoters (
account_id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
created_at INTEGER NOT NULL)`,
)
this.db.run(
`CREATE TABLE IF NOT EXISTS members (
account_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
trial_end INTEGER,
referred_by TEXT,
created_at INTEGER NOT NULL)`,
)
// One row per attributed spend, so a payout run is a sum over a promoter.
this.db.run(
`CREATE TABLE IF NOT EXISTS commissions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
promoter_code TEXT NOT NULL,
referred_account_id TEXT NOT NULL,
amount_cents INTEGER NOT NULL,
note TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL)`,
)
this.db.run('CREATE INDEX IF NOT EXISTS idx_members_ref ON members(referred_by)')
this.db.run('CREATE INDEX IF NOT EXISTS idx_commissions_code ON commissions(promoter_code)')
}

private genCode(): string {
for (;;) {
let code = ''
for (const b of randomBytes(8)) code += CODE_ALPHABET[b % CODE_ALPHABET.length]
if (!this.byCode(code)) return code
}
}

/** Make this account a promoter, or return the one it already is. Idempotent. */
join(accountId: string): Promoter {
const existing = this.byAccount(accountId)
if (existing) return existing
const promoter: Promoter = { account_id: accountId, code: this.genCode(), created_at: this.now() }
this.db.run('INSERT INTO promoters (account_id, code, created_at) VALUES (?, ?, ?)', [promoter.account_id, promoter.code, promoter.created_at])
return promoter
}

byAccount(accountId: string): Promoter | null {
return this.db.query<Promoter, [string]>('SELECT account_id, code, created_at FROM promoters WHERE account_id = ?').get(accountId) ?? null
}

byCode(code: string): Promoter | null {
if (!CODE_RE.test(code)) return null
return this.db.query<Promoter, [string]>('SELECT account_id, code, created_at FROM promoters WHERE code = ?').get(code) ?? null
}

member(accountId: string): Membership | null {
return this.db.query<Membership, [string]>('SELECT status, trial_end, referred_by FROM members WHERE account_id = ?').get(accountId) ?? null
}

/**
* Start a membership on a free 30-day trial, or return the existing one. The
* referrer is attributed exactly once, here, and only when the code is real
* and is not the member's own.
*/
startMembership(accountId: string, refCode?: string | null): Membership {
const existing = this.member(accountId)
if (existing) return existing
let referred_by: string | null = null
if (refCode) {
const promoter = this.byCode(refCode)
if (promoter && promoter.account_id !== accountId) referred_by = promoter.code
}
const now = this.now()
const membership: Membership = { status: 'trialing', trial_end: now + TRIAL_MS, referred_by }
this.db.run('INSERT INTO members (account_id, status, trial_end, referred_by, created_at) VALUES (?, ?, ?, ?, ?)', [accountId, membership.status, membership.trial_end, referred_by, now])
return membership
}

/**
* Accrue commission owed to a promoter for a referred account's spend. Called
* when a charge lands (staged billing); 20% of the amount by house rate.
*/
recordSpend(referredAccountId: string, amountCents: number, note = ''): number {
const m = this.member(referredAccountId)
if (!m?.referred_by) return 0
const commission = Math.round(amountCents * COMMISSION_RATE)
if (commission <= 0) return 0
this.db.run('INSERT INTO commissions (promoter_code, referred_account_id, amount_cents, note, created_at) VALUES (?, ?, ?, ?, ?)', [m.referred_by, referredAccountId, commission, note, this.now()])
return commission
}

stats(accountId: string): PromoterStats | null {
const promoter = this.byAccount(accountId)
if (!promoter) return null
const referred = this.db.query<{ n: number }, [string]>('SELECT COUNT(*) AS n FROM members WHERE referred_by = ?').get(promoter.code)?.n ?? 0
const commission_cents = this.db.query<{ s: number }, [string]>('SELECT COALESCE(SUM(amount_cents), 0) AS s FROM commissions WHERE promoter_code = ?').get(promoter.code)?.s ?? 0
const m = this.member(accountId)
return { code: promoter.code, referred, commission_cents, trial_end: m?.trial_end ?? null, member_status: m?.status ?? null }
}

count(): number {
return this.db.query<{ n: number }, []>('SELECT COUNT(*) AS n FROM promoters').get()?.n ?? 0
}
}
Loading
Loading