Skip to main content
A referral partner earns 0.25% of gross on every payment their referred merchants process. The payout address is committed on-chain in the pool’s init code, so it is paid automatically and immutably at every distribution — the platform cannot redirect it without deploying a visibly different pool.

Two levels

Hard rule: resolve the partner before the pool is created. Once the pool exists, its recipients are frozen. A partner bound afterwards cannot be paid from that pool.

Five layers

1

Capture

?ref=<CODE> on any landing page writes to localStorage["cc:ref-code"] via captureRefFromUrl(), called on app mount. Deliberately not a cookie — no Safari or ad-blocker loss.
2

Carry

The stashed code travels as a cc:ref:<CODE> SIWE resource line, the same channel as the partner role hint.
3

Bind

siwe-verify’s resolveRoles stamps merchants.partner_referrer_id only on the first merchant-row insert — new-merchant-only, first-touch. Guarded against inactive partners and self-referral. Bound once, never overwritten by a later link.
4

Manual recovery

A field in merchant Settings calls bind-referral, which binds only while partner_referrer_id IS NULL. Recovers a capture lost to cleared local storage.
5

Platform override

/platform-admin → Merchants calls platform-admin-set-referrer — the only path that can change an already-bound merchant. Writes an audit_logs row.

Codes

Six characters, canonical, generated by generateReferralCode, excluding 0, 1, and O to avoid transcription errors.
Hand-seeded non-canonical codes — for example the demo checkout’s demo-partner — do not validate for binding. If a test binding silently fails, check the code shape first.

Compliance

The partner payout address is screened for sanctions before it is baked into a pool’s immutable init code, regardless of whether that partner has ever signed in. A referrer auto-created from a code may never have completed SIWE and therefore never been screened at sign-in. This is self-protection as much as compliance: a sanctioned or frozen address committed into an immutable pool permanently locks funds for everyone in that pool.

Partner acquisition

/partners is the public landing page. It sets setPartnerRoleHint("partner"), and siwe-verify auto-creates the referrers row on first sign-in, so there is no separate signup.

Latent bug worth remembering

merchants.name is NOT NULL with no default. The auto-create path in resolveRoles did not seed it, so the insert errored, the error was swallowed, and all new merchant self-onboarding silently failed. It now seeds name: "Merchant <0x6>". The lesson generalises: a swallowed insert error in an auth path produces a silent, total onboarding failure with no visible symptom.

Open gap

Partner earnings display $0.00 everywhere, and partner-visible merchants RLS is incomplete. Both are open from the 2026-07-28 audit. Any change to the fee split must carry a regression test asserting the partner’s 0.25% is computed on gross and unaffected — the dashboard will not catch a regression.