# Compose a campaign (/docs/composition)



A campaign works best when one thing a member does moves several things at once. This page builds a launch campaign where a single welcome quest does four jobs:

* **Earns XP.** The member gets 10 XP for completing it.
* **Unlocks the next quest.** A follow-up quest opens once welcome is done.
* **Pays the friend who invited them.** Completing welcome qualifies the referral, and the inviter gets 25 XP.
* **Moves the weekly leaderboard.** XP from the campaign's quests ranks members each week, and the top member wins 100 XP.

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

A new member joins the launch campaign, completes the welcome quest and sees 10 XP land. The next quest unlocks. The friend who invited them gets their own 25 XP. Both of them can watch the weekly leaderboard, and when the week ends your team approves the prize.

Your developer or coding agent builds the campaign in code, following the steps below. Once it's published, your team runs it from Console: arrange the quests in a collection, watch the standings, and approve the weekly prize.

## Before you start [#before-you-start]

Start with a [connected project](/docs/start/existing-app) or [new app](/docs/start/new-app) that has the SDK and CLI installed. Use its test environment and two real member accounts for the referral check.

## Choose the connections [#choose-the-connections]

One welcome completion drives every other part of the campaign:

```mermaid
flowchart LR
    W["Welcome quest"] -->|"10 XP"| L["Community ledger"]
    W -->|unlocks| N["Next-step quest"]
    W -->|qualifies| R["Friends referral"]
    N -->|"5 XP"| L
    R -->|"25 XP to inviter"| L
    L -->|"earned this week"| B["Weekly leaderboard"]
```

| Source                              | Connection              | Result                                                   |
| ----------------------------------- | ----------------------- | -------------------------------------------------------- |
| Welcome quest completion            | Quest reward            | The member earns 10 community XP.                        |
| Welcome quest completion            | Completion prerequisite | The follow-up quest becomes available.                   |
| Invited friend's welcome completion | Referral qualification  | Their inviter earns 25 community XP.                     |
| Community ledger entries            | Weekly earned score     | Both members' eligible earnings contribute to standings. |

> [!NOTE] Collections and claims in this example
> A collection can display both quests together. It does not enforce their dependency. This example uses a direct claim to show how the pieces connect; it does not check any activity outside Domino.

## Build the campaign [#build-the-campaign]

Define the shared campaign in `campaigns/launch.ts`:

```ts
import { defineCampaign } from "@domino-sdk/relay/authoring";

export const launch = defineCampaign({
	key: "launch",
	title: "Launch campaign",
	window: {
		from: Date.parse("2026-10-01T00:00:00Z"),
		until: Date.parse("2026-11-01T00:00:00Z"),
	},
	enrollment: "required",
});
```

Both quests below import this value as `launch` and set `campaign: launch`. The member enrolls once for both quests with `relay.campaigns.enroll({ actionId, campaign: launch.key })`. Use dates appropriate for your program.

### 1. Define the ledger [#1-define-the-ledger]

Save the typed ledger handle as `ledgers.ts`:

```ts
import { defineLedger } from "@domino-sdk/relay/authoring";

export const community = defineLedger({
	key: "community",
	name: "XP",
	description: "Community participation",
});
```

Merge these entries into `relay.json`, preserving the project's connection, app configuration, and existing resources. Keep authoring keys stable. If your starter already has a `welcome` quest, update that file instead of creating a second definition.

```json
{
  "pointLedgers": [
    { "entry": "ledgers.ts", "exportName": "community" }
  ],
  "discover": {
    "quests": ["quests/**/*.quest.ts"],
    "questTypes": ["quests/**/*.quest-type.ts"]
  },
  "referrals": [{ "entry": "referrals/friends.ts" }],
  "leaderboards": [{ "entry": "leaderboards/weekly.ts" }]
}
```

Rewards, referrals, and the leaderboard import the same ledger handle.

> [!DEV]
> Each environment resolves the ledger's key to its own opaque ledger ID. See [match ledgers in your app](/docs/concepts/points#match-ledgers-in-your-app).

### 2. Award points for the welcome quest [#2-award-points-for-the-welcome-quest]

Save this as `quests/welcome.quest.ts`:

```text
import { defineQuest, settings, manual, once, points } from "@domino-sdk/relay/authoring";
import { community } from "../ledgers";
import { launch } from "../campaigns/launch";

export const welcome = defineQuest({
	key: "welcome",
	title: "Welcome to the community",
	campaign: launch,
	settings: settings.object({}),
	presentation: () => ({ instructions: "Claim your welcome XP." }),
	trigger: manual(),
	completion: once(),
	rewards: [points(community, 10)],
});

export default welcome;
```

Domino records completion and awards the points. Your app displays that result through the [participant API](/docs/reference/participant-api).

### 3. Unlock the next activity [#3-unlock-the-next-activity]

Save this as `quests/next-step.quest.ts`:

```text
import { defineQuest, settings, manual, once, completed, points } from "@domino-sdk/relay/authoring";
import { community } from "../ledgers";
import { launch } from "../campaigns/launch";
import { welcome } from "./welcome.quest";

export const nextStep = defineQuest({
	key: "next-step",
	title: "Take the next step",
	campaign: launch,
	settings: settings.object({}),
	presentation: () => ({ instructions: "Claim after completing the welcome quest." }),
	requires: [completed(welcome)],
	trigger: manual(),
	completion: once(),
	rewards: [points(community, 5)],
});

export default nextStep;
```

