Manage quests

Add quests in Console, put them in order, change their settings, and pause them when a campaign ends.

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 thisProgress
Show the same quest in two placesPut the quest in both collectionsShared between the two collections
Start a separate copy of an activityDuplicate the questNew identity with fresh progress

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.

Open Quests

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

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.

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.
  • 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.
  • 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.

Review and publish

The new quest stays unpublished until you publish it. See Review and publish.

Arrange collections

ActionEffect
Drag a quest by the grip on its leftReorders the collection.
Drag a quest onto another collectionMoves it there. A collection cannot list the same quest twice, so that drop is ignored.
⋯ menu → Move up / Move downReorders without dragging.
⋯ menu on a questShows the quest in another collection, or removes it from this one.
Open a quest, then under Collections choose Add to a collection, or × on a collectionAdds 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 ⋯ menu beside a collection title holds the collection's own actions:

ActionEffect
Edit detailsChanges 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 editsDrops 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.

Review and publish

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

Check what is pending

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

Review the changes

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

Publish

Choose Publish to test.

Drop changes instead

ActionEffect
Undo edits or Discard new quest, in the quest's ⋯ menu or at the top of the open questDrops that one change.
Discard allDrops every unpublished change.
Change a quest back to its published valuesRemoves the change.

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

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.

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

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.

Change or reset a value

Edit the setting, or choose Reset beside its name.

Check the before and after

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

Publish the settings

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

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.

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

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.

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

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

ActionEffect
PauseBlocks new participation. Existing attempts continue.
ArchiveDoes 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.

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.

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.

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

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.

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

Your staging site 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). 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.

ControlWhereWhat it does
Member nameHeaderSigns the site in as the test member you pick, or out with Signed out. The page reloads as that member.
Next dayHeaderMoves the member one project-calendar day forward, as in Console's Scenario.
QuestsTabLists 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 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.
ActivityTabLists 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.
SendTabSends 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. 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.
AccountsTabLists the member's wallets, and their X and Discord accounts marked Test-linked, Synthetic or Connected. 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 and 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⋯ menuCreates 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…⋯ menuAsks first, then deletes the member with everything they did and starts over as a new member with the same name.
Stop acting as member⋯ menuSigns 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⋯ menuHides 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⋯ menuOpens 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. 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.

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 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 commandMethod
domino test-members create NAMEcreate
domino test-members listlist
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 INSTANTadvance
domino test-members accept-referral MEMBER --code CODE or --inviter MEMBERacceptReferral
domino test-members redeem MEMBER OFFERredeem
domino test-members link-wallet MEMBER --chain CHAIN --address ADDRESS [--network NETWORK]linkWallet
domino test-members unlink-wallet MEMBER --chain CHAIN --address ADDRESSunlinkWallet
domino test-members link-account MEMBER --kind x|discord --subject ID or --username HANDLElinkAccount
domino test-members unlink-account MEMBER --kind x|discord --subject IDunlinkAccount
domino test-members record MEMBER --tracker TRACKER [--data JSON] [--as author|messageAuthor]record
domino test-members reset MEMBERreset

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.

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; purge 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.

MethodInputResult
create{ actionId, name }{ id, name, test: true, offsetDays: 0, today, wallets: [], accounts: [] }
listNo inputArray 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 reasonThe 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 IDThe redemption
linkWallet(member, input){ actionId, chain, address }, plus network for CosmosThe 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 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.

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.

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.

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.

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.

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.

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 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.

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 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.

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.

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 sourcedata fields you can setDefaults
x.search()content, author.username, author.name, isReply, isRetweet, isQuoteTweet, likeCount, replyCount, retweetCount, quoteCount, viewCount, bookmarkCountEmpty 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, messagePublishedAtSynthetic 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, 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.

Advance the member's clock

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

InputMoves the clockUse it for
days, 1–30By project-calendar dates, keeping the member's time of dayDaily quests, streaks, campaign dates
hours, 1–720, or minutes, 1–43200By exactly that much timeTime blocks, such as activity counted in 90-minute windows
to, epoch milliseconds or an ISO 8601 instant with an offsetTo that instant, which must be later than the member's clockJust 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.

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 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.

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.

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

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.

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.

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

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 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 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. Pace in real time only what people watch.

Coding agents follow the same steps in campaign testing.

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 before correcting an award.

Where next

On this page