# Affiliate Program

The Spaniel Syndicate affiliate program pays referrers a commission
on each primary product purchase (single pull, pack, box, crate,
vault) and on each official-marketplace trade attributed to their
referral link.

Base affiliate rate: **6.9%**. Scales quadratically with verified
**Affiliate Weight** (Spaniels + cards held, weighted by draw-slot
units — see §2) up to a direct-affiliate max of **69%** at
**1,000,000** Affiliate Weight. Buyers arriving via a valid
Barklink receive a **10% buyer discount** on the subtotal (post
bundle discount).

This is the public-facing contract. Canonical math:
[`js/lib/referral.js`](../js/lib/referral.js). Tests:
[`tests/referral.test.mjs`](../tests/referral.test.mjs).

---

## 1 — Affiliate identifier (Barklink)

System-generated. Anyone can create one.

- Format: `bark_<8 chars>` from the unambiguous alphabet
  `a-z` (minus `o, l, i`) + `2-9` (minus `0, 1`) — 31 chars total.
- One wallet → one permanent affiliate ID.
- No custom slugs. No editing. No deletion.
- URL-safe, lowercase.
- Self-referral is allowed; all pricing math accounts for it.

See [`js/lib/affiliate-id.js`](../js/lib/affiliate-id.js).

### Landing routes

Canonical:

```
/bonus/<affiliateId>
```

Aliases (all preserve query params and resolve to the canonical
experience):

- `/psssst/<id>`
- `/psst/<id>`
- `/deal/<id>`
- `/coupon/<id>`

Router lives in `404.html` (GitHub Pages 404 SPA fallback).

---

## 2 — Direct affiliate rate formula (1,000,000 Affiliate Weight to 69%)

```
affiliateWeightClamped = min(max(affiliateWeight, 0), 1_000_000)
referralBps = 690 + floor(6210 * affiliateWeightClamped^2 / 1_000_000^2)
```

Constants:

| Symbol | Value |
|---|---:|
| `BASE_AFFILIATE_BPS` | 690 (6.90%) |
| `MAX_AFFILIATE_BPS` | 6900 (69.00%) |
| `MAX_AFFILIATE_BOOST_WEIGHT` | 1,000,000 |
| `AFFILIATE_BOOST_BPS` | 6210 |

**Affiliate Weight** is the unit that drives the rate curve. Series 1
ships **1,000,000,000** cards on a public curve, so the top 69% rate
intentionally requires meaningful commitment (not the prior
1,000-Spaniel threshold from the retired 11.86M-print-run era).

Affiliate Weight per holding (mirrors Rescue Weight in
`js/lib/rescue.js`):

| Holding | Weight |
|---|---:|
| revealed card (any class) | 1 |
| sealed Single | 1 |
| sealed Pack | 10 |
| sealed Box | 240 |
| sealed Crate | 2,400 |
| sealed Vault | 24,000 |

Affiliate Weight is verified server-side from D1 (`revealed_cards` +
`sealed_products`) and via Helius DAS refresh. The client cannot
override; the Worker recomputes on every quote.

### Rate ladder (selected checkpoints — new 1M-weight scale)

| Affiliate Weight | Bps | Rate |
|---:|---:|---:|
| 0 | 690 | 6.90% |
| 1,000 | 690 | 6.90% |
| 10,000 | 690 | 6.90% |
| 100,000 | 752 | 7.52% |
| 250,000 | 1,078 | 10.78% |
| 420,000 | 1,785 | 17.85% |
| 500,000 | 2,242 | 22.42% |
| 690,000 | 3,646 | 36.46% |
| 999,999 | 6,899 | 68.99% |
| 1,000,000+ | 6,900 | 69.00% |

Old 1,000-Spaniel-to-69% threshold is retired. Tests at
`tests/referral.test.mjs`.

> **Packline:** Levels 2–5 of the upstream sponsor chain receive a
> separate set of override rates (4% / 3% / 2% / 1%) carved out of
> the post-rescue residual. See [PACKLINE.md](./PACKLINE.md).

---

## 3 — Buyer discount

