# Leaderboards, contests, waitlists, two-tier

A leaderboard inside one program, contests with prizes a person awards, milestones that give rewards, a waitlist people move up, and a fenced second tier.

Campaigns keep promoters coming back: a leaderboard inside one program, contests with prizes,
milestones that give rewards, a waitlist people move up by inviting friends, and, if you want it,
a second tier for partners who recruit partners. Every one of them counts only what really
happened, and none of them pays anybody by itself.

## Leaderboards

A program's leaderboard ranks its promoters by one of four things:

| Metric | What it counts |
|---|---|
| `customers` | Paying customers a promoter brought |
| `sales` | Payments those customers made |
| `revenue` | The amount of those payments, in the program's currency |
| `friends` | On a waitlist, friends who joined and confirmed their address |

Over one of four windows: this calendar month (UTC), the last 30 days, this calendar quarter, or
since the program began.

**What counts is counted activity, never a click.** A sale counts once no person or rule holds
it, and stops counting if it is refunded in full or disputed. A promoter with an open fraud flag
stays on the board, marked "Under review" to you and shown to nobody else as anything but a
name. Clicks never count: a click costs nothing to fake.

**You see everybody; promoters see a little.** `GET /v1/referrals/programs/{id}/leaderboard`
answers your view: every place, with the promoter's membership id, their name and, for one of
your own users, your `external_id`. Promoters see the first places only (10 by default, up to
100), on their program page and, if you switch on its card, in the in-app panel:

- each place named as that promoter chose: **hidden** ("A promoter", the default for partners),
  **initials** ("J. D.", the default for your own users, whose names you supplied) or **their
  display name**;
- the counts, never money, unless you choose to show amounts;
- their own place, even when they are hidden or below the list;
- no id of anybody, and nothing outside the program: there is no public leaderboard and no
  board across programs.

A board is counted again at most every 10 minutes. Set it on **Referrals, your program,
Campaigns**, or with `leaderboard` on `PATCH /v1/referrals/programs/{id}`:

```json
{ "leaderboard": { "metric": "customers", "period": "month", "show": "counts", "size": 10, "panel": true } }
```

`"leaderboard": null` switches off what promoters see. You always see your own.

## Contests

A contest is a leaderboard with an end and prizes:

- **a window**, up to a year;
- **a metric**, one of the leaderboard's four;
- **who may enter**: every promoter, or one group or level;
- **a minimum** a promoter must reach to win anything;
- **prizes by place**: cash (paid as a bonus by hand) or a reward that is not cash (credit, a gift
  card, a free period or a perk), for one place or a run of them ("2nd to 5th").

Only what happens inside the window counts. A program may have 10 contests scheduled or running
at once. Before it starts you may change anything; once it has started only its name and
description change, so its window, what it counts, who may enter and its prizes stay as promoters
saw them.

**A person awards the prizes.** When a contest ends its standings stop taking new activity, and
refunds and confirmed fraud still drop out. Award prizes is refused, with the reason, while any
winner is under review and until the last hold on what the winners counted has passed (the
contest's `award.awardable_at`). Then it pays once: a cash prize becomes a bonus paid by hand,
"Contest: Spring push, 1st place", approved at once and paid with the next payout; any other
prize becomes a reward ready to give, on the Grants tab. Awarding needs the
`referrals.payouts` permission, and over the API a key that holds `referrals:payouts` (an AI
assistant asks a person instead). `referral.contest.ended` tells your webhook a contest is over;
`POST /v1/referrals/contests/{id}/award` gives the prizes.

**A sudden run of sales just before a contest ends counts against it.** The risk engine adds a
reason ("12 conversions in a contest's last 48 hours, against a usual 2 for that long") and 30
points to each of those sales' risk score: with any other reason (a new partner's is enough) that
reaches your hold line, and the money waits longer. It never rejects anything, and the prizes wait
for every counted sale's hold anyway. You decide.

An AI assistant always asks a person before it awards or cancels a contest, and the person who
approves an award may not be one of its winners.

## Milestones that give rewards

A milestone reward rule ("after 10 paying customers this month") may give credit, a free period,
a perk or a gift card instead of cash. It is given once per window, like a cash milestone, as a
grant: it waits out the program's hold, then appears on the Grants tab ready to give, and a
refund that puts the promoter back under the line takes it back while it waits.

On a waitlist, a milestone may count `friends`: "invite 3 friends for early access". The panel
shows each person's progress: "2 of 3 friends joined: one more for early access".
`referral.milestone.reached` tells your webhook.

## Waitlists

A waitlist program (`kind: waitlist`) is a launch campaign: people join a line, each gets a link
to invite friends, and every friend who joins and confirms moves them up.

- **A place** is the join order, moved up `bump` places (5 by default) for each confirmed friend.
  Ties go to whoever joined first. It is shown to the person as "You are number 1,204; invite
  friends to move up" and is computed when asked, never kept as a public list.
- **Confirmed** means the friend confirmed their address. Until they do they count for nothing,
  and a flood of entries from one address, or a person referring themselves, moves nobody.
- **A status** of `waiting`, `invited` or `joined`, for your own record.

