# Referrals and inviter bonuses (/docs/build/referrals)



A referral program rewards members for bringing friends who go on to take part. For example:

* **"Invite a friend: you both get points"** once the friend finishes the welcome quest.
* **"Bring five friends, get a 250-point bonus."**
* **"Earn 10% of what your friends earn for 90 days"**, up to a cap.

Domino keeps track of who invited whom, decides when a friend counts, and pays both sides. Your app shows the invitation link and the results.

For the model behind this guide, read [Referral programs](/docs/concepts/referrals). See [Compose a campaign](/docs/composition) for a complete example.

## What members see [#what-members-see]

* **Their invitation link.** Each member has a code your app puts in a link they share.
* **A friend joins through the link.** The friend opens it and signs in. The first invitation a friend accepts is the one that counts.
* **Rewards once the friend qualifies.** When the friend does what the program asks, such as finishing a quest, the inviter and the friend get their rewards.
* **Their progress.** The inviter sees how many friends joined and qualified, their progress toward milestones, and their payouts.
* **Privacy for friends.** An inviter never sees their friends' identities, balances or activity.

```mermaid
sequenceDiagram
    autonumber
    actor Alice as Alice (inviter)
    participant App as Your app
    participant Domino
    actor Bob as Bob (friend)
    Alice->>App: Share link with her code
    Bob->>App: Open link, sign in
    App->>Domino: referrals.accept(code)
    Domino-->>App: Alice is Bob's inviter
    Bob->>Domino: Complete the qualifying quest
    Domino-->>Alice: Inviter reward and bonuses
    Domino-->>Bob: Invitee reward
```

## What you can do in Console [#what-you-can-do-in-console]

The program itself is defined in code. Once it's published, Console **Referrals** shows, for the selected project and environment, its programs, accepted invitations, who qualified, payouts, point corrections, and rewards that need review. You can:

* **Correct a referral** with a reason: suspend or resume payouts, qualify a friend or revoke their qualification, and clear a reward held for review. See [Correct, suspend, and resume](#correct-suspend-and-resume).
* **Manage inviter groups** in **Groups**, for programs only some inviters earn from.
* **Get started with a coding agent.** Until a program is published, the page offers ready-made requests, such as rewarding both sides or adding milestones. Choose **Copy prompt** and paste it into your coding agent; the prompt includes the project, environment and setup steps.

## Publish a referral program [#publish-a-referral-program]

Domino records one inviter per member in each project and environment.

> [!TIP] Test with two real accounts
> Check accepting an invitation and qualifying with two real member accounts in the same hosted test project. Development uses real sign-in and your configured providers; anonymous simulated previews are no longer available. Test keeps these points and referrals separate from live.

### 1. Define qualification and rewards [#1-define-qualification-and-rewards]

```text
import { defineReferral, points, questCompleted } from "@domino-sdk/relay/authoring";
import { community } from "../../ledgers";
import welcome from "../../quests/welcome.quest";
import weeklyActivity from "../../quests/weekly-activity.quest";

export default defineReferral({
  key: "friends",
  title: "Bring your friends",
  qualification: questCompleted(welcome),
  rewards: {
    inviter: [points(community, 25)],
    invitee: [points(community, 10)],
  },
  milestones: [
    { key: "five-friends", count: 5, rewards: [points(community, 250)] },
  ],
  bonuses: [
    {
      key: "ongoing",
      ledger: community,
      basisPoints: 1000,
      quests: [welcome, weeklyActivity],
      durationDays: 90,
      maxPoints: 1000,
    },
  ],
});
```

This program pays the inviter 25 points and the friend 10 once the friend finishes the welcome quest, 250 more at five friends, and 10% of what each friend earns from two quests for 90 days, up to 1,000 points. See [Choose eligible activity](#choose-eligible-activity) for each field.

### 2. Add the module to your project [#2-add-the-module-to-your-project]

Add the module to your existing `relay.json`:

```json
{
  "referrals": [{ "entry": "src/referrals/friends.ts" }]
}
```

> [!DEV]
> Entries can select a named export with `exportName`. `domino check` bundles referral modules, including in projects without quests. `domino dev` synchronizes referral definitions with the other project resources.

### 3. Preview and publish to test [#3-preview-and-publish-to-test]

```sh
domino deploy --preview
domino deploy --environment test
```

The first command shows the proposed definitions. The second publishes them to test.

### Live publication [#live-publication]

Publish to test directly with `domino deploy --environment test`. For live, the CLI prepares a request, and a person with publish permission approves it in Console. See [reviewed live publication](/docs/build/publishing#publish-behavior-to-live).

> [!IMPORTANT] Removing a program does not stop it
> A program you leave out of a later publication stays published. To stop new qualifications and bonuses, publish it with `enabled: false`.

## Choose eligible activity [#choose-eligible-activity]

### Qualification [#qualification]

Qualification is what a friend must do before anyone is paid.

| `qualification`           | The friend qualifies when…                                                  |
| ------------------------- | --------------------------------------------------------------------------- |
| `{ kind: "signup" }`      | They sign in and accept the invitation.                                     |
| `questCompleted(welcome)` | They complete the quest you name.                                           |
| `{ kind: "external" }`    | Your server tells Domino they qualify, after checking the condition itself. |

A member has one inviter, but each program decides on its own whether that friend qualifies. Each friend counts once per program. Completing a quest again, or your server qualifying them again, doesn't add another friend or pay fixed rewards twice.

* **Several actions.** Qualify through a quest that counts actions, such as three verified posts, and name that quest. See [qualification with counters](/docs/concepts/referrals#qualification-with-counters).
* **A condition that can be withdrawn.** Use `{ kind: "external" }`. Your server tells Domino the friend qualifies or no longer does, and Domino adjusts the friend's rewards, milestones and bonuses each time.
* **A one-time achievement your app checks.** Your app adds the member to a [group](/docs/concepts/groups) once it has checked the condition. An automatic quest that requires that group completes, and the referral qualifies on that quest. Removing the member from the group later leaves the friend qualified. As with any quest qualification, the friend accepts the invitation before completing any other quest (see [acceptance rules](#acceptance-rules)).

> [!DEV]
> For several actions, give the quest a counter, `completion: progress({ distinct, goal: count(3) })`, and pass it to `questCompleted`. For an external condition, your server calls `management.referrals.qualify` with `qualified: true` or `false`. For a group, qualify with `questCompleted` on an `automatic()` quest that `requires: [inGroup(group)]` and completes `once()`; qualification follows that quest's completion, not the group write.

### Fixed rewards and milestones [#fixed-rewards-and-milestones]

The inviter, the friend and each milestone can get points or tiered rewards. Badges, group memberships and Discord roles come from quests only. Tiered rewards use the same staff handover as quest rewards. Each referral reward is its own entitlement, so different qualified friends can earn separate items. Restoring a qualification reuses its original entitlement.

> [!IMPORTANT] Put Discord roles, badges and groups on a quest
> Referral rewards can be points and tiered rewards only. A deployment that puts a Discord role in an inviter, invitee or milestone reward fails with `Referral programs can grant points and tiered rewards; Discord roles need a quest`. A badge or a group membership there makes `defineReferral` throw, and a deployment refuse it, with `Badge rewards can only be granted by quests or manually, not by referral programs` or `Group membership rewards can only be granted by quests, not by referral programs`. Grant the role, badge or group from the qualifying quest instead, where Domino can tie its delivery to the member's attempt.

A referral program published by an older Domino release may still contain a Discord role. Domino keeps that program readable so an operator can archive it. When a friend qualifies, it skips the role and records `referral-reward-skipped` with the reason in **Audit history**; the qualification and the program's other rewards still go through. Publish a replacement program with points or tiered rewards, or move the role to the qualifying quest.

### Percentage bonuses [#percentage-bonuses]

A bonus gives the inviter a percentage of what their friend earns, on top of the friend's own points. The friend never loses anything.

> [!NOTE] Worked example
> At 10% (`1000` basis points), when Bob earns 100 eligible points, Bob keeps 100 and Alice receives 10 extra points. The qualifying quest's own award counts. Points Bob earned before qualifying don't earn a bonus. Fixed referral rewards and bonus payouts never trigger further bonuses.

| Field          | Meaning                                                                        |
| -------------- | ------------------------------------------------------------------------------ |
| `ledger`       | The point ledger the bonus reads and pays. One per bonus.                      |
| `basisPoints`  | The rate. `1000` basis points means 10%.                                       |
| `quests`       | The quests that count. Leave it out to count every quest award to that ledger. |
| `external`     | `true` also counts awards from your own server that opt in.                    |
| `durationDays` | Optional. Starts when the friend first qualifies. Suspensions don't extend it. |
| `maxPoints`    | Optional. Caps the bonus for that friend and rule.                             |

* **Engagement rewards count.** A quest's earnings include its completion award and the daily payouts of an [engagement rule](/docs/concepts/ugc#engagement-credits-growth) that reads it. A bonus in the same ledger that covers the qualifying quest, or names no quests, earns on each engagement payout. The usual limits apply: the friend must be qualified and not suspended, the program's inviter group, `durationDays` and `maxPoints` still apply, only whole points pay out, and an outstanding correction takes the payout first.
* **Leaderboard prizes don't count.** Prize points aren't quest earnings and earn no bonus, even from a bonus that names no quests. The points that won the prize already earned theirs.
* **Fractions add up.** Partial points build up per friend, rule and balance. Ten awards of 3 points at 10% eventually pay 3 whole points.

> [!WARNING] Overlapping rules are rejected
> Two percentage rules for the same balance and source can't both exist, even in separate programs. Named groups don't keep them apart, because a member can be in both. Fixed rewards, milestones and a percentage bonus can coexist.

### Inviter groups [#inviter-groups]

A program can pay only inviters in one [group](/docs/concepts/groups), such as ambassadors. Friends of other members can still accept invitations and qualify, but neither side earns anything from that program: no inviter or friend reward, no milestone and no bonus.

* **Groups are checked at the time.** Domino checks the inviter's group when a friend qualifies and when the friend's points are accepted. It doesn't pay missed rewards or bonuses after the inviter joins.
* **Publishing creates the group.** If the environment lacks the group, publishing creates it empty. Manage its members in Console **Groups**.

> [!DEV]
> Set `inviterGroup` on the program, next to `qualification`, and pass a group handle from `defineGroup`. Manage its members from code with `management.groups`.

## Share and accept invitations [#share-and-accept-invitations]

Your app shows each member their link and accepts the invitation for the friend after they sign in. The friend is always the signed-in member, never someone named in the request. You can also offer a box to type a code by hand. Your app decides how it all looks; Domino doesn't need a separate confirmation screen.

```ts
import { createBrowserRelayClient } from "@domino-sdk/relay/browser";

const domino = createBrowserRelayClient({ baseUrl: "/relay" });
const summary = await domino.referrals.summary();
const invitation = new URL("https://app.example.com/join");
invitation.searchParams.set("ref", summary.code);

console.log(invitation.toString());
console.log(summary.attributed, summary.programs, summary.accounts);

// On the invited friend's landing page, retain the code through sign-in.
const code = new URL(window.location.href).searchParams.get("ref");
if (code) sessionStorage.setItem("pending-referral", code);

// After sign-in, before any quest other than the program's qualifying quest completes:
const pending = sessionStorage.getItem("pending-referral");
if (pending) {
  await domino.referrals.accept({ code: pending, actionId: "accept-invitation" });
  sessionStorage.removeItem("pending-referral");
}

const history = await domino.referrals.history();
if (history.nextCursor) {
  const older = await domino.referrals.history({ cursor: history.nextCursor });
}
```

> [!DEV]
> Use your configured participant proxy or existing-app identity bridge at `/relay`. A member session is required. A manual code input uses the same `accept` call.

### Acceptance rules [#acceptance-rules]

* **Accept early.** Accept the invitation during onboarding. Completing any quest closes the window, except a referral program's qualifying quest: a friend who checked in, or claimed the entry quest at sign-in, can still accept, and accepting qualifies them in that program and pays both sides. Points the friend earned before accepting don't earn the inviter a percentage bonus. A revoked qualifying completion doesn't qualify. A leaderboard prize is an operator accounting record and doesn't close this window.
* **First inviter wins.** Opening another invitation later can't replace them.
* **Invalid codes are rejected.** This includes inviting yourself, invitation loops, and codes from another project or environment.
* **Retries are safe.** Repeating `accept` with the same action ID and payload after a timeout returns the original result.

Keep the code until you know the result. Show a clear error for an invalid code or a closed invitation window.

### What the reads return [#what-the-reads-return]

| Call        | Contains                                                                                                                                                                                                                                                                                                                                                               |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `summary()` | The inviter's code, how many friends accepted, qualified counts and milestone progress per program, and point accounts. Accounts separate earned amounts, settled amounts and outstanding corrections. Earned amounts leave out referral payouts an operator reversed. A settled amount can include points that paid off a correction instead of reaching the balance. |
| `history()` | Paginated payout history, `{ items, nextCursor }`. Pass `nextCursor` as the next call's `cursor`.                                                                                                                                                                                                                                                                      |

Neither response shows invited friends' identities, balances or detailed activity.

> [!DEV]
> Programs, rewards, accounts and history name ledgers and programs by ID. Find your program by `definition.key`, and match ledgers through `relay.pointLedgers.list()` ([Match ledgers in your app](/docs/concepts/points#match-ledgers-in-your-app)). Comparing a returned `balance` or `program` with a key never matches.

## Award external points and qualify a friend [#award-external-points-and-qualify-a-friend]

Your own server can qualify friends for programs that use `{ kind: "external" }`, and award points that count toward bonuses. It uses a server-side management key scoped to the project and environment.

| Operation                    | Required scope |
| ---------------------------- | -------------- |
| External point writes        | `points`       |
| Qualification and moderation | `publish`      |
| Reads                        | `read`         |

A hosted app's own key (`DOMINO_MANAGEMENT_KEY`, see [Call the management API from your server](/docs/build/hosted-app#call-the-management-api-from-your-server)) covers point writes and reads, but not qualification, which needs `publish`. To qualify from a hosted app, store a key with `publish` as an [app secret](/docs/build/hosted-app#set-app-secrets) and use it for those calls, as the example below does. An [automation key](/docs/agents/automation) with Publish works in test; a personal API key with `publish` can qualify and moderate in live too. **Audit history** records each change under the key: its owner for a personal key, the automation key itself for an automation key.

> [!WARNING] `resolveReward` releases rewards
> `management.referrals.resolveReward(entitlement, { actionId, reason })` is Console's **Mark reconciled**. It clears the review block on a referral reward, so an entitlement held for review can be handed over again. Any key with `publish` can call it in live. Give that permission only to automation you trust to release rewards.

```ts
import { createManagementClient } from "@domino-sdk/relay/server";

const scope = {
  baseUrl: "https://relay.domino.run",
  organization: "my-organization",
  project: "community",
  environment: "test",
} as const;

// Qualification needs a key with publish, stored as an app secret.
const publisher = createManagementClient({
  ...scope,
  getToken: async () => process.env.REFERRALS_PUBLISH_KEY!,
});

// For a program whose qualification kind is external:
await publisher.referrals.qualify("friends-external", "member_existing", {
  actionId: "verified-referral-42",
  qualified: true,
  reason: "Verified the required external activity",
});

// The hosted app's own key covers point writes.
const management = createManagementClient({
  ...scope,
  getToken: async () => process.env.DOMINO_MANAGEMENT_KEY!,
});

await management.points.record({
  kind: "award",
  actionId: "external-activity-42",
  member: "member_existing",
  balance: "community",
  amount: 100,
  referralEligible: true,
  reason: "Verified external activity 42",
});
```

> [!IMPORTANT] External awards need two opt-ins
> Awards from your server earn no bonus by default. They need both `referralEligible: true` on the award and `external: true` on a matching rule.

Adjustments, absolute-total updates, spending and referral rewards never generate bonuses. Reuse the source event's action ID and payload for retries. A source award and its inviter bonus are saved together, so a failed bonus never leaves a half-saved award.

## Correct, suspend, and resume [#correct-suspend-and-resume]

Select your project and **Test** or **Live** in the Console header, then open **Referrals**. Select an invited friend in the invitations list, or search for anyone by name, email or ID in **Find a participant**. Their details open beside the list, with their programs, payouts and payout history. Enter a **Reason**, then choose:

| Where                            | Action                   | Effect                                                                                                                     |
| -------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| **Referral payouts**             | **Suspend**              | Keeps the invitation and stops future payouts to both sides.                                                               |
| **Referral payouts**             | **Resume**               | Restarts payouts. The suspended period is not paid afterwards.                                                             |
| Each program                     | **Qualify** / **Revoke** | Qualifies the friend for that program, or revokes their qualification.                                                     |
| A reward marked **Needs review** | **Mark reconciled**      | Clears the review block once staff have resolved the original reward. It doesn't issue, delete or reclaim a physical item. |

Corrections need publish access. Select an inviter to see their payout history.

Suspending covers one invitation. To stop a farmer everywhere, [ban the member](/docs/use/member-bans): no referral payouts reach them or come from their points, and their invitation code stops working. The ban can also reverse the referral payouts they already received, following the rules below.

### How corrections work [#how-corrections-work]

* **Reversals use the original rate.** A reversed bonus uses the rate recorded with the original award.
* **Revoking qualification undoes its rewards.** Fully reversing the qualifying quest's award also revokes the qualification. Staff can revoke it themselves when other evidence turns out to be invalid. The friend stops counting as qualified, their past percentage bonuses are corrected, and affected fixed rewards and milestones are adjusted. Qualifying again doesn't replay earlier percentage earnings.
* **Balances never go negative.** A correction takes back available points right away without touching points held for a redemption. Anything left over comes out of future referral payouts in the same balance; ordinary future earnings aren't taken. Bob's reversal can go through even after Alice spent her bonus, as long as Bob's own balance covers his reversal.
* **A reversed payout stays reversed.** If an operator reverses all or part of a referral payout from point history, Domino writes those points off on the referral account. Later payouts never pay them again, whatever reconciles the account, and a later qualification correction doesn't take them a second time. Earnings that come after the reversal still pay: if a percentage bonus grows, the inviter receives only the growth. Revoking and restoring the qualification doesn't bring the reversed points back; to pay them again, award them by hand. Reusing the reversal's action ID returns its first result, and a new reversal can't exceed what's left of the payout.
* **Restoring reuses the original entitlement.** When a qualification or milestone is restored, Domino first cancels its outstanding correction, then restores amounts already taken back. It never grants a second copy of a delivered tiered reward. Invalidated tiered entitlements can't be handed over until restored or reviewed by staff.

### Editing a published program [#editing-a-published-program]

Changes to a program apply to actions accepted from then on. Backdated point awards use the current rules and eligibility; their effective date still counts for leaderboards.

> [!CAUTION] Keep IDs stable
> Keep program, bonus and milestone keys stable. Domino maps them to stored IDs; fixed rewards already issued keep their original payload and milestone threshold.

> [!DEV]
> Generated referral record IDs are the `ref_` prefix followed by 22 random alphanumeric characters. Treat the full value as opaque; existing record IDs stay valid. Invitation codes are separate from record IDs.

## Qualify referrals through Discord membership [#qualify-referrals-through-discord-membership]

A program can count a friend once they join your Discord server: name the [Discord membership quest](/docs/build/integrations#check-that-someone-joined-your-server) as the qualification, `qualification: questCompleted(discordMembership)`. Publish that quest in the same project first.

This uses Domino invitation codes and counts each friend once. It doesn't track Discord invite links or read Discord's invite counts.

Referral `balance` values are stable point ledger IDs. Find the ID in Console **Rewards → Point ledgers**. Two ledgers can share a display name, so use descriptions and IDs to tell them apart. See [point ledgers](/docs/build/leaderboards#choose-a-point-ledger).