Buyers arriving via a valid Barklink (i.e. a referral session is
active on the buyer's browser session) receive a 10% discount on
the subtotal:

```
discount    = floor(subtotalLamports * 1000 / 10000)
buyerPays   = subtotalLamports - discount
```

Where `subtotalLamports = stickerLamports - bundleDiscountLamports`
(see [`docs/PACKS_BOXES_CRATES.md`](./PACKS_BOXES_CRATES.md) §2).

The 10% buyer discount is **private**:

- Surfaced only on referral landing pages (`/bonus/<id>`).
- Not advertised on the homepage, product catalog, or affiliate
  dashboard.
- Independent of the affiliate's holder tier — whales don't unlock
  better buyer discounts.

---

## 4 — Full split (BigInt lamports)

For each product purchase:

```
P   = stickerLamports
B   = bundleDiscount        = P  * bundleDiscountBps / 10000
S   = subtotalLamports      = P - B

With valid referral:
D   = referralDiscount      = S  * 1000 / 10000   # 10%
BP  = buyerPaysLamports     = S - D
R   = rescueContribution    = BP * 690 / 10000    # 6.9% Rescue Pool
PR  = postRescueLamports    = BP - R
C   = affiliateCommission   = PR * affiliateRateBps / 10000
T   = treasuryLamports      = PR - C

Without valid referral:
D   = 0
BP  = S
R   = BP * 690 / 10000
PR  = BP - R
C   = 0
T   = PR
```

The Rescue Pool cut comes out of the buyer-paid amount before
affiliate commission. Affiliate commission and treasury share the
post-rescue residual.

Canonical implementation: `js/lib/products.js::computeProductQuote`.

For marketplace trades (secondary), see
[`docs/MARKETPLACE.md`](./MARKETPLACE.md) — a separate 2% fee where
6.9% of the fee goes to the Rescue Pool, and the post-rescue fee
splits 50/50 on valid referral attribution.

**Buy a fake dog. Help a real one.** The Rescue Pool contract lives
in [`docs/RESCUE_POOL.md`](./RESCUE_POOL.md).

---

## 5 — Attribution

A buyer is considered referred when:

1. They visit a `/bonus/<id>` (or alias) URL.
2. The Worker creates a `referral_sessions` row keyed by the
   browser's session cookie + buyer wallet.
3. The session has not expired (TTL: 30 days from last visit).
4. The session's `affiliate_wallet` is not the buyer's own wallet
   AND the affiliate is allowed under operator rules (self-
   referral allowed by default — see `js/lib/affiliate-id.js`).

The Worker resolves attribution at quote time. The on-chain program
verifies the affiliate wallet pinned in the signed quote matches
the wallet on the affiliate row; if the affiliate row is missing,
the program fails the tx with `InvalidReferral`.

---

## 6 — Holder count source

Spaniel/card holdings are computed by the Worker via Helius DAS
queries:

- Series 1 cards (cNFTs or Core NFTs — depending on the chosen
  standard, see [`docs/ONCHAIN_MINT_REQUIREMENTS.md`](./ONCHAIN_MINT_REQUIREMENTS.md)).
- Legacy Spaniel NFTs from the pre-Series-1 collection (if any
  exist; they count toward the affiliate's tier).
- Sealed products held by the affiliate do NOT count toward the
  holder tier — only revealed cards do. This is a deliberate
  choice: sealed products are inventory, not collection.

Holder count is refreshed on a schedule (every ~10 minutes) and on
every product quote that references the affiliate. The Worker stores
the most recent count + refresh timestamp; the affiliate dashboard
shows both.

---

## 7 — Dashboard surface

The affiliate dashboard at `/affiliate` shows for the connected
wallet:

- The affiliate's Barklink + canonical referral URL + aliases.
- Current affiliate rate (bps + percent).
- Verified Spaniel/card holdings + last refresh timestamp.
- Lifetime stats:
  - Product sales referred (singles, packs, boxes, crates, vaults
    broken out).
  - Total draw slots referred.
  - Total primary volume referred.
  - Total commission earned.
  - Marketplace trades referred.
  - Standing offer acceptances referred (if escrowed offers ship).
- Live clicks / sessions on referral landing pages.
- Conversion rates (sessions → purchases).
- Recent transactions referred.

Mandatory disclosure on the affiliate page:

> "Affiliate commissions are earned by referring buyers. Holding
> Spaniels may increase your affiliate commission rate, but
> Spaniels do not pay passive income, staking rewards, dividends,
> or guaranteed returns."

---

## 8 — Cross-references

- `js/lib/referral.js` — rate + split math.
- `js/lib/affiliate-id.js` — ID generation + validation.
- `js/lib/products.js` — composes referral into product quotes.
- `tests/referral.test.mjs` — invariants.
- `tests/affiliate-id.test.mjs` — id validation.
- `docs/CURVE_PRICING.md` — primary curve.
- `docs/MARKETPLACE.md` — secondary marketplace fee + split.
- `docs/WORKER_API_CONTRACT.md` — referral endpoints.
