{
  "format": 1,
  "capabilities": [
    {
      "id": "identity",
      "support": "native",
      "description": "Real email and configured OAuth sign-in, and wallet sign-in where a project enables it; identities and sessions are scoped to a project and environment. A signed-in participant reads their own verified sign-in email addresses with me.get() (email, or null after wallet-only sign-in, and emails); participant routes never return another member's. me.get() also returns the member's clock ({ now, offsetMs }), which runs ahead for a test member whose clock was advanced; memberTime(me) from @domino-sdk/relay/browser gives the member's current time for date-gated screens.",
      "entrypoints": [
        "auth.providers",
        "auth.start",
        "auth.emailStart",
        "auth.emailVerify",
        "auth.session",
        "me.get"
      ],
      "evidence": [
        "apps/backend/src/auth/sessions.ts",
        "apps/backend/src/public/routes/sign-in.ts",
        "apps/backend/src/auth/accounts.ts"
      ]
    },
    {
      "id": "connected-accounts",
      "support": "native",
      "description": "Project-scoped Discord membership, roles, nickname, display name, boosting status and captured message reads. Participants link identity; operators install the bot once. X and Discord sign-ins and connections store the account's public name, handle and picture for participant profiles. Linking X signs in with X (OAuth 2.0 with PKCE) and stores the account's stable X user ID, which tracker attribution and ctx.x ownership checks use; a handle never establishes ownership. No arbitrary Discord writes. Operators read active X and Discord selections and reviewers unlink them with a reason in Console or the management API. Subject and revision preconditions prevent stale reviews from unlinking newer selections. Unlinking affects the project and environment, preserving identity ownership, sign-in grants and existing rewards, except that an imported or vouched account the member never signed in with or proved is released so its owner can link it. Reviewers restore exact retained OAuth selections through the kind-keyed members.identityStates.relink operation, with a revision and audited reason. Relink keeps ownership, sign-in grants and sessions, and redecides unlinked historical activity under the current quest policy. members.identityStates reads every identity a member proved, by kind (wallets, X, Discord and Telegram accounts, app identities), with its link, revision, sign-in and allowed actions, and unlinks or relinks any of them with an expected revision, an audited reason and a receipt-backed action ID, naming the identity as a member import does ({ kind: \"x\" | \"discord\" | \"telegram\", subject }, { kind: \"app:<name>\", subject } or { kind: \"wallet\", chain, address }; an X or Discord subject resolves to the member's selected account, else the only one they hold, and 409 identity-ambiguous otherwise) or with the selector the list returned, whose oauth and app forms are deprecated. Each identity read says where it came from (source: member, import with the import's ID, app-login or test-link); the per-kind methods (oauthAccounts, unlinkOAuthAccount, identityLinks) are deprecated 1.x adapters.",
      "entrypoints": [
        "auth.accounts",
        "auth.start",
        "auth.disconnect",
        "management.members.identityStates.list",
        "management.members.identityStates.unlink",
        "management.members.identityStates.relink"
      ],
      "cli": [
        "members identities",
        "members unlink-identity",
        "members relink-identity"
      ],
      "reference": "verification",
      "evidence": [
        "apps/backend/src/auth/accounts.ts",
        "apps/backend/src/public/oauth-providers.ts",
        "apps/backend/tests/gateway/management-member-oauth.test.ts",
        "apps/backend/src/auth/identity-links.ts",
        "apps/backend/tests/gateway/management-identity-relink.test.ts",
        "apps/backend/src/auth/identity-states.ts",
        "apps/backend/tests/gateway/management-identity-states.test.ts",
        "apps/backend/src/public/routes/management-identity-context.ts",
        "apps/backend/tests/gateway/management-org.test.ts"
      ]
    },
    {
      "id": "identity-exchange",
      "support": "native",
      "description": "Keep an existing application login using createParticipantBackend on its server. A Console administrator creates a project/environment identity integration key. The adapter verifies the application session, exchanges its stable subject for a member-only Domino session, and routes participant requests with trusted scope. Keys never expire unless an expiry is chosen at creation. They rotate with overlap: addSecret issues a second secret while the first keeps working, revokeSecret retires the old one, and removeSecretExpiry makes an expiring secret permanent; each secret reports its last use. When an integration's last secret expires, its sessions end. rotate, which replaces every secret at once, is deprecated. A key created with an identity kind (identityKind: \"app:<name>\", set only at creation) links each exchanged subject to its member as an app:<name> identity, which custom tracker records can name and ctx.member.identities() lists. A kind belongs to the integration that first claimed it, also after a disconnect; a new integration reuses a kind with identities only by adopting them (adoptIdentitiesFrom plus an audited reason; identityIntegrations.kinds lists owners). Reviewers unlink and relink a member's app identity with an audited reason; unlinking ends its sessions and refuses its sign-in (409 APP_IDENTITY_UNLINKED) until relinked. Administrators manage these keys in Console, or with management.identityIntegrations from a Console session; an API key or CLI sign-in cannot change them. An administrator's CLI session lists them without secrets (identityIntegrations.list and kinds; domino identity-integrations list and kinds), so the owner creates the key in Console (which, for a Domino-hosted app, stores it as the app secret APP_IDENTITY_KEY in the same step: identityIntegrations.createIntoAppSecret and addSecretIntoAppSecret, publish permission, no token answered) or stores it with printf %s \"$KEY\" | domino secrets set APP_IDENTITY_KEY, and the agent confirms it; non-admin CLI sessions and every key type are refused. members.identityStates unlinks and relinks app identities by kind beside wallets and accounts; unlinkIdentity and relinkIdentity are deprecated adapters. With Vouches accounts on (identityIntegrations.setAccountVouching with a reason, from a Console session with publish permission), authenticate may also return accounts (wallets and one X, Telegram and Discord account each) that the exchange attaches under a member import's rules, answering each one attached, already-linked or refused with an import refusal code; with it off, accounts are refused with 403 ACCOUNT_VOUCHING_OFF. exchangeParticipant runs the same exchange from a server without a browser. inspectIntegrationKey (GET /v1/auth/integration) answers which integration a key belongs to (organization, project, environment, origin, this secret's expiry and the app's origins in that environment), never a secret and without counting as a use; domino secrets set runs it on every di_ value and refuses, storing nothing, a key that is unknown, expired, revoked or for another project or environment.",
      "entrypoints": [
        "management.identityIntegrations.list",
        "management.identityIntegrations.create",
        "management.identityIntegrations.rotate",
        "management.identityIntegrations.addSecret",
        "management.identityIntegrations.addSecretIntoAppSecret",
        "management.identityIntegrations.createIntoAppSecret",
        "management.identityIntegrations.revokeSecret",
        "management.identityIntegrations.removeSecretExpiry",
        "management.identityIntegrations.revoke",
        "management.identityIntegrations.kinds",
        "management.identityIntegrations.setAccountVouching",
        "management.members.unlinkIdentity",
        "management.members.relinkIdentity",
        "management.members.identityStates.list",
        "management.members.identityStates.unlink",
        "management.members.identityStates.relink"
      ],
      "cli": ["identity-integrations list", "identity-integrations kinds"],
      "reference": "participant-api",
      "evidence": [
        "apps/backend/src/auth/integrations.ts",
        "apps/backend/src/auth/identities.ts",
        "apps/backend/src/public/participant-scope.ts",
        "apps/backend/tests/auth.test.ts",
        "apps/backend/src/auth/identity-states.ts",
        "apps/backend/tests/gateway/management-identity-states.test.ts"
      ]
    },
    {
      "id": "quests",
      "support": "native",
      "description": "Claim, photo, quiz, staff and automatic quests; immutable releases pin attempts and promised rewards. Keyed completions and atomic lifetime, day, week (from Monday) or month budgets of the project calendar prevent duplicate awards for repeatable quests. Evaluations replay deterministically: ctx.now is the attempt's evaluation time, Date.now() and new Date() stay frozen at it, and Math.random() and crypto randomness are seeded per attempt. Reads name tiered rewards by ID; apps match them to their keys with tieredRewards.list. Deleting a quest file does not archive it: domino quests archive|unarchive <key> --reason (publish permission; test only, since live refuses archive state changes from every credential with promotion-required) or the quest's Console page publishes its current release again as archived or reopened, with an audited reason. Archived quests admit no new attempts, leave collections and supportedInteractions, keep completions and rewards, and stay archived when their code deploys again; unarchiving re-checks the app's supported interactions and the prerequisite graph. Console-created quests change participation in their Console editor. An operator with publish pauses or resumes one quest, or all redemptions, against the revision they saw and with a reason (management.quests.pause/resume and management.redemptions.pause/resume). With read permission, operators list each quest's current release, archived ones included (management.quests.list), and every paused or resumed operation (management.operations.list). With read and publish, they page through release history, every release a quest has had, newest first and optionally for one quest (management.releases.list), and read one current or historical release (management.releases.get). From the CLI, domino quests list|get read current quests, releases list|get read release history, and pauses list reads the revision pause and resume need; the bare domino quests, releases and status read Console's former pages and are deprecated.",
      "entrypoints": [
        "keyed",
        "budget",
        "quests.experience",
        "quests.observe",
        "quests.open",
        "tieredRewards.list",
        "management.quests.archive",
        "management.quests.unarchive",
        "management.quests.list",
        "management.releases.list",
        "management.releases.get",
        "management.operations.list",
        "management.quests.pause",
        "management.quests.resume",
        "management.redemptions.pause",
        "management.redemptions.resume"
      ],
      "cli": [
        "quests list",
        "quests get",
        "quests archive",
        "quests unarchive",
        "quests pause",
        "quests resume",
        "quests enrollments",
        "quests enroll",
        "quests withdraw",
        "releases list",
        "releases get",
        "pauses list",
        "redemptions pause",
        "redemptions resume"
      ],
      "reference": "authoring",
      "evidence": [
        "apps/backend/src/application/quest-catalog.ts",
        "apps/backend/src/ledger/participation/attempts.ts",
        "apps/backend/src/application/quest-lifecycle.ts",
        "apps/backend/tests/quest-lifecycle.test.ts",
        "apps/backend/tests/evaluator-determinism.test.ts",
        "packages/management/cli/commands/quests.mjs",
        "tests/cli-quests.test.mjs",
        "apps/backend/tests/contract-operations.test.ts",
        "apps/backend/tests/gateway/management-resource-reads.test.ts",
        "apps/backend/src/ledger/operations/quest-catalog.ts",
        "packages/management/cli/commands/inspect.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "points",
      "support": "native",
      "description": "Define typed point-ledger handles in code and pass them to authored rewards, budgets, prerequisites, leaderboards, referrals and engagement rules. settings.pointLedger selects a Console ledger as a typed reference; an empty selection pauses the quest. The server applies completion and rewards; the browser cannot write balances. A signed-in participant reads only their own balances (current and available after reservations) and point history, without operator reasons. Reads name ledgers by ID; apps match them to their keys and names with pointLedgers.list. An external score gets its own ledger, updated with absolute values (set updates) by the server that measures it; that ledger is never spent. Console member detail shows every balance and provides history, award, adjustment and reversal controls. Creating, editing or archiving a ledger needs publish and works only in test: live refuses it from every credential with promotion-required, and live ledgers come from a promotion from test. Archived ledgers stay readable and existing published rewards keep working, but new reward choices and definitions cannot use them. From the CLI, domino point-ledgers list|create|update|archive|unarchive manage ledgers.",
      "entrypoints": [
        "defineLedger",
        "points",
        "settings.pointLedger",
        "decision.accept",
        "me.observeProgress",
        "me.balances",
        "me.pointHistory",
        "pointLedgers.list",
        "management.pointLedgers.list",
        "management.pointLedgers.create",
        "management.pointLedgers.update",
        "management.pointLedgers.archive"
      ],
      "cli": [
        "point-ledgers list",
        "point-ledgers create",
        "point-ledgers update",
        "point-ledgers archive",
        "point-ledgers unarchive"
      ],
      "evidence": [
        "apps/backend/tests/ledger/accounting/inventory.test.ts",
        "apps/backend/tests/gateway/participant-ledger.test.ts",
        "apps/backend/tests/contract-points.test.ts",
        "apps/console/src/features/console/member-points.test.tsx",
        "packages/management/cli/commands/points.mjs"
      ]
    },
    {
      "id": "review",
      "support": "native",
      "description": "Queue a quest attempt for an authorized human reviewer. Review decisions and reward execution remain server-owned. Reviewers with review accept or reject a waiting attempt at the generation they saw, with a reason, and evaluate a failed attempt again (management.attempts.review, management.attempts.retry). With read permission, operators page through attempts newest first by quest, status and search, with the releases they pin, the members they name and a total (management.attempts.list), and read one attempt with its release, recorded events and member (management.attempts.get). From the CLI, domino attempts list|get reach the same reads. An evaluator rejects with decision.reject({ reason, code? }): the optional code (lowercase letters, digits and hyphens, at most 64 characters, none of the codes Relay writes itself such as budget-exhausted) names the cause, and these reads and Console's attempt detail show it beside the reason; member responses never carry it.",
      "entrypoints": [
        "decision.review",
        "decision.reject",
        "management.attempts.review",
        "management.attempts.retry",
        "management.attempts.list",
        "management.attempts.get"
      ],
      "cli": ["attempts list", "attempts get"],
      "evidence": [
        "tests/photo-review.test.mjs",
        "apps/backend/src/ledger/participation/attempts.ts",
        "apps/backend/tests/contract-attempts.test.ts",
        "apps/backend/tests/gateway/management-resource-reads.test.ts",
        "packages/management/cli/commands/inspect.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "text-ai-review",
      "support": "native",
      "description": "Review text against author criteria with ctx.review.text. Returns pass, fail or unclear; quest code chooses the decision. Text is limited to 16,000 JavaScript string units, criteria to 2,000. Successful results are captured for replay. Provider failures remain errors. Does not add a free-text submission form or verify authorship.",
      "entrypoints": ["defineQuest"],
      "evidence": [
        "tests/text-review.test.mjs",
        "apps/backend/tests/text-review-sandbox.test.ts",
        "packages/sdk/examples/text-review-quest.ts"
      ],
      "reference": "verification"
    },
    {
      "id": "image-checks",
      "support": "native",
      "description": "Evaluator ctx.images.inspect, compare, review, extract and seen check images without exposing bytes or URLs to quest code. References name the attempt's photo evidence, the linked X profile picture, a post's attached image, or a reference image deployed from relay.json images. Perceptual hash distances (0-64) compare images; AI review and field extraction accept up to four images; seen records the image when called, even for a later-rejected attempt, and counts other participants' near-duplicates. Results are captured for replay. Checks run at verification time and do not prove retention.",
      "entrypoints": ["defineQuest"],
      "evidence": [
        "apps/backend/tests/image-capabilities.test.ts",
        "apps/backend/tests/image-sandbox.test.ts",
        "tests/image-examples.test.mjs",
        "packages/sdk/examples/x-profile-picture-quest.ts",
        "snippets/x-profile-picture/x-profile-picture.quest.ts",
        "snippets/unique-photo/unique-photo.quest.ts",
        "snippets/community-screenshot/community-screenshot.quest.ts"
      ],
      "reference": "verification"
    },
    {
      "id": "streaks",
      "support": "native",
      "description": "streak({ days }) pays one lifetime milestone per member after consecutive project calendar days with an accepted attempt. It completes manual quests (the day each submission was admitted, so a manual streak counts submission days) and tracker-triggered quests (the day of each record observation's occurredAt, such as an X post's publication or a Discord message's post time). Several accepted attempts on one day count once, late evidence repairs gaps and merges runs, and a budget-exhausted milestone keeps its days. Days end at midnight in the project timezone, a timezone change keeps runs consecutive, and a published goal never changes. Content quests and prerequisite-only automatic() quests cannot be streaks.",
      "entrypoints": ["streak", "manual", "automatic"],
      "reference": "quest-rules",
      "evidence": [
        "apps/backend/src/ledger/participation/streaks.ts",
        "apps/backend/src/ledger/participation/attempts.ts",
        "apps/backend/src/ledger/model/quest-graph.ts",
        "apps/backend/tests/streaks.test.ts",
        "apps/backend/tests/quest-graph.test.ts",
        "apps/backend/tests/tracker-triggers.test.ts"
      ]
    },
    {
      "id": "project-calendar",
      "support": "native",
      "description": "One timezone per project and environment sets where days, weeks (ISO, from Monday) and months begin for quest budgets, engagement budgets, streak days and calendar leaderboard windows, so a weekly budget and a weekly board cover the same week. Evaluators use the attempt-pinned ctx.calendar.day(at?) for the YYYY-MM-DD@Zone key and exact day bounds; without at it reads ctx.eligibleAt. UTC until set with relay.json calendar.timeZone, Console Settings, or management.calendar.configure (publish permission; live refuses it from every credential with promotion-required and takes test's calendar through a promotion). Named IANA zones only; fixed offsets are rejected, and no quest, budget, streak or board names its own zone. A change takes effect at the next midnight of the current zone: the open day, week or month finishes in the old zone, the next one ends at the new zone's next boundary, and streak days stay consecutive. A deployment applies relay.json's zone only when the declaration changes, so a Console change stays until relay.json is edited. From the CLI, domino calendar get reads the calendar and calendar configure --time-zone --reason changes it as a saved operation (--operation-id).",
      "entrypoints": [
        "management.calendar.read",
        "management.calendar.configure",
        "budget",
        "streak",
        "defineLeaderboard",
        "defineEngagementReward"
      ],
      "cli": ["calendar get", "calendar configure"],
      "reference": "authoring",
      "evidence": [
        "packages/sdk/src/authoring.ts",
        "packages/contracts/src/evaluator-readers.ts",
        "apps/backend/src/workflows/execution-handler.ts",
        "apps/backend/src/ledger/model/calendar.ts",
        "apps/backend/src/ledger/definitions/calendar.ts",
        "apps/backend/tests/ledger/participation/project-calendar.test.ts",
        "apps/backend/tests/project-calendar-store.test.ts",
        "tests/evaluator-compat.test.mjs",
        "packages/management/cli/commands/operator-resources.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "milestones",
      "support": "composed",
      "description": "Compose typed same-participant prerequisites with completed(quest), a project-calendar day/week/month completion, balanceAtLeast(ledger, minimum), or inGroup(group). Completion prerequisites are durable facts; balance and group requirements observe current eligibility. Referral milestones are native, not composed: they count currently qualified friends and recover or restore rewards, with debt, when a friend's qualification changes. Other aggregate thresholds need application logic.",
      "entrypoints": [
        "completed",
        "balanceAtLeast",
        "defineGroup",
        "inGroup",
        "automatic",
        "defineQuest"
      ],
      "reference": "quest-rules",
      "evidence": [
        "apps/backend/src/ledger/participation/admission-facts.ts",
        "apps/backend/tests/campaign-eligibility.test.ts",
        "packages/sdk/src/prerequisites.ts"
      ]
    },
    {
      "id": "groups",
      "support": "native",
      "description": "Project-level groups: named sets of members that inGroup prerequisites, leaderboard group filters and referral inviterGroup rules read by key. defineGroup gives code a typed handle for a key. Publishing code that names a group the environment lacks creates it with no members and the handle's title (its key when only a leaderboard or referral rule names it): a deployment in test, an approved promotion in live, audited as group-created and listed as a group-created warning beforehand; it never changes an existing group's title or members. Operators create groups and manage their members in Console Groups, with domino groups, or with management.groups. Reads need read; changes need publish, which a personal API key can use in live but a CLI session cannot. Every membership change records its reason in the audit history, and adding a member checks prerequisite-only automatic quests. A quest reward, groupMembership(group) beside points(...) and badge(...), adds the member when the completion's rewards are delivered, with the completion as the audited reason; it is how an approved application (manual review) joins a curated group in one step. Publishing refuses a group reward naming an archived group (validation-failed; conflict on promotion approval), since publishing never unarchives a group. Revoking the completion removes the membership it added unless an operator's add or another unrevoked completion also backs it; an archived group keeps the member. Groups are archived, never deleted: an archived group keeps its members, which still count, but its membership cannot change until it is unarchived. A group that the current release of a quest that is not archived rewards cannot be archived (group-in-use, naming the quests). Leaderboard settlements read the membership a group had at their cutoff. A leaderboard's exclusion group is a group too: Relay generates one per board under the reserved domino--exclusions-- key prefix, and a group bound as any board's exclusion group cannot be archived (group-in-use) until those boards are bound elsewhere. management.leaderboards.group is a deprecated alias that creates a missing group on its first member.",
      "entrypoints": [
        "defineGroup",
        "inGroup",
        "groupMembership",
        "management.groups.list",
        "management.groups.get",
        "management.groups.create",
        "management.groups.update",
        "management.groups.archive",
        "management.groups.unarchive",
        "management.groups.members",
        "management.groups.member",
        "management.groups.addMember",
        "management.groups.removeMember"
      ],
      "cli": [
        "groups list",
        "groups get",
        "groups create",
        "groups update",
        "groups archive",
        "groups unarchive",
        "groups members",
        "groups member",
        "groups add",
        "groups remove"
      ],
      "reference": "authoring",
      "evidence": [
        "apps/backend/src/ledger/members/groups.ts",
        "apps/backend/src/ledger/operations/groups.ts",
        "apps/backend/tests/groups.test.ts",
        "apps/backend/src/ledger/accounting/rewards/kinds/group.ts",
        "apps/backend/tests/group-rewards.test.ts",
        "apps/console/src/features/console/groups.tsx",
        "packages/management/cli/commands/groups.mjs",
        "tests/cli-groups.test.mjs"
      ]
    },
    {
      "id": "campaign-eligibility",
      "support": "native",
      "description": "Code-authored campaigns coordinate a half-open time window and optional durable enrollment across quests. Quests may narrow that window. Participant quest views return the campaign, first enrollment time and current withdrawal state, effective window, typed prerequisite failures, stable blocked reasons, and each authored budget's current remaining capacity and reset time. Members may enroll, withdraw and re-enroll themselves. Trusted servers and Console operators with enroll permission may enroll, withdraw with an audited reason, and restore a member. Withdrawal blocks new admissions without deleting attempts, completions, rewards or the first enrollment time.",
      "entrypoints": [
        "defineCampaign",
        "campaigns.enroll",
        "campaigns.withdraw",
        "management.campaigns.enroll",
        "management.campaigns.withdraw",
        "quests.experience"
      ],
      "reference": "quest-rules",
      "evidence": [
        "packages/sdk/src/campaigns.ts",
        "apps/backend/src/ledger/operations/campaigns.ts",
        "apps/backend/src/ledger/participation/admission-facts.ts",
        "apps/backend/tests/campaign-eligibility.test.ts"
      ]
    },
    {
      "id": "referrals",
      "support": "native",
      "description": "Project-scoped invitation codes, immutable inviter attribution, configurable signup, quest or external qualification, fixed rewards and distinct-friend milestones. For several verified actions, qualify through a counter quest with questCompleted(quest). External qualification can be withdrawn and reconciles rewards; a group plus an inGroup quest qualifies once and group removal does not unqualify. Selective one-level inviter bonuses add points without reducing invitee awards. Code-defined rules publish through reviewed project deployments. From the CLI, domino referral-programs list|qualify, referral-relationships list|history|pause|resume and referral-rewards resolve reach the same reads and commands; changes are saved operations (--operation-id).",
      "entrypoints": [
        "defineReferral",
        "referrals.list",
        "referrals.summary",
        "referrals.accept",
        "referrals.history",
        "management.referrals.list",
        "management.referrals.participants",
        "management.referrals.history",
        "management.referrals.qualify",
        "management.referrals.resolveReward",
        "management.referrals.pause",
        "management.referrals.resume"
      ],
      "cli": [
        "referral-programs list",
        "referral-programs qualify",
        "referral-programs archive",
        "referral-programs unarchive",
        "referral-relationships list",
        "referral-relationships history",
        "referral-relationships pause",
        "referral-relationships resume",
        "referral-rewards resolve"
      ],
      "reference": "referrals",
      "evidence": [
        "apps/backend/tests/ledger/accounting/referrals.test.ts",
        "packages/sdk/src/referrals.ts",
        "packages/management/cli/commands/operator-resources.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "cross-participant-rewards",
      "support": "native",
      "description": "Referral programs can reward inviter and invitee independently and award a selective percentage of eligible points to the inviter. Bonuses accumulate fractions and support limits, suspension and correction recovery. Arbitrary participant-to-participant transfers and multi-level commissions are not supported.",
      "entrypoints": ["defineReferral", "referrals.summary"],
      "reference": "referrals",
      "evidence": [
        "apps/backend/tests/ledger/accounting/referrals.test.ts",
        "apps/backend/src/ledger/accounting/referral-operations.ts"
      ]
    },
    {
      "id": "rankings",
      "support": "native",
      "description": "Publish project-scoped points leaderboards through project deployments. Read paginated standings, your rank, and nearby competitors with shared tie ranks, or, on a board defined with ties: \"earliest\", equal scores ordered by who reached them first (the settlement's earliest order), each with its own rank. A signed-in member pages through the point entries behind their own score on a board (me.leaderboardHistory), selected by the board's own window, quests and calculation, so they sum to the score; an excluded member gets none. Code defines balance, earned, or net calculations and time windows. Public access is explicit; participant rows use board-specific aliases unless the project enables profiles, which add each member's name and avatar. A board's optional group ranks only that group's members (see the groups capability). A board defined with includeUnscored also lists its eligible members without qualifying points at score 0, sharing one rank after every member who scored; settlements leave them out. Scoped management APIs supply external points and corrections. A board leaves out the members of its exclusion group: its own generated group (key domino--exclusions--<board ID>), created by its first exclusion, or a group an operator binds with management.leaderboards.bindExclusionGroup so it moderates several boards. management.leaderboards.exclude and exclusions change and read that group, as the groups operations do; exclusion changes need publish, also in live from a personal API key; binding an exclusion group is a definition change that live refuses with promotion-required; both are audited; an external score, such as a tracked wallet balance, gets its own ledger, updated with absolute values (set updates). Referral bonus points contribute to their named balance. Fixed and calendar earned/net boards can declare point and tiered-reward prizes at window end. Relay freezes eligible standings and the members of the exclusion group bound at the cutoff, then a reviewer approves delivery in Console or with management.leaderboards.approveSettlement; each winner gets an ordinary completion that can be revoked. A settlement may also declare a pool prize (settle.pool: a base-unit amount in points or a token unit with decimals, an optional minScore): the freeze splits it over the eligible scores in exact integer math, rounding down and giving leftover units to the largest remainders, and stores each member's pool share on the frozen row. A points pool pays each share into its ledger in the winner's completion on approval, and revoking the completion reverses it; any other unit is only recorded, and management.leaderboards.settlementExport lists each row's share with the member's verified wallets for the operator to pay off-platform, settlementPayoutCsv turns that list into the CSV the CLI prints, and Console downloads it from the settlement panel. Cross-project balances are not supported. A board that counts engagement points first takes a closing measurement: the settlement is closing while Relay resamples the content (a sample within 30 minutes after the cutoff counts as measured at it), then freezes and lists content frozen at its last good sample as unmeasured; management.leaderboards.retrySettlementMeasurement measures it again. Reviewers can reject a pending settlement with an audited reason. Rejection pays no prizes and permanently retains the frozen window; settlementReviews and settlementReview include rejected history, while legacy reads keep their original statuses. From the CLI, domino leaderboards list|standings|history|exclusions|exclusion-group read boards, leaderboards exclude|include change exclusions, leaderboards bind-exclusion-group binds a board's exclusion group, and leaderboard-settlements list|get|approve|reject|retry-measurement review settlements, leaderboard-settlements export prints the payout list as CSV (--json for the typed result); changes are saved operations (--operation-id).",
      "entrypoints": [
        "defineLeaderboard",
        "leaderboards.list",
        "leaderboards.standings",
        "me.leaderboardHistory",
        "management.leaderboards.list",
        "management.leaderboards.standings",
        "management.leaderboards.history",
        "management.leaderboards.exclusions",
        "management.leaderboards.exclude",
        "management.leaderboards.exclusionGroup",
        "management.leaderboards.bindExclusionGroup",
        "management.leaderboards.group",
        "management.leaderboards.settlements",
        "management.leaderboards.settlement",
        "management.leaderboards.approveSettlement",
        "management.completions.revoke",
        "management.leaderboards.settlementReviews",
        "management.leaderboards.settlementReview",
        "management.leaderboards.settlementExport",
        "management.leaderboards.rejectSettlement",
        "management.leaderboards.retrySettlementMeasurement"
      ],
      "cli": [
        "leaderboards list",
        "leaderboards standings",
        "leaderboards history",
        "leaderboards exclusions",
        "leaderboards exclude",
        "leaderboards include",
        "leaderboards exclusion-group",
        "leaderboards bind-exclusion-group",
        "leaderboards archive",
        "leaderboards unarchive",
        "leaderboard-settlements list",
        "leaderboard-settlements get",
        "leaderboard-settlements export",
        "leaderboard-settlements approve",
        "leaderboard-settlements reject",
        "leaderboard-settlements retry-measurement"
      ],
      "evidence": [
        "apps/backend/tests/ledger/accounting/leaderboards.test.ts",
        "apps/backend/tests/ledger/accounting/leaderboard-settlements.test.ts",
        "apps/backend/tests/ledger/accounting/leaderboard-pool-settlements.test.ts",
        "apps/backend/tests/ledger/accounting/leaderboard-exclusion-groups.test.ts",
        "apps/backend/tests/gateway/participant-ledger.test.ts",
        "apps/backend/src/ledger/accounting/leaderboard-settlements.ts",
        "apps/backend/src/ledger/accounting/leaderboard-exclusions.ts",
        "apps/backend/src/ledger/operations/leaderboard-settlements.ts",
        "apps/console/src/features/console/leaderboards.tsx",
        "packages/sdk/src/leaderboards.ts",
        "apps/console/src/features/console/settlement-reject.tsx",
        "packages/management/cli/commands/operator-resources.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "hosted-app-secrets",
      "support": "native",
      "description": "Operator-set secrets for a hosted app's server code, separate in each environment: test's reach staged builds and live's the live app; a promotion never copies them and warns (binding-missing) about ones live lacks. Worker apps declare the secrets they read, with descriptions and required flags, in relay.json app.secrets; domino stage warns about missing required ones and reports missingSecrets without blocking, and domino secrets list reports declared secrets that are missing. domino secrets set/list/delete (test unless --environment live; RELAY_ENVIRONMENT and relay.json are ignored, with a warning when they name live, and the result's environment says which was used), Console Settings → App secrets with Test or Live selected, or the management API; publish in that environment changes them, read lists names, and a CLI session may set and delete them in live. Values are write-only, 1 byte to 5 KiB, at most 50 per project environment; names are uppercase letters, digits and underscores, excluding DOMINO_ names and ASSETS. Worker builds staged after a change receive them as secret bindings, replacing app vars of the same name; earlier builds keep their values. A live change reaches the live app with the next build promoted to live. Static apps and domino dev receive none. KV and D1 bindings are declared separately under app.storage. domino secrets set checks a value starting with di_ as an app login key before storing it (see identity-exchange).",
      "entrypoints": [
        "management.secrets.list",
        "management.secrets.set",
        "management.secrets.delete"
      ],
      "cli": ["secrets list", "secrets set", "secrets delete"],
      "reference": "hosted",
      "evidence": [
        "apps/backend/src/management/app-secrets.ts",
        "apps/backend/tests/gateway/management-app-secrets.test.ts",
        "packages/management/cli/commands/secrets.mjs",
        "tests/cli-secrets.test.mjs"
      ]
    },
    {
      "id": "plugin-connections",
      "support": "native",
      "description": "Experimental while the plugin contract is new: management.connections and the domino connections commands may change in a minor release. A project connects to plugins, the integrations Domino writes, separately in each environment; a promotion never copies a connection. management.connections.list or domino connections list answers every plugin available to the project, a never-connected one as disconnected with revision 0; get reads one (404 unknown-plugin otherwise), with its status (disconnected, ready, or attention with an issue), config, the JSON Schema the config must satisfy, its secret fields and whether each is set, and its revision. connect takes the config and a value for every secret field (422 invalid-config with the failing path, 422 invalid-secrets with the field, 409 already-connected); update replaces the config, some secrets or both and sets the status to ready (409 not-connected, 409 connection-changed on a stale expectedRevision); disconnect deletes the secrets and keeps the connection as disconnected, and connect works again. A plugin whose manifest declares OAuth connects through the vendor instead: authorize (domino connections authorize) takes the config and answers the vendor's authorizationUrl, which works once for ten minutes; the vendor's callback sends the browser to Console with a one-time oauthCode, and completeAuthorization redeems it within five minutes, for the person who started it only (403 scope-mismatch for anyone else, 409 oauth-flow-expired for a spent or expired code), making the connection ready with its tokens sealed on it; an automation, service or hosted app key can't authorize (403 credential-not-allowed); connect refuses such a plugin with 409 oauth-required, authorize refuses one connected with secrets with 409 oauth-not-supported, and a deployment without the vendor's OAuth app with 409 plugin-not-configured. A forged, replayed or expired state is refused and changes nothing. Domino refreshes the tokens before they expire; a refresh the vendor refuses, or tokens that can't be refreshed any more, set attention with reconnectReason revoked or expired, which only authorizing again clears; disconnect deletes the tokens, then revokes them at the vendor's revocation endpoint, logging a failed revocation. Every change takes an actionId, so a retry applies once; finishing an OAuth connection is once by its code instead, and a repeat answers 409 oauth-flow-expired. Reads need read and changes publish; a CLI session may change connections in live, an automation key or MCP client in test only. Secrets and OAuth tokens are sealed at rest and write-only: no read, error, log or CLI output answers one, and the CLI reads them from stdin as JSON or a hidden prompt, never from arguments. Test or live is chosen only by --environment (test by default). management.plugins.list answers the plugins the deployment runs with the public part of each manifest (name, slots, config schema, secret field names and labels, the events a destination receives, and for an OAuth plugin its scopes and whether the deployment has its OAuth app); it needs read, and Console's Integrations page builds each plugin's connection form from it. A plugin that connects through its own flow, such as Telegram's chats through Domino's bot, has no settings or secrets: its status comes from what the project connected, and connect, update and disconnect answer 409 plugin-managed. Each plugins.list entry also says which bespoke Console component, if any, replaces the generated card. A destination plugin whose manifest allows a dry run connects in the test environment without secrets or the vendor's account with connect { dryRun: true } (domino connections connect --dry-run): it records each request it would have sent instead (dryRunLog, domino connections log, the latest 500 kept), and authorizing the vendor's account turns the dry run off and starts the backfill again; 409 dry-run-not-supported and dry-run-test-only otherwise. A destination's activity (domino connections activity) answers its last sync, whether it is failing (10 failed deliveries in a row, or a day of failing deliveries without one success, until the vendor accepts one), members synced and skipped by reason, its latest backfill or resync with progress, and its latest 20 errors; resync (domino connections resync) sends every member's profile and attributes again, never past events, one at a time. These answer 409 not-a-destination for other plugins.",
      "entrypoints": [
        "management.connections.list",
        "management.connections.get",
        "management.connections.connect",
        "management.connections.authorize",
        "management.connections.completeAuthorization",
        "management.connections.update",
        "management.connections.disconnect",
        "management.connections.activity",
        "management.connections.resync",
        "management.connections.dryRunLog",
        "management.plugins.list"
      ],
      "cli": [
        "connections list",
        "connections get",
        "connections connect",
        "connections authorize",
        "connections update",
        "connections disconnect",
        "connections activity",
        "connections resync",
        "connections log"
      ],
      "reference": "management",
      "evidence": [
        "apps/backend/src/plugins/connections.ts",
        "apps/backend/src/plugins/oauth.ts",
        "apps/backend/tests/gateway/management-connections.test.ts",
        "apps/backend/tests/gateway/management-connections-oauth.test.ts",
        "packages/management/cli/commands/connections.mjs",
        "tests/cli-connections.test.mjs",
        "apps/backend/src/plugins/deliveries.ts",
        "apps/backend/src/workflows/destinations.ts"
      ]
    },
    {
      "id": "starter-export",
      "support": "native",
      "description": "domino starters export <template> --directory <dir> writes the starter that domino create would seed a new repository with today (the current promoted starter for the CLI release that published it) into a new or empty directory, including .domino/starter.json with its sourceCommit and lockfile hash, so an existing hosted project can move to a newer starter. The files are not personalized with a project name or brief, and nothing is installed: integrate them into the checkout on a branch, then run the package manager's install command and domino doctor. Any project reader can export, in the --project/--organization scope; only offered starter IDs are accepted, and a directory with files in it is refused with LOCAL_CONFLICT before anything is written. Older starter versions cannot be exported. Management API: GET /starters/{id} (read).",
      "entrypoints": [],
      "cli": ["starters export"],
      "reference": "hosted",
      "evidence": [
        "apps/backend/src/public/routes/hosting-repositories-routes.ts",
        "apps/backend/tests/gateway/management-hosting.test.ts",
        "packages/management/cli/commands/starters.mjs",
        "tests/cli-starters.test.mjs"
      ]
    },
    {
      "id": "profiles",
      "support": "native",
      "description": "Participant profiles per project and environment: members set a display name, upload an avatar (JPEG, PNG or WebP up to 5 MB, stored as a 512x512 WebP without metadata), pick an X, Discord or Google picture or the generated avatar, and can hide their profile. Operators choose who sees profiles (private by default, signed-in participants, or anyone) and which accounts fill in names and pictures, as an ordered list of X and Discord (empty for generated). Apps compose profile pages from the profile (name, avatar, public ID) and separate reads keyed by it, each behind the profile's visibility and its own resource's access: a board placement (the board's access), completions of quests that opt in with defineQuest profile: { visible: true } (paginated), and referral progress counts (signed-in viewers). Generated avatars are seeded by an HMAC of the member under a project secret, unchanged by linked wallets. Leaderboard rows and profile reads never show emails, wallet addresses or member IDs. Reviewers reset names, remove pictures and lock profiles with audited reasons. Live refuses settings changes from every credential with promotion-required. With the defaults every leaderboard row shows a generated name such as Participant 1a2b3c4d; domino stage and domino doctor --remote warn, without failing, when a project with leaderboards has private profiles or no default account in test, and print the domino profiles configure command that changes it. Badge reads keyed by the profile (profiles.badges) follow its visibility. No handles or vanity URLs or X analytics. From the CLI, domino profiles settings reads the settings, profiles configure <file> replaces them, and profiles moderate <member> resets, removes or locks one member's profile; changes are saved operations (--operation-id).",
      "entrypoints": [
        "profiles.mine",
        "profiles.update",
        "profiles.uploadAvatar",
        "profiles.get",
        "profiles.completions",
        "profiles.referral",
        "leaderboards.placement",
        "management.profiles.settings",
        "management.profiles.configure",
        "management.profiles.moderate"
      ],
      "cli": ["profiles settings", "profiles configure", "profiles moderate"],
      "reference": "participant-api",
      "evidence": [
        "apps/backend/src/ledger/members/profiles.ts",
        "apps/backend/src/public/avatars.ts",
        "apps/backend/tests/profiles.test.ts",
        "apps/backend/tests/gateway/participant-profile.test.ts",
        "packages/management/cli/commands/operator-resources.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "marketing-consent",
      "support": "native",
      "description": "Experimental. The signed-in member records their marketing opt-in or withdrawal per channel (email only; other channels are refused with unsupported-consent-channel) through relay.consent.set or the useConsent() React hook, and reads the current state of each channel they chose on with relay.consent.get. Relay stamps its own time and stores the wording the app showed verbatim (required to grant, at most 2,000 characters); the history is append-only, a repeat of the current state and wording records nothing, and a channel never recorded has no entry rather than reading as withdrawn. Consent is per project and environment, and test-member reset deletes it. The app shows the checkbox; Relay sends no messages. Operators with read permission read a member's current consent per channel (management.members.consent) and page through its history, every change on every channel newest first with its wording, time and source (management.members.consentHistory, { items, nextCursor }); domino members consent MEMBER prints both, and the Console member page shows them. An unknown member is 404 not-found; a member who never chose reads as empty. Operators cannot grant or withdraw consent for a member. No consent in member imports yet, and no destination that syncs it.",
      "entrypoints": [
        "consent.get",
        "consent.set",
        "management.members.consent",
        "management.members.consentHistory"
      ],
      "cli": ["members consent"],
      "reference": "participant-api",
      "evidence": [
        "apps/backend/src/ledger/operations/consent.ts",
        "apps/backend/src/ledger/members/consent.ts",
        "apps/backend/src/public/routes/participant-consent-routes.ts",
        "apps/backend/src/public/routes/management-consent-routes.ts",
        "apps/backend/tests/consent.test.ts",
        "apps/console/src/features/console/member-consent.tsx"
      ]
    },
    {
      "id": "wallet-linking",
      "support": "native",
      "description": "Participants link up to five Solana, EVM (EOA) and Cosmos SDK wallets per chain family to a project by signing a challenge; a Cosmos wallet is a bech32 address on any chain ID the app chooses (no built-in chain list); they can unlink; operators see and unlink them in Console. Evaluators read the pinned list with ctx.wallet.list(), and chain-specific balances with ctx.wallet.balance({ chain }). Linking never enables sign-in on its own (see wallet-sign-in). No smart-contract wallets. Nothing sends assets or executes payouts. Reviewers relink a retained proved wallet with members.identityStates.relink, preserving its original proof and first-link time, ownership and sign-in settings. Historical unlinked activity follows the current quest policy; admitted attempts stay pinned.",
      "entrypoints": [
        "wallets.list",
        "wallets.challenge",
        "wallets.verify",
        "wallets.unlink",
        "management.members.identityStates.list",
        "management.members.identityStates.unlink",
        "management.members.identityStates.relink"
      ],
      "cli": [
        "members identities",
        "members unlink-identity",
        "members relink-identity"
      ],
      "reference": "participant-api",
      "evidence": [
        "apps/backend/src/auth/wallets.ts",
        "apps/backend/src/kernel/wallet-proofs.ts",
        "apps/backend/tests/wallet-proofs.test.ts",
        "apps/backend/src/app/execution-providers.ts",
        "apps/backend/src/auth/identity-links.ts",
        "apps/backend/tests/tracker-admission-rules.test.ts"
      ]
    },
    {
      "id": "wallet-sign-in",
      "support": "native",
      "description": "Participants sign in, or sign up, with a Solana, EVM (EOA) or Cosmos SDK wallet by signing a challenge, where the project enables that wallet family (solana, eip155 or cosmos) per environment (off by default; Console Settings > Sign-in or PUT /participant-sign-in). domino stage and domino doctor --remote warn, without failing, when an app that signs in with a wallet has no wallet family enabled in test, and print the command that enables one. Cosmos wallets sign in on any chain the app offers; apps list the options with useSignInOptions({ cosmos }), which offers Cosmos wallets only on the chains the app names. A new address creates a member. Wallets linked while signed in cannot sign in; reviewers enable or disable sign-in per wallet with an audited reason, through its own command (members.identityStates.setSignIn; setWalletSignIn is its deprecated adapter), and unlinking a wallet never changes its sign-in. Identity-integration apps cannot use it. relay.me.get() returns an avatar seed that does not reveal the address, for defaultAvatar(seed).",
      "entrypoints": [
        "auth.wallet.start",
        "auth.wallet.verify",
        "auth.wallet.signIn",
        "me.get",
        "management.participantSignIn",
        "management.configureParticipantSignIn",
        "management.members.setWalletSignIn",
        "management.members.identityStates.setSignIn"
      ],
      "cli": ["members identity-sign-in"],
      "reference": "participant-api",
      "evidence": [
        "apps/backend/src/public/routes/sign-in.ts",
        "apps/backend/src/auth/wallets.ts",
        "apps/backend/src/db/avatar-seeds.ts",
        "apps/backend/src/auth/sign-in-settings.ts",
        "apps/backend/tests/gateway/participant-wallet-sign-in.test.ts",
        "apps/backend/tests/wallet-sign-in.test.ts"
      ]
    },
    {
      "id": "telegram-sign-in",
      "support": "native",
      "description": "Participants sign in, or sign up, with Telegram: Telegram Login (OpenID Connect on Domino's bot) where Domino configures it, and inside a project's own Telegram Mini App where the project names its bot ID per environment (Console Settings > Sign-in or PUT /participant-sign-in with telegram.miniAppBotId; Domino never holds the bot token). Mini App launch data must carry Telegram's Ed25519 signature for that bot, be at most five minutes old, and is accepted once. Both reach one identity, the numeric Telegram user ID; the phone number is never requested. Signed-in members connect and disconnect Telegram; operators read the selection and reviewers unlink and relink it with an audited reason (members.identityStates). In SDK 1.x Telegram is not an AuthProvider, so the closed provider lists leave it out; the open sign-in lists include it: providers().signInProviders, useSignInOptions().signInProviders, auth.signInAccounts() and useAccount(\"telegram\"), and for operators participantSignIn().signInProviders, members.accounts and members.unlinkAccount, whose provider is an open string; auth.start and auth.disconnect take \"telegram\". Mini App sign-in is relay.telegram.miniApp from @domino-sdk/relay/telegram (withTelegram(relay), experimental). Telegram's own relay.auth.telegram.start, account, disconnect and miniApp, telegramMiniAppInitData from @domino-sdk/relay/browser and /react, useTelegramAccount(), useSignInOptions().telegram, providers().telegram.login, members.telegramAccount and unlinkTelegramAccount are deprecated, keep working through 1.x and are removed in 2.0.0.",
      "entrypoints": [
        "auth.start",
        "auth.disconnect",
        "auth.providers",
        "auth.telegram.miniApp.signIn",
        "auth.telegram.miniApp.connect",
        "management.participantSignIn",
        "management.configureParticipantSignIn",
        "auth.signInAccounts",
        "management.members.accounts",
        "management.members.unlinkAccount",
        "management.members.identityStates.list",
        "management.members.identityStates.unlink",
        "management.members.identityStates.relink"
      ],
      "reference": "participant-api",
      "evidence": [
        "apps/backend/src/public/oauth-providers.ts",
        "packages/plugins/telegram/src/identity.ts",
        "apps/backend/src/public/routes/sign-in-plugin.ts",
        "packages/sdk/src/plugins/telegram.generated.ts",
        "apps/backend/tests/gateway/participant-telegram-sign-in.test.ts",
        "apps/backend/tests/telegram-mini-app.test.ts"
      ]
    },
    {
      "id": "discord-connection",
      "support": "native",
      "description": "Each project connects one Discord server per environment, which Discord quests, trackers, role rewards and announcements use. Installing Domino's bot into a server takes an organization administrator in Console Integrations, through Discord's own installation, which proves they manage the server; an API key, automation key or CLI session cannot install it. Where the shared bot already holds permissions in the server (such as Manage Roles), only the owner, an Administrator, or someone holding all of them can install it there. An administrator without Manage Server sends a one-time install link (Send to your server admin, valid seven days) to whoever manages the server, who needs no Domino account; the link first opens a page naming the organization, the project's name and slug, and the environment it connects to, with a Continue link to Discord. Once one project of the organization connects a server and it is ready in an environment, any credential with publish permission (an automation key, the CLI or Console) connects that server to another project of the same organization and environment without installing again: management.discord.servers (domino integrations discord servers) lists those servers, and management.discord.connect(server) (domino integrations discord connect --server <id>) connects one after Domino's bot still sees it. It refuses with 409 server-not-connected-in-organization for a server no other project connects ready in that environment (an administrator installs it), 409 integration-connected when the project already has a Discord connection, even one needing attention (an administrator changes or repairs it), and 422 server-not-reachable when the bot lost the server; connecting the server the project already connects changes nothing. The audit records who connected it and which project's connection it reused (discord-server-connected). management.discord.connection (domino integrations discord get; status is its deprecated former name) reads the project's connection. An automation key and a CLI session cannot connect or change the Discord server in live. Check access, reconnecting and disconnecting stay administrator-only in Console.",
      "entrypoints": [
        "management.discord.connection",
        "management.discord.servers",
        "management.discord.connect"
      ],
      "cli": [
        "integrations discord get",
        "integrations discord servers",
        "integrations discord connect"
      ],
      "reference": "authoring",
      "evidence": [
        "apps/backend/src/public/routes/integrations.ts",
        "apps/backend/src/app/integration-service.ts",
        "apps/backend/tests/discord-server-reuse.test.ts",
        "apps/backend/tests/gateway/management-integrations.test.ts"
      ]
    },
    {
      "id": "telegram-membership",
      "support": "native",
      "description": "Operators connect Telegram groups, supergroups and channels they administer to a project and environment through Domino's shared bot: Console Integrations > Telegram > Add chat (management.telegram.connect) returns a one-time t.me link; the operator starts the bot, which first names the organization, the project's name and slug, and the environment the chat will connect to, and picks the chat with Telegram's chat picker, which adds the bot as an administrator, and Relay checks both administrator statuses before binding it; an operator who is not the chat's creator must also hold every administrator right the shared bot holds there. A project binds several chats; management.telegram.chats lists them with bot health (an issue once the bot loses administrator status), management.telegram.remove unbinds one and the bot leaves when no project uses it. Evaluator ctx.telegram.connection() reads the member's linked Telegram account, vouched or proved, as { userId } or null, without a chat or the bot; ctx.telegram.member({ chat }) reads that account in a connected chat, by numeric ID or @username: { userId, username, status, isMember, isAdmin, untilDate } or null when they are not in it or have not linked Telegram. It needs no integrations declaration; the account is pinned at the attempt's first Telegram read; an unconnected chat or a chat whose bot lost access throws CapabilityError integration-required, an outage provider-unavailable. settings.telegramChat() lets operators pick a connected chat in Console. Inert until Domino configures its bot. CLI uses the routes through domino api.",
      "entrypoints": [
        "defineQuest",
        "settings.telegramChat",
        "management.telegram.chats",
        "management.telegram.connect",
        "management.telegram.remove"
      ],
      "reference": "trackers",
      "evidence": [
        "packages/plugins/telegram/src/install.ts",
        "apps/backend/src/workflows/capabilities.ts",
        "apps/backend/tests/gateway/management-telegram.test.ts",
        "apps/backend/tests/telegram-capabilities.test.ts",
        "snippets/telegram-membership/telegram-membership.quest.ts"
      ]
    },
    {
      "id": "telegram-reactions",
      "support": "native",
      "description": "Tracker source telegram.reactions() records the reactions members add to messages in the project's connected groups and supergroups, one record per added reaction, from Domino's bot webhook. Each record's author is the reactor's Telegram account and its messageAuthor account the message's author (known for messages the bot saw since the chat was connected); a record is kept only when one of them is a Telegram account a member of the tracker's project linked, and a removed reaction marks its record removed. data carries chatId, chatType, messageId, reactionType, emoji, customEmojiId, reactorStatus and reactorIsAdmin (from the chat's administrators, cached for ten minutes), so a trigger's where selects an emoji or administrators' reactions; pay the message's author with beneficiary messageAuthor. Channel reactions are anonymous and never recorded; reactions from before the bot was an administrator cannot be recovered. In SDK 1.x the source is a custom source on the reserved namespace telegram-reactions, which ingestion refuses. Message author IDs are kept 90 days and never message text.",
      "entrypoints": ["telegram.reactions", "defineTracker"],
      "reference": "trackers",
      "evidence": [
        "packages/plugins/telegram/src/reactions.ts",
        "packages/plugins/telegram/src/records.ts",
        "apps/backend/src/trackers/plugin-records.ts",
        "apps/backend/tests/telegram-reactions.test.ts"
      ]
    },
    {
      "id": "telegram-messages",
      "support": "native",
      "description": "Tracker source telegram.messages() records the messages people post in the project's connected groups and supergroups, one record per message keyed chatId:messageId, from Domino's bot webhook, for chats connected before the message. Each record's author is the poster's Telegram account, and a record is kept only when it is a Telegram account a member of the tracker's project linked. data carries chatId, chatType, messageId, replyToMessageId and threadId, never message text. Bots' messages, service messages, messages sent on behalf of a chat and channel posts are never recorded; an edit is the same message, and a deleted message stays recorded because the Bot API sends no deletes. The telegram-contribution snippet pays once per member per day. In SDK 1.x the source is a custom source on the reserved namespace telegram-messages, which ingestion refuses.",
      "entrypoints": ["telegram.messages", "defineTracker"],
      "reference": "trackers",
      "evidence": [
        "packages/plugins/telegram/src/messages.ts",
        "packages/plugins/telegram/src/records.ts",
        "apps/backend/src/trackers/plugin-records.ts",
        "apps/backend/tests/telegram-messages.test.ts",
        "snippets/telegram-contribution/telegram-contribution.quest.ts"
      ]
    },
    {
      "id": "creator-accounts",
      "support": "native",
      "description": "Signed-in members link creator accounts: the platform accounts they publish from, which managed tracker sources read with the member's own kept OAuth token. YouTube: Google OAuth on a client of its own (YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET; absent everywhere until Domino configures it), asking for youtube.readonly with offline access and Google's channel picker; the account's subject is the picked channel's ID (UC…), never the Google account. TikTok: Login Kit (TIKTOK_CLIENT_KEY, TIKTOK_CLIENT_SECRET; absent everywhere until Domino configures it), asking for user.info.basic and video.list; the subject is the account's open_id, never the @username, and its profile has a name and picture but no username. A creator account is never a sign-in method, linking another one on the same platform replaces it, and one is linked per member per project and environment. Members list, link and unlink them (GET /v1/auth/creator-accounts, POST /v1/auth/start with mode connect, POST /v1/auth/accounts/disconnect); each entry mirrors AccountConnection, with status reconnect once Relay can no longer read it. Operators read them on members.get (creatorAccounts) and members.creatorAccounts, and reviewers unlink one with an audited reason. In SDK 1.x creator accounts sit beside the closed provider lists: provider is an open string, with knownCreatorAccountProviders listing the values this SDK knows. CLI uses the routes through domino api.",
      "entrypoints": [
        "auth.creatorAccounts.list",
        "auth.creatorAccounts.connect",
        "auth.creatorAccounts.disconnect",
        "management.members.get",
        "management.members.creatorAccounts",
        "management.members.unlinkCreatorAccount"
      ],
      "reference": "participant-api",
      "evidence": [
        "apps/backend/src/public/oauth-providers.ts",
        "apps/backend/src/auth/creator-accounts.ts",
        "apps/backend/tests/gateway/participant-creator-accounts.test.ts",
        "apps/backend/tests/gateway/participant-tiktok-accounts.test.ts",
        "apps/backend/tests/public-sign-in.test.ts"
      ]
    },
    {
      "id": "youtube-shorts",
      "support": "native",
      "description": "Tracker source youtube.shorts() reads the Shorts members publish on the YouTube channels they linked as creator accounts, with each member's own token; there is no keyword search and no API key. Relay looks for new Shorts and refreshes their counters every refreshSeconds of the tracker: candidates come from the channel's Shorts playlist (the uploads playlist when that is missing) and count only at most 180 seconds long (60 for uploads before 2024-10-15) and vertical or square. Each record is one Short keyed by its video ID, its author the channel, attributed to the member who linked it, so link times, the historical policy and re-decision on link work as for X. Its content body is title, description, tags, durationSeconds and publishedAt, measured by views, likes and comments, so content() quests, performance and engagement rules use it; a counter YouTube hides is unknown, never zero. A Short that is deleted or made private is a content removal; an unlinked channel is no longer read and its records stay. A token that needs reconnecting, an outage or a spent daily quota (YouTube Data API units per environment per Pacific-time day) is provider-unavailable and never removes anything; a spent quota defers collection to the next quota day. YouTube counts Shorts views on every play from 2025-03-31, and every format at playback start from 2026-08-24, so counts are not comparable across those dates. In SDK 1.x the source is a custom source on the reserved namespace youtube-shorts, which ingestion refuses.",
      "entrypoints": ["youtube.shorts", "defineTracker"],
      "reference": "trackers",
      "evidence": [
        "apps/backend/src/trackers/youtube.ts",
        "apps/backend/src/trackers/sources.ts",
        "apps/backend/tests/youtube-shorts.test.ts"
      ]
    },
    {
      "id": "tiktok-videos",
      "support": "native",
      "description": "Tracker source tiktok.videos() reads the public videos members post on the TikTok accounts they linked as creator accounts, only ever with each member's own token: TikTok's Display API has no app-only read, so there is no search and an account whose token needs reconnecting is not read at all. Relay looks for new videos and refreshes their counters every refreshSeconds of the tracker: discovery pages through the account's video list, newest first, back to the window start; refresh queries known videos 20 at a time. Each record is one video keyed by its video ID, its author the account's open_id, attributed to the member who linked it, so link times, the historical policy and re-decision on link work as for X. Its content body is title, description (the caption), durationSeconds, width, height, createdAt, shareUrl and isAigc, measured by views, likes, comments and shares, so content() quests, performance and engagement rules use it; a counter TikTok leaves out is unknown, never zero. A known video the owner's account no longer returns is a content removal; an unlinked account is no longer read and its records stay. An account whose token needs reconnecting, or whose token TikTok refuses even after a refresh, is provider-unavailable, and a refused token whose refresh fails marks the account needs-reconnect at once; TikTok's per-endpoint request limit, counted for the whole platform over any 60 seconds, is provider-unavailable too and defers collection to the next minute; any other failure names the failure and retries from the saved cursor. None of these removes anything. In SDK 1.x the source is a custom source on the reserved namespace tiktok-videos, which ingestion refuses.",
      "entrypoints": ["tiktok.videos", "defineTracker"],
      "reference": "trackers",
      "evidence": [
        "apps/backend/src/trackers/tiktok.ts",
        "apps/backend/src/trackers/sources.ts",
        "apps/backend/tests/tiktok-videos.test.ts"
      ]
    },
    {
      "id": "wallet-balance-samples",
      "support": "native",
      "description": "Tracker source wallet.balances({ chain, token }) samples one token's balance in every wallet linked by a member who joined the project, once per project-calendar day at a time derived from a secret Relay holds, the tracker and the date, so members can't predict it. chain and token are those of ctx.wallet.balance(): Solana with SOL or an SPL/Token-2022 mint, an EVM chain with its native coin or an ERC-20, or a Cosmos chain with a bank denom. One run pins the chain's EVM block number or Cosmos height and reads every wallet at it, 100 wallets per step; on Solana every read requires minContextSlot = the run's pinned slot. Each record is one wallet keyed by its CAIP-10 account on the sampled chain, also its author; each day's sample is a new observation with occurredAt the sample time and data { amount (base units, decimal string), decimals, block }. A provider outage writes nothing and retries the same pin 15 minutes later; a run whose day ends is abandoned; Relay never writes a balance it didn't read, so a day with no successful sample breaks a streak. A second run on a sampled day writes nothing. Quests use ordinary tracker triggers, such as automatic({ tracker, where: { path: \"amount\", gte: { setting: \"threshold\" } } }) with streak({ days }), and total a member's wallets with trackers.records. At most 5 unarchived wallet.balances() trackers per project and environment (limit-reached). In SDK 1.x the source is a custom source on the reserved namespace wallet-balances, which ingestion refuses; data trackers take { kind: wallet-balances, chain, token }.",
      "entrypoints": ["wallet.balances", "defineTracker"],
      "reference": "trackers",
      "evidence": [
        "apps/backend/src/trackers/wallet-balances.ts",
        "apps/backend/src/trackers/sources.ts",
        "apps/backend/tests/wallet-balances.test.ts"
      ]
    },
    {
      "id": "wallet-balances",
      "support": "native",
      "description": "Evaluator ctx.wallet.balance({ chain, token?, address? }) reads SOL and SPL tokens on Solana mainnet, native and ERC-20 balances on listed EVM chains, and bank denominations on the listed Cosmos SDK chains (Cosmos Hub, Osmosis and any the platform operator configured) for wallets pinned on that exact Cosmos network. management.walletBalanceChains() (GET /wallet-balance-chains) lists supported chains. Cosmos reads use platform-owned HTTPS REST endpoints (never a customer-supplied URL), verify the chain ID, and pin bank and metadata requests to one block. Returns raw integer strings and decimals (or null when metadata is unavailable), or null when the member has no readable wallet, captured for replay; one of eight capability calls. Failures throw CapabilityError and never read as zero. An EVM token that is an NFT contract (ERC-165) throws author-error naming ctx.wallet.nfts(). No historical balances; a receipt token of staking (liquid-staking tokens, ERC-4626 shares) is read as a token, other staking with wallet-call or wallet-delegations.",
      "entrypoints": [
        "parseUnits",
        "formatUnits",
        "management.walletBalanceChains",
        "isEvmChainId"
      ],
      "reference": "verification",
      "evidence": [
        "apps/backend/src/providers/evm.ts",
        "apps/backend/src/providers/evm-chains.ts",
        "apps/backend/src/providers/cosmos.ts",
        "apps/backend/src/providers/cosmos-chains.ts",
        "apps/backend/src/providers/wallet-balance.ts",
        "apps/backend/src/providers/solana.ts",
        "apps/backend/tests/evm-provider.test.ts",
        "apps/backend/tests/cosmos-provider.test.ts",
        "apps/backend/tests/solana-provider.test.ts",
        "apps/backend/tests/wallet-capabilities.test.ts"
      ]
    },
    {
      "id": "wallet-nfts",
      "support": "native",
      "description": "Evaluator ctx.wallet.nfts({ collection, address? }) counts the NFTs of one collection the member's pinned linked wallets of its family hold: on EVM, ERC-721 balanceOf(owner) or one ERC-1155 token ID's balanceOf(owner, id), through dRPC on the chains management.walletBalanceChains() lists; on Solana mainnet, the NFTs (0-decimal, supply-1 mints held as 1 in an SPL Token or Token-2022 account) whose Metaplex metadata names the collection with verified: true, through the platform's Solana RPC. The collection is a CAIP-19 asset type from settings.nft() (eip155:<chain ID>/erc721:<contract>, eip155:<chain ID>/erc1155:<contract>/<token ID>, or solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:<collection mint>); publication, Console settings edits and unarchiving check it: an EVM chain and that the contract reports the standard through ERC-165 supportsInterface (a platform without its dRPC key refuses every EVM collection), or that a Solana mint is a Metaplex collection NFT. Returns { collection, chain, standard, total, wallets: [{ address, count }], at, observedAt } with integer-string counts, or null when the member has no linked wallet of the collection's family, captured for replay; one of eight capability calls. Failures throw CapabilityError and never read as holding none. Snippet nft-holder. No token ID listing, Solana compressed NFTs or Metaplex Core assets, or Cosmos NFTs.",
      "entrypoints": ["settings.nft", "isNftCollection", "parseNftCollection"],
      "reference": "verification",
      "evidence": [
        "apps/backend/src/providers/evm.ts",
        "apps/backend/src/providers/wallet-nfts.ts",
        "apps/backend/src/application/publication-preflight.ts",
        "apps/backend/tests/evm-provider.test.ts",
        "apps/backend/tests/wallet-capabilities.test.ts",
        "apps/backend/tests/nft-settings-preflight.test.ts",
        "snippets/nft-holder/nft-holder.quest.ts",
        "tests/nft-examples.test.mjs"
      ]
    },
    {
      "id": "wallet-call",
      "support": "native",
      "description": "Evaluator ctx.wallet.call({ call, args?, address? }) calls one view or pure EVM contract function once per pinned linked EVM wallet, every call at one block (eth_blockNumber, then batched eth_call at it), through dRPC on the chains management.walletBalanceChains() lists, for state a contract keeps per address such as stakedOf(address), getVotes(address) or userInfo(uint256, address). call is a settings.contractCall() value: the contract's CAIP-10 ID, a space and a human-readable signature such as \"eip155:8453:0x… function stakedOf(address) view returns (uint256)\"; only view or pure functions with elementary parameters returning one uint, int, bool or address are accepted, and publication, Console settings edits and unarchiving also check the chain. args fill the parameters in order; { wallet: true } stands for each wallet's address in at most one address parameter. Returns { call, chain, contract, returns, total, wallets: [{ address, value }], at, observedAt } with integers as decimal strings (total their sum, null for bool and address), or null when the member has no linked EVM wallet, captured for replay; one of eight capability calls. A revert (with its reason), no contract, undecodable data, arguments that do not fit and an unlinked address throw author-error; outages throw provider-unavailable, never zero. Snippet contract-state. No Solana or Cosmos contracts, history or events.",
      "entrypoints": [
        "settings.contractCall",
        "isContractCall",
        "parseContractCall",
        "parseContractFunction"
      ],
      "reference": "verification",
      "evidence": [
        "apps/backend/src/providers/evm.ts",
        "apps/backend/src/providers/wallet-call.ts",
        "apps/backend/src/application/publication-preflight.ts",
        "apps/backend/tests/contract-call-provider.test.ts",
        "apps/backend/tests/wallet-capabilities.test.ts",
        "apps/backend/tests/nft-settings-preflight.test.ts",
        "snippets/contract-state/contract-state.quest.ts",
        "tests/staking-examples.test.mjs"
      ]
    },
    {
      "id": "wallet-delegations",
      "support": "native",
      "description": "Evaluator ctx.wallet.delegations({ chain, validator?, address? }) reads the native staking delegations of the member's wallets pinned on one Cosmos SDK chain management.walletBalanceChains() lists, through the platform-owned REST endpoint Cosmos balances use: the chain's bond_denom and each wallet's /cosmos/staking/v1beta1/delegations/{delegator}, every request pinned to the latest block's height (an endpoint that answers every request at one other height has that block recorded; mixed heights are an outage). validator is an operator address (bech32 …valoper) that limits the count to it. Returns { chain, denom, validator, total, wallets: [{ address, amount }], validators: [{ validator, amount }], at, observedAt } in base units of the staking denomination, or null when the member has no wallet linked on that chain, captured for replay; one of eight capability calls. An unsupported chain, a malformed validator and an unlinked address throw author-error; outages throw provider-unavailable, never zero. Snippet cosmos-staker. Bonded delegations only: no unbonding delegations, rewards, history or governance votes.",
      "entrypoints": ["management.walletBalanceChains"],
      "reference": "verification",
      "evidence": [
        "apps/backend/src/providers/cosmos.ts",
        "apps/backend/src/providers/wallet-delegations.ts",
        "apps/backend/tests/cosmos-delegations-provider.test.ts",
        "apps/backend/tests/wallet-capabilities.test.ts",
        "snippets/cosmos-staker/cosmos-staker.quest.ts",
        "tests/staking-examples.test.mjs"
      ]
    },
    {
      "id": "wallet-events",
      "support": "native",
      "description": "Evaluator ctx.wallet.events({ event, account, since, afterActivation?, where?, address?, newest? }) reads the events of one EVM contract whose indexed address argument `account` is one of the member's pinned linked EVM wallets, from since (at most 30 days before the evaluation; afterActivation raises it to the quest's activation) to the chain's finalized block, or its safe block on a chain without finality, through dRPC eth_getLogs on the chains management.walletBalanceChains() lists. The event is settings.contractEvent(): a CAIP-10 contract and the Solidity event signature with argument names and indexed markers; publication, Console settings edits and unarchiving check the chain reports a finalized or safe block and the address holds a contract. where holds equality or { gte } conditions on other arguments. Returns { event, chain, contract, since, at, finality, complete, logs: [{ wallet, block, timestamp, tx, logIndex, args }], observedAt } with decoded arguments, or null when the member has no linked EVM wallet, captured for replay; one of eight capability calls. Reads over more than 3,000,000 blocks or 1,000 logs throw author-error, unless newest (1 to 1,000) asks for the newest matching events, which stops at the caps with complete false; failures throw CapabilityError and never read as no activity. Snippet onchain-action. Only events naming the member in an indexed argument: no tx.from matching, polling sources or Cosmos activity; Solana is wallet-transactions.",
      "entrypoints": [
        "settings.contractEvent",
        "isContractEvent",
        "parseContractEvent"
      ],
      "reference": "onchain",
      "evidence": [
        "apps/backend/src/providers/evm-events.ts",
        "apps/backend/src/providers/wallet-events.ts",
        "apps/backend/src/application/publication-preflight.ts",
        "apps/backend/tests/evm-events-provider.test.ts",
        "apps/backend/tests/wallet-capabilities.test.ts",
        "apps/backend/tests/nft-settings-preflight.test.ts",
        "snippets/onchain-action/onchain-action.quest.ts",
        "tests/onchain-action-example.test.mjs"
      ]
    },
    {
      "id": "wallet-transactions",
      "support": "native",
      "description": "Evaluator ctx.wallet.transactions({ program, since, afterActivation?, mint?, address? }) reads the successful transactions at finalized commitment of the member's pinned linked Solana wallets since since (at most 30 days before the evaluation; afterActivation raises it to the quest's activation) that invoked program in a top-level or inner instruction, oldest first, through the platform's Solana RPC (dRPC where it has the key): getSignaturesForAddress paged back to since, failed signatures skipped, then getTransaction with jsonParsed and maxSupportedTransactionVersion 1. With mint, each wallet's associated token account for the mint is paged instead of the wallet. The program is settings.solanaProgram(): a base58 address; publication, Console settings edits and unarchiving check that the account exists and is executable. Returns { program, chain, mint, since, at, complete, transactions: [{ wallet, signature, slot, timestamp, signer, programs, balanceChanges: [{ mint | \"SOL\", delta }] }], observedAt }, or null when the member has no linked Solana wallet, captured for replay; one of eight capability calls. signer is whether that wallet signed the transaction (false for an incoming transfer someone else signed, and in results captured before it existed). Each wallet's read fetches at most 1,000 transactions; a busy wallet returns its newest with complete: false. Helpers received(), spent(), swapped(), invoked() and instruction(tx, name) read one transaction. Saving the setting stores the program's Anchor IDL (legacy or 0.30+) from its on-chain IDL account in the release's solanaIdls, or an operator pastes one in Console or with quests.updateSettings({ solanaIdls }); with an IDL, each transaction also has instructions: the program's top-level and inner instructions decoded as { name, args, accounts }, undescribed ones skipped, captured with the read. A non-program account, a non-mint mint, since over 30 days and an unlinked address throw author-error; outages throw provider-unavailable, never no activity. Shank and Codama IDLs are not decoded. Snippet solana-action.",
      "entrypoints": [
        "settings.solanaProgram",
        "isSolanaProgram",
        "received",
        "spent",
        "swapped",
        "invoked",
        "instruction"
      ],
      "reference": "onchain",
      "evidence": [
        "apps/backend/src/providers/solana-transactions.ts",
        "apps/backend/src/providers/anchor-idl.ts",
        "apps/backend/src/providers/wallet-transactions.ts",
        "apps/backend/src/application/publication-preflight.ts",
        "apps/backend/tests/solana-transactions-provider.test.ts",
        "apps/backend/tests/anchor-idl.test.ts",
        "apps/backend/tests/contract-publication.test.ts",
        "apps/backend/tests/wallet-capabilities.test.ts",
        "apps/backend/tests/nft-settings-preflight.test.ts",
        "tests/solana-transactions.test.mjs",
        "snippets/solana-action/solana-action.quest.ts",
        "tests/solana-action-example.test.mjs"
      ]
    },
    {
      "id": "resource-settings",
      "support": "native",
      "description": "Reusable Console point-ledger, integration-resource and date/time fields. settings.pointLedger yields a typed ledger reference usable by authored point and budget helpers. Discord channels resolve from the connected project integration; backend publication validates access. Empty resource settings keep code-authored quests paused. An operator with publish changes a quest's settings values, or resets them to their defaults, by publishing a new release from the release they saw (management.quests.updateSettings); live refuses it from every credential with promotion-required. settings.text({ label, secret: true }) is write-only: no default, no value from source or deploy settings, and no management response, CLI output or Console screen returns it; releases list the set ones in withheldSettings. Reusable quest types refuse secret settings.",
      "entrypoints": [
        "settings.pointLedger",
        "settings.resource",
        "settings.datetime",
        "discord.channels",
        "discord.roles",
        "management.quests.updateSettings"
      ],
      "reference": "authoring",
      "evidence": ["apps/backend/src/application/quest-publication.ts"]
    },
    {
      "id": "discord-role-rewards",
      "support": "native",
      "description": "Quest-declared Discord role rewards commit durable delivery requests with completion. One writer per server/account/role reconciles accepted grants across projects and Test/Live, remains bound to original identities, and removes only Relay-owned assignments after the last grant is revoked. Preexisting or explicitly preserved assignments stay. Console and management APIs expose current reconciliation, retry and preservation with a reason, and list every role deliveries granted with counts by status (management.discordRoles.list); CLI uses the same routes through domino api. Preservation is an operation of the Discord role reward kind (management.discordRoles.preserve; management.preserveRewardRole is its deprecated alias), and a delivery of a kind without it is refused with 409. Referral reward lists do not accept integration rewards.",
      "entrypoints": [
        "discord.grantRole",
        "me.rewardDeliveries",
        "management.rewardDeliveries",
        "management.discordRoles.list",
        "management.retryRewardDelivery",
        "management.preserveRewardRole",
        "management.discordRoles.preserve"
      ],
      "reference": "authoring",
      "evidence": [
        "apps/backend/tests/deliveries.test.ts",
        "apps/backend/src/providers/discord.ts",
        "apps/backend/tests/discord-role-reconciliation.test.ts"
      ]
    },
    {
      "id": "quest-announcements",
      "support": "native",
      "description": "A quest lists telegram.postMessage({ chat, text, replyToTriggeringMessage }) or discord.postMessage({ channel, text, replyToTriggeringMessage }) (declare integrations: [discord]) in its optional announcements beside rewards: at most three, an array or a function of { settings }. Relay posts in the project's connected Telegram chat or Discord channel after the completion's rewards commit, as a reward delivery with action post-message and an empty roleId, so a failed post never undoes the rewards. The text is a template with only {beneficiary}, {author}, {points}, {rewards} and {quest} (announcementPlaceholders; {{ and }} are literal braces); an unknown placeholder fails when the quest is built, and {author} and replyToTriggeringMessage need an automatic tracker trigger. People are mentioned through their account linked on that platform, and member text is escaped; Discord posts ping only the people named. Posts are paced per chat or channel and retried with backoff for about a day; management.retryRewardDelivery(id) (Retry delivery in the member's Announcements in Console) retries a failed one, and reward.delivery.updated and reward.delivery.failed report them. Publication refuses a chat not connected in the environment, a Discord channel outside the connected server or one the bot can't post in, and, in Test, a chat or server any project connected in Live; an announcement that cannot post at all creates no delivery and the attempt records why. Revoking the completion cancels a post not yet sent. Choose a channel with discord.channels({ postable: true }) and a chat with settings.telegramChat().",
      "entrypoints": [
        "telegram.postMessage",
        "discord.postMessage",
        "management.rewardDeliveries",
        "management.retryRewardDelivery"
      ],
      "reference": "authoring",
      "evidence": [
        "packages/sdk/src/announcements.ts",
        "packages/sdk/src/announcement-template.ts",
        "apps/backend/src/ledger/accounting/rewards/announcements.ts",
        "apps/backend/src/application/announcement-targets.ts",
        "packages/plugins/telegram/src/announcements.ts",
        "apps/backend/src/application/publication-preflight.ts",
        "apps/backend/tests/announcements.test.ts"
      ]
    },
    {
      "id": "trackers",
      "support": "native",
      "description": "Maintained datasets collect all configured X search matches with author verification and engagement preserved. Discord trackers (discord.messages(), optionally with text: true, and discord.reactions()) receive the connected server's new messages and reactions from Domino's gateway, kept only when one of the record's accounts is a Discord account a member of the tracker's project linked; they cannot be ingested into or refreshed. Filter on read, query connected member posts, and use captured point-in-time reads in quest code (as of the activity's time, at one watermark per attempt). Custom trackers declare a zod schema for each record's data and accept indexer data from keys with the ingest permission (hosted-app keys, or a personal API key limited to ingest); ingestion rejects a batch whose record data does not match with the stable code invalid-record-data, the record, path and reason (record-data-too-complex past the validation budget, tracker-schema-required for a tracker published without a schema), and a schema change that breaks a live quest's filter is refused. They can name record accounts (author plus declared roles) as verified wallet, X, Discord or member identities, or as the app's own user IDs (app:<name> kinds an identity integration links). Evaluators read the member's X, Discord and app identities with ctx.member.identities(), pinned once per evaluation run. A quest can declare a tracker trigger with a firing mode (new record, new observation, or changed data), an optional data filter (one condition or a list that must all hold, with paths and values type-checked against the tracker's data and setting bounds against the quest's integer settings), and the account role it pays (an X post's author, inReplyTo, quoted or reposted, a Discord reaction's reactor or messageAuthor, or a custom record's author or named account), and complete once, per key, or as a consecutive-day streak counted by each record's occurrence time. A trigger can require members to enroll in the quest (members enroll themselves, or a server with the enroll permission enrolls them), count activity from the quest's activation, the tracker's window or each member's enrollment, and choose who releases historical activity from before the member linked the account or enrolled (a reviewer, the member's claim, or automatically). Every selected observation is recorded as one admission or a skip with a stable skip code and reason; a skip is decided again only when the member links the account, a conflicting link is removed, the member joins or enrolls, and reviewers can retry any skip. Members read their own outcomes and claim claimable historical activity. A content quest on X or a custom source with a declared body and metrics uses the same admission pipeline, then qualifies body revisions and optionally gates rewards on performance; collection alone does not award or reverse rewards. X does not allow apps that reward users for posting, so a quest that pays for X posts (to the post's author, or a content quest on X) takes an inGroup prerequisite naming a group of creators the operator approves; a deployment without one warns with x-post-rewards-ungated and still publishes. Operators with enroll permission read and withdraw quest enrollment with a revision and reason. Withdrawal blocks new admission and retains attempts and rewards. Re-enrollment starts a new consent time and redecides not-enrolled skips under the current history policy. Trackers of every managed source can also be created as data in test, without a deployment, with management.trackers.create({ key, title, source, window, refreshSeconds?, maxPages? }) or domino trackers create --source (publish permission, same validation as defineTracker): source is { kind: x-search, query } (or query alone), { kind: discord-messages, text? }, discord-reactions, telegram-messages, telegram-reactions, youtube-shorts, tiktok-videos or { kind: wallet-balances, chain, token } (--chain, --token), stored as the source its builder makes; custom sources stay in code. An X tracker's first collection searches from window.start. management.trackers.update(tracker, changes) or domino trackers update changes them, never the key or the source kind (validation-failed): a new query or window collects from now on and never searches the past, and turning Discord message text off erases stored text. Live refuses both with promotion-required: a data tracker reaches live, still owned by data, through a promotion from test (domino promote). X tracking is best effort: known posts are re-read every 6 hours for 48 hours, daily until day 7, then never; trackers run no more often than the plan allows (6 hours below Enterprise, effectiveRefreshSeconds on status; deployments warn tracker-refresh-below-plan-minimum); X reads count toward a monthly budget per live project (and a shared 5,000 per organization in test), and a spent budget pauses X trackers with pauseReason x-budget-exhausted until resumesAt (resume answers 409 x-budget-exhausted meanwhile), while a test X tracker pauses with test-window-ended after 14 days until resumed. A key belongs to code or to data, shown as origin on tracker status: a deployment declaring a data tracker's key is refused with tracker-owned-by-data, and create or update of a code tracker with tracker-owned-by-code. From the CLI, domino trackers list|get|records|summary|outcomes read trackers, trackers ingest <tracker> <file> stores custom records, and trackers retry-outcome re-runs a skipped admission; changes are saved operations (--operation-id). Operators with read permission resolve an X handle (with or without @) to the stable user ID trackers, --author filters, custom x identities and ctx.x.follows take, as { id, username, name } for the account holding it (read through a cache up to a day old) or 404 not-found, at 30 lookups a minute per user and 300 an hour per organization (management.x.lookup, domino x lookup HANDLE); a lookup does not establish who owns the account.",
      "entrypoints": [
        "defineTracker",
        "x.search",
        "discord.messages",
        "discord.reactions",
        "customSource",
        "automatic",
        "management.trackers.create",
        "management.trackers.update",
        "management.trackers.records",
        "management.trackers.summary",
        "management.trackers.ingest",
        "management.trackers.preview",
        "management.x.lookup",
        "management.trackers.triggerOutcomes",
        "management.trackers.retryOutcome",
        "management.trackers.list",
        "management.trackers.get",
        "management.trackers.content",
        "quests.enroll",
        "quests.outcomes",
        "quests.claim",
        "management.quests.enroll",
        "management.trackers.pause",
        "management.trackers.resume",
        "management.trackers.refresh",
        "management.quests.enrollments",
        "management.quests.withdraw"
      ],
      "cli": [
        "trackers list",
        "trackers get",
        "trackers create",
        "trackers update",
        "trackers records",
        "trackers summary",
        "trackers ingest",
        "trackers content",
        "trackers outcomes",
        "trackers retry-outcome",
        "trackers pause",
        "trackers resume",
        "trackers refresh",
        "trackers archive",
        "trackers unarchive",
        "x lookup"
      ],
      "reference": "trackers",
      "evidence": [
        "packages/sdk/src/trackers.ts",
        "packages/sdk/src/tracker-triggers.ts",
        "packages/sdk/src/tracker-data.ts",
        "packages/sdk/src/discord.ts",
        "apps/backend/src/kernel/discord-gateway-events.ts",
        "apps/backend/src/trackers/discord.ts",
        "apps/backend/src/discord-gateway/delivery.ts",
        "apps/backend/tests/discord-trackers.test.ts",
        "tests/tracker-data.test.mjs",
        "apps/backend/tests/trackers.test.ts",
        "apps/backend/tests/contract-trackers.test.ts",
        "apps/backend/tests/contract-tracker-authoring.test.ts",
        "apps/backend/tests/tracker-preview.test.ts",
        "apps/backend/tests/x-user-lookup.test.ts",
        "apps/backend/tests/gateway/x-user-lookup.test.ts",
        "apps/backend/tests/tracker-triggers.test.ts",
        "packages/sdk/src/outcomes.ts",
        "apps/backend/src/trackers/quest-outcomes.ts",
        "apps/backend/src/trackers/redecision.ts",
        "apps/backend/tests/tracker-admission-rules.test.ts",
        "apps/backend/src/trackers/attribution.ts",
        "apps/backend/tests/member-identities.test.ts",
        "apps/console/src/features/console/member-quest-enrollments.tsx",
        "packages/management/cli/commands/trackers.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "engagement-settlement",
      "support": "native",
      "description": "Versioned engagement formulas credit daily points for qualified X or custom content whose source declares the selected metrics. Preview/apply formula changes settle each item through an available observed boundary without repricing prior counters or resetting budgets. Engagement manifest entries join atomic deploy previews and publication. Operators read admission through management.trackers.triggerOutcomes, qualification and performance through management.content, enroll members through management.quests.enroll, and retry historical admission through management.trackers.retryOutcome with review permission. Retry does not approve content or guarantee a reward. No historical backfill or automatic reversals.",
      "entrypoints": [
        "defineEngagementReward",
        "management.engagement.list",
        "management.engagement.page",
        "engagement.page",
        "management.engagement.ruleVersions",
        "management.engagement.boundaries",
        "engagement.boundaries",
        "management.content.performance",
        "management.content.qualifications",
        "management.trackers.triggerOutcomes",
        "management.trackers.retryOutcome"
      ],
      "reference": "trackers",
      "evidence": [
        "packages/sdk/src/engagement.ts",
        "apps/backend/src/engagement/settlement.ts",
        "apps/backend/tests/ledger/accounting/engagement-settlement.test.ts",
        "apps/backend/tests/gateway/engagement.test.ts",
        "apps/backend/src/engagement/versions.ts",
        "apps/backend/tests/engagement-versions.test.ts",
        "apps/backend/tests/engagement-deployment.test.ts",
        "tests/discovery.test.mjs",
        "docs/public/concepts/ugc.md"
      ]
    },
    {
      "id": "text-url-submissions",
      "support": "native",
      "description": "Required or optional text and HTTP/HTTPS URL claims with published length and exact-host constraints, recovery from Relay-kept submissions (one open draft and one unfinished attempt per member and quest) and evaluator access. URLs are never fetched automatically. Parse X post IDs with xPostId and read captured provider facts through ctx.x.post; participant X ownership is not verified.",
      "entrypoints": [
        "evidence.text",
        "evidence.url",
        "me.submission",
        "quests.open",
        "management.quests.submissions",
        "management.quests.discardSubmission"
      ],
      "reference": "verification",
      "evidence": [
        "packages/sdk/src/submission-input.ts",
        "apps/backend/src/ledger/participation/admission.ts",
        "apps/backend/src/ledger/participation/quest-submissions.ts"
      ]
    },
    {
      "id": "x-verification",
      "support": "native",
      "description": "Evaluator ctx.x.connection(), profile(), post({ postId }), follows({ userId }) and reposted({ postId }) provide captured X verification. Post reads include attached media; check images with ctx.images. Requires platform wrapper access except for connection lookup. Without a linked X account, profile, follows and reposted return null, and post returns null for a post that does not exist; provider failures throw CapabilityError. The account is pinned at each evaluation run's first X read; captured results survive retries within that run. Scheduled deferred runs read the current account. No likes or automatic X events.",
      "entrypoints": ["defineQuest", "auth.start", "auth.accounts"],
      "reference": "verification",
      "evidence": [
        "apps/backend/tests/x-capabilities.test.ts",
        "apps/backend/tests/gateway/x-verification.test.ts"
      ]
    },
    {
      "id": "staff-confirmation",
      "support": "native",
      "description": "Staff confirm a manual({ actor: \"staff\" }) quest for a member in Console, or with management.attempts.confirm({ quest, member }) and confirm permission; a repeated action ID returns the original attempt, and any other quest is refused with 403. Both routes need a member that already exists in that project and environment; an unknown member ID is refused with 404 not-found and creates nothing. An app that verified an external event pass records it with management.passes.verify({ provider, event, subject, member, quest }), which submits the entry quest; Relay records unique pass ownership but does not check the ticket itself. Hosted-app keys do not hold confirm.",
      "entrypoints": [
        "manual",
        "management.attempts.confirm",
        "management.passes.verify"
      ],
      "reference": "verification",
      "evidence": [
        "apps/backend/src/ledger/participation/linked-passes.ts",
        "apps/backend/tests/linked-passes.test.ts",
        "apps/backend/src/ledger/members/participant.ts",
        "apps/backend/tests/staff-confirmation-members.test.ts",
        "packages/sdk/src/server/hosted.ts"
      ]
    },
    {
      "id": "staff-handover",
      "support": "native",
      "description": "Staff with handover permission hand over a member's tiered reward: resolve a collection pass code (management.collection.resolve), start the handover (management.collection.begin, or management.handover.begin at the entitlement's revision, with a pickup selection where the reward's inventory is tracked at pickup), and confirm it (management.handover.confirm), after which the entitlement is collected. management.inventory.availability reads the pickup options for one entitlement.",
      "entrypoints": [
        "management.collection.resolve",
        "management.collection.begin",
        "management.handover.begin",
        "management.handover.confirm",
        "management.inventory.availability"
      ],
      "evidence": [
        "apps/backend/src/ledger/accounting/rewards/handover.ts",
        "apps/backend/src/ledger/accounting/rewards/collection.ts",
        "apps/backend/src/ledger/accounting/rewards/pickup.ts"
      ]
    },
    {
      "id": "entitlement-corrections",
      "support": "native",
      "description": "Operators correct a member's tiered reward entitlement at the revision they saw, with an audited reason: cancel a handover in progress (management.handover.cancel, handover permission), void an available entitlement granted by mistake (management.entitlements.void, review permission), restore an entitlement an operator voided (management.entitlements.restore or domino entitlements restore, review permission), or correct a collection recorded by mistake (management.entitlements.correctCollection, review permission). Cancelling and correcting make the entitlement available again and return any pickup size to stock; a voided entitlement cannot be handed over until it is restored or a quest grants the member the same reward again (a voided referral reward stays void until restored). Restoring makes it available at the tier an accepted completion still earns and brings back any referral review the void settled; it is refused with 409 entitlement-status when a completion revocation voided it or no accepted completion still earns it (the body names the completion to restore instead), when canceling a redemption voided it, or when a referral reward was voided before voids kept their reviews. Reads return entitlements whose status this SDK does not know as unsupported. API keys with the permission can make these corrections in live. Console Rewards and member detail offer the same actions, and Audit history lists each one with its reason. With read permission, operators page through entitlements newest first by status and search, with the members they name and a total (management.entitlements.list), and read one with its member (management.entitlements.get).",
      "entrypoints": [
        "management.handover.cancel",
        "management.entitlements.void",
        "management.entitlements.restore",
        "management.entitlements.correctCollection",
        "management.entitlements.list",
        "management.entitlements.get"
      ],
      "cli": ["entitlements restore"],
      "reference": "management",
      "evidence": [
        "apps/backend/src/ledger/accounting/rewards/corrections.ts",
        "apps/backend/src/public/routes/management.ts",
        "apps/backend/tests/ledger/accounting/contract-rewards.test.ts",
        "apps/backend/tests/gateway/management-resource-reads.test.ts"
      ]
    },
    {
      "id": "stock",
      "support": "native",
      "description": "Stock items with server-assigned IDs (management.inventory.createItem, retryable by action ID) and quantity additions to existing items (management.inventory.addStock), both with inventory permission; pickup inventory per tiered reward with labelled variants (management.inventory.catalog, management.inventory.configure). With read permission, management.inventory.list returns every stock item's available, reserved and allocated counts and the offers that sell it. Console Rewards shows and adjusts the same stock. Operators rename, archive, unarchive and correct free stock with inventory permission, revision preconditions and audited reasons. Corrections cannot remove reserved or redeemed units. Any retained offer, including a paused or archived offer, or pickup variant blocks archive with inventory-in-use and named blockers, so an offer's backing item cannot be archived; archive the offer instead, and reconfigure pickup references away. Archived items refuse new allocation, restocking and pickup configuration. Live refuses item creation, renames, archive state and pickup configuration with promotion-required; stock additions and corrections work in live.",
      "entrypoints": [
        "management.inventory.list",
        "management.inventory.createItem",
        "management.inventory.addStock",
        "management.inventory.catalog",
        "management.inventory.configure",
        "management.inventory.items",
        "management.inventory.rename",
        "management.inventory.archive",
        "management.inventory.unarchive",
        "management.inventory.adjustStock"
      ],
      "cli": [
        "stock-items list",
        "stock-items rename",
        "stock-items archive",
        "stock-items unarchive",
        "stock-items adjust-stock"
      ],
      "reference": "management",
      "evidence": [
        "apps/backend/src/ledger/accounting/rewards/inventory.ts",
        "apps/backend/tests/contract-stock.test.ts",
        "apps/backend/tests/gateway/management-resource-reads.test.ts",
        "apps/backend/tests/ledger/accounting/inventory.test.ts",
        "apps/console/src/features/console/inventory-item-actions.tsx"
      ]
    },
    {
      "id": "offers",
      "support": "native",
      "description": "Code-defined offers exchange points for one tiered reward from declared stock with a per-member limit. An offer's requires takes the prerequisites quests do (completed, balanceAtLeast, inGroup); redemption refuses a member who misses one with 409 prerequisites-incomplete and missing, spending nothing, and member offer reads list what they lack in missing. relay.offers.list and useOffers expose public or member-specific availability; relay.offers.redeem and useRedeemOffer reserve stock and points, grant the entitlement, and settle the spend atomically. Tier rewards use staff handover or automatic fulfillment. Operators with inventory permission pause or resume an offer and cancel a fulfillment with a reason. With publish permission, an operator archives or restores an offer in test (offers archive|unarchive, Settings → Resources), and a promotion carries it to live: an archived offer is hidden from member reads, refuses redemption with offer-paused, and keeps its redemptions cancelable; cancellation refunds the original points, returns stock, voids the entitlement, and remains in redemption history. Console Rewards exposes the same controls. management.redemptions.history pages redemptions newest first by cursor, for everyone or one member. From the CLI, domino offers list reads offers, redemptions history reads a page of redemptions (--member, --cursor, --limit), and redemptions cancel <redemption> --expected-revision N --reason cancels one as a saved operation (--operation-id); --revision is the option's deprecated name.",
      "entrypoints": [
        "defineOffer",
        "automaticFulfillment",
        "offers.list",
        "offers.redeem",
        "management.offers.list",
        "management.offers.redemptions",
        "management.redemptions.history",
        "management.offers.cancelRedemption",
        "management.offers.pause",
        "management.offers.resume"
      ],
      "cli": [
        "offers list",
        "offers pause",
        "offers resume",
        "redemptions history",
        "redemptions cancel",
        "redemptions pause",
        "redemptions resume"
      ],
      "reference": "authoring",
      "evidence": [
        "packages/sdk/src/offers.ts",
        "apps/backend/src/ledger/accounting/rewards/offers.ts",
        "apps/backend/tests/ledger/accounting/contract-offers.test.ts",
        "apps/console/src/features/console/offer-management.tsx",
        "packages/management/cli/commands/operator-resources.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "point-corrections",
      "support": "native",
      "description": "A management key with points permission awards, spends, adjusts, reverses or sets a member's balance with management.points.record. Every command carries an action ID and reason, and an exact retry returns the original result. A reversal references an original award and corrects its original period; set needs the current expectedTotal and an earned or spend classification. Deductions cannot make a balance negative. management.points.balance and management.points.history read a balance with read permission. Hosted-app keys hold points. From the CLI, domino points get|list read a member's balance and entries, and points award|spend|adjust|set|reverse record a change as a saved operation (--operation-id).",
      "entrypoints": [
        "management.points.record",
        "management.points.balance",
        "management.points.history"
      ],
      "cli": [
        "points get",
        "points list",
        "points award",
        "points spend",
        "points adjust",
        "points set",
        "points reverse"
      ],
      "reference": "authoring",
      "evidence": [
        "apps/backend/tests/contract-points.test.ts",
        "packages/sdk/src/leaderboards.ts",
        "packages/management/cli/commands/points.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "deploy-and-stage",
      "support": "native",
      "description": "domino deploy publishes quests, engagement rules, reusable types and collection slots atomically to test, preserving Console-owned settings; --preview resolves the whole deployment against Relay without publishing and --resume publishes a test preview or recovers its receipt. --only <quest-key> (repeatable) deploys just the named quests, with the SDK runtimes they import, the relay.json images their code names and supportedInteractions; every other quest keeps its release and every other resource stays as published, so parallel worktrees publish their own quests without reverting each other's. It works with --preview and --yes; an unknown key, or --only with --resume, --promote, --dry-run or --out, fails with INVALID_ARGUMENT before any request. The preview lists each quest's change (questChanges: created, updated with the release it replaces, or unchanged), and an unchanged quest keeps its active release instead of getting a new one. Missing project configuration does not block: a quest that needs an unconnected integration such as Discord publishes, and the preview and result list it in warnings (code integration-required, the integration and quest keys), which the CLI also prints; its attempts fail with integration-required until the integration is connected, and automatic attempts that failed need an explicit retry. A quest that rewards X posts (an X tracker trigger paying the post's author, or a content quest on an X tracker, with declared rewards or an engagement rule) without an inGroup prerequisite also publishes, with warning x-post-rewards-ungated: X does not allow apps that reward users for posting, so gate it on a group of approved creators. A broken definition still blocks. Live refuses domino deploy --environment live and management.deployments.preview from every credential with 422 promotion-required, before the bundle is checked: code reaches live only through a promotion from test. Console lists promotions only, not live deployments submitted before that refusal; management.deployments.history pages that deployment history newest first by cursor, with the live deployment and the rollback target beside every page. domino deploy rollback publishes the previous published test deployment again at once (management.deployments.rollback with an action ID, so a retry is safe): only what that deployment contains goes back, what the later deployment added stays, and the staged build does not change; the CLI refuses live with promotion-required. Console's test Deployments page lists those deployments (Code deployments) and offers the same rollback behind a confirmation. --promote, once its test deployment is published, prepares an environment promotion of all of test (see promote-to-live) and answers it in the deployment record shape; it and management.deployments.promote are deprecated in favor of domino promote and management.promotions.prepare, and management.deployments.submit and reject, which act only on live deployments made before the refusal, are deprecated too (removed in 2.0.0). Console-created quests change through catalog drafts in test. Live refuses every direct definition change (quests, settings, archive state, point ledgers, badges, calendar, profile settings, pickup policy, stock items, exclusion-group bindings) from every credential with 422 promotion-required; operations such as pauses, points, groups and moderation work in live. domino stage builds a pushed commit in the cloud and deploys the hosted app's staging preview on test data; domino builds and domino logs follow it. A Worker app's build script ends with domino build --hosting-artifacts, which writes the quest and catalog artifacts the cloud builder reads to .domino-build/ beside relay.json; dominoHosting({ command }) from @domino-sdk/relay/vite returns the Cloudflare Vite plugin options (domino dev's variables forwarded to the local Worker only when serving) and the Vite server settings (DOMINO_APP_PORT, the DOMINO_PREVIEW_ORIGIN host and HTTPS hot reload) that domino dev and hosting expect.",
      "entrypoints": [
        "management.deployments.list",
        "management.deployments.history",
        "management.deployments.get",
        "management.deployments.preview",
        "management.deployments.publish",
        "management.deployments.submit",
        "management.deployments.reject",
        "management.deployments.promote",
        "management.deployments.rollback",
        "management.catalog.read",
        "management.catalog.allocateQuest",
        "management.catalog.saveDraft",
        "management.catalog.preview",
        "management.catalog.publish"
      ],
      "cli": ["deploy", "deploy rollback", "stage", "builds", "logs"],
      "reference": "hosted",
      "evidence": [
        "packages/management/cli/commands/project.mjs",
        "packages/management/cli/commands/staging.mjs",
        "tests/cli-deploy-rollback.test.mjs",
        "packages/sdk/src/vite.ts",
        "tests/vite-hosting.test.mjs",
        "apps/backend/tests/gateway/management-publication.test.ts"
      ]
    },
    {
      "id": "promote-to-live",
      "support": "native",
      "description": "Live changes only through a promotion: domino promote (management.promotions.prepare against test, publish permission) compares every definition in test with live by authoring key and saves what live would change (added, changed, archived and unarchived resources, plus the hosted app's active staged build, which approval deploys as the live app) with warnings for environment bindings live lacks (integrations, webhooks, participant sign-in, app secrets), which a promotion never copies. It refuses with promotion-empty when live already matches, and a newer promotion supersedes a pending one. A person with publish permission approves or rejects it in Console; approval applies it in one transaction and refuses with promotion-changed when test or live changed since it was prepared. management.promotions.rollback prepares a promotion of an earlier applied promotion's definitions and build. domino promotions list and get read them.",
      "entrypoints": [
        "management.promotions.prepare",
        "management.promotions.history",
        "management.promotions.get",
        "management.promotions.approve",
        "management.promotions.reject",
        "management.promotions.rollback"
      ],
      "cli": ["promote", "promotions list", "promotions get"],
      "reference": "authoring",
      "evidence": [
        "packages/management/cli/commands/promotions.mjs",
        "apps/backend/tests/contract-promotions.test.ts"
      ]
    },
    {
      "id": "operator-members",
      "support": "native",
      "description": "Operators with read permission list the project's members, most recently joined first, with their accounts, linked app identities, attempt, completion and reward counts and the total that match, searching part of a member ID, profile name, account subject, name or email, or app identity subject and optionally only members with an attempt waiting for review, 1 to 100 per page by cursor (management.members.list, domino members list); read a member's accounts, progress, passes, wallets and profile (management.members.get), or only their progress on each active quest, 25 most recent attempts and point balances, without the quest releases and evaluator code member detail carries (management.members.progress, domino members progress); identify up to 100 members by ID, with their accounts and linked app identities (management.members.people); and find the one project member holding an exact account or linked app identity, named as a member import names it ({ kind: \"x\" | \"discord\" | \"telegram\", subject }, { kind: \"app:<name>\", subject }, which maps an app's user ID to its member, or { kind: \"wallet\", chain, address }), or by the deprecated provider and subject as the member's accounts list them (management.members.lookup). Lookup never matches part of a value or another letter case; it fails with 404 member-not-found or 409 account-ambiguous. From the CLI, domino members list|get|progress|lookup reach the same reads.",
      "entrypoints": [
        "management.members.list",
        "management.members.get",
        "management.members.progress",
        "management.members.people",
        "management.members.lookup"
      ],
      "cli": [
        "members list",
        "members get",
        "members progress",
        "members lookup"
      ],
      "reference": "management",
      "evidence": [
        "apps/backend/src/auth/accounts.ts",
        "apps/backend/src/ledger/queries/member-reads.ts",
        "apps/backend/src/ledger/queries/member-detail.ts",
        "apps/backend/tests/gateway/management-project-pages.test.ts",
        "apps/backend/tests/gateway/management-member-progress.test.ts",
        "packages/management/cli/commands/members.mjs",
        "tests/cli-members.test.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "project-access",
      "support": "native",
      "description": "Organization administrators create projects, delete them (Domino support can restore one for 30 days), move a project, with its participants, to another organization they administer, and grant editor, reviewer and staff roles per project and environment (management.grants, management.team), plus the experimental viewer role, which reads the project's dashboards and nothing else, and an experimental end date (expiresAt) after which a grant gives nothing and stays listed. A person issues personal API keys (management.tokens) for one project and environment, limited to permissions they hold there and never access, for 1 to 90 days (days, default 30; Console always 30); a Console session or unscoped human CLI sign-in lists, creates, scopes and revokes keys (domino tokens list|create|scope|revoke), except that a CLI sign-in creates and scopes test keys only: a live key is created and scoped in Console Settings > API keys (403 console-approval-required otherwise). A credential that must not expire is a service key, which only an organization admin creates in Console. API keys and project-scoped playground sessions cannot manage keys. Optional ingestTrackers restricts ingestion to up to 100 custom tracker IDs; creation and scope edits resolve keys to IDs in the selected project/environment. Null or omission keeps project-wide ingestion. A denied tracker returns 403 TRACKER_SCOPE_DENIED. Console Settings > API keys offers selected trackers and Edit tracker scope; management.tokens.scope edits a restriction without rotating the secret. management.access lists the caller's projects and permissions; management.readiness reports launch checks; management.metrics.read returns the project's headline counts and each quest's completions; management.usage.activeMembers returns the project's live active members this UTC month against its plan's limit, with six earlier months; management.usage.realPeopleInTest returns the real people who joined the test environment against the plan's limit. management.usage.plan returns the project's plan and its source, whether it may serve live and why (plan, trial or grace, or refused), live's notice or offline state, the organization's one trial, whether the plan includes a custom domain, the AI builder balance by pool and when it renews, the Stripe subscription and whether limits are enforced; it needs only read, unlike buying through management.billing. With billing enforcement on, a sign-in that would add a real person to a full test environment fails 403 limit-reached (limit real-people-in-test), creating a Free project beyond the organization's 10 fails 403 limit-reached (limit free-projects), and so does transferring a Free project into an organization at that limit. With read permission, management.audit.list pages through the project's audited operations in one environment (reviews, reward changes, configuration, and operators acting as test members, including signing a staging app in as one, which reset keeps); organization administrators, from a Console session or CLI sign-in, page through its access and API key changes with management.audit.access. Console organization admins rename projects and change slugs with management.updateProject; slug edits require the current name as confirm, and refuse slugs shorter than 3 characters or reserved by Domino (exact match). CLI sessions and API keys cannot edit these settings. Project access responses include recorded creator identity, and audit records resolve operator names, primary emails and avatars when available.",
      "entrypoints": [
        "management.access",
        "management.readiness",
        "management.metrics.read",
        "management.usage.activeMembers",
        "management.usage.realPeopleInTest",
        "management.usage.plan",
        "management.audit.list",
        "management.audit.access",
        "management.createProject",
        "management.updateProject",
        "management.deleteProject",
        "management.transferProject",
        "management.tokens.list",
        "management.tokens.create",
        "management.tokens.scope",
        "management.tokens.revoke",
        "management.grants.list",
        "management.grants.set",
        "management.team.list"
      ],
      "cli": [
        "whoami",
        "tokens list",
        "tokens create",
        "tokens scope",
        "tokens revoke",
        "metrics get",
        "usage active-members",
        "usage real-people-in-test",
        "usage plan"
      ],
      "evidence": [
        "apps/backend/src/public/routes/management.ts",
        "apps/backend/src/management/projects.ts",
        "apps/backend/tests/gateway/management-org.test.ts",
        "apps/backend/tests/gateway/management-resource-reads.test.ts"
      ]
    },
    {
      "id": "project-billing",
      "support": "native",
      "description": "Experimental while billing is open to Domino staff only: the management.billing methods, the domino billing commands and the plan names may change in a minor release. A project subscribes to Launch, Growth or Scale, billed monthly or annually, through Stripe Checkout: management.billing.checkout({ plan, interval }) or domino billing checkout --plan --interval returns the Checkout URL to open. The plan is any name, and Relay refuses one it does not offer with 400. Checkout collects the billing address and tax ID, and Stripe Tax adds tax. The organization has one Stripe customer, created on its first checkout; management.billing.portal or domino billing portal returns Stripe's customer portal for its cards, invoices and billing details (409 no-customer before a first checkout). Both need an organization administrator from Console or a CLI sign-in, never an API key (403 permission-required). While billing is staff-only, which it is until the billing launch, both are refused for every organization but Domino's own with 403 billing-staff-only, and Console shows no purchase or portal actions. A project whose subscription still holds its plan is refused another checkout (409 subscription-exists) and changes plan instead: management.billing.changePlan({ plan, interval }) or domino billing change applies a higher plan or annual billing now, with the prorated difference invoiced at once (invoiceUrl when that payment needs the customer, and nothing changes until it is paid), and holds a lower plan or monthly billing for the renewal (the subscription's scheduledChange); asking for the current plan drops a held change. While an invoice is overdue the subscription keeps its plan and reports unpaid (since, promotionsRefusedAt, offlineAt): from 14 days the organization's new promotions are refused with plan-required (reason unpaid), and with billing enforcement on the project's live is in notice from 38 days and offline from 45 until it is paid. Deleting a paying project sets its subscription to end with its period, and restoring the project resumes it; purging ends it at once; a transferred project keeps its plan until the old organization's period ends (subscription.endedFor project-transferred), and the receiving organization's checkout charges nothing before then. management.billing.topup({ quantity }) or domino billing topup --quantity returns a Checkout URL buying 1 to 10 packs of $10 AI builder credit for the project, on any plan; the credit is added once the payment is in, once per purchase. management.billing.cancel or domino billing cancel ends the subscription when its period does, keeping the plan until then, and management.billing.resume or domino billing resume takes that back; a plan change is refused while it is set to cancel (409 subscription-canceling), and all three need a subscription that holds its plan (409 no-subscription). When Stripe reports the subscription, the project is on that plan with source stripe; when the subscription ends, it is back on Free. management.billing.read or domino billing show returns whether billing is open to the caller (available, and reason admin-required, staff-only or not-configured) and, when it is, the project's subscription as Stripe last reported it: plan, interval, Stripe's status, current period end, whether it cancels at the period end, the last invoice's outcome and livemode. management.billing.organization or domino billing organization returns, for any member of the organization and without a project, each project the caller can read with its plan, source, live entitlement, live state and subscription, the organization's Free projects against the limit of 10, the free AI allowance its Free projects share, its trial, whether limits are enforced and whether the caller may open the portal; management.billing.organizationPortal opens the portal from Console's organization Billing page and returns there. A deployment without Stripe configured answers 503 not-configured.",
      "entrypoints": [
        "management.billing.read",
        "management.billing.checkout",
        "management.billing.changePlan",
        "management.billing.cancel",
        "management.billing.resume",
        "management.billing.topup",
        "management.billing.portal",
        "management.billing.organization",
        "management.billing.organizationPortal"
      ],
      "cli": [
        "billing show",
        "billing checkout",
        "billing change",
        "billing cancel",
        "billing resume",
        "billing topup",
        "billing portal",
        "billing organization"
      ],
      "evidence": [
        "apps/backend/src/public/stripe-billing.ts",
        "apps/backend/src/public/routes/management-billing-routes.ts",
        "apps/backend/src/billing/stripe.ts",
        "apps/backend/tests/gateway/management-billing.test.ts",
        "packages/management/cli/commands/billing.mjs"
      ]
    },
    {
      "id": "project-analytics",
      "support": "native",
      "description": "With read permission, management.analytics.read({ range, ledger }) (GET /management/v1/analytics, domino analytics get) returns one project's activity in one environment over 7d, 30d or 90d of project-calendar days or all time since the earliest dated record (the first membership, or an older imported completion), ending now: sign-ups, active members, attempts, completions, rejections and attempts sent to staff review, each quest's attempts, completions, decisions, rejection rate (null below 10 decided), top 5 rejection reasons (grouped by code and who wrote it when there is one) and rejections by source (evaluator, staff, budget, expired, keyUsed: Relay's key-used for a key another member's completion holds), one point ledger's points awarded and members earning, with spent and net when the ledger has a spend entry, fulfilled offer redemptions when the project has offers, accepted and qualified referrals when it has a referral program, with how many of the range's accepted referrals have qualified since (acceptedQualified), and in test the last test member reset and how many test members have an advanced clock. Each number has a total, a value per project-calendar day (7d, 30d) or Monday week (90d, all), and the total of the range of equal length before it (not for all). Records count as they stand now, so a completion revoked or a redemption canceled later leaves its bucket, and activity a test member's advanced clock dates in the future is left out. domino analytics get prints a readable summary, and the read with --json; agents quote these numbers rather than computing them. There are no custom ranges. management.analytics.catalog() (GET /management/v1/analytics/catalog, domino analytics metrics) lists the closed metric catalog: each metric's ID, one-line definition, kind (count, distinct or rate), the breakdowns and filters it accepts, its allowed charts (number, line, bar, table; rates no bar) and whether it needs a point ledger. management.analytics.query({ metric, range, breakdown, filter, ledger, limit }) (GET /management/v1/analytics/query, domino analytics query --metric ID --range R [--breakdown D] [--filter name=a,b]... [--ledger KEY] [--limit N]) reads one metric over the same ranges and buckets, and with no breakdown or filter equals analytics.read's number for the metrics that read shows; group, tracker and leaderboard settlement metrics are only in analytics.query. A breakdown (quest, offer, referral program, group, tracker or leaderboard, source and reason for rejections, or outcome for tracker records) has a row per matching resource, archived ones only with activity in the range or the range before, each with key, title, archived, total, previous total and series, sorted by total and capped by limit (1 to 100, default 100) with the rest summed into other. Filters narrow to quests, offers, programs or leaderboards by key or ID, to the quests a tracker triggers (on trackers.records, that tracker's outcomes), or to members now in a group (on groups.joins and groups.leaves, the changes in that group). groups.joins and groups.leaves count membership changes by when they happened, without changes that left membership as it was or history recorded without a date; trackers.records counts trigger outcomes by when the record was observed, as each outcome is now, since a retry overwrites it; leaderboards.settledParticipants counts the rows of pending and approved settlements at each settlement's cutoff and draws as number, bar or table, not line. Points metrics need ledger, which includes archived ledgers. quests.rejectionRate and referrals.qualificationRate are null below 10 in the denominator, in the value, each bucket and each row. An unknown metric, a breakdown or filter the metric does not accept, a missing or needless ledger, a limit outside 1 to 100 or an unknown key is a 422 with its own code; a missing group is 422 group-not-found naming the group. There are no formulas or custom queries.",
      "entrypoints": [
        "management.analytics.read",
        "management.analytics.catalog",
        "management.analytics.query"
      ],
      "cli": ["analytics get", "analytics metrics", "analytics query"],
      "reference": "analytics",
      "evidence": [
        "apps/backend/src/ledger/queries/analytics/read.ts",
        "apps/backend/src/ledger/queries/analytics/metrics.ts",
        "apps/backend/src/ledger/queries/analytics/quests.ts",
        "apps/backend/src/ledger/queries/analytics/window.ts",
        "apps/backend/src/ledger/queries/analytics/query.ts",
        "packages/sdk/src/server/analytics.ts",
        "apps/backend/tests/analytics-read.test.ts",
        "apps/backend/tests/analytics-query.test.ts",
        "apps/backend/tests/ledger/queries/analytics-window.test.ts",
        "packages/management/cli/commands/analytics.mjs",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "dashboards",
      "support": "native",
      "description": "Dashboards are definitions in project code: default-export defineDashboard({ key, title, description, defaultRange, position, sections }) from @domino-sdk/relay/authoring and list it under relay.json dashboards as { entry, exportName }. Each section's panels name one catalog metric (domino analytics metrics) with an optional breakdown, filters, ledger for points metrics, a chart (number, line, bar or table) and a width of 1 to 4 (default 1 for number, 2 for line and bar, 4 for table); defaultRange defaults to 30d. domino deploy refuses, with a 422 and its own code, an unknown metric (unknown-metric), a breakdown or filter the metric does not take or a number panel with a breakdown (breakdown-not-accepted, filter-not-accepted), a table panel without one (breakdown-required), a chart the metric cannot draw (chart-not-accepted), a points metric without ledger or ledger on another metric (ledger-required, ledger-not-accepted), a quest, tracker, ledger, offer, referral program or leaderboard key the environment lacks (unknown-reference; archived resources are valid and keep counting), a referral metric with no referral program (referral-program-required), duplicate dashboard keys or panel keys (duplicate-key), more than 24 panels (too-many-panels) or more than 20 dashboards that are not archived (too-many-dashboards). Groups are checked when a panel is read. A changed definition publishes a new version; an identical one publishes nothing, and the preview's dashboards say created, updated or unchanged. With read permission, management.dashboards.list({ includeArchived }) (GET /management/v1/dashboards, domino dashboards list) lists key, title, description, position, archivedAt and updatedAt by position (none last), then title, without archived ones unless includeArchived; management.dashboards.read({ key, range }) (GET /management/v1/dashboards/KEY, domino dashboards get KEY [--range]) returns the definition and every panel's analytics.query result over the range (the dashboard's defaultRange by default), as of one instant, broken-down line and bar panels with the top 8 rows and other and tables up to 100; a panel that fails carries its own error (group-not-found, panel-timeout, panel-failed) while the others read; each statement gets 2 s and the read about 18 s, and archived dashboards stay readable. management.dashboards.archive/unarchive(key, { reason }) (POST /management/v1/dashboards/KEY/archive|unarchive, domino dashboards archive|unarchive KEY --reason) return { dashboard, changed }, refuse an unarchive past 20 dashboards that are not archived (too-many-dashboards), need publish, work in test only (live refuses with promotion-required) and are audited; removing a dashboard from relay.json never deletes it, and deploying an archived dashboard's code leaves it archived. A promotion carries dashboards to live by key, as changes of kind dashboard, with no environment bindings. Console's Analytics page shows dashboards: its switcher lists those that are not archived, each opens at /analytics/KEY, and Console cannot edit them.",
      "entrypoints": [
        "management.dashboards.list",
        "management.dashboards.read",
        "management.dashboards.archive",
        "management.dashboards.unarchive"
      ],
      "cli": [
        "dashboards list",
        "dashboards get",
        "dashboards archive",
        "dashboards unarchive"
      ],
      "reference": "analytics",
      "evidence": [
        "packages/sdk/src/dashboards.ts",
        "packages/sdk/src/server/dashboards.ts",
        "apps/backend/src/ledger/definitions/dashboards.ts",
        "apps/backend/src/application/dashboard-reads.ts",
        "apps/backend/src/application/deployment-resource-kinds.ts",
        "apps/backend/src/application/promotion/kinds.ts",
        "apps/backend/tests/dashboards.test.ts",
        "packages/management/cli/commands/dashboards.mjs"
      ]
    },
    {
      "id": "management-api",
      "support": "native",
      "description": "createManagementClient and hostedManagementClient from @domino-sdk/relay/server have a typed method for each management route an app needs; management.request and management.raw, or domino api METHOD /path [file] from the CLI, reach any other /management/v1 route. Every failed call is a ManagementError whose kind (http, network, timeout, aborted, invalid-response, client) separates a refusal from a lost or unreadable answer and from input rejected before sending; it keeps the server's message and code, including codes newer than the client, and reports uncertain, with operationId (the action ID it sent) and recovery (replay or reconcile), when a write may have been applied; it extends the shared RequestError, and isManagementError() recognizes it. Calls end after 30 seconds, retries included, unless the client or call sets another timeout, and take an AbortSignal; reads and writes whose typed route keeps a receipt are sent again, at most three times, after a lost answer, a 500, 502, 503 or 504, or a 429 whose Retry-After fits the deadline. The CLI's --json results, error codes and exit statuses, including domino api's, follow the CLI automation contract (/docs/reference/automation, or domino reference automation); a ManagementError maps onto the same codes. Each command in the contract's contract.json has a recovery (resume, replay, reconcile, or null for reads) saying what to do after it exits 3, and an uncertain error that names no saved operation carries details.recovery reconcile.",
      "entrypoints": [
        "management.request",
        "management.raw",
        "management.response"
      ],
      "cli": ["api"],
      "reference": "management",
      "evidence": [
        "packages/sdk/src/server/errors.ts",
        "packages/management/cli/commands/api.mjs",
        "tests/management-client.test.mjs"
      ]
    },
    {
      "id": "mcp-server",
      "support": "native",
      "description": "Domino's remote MCP server at https://mcp.domino.run (Streamable HTTP, OAuth sign-in with the user's Domino account) lets an operator's Claude or ChatGPT act as them. Reads, in test and live: list_projects, get_project, list_quests, get_quest, list_campaigns, list_submissions, get_submission, find_member, get_member, get_leaderboard, list_offers, get_usage (active members against the plan's limit, without prices), list_analytics_metrics, query_analytics (one catalog metric, as management.analytics.query) and list_audit_events. Writes, in test only: create_test_member, act_as_test_member (attempts.submit as the test member), reset_test_member, review_submission and delete_submission_evidence (attempts.redactEvidence). Each tool calls the management operation its CLI command uses, as the user, so their roles apply; a write in live is refused with 403 console-approval-required, and grant, team and token administration is refused. Lists answer { items, nextCursor }. There is no tool for publishing, points, deployments or arbitrary management calls.",
      "entrypoints": [],
      "reference": "automation",
      "evidence": [
        "apps/backend/src/public/mcp.ts",
        "apps/backend/src/public/mcp-tools.ts",
        "apps/backend/tests/gateway/mcp.test.ts",
        "apps/backend/tests/gateway/mcp-tools.test.ts"
      ]
    },
    {
      "id": "service-keys",
      "support": "native",
      "description": "A self-hosted app's backend authenticates with a service key (dsk_), which an organization administrator creates in Console Settings > API keys for one project and environment, with chosen permissions (never access). No person owns it, so it keeps working when people leave; its actor is service:<key ID>. It never expires unless an expiry is chosen at creation. It rotates with overlap: addSecret issues a second secret while the first keeps working, and revokeSecret retires the old one once its lastUsedAt stops advancing; a key's last secret cannot be revoked on its own. Every key fence applies: it cannot approve live publication, manage test members, use /tokens or mint keys. Creation, rotation and revocation are audited and listed by management.audit.access. Only an administrator's Console session manages service keys; API keys, service keys and CLI sign-ins get 403.",
      "entrypoints": [
        "management.serviceKeys.list",
        "management.serviceKeys.create",
        "management.serviceKeys.addSecret",
        "management.serviceKeys.revokeSecret",
        "management.serviceKeys.revoke"
      ],
      "reference": "management",
      "evidence": [
        "apps/backend/src/management/service-keys.ts",
        "apps/backend/src/public/routes/management-service-key-routes.ts",
        "apps/backend/src/public/routes/management-authentication-context.ts",
        "apps/backend/tests/gateway/management-service-keys.test.ts",
        "apps/console/src/features/console/service-key-settings.tsx"
      ]
    },
    {
      "id": "automation-keys",
      "support": "native",
      "description": "A headless agent or CI authenticates with an organization automation key (dak_) as RELAY_MANAGEMENT_TOKEN or through domino login --token-stdin. An organization administrator creates it in Console Settings > API keys for all of the organization's projects (current and future) or a chosen list, with chosen permissions (read, publish, inventory, points, ingest, enroll) and a separate createProjects permission. No person owns it; its actor is automation:<key ID>, and it never expires unless an expiry is chosen at creation. It rotates with overlap like a service key. It works for domino create, git push through the credential helper, domino stage, authoring and test members, in the test environment only: every live write (drafts, previews and bindings included) is refused with console-approval-required; grants, the team, /project, /organization/* (except GET /organization/billing), /tokens and CLI sign-in approval are refused with credential-not-allowed; it mints no credential. POST /projects needs createProjects, and a key with a project list gains the projects it creates; a project outside the list is refused with scope-mismatch. domino whoami --json reports credential: { kind: 'automation', id, name, projects, createProjects, permissions }. Only an administrator's Console session manages automation keys.",
      "entrypoints": [
        "management.automationKeys.list",
        "management.automationKeys.create",
        "management.automationKeys.addSecret",
        "management.automationKeys.revokeSecret",
        "management.automationKeys.revoke"
      ],
      "cli": ["whoami", "login"],
      "reference": "automation",
      "evidence": [
        "apps/backend/src/management/automation-keys.ts",
        "apps/backend/src/public/routes/management-automation-key-routes.ts",
        "apps/backend/src/public/routes/management-authentication-context.ts",
        "apps/backend/src/public/routes/git.ts",
        "apps/backend/tests/gateway/management-automation-keys.test.ts",
        "apps/console/src/features/console/automation-key-settings.tsx"
      ]
    },
    {
      "id": "tracker-scoped-keys",
      "support": "native",
      "description": "A personal API key with ingest permission can be restricted to up to 100 custom trackers in its project and environment: pass ingestTrackers (tracker keys or IDs, stored as immutable IDs) to management.tokens.create, or replace the list without rotating the secret with management.tokens.scope(id, { ingestTrackers }). null or omission keeps project-wide ingestion; an empty list is invalid. Ingesting into a tracker outside the list returns 403 TRACKER_SCOPE_DENIED and stores nothing. Console Settings > Keys offers Selected trackers only and Edit tracker scope; a human CLI sign-in uses domino tokens create --ingest-tracker and domino tokens scope for test keys, and a live key is created and scoped in Console Settings > API keys. Hosted app keys stay project-wide.",
      "entrypoints": ["management.tokens.create", "management.tokens.scope"],
      "reference": "management",
      "evidence": [
        "apps/backend/src/public/routes/management.ts",
        "apps/backend/src/public/clerk.ts",
        "apps/backend/tests/gateway/management-tracker-scopes.test.ts",
        "apps/console/src/features/console/management-settings.tsx"
      ]
    },
    {
      "id": "webhooks",
      "support": "native",
      "description": "Project/environment outbound webhooks for the nine activityTypes: completions, points awards, pending review, rejected attempts, granted and collected entitlements, reward delivery updates and failures, and qualified referrals. HTTPS public endpoints, HMAC signatures, retries, secret rotation, delivery logs and replay through management APIs and CLI. At-least-once with stable event IDs; no ordering guarantee. Console Settings > Webhooks provides endpoint creation, editing, enable/disable, secret rotation, paged delivery diagnostics and replay. A secret rotation returns no overlap when the previous secret is unusable. Operators with publish permission delete an endpoint with a revision and reason. Deletion stops new deliveries and retries, frees endpoint capacity and retains read-only delivery history. An in-flight request may finish. Deleted endpoints cannot be resumed, rotated or replayed; states and state expose retained tombstones. Subscriptions must name known activity types (unknown-activity-type otherwise). Create and secret rotation take an actionId, so a retry returns the same endpoint and secret; from the CLI they are saved operations (--operation-id). domino webhooks replay takes --action-id: a retry with the same ID returns the delivery the first replay created instead of queuing another.",
      "entrypoints": [
        "management.webhooks.list",
        "management.webhooks.create",
        "management.webhooks.update",
        "management.webhooks.rotateSecret",
        "management.webhooks.deliveries",
        "management.webhooks.replay",
        "management.webhooks.pause",
        "management.webhooks.resume",
        "management.webhooks.state",
        "management.webhooks.states",
        "management.webhooks.delete"
      ],
      "cli": [
        "webhooks list",
        "webhooks get",
        "webhooks states",
        "webhooks create",
        "webhooks update",
        "webhooks pause",
        "webhooks resume",
        "webhooks rotate-secret",
        "webhooks deliveries",
        "webhooks replay",
        "webhooks delete"
      ],
      "evidence": [
        "apps/backend/src/ledger/events.ts",
        "apps/backend/src/webhooks/worker.ts",
        "apps/backend/src/public/routes/webhooks.ts",
        "apps/backend/tests/webhook-deletion.test.ts",
        "apps/console/src/features/console/webhook-delete.tsx"
      ],
      "reference": "webhooks"
    },
    {
      "id": "member-import",
      "support": "native",
      "description": "Operators with publish permission import members, opening point balances, quest progress and referrals from another platform into one project and environment. Rows are JSONL, one member each, keyed by the identity the member will sign in with (an app:<name> subject, a wallet or an X account), with X, Telegram and Discord accounts and wallets the organization vouches for, opening balances by ledger, quest progress as accepted actions with times, and the member's inviter (another row's ref or an existing member's identity) with the referral programs they qualified in. An import is checked first (the dry run, which changes nothing and reports members created and existing, accounts, balances, actions and refused rows by code), then applied one transaction per row, resumably. Stopping an applying import (management.memberImports.stop, domino imports stop) lets the row being applied finish and applies no more: the import's state is stopped (its 1.x status still reads applied), with appliedAt null and stoppedAt set, its remaining rows stay ready, and it cannot be applied again, so the rest go in a new import; API keys and CLI sessions may stop a live import. Reverting an applied or stopped import (management.memberImports.revert, domino imports revert --reason) runs in the background, one applied row at a time, and reverses each row's opening balances as far as members still have the points (unreversedPoints lists the rest; no balance goes below zero), revokes their imported completions and removes the links they created unless the member signed in with one since; members stay in the project, an import is reverted once, its state reads reverting then reverted and its reverted rows read reverted: true, and the import's revert.outcome lists unreversed points, kept links and linksUntracked for rows applied before links were recorded. Applying never merges members, pays no rewards, sends no points.awarded or completion.created, counts imported balances on balance leaderboards only, counts imported completions toward completion limits and prerequisites, and records referrals and qualifications without referral rewards, milestones or bonuses (they count toward the inviter's qualified friends, and the friend's later points pay the bonus). In test, domino import applies after the check; a live import is checked from the CLI or an API key and applied only by a person in Console (console-approval-required otherwise). Vouched accounts are trusted like verified links and replaced by the member's own link.",
      "entrypoints": [
        "management.memberImports.create",
        "management.memberImports.list",
        "management.memberImports.get",
        "management.memberImports.rows",
        "management.memberImports.addRows",
        "management.memberImports.check",
        "management.memberImports.apply",
        "management.memberImports.cancel",
        "management.memberImports.stop",
        "management.memberImports.revert"
      ],
      "cli": [
        "import",
        "imports list",
        "imports get",
        "imports rows",
        "imports apply",
        "imports cancel",
        "imports stop",
        "imports revert"
      ],
      "reference": "imports",
      "evidence": [
        "apps/backend/src/application/member-imports.ts",
        "apps/backend/src/auth/member-imports.ts",
        "apps/backend/src/ledger/participation/imported-progress.ts",
        "apps/backend/src/public/routes/management-member-import-routes.ts",
        "apps/backend/tests/gateway/management-member-imports.test.ts",
        "packages/management/cli/commands/member-imports.mjs"
      ]
    },
    {
      "id": "data-export",
      "support": "unsupported",
      "description": "There is no bulk export of members, completions, points history or audit records. Page through management reads, such as management.points.history, management.leaderboards.standings and management.trackers.records.",
      "entrypoints": [],
      "evidence": ["apps/backend/src/public/routes/management.ts"]
    },
    {
      "id": "completion-revoke",
      "support": "native",
      "description": "Operators with review permission revoke an accepted completion with a reason (management.completions.revoke({ completion, reason, freeOccurrence })). In one transaction, the completion stops counting for progress, prerequisites of new attempts, counts, profiles, and referral qualification. Its point awards are reversed up to the member's unheld total. Tiered rewards it granted are voided, or lowered to the tier another completion still grants. Rewards already collected are flagged. Discord role grants lose their justification and schedule shared membership reconciliation. Relay removes only its own assignments after the last accepted grant across projects and environments is revoked. Referral programs that qualified on the completion are re-evaluated, and the inviter's award is reversed when the member no longer qualifies. A group membership its group reward added ends unless an operator's add or another unrevoked completion also backs it, or the group is archived; the result's groups reports whether the member is still in each group. A streak milestone it paid is revoked while its days stay. Any point remainder is persisted and reported for each point ledger with no member debt. Console keeps the remainder visible and links an operator to the existing explicit point adjustment. freeOccurrence lets the member complete the quest again for the same completion key. Otherwise, later attempts for it are rejected. Freeing the occurrence returns to each quest budget what the completion was charged (budgets: budget, released), less the cost of points the revoke could not reverse for a point-priced budget; a kept occurrence leaves budgets spent. Completions it already unlocked stay. The action ID makes the command idempotent. API keys with review permission can revoke in live. Console member detail and attempt detail offer the action, and the attempt history shows the revoke and its reason. A revoke made by mistake is undone with management.completions.restore({ completion, reason }) or domino completions restore <completion> --reason <text> (review permission, live too): it re-applies what the revocation recorded, not a fresh reward evaluation. The completion counts and holds its occurrence again; the points the revoke reversed are awarded again (points it could not reverse are not); badges it revoked are awarded again; group memberships it removed are re-added unless the group is archived; tiered rewards it voided are restored and rewards it lowered or flagged earn their tier again, unless an operator changed them since; Discord role grants it stopped count again, and those that never reached Discord are queued again; referral programs qualify on the completion again; a streak milestone it revoked is awarded again. Budget a freeing revoke returned is charged again (budgets: budget, charged), and no webhook is sent. Restoring refuses with 409 completion-not-revoked when the completion isn't revoked, with 409 occurrence-taken (naming the completion) when the revoke freed the occurrence and the member completed it again, and with 409 budget-exhausted (naming the budget) when a budget has no room for the charge. Member detail lists revoked completions with each revoke's reason and result (revokedCompletions), Console member detail and attempt detail offer Restore completion, and the attempt history shows the revoke and the restore, each with its reason.",
      "entrypoints": [
        "management.completions.revoke",
        "management.completions.restore"
      ],
      "cli": ["completions restore"],
      "reference": "management",
      "evidence": [
        "apps/backend/src/ledger/participation/completion-revocation.ts",
        "apps/backend/src/ledger/accounting/completion-revocation.ts",
        "apps/backend/src/ledger/operations/completion-restoration.ts",
        "apps/backend/tests/contract-completion-revoke.test.ts",
        "apps/backend/tests/contract-completion-restore.test.ts",
        "apps/backend/tests/ledger/accounting/completion-revoke.test.ts",
        "apps/console/src/features/console/completion-revoke.tsx",
        "apps/console/src/features/console/completion-revoke.test.tsx"
      ]
    },
    {
      "id": "accept-anyway",
      "support": "native",
      "description": "Operators with review permission accept a rejected attempt anyway, for any member, with a reason (management.attempts.acceptAnyway({ attempt, reason }), domino attempts accept-anyway ATTEMPT --reason TEXT). It reverses a reviewer's rejection or an evaluator's: the same attempt is decided again under its pinned release through a reviewer's acceptance, so completion, rewards, budgets, referral qualification and the accepted events and webhook follow as for an approval, and its retry cooldown ends. The rejection and the reversal both stay in the attempt's events, the reversal as a staff-decision with decision accept-anyway and its reason. Only the member's latest attempt on the quest qualifies (409 attempt-superseded; management.attempts.get answers supersededBy), and not on an archived quest (409 quest-archived). A cap the acceptance would break refuses it instead of rejecting it again: completion-cooldown, budget-exhausted, completion-revoked or already-completed. Any other status is refused with attempt-not-rejected. The action ID makes it idempotent. Allowed in live. Console's attempt detail offers Accept anyway on the member's latest rejected attempt. Test members keep testMembers.acceptAnyway.",
      "entrypoints": ["management.attempts.acceptAnyway"],
      "cli": ["attempts accept-anyway"],
      "reference": "management",
      "evidence": [
        "apps/backend/src/ledger/participation/attempts.ts",
        "apps/backend/tests/attempt-accept-anyway.test.ts",
        "packages/management/cli/commands/inspect.mjs",
        "apps/console/src/features/console/attempt-accept-anyway.tsx",
        "apps/console/src/features/console/attempt-accept-anyway.test.tsx"
      ]
    },
    {
      "id": "evidence-redaction",
      "support": "native",
      "description": "Operators with review permission delete the evidence a member submitted for a decided attempt, with a reason (management.attempts.redactEvidence({ attempt, reason }), domino attempts redact-evidence ATTEMPT --reason TEXT), in test and live. The photo leaves the evidence bucket and downloads of it answer 404; the text, URL or quiz answers leave the attempt and its quest submission; what the evaluator's capabilities read (attempts.capture) is deleted. The attempt keeps its status, reason, completion, rewards and history, and management.attempts.get answers its redaction ({ at, actor, reason }) with evidence null and no input. Every other attempt that submitted the same photo is redacted with it; the result lists the attempts redacted, empty when it already was. Accepted, progressed, rejected and failed attempts qualify; anything still waiting for evaluation or review is refused with 409 attempt-not-decided. A redacted attempt cannot be retried (409 evidence-redacted); attempts.acceptAnyway still works. Each redacted attempt records an evidence-redacted event in the reviews audit category. The action ID makes it idempotent. Duplicate-photo hashes (ctx.images.seen) are kept. Console's attempt detail offers Delete evidence.",
      "entrypoints": ["management.attempts.redactEvidence"],
      "cli": ["attempts redact-evidence"],
      "reference": "management",
      "evidence": [
        "apps/backend/src/ledger/participation/evidence-redaction.ts",
        "apps/backend/src/images/evidence-deletions.ts",
        "apps/backend/tests/attempt-evidence-redaction.test.ts",
        "packages/management/cli/commands/inspect.mjs",
        "apps/console/src/features/console/attempt-evidence-redaction.tsx",
        "apps/console/src/features/console/attempt-evidence-redaction.test.tsx"
      ]
    },
    {
      "id": "member-ban",
      "support": "native",
      "description": "Experimental. Operators with review permission ban a member in one project and environment, in test and live (management.members.ban(member, { reason, rollback?, actionId? }), POST /members/:member/ban, domino members ban MEMBER --reason TEXT [--rollback completions,referral-payouts,point-awards] [--dry-run] [--yes]); a rollback that reverses referral payouts or point awards also needs publish. While banned, attempts, completions, invitation acceptances and referral qualifications are refused with 403 member-banned, the member's invitation code answers 404 invitation-not-found, attempts waiting for review or a deferral are rejected, an attempt in flight is rejected with code member-banned when it would complete, tracker records counting for the member are skipped with member-banned and never decided again, every leaderboard's standings and any settlement frozen while banned leave them out (approving a pending settlement withholds their prize), referral payouts to and from them stop, engagement payouts wait, member imports refuse their rows, and relay.me.get() answers eligible: false with no reason. rollback runs in a memberBan workflow, one step per transaction with its outcome recorded, resumable: completions revokes every completion through the revocation path keeping occurrences (point reversal, inviter bonus and qualification corrections follow), referralPayouts reverses and writes off the member's referral accounts keeping what they spent as referral debt, pointAwards reverses manual awards, imported opening balances and engagement payouts; balances never go negative. management.members.previewBan(member, { rollback? }) (POST /members/:member/ban/preview, domino members ban --dry-run) is the dry run per point ledger with the inviter's corrected bonus. management.members.unban(member, { reason, restoreCompletions?, actionId? }) (POST /members/:member/unban, domino members unban MEMBER --reason TEXT [--restore-completions]) lifts the ban, stops a running rollback and optionally restores the revoked completions through the restore path; 409 member-not-banned without a ban. members.get answers the latest ban as ban, member lists bannedAt. Each ban, added rollback, unban, finished rollback and restore records an event in the reviews audit category. In live a domino login session is refused; use Console or a personal API key. There is no MCP tool: MCP writes are test-only.",
      "entrypoints": [
        "management.members.ban",
        "management.members.previewBan",
        "management.members.unban"
      ],
      "cli": ["members ban", "members unban"],
      "reference": "management",
      "evidence": [
        "apps/backend/src/ledger/operations/member-bans.ts",
        "apps/backend/src/ledger/accounting/member-ban-reversals.ts",
        "apps/backend/src/public/routes/management-member-routes.ts",
        "apps/backend/tests/member-bans.test.ts",
        "packages/management/cli/commands/members.mjs"
      ]
    },
    {
      "id": "member-erasure",
      "support": "native",
      "description": "Operators with publish permission erase a member on the person's request, in test and live (management.members.erase(member, { reason, actionId? }), POST /members/:member/erase, domino members erase MEMBER --reason TEXT [--yes], which confirms in a terminal and needs --yes otherwise). It can't be undone. Deleted: the profile and uploaded avatar, sign-in identities, linked accounts with their profiles and tokens (queued for provider revocation), app identities, wallets, released accounts, sessions, consent and its pending intents, leaderboard aliases, referral codes, image sightings, destination plugin state, the member's command receipts, every attempt's evidence (as attempts.redactEvidence, with the erasure's reason) and the stored content of tracker records the member's accounts authored. Kept with typed text as [erased] or removed: operator point reasons, ban and unban reasons (the ban stays in force), enrollment withdrawal reasons, redemption cancel reasons, counter and attempt reasons, the ledger and operator audit events naming the member, a test member's name, linked pass subjects and member import rows (accounts, identity and consent go; ref stays; a later revert passes over the row with keptLinks reason member-erased). Kept as it was: the member ID and membership, points, balances, completions, rewards, referrals, settlement rows, enrollments, attempts' status and webhook deliveries. Member reads answer erasedAt; members.get answers profile null. Refused with 409 member-erase-blocked (state) while an attempt waits for evaluation or review or a handover is in progress, changing nothing. Afterwards writes naming the member fail with 409 member-erased, records naming its ID attribute to no one, and the same person signing in becomes a new member. A repeat answers the first erasure; the action ID replays. One member-erased event in the configuration audit category records the actor, reason and counts. There is no MCP tool: MCP writes are test-only and this is irreversible.",
      "entrypoints": ["management.members.erase"],
      "cli": ["members erase"],
      "reference": "management",
      "evidence": [
        "apps/backend/src/application/member-erasure.ts",
        "apps/backend/src/auth/member-erasure.ts",
        "apps/backend/src/ledger/operations/member-erasure.ts",
        "apps/backend/tests/member-erasure.test.ts",
        "packages/management/cli/commands/members.mjs"
      ]
    },
    {
      "id": "evidence-retention",
      "support": "native",
      "description": "A retention job deletes the evidence of one quest's attempts decided before a cutoff, as attempts.redactEvidence does for one attempt (management.attempts.redactEvidenceBefore({ quest, decidedBefore, reason }), POST /attempts/redact-evidence, review permission; in live only an organization admin's Console session or personal key, or a service key, else 403 permission-required; domino attempts redact-evidence-before --quest QUEST --decided-before MS --reason TEXT, which repeats until done and prints the total). quest is a key or ID; decidedBefore is Unix milliseconds and is refused with 400 invalid-request when in the future. Each call deletes up to 100 accepted, progressed or rejected attempts' evidence, oldest decision first, and answers { quest, decidedBefore, attempts, more, redaction }; repeat while more is true. Attempts still waiting for a decision and failed attempts (no decision time) are skipped, never refused, and so is a photo that an attempt outside the pass (undecided, decided after the cutoff or of another quest) also submitted; already redacted attempts are not counted again. Each call records one evidence-redacted-before event in the reviews audit category naming the quest, cutoff, count, attempts and reason. The action ID makes a call idempotent. The hosted app's own key lacks review, so a scheduled job uses a service key with review, which an organization admin creates, saved as an app secret.",
      "entrypoints": ["management.attempts.redactEvidenceBefore"],
      "cli": ["attempts redact-evidence-before"],
      "reference": "management",
      "evidence": [
        "apps/backend/src/ledger/participation/evidence-redaction.ts",
        "apps/backend/tests/attempt-evidence-retention.test.ts",
        "packages/management/cli/commands/inspect.mjs",
        "docs/public/build/schedules.md"
      ]
    },
    {
      "id": "evidence-retention-period",
      "support": "native",
      "description": "Domino deletes the evidence of decided attempts automatically, 90 days after the decision by default, in both environments of the project. management.attempts.evidenceRetention() (GET /evidence-retention, read permission; domino attempts evidence-retention) answers { evidenceRetention, noticeEndsAt }: evidenceRetention is { days } (1 to 3650) or \"keep\", and nothing is deleted automatically before noticeEndsAt (Unix milliseconds), 90 days after the project was created or, for projects that existed when the setting shipped, after the ship date. management.attempts.setEvidenceRetention({ evidenceRetention }) (POST /evidence-retention, publish permission; domino attempts set-evidence-retention --days N | --keep) changes it for both environments; a value outside 1 to 3650 days or not a whole number is refused with 400 invalid-request. Shortening the period or switching from keep to days also needs an organization admin's Console session or personal key, or a service key, from either environment, else 403 permission-required; lengthening it or keeping evidence needs only publish. Each change records an evidence-retention-changed event in the project's access history with the old and new value. Domino's hourly job deletes due evidence as attempts.redactEvidenceBefore does, across every quest: undecided attempts and photos shared with an attempt that is not due are skipped, and each batch of up to 100 records one evidence-redacted-before event in the reviews audit category with no actor and no quest. Experimental: a minor release can change these methods and their result.",
      "entrypoints": [
        "management.attempts.evidenceRetention",
        "management.attempts.setEvidenceRetention"
      ],
      "cli": ["attempts evidence-retention", "attempts set-evidence-retention"],
      "reference": "management",
      "evidence": [
        "apps/backend/src/management/evidence-retention.ts",
        "apps/backend/src/maintenance/evidence-retention.ts",
        "apps/backend/tests/gateway/evidence-retention-setting.test.ts",
        "apps/backend/tests/evidence-retention-job.test.ts",
        "packages/management/cli/commands/inspect.mjs",
        "docs/public/concepts/quests.md"
      ]
    },
    {
      "id": "evaluator-defer-progress",
      "support": "native",
      "description": "Quest evaluators return decision.defer({ reason, retryAfterMs?, until? }) or decision.progress({ current, target, reason }). Neither spends budgets or completes the quest. Temporal schedules fresh evaluation runs; failure retries replay the current run. Delay 1s to 1h, default 1 minute; at most 100 deferrals and 24h from first deferred commit. Expiry rejects with an open code. QuestView.progress exposes the latest attempt's committed absolute value while waiting/evaluating. Reviewers can reject a deferred attempt; pause/archive retain admitted attempts on pinned rules.",
      "entrypoints": [
        "decision.defer",
        "decision.progress",
        "management.attempts.review"
      ],
      "reference": "quest-rules",
      "evidence": [
        "apps/backend/tests/deferred-attempts.test.ts",
        "apps/backend/tests/ledger/participation/attempt-transition.test.ts",
        "apps/backend/tests/capability-error-sandbox.test.ts",
        "apps/backend/tests/workflows.test.ts",
        "tests/authoring.test.mjs"
      ]
    },
    {
      "id": "evaluator-member-state",
      "support": "native",
      "description": "Quest evaluators read the member's own Relay state: ctx.member.balance(ledger) returns their balance in a point ledger (handle, key or ID) as relay.me.balances() lists it, and ctx.member.completions({ quest?, since?, limit? }) their accepted, unrevoked quest completions newest first as { quest, key, completedAt }, on the member's clock (an advanced test member's), at most 100. Each is pinned once per evaluation run at its first call, so retries read the same values, and counts toward the run's capability calls; an unknown ledger or quest throws CapabilityError author-error. Rules prerequisites cannot express, such as two of five quests or a balance or a completed quest, go in evaluate with them. The testing kit answers them from facts.member.balances and facts.member.completions.",
      "entrypoints": ["defineQuest", "runQuest", "createTestContext"],
      "reference": "verification",
      "evidence": [
        "apps/backend/tests/member-state.test.ts",
        "tests/testing-kit.test.mjs",
        "tests/testing-kit-contract.test.mjs",
        "packages/contracts/src/evaluator-readers.ts"
      ]
    },
    {
      "id": "evaluator-member-relations",
      "support": "native",
      "description": "Quest evaluators read the member's groups, campaign enrollment and referral progress: ctx.member.groups() returns the keys of the project's unarchived groups they belong to, in byte order; ctx.member.enrolled(campaign) takes a campaign definition or key and returns whether their enrollment is active (false after withdrawal and without one); ctx.member.referrals(program) takes a referral program handle, key or ID and returns their progress as inviter as relay.profiles.referral() does ({ program, key?, title, invited, qualified, milestones }), never codes or accounts. Each is pinned once per evaluation run at its first call and counts toward the run's capability calls; an unknown campaign or program throws CapabilityError author-error. The testing kit answers them from facts.member.groups, facts.member.campaigns and facts.member.referrals.",
      "entrypoints": ["defineQuest", "runQuest", "createTestContext"],
      "reference": "verification",
      "evidence": [
        "apps/backend/tests/member-state.test.ts",
        "tests/testing-kit.test.mjs",
        "tests/testing-kit-contract.test.mjs",
        "packages/contracts/src/evaluator-readers.ts"
      ]
    },
    {
      "id": "retry-cooldown",
      "support": "native",
      "description": "defineQuest({ retryCooldownMs }) (1 second to 7 days) or Console Retry cooldown (a new Console quiz starts at 5 minutes) makes a member wait after the evaluator or a reviewer rejects their latest attempt on a quest they submit. A new attempt is refused with 409 retry-cooldown and retryAt (RelayError.retryAt); member progress and QuestView carry retryAt while it runs, questStatus reports retry-cooldown, and canSubmit is false until then. Rejections Relay makes itself (exhausted budget, revoked completion, expired deferral) and failed executions start none; an acceptance ends it. Omitted means no cooldown.",
      "entrypoints": ["defineQuest", "defineQuestType"],
      "reference": "quest-rules",
      "evidence": [
        "apps/backend/tests/retry-cooldown.test.ts",
        "apps/backend/tests/ledger/participation/attempt-transition.test.ts",
        "tests/react-status.test.mjs",
        "tests/authoring.test.mjs"
      ]
    },
    {
      "id": "completion-cooldown",
      "support": "native",
      "description": "defineQuest({ completionCooldownMs }) (1 second to 7 days) or Console Completion cooldown makes a member wait after an accepted completion, of any key, of a repeatable quest they submit. A new attempt is refused with 409 completion-cooldown and retryAt (RelayError.retryAt); with the retry cooldown also running, the later one is named. A quest that completes once, or one staff confirm or Relay starts, refuses it at definition and publication. Member progress and QuestView carry nextAvailableAt, the latest of the retry cooldown end, the completion cooldown end and an exhausted budget's next reset (omitted when nothing will reopen the quest); questStatus reports completion-cooldown and canSubmit is false until then. Omitted means no cooldown.",
      "entrypoints": ["defineQuest", "defineQuestType"],
      "reference": "quest-rules",
      "evidence": [
        "apps/backend/tests/completion-cooldown.test.ts",
        "apps/backend/tests/admission.test.ts",
        "tests/react-status.test.mjs",
        "tests/authoring.test.mjs"
      ]
    },
    {
      "id": "quest-testing",
      "support": "native",
      "description": "Unit-test a quest locally, as Relay's hosted runtime evaluates it, from @domino-sdk/relay/testing. runQuest evaluates the quest with its published settings, rewards and review switch, freezes Date at now, seeds Math.random and crypto.randomUUID, answers ctx.member, ctx.wallet, ctx.x, ctx.discord, ctx.trackers, ctx.images and ctx.review from fake facts checked against Relay's capability contracts, and checks the decision's rewards against the published bundle. trackerObservation and trackerPage build typed tracker input; expectAccept, expectReject, expectReview, expectDefer and expectProgress assert the decision. An evaluation run makes at most eight capability calls (limits.capabilityCalls): the ninth throws CapabilityError capability-limit, as hosted. Anything quest code throws other than a capability failure it received rejects runQuest with EvaluatorError (code evaluator-error, the error as cause), matching Relay, which fails the attempt once with that code instead of retrying. replayAttempt evaluates current quest code against a captured evaluation run of a test-environment attempt (management.attempts.capture, domino quests replay) and reports whether the decision changed; captures of live attempts are refused with test-environment-required. Quest bundles cannot import the testing kit.",
      "entrypoints": [
        "runQuest",
        "createTestContext",
        "trackerObservation",
        "trackerPage",
        "expectAccept",
        "expectReject",
        "expectReview",
        "replayAttempt",
        "management.attempts.capture"
      ],
      "reference": "quest-rules",
      "evidence": [
        "tests/testing-kit.test.mjs",
        "apps/backend/tests/attempt-captures.test.ts",
        "tests/testing-kit-contract.test.mjs",
        "packages/sdk/src/testing/run.ts"
      ]
    },
    {
      "id": "participant-reward-reads",
      "support": "native",
      "description": "Participants read earned tiers with pickup and collection state, and unified point/tier/integration delivery snapshots. Reads preserve unfamiliar delivery kinds and statuses and follow the current participant session.",
      "entrypoints": ["rewards.entitlements", "rewards.deliveries"],
      "reference": "participant-api",
      "evidence": [
        "apps/backend/tests/participant-reads.test.ts",
        "tests/participant-rewards.test.mjs",
        "tests/react-query.test.mjs"
      ]
    },
    {
      "id": "participant-activity",
      "support": "native",
      "description": "Member-only cursor polling and observation of committed attempts, point awards, entitlements, integration delivery changes and referral qualification. Each poll is one session-bound request; observers pause while hidden and reset for session changes. start=latest follows only new events; RelayProvider uses it to read again only the hooks each event changes.",
      "entrypoints": ["me.activity.list", "me.activity.observe"],
      "reference": "participant-api",
      "evidence": [
        "apps/backend/tests/participant-activity.test.ts",
        "tests/participant-activity.test.mjs"
      ]
    },
    {
      "id": "cli-mutation-recovery",
      "support": "native",
      "description": "The CLI persists receipt-backed project creation, quest archive/unarchive, build submission and deployment publication before sending. domino operations list/show/resume finds and reuses the original scoped payload and actor after process restart; completed operations return their cached committed result, without a webhook signing secret once the CLI has printed it. domino operations forget removes one record and operations prune removes completed ones older than --older-than days (default 30). create resumes repository provisioning and checkout without creating another project. Recovery records never contain authentication credentials. Other management writes require reconciliation before repeating; an actionId alone does not prove replay safety.",
      "entrypoints": [],
      "cli": [
        "operations list",
        "operations show",
        "operations resume",
        "operations forget",
        "operations prune"
      ],
      "reference": "automation",
      "evidence": [
        "packages/management/cli/operations.mjs",
        "packages/management/cli/commands/operations.mjs",
        "tests/cli-recovery.test.mjs"
      ]
    },
    {
      "id": "unattended-agent-mode",
      "support": "native",
      "description": "Whoever launches an agent with nobody present sets DOMINO_AGENT_MODE=unattended; the agent cannot turn it on itself. domino doctor and whoami report agentMode (unattended or interactive). In unattended mode browser login refuses with UNATTENDED_LOGIN_UNAVAILABLE and a command with no credential fails with AUTH_REQUIRED, so remote work needs RELAY_MANAGEMENT_TOKEN. domino feedback appends each report as a JSON line to ./domino-feedback.jsonl or DOMINO_FEEDBACK_FILE instead of sending it, unless --submit or DOMINO_FEEDBACK=submit. The Domino skill then treats the supplied brief as confirmed, records the gaps it fills in CAMPAIGN.md, and ends with a report instead of questions. Live publication still needs human review in Console.",
      "entrypoints": [],
      "cli": ["doctor", "whoami", "login", "feedback"],
      "reference": "automation",
      "evidence": [
        "packages/management/cli/agent-mode.mjs",
        "packages/management/cli/device-login.mjs",
        "packages/management/cli/commands/feedback.mjs",
        "tests/cli-unattended.test.mjs"
      ]
    },
    {
      "id": "resource-lifecycle",
      "support": "native",
      "description": "Read active and archived trackers, referrals, leaderboards, engagement rules, collection slots, quest types, images, point ledgers and offers. Archive or restore explicitly with a reason and receipt-backed action ID through Settings → Resources, the per-kind archive/unarchive commands, or deprecated resources archive/unarchive; offers archive only with offers archive/unarchive and management.offers.archive/unarchive, and resources list lists them in offers. Tiered rewards (defineTieredReward, not badge or group rewards) archive only with the experimental tiered-rewards archive/unarchive and management.tieredRewards.archive/unarchive, and resources list and tiered-rewards list list them in tieredRewards; while one is archived, a deploy or promotion that would make a quest, offer or leaderboard prize newly grant it fails with 422 tiered-reward-archived, while what already granted it, its entitlements and handover keep working and its key stays taken. Publish permission is required; live refuses lifecycle changes from every credential with promotion-required. IDs and history remain. Source omission and redeployment preserve destination lifecycle state. Restoring engagement can settle growth since its retained watermark. Tracker and ledger configuration remains current operator state. Canonical archive/unarchive methods use path verbs per resource; resources.lifecycle remains a deprecated 1.x alias.",
      "entrypoints": [
        "management.resources.list",
        "management.trackers.archive",
        "management.trackers.unarchive",
        "management.referrals.archive",
        "management.referrals.unarchive",
        "management.leaderboards.archive",
        "management.leaderboards.unarchive",
        "management.engagement.archive",
        "management.engagement.unarchive",
        "management.collections.archive",
        "management.collections.unarchive",
        "management.questTypes.archive",
        "management.questTypes.unarchive",
        "management.images.archive",
        "management.images.unarchive",
        "management.pointLedgers.archive",
        "management.pointLedgers.unarchive",
        "management.offers.archive",
        "management.offers.unarchive",
        "management.tieredRewards.archive",
        "management.tieredRewards.unarchive",
        "management.quests.archive",
        "management.quests.unarchive"
      ],
      "cli": [
        "resources list",
        "engagement archive",
        "engagement unarchive",
        "collections list",
        "collections archive",
        "collections unarchive",
        "quest-types archive",
        "quest-types unarchive",
        "images archive",
        "images unarchive",
        "offers archive",
        "offers unarchive",
        "tiered-rewards list",
        "tiered-rewards archive",
        "tiered-rewards unarchive"
      ],
      "reference": "management",
      "evidence": [
        "apps/backend/src/application/resource-lifecycle.ts",
        "apps/backend/tests/resource-lifecycle.test.ts",
        "apps/backend/tests/tiered-reward-lifecycle.test.ts",
        "apps/console/src/features/console/resource-lifecycle.tsx",
        "packages/management/cli/commands/resources.mjs",
        "tests/cli-recovery.test.mjs"
      ]
    },
    {
      "id": "progress-counters",
      "support": "native",
      "description": "Native progress({ distinct, goal: count(n), window, repeat }) completion policies count accepted distinct actions in lifetime, project-calendar or rolling windows. Repeated goals consume separate batches; budget-denied achievements retain evidence and remain final. Management and Console expose history and audited exclude/restore for unsettled evidence. Qualified referrals compose a qualification quest; the snippet includes a seven-day continuous Discord membership claim. From the CLI, domino members counters <member> <quest> pages a member's contributions and domino members moderate-counter <member> <quest> <contribution> --reason excludes one (--include counts it again) as a saved operation (--operation-id).",
      "entrypoints": [
        "progress",
        "count",
        "calendar",
        "rolling",
        "management.members.counters.history",
        "management.members.counters.moderate"
      ],
      "cli": ["members counters", "members moderate-counter"],
      "reference": "quest-rules",
      "evidence": [
        "packages/sdk/src/counters.ts",
        "apps/backend/src/ledger/participation/counters.ts",
        "apps/backend/tests/counters.test.ts",
        "snippets/qualified-referrals/qualification.quest.ts",
        "snippets/qualified-referrals/continuous-membership.quest.ts",
        "tests/cli-resources.test.mjs"
      ]
    },
    {
      "id": "automatic-prerequisite-outcomes",
      "support": "native",
      "description": "Prerequisite-only automatic quests record admitted/skipped decisions per durable transition. Committed point entries schedule current eligibility asynchronously. Members read their own outcomes; operators inspect reasons and linked attempt status codes, preview up to 50 paused outcomes or claim-time budget rejections with declared reward and budget impact, and confirm recovery with review permission and an audited reason. Multiple events reuse one pending once occurrence. A budget-exhausted attempt gets one deterministic retry on the next eligible event; evaluator, staff and revoked-occurrence rejections stay final. Failed point scheduling remains visible and retriable.",
      "entrypoints": [
        "quests.outcomes",
        "management.quests.automaticOutcomes",
        "management.quests.previewAutomaticCatchUp",
        "management.quests.catchUpAutomatic",
        "management.quests.automaticScheduling",
        "management.quests.retryAutomaticScheduling"
      ],
      "reference": "quest-rules",
      "evidence": [
        "packages/sdk/src/automatic-outcomes.ts",
        "apps/backend/src/ledger/participation/automatic.ts",
        "apps/backend/src/ledger/participation/automatic-outcomes.ts",
        "apps/backend/tests/automatic-outcomes.test.ts",
        "apps/console/src/features/console/automatic-outcomes.tsx"
      ]
    },
    {
      "id": "hosted-app-storage",
      "support": "native",
      "description": "Hosted Worker KV and SQL stores declared in relay.json app.storage, separate in test and live, at most 5 each per build and 10 each per project including retained stores, with native KVNamespace and D1Database bindings. Domino provisions per project and environment: test's on staging builds, live's when a build declaring them is promoted to live, with no data copied between them. Forward-only SQL migrations run before activation in each environment, tracked in d1_migrations; each file rolls back on failure with D1's message, and tracking rows make retries safe. Removing declarations retains data; re-declaring reuses resources; deleting an undeclared store (publish) or project purge removes it. domino storage list and GET /storage (read) return names, kinds, declaration status and creation times without provider IDs. With publish, domino storage tables/rows/cell/keys/get and the matching routes browse contents read-only: 50 rows or 100 keys per page, text cut at 2,000 characters, BLOBs as sizes; storage cell reads one SQL value in full, 100,000 characters per page. domino storage uses test unless --environment live; a CLI session cannot delete in live. Console lists, browses and deletes stores under Settings → Storage with Test or Live selected. In test only, domino storage clear and POST /storage/{kind}/{name}/clear (publish) empty named SQL tables or all but kept ones, or delete KV keys under a prefix or all keys, with a dry-run count first; the schema, d1_migrations, sqlite_* and _cf_* stay, a KV call deletes up to 10,000 keys and answers complete, live refuses with test-environment-required, and each clear is audited. Test member reset, purge and erase never touch app storage. Local Wrangler binding names are mirrored manually, with warnings from dev and stage. Static apps, export and other writes are not supported.",
      "entrypoints": [
        "management.storage.list",
        "management.storage.tables",
        "management.storage.rows",
        "management.storage.cell",
        "management.storage.keys",
        "management.storage.value",
        "management.storage.clear",
        "management.storage.delete"
      ],
      "cli": [
        "storage list",
        "storage tables",
        "storage rows",
        "storage cell",
        "storage keys",
        "storage get",
        "storage clear",
        "storage delete"
      ],
      "reference": "hosted",
      "evidence": [
        "apps/backend/src/management/app-storage.ts",
        "apps/backend/tests/gateway/management-app-storage.test.ts",
        "apps/builder/src/storage.ts",
        "tests/builder-storage.test.mjs",
        "tests/builder-storage-status.test.mjs",
        "apps/builder/src/storage-read.ts",
        "tests/builder-storage-read.test.mjs",
        "apps/builder/src/storage-clear.ts",
        "tests/builder-storage-clear.test.mjs",
        "packages/management/cli/commands/storage.mjs",
        "tests/cli-storage.test.mjs"
      ]
    },
    {
      "id": "hosted-live-app",
      "support": "native",
      "description": "An approved promotion puts the hosted app build it carries (the build serving staging when it was prepared, or a rollback's earlier build) live at https://<project-slug>.domino.page. Once approval commits, Domino deploys the same build again with live's own dhk_ management key (read, points and ingest in live; DOMINO_ENVIRONMENT is live), live's app secrets, live's KV and SQL stores (created if missing, with migrations applied before the switch) and a separate DOMINO_SCHEDULE_KEY, then switches the live app, typically within a couple of minutes; the previous live build serves until then. A failed or unfinished (one hour) deployment leaves the previous build serving, and promoting the build again retries it. The live app is public, with no staging sign-in, banner or noindex; participant sign-in from its origin selects the project's live environment. Before the first such promotion, and for a deleted project, the address answers 404 \"Nothing is live here yet\". A slug change moves the address within about a minute without a redirect; delete takes it offline and restore brings it back. Only a person in Console approves. Direct live code deployments are refused with promotion-required.",
      "entrypoints": [
        "management.promotions.prepare",
        "management.promotions.approve",
        "management.promotions.rollback",
        "management.secrets.set",
        "management.liveApp.get"
      ],
      "cli": ["promote", "secrets set"],
      "reference": "hosted",
      "evidence": [
        "apps/backend/src/management/live-hosting.ts",
        "apps/backend/src/management/live-pointer.ts",
        "apps/api/src/live.ts",
        "apps/page-gateway/worker.ts"
      ]
    },
    {
      "id": "hosted-custom-domains",
      "support": "native",
      "description": "An organization admin attaches a hostname such as quests.example.com to the project's live app with domino domains add, Console Settings → Domains, or management.domains.add; reading needs read, from either environment, and API keys cannot attach, re-check or remove. The domain shows a CNAME to cname.domino.page and a per-attachment _domino.<hostname> TXT record (domino-verify=<token>). Once Domino finds the TXT record it sets the domain up at Cloudflare (provisioning), and it is active once Cloudflare reports both the hostname and its certificate active; HTTPS fails until then. Status shows each record against what public DNS returned, Cloudflare's statuses and validation errors word for word, and when Domino last checked and checks next. The domain serves the live app, never staging; before the first live build it answers as <project-slug>.domino.page does, and while it is active GET and HEAD to <project-slug>.domino.page redirect to it (302). One domain per project and 10 per organization. With billing enforcement on, adding one needs the project on Growth or above (403 plan-required) unless Domino support raised the organization's domain limits; a project that moves to a smaller plan keeps its domain. Root domains (example.com), wildcards, IP addresses and Domino's own domains are refused (invalid-hostname): attach a subdomain and redirect the root at the DNS provider. A domain not active within 7 days goes to attention and needs a re-check; re-checks are limited to one every 10 seconds, and adding, re-checking and removing to 30 per organization per hour (rate-limited). A fresh proof from another project takes the domain over and the first project's domain goes to attention. Removing it stops serving within about a minute; re-attaching needs a new TXT record. Domino staff can suspend a domain, which then cannot be removed (domain-suspended). A deployment without custom domains answers 503 custom-domains-unavailable.",
      "entrypoints": [
        "management.domains.list",
        "management.domains.get",
        "management.domains.add",
        "management.domains.recheck",
        "management.domains.remove"
      ],
      "cli": [
        "domains list",
        "domains get",
        "domains add",
        "domains recheck",
        "domains remove"
      ],
      "reference": "hosted",
      "evidence": [
        "apps/backend/src/management/custom-domains.ts",
        "apps/backend/src/public/routes/hosting-domains-routes.ts",
        "apps/page-gateway/worker.ts",
        "apps/backend/tests/gateway/custom-domains.test.ts",
        "packages/management/cli/commands/domains.mjs"
      ]
    },
    {
      "id": "hosted-app-schedules",
      "support": "native",
      "description": "Worker apps declare up to ten schedules in relay.json app.schedules, with intervals or five-field UTC cron and a five-minute minimum. Relay sends signed POST requests to the active staged build in test and to the live app in live, with separate runs, pause state and signing keys. verifyScheduledRequest checks the separate DOMINO_SCHEDULE_KEY. A route may answer with scheduleResult({ outcome, summary, metrics, diagnostics }), recorded with the run; a partial or failed outcome fails a 2xx run. Console Settings Schedules, domino schedules list and domino schedules runs show the latest 20 runs per schedule with results and next runs. domino schedules uses test unless --environment live. Controls need publish and reads need read in the environment; a CLI session cannot run or control live jobs. Jobs run serially with one attempt, no retries and a 30-second timeout. Uncertain results block future runs until an operator confirms recovery; pause/archive persist across builds.",
      "entrypoints": [
        "management.schedules.list",
        "management.schedules.runs",
        "management.schedules.run",
        "management.schedules.setState",
        "management.schedules.recover"
      ],
      "reference": "hosted",
      "evidence": [
        "apps/backend/src/management/app-schedules.ts",
        "packages/sdk/src/server/schedules.ts",
        "apps/backend/tests/app-schedules.test.ts"
      ]
    },
    {
      "id": "badges",
      "support": "native",
      "description": "Project badge definitions authored with defineBadge or in Console. Quests attach badge rewards alongside points and roles. A member holds each badge once; completion revocation removes that completion’s claim and preserves other claims. Reviewers manually award or revoke with an audited reason. Archived definitions remain on existing awards and published quests. Profile badge reads follow profile visibility. Apps compose levels and tiers themselves. Live refuses definition changes with promotion-required, so live badges come from a promotion from test; awards and revokes work in live.",
      "entrypoints": [
        "defineBadge",
        "badge",
        "badges.list",
        "me.badges",
        "profiles.badges",
        "management.badges.list",
        "management.badges.create",
        "management.badges.update",
        "management.badges.archive",
        "management.badges.unarchive",
        "management.badges.member",
        "management.badges.award",
        "management.badges.revoke"
      ],
      "cli": [
        "badges list",
        "badges create",
        "badges update",
        "badges archive",
        "badges unarchive",
        "badges member",
        "badges award",
        "badges revoke"
      ],
      "reference": "authoring",
      "evidence": [
        "packages/sdk/src/badges.ts",
        "apps/backend/src/ledger/accounting/badges.ts",
        "apps/backend/src/ledger/operations/badges.ts",
        "apps/backend/tests/badges.test.ts"
      ]
    },
    {
      "id": "test-members",
      "support": "native",
      "description": "Members an operator or agent creates in the test environment to drive quests end to end through real admission, evaluation and rewards: submit as them (a photo quest's photo uploaded for them), pass a manual quest for them without running its check or accept their rejected attempt anyway (an operator's accept in place of the evaluator's, through the same completion policy, budgets and rewards, shown in their history and the audit; by default without live's refusals or a reason, and with rules: \"live\" and a reason, --live-rules --reason in the CLI, refused as live attempts.acceptAnyway refuses), give them wallet addresses without a signature, link them real X and Discord accounts without OAuth (by { kind, subject } with the numeric user ID as a member import names it, or an X username resolved through X; { provider, id } is deprecated) so quest code's X and Discord reads and tracker attribution use the real account until its owner signs in with or connects it, send synthetic X posts, Discord messages and Discord reactions into managed trackers as them (attributed through the linked account, else a synthetic account linked on first use, with where filters and quest triggers run as for collected records), advance their own clock by days, hours or minutes, or to a later instant (epoch milliseconds or an ISO 8601 instant with an offset), up to 90 days ahead, accept a referral invitation as them (by code or another test member as inviter, with the participant route's refusals, qualification and rewards), redeem an offer as them (by key or ID, with the participant route's price, stock, per-member limit, requirements and refusals, writing the same redemption and entitlement; reset then refuses the member), read their history and reset them, which removes their synthetic accounts and records and unlinks linked accounts and accounts they connected while a staging site acted as them, or purge up to 500 at once with a reason, deleting the redemptions, reservations, reward handovers and referrals reset refuses, giving back the stock unit each redemption, reservation and pickup handover took, and refusing a referral with a member outside the list. Needs publish in test; hosted app keys cannot use them.",
      "entrypoints": [
        "management.testMembers.create",
        "management.testMembers.list",
        "management.testMembers.history",
        "management.testMembers.act",
        "management.testMembers.uploadEvidence",
        "management.testMembers.pass",
        "management.testMembers.acceptAnyway",
        "management.testMembers.advance",
        "management.testMembers.acceptReferral",
        "management.testMembers.redeem",
        "management.testMembers.linkWallet",
        "management.testMembers.unlinkWallet",
        "management.testMembers.linkAccount",
        "management.testMembers.unlinkAccount",
        "management.testMembers.record",
        "management.testMembers.reset",
        "management.testMembers.purge"
      ],
      "cli": [
        "test-members create",
        "test-members list",
        "test-members history",
        "test-members act",
        "test-members pass",
        "test-members accept-anyway",
        "test-members advance",
        "test-members accept-referral",
        "test-members redeem",
        "test-members link-wallet",
        "test-members unlink-wallet",
        "test-members link-account",
        "test-members unlink-account",
        "test-members record",
        "test-members reset",
        "test-members purge"
      ],
      "reference": "campaign-testing",
      "evidence": [
        "apps/backend/src/public/routes/management-access-policy.ts",
        "apps/backend/src/ledger/members/test-members.ts",
        "packages/sdk/src/server/test-members.ts",
        "packages/management/cli/commands/test-members.mjs",
        "apps/backend/tests/gateway/test-members.test.ts",
        "apps/backend/src/application/test-member-wallets.ts",
        "apps/backend/src/application/test-member-records.ts",
        "apps/backend/src/application/test-member-decisions.ts",
        "apps/backend/src/trackers/synthetic.ts",
        "apps/backend/tests/test-member-records.test.ts",
        "apps/backend/tests/test-member-decisions.test.ts",
        "apps/backend/src/auth/test-member-accounts.ts",
        "apps/backend/tests/test-member-accounts.test.ts",
        "apps/backend/tests/test-member-redemptions.test.ts",
        "apps/backend/src/application/test-member-reset.ts",
        "apps/backend/tests/test-member-purge.test.ts"
      ]
    }
  ],
  "excludedManagementMethods": [
    {
      "method": "members.telegramAccount",
      "reason": "Deprecated, removed in 2.0.0: the \"telegram\" entry of management.members.accounts reads the same selection."
    },
    {
      "method": "members.unlinkTelegramAccount",
      "reason": "Deprecated, removed in 2.0.0: use management.members.unlinkAccount with provider \"telegram\"."
    },
    {
      "method": "members.oauthAccounts",
      "reason": "Deprecated, removed in 2.0.0: management.members.identityStates.list reads every identity a member proved, X and Discord included."
    },
    {
      "method": "members.unlinkOAuthAccount",
      "reason": "Deprecated, removed in 2.0.0: use management.members.identityStates.unlink."
    },
    {
      "method": "members.identityLinks.list",
      "reason": "Deprecated, removed in 2.0.0: management.members.identityStates.list covers every identity kind."
    },
    {
      "method": "members.identityLinks.relink",
      "reason": "Deprecated, removed in 2.0.0: use management.members.identityStates.relink."
    },
    {
      "method": "members.unlinkWallet",
      "reason": "Deprecated, removed in 2.0.0: use management.members.identityStates.unlink."
    },
    {
      "method": "points.ledgers",
      "reason": "Deprecated, removed in 2.0.0: management.pointLedgers.list reads the same ledgers."
    },
    {
      "method": "points.createLedger",
      "reason": "Deprecated, removed in 2.0.0: use management.pointLedgers.create."
    },
    {
      "method": "points.update",
      "reason": "Deprecated, removed in 2.0.0: management.points.record makes the same award, spend, adjustment, reversal or set."
    },
    {
      "method": "pointLedgers.save",
      "reason": "Deprecated, removed in 2.0.0: use management.pointLedgers.update(ledger, { name, description })."
    },
    {
      "method": "referrals.control",
      "reason": "Deprecated, removed in 2.0.0: use management.referrals.pause/resume, qualify or resolveReward, which name the member, program or entitlement in the path."
    },
    {
      "method": "trackers.outcomes",
      "reason": "Deprecated, removed in 2.0.0: management.trackers.triggerOutcomes reads the same outcomes and pages by cursor and nextCursor."
    },
    {
      "method": "engagement.versions",
      "reason": "Deprecated, removed in 2.0.0: management.engagement.ruleVersions reads the same versions and pages by cursor and nextCursor."
    },
    {
      "method": "page",
      "reason": "Deprecated, removed in 2.0.0: Console's former page-shaped reads (overview, quests, members and the rest). Compose resource reads: management.metrics.read, quests.list, operations.list, members.list, attempts.list, entitlements.list, inventory.list and audit.list."
    },
    {
      "method": "operations.configure",
      "reason": "Deprecated, removed in 2.0.0: Use quests.pause/resume or redemptions.pause/resume."
    },
    {
      "method": "resources.lifecycle",
      "reason": "Deprecated, removed in 2.0.0: Use each resource archive/unarchive method."
    },
    {
      "method": "trackers.control",
      "reason": "Deprecated, removed in 2.0.0: use management.trackers.pause/resume/refresh."
    },
    {
      "method": "offers.control",
      "reason": "Deprecated, removed in 2.0.0: use management.offers.pause/resume."
    },
    {
      "method": "referrals.suspend",
      "reason": "Deprecated, removed in 2.0.0: use management.referrals.pause/resume."
    },
    {
      "method": "webhooks.disable",
      "reason": "Deprecated, removed in 2.0.0: use management.webhooks.pause."
    },
    {
      "method": "catalog.deploy",
      "reason": "Deprecated, removed in 2.0.0: use management.deployments.publish."
    },
    {
      "method": "catalog.previewDeployment",
      "reason": "Deprecated, removed in 2.0.0: use management.deployments.preview."
    },
    {
      "method": "engagement.publish",
      "reason": "Deprecated, removed in 2.0.0: use management.deployments.preview/publish."
    },
    {
      "method": "engagement.change",
      "reason": "Deprecated, removed in 2.0.0: use management.deployments.preview/publish."
    },
    {
      "method": "engagement.previewChange",
      "reason": "Deprecated, removed in 2.0.0: use management.deployments.preview."
    }
  ]
}
