# Manage quests (/docs/use/quests)



Use Console to run the quests in your app day to day: add a quiz or a photo quest, put quests in the order members see them, change a setting, and pause a quest when a campaign ends. Your edits wait as unpublished changes until you publish them.

A quest is one activity, such as "Answer three questions about our launch", with what it takes to complete it and what it rewards. A collection is an ordered list of quests your app shows in one place.

| You want to…                         | Do this                           | Progress                           |
| ------------------------------------ | --------------------------------- | ---------------------------------- |
| Show the same quest in two places    | Put the quest in both collections | Shared between the two collections |
| Start a separate copy of an activity | Duplicate the quest               | New identity with fresh progress   |

## Create a quest [#create-a-quest]

In Console you create quests from the kinds your app already supports, such as a quiz, a photo or a one-tap claim. New kinds of quests and new collections come from your project's code: ask your developer for them.

### 1. Open Quests [#1-open-quests]

In Console, open **Quests**. Quests are listed under the collection they appear in.

### 2. Add a quest to a collection [#2-add-a-quest-to-a-collection]

Choose **Add** on a collection, then pick a type. The quest is created in that collection and opens with its title selected.

The app must support the quest's interaction, such as a photo, quiz, or claim. Types your app does not support yet cannot be chosen.

### 3. Set title, settings, and prerequisites [#3-set-title-settings-and-prerequisites]

The quest opens with **How it works**, a summary of the rules its type sets. Everything below it can be changed in Console.

Fill in the quest's title and **Settings**, then any of these:

* **Badge rewards** and **Prerequisites**.
* **Retry cooldown**, for a quest members submit: the minutes a member waits to try again after a rejection. Leave it empty for none, or choose **Reset** to use the type's value. A new quiz starts at 5 minutes. See [retry cooldown](/docs/concepts/quests#retry-cooldown).
* **Completion cooldown**, for a quest members submit and can complete more than once: the minutes a member waits to submit again after a completion is accepted. See [completion cooldown](/docs/concepts/quests#completion-cooldown).
* **Presentation**: an image and alt text, ordered steps with optional HTTPS links, and hints about which account or wallet to link. These hints describe what to link; the quest's rules still decide who qualifies. For a new quest, or after changing its type or settings, choose **Load presentation** first to read the content those settings give.

Console previews an image with an HTTPS address. A path beginning with `/` is served by your app, so Console does not preview it. Console saves as you go.

### 4. Review and publish [#4-review-and-publish]

The new quest stays unpublished until you publish it. See [Review and publish](#review-and-publish).

## Arrange collections [#arrange-collections]

| Action                                                                                                      | Effect                                                                                                                                          |
| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Drag a quest by the grip on its left                                                                        | Reorders the collection.                                                                                                                        |
| Drag a quest onto another collection                                                                        | Moves it there. A collection cannot list the same quest twice, so that drop is ignored.                                                         |
| **⋯** menu → **Move up** / **Move down**                                                                    | Reorders without dragging.                                                                                                                      |
| **⋯** menu on a quest                                                                                       | Shows the quest in another collection, or removes it from this one.                                                                             |
| Open a quest, then under **Collections** choose **Add to a collection*&#x2A;, or &#x2A;*×** on a collection | Adds or removes the quest. Each collection shows the quest's position, such as **4 of 5**. **Undo** puts back a removal you have not published. |

The &#x2A;*⋯** menu beside a collection title holds the collection's own actions:

| Action                    | Effect                                                                                                                                                                                                                                                                  |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Edit details**          | Changes the title, description, image and alt text. Console previews an HTTPS image; a path beginning with `/` is served by your app, so Console does not preview it. **Save changes** adds the edit to your unpublished changes; clear an optional field to remove it. |
| **Undo collection edits** | Drops this collection's unpublished order and detail changes.                                                                                                                                                                                                           |
| **Archive collection…**   | Hides the collection from your app in the current environment as soon as you confirm, without publishing. It asks for a reason. Its quests and their order stay.                                                                                                        |
| **Unarchive collection…** | Shows an archived collection in your app again. It asks for a reason too. Archived collections stay in the list, marked **Archived**.                                                                                                                                   |

Detail changes and quest reordering share the same draft, so review and publish to show them in your app. Deploying again does not change whether a collection is archived.

Dragging is paused while you filter the list.

Quests under **Not in a collection** are still visible in Console. How they appear in your app depends on its code. Removing a quest from a collection does not pause it; see [Pause or archive](#pause-or-archive).

## Review and publish [#review-and-publish]

Console saves your changes as you work. Nothing reaches your app until you publish.

```mermaid
flowchart LR
    Edit["Edit in Console"] --> Pending["Unpublished change"]
    Pending --> Review["Review & publish"]
    Review --> Test["Publish to test"]
    Pending -- "Undo edits / Discard all" --> Dropped["Change dropped"]
```

### 1. Check what is pending [#1-check-what-is-pending]

New and edited quests are marked in the list, and the bar at the bottom counts your unpublished changes.

### 2. Review the changes [#2-review-the-changes]

Choose **Review & publish**. Review what each quest changes, and how each affected collection will be ordered.

### 3. Publish [#3-publish]

Choose **Publish to test**.

> [!NOTE] Live is read-only in Console
> In live, Quest Studio and the other definition editors are read-only and show "Live changes only through a promotion from test." Choose **Edit in Test** to open the same page in test. A [promotion](/docs/concepts/promotion) from test carries Console changes to live; **Promote to live**, there or in the project header, prepares one. See [test and live](/docs/concepts/environments).

### Drop changes instead [#drop-changes-instead]

| Action                                                                                                       | Effect                          |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------- |
| **Undo edits** or **Discard new quest*&#x2A;, in the quest's &#x2A;*⋯** menu or at the top of the open quest | Drops that one change.          |
| **Discard all**                                                                                              | Drops every unpublished change. |
| Change a quest back to its published values                                                                  | Removes the change.             |

If another person edits the same unpublished changes, Console asks you to load their version before you continue.

## Change a setting [#change-a-setting]

A value you customize in Console takes precedence over the default from your code or the quest type. Console marks the setting **Customized** and shows the default under it. To follow the default again, choose **Reset** beside the setting's name, then review and publish.

> [!NOTE] Existing attempts keep their rules
> New releases apply to new attempts. Existing attempts keep the rules and promised rewards that they started with.

A quest synced from code keeps its rules there, but you can adjust its settings in Console:

### 1. Open the synced quest [#1-open-the-synced-quest]

Open it under **Quests**.

The **How it works** summary at the top shows what its code decides: trigger, completion, rewards, and any prerequisites, campaign, or quest window.

### 2. Change or reset a value [#2-change-or-reset-a-value]

Edit the setting, or choose **Reset** beside its name.

### 3. Check the before and after [#3-check-the-before-and-after]

The bottom of **Settings** compares the published value with your change.

### 4. Publish the settings [#4-publish-the-settings]

Choose **Publish settings to test**. Live settings are read-only in Console.

> [!CAUTION] Publishing settings goes out immediately
> **Publish settings** publishes right away, separately from your other unpublished changes.

### Secret settings [#secret-settings]

Some code quests have a secret setting, such as a scan code members must not see. Console shows it as **Set** or **Not set**, never its value.

* **Replace it.** Enter a new value in the field. The comparison at the bottom says **Replaced**, not the value.
* **Clear it.** Choose **Clear** beside its name.

Then publish the settings as above. Deploy output and quest listings never show the value either.

> [!CAUTION] A setting that just became secret
> Earlier versions of the quest still list its old value. Replace the value once the setting is secret.

> [!DEV] For your developer
> Declare it with `settings.text({ label, secret: true })`. It has no default, so the code reads `""` until you set it. Responses list a set secret in the release's `withheldSettings` and leave it out of `values`.

Presentation edits on a synced quest publish the same way, with **Publish presentation**. **Reset to code** restores the steps, image and account hints from your code.

## Choose a point ledger [#choose-a-point-ledger]

### 1. Create a ledger [#1-create-a-ledger]

Open **Rewards → Point ledgers → New ledger**. The name and description help operators distinguish ledgers that use the same unit, such as XP. Domino assigns the ID.

### 2. Select it on the quest [#2-select-it-on-the-quest]

Select the ledger in the quest's **Point ledger** setting. Publishing rejects an unknown ledger, so a typo cannot create a separate balance.

### How IDs are assigned [#how-ids-are-assigned]

New quests and drafts also receive IDs from Domino. Duplicating a quest creates a separate identity and completion history. Existing quest and ledger IDs remain valid.

## Pause or archive [#pause-or-archive]

| Action  | Effect                                                             |
| ------- | ------------------------------------------------------------------ |
| Pause   | Blocks new participation. Existing attempts continue.              |
| Archive | Does what pausing does, and also hides the quest from collections. |

Neither removes earned progress or rewards.

* **A quest you created in Console.** Open it and under **Participation** choose **Pause**, **Resume**, **Archive quest** or **Unarchive quest**, then review and publish. Console asks you to confirm before archiving.
* **A quest synced from code.** Open it and choose **Archive quest** or **Unarchive quest** under **Participation**, then give a reason. This publishes right away, separately from your other unpublished changes, and the audit log records it. Deploying the quest's code again keeps it archived.
* **Unarchiving needs app support.** It fails if your app no longer supports the quest's interaction. See [Archive a quest](/docs/build/publishing#archive-a-quest).

These controls change the quest's definition, so they are read-only in live. To stop new participation in a live quest, open it and choose **Pause quest** beside its status, or under **Quest availability** in **Activity & details**, with a reason. Resume it the same way. **Quest availability** reads **Available** or **Paused** for the selected environment, with the reason for a pause.

> [!IMPORTANT] Removing a quest from a collection does not pause it
> Pause the quest if you need to stop new participation.

## Recover automatic quest outcomes [#recover-automatic-quest-outcomes]

Some quests run on their own: Domino checks a member as soon as they meet the quest's prerequisites, such as reaching 100 points. While such a quest is paused, members who qualify are skipped. When its budget runs out, their attempts are rejected. You can review those members and let them through.

For a quest that runs on prerequisites alone, not on a tracker, open its detail in **Quests**, then **Automatic outcomes**. You need `read` permission to inspect decisions and `review` permission to recover paused outcomes, attempts rejected by budget exhaustion, or attempts that failed because an integration wasn't ready, in either test or live.

1. For paused outcomes, resume the quest through its operation controls. Resume alone does not admit the paused backlog. For an attempt rejected because its budget ran out (status `rejected`, code `budget-exhausted`), wait until its budget can cover the completion, such as in the next budget period. For an attempt that failed because an integration wasn't ready (status `failed`, code `integration-required`), connect or repair the integration first. That admits these attempts again on its own, so recover only the ones it left, such as on a quest that was paused.
2. Select up to 50 paused, budget-exhausted or integration-required outcomes. Each admitted outcome shows its linked attempt status and code.
3. Choose **Preview recovery**. Review each member, how many outcomes are ready to run, declared rewards, budget capacity and costs. Several selected events may reuse one occurrence's attempt.
4. Enter a **Recovery reason**, then choose **Confirm recovery**. A changed release, eligibility result, existing attempt or budget view requires another preview.
5. Open the admitted attempt to follow evaluation and rewards. Skips that remain ineligible retain their reason.

A preview doesn't reserve rewards or budget: the current budget is checked again when each evaluation settles.

**Balance checks** shows project-wide point-entry checks that have not finished. Checks retry after failures; exhausted work remains visible. An operator with `review` permission can choose **Retry** after the cause is resolved. Retrying keeps the original point-entry identity and cannot duplicate an admitted occurrence.

> [!DEV]
> These are `automatic()` quests with prerequisites and no tracker. The management client exposes the same flow:
>
> | Method | Use |
> | --- | --- |
> | `management.quests.automaticOutcomes({ quest, member?, state?, code?, cursor? })` | Read decisions and optional `linkedAttempt` details, 50 per page. |
> | `management.quests.previewAutomaticCatchUp({ quest, outcomes })` | Preview selected paused, budget-exhausted or integration-required outcome IDs and receive a token. |
> | `management.quests.catchUpAutomatic({ quest, outcomes, token, actionId, reason })` | Confirm the reviewed recovery. Reuse the action ID for an identical retry. |
> | `management.quests.automaticScheduling({ member?, cursor? })` | Inspect pending, retrying and failed point-entry checks. |
> | `management.quests.retryAutomaticScheduling({ id, actionId, reason })` | Requeue a failed check with an audited reason. |
>
> A preview is not a reward reservation. Evaluators can choose rewards and dynamic costs; settlement enforces the current budget capacity. Manual submissions and tracker-outcome retries keep their own APIs.

## Try quests with test members [#try-quests-with-test-members]

A test member is a pretend member that you control in **Test**. Use one to try a quest the way a member would before your community sees it: claim it, send an answer, move to tomorrow to try a streak, or skip a check to reach the quests behind it. Live refuses test members.

You need `publish` permission in Test, which the editor role includes. **Audit history → Test members** lists who did what as each test member.

### Try a quest in Console [#try-a-quest-in-console]

In **Test**, open **Project map**, then select a published quest in the **Builder** view. Its **Try** tab acts as a test member:

1. Choose **Test member** to create one, or pick one you created before. To try a wallet or balance quest, choose **Wallet**, pick **Solana**, **EVM** or **Cosmos**, enter an address (and the chain ID for Cosmos, such as `cosmos:cosmoshub-4`), and choose **Save**. **Change** and **Remove** sit beside each address.
2. The tab shows the quest's state for that member: available, locked with the quests it needs first, blocked with the reason, its streak or count so far, and how often it has been completed.
3. Act as the member: **Enroll in** the campaign when the quest requires enrollment, **Claim**, submit text, a link or quiz answers, or **Confirm as staff** for staff-confirmed quests. For a custom tracker whose source declares `identities: ["member"]`, fill in the record's fields and choose **Send record**. For an X or Discord tracker, fill in the post, message or reaction and choose **Send post**, **Send message** or **Send reaction**; for a reaction, **Member is** picks **The reactor** or **The message's author**. Fields start at the values the quest's filter asks for, so the first record matches it. After each record, the tab says how many of the tracker's quests admitted, skipped or filtered it out.
4. The latest attempts and the member's balances update as evaluation finishes. Choose **Reset** to delete the member and start over with another.

The **Try** tab can't submit a photo. Live shows no **Try** controls.

> [!DEV]
> Try a photo quest as a test member from the CLI: `domino test-members act MEMBER QUEST --photo shot.jpg --wait` uploads the image and runs the quest's real image check.

Open **Scenario** at the bottom of the **Builder** view to see every test member side by side: a row per member with their balances, and a column per day in the project calendar with a mark for each attempt (completed, progress, needs review, rejected or waiting). Hover or focus a day to list its attempts with their quests and outcomes. Select a member's row to act as them in the **Try** tab. Activity sits on the day the ledger credits it, and tracker records a quest skipped are marked too. If the history can't be read, the dock falls back to each member's latest 25 attempts, placed on the day they were admitted.

To try tomorrow, choose **Next day** for the selected member. That member moves one project-calendar day forward, and the dock shows their simulated date and how many days they've advanced. To try a three-day streak, act today, choose **Next day**, act again, then repeat once more: two advances rather than two real days. Other test members keep their own clocks, and real members and live stay on real time. The dock shows up to two weeks around the selected member's day; a member whose day falls outside it shows that day under their name. To start over, reset the member.

### Try quests on your staging site [#try-quests-on-your-staging-site]

Your [staging site](/docs/build/staging) shows a test toolbar to anyone who opened it through Console and holds `publish` permission in Test. Your coding agent can open it with a staging link and use the toolbar as you, or as an automation key ([staging links for agents](/docs/build/staging#try-quests-with-the-test-toolbar)). It runs on top of your real app, so you can try a quest in the interface members will use. Visitors who entered the staging password don't get the toolbar.

The collapsed bar in the bottom corner shows who the site is acting as and that member's simulated date. Select it to open the toolbar. Its header stays in place: the project, the member the site acts as, their simulated date, and **Next day**. Below it, tabs hold the rest. The toolbar remembers the open tab, whether it is open, and whether you hid it, until you close the browser tab. **Escape** closes the menu or the open form first, then the toolbar.

The toolbar follows your app without a reload. Right after the app makes a request to its own site, such as a sign-in, a sign-out or a submission, and every few seconds otherwise, it reads who the site is signed in as and, while it is open, the member's activity and the open tab. When your app signs in with a real account instead of a test member, the toolbar shows **Not a test member**: it acts only on test members, and picking one signs that account out.

| Control                   | Where    | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Member name               | Header   | Signs the site in as the test member you pick, or out with **Signed out**. The page reloads as that member.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| **Next day**              | Header   | Moves the member one project-calendar day forward, as in Console's **Scenario**.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| **Quests**                | Tab      | Lists the project's quests with where the member stands on each: completed and how often, a streak or count so far, available, locked with the quests it needs, or the time its retry cooldown ends. **Pass** [passes a manual quest](#pass-a-quest-or-accept-an-attempt-anyway) without its check; for a keyed or counter quest, the key button beside it opens an optional **Completion key**. A quest a tracker's record starts offers **Send a record**, which opens **Send** with that tracker picked. A refused pass says why in the quest's row.                                                                                                                                                                                                                 |
| **Activity**              | Tab      | Lists the member's attempts and skipped tracker records, newest first, and follows waiting attempts until they settle. Attempts your app makes show as they arrive. A dot on the tab marks a waiting attempt. A rejected attempt offers **Accept anyway**, and an attempt you passed or accepted anyway says so, with who did it.                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| **Send**                  | Tab      | Sends a record as the member to a tracker you pick: a custom tracker whose source declares `identities: ["member"]`, or an X or Discord tracker, which takes a synthetic post, message or reaction as in [Send X and Discord activity](#5-send-x-and-discord-activity). Edit the record's `data` as JSON. It starts from a custom tracker's data schema, or from the fields a synthetic record takes. For a Discord reaction, pick **Member is the reactor** or **Member is the message's author**. Archived trackers aren't listed. A paused tracker is listed but refuses the record until you resume it. A custom tracker also needs `ingest` permission. A post, message or reaction names the member's real X or Discord account when it has one, as the tab says. |
| **Accounts**              | Tab      | Lists the member's wallets, and their X and Discord accounts marked **Test-linked**, **Synthetic** or **Connected**. &#x2A;*Link…** opens one form: pick **Wallet** with its chain and address (and network for Cosmos), **X account** with a handle or user ID, or **Discord account** with its user ID, as in [Give the member a wallet](#3-give-the-member-a-wallet) and [Link an X or Discord account](#4-link-an-x-or-discord-account). Each wallet and each test-linked account can be unlinked; a synthetic or connected account can't be unlinked here. A refusal, such as an account another member holds, shows under the form.                                                                                                                               |
| **New member**            | `⋯` menu | Creates a test member with the name you enter, then acts as them. While the site is signed out, the toolbar offers it in place of the tabs too.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| **Reset…**                | `⋯` menu | Asks first, then deletes the member with everything they did and starts over as a new member with the same name.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| **Stop acting as member** | `⋯` menu | Signs the site out of the test member, as **Signed out** in the member list does. While the site is signed in with a real account, it reads **Sign the page out** and signs that account out. The page reloads signed out. Your Console session stays signed in.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| **Hide toolbar**          | `⋯` menu | Hides the toolbar until you close the browser tab, so the page lays out as it will on live, for example to check that a screen fits a phone. Only a small square marker stays in the bottom-right corner, over the corner of your app; select it to bring the toolbar back. Reloads and other pages in the same tab keep it hidden.                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| **Open in Console**       | `⋯` menu | Opens the project in Console.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |

Accounts and wallets you connect in your app while acting as a test member are linked to that member, as they would be for a real one. &#x2A;*Reset…** unlinks them, so you can connect the same accounts to the next member.

The toolbar runs in its own isolated layer, so your app's styles don't change it, and it never gives the page a Console credential. On a phone, the collapsed bar can cover the right end of your app's bottom tab bar; your developer can lift the tab bar above it. To see the screen as members will on live, choose **Hide toolbar**.

> [!DEV]
> While the toolbar is on the page, `<html>` carries `data-domino-staging-toolbar` and `--domino-staging-toolbar-inset-bottom`: how far the collapsed bar reaches up from the bottom of the screen, in px. Opening the toolbar doesn't change it. While the toolbar is hidden, the property is `0px` and the attribute is gone, as on live. Add it to a fixed bottom bar's padding or `bottom`, with a `0px` fallback for live, which has no toolbar:
>
> ```css
> nav.tab-bar {
>   padding-bottom: calc(env(safe-area-inset-bottom) + var(--domino-staging-toolbar-inset-bottom, 0px));
> }
> ```

> [!DEV]
> After each change the toolbar fires `domino:test-state-changed` on `window`, and also when an attempt it follows settles. [`RelayProvider`](/docs/reference/participant-api#react-hooks) answers by reading the session and every hook again, so the page shows the new state without a reload. An app that reads Domino without `RelayProvider` can listen for the same event.
>
> The event's `detail` says what changed (`change`, such as `"clock"` after **Next day**) and which test member the page is signed in as (`actingAs`). An app that picks what to show by date reads the member's clock from `relay.me.get()` and `memberTime()` rather than the browser's clock: see [The member's clock](/docs/reference/participant-api#the-members-clock).

## Script a test with code [#script-a-test-with-code]

This section is for your developer or coding agent: the same test members, driven from code or the CLI so a whole scenario can run as a script.

Use `management.testMembers` from `@domino-sdk/relay/server` in the selected project with `environment: "test"`, or the `domino test-members` commands. A Console session, a personal API key, or a `domino login` CLI session can call them. The caller needs `publish` permission, which the editor role includes. Hosted app keys and service keys can't use test members, and live refuses them. Console's [Builder view](#try-a-quest-in-console) uses these methods.

Audit records name the person behind the credential: in Test, **Audit history → Test members** lists who created, submitted a quest, accepted a referral or redeemed an offer as, passed a quest or accepted an attempt anyway for, moved the clock of, gave or removed a wallet for, linked or unlinked an X or Discord account for, sent a synthetic X or Discord record as, signed a staging app in or out as, and reset or purged each test member (`management.audit.list({ category: "test-members" })`). Reset and purge delete those records except the staging sign-ins and sign-outs and the reset or purge record itself, which then name the member by ID.

| CLI command                                                                                            | Method                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `domino test-members create NAME`                                                                      | `create`                                                                                                                                 |
| `domino test-members list`                                                                             | `list`                                                                                                                                   |
| `domino test-members history MEMBER [--since MS]`                                                      | `history`                                                                                                                                |
| `domino test-members act MEMBER QUEST [--input JSON] [--release RELEASE] [--wait [--timeout SECONDS]]` | `act`; `--wait` then reads the attempt with `attempts.get` until it is no longer queued, evaluating or deferred, which also needs `read` |
| `domino test-members pass MEMBER QUEST [--completion-key KEY]`                                         | `pass`                                                                                                                                   |
| `domino test-members accept-anyway MEMBER ATTEMPT [--live-rules --reason TEXT]`                        | `acceptAnyway`                                                                                                                           |
| `domino test-members advance MEMBER --days N`, `--hours N`, `--minutes N` or `--to INSTANT`            | `advance`                                                                                                                                |
| `domino test-members accept-referral MEMBER --code CODE` or `--inviter MEMBER`                         | `acceptReferral`                                                                                                                         |
| `domino test-members redeem MEMBER OFFER`                                                              | `redeem`                                                                                                                                 |
| `domino test-members link-wallet MEMBER --chain CHAIN --address ADDRESS [--network NETWORK]`           | `linkWallet`                                                                                                                             |
| `domino test-members unlink-wallet MEMBER --chain CHAIN --address ADDRESS`                             | `unlinkWallet`                                                                                                                           |
| `domino test-members link-account MEMBER --kind x\|discord --subject ID` or `--username HANDLE`        | `linkAccount`                                                                                                                            |
| `domino test-members unlink-account MEMBER --kind x\|discord --subject ID`                             | `unlinkAccount`                                                                                                                          |
| `domino test-members record MEMBER --tracker TRACKER [--data JSON] [--as author\|messageAuthor]`       | `record`                                                                                                                                 |
| `domino test-members reset MEMBER`                                                                     | `reset`                                                                                                                                  |

`acceptReferral` accepts a referral invitation as the member, as a signed-in member accepting it would: the program's qualification and rewards apply, and so do its refusals (`self-referral`, `inviter-already-accepted`, `invitation-closed` after the member completed a quest that isn't a referral program's qualifying quest, `referral-cycle`, `invitation-not-found`). `inviter` uses that test member's invitation code, allocating one if it has none. A member in a referral relationship can't be [reset](#7-reset-and-create-another-member).

`redeem` redeems an offer, named by key or ID, as the member, as a signed-in member redeeming it would, and answers the redemption. The price, stock, per-member limit and requirements apply, and so do the refusals: `offer-not-found`, `offer-paused` for a paused or archived offer, `collection-paused` while redemptions are paused, `prerequisites-incomplete`, `offer-limit-reached`, `insufficient-points` and `offer-out-of-stock`. A refusal changes nothing. The redemption and its entitlement are the ones a member's own redeem writes, so `management.redemptions.list` lists it and staff hand the entitlement over as they would a member's (`management.handover.begin`, then `confirm`). The action ID replays it. A member with a redemption can't be [reset](#7-reset-and-create-another-member); [purge](#purge-members-reset-refuses) it instead.

The commands always address the test environment. Writes are saved operations, so an uncertain answer resumes with `domino operations resume`. Pass `--input` as JSON: `{"kind":"text","value":"…"}`, `{"kind":"url","value":"…"}`, or quiz answers with their release. Omit it for a claim.

`createManagementClient` and `hostedManagementClient` return a `ManagementClient`, which always includes `testMembers`. Type a function's parameter as `ManagementClient` when it needs the complete client, as in the examples below.

| Method                                 | Input                                                                                                                                             | Result                                                                      |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `create`                               | `{ actionId, name }`                                                                                                                              | `{ id, name, test: true, offsetDays: 0, today, wallets: [], accounts: [] }` |
| `list`                                 | No input                                                                                                                                          | Array of active test members                                                |
| `history(member, query?)`              | `{ since?: number }`                                                                                                                              | `{ attempts, skipped, complete }`                                           |
| `act(member, input)`                   | `{ operation: "attempts.submit", actionId, quest, input?, evidence?, release? }`                                                                  | The queued attempt, or the existing attempt on replay                       |
| `uploadEvidence(member, file, input?)` | A JPEG or PNG `Blob` of at most 5 MB, and `{ actionId? }`                                                                                         | `{ id, member, type, size }`, the same evidence on replay                   |
| `pass(member, input)`                  | `{ actionId, quest, completionKey? }`                                                                                                             | The decided attempt, or the existing attempt on replay                      |
| `acceptAnyway(member, input)`          | `{ actionId, attempt, rules?, reason? }`; `rules: "live"` needs `reason`                                                                          | The decided attempt                                                         |
| `advance(member, input)`               | `{ actionId }` and one of `days` (1–30), `hours` (1–720), `minutes` (1–43200) or `to` (epoch milliseconds, or an ISO 8601 instant with an offset) | `{ id, name, test: true, offsetDays, today, wallets, accounts }`            |
| `acceptReferral(member, input)`        | `{ actionId, code }`, or `{ actionId, inviter }` naming another test member                                                                       | `{ ok: true }`                                                              |
| `redeem(member, input)`                | `{ actionId, offer }`, the offer's key or ID                                                                                                      | The redemption                                                              |
| `linkWallet(member, input)`            | `{ actionId, chain, address }`, plus `network` for Cosmos                                                                                         | The test member with its `wallets`                                          |
| `unlinkWallet(member, input)`          | `{ actionId, chain, address }`                                                                                                                    | The test member with its `wallets`                                          |
| `linkAccount(member, input)`           | `{ actionId, kind, subject }`, or `{ actionId, provider: "x", username }`                                                                         | The test member with its `accounts`                                         |
| `unlinkAccount(member, input)`         | `{ actionId, kind, subject }`                                                                                                                     | The test member with its `accounts`                                         |
| `record(member, input)`                | `{ actionId, tracker, data?, as? }`                                                                                                               | `{ tracker, record, account, stored, admissions }`                          |
| `reset(member, input)`                 | `{ actionId }`                                                                                                                                    | `{ member, deleted: true }`                                                 |

The routes are `POST /management/v1/test-members`, `GET /management/v1/test-members`, `POST /management/v1/test-members/:member/actions`, `POST /management/v1/test-members/:member/evidence`, `POST /management/v1/test-members/:member/passes`, `POST /management/v1/test-members/:member/accept-anyway`, `POST /management/v1/test-members/:member/clock`, `POST /management/v1/test-members/:member/referrals/accept`, `POST /management/v1/test-members/:member/redemptions`, `POST /management/v1/test-members/:member/wallets`, `POST /management/v1/test-members/:member/wallets/unlink`, `POST /management/v1/test-members/:member/accounts`, `POST /management/v1/test-members/:member/accounts/unlink`, `POST /management/v1/test-members/:member/records`, `POST /management/v1/test-members/:member/reset`, and `POST /management/v1/test-members/purge`. Reuse an action ID only to retry the same write. Keep that ID when a response is lost.

Read `management.testMembers.history(member, { since })` through `GET /management/v1/test-members/:member/history?since=<ms>`. Without `since`, the inclusive window runs from 14 days before the member’s current clock through that clock’s current time. An explicit `since` keeps an inclusive lower bound with no upper bound. Attempts and skipped outcomes name their quest by ID as `quest` and by authoring key as `questKey`, which is absent for a quest created without a key. Attempts include `eligibleAt`: the pinned tracker observation's `occurredAt`, or `createdAt` for manual attempts. An attempt an operator decided [in place of its check](#pass-a-quest-or-accept-an-attempt-anyway) also has `operatorDecision`: `{ kind, actor, at }`, where `kind` is `"pass"` or `"accept-anyway"` and `actor` is the operator. Skipped tracker outcomes include `quest`, `tracker`, `record`, the reason `code`, and the beneficiary's occurrence time pinned at the decision. Both lists filter and sort by these activity times, newest first, with at most 500 entries each; `complete: false` means older entries were omitted. Use `eligibleAt` and `occurredAt` with the project calendar to group activity by day.

### 1. Create and submit [#1-create-and-submit]

Create a test member with a display name of 1–40 characters. Submit a claim without `input`, or supply the normal member text, URL, or quiz input. Quiz input includes its published release ID. For a photo quest, upload the image first with `management.testMembers.uploadEvidence(member.id, file, { actionId })`, a JPEG or PNG of at most 5 MB, then pass its `id` as `evidence`. Only that member can submit it. From the CLI, `domino test-members act MEMBER QUEST --photo shot.jpg` does both.

```ts
import type { ManagementClient } from "@domino-sdk/relay/server"

async function tryQuest(management: ManagementClient, quest: string) {
  const member = await management.testMembers.create({
    actionId: crypto.randomUUID(),
    name: "Test Alice",
  })
  const attempt = await management.testMembers.act(member.id, {
    operation: "attempts.submit",
    actionId: crypto.randomUUID(),
    quest,
  })
  return { member, attempt }
}
```

Submission admits an attempt; evaluation continues asynchronously. Normal enrollment, prerequisites, availability, budgets, and human review still apply. Use the existing campaign or quest enrollment methods when required.

### 2. Inspect the result [#2-inspect-the-result]

Read `management.members.get(member.id)` for progress, attempts, completions, balances, campaign enrollments, and identities. To poll for an outcome, read `management.members.progress(member.id)`: the same quest progress, latest attempts and balances without every quest's evaluator code. Custom trackers whose source declares `identities: ["member"]` can attribute a record to this member without a linked identity. Use `management.trackers.triggerOutcomes` to inspect admission.

### 3. Give the member a wallet [#3-give-the-member-a-wallet]

Call `management.testMembers.linkWallet(member.id, { actionId, chain, address })` to give the member a wallet address without a signature. `chain` is `"solana"`, `"eip155"` or `"cosmos"`; a Cosmos address also needs its `network`, such as `"cosmos:cosmoshub-4"`. Quest code reads the address exactly as it reads a wallet a member linked: `ctx.wallet.list()` returns it and `ctx.wallet.balance()` reads its on-chain balance. A quest that requires holding 100 tokens accepts a member whose address holds them and rejects one whose address doesn't, with the quest's own reason. Tracker records that name the address are attributed to the member too.

```ts
import type { ManagementClient } from "@domino-sdk/relay/server"

async function giveWallet(management: ManagementClient, member: string) {
  return management.testMembers.linkWallet(member, {
    actionId: crypto.randomUUID(),
    chain: "solana",
    address: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
  })
}
```

The result is the test member, whose `wallets` lists each address as `{ chain, network, address }`: by chain, newest first, in canonical form (EIP-55 for EVM, lowercase for Cosmos). Linking an address the member already has changes nothing, except that a Cosmos address linked with another `network` moves to that chain ID. `unlinkWallet(member, { actionId, chain, address })` removes one and frees it for anyone to link; an address the member doesn't list leaves them unchanged. To change an address, link the new one and unlink the old one.

* Each chain family holds at most five addresses. Linking a sixth returns HTTP 409 with `code: "limit-reached"`.
* An address that belongs to another member returns HTTP 409 with `code: "account-linked-elsewhere"`. That includes any member who signed a link to it in the project and environment, and another test member in the project that lists it.
* The address can't be used to sign in. Unlinking it or resetting the member frees it.
* A test member never keeps an address from its owner. When someone signs in with the wallet, or connects it with a signature, in the same project and environment, the address moves to them and leaves the test member's `wallets`. Link another address to keep trying the quest.

Tracker records the address authored before you linked it, and that a quest skipped because no member had it, are decided again, as they are for a member who links a wallet.

### 4. Link an X or Discord account [#4-link-an-x-or-discord-account]

To try a quest that reads `x.profile()`, `follows()`, `reposted()` or the member's Discord roles, link the member a real, public X or Discord account. Call `management.testMembers.linkAccount(member.id, { actionId, kind, subject })` with `kind` `"x"` or `"discord"` and the account's numeric user ID, as a [member import](/docs/use/imports) names it, or `{ actionId, provider: "x", username }` for an X account by its handle. The older `{ actionId, provider, id }` and the CLI's `--provider` and `--id` still work; they are deprecated and removed in 2.0.0. No OAuth is involved: you vouch for the link, so pick an account whose public state puts the member where you want it, such as one that follows the target account or one that doesn't.

```ts
import type { ManagementClient } from "@domino-sdk/relay/server"

async function linkX(management: ManagementClient, member: string) {
  return management.testMembers.linkAccount(member, {
    actionId: crypto.randomUUID(),
    provider: "x",
    username: "XDevelopers",
  })
}
```

Quest code reads the account as it reads one a member linked: `x.connection()` returns its ID, and `profile()`, `follows()`, `reposted()` and `discord.member()` ask X and Discord about the real account. Discord answers with the account's roles in your project's connected server. Tracker records the account authors are attributed to the member, and [synthetic records](#5-send-x-and-discord-activity) post as it. Records it authored before you linked it, which a quest skipped because no member had it, are decided again.

The result is the test member, whose `accounts` lists each X and Discord account as `{ provider, id, username, source }`, X first. `source` is `"linked"` for an account you linked, `"synthetic"` for a synthetic one, and `"connected"` for one the member connected on your staging site. `username` is the handle for an X account you linked by username, and otherwise `null`. Console marks a linked account **Test-linked** in the member's identities.

* A `username` is resolved through X. A handle no X account holds returns HTTP 404 with `code: "not-found"`. Lookups share the limits of `GET /management/v1/x/users/lookup`, 30 a minute per person and 300 an hour per organization, and answer HTTP 429 with `code: "rate-limited"` past them or while X is busy. Discord accounts link by ID only.
* Linking replaces the member's synthetic account or previously linked account of that provider. Linking the account the member already has changes nothing. A member that connected an X or Discord account on your staging site returns HTTP 409 with `code: "conflict"` for that provider.
* An account that belongs to another member returns HTTP 409 with `code: "account-linked-elsewhere"`. That includes any member who signed in with it or connected it in the project and environment, an account a member import attached, and another test member's link.
* The account can't be used to sign in. `unlinkAccount(member, { actionId, kind, subject })` unlinks it and frees it for anyone to link; an account you didn't link to the member leaves them unchanged. Disconnecting it on your staging site while acting as the member, unlinking it from the member's identities in Console, and reset free it too.
* Domino only reads the account; it never writes to it. A test-linked Discord account never receives role rewards: a quest's Discord role reward is never sent to Discord and creates no delivery, even for an attempt evaluated before the link and accepted anyway after it. The completion stands, and the attempt in the toolbar's **Activity** and in Console says the role reward was not sent and why. Reset still works.
* A test member never keeps an account from its owner. When someone signs in with the account, or connects it, in the same project and environment, it moves to them and leaves the test member's `accounts`. The audit history records no event for this move.

### 5. Send X and Discord activity [#5-send-x-and-discord-activity]

Call `management.testMembers.record(member.id, { actionId, tracker, data })` to write one synthetic record into a managed X or Discord tracker, as if the member had posted on X, sent a Discord message or added a Discord reaction. Relay attributes it, applies each quest's `where` filter and decides the tracker's quest triggers exactly as it does for collected activity.

```ts
import type { ManagementClient } from "@domino-sdk/relay/server"

async function react(management: ManagementClient, member: string) {
  return management.testMembers.record(member, {
    actionId: crypto.randomUUID(),
    tracker: "discord-reactions",
    data: { channelId: "1300000000000000001", emoji: { name: "⭐" } },
  })
}
```

`data` uses the field names and nesting of the tracker's stored record, so a field you set is the field a quest's `where` filter reads. Relay sets every ID and timestamp you can't, and uses real time.

| Tracker source        | `data` fields you can set                                                                                                                                                  | Defaults                                                                                                                                                                     |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x.search()`          | `content`, `author.username`, `author.name`, `isReply`, `isRetweet`, `isQuoteTweet`, `likeCount`, `replyCount`, `retweetCount`, `quoteCount`, `viewCount`, `bookmarkCount` | Empty `content`, `false`, zero counts, the handle of an account you linked by username or else `test_` plus the end of the account ID as the username, and the member's name |
| `discord.messages()`  | `channelId`, `parentChannelId` (for a message in a thread), `replyTo`, and `text` when the tracker stores text (`discord.messages({ text: true })`)                        | A synthetic channel, not in a thread; `text` is `null`                                                                                                                       |
| `discord.reactions()` | `channelId`, `parentChannelId` (for a reaction in a thread), `messageId`, `emoji` (`{ id?, name }`), `reactorRoles`, `messagePublishedAt`                                  | Synthetic channel and message, not in a thread, 👍, no roles, now                                                                                                            |

`guildId` is the server of the project's Discord connection, or a synthetic one without it. A reaction names the member as its reactor (the `author`). Pass `as: "messageAuthor"` to make the member the reacted-to message's author instead, with a synthetic reactor no member holds, to try a quest that pays a message's author.

A record names the member's real account of that kind when it has one: an account you [linked](#4-link-an-x-or-discord-account), or one the member connected on your staging site. Otherwise the first record for X or for Discord links the member a synthetic account of that kind, and later records reuse it. It is an ordinary verified link with a 20-digit ID that no real X or Discord account has, so `management.members.get` lists it in the member's `identities` and quest code's `x.connection()` returns it. X and Discord don't know the account: `profile()`, `follows()`, `reposted()` and the member's Discord roles read what those services answer for an unknown account. Link a real account to try those reads. If you unlink the synthetic account from the member, later records are skipped with `unlinked-account`, as a real member's would be.

The result names the stored `record`, the tracker's ID, the `account` it names as `{ kind, id }`, and `admissions`: how many of the tracker's quests admitted, skipped, and filtered out the record. Read outcomes with `management.trackers.triggerOutcomes` or the member's `history`. In the tracker's records the record has `synthetic: true`, and Console marks it **Synthetic**. The X collector never asks X to refresh a synthetic post.

The tracker must be an X or Discord tracker in the test environment. A custom tracker, a field its source doesn't take, `text` for a tracker that doesn't store it, or `as: "messageAuthor"` on anything but a reaction is refused with HTTP 422 `validation-failed`; so is a tracker window that doesn't include now. A paused tracker answers 409 `tracker-paused`, an archived one 409 `tracker-archived`.

### 6. Advance the member's clock [#6-advance-the-members-clock]

Call `management.testMembers.advance(member.id, { actionId, days: 1 })` to try the next day. Each call moves that member forward by one of:

| Input                                                          | Moves the clock                                              | Use it for                                                                           |
| -------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `days`, 1–30                                                   | By project-calendar dates, keeping the member's time of day  | Daily quests, streaks, campaign dates                                                |
| `hours`, 1–720, or `minutes`, 1–43200                          | By exactly that much time                                    | Time blocks, such as activity counted in 90-minute windows                           |
| `to`, epoch milliseconds or an ISO 8601 instant with an offset | To that instant, which must be later than the member's clock | Just before or after midnight in the project's time zone, or a window's opening time |

The member's clock can run at most **90 days** ahead of real time, whichever units moved it: `days` add up to at most 90 and never land past the 90th date, and `hours`, `minutes` and `to` never move the clock past 90 days from now. Exceeding that returns HTTP 422 with `code: "test-member-clock-limit"`; a `to` at or before the member's clock returns HTTP 422 with `code: "test-member-clock-past"`. The server also returns optional `today?: string`, the current project-calendar date as `YYYY-MM-DD`. Use this server date for display. The server returns `offsetDays`, initially zero: the days advanced with `days`, which hours, minutes and `to` leave unchanged. Older responses may omit it, which means zero. Replaying the same action ID returns its original result, even after later advances; reusing it with different input fails. From the CLI, `domino test-members advance MEMBER` takes one of `--days`, `--hours`, `--minutes` or `--to`.

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

async function nextDay(
  management: ReturnType<typeof createManagementClient>,
  member: string,
) {
  const updated = await management.testMembers.advance(member, {
    actionId: crypto.randomUUID(),
    days: 1,
  })
  const detail = await management.members.get(member)
  return { updated, detail }
}
```

Claims, campaign and quest enrollment, evaluator time, availability, period prerequisites, streaks, counters, and budget periods use the member's clock. So do a completion's rewards, from a quest or a leaderboard prize: its points take effect, its badges are awarded and its automatic rewards are collected at the member's time, while receipt and audit times stay real. `members.get` reports progress and reset boundaries on it. Other test members, real members and live activity keep real time. Leaderboard windows and settlements stay on real time, and deferred evaluations and outbox retries still wait in real time: advancing does not wake them early. For custom tracker records, keep sending real `occurredAt` and `observedAt` timestamps. Advancing changes no existing attempt or record.

See [test member clock](/docs/reference/quest-rules#test-member-clock) for how the shift handles daylight-saving changes and timezone changes, tracker and point timestamps, budgets and leaderboards.

There is no backward adjustment. Reset and create another member to start at zero; the reset restrictions below still apply. The clock endpoint requires the same test environment and `publish` permission as the other test-member methods.

### 7. Reset and create another member [#7-reset-and-create-another-member]

Call `management.testMembers.reset(member.id, { actionId })`. A successful reset deletes attempts, completions, streak and counter state, member budget usage, enrollments, tracker outcomes and qualifications, balances and point history, uncollected entitlements, referral codes, member leaderboard state, the member's wallet addresses, their synthetic X and Discord accounts and synthetic records, and their group memberships with their history. Current leaderboard reads use the remaining point history. Reset preserves every other tracker record. It also ends the member's sessions, signing out a staging app acting as that member.

Reset also unlinks the X and Discord accounts you linked to the member, and the accounts the member connected in your app while a staging site acted as them: X, Discord, Telegram, Google or Apple accounts and wallets. Only the member's link goes. The account itself is untouched, and anyone can link it afterwards.

Reset returns HTTP 409 with `code: "test-member-reset-blocked"` and a `state` explaining the refusal when:

* An attempt is queued, evaluating, or deferred.
* A completed quest has a shared project budget, whose exact refundable charges are not journaled.
* Inventory reservations, redemptions, a reward handover, reward delivery, or webhook delivery history exists.
* Referral relationships or reward accounting affect other members.
* An immutable leaderboard settlement, engagement account, or legacy content admission includes the member.
* The member has an account that signs in, an app user ID, or an X account a member import attached. Accounts you linked to the member, accounts the member connected in your app, and synthetic X and Discord accounts don't count: reset unlinks them.

A refusal changes nothing. Wait for running evaluations, or retain the member for inspection and create another test member. Completed external effects and shared accounting cannot be erased by this API. Reset is unavailable in live and for ordinary members.

Reset, purge and erasure remove only Domino's records. Data your hosted app stored about the member, such as a ticket mapping or an uploaded photo, stays in the app's own storage. Clear it in test with `domino storage clear`, as in [Clear test data](/docs/build/storage#clear-test-data).

#### Purge members reset refuses [#purge-members-reset-refuses]

A simulation leaves members that redeemed an offer or took part in a referral, which reset refuses. Purge them instead: `management.testMembers.purge({ actionId, members, reason })`, or `domino test-members purge MEMBER... --reason TEXT`. A purge deletes up to 500 test members in one transaction. It removes everything reset removes, plus their redemptions, inventory reservations, reward handovers (collected ones too), referral relationships, referral rewards and referral accounting.

* **Stock goes back.** Each redemption that was not canceled, each reservation and each pickup handover gives its stock unit back to its stock item, as canceling or voiding would, so the offer's stock and redeemed count stay consistent. Points the members spent go with their balances.
* **List both sides of every referral.** A referral between a listed member and one outside the list refuses the whole call with `test-member-reset-blocked` and `state: "referrals affect another member"`; `member` names the listed member and `outside` the other. A purge never changes the rewards of a member who stays.
* **Every other reset refusal still applies** to each listed member: a running attempt, a shared project budget, reward or webhook delivery history, a leaderboard settlement, an engagement account, a member import, or an account that signs in. The error's `member` names the member it concerns.

A refusal changes nothing for any listed member. The answer is `{ members, purged: true }`, each ID listed once, and the action ID replays it. Purged members can't come back, as after reset; a member already reset or purged counts as purged. The project audit records one **Test members purged** event with you as actor, the members, your reason and the stock given back per offer. A purge needs the same test environment and `publish` permission as reset.

### Pass a quest or accept an attempt anyway [#pass-a-quest-or-accept-an-attempt-anyway]

To reach the quests behind one whose check you can't or don't want to satisfy, such as a follow on X, an on-chain balance or a quiz, pass it: Domino admits an attempt as a submission would, then accepts it as you without running the quest's check. **Pass** in the staging toolbar does the same. In code, call `management.testMembers.pass(member.id, { actionId, quest })`; the answer is the decided attempt.

```ts
import type { ManagementClient } from "@domino-sdk/relay/server"

async function skipCheck(
  management: ManagementClient,
  member: string,
  quest: string,
) {
  return management.testMembers.pass(member, {
    actionId: crypto.randomUUID(),
    quest,
  })
}
```

A pass skips only the check:

* **The other rules still apply.** Enrollment, availability, prerequisites, a waiting attempt and the retry cooldown refuse it as they refuse a submission, with the same codes. A pass needs no input, so a quiz, text or URL quest passes without answers, and a staff-confirmed quest without a confirmation.
* **Completion, budgets and rewards apply.** They apply as for an accepted check. You decide in place of a reviewer too: a quest with human review on is accepted at once, with no review to wait for. A pass is one accepted action: a counter quest counts one action and a streak quest today's action, so combine passes with **Next day** or `advance`. A budget that can't cover the completion leaves the attempt `rejected` with `code: "budget-exhausted"`.
* **Keys are optional.** A keyed quest takes `completionKey`, and a counter quest takes it as the action's distinct key. Without one, the pass uses a fresh key, so every pass is a new completion or counted action. Other quests refuse a key with HTTP 422 `validation-failed`.
* **Automatic quests can't be passed** (HTTP 422 `validation-failed`). For a tracker-triggered quest, send the record that starts its attempt, with `record` or a custom tracker ingest. If the check rejects it, accept it anyway.

**Accept anyway** in the toolbar, or `management.testMembers.acceptAnyway(member.id, { actionId, attempt })`, accepts a rejected attempt as you, in place of the check's or a reviewer's rejection. It is the same attempt, decided again: its history keeps the rejection, and the completion policy, budgets and rewards apply as for a pass. When it is the member's latest attempt on the quest, the quest's retry cooldown ends too. Like a staff review, it decides the attempt under its pinned release without checking enrollment, the availability window or whether the quest was archived since. An attempt that isn't `rejected` is refused with HTTP 409 `attempt-not-rejected`: a failed attempt is retried, and one that needs review is reviewed. Attempts of a quest that completes per content item can't be accepted this way (HTTP 422 `validation-failed`).

These rules differ from live, where staff accept a rejected attempt anyway with a reason, and live refuses an archived quest, an attempt the member has since followed with a newer one, a completion still in its cooldown and an acceptance that would pay nothing. The test default lets you get a test member unstuck, so it proves nothing about what live allows.

> [!DEV]
> To rehearse live, pass `{ rules: "live", reason }` (`domino test-members accept-anyway MEMBER ATTEMPT --live-rules --reason TEXT`). The reason is required (HTTP 422 `validation-failed` without one) and recorded with the decision, and live's refusals apply with live's codes: HTTP 409 `quest-archived`, `attempt-superseded`, `completion-cooldown` with `retryAt`, `budget-exhausted`, `already-completed`, `completion-revoked` and `key-used`. A refusal leaves the attempt and balances as they were. The toolbar's **Accept anyway** uses the test default.

Both are your decision, not the check's. The attempt's history records a `test-member-decision` with you as the actor, `history` returns its `operatorDecision`, and **Audit history → Test members** lists it. Reset removes the attempts as it removes any.

### Pace a live rehearsal [#pace-a-live-rehearsal]

To rehearse with many test members acting in real time, so leaderboards and screens move every few seconds while people watch, run the scenario from a driver process outside your hosted app: a terminal, a CI job or any long-running process.

* **Drive members with your own credential.** The driver calls `management.testMembers` or `domino test-members` with a personal API key, an organization automation key or a `domino login` session that holds `publish` in Test. A hosted app key can't act as test members.
* **Don't pace it with a schedule or a browser tab.** [Scheduled jobs](/docs/build/schedules) run at most every 5 minutes, get one try per run, and must finish within 30 seconds without starting work that outlives the response, such as `waitUntil`. They suit coarse steps, not seconds. A loop in an open page stops when the tab closes.
* **Name each step.** Derive each write's `actionId` from the scenario, such as `rehearsal-3-member-17-step-4`, instead of a random one. From the CLI, pass the same ID as `--action-id`. A restarted driver sends the same steps with the same IDs and input, and each answers what it already did instead of acting twice.
* **Keep a fixed number of calls in flight.** The management client sends a write with an action ID again after no answer, HTTP 500, 502, 503 or 504, or a 429 with `Retry-After`, waiting as asked, within the call's deadline. See [rate limits](/docs/agents/automation#rate-limits) for the few calls with limits of their own.
* **Move clocks instead of waiting.** For streaks, daily quests and availability windows, [advance the member's clock](#6-advance-the-members-clock). Pace in real time only what people watch.

Coding agents follow the same steps in [campaign testing](/docs/reference/campaign-testing#pace-a-live-rehearsal).

## Attach badge rewards [#attach-badge-rewards]

Create a badge in **Rewards** with its key, name, description and image URL. Open a quest created from a Console quest type, choose **Add badge**, and pick the achievement by name. Review and publish the quest changes as usual.

The member drawer lists a member's badges. With `review` permission, enter a reason and choose **Award badge** or **Revoke**. Awards and revokes are recorded in audit history. In **Rewards**, definition edits, archiving and restoration require `publish` permission. See [badge lifecycle](/docs/concepts/rewards#badges) before correcting an award.

> [!DEV]
> Code-authored quests attach badges with `badge(...)`; see [authoring](/docs/reference/authoring#badges).

## Where next [#where-next]

- [Progress and review](/docs/use/progress) — What happens after someone submits an activity.
- [Quests](/docs/concepts/quests) — Activities, attempts, prerequisites, and reusable quest types.
- [Collections](/docs/concepts/collections) — Arrange quests without duplicating their progress.