This checks completion of a particular quest. It does not check whether the member currently holds 10 XP. Spending points does not erase that completion.

Keep a manual trigger when the member must take another action. For an automatic milestone after prerequisites are met, use `automatic()` instead. For consecutive days, use a [streak](/docs/concepts/quests#calendar-day-streaks). For several separate actions, such as three different posts, use a [distinct-action counter](/docs/concepts/quests#distinct-action-counters). `completed(...)` means ever completed.

### 4. Qualify an invitation from the same completion [#4-qualify-an-invitation-from-the-same-completion]

Save this as `referrals/friends.ts`:

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

export default defineReferral({
	key: "friends",
	title: "Invite a friend",
	qualification: questCompleted(welcome),
	rewards: { inviter: [points(community, 25)] },
});
```

Wire invitation-code acceptance through sign-in using [the referral guide](/docs/build/referrals). A quest completion alone does not identify an inviter. Accept the invitation before the friend completes any other quest; completing the welcome quest first is fine, since it qualifies the referral.

The invited friend earns the quest's 10 XP. Their inviter receives a separate 25 XP award when the referral qualifies. The friend's award is not reduced.

### 5. Rank weekly earnings [#5-rank-weekly-earnings]

Save this as `leaderboards/weekly.ts`:

```text
import { defineLeaderboard, earned, points } from "@domino-sdk/relay/authoring";
import { community } from "../ledgers";
import { nextStep } from "../quests/next-step.quest";
import { welcome } from "../quests/welcome.quest";

export default defineLeaderboard({
	key: "weekly",
	title: "This week's contributors",
	ledger: community,
	score: earned({
		window: { kind: "calendar", period: "week" },
		quests: [welcome, nextStep],
	}),
	settle: {
		at: "window-end",
		ties: "shared",
		prizes: [{ ranks: [1, 1], rewards: [points(community, 100)] }],
	},
});
```

The week starts on Monday in the [project calendar](/docs/concepts/calendar)'s timezone. Set the project timezone once, in `relay.json` as `"calendar": { "timeZone": "Europe/Budapest" }`; streaks and budgets follow it too. The quest filter makes this a campaign ranking: referral awards do not count. Spending does not reduce an earned score; corrections can. See [Choose what counts](/docs/build/leaderboards#choose-what-counts).

Render standings with [the leaderboard reads for members](/docs/build/leaderboards#read-standings-in-your-app). The board reads existing points; no extra score write is needed.

### 6. Publish to test [#6-publish-to-test]

```sh
pnpm exec domino deploy --preview --environment test --json
pnpm exec domino deploy --environment test --json
```

Arrange the quests in a collection through [Console](/docs/use/quests#arrange-collections) if your app displays collections.

## Verify the connections [#verify-the-connections]

Sign in as two members of the same test project, Alice and Bob, then check each connection in order:

| # | As    | Do                                                                       | Expect                                                                           |
| - | ----- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| 1 | Alice | Sign in and obtain her invitation code through your app's referral flow. | An invitation code.                                                              |
| 2 | Bob   | Sign in, enroll in `launch`, and accept Alice's invitation.              | Welcome is available; before Bob completes it, the next-step quest is locked.    |
| 3 | Bob   | Complete welcome.                                                        | A single completion, a 10 XP award, and availability of next-step.               |
| 4 | —     | Check the referral summary.                                              | Bob is qualified, and Alice has a separate 25 XP referral award.                 |
| 5 | —     | Read weekly standings.                                                   | Bob has 10. Alice's referral award is outside the board's campaign-quest filter. |
| 6 | Bob   | Refresh the app and revisit welcome.                                     | Completion and points persist without a second award.                            |
| 7 | Bob   | Complete next-step.                                                      | His earned score becomes 15 under the same conditions.                           |

> [!TIP] Totals differ from the table?
> Existing test data persists between development sessions, and restarting the app does not reset the ledger. Inspect point history if totals differ.

Follow [publishing](/docs/build/publishing) when the test behavior is ready for live.

## Approve the weekly prize [#approve-the-weekly-prize]

At the end of the week, Console **Leaderboards** shows a frozen prize settlement. Review its eligible winner and select **Approve and deliver**, then confirm, to grant the 100-point prize. Equal first-place scores share the rank and each receive the prize. See [Settle prizes after a window](/docs/build/leaderboards#settle-prizes-after-a-window) for review, API approval, and correction.

## Extend the campaign [#extend-the-campaign]

- [Quest types](/docs/concepts/quests#quest-types-and-settings) — When operators need multiple configurable activities with shared behavior.
- [Verified identities](/docs/concepts/participants#verified-identities) — When verification needs a provider account or a wallet.
- [Reward delivery](/docs/concepts/rewards#completion-and-delivery) — For Discord roles, show delivery separately from completion.

## Add external content [#add-external-content]

Want members to post about the launch on X too? An [X tracker](/docs/build/trackers) collects launch posts, and a quest checks each one against the member's connected X account, with its own approval and engagement thresholds. Its reward can feed the same XP, leaderboard and referral used above. Collecting posts awards nothing until a quest acts on them. X doesn't allow rewards for posting, so require a group of creators you approve on these quests. Domino doesn't enforce this: a deployment with a quest that rewards X posts from anyone still publishes, with a warning. See [UGC programs](/docs/concepts/ugc).