**Two ways in.**

1. **Your own sign-up form.** Your server calls `POST /v1/referrals/programs/{id}/entries` with
   your id for the person (`external_id`) and the referral code they arrived with, and later
   `PATCH /v1/referrals/entries/{id}` with `verified: true` once they confirmed their address.
   BoltClaw stores no address: an `email`, when you send one, is kept only as a keyed
   fingerprint for the self-referral check.
2. **The hosted waitlist page**, for a business with no backend. Switch `waitlist.page` on and
   the program gets a page at `boltclaw.ai/r/w/<token>`: your name, logo and pitch, one email
   field and a button. The address is unguessable, never indexed, and you can make a new one at
   any time. BoltClaw then holds the entrants' addresses for you, as your processor:
   - sealed at rest, never in `/v1`, never in a webhook body;
   - one confirmation email, from BoltClaw in your brand, with a fixed subject; nothing else
     unless you invite them;
   - exported by you as CSV from the dashboard, after a recent sign-in (recorded in the activity
     log): an address only for a person who confirmed it;
   - deleted when you end the waitlist and choose to forget them, 90 days after it ends, or 14
     days after the person joined if they never confirmed.

   The page asks the browser to solve a small puzzle before it takes an address, refuses
   throwaway domains, takes at most a few sign-ups a minute from one address, and answers the
   same way whether or not an address was already on the list. One address is sent at most three
   confirmation emails a day from every waitlist together, and one page sends at most 500 an hour.
   `jane+news@example.com` and `jane@example.com` are one entry, as are Gmail's dotted spellings.
   A page can be switched on by a person in the dashboard, or by your own server, once your
   organisation is three days old; an AI assistant cannot switch it on.

Entries are your own users in the in-app panel: the panel shows a person their place, their
friends and their milestones instead of commissions. They count toward your plan's referrers,
never its partners.

**Inviting.** `POST /v1/referrals/programs/{id}/entries/invite` marks up to 500 entries `invited`
and emails your message to the ones who joined on the hosted page and confirmed their address
(their addresses never leave BoltClaw); a hosted entry nobody confirmed is not invited, and an
entry from your own form is marked only, for your own system to email. A waitlist can be invited
five times a day. An AI assistant always asks a person first, and before it disqualifies somebody.

**Closing and ending.** `waitlist.closed` on the program stops new entries for a while; everybody
keeps their place, and you can open it again. `POST /v1/referrals/programs/{id}/waitlist/end` ends
it for good: nobody new joins, by the page or by your form, and the people in keep their places and
can still be invited; the addresses the hosted page holds are deleted 90 days later.
`POST /v1/referrals/programs/{id}/waitlist/forget` deletes them now, and ends the waitlist too. An
AI assistant always asks a person before either, and the person reads which one it is.
`POST /v1/referrals/programs/{id}/waitlist/rotate-page` gives the hosted page a new address and
stops the old one (referral links keep working); an AI assistant always asks a person first,
because it stops the address you published.

`referral.waitlist.entry_created` (for a hosted entry, once its person confirmed) and
`referral.waitlist.position_changed` (at most once an hour per entry) tell your webhook, each by
`external_id` and never an address.

## Two-tier

A second tier pays a partner a share of what the partners they recruited earn. It is **off by
default**, and it is fenced, on purpose:

- **One level only.** A recruiter earns from their recruits and never from their recruits'
  recruits. There is no third tier.
- **At most 20%** of the recruit's own commission, for **1 to 24 months** after the recruit
  joined, on cash commissions on sales only: never on a bonus, a reward that is not cash, or a
  contest prize.
- **Never on a partner's own purchases.** A recruit who looks like the recruiter themselves (the
  same email when they joined, or the same payout details) is flagged, and the share waits for a
  person to decide.
- **Only real sales pay it.** A recruiter's share follows the recruit's commission: held as long
  as it is held, reversed when it is reversed, and refused if the sale is fraud.
- **It is written into your terms.** Switching it on adds a paragraph to the program's terms
  saying exactly this, and every approved partner is asked to accept the new version before their
  next payout.

Why the fence: a program that pays people mainly for recruiting other people, at many levels, is
what regulators call a pyramid or a multi-level marketing scheme. One level, a small share and
real customer sales keep a referral program on the right side of that line. Ask your own lawyer
if you are unsure.

A recruiter shares a recruit link (their own link with `?by=` and their code) from their program
page; a partner who joins through it is recorded as theirs only while the second tier is on.
Turn it on from **Referrals, your program, Settings**, or with
`PATCH /v1/referrals/programs/{id}/second-tier`:

```json
{ "enabled": true, "percent_bps": 1000, "months": 12 }
```

A second-tier commission carries `tier: 2` and says why: "10% of a recruit's 30.00 commission
(second tier)". Switching the second tier on is always asked first for an AI assistant;
switching it off is a stop and asks nobody (what was earned stays).

<!-- https://boltclaw.ai/docs/referral-campaigns as markdown. Generated from openapi.yaml; API version 1.22.0. -->
<!-- Every /docs page answers as markdown: add .md to its path, or send Accept: text/markdown. -->
