# Changelog (/docs/reference/changelog)



These notes come from the package changelogs in this documentation revision. Compare them with the versions in `domino doctor --json`, then read the entries between your installed version and the release you plan to use. Follow the [upgrade guidance](/docs/reference/troubleshooting#check-for-an-sdk-upgrade). Versions like `1.0.0-next.N` are the v1 prereleases published before 1.0.0, where breaking changes were allowed between prereleases.

Some stable versions in these notes never reached npm. Each is marked *Not published to npm* and names the first published version that carries its changes; install that one instead. A documentation preview may also include versions not yet published. Confirm that the target release is available on npm before upgrading.

## SDK [#sdk]

### 1.66.1 [#1661]

No changes in this release.

### 1.66.0 [#1660]

#### Minor Changes [#minor-changes]

* 165ca99: Harder sign-in.

  * **`domino login` codes now expire after 15 minutes, not an hour.** Console's approval page shows when the request was made, the country of the IP it came from and the CLI's version and OS, and warns to approve only a code from your own terminal. The CLI sends that description as its `User-Agent`. One client network (an IPv4 address, or an IPv6 /48) holds at most 20 pending sign-ins.
  * **Email sign-in codes name the app.** The email says which app and host the code is for, and to enter it only there. Only the latest code for an address works: an earlier one answers `code-expired`. Ten wrong codes for an address on a project in an hour, or twenty in a day, refuse `emailVerify` with `rate-limited` and `Retry-After` until the window ends, whatever new codes it gets. An address gets at most ten codes an hour from every app together; past that, `emailStart` answers `rate-limited`.
  * **Plugin OAuth connections finish in Console, as the person who started them (experimental).** The vendor's callback no longer connects by itself: it sends the browser to Console with a one-time `oauthCode`, and the new `management.connections.completeAuthorization(plugin, { code })` redeems it within five minutes, for the same person, project and environment only (403 `scope-mismatch` for anyone else, 409 `oauth-flow-expired` for a spent or expired code). Without `returnTo`, the browser returns to Console's Integrations page for the project. `authorize` now refuses automation, service and hosted app keys with 403 `credential-not-allowed`. New experimental exports: `connectionAuthorizationCompleteInputSchema` and `ConnectionAuthorizationCompleteInput`.

### 1.65.0 [#1650]

#### Minor Changes [#minor-changes-1]

* 9a37fe5: Clear test data in hosted app storage. `domino storage clear sql DB --table tickets` (or `--all-tables --keep settings`) empties SQL tables and keeps their schema and `d1_migrations`; `domino storage clear kv PHOTOS --prefix photo:` (or `--all-keys`) deletes KV keys. `--dry-run` counts first, and without `--yes` the command asks in a terminal and refuses unattended. The management API has the same operation as `POST /management/v1/storage/{kind}/{name}/clear`, and `createManagementClient` as `storage.clear(kind, name, input)`. It works only in the test environment: live refuses with `test-environment-required`. It needs `publish`, and each clear is audited. A KV clear deletes at most `limits.apps.storageClearKeys` (10,000) keys a call and answers `complete: false` while keys remain; the CLI repeats until none do. Test member reset, purge and erase still leave app storage alone.

### 1.64.0 [#1640]

#### Minor Changes [#minor-changes-2]

* 0a4d580: Domino now deletes the evidence members submit (photos, text, links and quiz answers) 90 days after the attempt is decided, in test and live, unless you change it. Nothing was deleted when this shipped: evidence that already existed is first deleted 90 days after the change reached Domino, on 18 January 2027 for projects that existed on 20 October 2026, and 90 days after creation for newer projects. Attempts waiting for a decision keep their evidence. Each project's date shows in Console's project settings and in `noticeEndsAt`.

  To keep evidence longer, or until you delete it, change the period in Console's project settings, with `management.attempts.setEvidenceRetention({ evidenceRetention: { days } })` (1 to 3650 days) or `{ evidenceRetention: "keep" }`, or with `domino attempts set-evidence-retention --days N` or `--keep`. Changing it needs `publish`; shortening it, or switching from keep to days, also needs an organization admin or a service key. `management.attempts.evidenceRetention()` and `domino attempts evidence-retention` read it. Both methods are experimental. Each change is recorded in the project's access history, and each deletion pass as an `evidence-redacted-before` event with no actor and no quest. To delete sooner, or per quest, keep using `attempts.redactEvidenceBefore`.

* 59cf5db: `management.identityIntegrations.createIntoAppSecret(input, name)` and `addSecretIntoAppSecret(id, name)` create an app login key, or add a secret to one, and store the new secret as the hosted app secret `name` in the key's environment instead of returning it. The response has `appSecret` and no `token`. They need publish permission, and like every app login key change they work only from an organization administrator's Console session. When the app secret is refused, no key or secret is created. The CLI skill reference and capabilities describe Console's matching "Also save it as an app secret" option.

* 0b1f8de: Operators can ban a member, and optionally take back what they earned. While banned, a member's attempts, completions and invitation acceptances are refused with 403 `member-banned`, their invitation code answers `invitation-not-found`, attempts waiting for a decision are rejected, tracker records that would count for them are skipped with `member-banned`, leaderboards and settlements leave them out, and referral payouts to and from them stop. `relay.me.get()` now answers `eligible`, false while the member is banned (true from older Relay versions).

  `management.members.ban(member, { reason, rollback })` bans, where `rollback` can revoke all their completions, reverse the referral payouts they received, and reverse manual awards, imported opening balances and engagement payouts, a step at a time in the background. `management.members.previewBan(member, { rollback })` previews it per point ledger, and `management.members.unban(member, { reason, restoreCompletions })` lifts it. Banning and unbanning need `review` permission; reversing referral payouts or point awards also needs `publish`. `members.get` answers the member's latest ban as `ban`, and member lists answer `bannedAt`. The CLI adds `domino members ban` (with `--rollback` and `--dry-run`) and `domino members unban`. Member bans are experimental. Member imports refuse rows naming a banned member with `member-banned`.

#### Patch Changes [#patch-changes]

* 166254b: App login now works on a private staging address. `createParticipantBackend` no longer sends `X-Relay-App-Origin` with its proxied calls, and Relay selects a request that carries an app login key by the key even when it also names an app origin, so a Test key registered for `https://<slug>--staging.domino.page` signs members in there without a workaround. Apps on SDK 1.63.0 work without upgrading. The `staging-access-required` error reference says a request with an app login key needs no staging pass.
* ff6c976: The editor help for `useSession` and `createParticipantBackend` now says what an app with its own login does when it signs members in or out without a page load: call `useSession().refresh()` after its own sign-in route succeeds and again after its logout route, and sign out through the app rather than `useSignOut()`.
* 7bd9a79: A refused ID now says what it may hold: an `actionId`, reference or other ID with a `:`, `.` or space is refused with "use 1–100 letters, digits, \_ or - (no :, . or spaces)" instead of a regular expression. A management call refused before sending puts each failing field and its reason on one line, such as `Invalid management request: actionId: use 1–100 letters, digits, _ or - (no :, . or spaces)`; `kind`, `code` (`invalid-input`) and `cause` are unchanged. Relay's own refusal of an invalid request body names the failing fields too, such as `Invalid request: actionId`, with the same status and `code`.

### 1.63.0 [#1630]

#### Minor Changes [#minor-changes-3]

* 1fa78d2: `limits` reads Relay's platform limits as `number`, so a 1.x release can raise one or add one (released SDKs keep checking the values they shipped with, so Relay answers them within those): `limits.quests`, `limits.questTypes`, `limits.collections`, `limits.sdkRuntime`, `limits.memberCompletions`, `limits.dashboardPanels`, `limits.dashboards`, `limits.defer`, `limits.retryCooldown`, `limits.analyticsQueryRows`, `limits.apps` (secrets, KV and SQL stores, migrations, storage browsing and schedules), `limits.stagedDeployment`, `limits.capabilityCalls`, `limits.trackerData`, `limits.trackerPreview`, `limits.walletsPerChain`, `limits.walletEventLogs` and `limits.walletTransactions`. It is exported from `@domino-sdk/relay/schema`, `/authoring`, `/server` and `/testing`, with its `Limits` type.

  Deprecated, with unchanged values and types: the literal-typed limit constants `limits` replaces, which can never change in 1.x. They are `questScaleLimits`, `sdkRuntimeLimits`, `maxMemberCompletions`, `dashboardLimits`, `deferPolicy`, `retryCooldownPolicy`, `analyticsQueryLimit`, `APP_SECRET_LIMIT`, `APP_KV_LIMIT`, `APP_SQL_LIMIT`, `APP_MIGRATION_LIMIT`, `APP_MIGRATION_FILE_BYTES`, `APP_MIGRATION_TOTAL_BYTES`, `APP_STORAGE_ROW_LIMIT`, `APP_STORAGE_PREVIEW_CHARACTERS`, `APP_STORAGE_CELL_CHARACTERS`, `stagedDeploymentLimits`, `APP_SCHEDULE_LIMIT`, `APP_SCHEDULE_MIN_INTERVAL`, `APP_SCHEDULE_TIMEOUT`, `APP_SCHEDULE_RESULT_BYTES`, `maxCapabilityCalls`, `trackerDataMaxBranches`, `trackerDataMaxEntries`, `trackerPreviewLimits`, `WALLETS_PER_CHAIN`, `maxWalletEventLogs` and `maxWalletTransactions`.

  Deprecated and removed in 2.0.0: Relay's own implementation helpers, which apps and quests have no use for. From `@domino-sdk/relay/server`: `participantRoutes`, `participantRoutePattern`, `sequenceCursorSchema`, `sourceDataSchema`, `trackerDataIssue`, `trackerDataIssues`, `whereSchemaIssue`, `validateQuestShape`, `containsReferralIntegrationReward`, `REFERRAL_INTEGRATION_REWARD_ERROR` and `nextScheduleRun`. From `@domino-sdk/relay/schema`: `dashboardIssue`, `normalizeDashboard`, `compatibleSettingKinds`, `isBoundSetting`, `whereSettings` and `sourceContent`. They keep working until then.

  `z` from `@domino-sdk/relay/authoring` stays zod 4 for all of 1.x.

* 052744c: `inspectIntegrationKey({ baseUrl, integrationKey })` from `@domino-sdk/relay/portal-proxy` answers which app login key a `di_` key is: its `id`, `name`, `organization`, `project`, `environment`, `origin`, `identityKind`, `vouchesAccounts`, `secretExpiresAt` and `appOrigins`, the addresses Domino serves the project's app from in that environment. It calls the new `GET /v1/auth/integration`, never returns a secret, and does not count as a use of the key. An unknown, expired or revoked key throws `ParticipantExchangeError` with 401 `invalid-credential`. `identityIntegrationKeySchema` and the `IdentityIntegrationKey` type are exported from `@domino-sdk/relay/server`.

  Behavior change: `domino secrets set` now checks a value that starts with `di_` before storing it. It refuses, storing nothing and exiting 2 with `INVALID_ARGUMENT`, a key that is unknown, expired or revoked, or that belongs to another project or environment, such as a Live key set as Test's secret. It stores a key whose origin is not the app's own address for that environment with a warning, and a key it could not check (network failure, or a Relay without the route) with a warning. A checked key is named in the result's new `identityIntegration` field. Other values are stored as before.

* e71245d: `createParticipantBackend` accepts `onExchangeError(error, request)`, called with each refused sign-in exchange as Domino answered it (a `ParticipantExchangeError` with `status`, `code`, Domino's `message` and `retryAfter`) before the browser gets its answer, so the app's server can log why sign-in failed. The browser's answer is unchanged, also when the callback throws.

  Behavior change: creating an app login key for one of Domino's own app addresses in the environment that address does not serve is now refused with 400 `invalid-request`, and the message names the right environment. A `<project-slug>--staging.domino.page` or preview address belongs in Test, a live `<project-slug>.domino.page` address in Live. Other origins, custom domains included, can still use either environment, and existing keys keep working. An origin that is a URL but not an exact HTTPS origin is refused with a message that names the origin it accepts ("Use the exact origin [https://app.example.com](https://app.example.com), without a path, query or trailing slash.") or "Use an HTTPS origin." instead of "Use an exact HTTPS application origin"; over HTTP the management API now answers that message instead of "Invalid management request". The app login guide and the Domino skill references gain a troubleshooting section with the exchange refusal codes.

* eaaae2a: Test members that `reset` refuses because they redeemed an offer, hold a reservation or a started reward handover, or took part in a referral can now be purged: `management.testMembers.purge({ actionId, members, reason })` and `domino test-members purge MEMBER... --reason TEXT` delete up to 500 test members in one transaction, in the test environment only. The stock unit each redemption, reservation and pickup handover took goes back to its stock item, so the offer's stock and redeemed count stay consistent, and the members' referrals and referral accounting go with them. List both sides of every referral: one with a member outside the list refuses the call with `test-member-reset-blocked` (`referrals affect another member`, naming the unlisted member in `outside`). Every other reason reset refuses still applies, and a refused purge changes nothing. The audit records one event with the operator, the members, the reason and the stock returned. `reset` is unchanged. See [Purge members reset refuses](https://console.domino.run/docs/use/quests#purge-members-reset-refuses).

* 69e017d: Erase a member on the person's request: `management.members.erase(member, { reason, actionId? })` and `domino members erase MEMBER --reason TEXT [--yes]` (`POST /management/v1/members/:member/erase`, `publish` permission, live and test). It can't be undone. The member's profile and uploaded avatar, sign-in identities, linked accounts and their tokens, app identities, wallets, sessions, consent, leaderboard aliases, referral code, the evidence of every attempt and the stored content of the tracker records their accounts authored are deleted; reasons people typed on the rows that stay read `[erased]`. The member ID keeps its points, completions, rewards and referrals. The result is `{ member, erasedAt, deleted }`; while an attempt waits for a decision or a reward handover is in progress it is refused with 409 `member-erase-blocked`. See [Erase a member on request](https://console.domino.run/docs/use/member-erasure).

  Member reads (`members.get`, `members.list`) carry a new `erasedAt` on an erased member, and `members.get` answers `profile: null` for it. After erasure, writes naming the member (points, badges, groups, campaigns, consent, profile changes) are refused with the new 409 code `member-erased`, records naming its ID attribute to no one, and the same person signing in again becomes a new member. A member import's revert passes over an erased member's row and reports it in `keptLinks` with reason `member-erased`. The CLI command asks for confirmation in a terminal and needs `--yes` otherwise.

* bbc3623: Klaviyo is the first destination plugin: connected through Klaviyo's OAuth, it keeps opted-in members' Klaviyo profiles current with `domino_` properties (points, ranks, quests completed, unredeemed rewards, campaigns), subscribes and unsubscribes them as their email consent changes (a backfill or resync never resubscribes someone Klaviyo has unsubscribed), and records Domino's events as `Domino …` metrics. It never calls Klaviyo's send, campaign or flow APIs. See the new guide, "Send reminders from Klaviyo".

  `management.connections` gains, experimental like the rest of it:

  * `connect(plugin, { config, dryRun: true })` connects a destination that supports a dry run, in the test environment, without secrets or the vendor's account. It records what it would have sent instead of sending it. `Connection.dryRun` and `PluginInfo.dryRun` say which. Refusals: 409 `dry-run-not-supported`, `dry-run-test-only`.
  * `activity(plugin)` reads a destination's delivery record: last sync, failing state, members synced and skipped per reason, the latest backfill or resync with its progress, and the latest 20 errors.
  * `resync(plugin)` sends every member's profile and attributes again, never past events; one runs at a time.
  * `dryRunLog(plugin, { cursor, limit })` pages through a dry run's requests, newest first.

  These answer 409 `not-a-destination` for other plugins. The CLI adds `domino connections connect --dry-run`, `connections activity`, `connections resync` and `connections log`.

* 9cb68e2: A text setting can be secret: `settings.text({ label, secret: true })`. Operators set its value in Console or with `management.quests.updateSettings`, and the quest's code reads it as usual, but no management response returns it. Releases leave secret settings out of `values` and `configuration`, and list the ones that hold a value in the new optional `withheldSettings`. So `domino deploy --json`, `--preview --json`, `quests list`, `releases get`, deployments, attempts and member reads no longer print a shared code a guest must not see. Console shows a secret as **Set** or **Not set**, with a field to replace it and **Clear**. A secret setting takes no default: one with a non-empty `default`, or a value from a deploy entry's `settings`, is refused, and reusable quest types refuse secret settings. A secret's entry in a release's `fields` says `secret: true`. Settings without `secret` behave as before. Withholding follows each release's own fields, so releases and deployments from before a setting became secret still show its old value: set a new one after marking an existing setting secret.

* 8ee88a9: Test members can redeem an offer: `management.testMembers.redeem(member, { offer })`, with the offer's key or ID, and `domino test-members redeem MEMBER OFFER`. The redemption runs as a signed-in member's would, with the same price, stock, per-member limit, requirements and refusals, writes the same redemption and entitlement, answers the redemption, and replays by action ID. It works in the test environment only, and the audit names the operator. A test member with a redemption can't be reset.

* 6fe6f22: The testing kit lists and replays every capability reader, and a few small defaults change.

  * **`capabilityCalls`.** `runQuest`, `createTestContext` and `replayAttempt` results gain `capabilityCalls`: every capability call as `{ sequence, request, outcome }`, with an open `CapabilityCallRequest` whose `kind` names the reader. It includes the calls `calls` left out: Telegram, NFT, contract call, delegation, wallet event and transaction reads, and the member's state. `calls` is deprecated.
  * **Replay divergences of any reader.** `replayAttempt` reports a call of any reader the capture cannot answer in the new `capabilityDivergence`, and stops there unless `facts` answers it. Before, such a call of a newer reader failed the replay with `TestSetupError`; now the replay resolves with `decision: null`. `divergence` is deprecated, and is `null` when the first divergence is a newer reader's call.
  * **Tolerant replay.** A captured answer with a field or an enum value a later Relay added, such as a new field on a Discord message, now replays instead of failing. Answers in `facts` are still checked strictly.
  * **Reader fakes.** `fakeWalletReader(facts)` and `fakeTelegramReader(facts)` from `@domino-sdk/relay/testing` build readers with every method Relay supplies, for tests that assemble their own evaluation context. The readers added after 1.0 stay optional in their types; quest code calls them with a non-null assertion, as the snippets now do (`ctx.wallet.nfts!(…)`, `ctx.telegram!.connection!()`), instead of guarding against a runtime that lacks them.
  * **Behavior change: Telegram trackers fire once per record.** `automatic()` on a `telegram.reactions()` or `telegram.messages()` tracker now defaults to `on: "new-record"` instead of `"new-observation"`, so a reaction removed and added again no longer runs the quest again. Published quests keep the mode they were published with; republishing one with this SDK switches it unless it sets `on`.
  * **`domino check` warns `TRIGGER_REFIRES_ON_REFRESH`** for a non-content quest on a YouTube Shorts or TikTok tracker that runs on every observation: each refresh evaluates it again for every video. Use `content({ tracker })` with `performance` for a metric threshold.
  * **`RelayProvider` takes `followWallet`.** `followWallet={false}` keeps a wallet sign-in's session when the wallet switches account or locks. The default, `true`, signs out as before.
  * **Behavior change: `unauthenticated` ends the session.** A participant backend's (`createParticipantBackend`) 401 `unauthenticated` now ends the member's session in the SDK's experience, progress and activity observers, as `not-signed-in` does, instead of reporting an error. `RelayErrorCode.Unauthenticated` names it.
  * **`enrollment` on `not-enrolled` skips.** Tracker trigger outcomes, `relay.quests.outcomes()` and automatic outcomes carry an optional `enrollment` on `not-enrolled` skips: `"quest"` for the quest's own enrollment, `"campaign"` for its campaign's. A campaign skip on a tracker trigger now says "Member has not enrolled in the quest's campaign".
  * **Naming.** A leaderboard's `balance` is the ledger it ranks and its points pool's `ledger` the one the pool pays into; both are documented on the type. New fields that name a ledger are called `ledger`; existing `balance` fields keep their name in 1.x.

* 561f4ab: Tiered rewards (`defineTieredReward`) can be listed, archived and restored. `management.resources.list()` and `domino resources list` list them in a new `tieredRewards` array (`managedTieredRewardSchema`), beside `offers`, and `domino tiered-rewards list` lists them alone. The experimental `management.tieredRewards.archive` and `unarchive` (`POST /management/v1/tiered-rewards/REWARD/archive|unarchive`) and `domino tiered-rewards archive|unarchive KEY --reason` archive and restore one in test, by key or ID, with `publish` permission; a promotion carries it to live, and Console **Settings → Resources** lists them with the same controls. Each returns `{ resource, changed }`, typed by `tieredRewardLifecycleResultSchema`. 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`, naming it in `reward`. What already granted it keeps granting it, its entitlements and handover keep working, and its key stays taken. Console **Rewards** leaves archived tiered rewards out of its rewards. Promotions now list tiered rewards as definitions of kind `tiered-reward`, and a rollback archives one the earlier promotion did not carry.

* 8d79fd6: Four operations that had two public paths with different results now agree, or say how they differ:

  * `management.deployments.promote`, `submit` and `reject` are deprecated, removed in 2.0.0. `promote` prepares a promotion of all of test and publishes nothing; its JSDoc now says so, and that the answered `id` is the promotion's, read with `promotions.get` and rejected by `deployments.get` and `publish`. Use `management.promotions.prepare()` with a client for test, and `promotions.reject` to decline. `submit` and `reject` act only on live deployments made before live refused them. `domino deploy --promote` is deprecated in favor of `domino promote --environment test`, and prints a warning naming it.
  * `management.testMembers.acceptAnyway(member, { attempt, rules: "live", reason })` applies live `attempts.acceptAnyway`'s rules in test: `reason` is required, and an archived quest, a superseded attempt, a completion cooldown, an exhausted budget, an existing or revoked completion and a used key are refused with live's 409 codes, leaving the attempt and balances unchanged. The default (`rules: "test"`) still accepts those, with no reason, so a tester can get a test member unstuck. A `reason` is recorded with the decision either way. `domino test-members accept-anyway MEMBER ATTEMPT --live-rules --reason TEXT` sends it.
  * `GET /v1/reward-deliveries` and `GET /management/v1/reward-deliveries` answer `items` beside `deliveries`, with the same rows, so `paginate((cursor) => relay.me.rewardDeliveries({ cursor }))` and `management.rewardDeliveries({ cursor })` read every page. Both clients fill `items` from `deliveries` when an older Relay sends only `deliveries`. `useExperience().deliveries` reads the role deliveries again on each `delivery.updated` activity event, so a role an operator revoked shows without waiting for focus.
  * `defineLeaderboard` prints a warning, and `domino check` reports a `LEADERBOARD_TIES_DIFFER` warning, when a board's live `ties` differs from its `settle.ties`, including a board that sets only `settle.ties: "earliest"`, whose standings share ranks while its prizes pay first-to-reach. `leaderboardTiesWarning(board)` from `@domino-sdk/relay/authoring` returns that message, or null. `domino check`'s `diagnostics` now include warnings from data definitions beside its errors; only errors fail it.

#### Patch Changes [#patch-changes-1]

* cc04dae: Browser apps bundle less of the SDK: an app's bundler now drops the schemas it never reads, such as tracker, wallet-read and management schemas, instead of keeping every schema in each module the client imports. `@domino-sdk/relay/react` adds about 41 KB less minified JavaScript (235 KB instead of 276 KB), and `@domino-sdk/relay/browser` about 42 KB less. Exports and behavior are unchanged.
* 4842e30: Wording only. The app identity kind example is now `app:game`, including the validation message "Name an app identity kind like app:game: …", since a Telegram account is vouched for rather than modelled as an app identity. The JSDoc of `progress()`, `quests.claim`, `me.claim` and the quest interactions says which meaning of "progress" and "claim" each uses, and `domino test-members pass` help calls `progress(...)` quests counter quests. Examples use the `@XDevelopers` X account. The changelogs mark the versions that were never published to npm.
* 425e4df: Internal: the "HTTPS, or plain HTTP on a loopback host" check for API URLs, sign-in URLs and portal proxy origins now has one implementation, shared through `@domino-sdk/relay/internal`. Published behavior is unchanged.
* 8515f3f: `participantResponses` and `capabilityResponses` from `@domino-sdk/relay/schema` now build each reader on first use, as `managementResponses` and the field-tolerant tables already do. Each reader is built once and stays writable; types and parse results are unchanged.
* 8278803: Relay now refuses a malformed personal API key ID with 400 `invalid-request` ("Use the key's ID, such as ak\_…") on `DELETE /management/v1/tokens/:id` and `PUT /management/v1/tokens/:id/scope`. A key ID is `ak_` followed by letters, digits or `_`. IDs from `tokens.list()` and `domino tokens list` already have that form, so no working call changes: an unknown `ak_…` ID still revokes as a silent success, and a key that belongs to someone else is still refused with 403.
* eb8572b: The testing kit's participant API answers an activity cursor it cannot read as Relay does: 400 `invalid-cursor`, "Invalid cursor. Start from the first page.", instead of `invalid-request`.

### 1.62.0 [#1620]

#### Minor Changes [#minor-changes-4]

* a341b16: Analytics counts Relay's own `key-used` rejection, of an attempt whose completion key another member's completion holds, as its own rejection source. `rejectionsBySource` gains `keyUsed`, and `analytics.query`'s `source` breakdown a `keyUsed` row; `evaluator` no longer includes them, while an evaluator's own `key-used` still counts there. Each `topReasons` item carries `codeSource` (`relay`, `evaluator`, or null without a code), and Relay's `key-used` and an evaluator's are separate rows, in `topReasons` and in the `reason` breakdown. Both fields are optional in the schema, since older servers don't send them. `domino analytics get` marks the code of a top reason Relay wrote, such as `[relay key-used]` or `[relay budget-exhausted]`.

* bd622ae: An app login's server can vouch for its members' X, Telegram and Discord accounts and wallets when it signs them in. `createParticipantBackend`'s `authenticate` may return `accounts` next to `subject`, and `onAccounts` receives each account's outcome (`attached`, `already-linked`, or `refused` with a member import refusal code). The new `exchangeParticipant` from `@domino-sdk/relay/portal-proxy` runs the same exchange from a server without a browser and throws `ParticipantExchangeError`, a `RequestError` with the refusal's `status`, `code` and `retryAfter` in milliseconds (`isParticipantExchangeError` recognizes it). The key needs **Vouches accounts**, which `management.identityIntegrations.setAccountVouching(id, { enabled, reason })` turns on from a Console session with publish permission; identity integrations report it as `vouchesAccounts`. Vouched accounts and wallets carry `vouchedBy: { integration, integrationName }` next to `imported: true`.

* 76be180: An attempt carries `codeSource`: `relay` when Relay wrote its `code`, `evaluator` when the evaluator's `decision.reject({ reason, code })` did, and null without a code (`AttemptCodeSource`, open). `reservedAttemptCodes` no longer grows, so a quest built with any 1.x SDK keeps working when Relay adds a code: `key-used` stays usable as an evaluator's reject code, and `codeSource` tells it apart from Relay's own shared-key rejection. Members still read a rejected attempt's code only when Relay rejected it.

* 5233be5: The experimental billing results read the plan as `BillingPlan` (`KnownBillingPlan | (string & {})`): `subscription.plan` and `scheduledChange.plan` in `management.billing.read()`, `cancel()`, `resume()` and `organization()`, `billingSubscriptionSchema`, `projectBillingSchema` and `organizationBillingSchema`, and the `--json` results of `domino billing show`, `billing organization`, `billing cancel` and `billing resume`, so a plan Relay adds no longer fails to parse. Handle a plan name this SDK does not know. The stable `management.usage.plan()` and `domino usage plan` are unchanged: the `subscription` they carry keeps the plans this SDK knows (`launch`, `growth`, `scale`).

* 3665a4e: Billing is experimental while it is open to Domino staff only: the `management.billing` methods, the billing schemas and types in `@domino-sdk/relay/server`, and the `domino billing` commands may change in a minor release, plan names included. `domino billing` now appears under **Experimental commands**. `management.billing.checkout()` and `changePlan()` take any plan name (`BillingPlan`, `KnownBillingPlan | (string & {})`), and `domino billing checkout` and `billing change` take any `--plan`; Relay refuses a plan it does not offer with 400, as before. `management.usage.plan()` and `domino usage plan` stay stable.

  The CLI's JSON result schemas now leave objects open, as documented, so a JSON Schema validator such as Ajv accepts fields a later release adds; only the variants of a `oneOf` stay closed. `webhooks deliveries`, `storage rows` and `storage keys` results declare the optional `nextCursor` the API already sent. `secrets`, `storage` and `schedules` results include `environment`, and these commands warn on stderr when `RELAY_ENVIRONMENT` or `relay.json` names live while they use test. `contract.json` marks deprecated options with `deprecated`, as it does commands, and `capabilities.json` no longer lists deprecated commands. `management.referrals.control` and `referralControlSchema` now point to `pause` and `resume` instead of the deprecated `suspend`.

* 361a5b1: A deployment keeps the active release of a quest it doesn't change, instead of publishing an equal one with a new ID. Deployment previews and results carry `questChanges`: each quest by key, `created`, `updated` (with the release it replaces) or `unchanged`. `domino deploy` lists the created and updated quests in its confirmation and in the `--preview --json` summary, so a deploy that would replace a quest you didn't touch shows before it publishes.

* 154af63: Discord message and reaction records name a thread's parent channel: `DiscordMessageData` and `DiscordReactionData` gain `parentChannelId`, the channel a thread belongs to (a forum post's forum), or `null` outside a thread. It is absent when it is unknown, because Discord's channel read failed. `channelId` stays the thread's own ID. A trigger's `where: { path: "parentChannelId", equals }` selects a channel's threads; to count a channel and its threads, compare both fields in the evaluator. `management.testMembers.record` and `domino test-members record --data` take `parentChannelId` for Discord records.

* 79b7c68: `keyed(fn, { across: "members" })` makes a quest's completion keys unique across members: each key completes the quest once in the project and environment, for whichever member's completion commits first, such as a single-use code or a post that pays once. The release's completion is `{ kind: "keyed", across: "members" }`, and quest progress and experience reads carry it. Another member's attempt with a used key is rejected with `key-used` and `codeSource: "relay"`, which members see; `attempts.acceptAnyway` refuses it with 409 `key-used` while the key is held. Revoking the holder's completion with `freeOccurrence: true` frees the key. Member imports refuse a row with `MemberImportRefusalCode.KeyUsed` when another member or an earlier row already uses one of its keys.

* 56a0601: `fakeEvaluationMember(methods)` from `@domino-sdk/relay/testing` builds an `EvaluationMember` for a test that assembles its own evaluation context. Pass the methods your code reads; the rest answer for a member with no linked accounts, completions, groups or enrollments, and `balance` and `referrals` fail with a `CapabilityError` of code `author-error`. A member written by hand stops compiling when a minor release adds a method; one from `fakeEvaluationMember` does not.

  The CLI's automation contract (`cli/contract/contract.json`) lists the JSON Schemas of `relay.json` and the checkout's `domino.json` under `files`, so a later release is checked to accept every file an older CLI wrote.

* 5cc0d4d: Revoking a completion with `freeOccurrence: true` now returns the budget it used. Each quest budget the completion was charged gets that charge back, in the same transaction and in the budget period it was charged to, so the freed occurrence can be completed again within the cap. A point-priced budget gets back its charge less the points the revoke couldn't reverse. `CompletionRevocation` reports this in a new optional `budgets` (`budget`, `released`). Revoking without freeing the occurrence leaves budgets spent, as before. Completions recorded before this release give no budget back and report `budgets: []`.

  Restoring such a revocation charges that budget again and reports it in a new optional `CompletionRestoration.budgets` (`budget`, `charged`). The charge is checked against the budget's current limit, or the limit it was first charged under if the quest removed the budget or changed its period or cost. Restore now also refuses with 409 `budget-exhausted`, naming the `budget`, when a budget has no room left for the charge, and then changes nothing. This is a behaviour change: until now, budgets stayed spent after any revoke.

* f9c9f34: `defineOffer`, `defineLeaderboard` (prizes) and `defineReferral` (inviter, invitee and milestone rewards) now throw when given a `badge()` or `groupMembership()` reward, so `domino check` reports it before a deploy. These definitions type-checked but a deployment always refused them; now they fail when defined, with `Badge rewards can only be granted by quests or manually, not by offers` (or `leaderboard prizes`, `referral programs`), or `Group membership rewards can only be granted by quests, not by …`. A deployment refuses them with the same message and 422 `validation-failed`, instead of claiming a tier reward was needed. `badgeReward` and `groupReward` from `@domino-sdk/relay/schema` are deprecated: use `badge()` and `groupMembership()` from `@domino-sdk/relay/authoring`. `defineOffer` from `@domino-sdk/relay/schema` is deprecated too and keeps its old behaviour; import it from `@domino-sdk/relay/authoring`, which does the check.

* dcc32da: `ctx.wallet.events()` takes `newest`, from 1 to 1,000: it returns at most that many of the newest matching events, reading back from the newest block, and stops at the 3,000,000-block or 1,000-log cap instead of throwing `author-error`. `WalletEvents` gains an optional `complete`, `false` when a `newest` read left older matching events out or stopped before `since`. A busy wallet, or a long window on a fast chain such as Arbitrum One (3,000,000 blocks is about 9 days there), no longer has to fail the attempt. A call without `newest` behaves as before: it returns every matching event, now with `complete: true`, or throws `author-error`. A read whose start-block estimate overshot 3,000,000 blocks while the span itself fit is no longer refused. The `onchain-action` snippet reads with `newest: maxWalletEventLogs`.

* 98ad384: A member import carries its real state in a new `state` field: `open`, `checking`, `checked`, `applying`, `applied`, `stopped`, `reverting`, `reverted` or `canceled`, typed open (`KnownMemberImportState | (string & {})`, with `MemberImportState` listing the known values), so Relay may add states. `status` is unchanged and still reads `applied` for a stopped, reverting or reverted import, so code filtering on `status === "applied"` counts those too; read `state` instead. Import rows gain `reverted`, true once the import's revert went through the row. Both fields are optional in the schema, since older servers don't send them. `domino import` and `domino imports` results include `state`, and the CLI polls on it.

  `MemberSummary` (`relay.me()`, `useMe()`) now types `email`, `emails` and `clock` as required, since Relay always sends them: code no longer needs to check them for `undefined`. The SDK reads a `/v1/me` response from a Relay older than these fields with `email: null`, `emails: []` and a `clock` at real time (offset 0), the same as code treating them as absent did. `memberTime()` still accepts an object without `clock`.

  `MemberSummary`, `LeaderboardPage`, `LeaderboardHistoryPage`, `Badge`, `MemberBadge` and `MemberBadges` are marked as response types Relay may add properties to.

* 4bf82e5: More management writes are safe to retry. `memberImports.check`, `apply`, `cancel` and `stop`, and `promotions.approve`, take an optional `{ actionId }` before the call options (`check(id, { actionId })`; `check(id, call)` still works), and so does `webhooks.replay(id, delivery, { actionId })`. `promotions.reject`, `members.unlinkCreatorAccount` and `identityIntegrations.setAccountVouching` take `actionId` in their input. With an action ID, a retry answers with the first result instead of a refusal for the state the first call left. These methods now generate an action ID when you pass none, as other writes do, so the client retries them after a lost answer.

  Inputs fenced by `revision` also take it as `expectedRevision`, the name newer methods use: quest and redemption `pause` and `resume`, `offers.control`, `offers.cancelRedemption`, `handover.begin` and `cancel`, `entitlements.void`, `restore` and `correctCollection`, the pickup policy, `members.unlinkCreatorAccount` and the deprecated account unlinks. Pass either; a call that names two different revisions is refused. `expectedRevision()` and the `ExpectedRevision` type are exported from `@domino-sdk/relay/server`.

  `attempts.acceptAnyway`, `attempts.redactEvidence`, `completions.revoke` and `completions.restore` take their path ID first, as the route names it: `acceptAnyway(attempt, { reason })`. The object form, `acceptAnyway({ attempt, reason })`, keeps working.

* 4bf82e5: `relay.badges.catalog()` reads the project's active badge catalog as `{ items }`, like `relay.me.badges()`, from the new `GET /v1/badges/catalog`. `relay.badges.list()` and `GET /v1/badges` keep answering the bare array and are deprecated. `useBadgeCatalog()` now reads the new route and still returns the array. `createFakeRelay` takes a `badges` seed and answers both routes from it.

  `relay.auth.exchange()`, `ExchangeInput` and `exchangeSchema` are deprecated: `POST /v1/auth/exchange` refuses the body they send with 400, because the route takes `{ subject, accounts?, previousToken? }` with an identity integration server key. Sign a user in from a server with `exchangeParticipant` from `@domino-sdk/relay/portal-proxy`.

  The activity feed's JSDoc, and `paginate()`'s, now say that `me.activity.list` never answers a null `nextCursor`, so a loop over it stops on its own condition, such as an empty page or a count.

* 4bf82e5: ISO times in management responses carry their epoch milliseconds beside them, named `<field>Ms`. `analytics.read` and `analytics.query` (and so each `dashboards.read` panel) send `range.fromMs`, `range.toMs`, each bucket's `startMs` and `endMs`, `previous.fromMs` and `previous.toMs`, and `testEnvironment.lastResetAtMs`; custom domains (`management.domains.list|get|add|recheck`) send `checkedAtMs`, `nextCheckAtMs`, `createdAtMs` and `cloudflare.certificateExpiresAtMs`. The ISO fields are unchanged, and the new fields are optional in the schemas, since older servers don't send them. `management.testMembers.advance(member, { actionId, to })` takes `to` as epoch milliseconds as well as an ISO 8601 instant with an offset; `testMemberClockAdvanceSchema` parses epoch milliseconds to that instant as an ISO string in UTC, so its parsed `to` stays a string.

* ea2c9be: A retention job can delete the evidence of a quest's attempts decided before a cutoff: `management.attempts.redactEvidenceBefore({ quest, decidedBefore, reason })` (`review` permission; in live it also requires an organization administrator's Console session or personal key, or a service key, and refuses any other caller with `403 permission-required`, because the deletion can't be undone) deletes up to 100 attempts' evidence per call, oldest decision first, exactly as `attempts.redactEvidence` does for one, and answers `more: true` while others remain. Attempts still waiting for a decision are skipped, and a `decidedBefore` in the future is refused with `400 invalid-request`. Each call records one `evidence-redacted-before` audit event. `domino attempts redact-evidence-before --quest QUEST --decided-before MS --reason TEXT` repeats the call until nothing is left and prints the total.

* 2de5d71: `@domino-sdk/relay/testing` adds `createFakeRelay(options)`, an experimental in-memory participant API for UI tests of apps built on `@domino-sdk/relay/react`. Seed it with a `member` (or `null` for signed out), `quests`, `balances`, `activity`, `leaderboards` and the rest; it answers the session, logout, experience, points, point history, point ledgers, activity, leaderboard standings and the server integration exchange, under `/v1/...` and `/relay/v1/...`, and returns `handle(request)` for a Playwright or MSW route handler, `fetch` for `createRelayClient`, and `requests`, every request it answered. Each answer is checked against the SDK's response schema, so a seed Relay could never send throws a `TestSetupError` naming the route. Another participant route answers `501` with code `not-faked` until `routes` answers it. The testing guide lists the requests `RelayProvider` makes on mount and while polling. `createFakeRelay` and its `FakeRelay`, `FakeRelayOptions` and `FakeRelayRequest` types are tagged `@experimental`, outside the 1.x compatibility promise: a minor release can change them.

* e58c2ae: Name a member's account the way a member import does, everywhere an operator command takes one: `{ kind: "x" | "discord" | "telegram", subject }` with the numeric user ID, `{ kind: "app:<name>", subject }`, or `{ kind: "wallet", chain, address }` (`identityReferenceSchema`, `IdentityReference`).

  * `members.identityStates.unlink` and `relink` take it as `identity` (`memberIdentityStateChangeInputSchema`, `MemberIdentityStateChangeInput`); `setSignIn` also takes a Cosmos wallet with its `network`. An X or Discord subject names the member's selected account with that subject, else the only one they hold. A subject the member holds through several issuers, none of them selected, fails with 409 `identity-ambiguous`; name it by `issuer` then.
  * `members.lookup` takes it, and sends it as the `provider` and `subject` every Relay version reads.
  * `testMembers.linkAccount` and `unlinkAccount` take `{ actionId, kind, subject }` for an X or Discord account.
  * The CLI takes `--kind` and `--subject` on `domino members lookup` (with `--chain` and `--address` for a wallet) and on `domino test-members link-account` and `unlink-account`.

  Deprecated, removed in 2.0.0: the `{ kind: "oauth", provider, issuer, subject }` and `{ kind: "app", appKind, subject }` input selectors, `memberIdentityLinkChangeInputSchema` and `MemberIdentityLinkChangeInput`, `members.lookup({ provider, subject })`, the test-member `{ provider, id }`, and the CLI's `members lookup --provider` and `test-members link-account|unlink-account --provider|--id`. They keep working through 1.x; `contract.json` marks the deprecated options with `deprecated`.

  Identity reads gain an optional `source`: `member`, `import`, `app-login` or `test-link`, an open string (`IdentitySource`, `KnownIdentitySource`), and `import`, the ID of the member import that attached the identity when Relay recorded it. Identity states of every kind carry them, and so do `members.get`'s `wallets` and `identities` and the deprecated `oauthAccounts` and `telegramAccount`. `imported` is unchanged.

* 3a3feef: A leaderboard can rank its live standings first-to-reach. `defineLeaderboard({ ..., ties: "earliest" })` orders equal scores by each member's latest qualifying entry, earliest first, then by member ID, and gives every member who scored their own rank: the order a settlement with `settle.ties: "earliest"` freezes. Standings pages, `me`, `nearby`, placement, and the management and CLI standings reads all follow it, so a tie-broken board is one paged read.

  `ties` is optional. Omitted or `"shared"`, equal scores share a rank as before, so published boards rank unchanged. It is independent of `settle.ties`; set both on a board that settles so members see the order prizes follow. Members listed through `includeUnscored` still share one rank after every member who scored.

* 7a93f76: `createParticipantBackend` from `@domino-sdk/relay/portal-proxy` accepts a loopback HTTP app origin (`http://localhost`, `http://127.0.0.1` or `http://[::1]`, any port), so a browser test can run the real adapter, identity exchange included, at a local address against `createFakeRelay`'s `fetch`. On such an origin the `domino_app_session` cookie omits `Secure`. Any other `http:` origin still throws, `baseUrl` must still be HTTPS, and Relay still registers only HTTPS app origins.

* 1a70098: `management.members.lookup({ provider: "app:<name>", subject })` finds the member holding a linked app identity, so a server can map one of its user IDs to the member. `members.people()`, `members.list()`, `members.lookup()` and `members.get()` list each member's linked `appIdentities` (`{ kind, subject }`), and `members.list({ q })` also matches part of an app identity's subject. Identities an operator unlinked are neither found nor listed.

* dd7ced3: Experimental: a member import row can carry the member's marketing consent from the platform they come from. The optional `consent` list on `memberImportRowSchema` takes one `{ channel, state: "granted" | "withdrawn", at, wording? }` per channel; applying the row records it with its original time and wording, marked `source: "imported"`, in the row's transaction, unless the member chose on that channel in Relay (their choice always stays) or an earlier import recorded consent as recent or more recent. A check refuses the row with `future-action` for a time in the future, `unsupported-consent-channel` for a channel other than `email`, `consent-wording-required`, `consent-wording-too-long` or `consent-wording-invalid` for the wording, and `duplicate-entry` for a channel named twice. `KnownConsentSource` gains `imported`. The CLI imports reference describes the field.

* 5a34b36: Experimental: members record their marketing consent per channel. `relay.consent.get()` lists the signed-in member's current consent, one entry per channel they chose on (`member`, `channel`, `state`, `at`, `wording`, `source`), and `relay.consent.set({ channel, granted, wording, actionId? })` grants or withdraws one channel. The `useConsent()` React hook returns the query with `grant(channel, wording)` and `withdraw(channel, wording?)`. Relay stamps the time and stores the wording verbatim; a repeat of the current state and wording records nothing. Only `email` is accepted for now: other channels are refused with `unsupported-consent-channel`, a grant without wording with `consent-wording-required`, wording over 2,000 characters with `consent-wording-too-long`, and wording with a NUL or a lone surrogate with `consent-wording-invalid`. Channel, state and source are open strings in results. The consent API is `@experimental` and can change in a minor release. The participant API reference and `capabilities.json` describe it.

* 4b7c062: Integration modules, reward kinds and webhook event types now have open types beside SDK 1.x's closed ones, which keep their exact shapes.

  * `defineQuest` accepts an integration module whose provider the SDK does not know, such as `{ provider: "future-chat" }`. `IntegrationModule.provider` is `IntegrationModuleProvider`, `KnownIntegrationModuleProvider | (string & {})`. The quest builds; its description keeps `integrations` to the modules the SDK knows and adds `integrationModules`, every declared module. Publishing a quest that declares a module Relay does not have is refused with 422 `unknown-integration-module`. Discord and X quests behave as before. Evaluator contexts type readers only for the modules the SDK knows, now named `KnownIntegrationModule`: `EvaluationContext`'s module parameter takes `readonly KnownIntegrationModule[]`, so a helper that passed `readonly IntegrationModule[]` to it explicitly passes `readonly KnownIntegrationModule[]` instead.
  * Release reads (`management.releases.get`, `releases.list`, publish results and every other `PublishedRelease`) add `integrationModules` and `rewardEntries`. `rewardEntries` lists every reward as a `RewardEntry`, `{ type, data }`, with an open `RewardType`; a reward kind the SDK does not know, which a later Relay adds, will appear only there and never in `rewards`. `management.quests.previewAutomaticCatchUp()` adds the same `rewardEntries` beside `rewards`. Older Relay versions omit these fields.
  * `ActivityType` (`KnownActivityType | (string & {})`) from `@domino-sdk/relay/server` names the webhook and activity event types for your own handlers; `WebhookEvent.type` stays a `string`. Ignore a type you don't handle: Relay adds event types.

* de99154: Every sign-in provider Relay has, Telegram included, now has open surfaces beside the closed `AuthProvider` lists, which keep their exact shapes. `SignInProvider` is `KnownSignInProvider | (string & {})`, so handle a provider you do not know.

  * `relay.auth.providers()` adds `signInProviders`, `[{ provider, configured }]` for every sign-in provider. `management.participantSignIn()` adds the same `signInProviders`. Older Relay versions omit both.
  * `relay.auth.signInAccounts()` (`GET /v1/auth/sign-in-accounts`) returns the member's account on every sign-in provider as `SignInAccount`s, shaped like `AccountConnection` with an open `provider` and `status`.
  * `relay.auth.start({ provider, mode })` and `relay.auth.disconnect(provider)` take any `SignInProvider`, such as `"telegram"`.
  * `useAccount(provider)` from `@domino-sdk/relay/react` reads one provider's account, with `connect()`, `disconnect()` and `isConnected`.
  * `management.members.accounts(member)` (`GET /members/:member/accounts`) returns `{ accounts }`, the member's X, Discord and Telegram selections as `ManagedAccount`s with an open `provider`. `management.members.unlinkAccount(member, { provider, subject, expectedRevision, reason, actionId? })` (`POST /members/:member/accounts/unlink`, `review` permission) unlinks one and returns that provider's selection; a retry with the same `actionId` returns the first result.

* 80e09c2: Experimental: operators read a member's marketing consent. `management.members.consent(member)` lists the member's current consent, one entry per channel they chose on, and `management.members.consentHistory(member, { cursor, limit })` pages through every change on every channel, newest first, as `{ items, nextCursor }`; each entry is `{ member, channel, state, at, wording, source }`, and `memberConsentHistorySchema` and `memberConsentHistoryQuerySchema` are exported from `@domino-sdk/relay/schema`. Both need `read` permission (`GET /management/v1/members/:member/consent` and `/consent/history`); a member who never chose reads as empty, and an unknown member fails with 404 `not-found`. `domino members consent MEMBER` prints both, with `--limit` and `--cursor` for the history. The Console member page shows the same, read-only. There is no management write: only the member grants or withdraws consent. The consent API is `@experimental` and can change in a minor release.

* 423b6fc: Plugin connections, experimental. `management.connections` (`list`, `get`, `connect`, `update`, `disconnect`) connects a project to the plugins Domino writes, separately in test and live; a promotion never copies a connection. A connection's config is checked against the plugin's JSON Schema (422 `invalid-config` names the failing `path`), its secrets are write-only and sealed at rest, every change takes an `actionId`, and `update` and `disconnect` take an `expectedRevision` (409 `connection-changed`). The new exports (`connectionSchema`, `connectionPageSchema`, `Connection`, `ConnectionStatus` and the connect, update and disconnect inputs) are experimental and may change in a minor release. The CLI adds the experimental `domino connections list|get|connect|update|disconnect`, test unless `--environment live`, reading secrets from stdin as JSON or at hidden prompts, never from arguments.

* 01b789e: Experimental `management.plugins.list()` answers the plugins a deployment runs, each with the public part of its manifest: `name`, `slots`, the config's JSON Schema (`configSchema`), its secret fields and the Domino event types a destination receives. The new exports (`pluginSchema`, `pluginPageSchema`, `PluginInfo`, `PluginPage`, `PluginSlot`, `KnownPluginSlot` and `knownPluginSlots`) are experimental and may change in a minor release. Console builds each plugin's connection form on **Integrations** from it.

* 2a83792: Plugin OAuth connections, experimental. A plugin whose `plugins.list()` entry has `oauth` connects through its vendor's OAuth instead of entered secrets: `management.connections.authorize(plugin, { config, returnTo?, actionId? })` answers `{ authorizationUrl, expiresAt }`, a person opens the URL and grants access, and the vendor's callback makes the connection `ready`. The URL works once, for ten minutes. Domino refreshes the tokens before they expire and never answers them; when the vendor refuses a refresh, the connection is `attention` with the new `reconnectReason` (`revoked` or `expired`), and authorizing again reconnects it. Disconnecting deletes the tokens, then revokes them at the vendor; a failed revocation is logged. `connect` refuses an OAuth 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` (`oauth.configured` is false). New experimental exports: `connectionAuthorizeInputSchema`, `connectionAuthorizationSchema`, `ConnectionAuthorizeInput`, `ConnectionAuthorization`, `ConnectionReconnectReason`, `KnownConnectionReconnectReason` and `knownConnectionReconnectReasons`. The CLI adds the experimental `domino connections authorize <plugin> --config-json …`, which prints the authorization URL.

* 172c3b5: Experimental: a `viewer` project role and grant end dates, to share a project's dashboards with a sponsor or client. A viewer holds one permission, `dashboards`, which reads `dashboards.list` and `dashboards.read` and nothing else: no members, evidence, quests or settings. Every other role holds it too, and `read` keeps reading dashboards, so no existing person or key loses access.

  `management.grants.set` takes `projectGrantSchema`: `roles` may name `viewer`, and an optional `expiresAt` (epoch ms, or null for no end) ends the grant's access without anyone removing it. An ended grant stays in `grants.list()`, and the access history records each end date. Released SDKs keep reading grants: a grant's `roles` keeps the roles 1.x names, so a viewer's is empty, and the new `projectRoles` lists every role. Likewise a project in `management.access()` keeps `dashboards` out of `permissions` and lists it in the new `projectPermissions`. `projectRoleSchema`, `projectPermissionSchema`, `projectRolePermissions`, `projectGrantSchema` and these fields are experimental.

  Console grants the viewer role and an end date under **Settings → Access → Team**, and shows an ended grant as **Expired**. A viewer's Console shows only **Analytics**, opened on the project's first dashboard. The CLI's agent references describe the role.

* b2b8127: A full deployment to test warns `quests-not-in-bundle` with the keys of quests published from code that it leaves out, such as a starter's sample quests whose files were deleted: they stay as published until archived with `domino quests archive <key> --reason "…"`. Archived quests and quests created in Console are not listed. A deployment input with `partial: true` doesn't warn, and `domino deploy --only` sends it. `domino stage` now prints the warnings of the deployment its build published, and its `--json` result carries them in `warnings`; a build's status (`GET /builds/{id}`) names that deployment in `deployment`.

* a381dde: Staff can delete the evidence a member submitted for a decided attempt: `management.attempts.redactEvidence({ attempt, reason })` and `domino attempts redact-evidence ATTEMPT --reason TEXT` (`review` permission, test and live). The photo, text, URL or quiz answers, and what the quest's capabilities read from them, are deleted; the attempt keeps its outcome, completion and rewards. Attempts that submitted the same photo are redacted together. `management.attempts.get` answers the new `redaction` (`{ at, actor, reason }`). Undecided attempts are refused with `attempt-not-decided`, and `attempts.retry` refuses a redacted attempt with `evidence-redacted`.

* 4443124: Quest code can read whether the member linked Telegram: `ctx.telegram.connection()` resolves to `{ userId }`, the numeric Telegram user ID, or `null`. It needs no connected chat and doesn't ask Telegram; an account an app login vouched for or a member import brought in counts. It reads the account the attempt pinned, like `member()`, and is captured for replay. The testing kit answers it from `facts.telegram.connection`, and `TelegramConnection` is exported from `@domino-sdk/relay/authoring`.

* b9270ff: Telegram Login, the member's Telegram account and its unlink now go through the generic sign-in surfaces, and Telegram's own members for them are deprecated. They keep working, with their routes, through SDK 1.x and are removed in 2.0.0:

  * `relay.auth.telegram.start(mode)` → `relay.auth.start({ provider: "telegram", mode })`.
  * `relay.auth.telegram.account()` → the `"telegram"` entry of `relay.auth.signInAccounts()`.
  * `relay.auth.telegram.disconnect()` → `relay.auth.disconnect("telegram")`.
  * `useTelegramAccount()` → `useAccount("telegram")`. Inside a Mini App, link with `relay.auth.telegram.miniApp.connect(initData)`, then call `invalidate([relayKeys.accounts, relayKeys.experience])` from `useInvalidate()`.
  * `useSignIn().withTelegram()` → `useSignIn().withProvider("telegram")`; `withProvider` now takes any `SignInProvider`.
  * `SignInOptions.telegram` and `useSignInOptions().telegram`, `TelegramSignInOption` and `SignInActions.telegram` → `signInProviders`, below.
  * `relay.auth.providers().telegram.login` and `management.participantSignIn().telegram.login` → the `"telegram"` entry of `signInProviders`. `TelegramSignInMethods` and the new `ParticipantSignInTelegram` are written-out types, so editors show these two deprecations.
  * `management.members.telegramAccount(member)` and `unlinkTelegramAccount(member, input)` → `members.accounts(member)` and `members.unlinkAccount(member, { provider: "telegram", subject, expectedRevision, reason })`.

  `useSignInOptions()` and `signInOptions(relay)` add `signInProviders`, the open sibling of the `oauth` entries of `options`: every sign-in provider Domino configured, Telegram included, in Relay's order, each a `ProviderSignInOption` (`{ kind: "provider", id, label, icon, start() }`). A newer Relay may list a provider this SDK does not know; its label is its name. Against a Relay without `signInProviders`, the list holds its providers and Telegram Login. `SignInMethods` adds `signInProviders` and `SignInActions` an optional `provider(provider)` action. The starters render their provider buttons from `signInProviders`.

  Mini App sign-in (`relay.auth.telegram.miniApp`, `useSignIn().withTelegramMiniApp`, `providers().telegram.miniApp`, `participantSignIn().telegram.miniAppBotId`) is not deprecated.

* 90a20c6: Telegram is now a plugin: `management.connections.list()` and `domino connections get telegram` include it, `ready` while the environment has a connected chat, `attention` with the chat's issue while Domino's bot can't read one, and `disconnected` without chats. Chats still connect only through the bot (`management.telegram.connect()`, unchanged), so `connections.connect`, `update` and `disconnect` answer 409 `plugin-managed` for Telegram and for any plugin that connects through its own flow. `knownPluginSlots` adds `announcement`, the slot Telegram posts quest announcements through. `management.plugins.list()` items gain `console`: `{ component }` when Console shows the plugin's own component instead of the generated card, otherwise null; the schema defaults it to null, since older servers don't send it.

* 8e8e24a: Telegram Mini App sign-in has its own entrypoint, `@domino-sdk/relay/telegram` (experimental): `withTelegram(relay)` adds `relay.telegram` to a client and answers the same client, `relay.telegram.miniApp.signIn(initData)` and `.connect(initData)` do what `relay.auth.telegram.miniApp` did, and `telegramMiniAppInitData()` reads the launch data. Apps that don't import it don't carry it. `relay.auth.telegram.miniApp` and the `telegramMiniAppInitData` exports of `@domino-sdk/relay/browser` and `@domino-sdk/relay/react` are deprecated, keep working through 1.x and are removed in 2.0.0.

  Telegram Login and Mini App sign-in now run in the Telegram plugin, with no change to what they accept or answer: members linked before keep signing in as the same member, and `/v1/auth/telegram/mini-app` takes the same requests. `management.plugins.list()` and `domino connections get telegram` list one more slot for Telegram, which `knownPluginSlots` adds: `identity` (members sign in with it).

* 52ed037: Telegram's trackers and `ctx.telegram` now come from the Telegram plugin, with no change to what they record or read: `telegram.messages()` and `telegram.reactions()` trackers, the bot's webhook at `/v1/integrations/telegram/updates`, and `ctx.telegram.connection()` and `member()` behave as before, and published quests keep working. `management.plugins.list()` and `domino connections get telegram` list two more slots for Telegram, which `knownPluginSlots` adds: `tracker-source` (the plugin writes its trackers' records) and `evaluator-capability` (quest code reads it as `ctx.<plugin>`). A quest may now declare the `telegram` integration module, `{ provider: "telegram" }`, and publishes; before, publishing refused it with `unknown-integration-module`. Declaring it is optional, since every quest gets `ctx.telegram`.

* 2fe2d72: `management.testMembers.history()` and `domino test-members history` name each attempt's and skipped tracker outcome's quest by its authoring key as `questKey`, next to the quest ID in `quest`. It is absent for a quest created without a key. A script can filter a test member's history by quest key without calling `quests get` first.

* 7fa2a06: Test members can submit photo quests. `management.testMembers.uploadEvidence(member, file, { actionId })` uploads a JPEG or PNG of at most 5 MB as the test member's photo, and `act` submits its `id` as `evidence`. `domino test-members act MEMBER QUEST --photo FILE` does both, so the quest's real image check and the review queue can be proven from the CLI. The campaign-testing reference now points photo evaluator tests at `facts.images.review`.

* 06046f1: X trackers can pay the member whose post a collected post quotes or reposts. `automatic({ tracker, beneficiary: "quoted" })` pays the quoted post's author and `beneficiary: "reposted"` the reposted post's author, so a "get noticed" quest can reward members an account picks up: `x.search({ query: "from:examplelabs filter:nativeretweets" })` with `beneficiary: "reposted"`. `xAccountRoles` gains both. A post that is no quote or repost is skipped with `no-account` ("Post is not a quote" / "Post is not a repost"), and a quote or repost of the author's own post with `beneficiary-is-author`. Like `inReplyTo`, these quests don't reward posting and don't raise `x-post-rewards-ungated`. `xAccountRoles` and `SourceRoles` list the roles this SDK knows, and a later release may add roles to them, so code that switches over them needs a default branch.

#### Patch Changes [#patch-changes-2]

* ee3f488: Bulk management writes (`catalog.deploy`, catalog previews and drafts, `deployments.preview`, `releases.publishBatch`, tracker record ingest and `memberImports.addRows`) refuse a body with more than 2,000,000 JSON values with 413 `payload-too-large` before parsing it, whatever its byte size; other management writes refuse one past 500,000. A server handles one body over 4 MiB or 500,000 values at a time and answers another 503 with `Retry-After` when it has waited 10 seconds. `memberImports.addRows` now takes at most 64,000,000 bytes, like the other bulk writes, so a single batch above 64 MB from an older CLI gets 413. `domino import` now splits rows into batches of at most 500 rows, 16 MB and 1,000,000 JSON values, sends a row larger than that in a batch of its own, and refuses a row too large for one call (63 MB or 1,990,000 values) before uploading anything, naming its line.

  The management client, the CLI and the participant client now send any call again after a 503 with `Retry-After`, within its deadline and attempts, including writes without an action ID: Relay answers such a 503 only to a request it refused before doing anything. Before, those writes were sent once.

* f13cfb6: A `domino login` CLI session creates and scopes personal API keys in test only. `management.tokens.create` with `environment: "live"`, and `management.tokens.scope` on a live key, now answer 403 `console-approval-required` from a CLI session, whatever `X-Relay-Environment` selects: a live key would carry the live operations (points, attempt confirmations, reviews, pauses) that the CLI session itself is refused, and would outlive `domino logout`. Create and change live API keys in Console (**Settings → API keys**). A CLI session still lists its keys and revokes any of them, live included, and Console sessions are unchanged.

  This changes a 1.x behaviour, an exception Peter Szoke granted on 2026-10-15: no request or response shape changes, and the refusal is a typed 403 that says where to go. `domino tokens create --environment live` prints that refusal, and `domino tokens --help` says live keys come from Console.

* eed1808: Participant cookies on HTTPS now use `__Host-` names (`__Host-relay_session` and the sign-in binding cookies), so one app on `domino.page` can no longer set them for another, and a cookie name the browser sends twice reads as absent. `createParticipantBackend` forwards the OAuth return binding as `__Host-relay_auth_return` when the browser has it, otherwise `relay_auth_return`, and forwards neither when a name appears twice.

  Members signed in when this deploys are signed out once and sign in again, and a sign-in started just before the deploy fails once and starts over. Older SDK versions keep working: through an identity integration, Relay still sets and reads `relay_auth_return`.

* 94ca2b5: Unlinking an imported or vouched account now releases it. `management.members.identityStates.unlink` (and `domino members unlink-identity` and the deprecated per-kind unlink methods) on an X, Discord or Telegram account or wallet that a member import or an app login vouched for, and that the member never signed in with or proved, removes it from the member instead of keeping it bound: its owner can then link it and sign in with it. The answer shows it unlinked with `canRelink: false`, and later imports and app-login vouching refuse it for that member with `identity-unlinked`. Accounts the member signed in with or proved keep the previous behaviour: the link is cleared, ownership stays, and it can be relinked.

* d4fcade: Role delivery helpers ignore announcement rows. `useExperience().deliveries`, `roleDeliveryStatus` and the watch behind them count only `grant-role` deliveries, so a quest that only posts an announcement in a Telegram chat or Discord channel no longer shows "Role delivery pending" or "Role granted", and its pending post is no longer polled every 5 seconds. A quest with a Discord role reward reports its role as before, also when it announces. `roleDeliveryStatus` applies the same rule to raw `me.rewardDeliveries()` rows, and `RoleDelivery` gains an optional `action`. `me.rewardDeliveries()` itself still returns every delivery.

* 8064983: Deployment previews, records and history pages (`deploymentPreviewSchema`, `deploymentRecordSchema`, `deploymentHistoryPageSchema`, and the `management.deployments` methods that return them), `collectionSlotSchema` and `questCollectionSchema` read collections without rejecting unknown fields, so a field a newer Relay adds to a deployment collection no longer fails the response. Collection inputs (`collectionSlotDeclarationSchema`, `catalogDeploymentSchema` and saved draft changes) still reject unknown keys.

* 980e6ca: Sign-in rate limits count an IPv6 client by its /64, so every address in one /64 shares the per-IP limits on email, wallet, OAuth and Telegram sign-in starts, `domino login` device starts and staging password attempts. An IPv4-mapped IPv6 address counts as its IPv4 address. Per-address limits now count per client: three sign-in codes per email address and ten wallet challenges per wallet address from one IP address every ten minutes, so another client hitting them no longer refuses that address's own sign-in, on this project or another. Each project and environment also caps an address at ten sign-in codes and thirty wallet challenges every ten minutes from all clients together.

* d683d00: Sign-in rate limits per IP address count each member of a Domino-hosted app on their own. Hosted apps reach the API from Cloudflare's shared Worker address, so until now every member of every hosted app shared one set of per-IP limits. Domino now attests the visitor's IP on every request a hosted app's server sends to the API, with no app change or rebuild. A request from Cloudflare's shared Worker address without Domino's attestation, such as from an app you run on your own Cloudflare Workers, is counted per project and environment instead of with every other Worker.

* 081806c: Personal keys (`ak_…`) and MCP OAuth tokens that Clerk does not recognize now count against the client that presented them, on the management API, `git` and the MCP server: after 30 in 10 minutes from one IPv4 address or IPv6 /64, counted by each API server on its own, every `ak_` and MCP token from it answers 429 `rate-limited` with `Retry-After` until the window ends. Expired, revoked and other refused credentials Clerk knows are not counted, and neither is Clerk refusing Domino's own secret key. A refused value presented again within a minute can be refused without asking Clerk, and a value no Clerk credential can be (under 16 characters after its prefix, over 4096, or with a character a bearer token cannot hold) is refused with 401 `invalid-credential` without asking Clerk. When Domino is checking many new credentials at once, a key or token it has not accepted recently can answer 503 with `Retry-After: 1`. Clerk's own rate limit now answers 503 with Clerk's `Retry-After`.

* 114e0c7: Telegram and Discord install links now tell whoever opens them which Domino project they connect to. The Telegram bot's reply to a link, before the chat picker, names the organization, the project's name and slug, and the environment, and so does its `Connected …` reply. A Discord install link sent to a server's administrator (**Send to your server admin**) first opens a page naming the same, with a **Continue to Discord** link; the page changes nothing, so a link preview that fetches it stays harmless, and the page shown after connecting names the project too. Creating either link (`management.telegram.connect()`, `POST /management/v1/integrations/discord/links`) now reads the organization's name from Clerk, and answers 503 rather than issue a link that names nobody when Clerk can't answer. Links made before this change still work and name no project. Console's own Discord installation is unchanged.

* 92ee725: Connecting a Discord server or Telegram chat where Domino's shared bot already holds rights now takes someone who holds those rights too. In Discord, installing the bot (Console's **Connect Discord** or a shared install link) refuses a user who is neither the server's owner nor an Administrator when the bot already has a permission there they lack, such as Manage Roles another organization gave it; the install page says "Domino's bot already has permissions in this server that you don't have. Ask a server administrator to connect it." and nothing is connected. A fresh install, where the bot holds nothing beyond `@everyone`, still needs only Manage Server. In Telegram, an administrator who lacks an administrator right the bot holds in the chat, such as posting messages, is refused with a reply naming the missing rights; the chat's creator always connects it. Reusing a Discord server the organization already connected is unchanged, and existing connections are not revisited.

* 10826dc: An organization administrator's CLI session lists the project's identity integrations (app login keys) and their claimed kinds: `domino identity-integrations list` and `domino identity-integrations kinds`, or `management.identityIntegrations.list()` and `kinds()`. They return each key's origin, identity kind and secrets' expiry and last use, never a secret. Creating, rotating and revoking keys and their secrets stays in Console; non-admin CLI sessions and every key type are refused.

* c391c86: Documentation only: the JSDoc for `pointLedgers.create`, `pointLedgers.update`, `calendar.configure`, `profiles.configure` and the `campaigns` methods now says what live refuses. In live, every definition write is refused for every credential, Console included, unless it applies an approved promotion; `campaigns.enroll` and `campaigns.withdraw` are operations that need `enroll` and work in live with a personal API key.

* 1c61073: `domino tokens list|create|scope|revoke` manage your personal API keys from a `domino login` session, as `management.tokens` does. `domino tokens create --name NAME --permission P [--ingest-tracker T] [--days N]` issues a key for the selected project and environment and prints its one-time `token` and expiry; `--json` prints the SDK's answer, so `domino tokens create … --json | jq -r .token | domino secrets set NAME` stores it. A personal key expires after `days`, 1 to 90, default 30; `--days 91` is refused before anything is sent. `domino tokens list` shows each key's expiry. API keys still cannot manage keys (`credential-not-allowed`), and a credential that must not expire is a service key, which an organization admin creates in Console.

* aaf28f9: Quest submissions are limited to 120 an hour per member in each project environment, counted together across `POST /v1/me/attempts`, `POST /v1/attempts`, `POST /v1/me/submissions/:id/submit` and `POST /v1/attempts/:id/retry`. Past it they answer 429 `rate-limited` with `Retry-After`, before the request is read. The quest controllers keep the saved draft, so `resume()` continues it. The participant reference in the Domino skill states the limit.

* 9f39ea4: An operator's reversal of a referral payout stays reversed. Reversing all or part of a referral payout (Console point history **Reverse**, or `management.points.record({ kind: "reverse", entry })`) writes those points off on the referral account, so no later reconcile pays them again; earlier, the inviter's next invitee activity or a new acceptance of their code paid the reversed amount back. Later bonus growth still pays, and restoring a revoked qualification does not repay the reversed points. `earned` in the referral summary and the management referral accounts excludes the reversed points, so a reversed payout no longer reads as earned.

* b866593: Behavior change: completing a referral program's qualifying quest no longer closes the member's invitation. A friend who claimed an entry quest at sign-in, for example through `management.passes.verify`, can still accept an invitation afterwards; accepting qualifies them in each enabled program whose quest they already completed, on their earliest unrevoked completion, and pays the inviter and the friend once. Completing any other quest still makes `referrals.accept` and `testMembers.acceptReferral` refuse with `invitation-closed`, and a revoked qualifying completion neither closes the invitation nor qualifies. Points the friend earned before accepting don't earn the inviter a percentage bonus.

* 1cdc64b: Staging a commit the project's hosted repository doesn't have (a mistyped SHA, or a commit not pushed yet) now answers a certain 422 `commit-not-found` before any build is reserved, instead of an uncertain 502 `upstream-failed`. `domino stage` reports it as `REJECTED` with `apiCode: "commit-not-found"`, saves no operation to resume and prints no retry command: push the commit, then stage again. Other GitHub failures during the commit check still answer 502 and stay uncertain.

* 3d2f654: Staging's participant API is now private, like staging itself. A participant request that names a staging address (`<slug>--staging.domino.page`) in `X-Relay-App-Origin` answers only when it carries the staging pass, which the staging gateway gives browsers signed in to staging; anything else is refused with 403 `staging-access-required`. Apps on staging keep working with no change: the gateway adds the pass to the requests it hands the app and to legacy `/relay/*` calls, and `proxyParticipant` forwards it with the browser's other cookies. Only callers that sent a staging origin straight to the API, outside the staging address, see the new refusal. The retired `staging-<slug>.domino.run` origin now selects nothing (403 "Application origin is not registered or is offline"). Previews, live apps and identity integrations are unchanged.

### 1.61.0 [#1610]

#### Minor Changes [#minor-changes-5]

* f0ce453: `RelayProvider` follows the EVM wallet a participant signed in with through `useSignIn().withWallet()` or a wallet option of `useSignInOptions()`, so the starters need no code for it. When the wallet switches to another account (`accountsChanged`, compared case-insensitively) or reports none because it was locked or disconnected, the provider signs out as `useSignOut()` does and the app shows its sign-in again; it does not sign the new account in. The same account, including the event wallets send on first connect, changes nothing. After a reload the provider finds the wallet again by name and checks `eth_accounts` without a prompt. `WalletSigner` gains an optional `watchAccounts(listener)`, which `evmSigner()` implements, and `Eip1193Provider` an optional `on` and `removeListener`.
* b369a75: A project connects a Discord server that another project of its organization already connects, ready, in the same environment, without installing Domino's bot again. Any credential with `publish` permission can: an automation key, a CLI session, a personal API key or Console (**Integrations**, **Or reuse**). Installing the bot into a new server stays with an organization administrator in Console.

  * `management.discord.servers()` and `domino integrations discord servers` list the servers the project can connect.
  * `management.discord.connect(server)` and `domino integrations discord connect --server <id>` connect one, once the bot still sees it. They refuse with 409 `server-not-connected-in-organization` for any other server, 409 `integration-connected` when the project already has a Discord connection, and 422 `server-not-reachable` when the bot lost the server. Connecting the server the project already connects changes nothing.
  * `management.discord.connection()` and `domino integrations discord status` read the project's Discord connection.

  The project's audit records each connection as `discord-server-connected`, with who connected it and the project whose connection it reused. An automation key and a CLI session connect only in test.

#### Patch Changes [#patch-changes-3]

* e71f881: `domino check` runs each badge, point ledger, offer, leaderboard, referral, dashboard and tracker entry in a separate Node process and parses its export with the schema Relay parses it with at deployment. A definition that fails, such as an offer without `stock`, reports a `DEFINITION_INVALID` error diagnostic with its `file`, `export`, field `path` and message, and fails the check instead of failing later in staging.
  `defineOffer`, `defineBadge`, `defineLedger`, `defineLeaderboard`, `defineReferral`, `defineDashboard` and `defineTracker` still throw a `ZodError` for invalid input, and its message now names the kind, key and fields, for example `Offer "ladder" is invalid: stock: Invalid input: expected number, received undefined`. Staging shows that message when its data setup fails.
  A deployment, preview or staging activation with an inline or described definition that fails its schema now answers 422 `validation-failed` with that message, where it answered 400 `Invalid request`.

### 1.60.0 [#1600]

#### Minor Changes [#minor-changes-6]

* d258795: A budget can cap what one person gives, not only what one member receives. `budget({ key, scope: "author", period, limit, cost })` counts per tracker record author: the account that caused the record, such as a reaction's reactor, whether or not a member linked it. Put it in a tracker quest's `budgets` beside a `"member"` budget to cap both sides, for example a tip paid to a message's author with `beneficiary: "messageAuthor"`: at most 20 tips a day per receiver and 10 a day per reactor. A tip over either cap is rejected with `budget-exhausted`, permanently; other reactors' tips keep paying. It works on `telegram.reactions()`, `discord.reactions()`, kudos recipients and X `inReplyTo` replies. Author budgets share keys, calendar periods and the atomic commit with the other budgets, and imported completions don't count toward them.

  An author budget needs a tracker trigger. `domino check` reports `AUTHOR_BUDGET_TRACKER_REQUIRED` on another quest, `domino build` and `domino deploy` refuse it, and Relay refuses its publication.

  The types SDK 1.x code reads are unchanged: `Budget` keeps `scope: "member" | "project"`, and a quest's description lists author budgets in the new optional `authorBudgets` (`AuthorBudget`, `authorBudgetSchema` from `@domino-sdk/relay/schema`), apart from `budgets`. `budget()` returns an `AuthorBudget` for `scope: "author"`. A member's quest `budgets` (`QuestBudgetView`) leave author budgets out, since they cap someone else's giving.

* d41f5da: `relay.me.get()` and `useMe()` now return the member's `clock: { now, offsetMs }`, the clock Relay judges their activity on. A test member's clock runs ahead after the staging toolbar's **Next day** or `domino test-members advance`; every other member's `offsetMs` is 0. New `memberTime(me, at?)` from `@domino-sdk/relay/browser` turns a real instant into the member's time, so an app that shows campaign phases or open quests by date follows an advanced test member. `clock` is optional, and `memberTime` returns real time when a Relay older than it omits it. On staging, the toolbar's `domino:test-state-changed` event now carries `detail: { change, actingAs }`: what changed and which test member the page is signed in as.

* e899f47: Member imports carry referrals. A row's optional `referral` names the member's inviter (another row's `ref`, or the sign-in identity of an existing member), when they accepted, and the referral programs they already qualified in (`qualified: [{ program, at }]`). Applying records the referral and the qualifications without paying any referral reward, milestone or bonus; they count toward the inviter's qualified friends, and the friend's later points pay the program's bonus. An inviter's row applies before the rows that name it. New refusal codes: `unknown-inviter`, `self-referral`, `inviter-already-accepted`, `referral-cycle`, `inviter-refused` and `unknown-referral-program`. The check summary adds `referrals` (`accepted`, and `qualified` by program), `management.referrals.participants()` names the import that recorded a relationship or qualification in `import`, and `management.members.get()` returns the member's accepted referral in an optional `referral` (`inviter`, `acceptedAt`, `suspended`, `import`).

* 8ee2000: A member import vouches for Telegram and Discord accounts too. A row's `accounts` takes `{ kind: "telegram", subject }` and `{ kind: "discord", subject }`, each a numeric user ID, at most one of each per member, from `domino import` and `management.memberImports.addRows`. They work like a vouched X account: Telegram and Discord tracker records, quest checks and Discord role checks trust them from when the import attached them, nobody else can link them, and the member's own Telegram or Discord link replaces them. They never sign in: a Telegram or Discord sign-in with one is refused with `sign-in-method-disabled`. A row naming a second account of a kind, or another one than the member's, is refused with the new `MemberImportRefusalCode.TelegramAccountSelected` (`telegram-account-selected`) or `DiscordAccountSelected` (`discord-account-selected`).

  `@domino-sdk/relay/server` adds `memberImportVouchedAccountSchema`, `memberImportFullRowSchema` and `memberImportFullRowsInputSchema`, with the types `MemberImportVouchedAccount`, `MemberImportFullRow`, `MemberImportFullRowInput` and `MemberImportFullRowsInput`, for rows with every kind of vouched account; `addRows` takes `MemberImportFullRowInput[]`. `memberImportAccountSchema`, `memberImportRowSchema` and `memberImportRowsInputSchema` stay the X-and-wallet form. Member identity states carry `imported: true` for an X, Discord or Telegram account an import attached, and so does a connected `ManagedTelegramAccount`.

  An imported X, Telegram or Discord account now pays tracker records from the moment it was imported. Before, a vouched X account had no link time, so its records were skipped as historical. Members imported before this release keep that unknown link time until they link their X account themselves.

* f0d5b8e: Offers can be archived and restored. `management.offers.archive` and `unarchive` (`POST /management/v1/offers/OFFER/archive|unarchive`), `domino offers archive|unarchive OFFER --reason`, Console **Settings → Resources** and the offer's card in Console **Rewards** archive one in test, and a promotion carries it to live. Each returns `{ resource, changed }`, typed by the new `offerLifecycleResultSchema`, with `resource.kind` `offer`. An archived offer leaves member offer reads and refuses redemption with `offer-paused`; management reads keep it, with the new `archivedAt` field on `Offer`, and its redemptions can still be canceled. `management.resources.list()` and `domino resources list` list offers in a new `offers` array (`managedResourceListSchema`), beside the existing kinds. A rollback now archives an offer the earlier promotion did not carry.

* 6b5228b: Quests can announce a completion in a connected Telegram chat or Discord channel. List `telegram.postMessage({ chat, text, replyToTriggeringMessage })` or `discord.postMessage({ channel, text, replyToTriggeringMessage })` in the new optional `announcements` of `defineQuest`, beside `rewards` (at most three; an array or a function of the settings). The text is a template with the placeholders `{beneficiary}`, `{author}`, `{points}`, `{rewards}` and `{quest}` (`announcementPlaceholders`); people are mentioned through their linked account on that platform. Relay posts after the rewards commit, as a reward delivery with the new `RewardDeliveryAction.PostMessage` action and an empty `roleId`, retried with backoff and paced per chat; a failed post never undoes the rewards, and `management.retryRewardDelivery(id)` retries one. Descriptions carry them in the new optional `announcements` field (`announcementSchema`). `discord.channels({ postable: true })` lists channels the bot can't post in as unavailable, and `TelegramChat` gains an optional `canPost`. The CLI's authoring reference documents announcements.

* ea1f033: Revert an applied or stopped member import: `management.memberImports.revert(id, { reason })` starts it, and `domino imports revert IMPORT --reason TEXT` starts it and waits. In the background, one row at a time, it reverses the applied rows' opening balances (taking only points members still have; `unreversedPoints` lists the rest), revokes their imported completions (reason `import-reverted`, freeing the occurrence) and removes the wallets, accounts and identities they linked unless the member signed in with one since (`keptLinks`). Members stay in the project. A reverting or reverted import reads as `applied` with `revert` set, and is reverted once; the same `actionId` answers the import again. Rows applied before imports recorded their links keep them (`linksUntracked`). Reverting needs `publish` permission and works in live from API keys and CLI sessions too. Console shows **Revert import** on applied and stopped imports.
  `MemberImport` gains `revert`: the reason, who asked and when, rows reverted so far, then `revertedAt` and the `outcome`.

* 9a7ad11: Stop a member import while it applies: `management.memberImports.stop(id)` and `domino imports stop IMPORT` let the row being applied finish, apply no more rows, and end the import: it reads as `applied` with `appliedAt` null and `stoppedAt` and `stoppedBy` set, and its remaining rows stay `ready`. A stopped import can't be applied again; import the remaining rows in a new import. Stopping needs `publish` permission and works in live from API keys and CLI sessions too. Console shows **Stop import** on an applying import.
  `MemberImport` gains `stoppedAt` and `stoppedBy`; check `stoppedAt` to tell a stopped import from a fully applied one.

* df3a08c: A test member's clock moves by hours and minutes, or to an instant, so time-of-day rules can be tested: block edges, or midnight in the project's time zone. `management.testMembers.advance(member, input)` takes `{ actionId }` with one of `days` (1–30, unchanged: project-calendar days that keep the time of day), `hours` (1–720), `minutes` (1–43200) or `to`, an ISO 8601 instant with an offset that must be later than the member's clock (else 422 `test-member-clock-past`). From the CLI, `domino test-members advance MEMBER` takes one of `--days`, `--hours`, `--minutes` or `--to`. The 90-day limit holds across units (422 `test-member-clock-limit`): `days` still add up to at most 90 and land no later than the 90th date, and hours, minutes and `to` never move the clock past 90 days from now. `offsetDays` still counts the days advanced with `days`; hours, minutes and `to` leave it unchanged, so read the member's date from `today`. `@domino-sdk/relay/server` exports `testMemberClockAdvanceSchema` and `TestMemberClockAdvanceInput` for every form; `testMemberAdvanceSchema` and `TestMemberAdvanceInput` stay the `days` form.

### 1.59.0 [#1590]

#### Minor Changes [#minor-changes-7]

* 1d5a195: `management.attempts.acceptAnyway({ attempt, reason })` (`POST /attempts/<id>/accept-anyway`, `review` permission) accepts a rejected attempt anyway, for any member, reversing a reviewer's or an evaluator's rejection. The acceptance pays as a reviewer's approval would, with the normal `completion.created` webhook; the rejection stays in the attempt's events beside a `staff-decision` with `decision: "accept-anyway"` and the reason, and the retry cooldown ends. Only the member's latest attempt on a quest that isn't archived qualifies (409 `attempt-superseded`, `quest-archived`), and a cap the acceptance would break refuses it instead of rejecting it again (409 `completion-cooldown`, `budget-exhausted`, `completion-revoked`, `already-completed`). `management.attempts.get` answers `supersededBy`, the member's newer attempt on the quest, or null. The CLI adds `domino attempts accept-anyway ATTEMPT --reason TEXT`.
* a0c77e4: Archiving or unarchiving a group, pausing or resuming a tracker, and changing the project's schedule state take an optional reason, which the audit log records: `management.groups.archive(group, { reason })` and `unarchive(group, { reason })`, `management.trackers.pause(reference, { actionId, reason })` and `resume`, and `management.schedules.setState(state, { reason })`. A reason is a trimmed string of 1–500 characters, like the other lifecycle reasons; leaving it out works as before. The CLI adds `--reason TEXT` to `domino groups archive|unarchive`, `domino trackers pause|resume` and `domino schedules pause|archive|resume`. Console asks for a reason in a dialog for each of these, and the project schedule menu no longer applies a state as soon as you pick it.
* 424863f: Operators can restore an entitlement they voided. `management.entitlements.restore(entitlement, { revision, reason })` (`POST /management/v1/entitlements/<id>/restore`, `review` permission) and `domino entitlements restore <id> --revision <n> --reason <text>` make a `void` entitlement `available` again at the tier an accepted completion still earns, and bring back any referral review the void settled. Restoring is refused with 409 `entitlement-status` when a completion revocation voided the entitlement or no accepted completion still earns it (the body names the `completion` to restore instead), when canceling an offer redemption voided it, or when a referral reward was voided before voids kept their reviews. Console Rewards and member detail offer **Restore reward** on a voided reward, and Audit history lists each restore with its reason.
* 790aa5d: Operators can restore a completion they revoked by mistake. `management.completions.restore({ completion, reason })` (`POST /management/v1/completions/<id>/restore`, `review` permission) and `domino completions restore <completion> --reason <text>` re-apply what the revocation recorded, not a fresh reward evaluation: the points it reversed are awarded again (points it could not reverse are not), its badges, group memberships, tiered rewards, Discord role grants, referral qualifications and streak milestone come back, and budgets stay spent. The result (`CompletionRestoration`) lists what came back. Restoring is refused with 409 `completion-not-revoked` when the completion isn't revoked, and with 409 `occurrence-taken` (naming the `completion` that holds it) when the revocation freed the occurrence and the member completed it again. `members.get(id).revokedCompletions` lists up to 25 revoked completions, each with its revoke's reason and result. Console member detail and attempt detail offer **Restore completion**, and the attempt history and Audit history show the restore beside the revoke.
* 850a252: `settlementPayoutCsv(exported)` turns the result of `management.leaderboards.settlementExport(id)` into the payout list as CSV, with the columns `member,score,rank,share,amount,unit,wallets`. `domino leaderboard-settlements export` now prints it through this helper, unchanged, and Console's prize settlement panel offers &#x2A;*Download payout list (CSV)** for any pool that is not points, with the same content.
* 7ac13ef: Solana transactions from `ctx.wallet.transactions()` now carry `signer`: whether the linked wallet signed the transaction. It is `false` for a transfer someone else sent into the wallet, and for results captured before it existed. Check it before rewarding an action. The new `solana-action` snippet completes a quest once per transaction a linked wallet signed that called your program, optionally receiving at least an amount of a token.
* 5b94440: Anchor instruction decoding for `ctx.wallet.transactions()`. Saving a `settings.solanaProgram()` setting now stores the program's Anchor IDL (legacy or 0.30+), read from its on-chain IDL account, in the published release's new optional `solanaIdls` (`SolanaProgramIdl`: `{ program, source, idl }`, by setting key). For a program without an on-chain IDL, an operator pastes one in Console or sends the new optional `solanaIdls: { [settingKey]: idl | null }` with `quests.updateSettings()` (`anchorIdlSchema`, `AnchorIdl`). With an IDL, each transaction gains optional `instructions`: the program's top-level and inner instructions in call order, decoded as `{ name, args, accounts }` (`WalletInstruction`). An instruction the IDL does not describe is skipped. The new `instruction(tx, name)` helper in `@domino-sdk/relay/authoring` returns the first matching instruction's args, or `null`. The testing kit's `facts.wallet.transactions` accepts `instructions`.
* 95cb8d8: On-chain action checks on Solana. `ctx.wallet.transactions({ program, since, afterActivation?, mint?, address? })` reads the successful, finalized transactions of the member's linked Solana wallets since `since` that invoked `program` in a top-level or inner instruction, oldest first. It returns `{ program, chain, mint, since, at, complete, transactions: [{ wallet, signature, slot, timestamp, programs, balanceChanges: [{ mint, delta }] }], observedAt }` (`WalletTransactions`, `WalletTransaction`, `WalletBalanceChange`, `walletTransactionsSchema`), with `mint` `"SOL"` for lamports (rent included, the fee the wallet paid left out) and `delta` a signed integer string, or null when the member has no linked Solana wallet, and follows `ctx.wallet.balance()`'s capture, `author-error` and `provider-unavailable` rules. With `mint`, only each wallet's associated token account for that mint is read. `since` may be at most 30 days before the evaluation (`maxWalletTransactionLookback`); each wallet's read fetches at most 1,000 transactions (`maxWalletTransactions`), and a wallet with more returns its newest ones with `complete: false` instead of throwing. `WalletReader.transactions` is optional in the type so that existing test doubles compile; Relay supplies it to every evaluation.

  The program comes from the new `settings.solanaProgram({ label, default })` setting, a base58 address (`SolanaProgram`, `isSolanaProgram`) stored as a text setting with `picker: "solana-program"`. Publication, Console settings edits and unarchiving check that the account exists on Solana mainnet and is executable.

  New pure helpers in `@domino-sdk/relay/authoring` read one transaction: `received(tx, mint)` and `spent(tx, mint)` return the wallet's gain or loss as an integer string, or `"0"`; `swapped(tx, { into?, from? })` checks that one mint went down and a different one went up; `invoked(tx, program)`. The testing kit answers `ctx.wallet.transactions()` from `facts.wallet.transactions`.

#### Patch Changes [#patch-changes-4]

* aaa07ad: The SDK and CLI depend on Zod 4.6.5, whose schemas keep their methods on a shared prototype instead of binding about 7 KB of them to every schema instance. Importing the SDK takes far less memory: a Relay backend process that loads the server entry starts with about half the heap. Quest `settings.datetime()` values, member-import times and X post `createdAt` values (including hand-written testing fixtures) still accept minute-precision instants such as `2026-11-01T00:00Z`, which Zod 4.6 alone rejects. An X post read without a `media` field still gives an empty list.

  One tracker data schema change after a rebuild: a field declared as `z.number().min(0).int()` now stores `minimum: 0`, so Relay refuses negative values at ingestion that the app's own schema already refused. Zod 4.3 stored the safe-integer minimum for that order of checks.

### 1.58.0 [#1580]

*Not published to npm. Its changes first reached npm in 1.59.0.*

#### Minor Changes [#minor-changes-8]

* 321763b: `management.discordRoles.list()` lists every Discord role the project's reward deliveries have granted in the environment, with the quests that requested it and its deliveries counted by status (`pending`, `delivered`, `failed`). It reads the whole delivery history, so a role granted only before the newest page of `management.rewardDeliveries` still appears. The response schema is `discordRoleRewardsSchema`.

#### Patch Changes [#patch-changes-5]

* a2318dd: `stagingPathSchema` refuses paths with control characters and accepts only a path that resolves on the staging host. Browsers drop tab and newline from a URL, so a path such as `/<tab>/example.com` used to pass and open another site after a staging sign-in.

### 1.57.0 [#1570]

*Not published to npm. Its changes first reached npm in 1.59.0.*

#### Minor Changes [#minor-changes-9]

* 7012d1c: Contract state and Cosmos staking reads in quest evaluators.

  `ctx.wallet.call({ call, args?, address? })` calls one `view` or `pure` EVM contract function once per linked EVM wallet, every call at one block, through dRPC on the chains `management.walletBalanceChains()` lists: state a contract keeps per address, such as `stakedOf(address)`, `getVotes(address)` or `userInfo(uint256, address)`. `{ wallet: true }` in `args` stands for each wallet's address. It returns `{ call, chain, contract, returns, total, wallets: [{ address, value }], at, observedAt }` (`WalletCall`, `walletCallSchema`), with integers as decimal strings and `total` their sum (null for `bool` and `address` returns), or null when the member has no linked EVM wallet. A revert is an `author-error` carrying the contract's reason. The call comes from the new `settings.contractCall({ label, default })` setting (`picker: "contract-call"`): the contract's CAIP-10 ID and a human-readable signature, such as `eip155:8453:0x… function stakedOf(address) view returns (uint256)` (`ContractCall`, `isContractCall`, `parseContractCall`, `parseContractFunction`). Only `view` or `pure` functions with elementary parameters that return one `uint`, `int`, `bool` or `address` are accepted; publication, Console settings edits and unarchiving also check the chain.

  `ctx.wallet.delegations({ chain, validator?, address? })` reads the native staking delegations of the member's wallets linked on a Cosmos SDK chain Relay lists, at one block, in base units of the chain's staking denomination: `{ chain, denom, validator, total, wallets, validators, at, observedAt }` (`WalletDelegations`, `walletDelegationsSchema`), optionally only to one validator, or null when the member has no wallet on that chain.

  Both follow `ctx.wallet.balance()`'s capture, `author-error` and `provider-unavailable` rules. `WalletReader.call` and `WalletReader.delegations` are optional in the type so that existing test doubles compile; Relay supplies them to every evaluation, and the testing kit answers them from `facts.wallet.call` and `facts.wallet.delegations`. New `contract-state` and `cosmos-staker` snippets accept members whose view call reaches a minimum, or who delegate at least a minimum, optionally to a configured validator.

* 59a01d8: On-chain action checks on EVM chains. `ctx.wallet.events({ event, account, since, afterActivation?, where?, address? })` reads the events of one contract whose indexed address argument `account` is one of the member's linked EVM wallets, from `since` to the chain's finalized block (its safe block on a chain without finality), through dRPC `eth_getLogs` on the chains `management.walletBalanceChains()` lists. It returns `{ event, chain, contract, since, at, finality, logs: [{ wallet, block, timestamp, tx, logIndex, args }], observedAt }` (`WalletEvents`, `walletEventsSchema`) with decoded arguments, or null when the member has no linked EVM wallet, and follows `ctx.wallet.balance()`'s capture, `author-error` and `provider-unavailable` rules. `where` holds equality or `{ gte }` conditions on other arguments (`WalletEventCondition`); `afterActivation` skips events before the quest's activation. `since` may be at most 30 days before the evaluation (`maxWalletEventLookback`), and a read over more than 3,000,000 blocks or 1,000 logs (`maxWalletEventLogs`) throws `author-error`. `WalletReader.events` is optional in the type so that existing test doubles compile; Relay supplies it to every evaluation.

  The event comes from the new `settings.contractEvent({ label, default })` setting: a CAIP-10 contract, a space and the event's Solidity signature with argument names and `indexed` markers, such as `eip155:8453:0x… Transfer(address indexed from, address indexed to, uint256 value)` (`ContractEvent`, `isContractEvent`, `parseContractEvent`, `ContractEventParts`), stored as a text setting with `picker: "contract-event"`. An event without an indexed address argument is refused. Publication, Console settings edits and unarchiving also check that the chain reports a finalized or safe block and that the address holds a contract.

  The testing kit answers `ctx.wallet.events()` from `facts.wallet.events`. A new `onchain-action` snippet rewards each new matching event since the later of the quest's activation and the wallet's link, with an optional minimum amount, keyed on `chain:tx:logIndex`.

* 956b721: NFT holding checks on EVM chains and Solana. `ctx.wallet.nfts({ collection, address? })` counts the NFTs of one collection the member's linked wallets of its family hold: on EVM, an ERC-721 contract's `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 whose Metaplex metadata names the collection with `verified: true` (SPL Token and Token-2022 accounts holding exactly 1 of a 0-decimal, supply-1 mint; compressed NFTs and Metaplex Core assets are not counted). It returns `{ collection, chain, standard, total, wallets: [{ address, count }], at, observedAt }` (`WalletNfts`, `walletNftsSchema`; `standard` is `"erc721"`, `"erc1155"` or `"metaplex"`), or null when the member has no linked wallet of that family, and follows `ctx.wallet.balance()`'s capture, `author-error` and `provider-unavailable` rules. `WalletReader.nfts` is optional in the type so that existing test doubles compile; Relay supplies it to every evaluation.

  The collection comes from the new `settings.nft({ label, default })` setting, a CAIP-19 asset type such as `eip155:1/erc721:0x…`, `eip155:8453/erc1155:0x…/1` or `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:<collection mint>` (`NftCollection`, `isNftCollection`, `parseNftCollection`, `NftCollectionParts`), stored as a text setting with `picker: "nft"`. Publication and Console settings edits check it: an EVM chain and that the contract reports the standard through ERC-165 `supportsInterface`, so a token contract or the wrong standard is refused when it is set; a Solana mint must be a Metaplex collection NFT, so a fungible mint, a mint without Metaplex metadata, a print edition or a member NFT is refused. Unarchiving checks it too, and a platform without its dRPC key refuses every EVM collection. `ctx.wallet.balance()` pointed at an ERC-721 or ERC-1155 contract now throws `author-error` naming `ctx.wallet.nfts()` instead of reading a token count; it asks ERC-165 alongside `decimals()` on every token read.

  The testing kit answers `ctx.wallet.nfts()` from `facts.wallet.nfts`. A new `nft-holder` snippet accepts members whose linked wallets hold at least one NFT from the configured collection, on EVM or Solana.

* 1770ada: A seventh starter, `loyalty`: members join a campaign, earn points from its steps, join a members' group through a quest's group membership reward, and spend points on offers, one of them limited to the group with `inGroup`. Console offers it and `domino create --template loyalty` creates it.

  Starters are now an open list, so adding one no longer changes the SDK's types. `starters` lists every starter as a `Starter` with a plain string `id`, `offeredStarterSchema` checks an ID against it, and `projectCreateInputSchema` and `repositoryCreateSchema` accept any offered starter; `management.createProject` uses the first. `starterTemplates`, `StarterTemplate`, `StarterId`, `starterIdSchema`, `projectInputSchema` and `createRepositorySchema` are deprecated: they keep the first six starters and are removed in 2.0.0. To upgrade, replace `starterTemplates` with `starters` and `starterIdSchema` with `offeredStarterSchema`, and type a starter ID as `string`.

* cec850f: Create and change a tracker of every managed source without a deployment, not only X search. `management.trackers.create` and `management.trackers.update` (`POST /trackers`, `POST /trackers/:tracker/update`) take a `source`: `{ kind: "x-search", query }`, `{ kind: "discord-messages", text? }`, `{ kind: "discord-reactions" }`, `{ kind: "telegram-messages" }`, `{ kind: "telegram-reactions" }`, `{ kind: "youtube-shorts" }` or `{ kind: "tiktok-videos" }`. Each is stored as the source its builder makes, such as `telegram.reactions()`, so quests read a data tracker as they read the same tracker in code. `query` alone still creates an X search tracker; create takes exactly one of `query` or `source`. An update's `source` replaces the source's settings but never its kind, which is refused with 422 `validation-failed`. Turning Discord message text off erases the text already stored. `managedTrackerCreateSchema` validates the new input, while `trackerCreateSchema` keeps its X-only shape. `managedTrackerSource(source)` reads a published source by these names, and `managedTrackerSourceKinds` lists them.

  `domino trackers create KEY --source <kind>` creates one from the CLI, with `--text on` for Discord messages; `domino trackers update TRACKER --text on|off` turns message text on or off. Console **Trackers → New tracker** offers every source.

* 94726d8: Organization automation keys (`dak_`): a credential for CI and headless coding agents that no person owns. An organization administrator creates one in Console Settings → API keys for every project or a chosen list, with chosen permissions and an optional permission to create projects. It works as `RELAY_MANAGEMENT_TOKEN`, with `domino login --token-stdin` and the Git credential helper, in the test environment only: live writes, administration and minting other credentials are refused.

  The management client adds `automationKeys` (`list`, `create`, `addSecret`, `revokeSecret`, `revoke`) with `automationKeyInputSchema`, `automationKeySchema`, `issuedAutomationKeySchema` and `automationKeyPermissionSchema`. `accessSchema` gains an optional `credential` that `/access` and `domino whoami --json` fill for an automation key with its kind and scope.

* 6d9f0de: `telegram.messages()` is a tracker source of the messages members post in the project's connected Telegram groups and supergroups, one record per message with the poster's Telegram account as `author`. `data` is `TelegramMessageData`: `chatId`, `chatType`, `messageId`, `replyToMessageId` and `threadId`, never the text. Bots', service and channel messages are not recorded. In SDK 1.x it is a custom source on the reserved namespace `telegram-messages`, which Relay alone writes. The new `telegram-contribution` snippet rewards posting once per member per day.

* 8d11f64: `wallet.balances({ chain, token })` is a tracker source of daily balance samples that Relay writes itself. Once per project-calendar day, at a time members can't predict, Relay reads one token's balance in every wallet linked by a member who joined the project, every wallet of a run at one pinned EVM block or Cosmos height (on Solana, every read must reach the run's pinned slot). `chain` and `token` are those of `ctx.wallet.balance()`. Each record is one wallet, keyed by its CAIP-10 account on the sampled chain, which is also its `author`; each sample is a new observation whose `occurredAt` is the sample time and whose `data` is `WalletBalanceSample` (`amount` in base units, `decimals`, `block`). A provider outage retries the same block later that day and never writes a zero, so a day with no successful sample breaks a `streak({ days })`. Reward holding for days with `automatic({ tracker, where: { path: "amount", gte: { setting: "threshold" } } })` and `streak({ days })`. In SDK 1.x it is a custom source on the reserved namespace `wallet-balances`, with the chain and token in the new optional `balance` field of custom sources (`walletBalanceSourceSchema`); ingestion refuses the namespace, and a project and environment can have at most 5 such trackers (`limit-reached`).

  `management.trackers.create` and Console **New tracker** take `{ kind: "wallet-balances", chain, token }` (`managedTrackerSourceSchema`), and `domino trackers create --source wallet-balances` takes `--chain` and `--token`. The new `token-hold-streak` snippet rewards holding a token for 7 days in a row.

### 1.56.0 [#1560]

#### Minor Changes [#minor-changes-10]

* 9ec9f2f: Publishing creates the groups code names. A deployment whose quest `groupMembership(group)` reward or `inGroup(group)` prerequisite, offer `inGroup(group)` requirement, leaderboard `group` or referral `inviterGroup` names a group the environment doesn't have now creates it, with no members and the `defineGroup` handle's title (the key when only a leaderboard or referral program names it), instead of refusing with 422 `validation-failed`. Approving a promotion creates the groups live lacks the same way. The deployment preview and result, and the prepared promotion, list each such group as a `group-created` warning, and the audit history records `group-created` with the deployment or promotion as its reason. An existing group keeps its title and members, and publishing never unarchives one: a `groupMembership` reward naming an archived group still refuses. A project whose code uses groups no longer needs `domino groups create` before its first deployment or promotion.
* fbe8e64: Offers take prerequisites. `defineOffer({ ..., requires })` accepts the prerequisites quests do, `completed(quest)`, `balanceAtLeast(ledger, minimum)` and `inGroup(group)`, at most 30; `offerDefinitionSchema` and `offerAuthoringSchema` gain an optional `requires`. Publication refuses a required quest or ledger the deployment doesn't know. Redeeming as a member who misses one fails with 409 `prerequisites-incomplete` and `missing`, the shape quest admission uses, before any points or stock are spent. `Offer` gains `requires` and `missing`: for a signed-in member, `relay.offers.list()` and `useOffers()` list the prerequisites they have not met, empty for a read without a member or an offer without `requires`. `availability` keeps its four kinds. Console Rewards shows an offer's requirements on its card, and a promotion carries them to live.
* cba70f4: Add the organization's billing read. `management.billing.organization()` and `domino billing organization` (`GET /management/v1/organization/billing`) are open to any member of the organization and need no project. They list the projects the caller may read, each with its plan, `live` and `liveState` as `management.usage.plan()` reports them, and its Stripe subscription. They also return the organization's Free projects against their limit (`freeProjects`), the free AI allowance its Free projects share (`freeAi`), the monthly AI allowance across the organization, the trial, whether plan limits are enforced, and whether the caller may open the customer portal (`billing`). `management.billing.organizationPortal()` (`POST /management/v1/organization/billing/portal`) opens the customer portal as `management.billing.portal()` does, returning to Console's organization billing page. `organizationBillingSchema` and the `OrganizationBilling` type are exported from `@domino-sdk/relay/server`, with a reader in `managementResponses`.
* 6458ebe: Add the project's plan read. `management.usage.plan()` and `domino usage plan` (`GET /management/v1/usage/plan`, `read` permission) return the project's plan and where it came from (`source`), whether it may serve live and why (`live`), live's notice or offline state (`liveState`), the organization's one live trial (`trial`), whether the plan includes a custom domain, the AI builder balance by pool with when it renews (`ai`), the Stripe subscription as `management.billing.read()` reports it, and whether plan limits are enforced. Unlike `management.billing`, it is open to anyone who can read the project. `projectPlanSchema` and the `ProjectPlan` type are exported from `@domino-sdk/relay/server`, with a reader in `managementResponses`.
* 62c3791: Add TikTok creator accounts and the `tiktok.videos()` tracker source. A signed-in member links their TikTok account through TikTok's consent screen with `relay.auth.creatorAccounts.connect("tiktok")` (`useCreatorAccounts()` in React); the account's subject is its `open_id`, its profile a display name and avatar, and `knownCreatorAccountProviders` now lists `"tiktok"`. Operators read and unlink it like a YouTube channel. `tiktok.videos()` from `@domino-sdk/relay/authoring` reads the public videos on the accounts members linked, only with each member's own token, every `refreshSeconds` of the tracker, as content with a `TikTokVideoBody` (`title`, `description`, `durationSeconds`, `width`, `height`, `createdAt`, `shareUrl`, `isAigc`) and the metrics `views`, `likes`, `comments` and `shares`, so `content()` quests, `performance` and engagement rules use it. A known video the account no longer returns is a content removal; an account whose token needs reconnecting is not read, as `provider-unavailable`. It is a custom source on the reserved namespace `tiktok-videos`, which ingestion refuses. Both are inert until Domino configures TikTok. The `tiktok-videos` capability and the participant and authoring references describe them.
* a533f93: Add creator accounts and the `youtube.shorts()` tracker source. A signed-in member links the YouTube channel they publish from as a creator account: `relay.auth.creatorAccounts.list()` (`GET /v1/auth/creator-accounts`), `connect(provider)` and `disconnect(provider)`, and `useCreatorAccounts()` in React. A creator account is never a sign-in method, linking another channel replaces the current one, and the account's subject is the channel ID picked on Google's consent screen. Each entry mirrors `AccountConnection` with the channel's `profile`; `provider` is an open string, with `knownCreatorAccountProviders` in `@domino-sdk/relay/schema` listing `"youtube"`, so `CreatorAccount`, `CreatorAccounts` and their schemas sit beside SDK 1.x's closed provider lists. Operators read them on `management.members.get(member).creatorAccounts` and `management.members.creatorAccounts(member)`, and `management.members.unlinkCreatorAccount(member, { provider, subject, revision, reason })` unlinks one with `review` permission; `managedCreatorAccountSchema`, `managedCreatorAccountsSchema` and `memberCreatorAccountUnlinkInputSchema` are exported from `@domino-sdk/relay/server`. `youtube.shorts()` from `@domino-sdk/relay/authoring` reads the Shorts on the channels members linked, every `refreshSeconds` of the tracker, as content with a `YouTubeShortBody` (`title`, `description`, `tags`, `durationSeconds`, `publishedAt`) and the metrics `views`, `likes` and `comments`, so `content()` quests, `performance` and engagement rules use it. It is a custom source on the reserved namespace `youtube-shorts`, which ingestion refuses. Both are inert until Domino configures YouTube. The `creator-accounts` and `youtube-shorts` capabilities and the participant and authoring references describe them.

### 1.55.0 [#1550]

#### Minor Changes [#minor-changes-11]

* 5c70548: Add monthly active member usage. `management.usage.activeMembers()` and `domino usage active-members` (`GET /management/v1/usage/active-members`, `read` permission) return the project's live active members this UTC month (`month`, `activeMembers`), its plan and the plan's `limit` (null when the plan has none), and the six earlier months in `history`, newest first. An active member was active in the month on a UTC day after the day they joined the project: a signed-in participant API request, which includes their attempts and redemptions. Imports, server-reported tracker records and test members never count. `activeMemberUsageSchema` and `activeMemberMonthSchema` are exported from `@domino-sdk/relay/server`, with a reader in `managementResponses`.

* 65f3a9a: Add four metrics to the analytics catalog. `groups.joins` and `groups.leaves` count members added to and removed from a group, by when the change happened, without repeats that left membership as it was or memberships recorded without a date; their `group` breakdown and filter mean the change's group. `trackers.records` counts tracker trigger outcomes by when the record was observed, as each outcome is now, with an `outcome` (`admitted`, `skipped`) or `tracker` breakdown and a `tracker` filter that keeps the tracker's own outcomes. `leaderboards.settledParticipants` counts the rows of pending and approved leaderboard settlements by each settlement's cutoff, with a `leaderboard` breakdown and filter, and draws as `number`, `bar` or `table`. `analyticsBreakdowns` gains `group`, `tracker`, `outcome` and `leaderboard`, and `analyticsFilters` gains `leaderboard`. A dashboard panel naming a leaderboard the environment doesn't have fails the deployment with `unknown-reference`.

* 8f12977: Add the closed metric catalog. `management.analytics.catalog()` and `domino analytics metrics` (`GET /management/v1/analytics/catalog`) list every metric with its definition, kind (`count`, `distinct` or `rate`), the breakdowns and filters it accepts, its allowed charts and whether it needs a point ledger; `analyticsCatalog` and the `AnalyticsMetricId`, `AnalyticsBreakdown`, `AnalyticsFilter` and `AnalyticsChart` types are exported from `@domino-sdk/relay/server`. `management.analytics.query({ metric, range, breakdown?, filter?, ledger?, limit? })` and `domino analytics query --metric ID [--range] [--breakdown] [--filter name=a,b]... [--ledger] [--limit]` (`GET /management/v1/analytics/query`, `read` permission) read one metric bucketed like `analytics.read`, and with no breakdown or filter return the same numbers. A breakdown by quest, offer, referral program, rejection source or rejection reason has a row per matching resource (archived ones only with activity), sorted by total and capped by `limit` (1 to 100, default 100) with the rest summed into `other`. Filters narrow to quests, offers or programs by key, the quests a tracker triggers, or members now in a group. The new rates `quests.rejectionRate` and `referrals.qualificationRate` are `null` below 10 in the denominator. A query the catalog doesn't accept is a 422 with `unknown-metric`, `breakdown-not-accepted`, `filter-not-accepted`, `ledger-required`, `ledger-not-accepted`, `invalid-limit` or `unknown-reference`, and a missing group is `group-not-found`. `analyticsCatalogSchema`, `analyticsMetricQuerySchema` and `analyticsMetricResultSchema` are exported, with readers in `managementResponses`.

* c91ba35: A repeatable quest can space out a member's completions. Set `completionCooldownMs` (1,000 to 604,800,000) on a quest members submit that completes more than once, or **Completion cooldown** on a quest made in Console. After a completion of any key is accepted, a new attempt is refused with 409 `completion-cooldown` (`RelayErrorCode.CompletionCooldown`) until `retryAt`; when the retry cooldown also runs, the later one is named. A quest that completes once refuses the field. Quest progress and `QuestView` carry `nextAvailableAt`, when the member may next submit: the latest of the retry cooldown's end, the completion cooldown's end and an exhausted budget's next reset, omitted when nothing will reopen the quest. `questStatus` reports `completion-cooldown`, `canSubmit` is false until then, and `useQuest` renders again when it ends. Catalog quest instances take an optional `completionCooldownMs` (`null` for none). A quest without the field keeps today's behavior.

* 6b946d8: Add dashboards: a `relay.json` definition kind built from the metric catalog. Default-export `defineDashboard({ key, title, description?, defaultRange?, position?, sections })` from `@domino-sdk/relay/authoring`, typed so each panel accepts only the breakdowns, filters and charts its metric allows, and list it under `dashboards: [{ entry, exportName? }]` in relay.json. A deployment refuses a dashboard with a coded 422 (`unknown-metric`, `breakdown-not-accepted`, `breakdown-required`, `filter-not-accepted`, `chart-not-accepted`, `ledger-required`, `ledger-not-accepted`, `unknown-reference`, `referral-program-required`, `duplicate-key`, `too-many-panels` or `too-many-dashboards`), publishes a new version only when the definition changed, and reports each one in the preview's `dashboards` as `created`, `updated` or `unchanged`. `management.dashboards.list({ includeArchived? })` and `domino dashboards list` (`GET /management/v1/dashboards`) list them; `management.dashboards.read({ key, range? })` and `domino dashboards get KEY [--range]` (`GET /management/v1/dashboards/KEY`) return the definition and every panel's `analytics.query` result, with a failing panel's own `error` (`group-not-found`, `panel-timeout`) beside the others. `management.dashboards.archive|unarchive(key, { reason })` and `domino dashboards archive|unarchive KEY --reason` (`POST /management/v1/dashboards/KEY/archive|unarchive`) archive and restore one in test, returning `{ dashboard, changed }`, and promotions carry dashboards to live by key as changes of kind `dashboard`. The deployment preview's `dashboards` is optional, so values built for older SDKs still fit. `dashboardDefinitionSchema`, `dashboardAuthoringSchema`, `normalizeDashboard`, `dashboardIssue` and `dashboardPreviewSchema` are exported from `@domino-sdk/relay/schema`, and `dashboardListSchema`, `dashboardReadSchema` and `dashboardLifecycleResultSchema` from `@domino-sdk/relay/server`, with readers in `managementResponses`.

* 3a8a888: Engagement points land in the leaderboard window they were measured in. Each engagement settlement's point entries now take effect when the content sample that measured their growth was observed, instead of when the daily settlement paid them, and budgets charge each entry to the period of its `effectiveAt`. When the growth between two samples spans a leaderboard window or budget period boundary, Relay splits it there in proportion to time (rounded down, the remainder to the later part), one entry per side. Before, every earned board over engagement points ran about a day late.

  A settling leaderboard whose score counts an engagement rule's points now takes a closing measurement before it freezes. While it closes, its settlement reads as `pending` with no rows in the review reads (`settlementReviews`, `settlementReview`), with a new optional `closing` field holding `startedAt` and `deadline`: Relay asks scheduled trackers to resample the content, a sample observed within 30 minutes after the cutoff counts as measured at it, and the settlement freezes when everything is measured or the wait ends. Settlements gain `unmeasured`: content frozen at its last good sample, with `content`, `member` and `observedAt`. `management.leaderboards.retrySettlementMeasurement(id, { actionId? })` and `domino leaderboard-settlements retry-measurement SETTLEMENT` (`POST /management/v1/leaderboard-settlements/:id/retry-measurement`, `review` permission) close such a settlement again for another 30 minutes. Approving a closing settlement fails with `settlement-closing`; retrying one with nothing unmeasured fails with `settlement-measured`. The older `settlements` and `settlement` reads omit closing settlements.

* 5ae31db: Evaluators read the member's groups, campaign enrollment and referral progress. `ctx.member.groups()` returns the keys of the project's unarchived groups the member belongs to. `ctx.member.enrolled(campaign)` takes a campaign definition or key and returns whether the member's enrollment is active: `false` after withdrawal and without one. `ctx.member.referrals(program)` takes a referral program handle, key or ID and returns the member's progress as inviter, as `relay.profiles.referral()` returns it, never codes or accounts. Each is pinned once per evaluation run at its first call and counts against the run's capability calls; an unknown campaign or program fails with `author-error`. `ReferralHandle` names the handle `defineReferral` returns.

  The testing kit answers them from `facts.member.groups`, `facts.member.campaigns` (`{ key, enrolled }` per campaign) and `facts.member.referrals`. Like the other member-state calls, they are not listed in a run's `calls`.

* d463895: Evaluators read the member's point balances and quest completions. `ctx.member.balance(ledger)` takes a ledger handle or key and returns the member's balance in it, as `relay.me.balances()` lists it. `ctx.member.completions({ quest?, since?, limit? })` returns the member's accepted, unrevoked quest completions in the project, newest first, each with the quest key, completion key and `completedAt`; `limit` defaults to and is capped at 100. Both are pinned once per evaluation run at their first call, count against the run's capability calls, and fail with `author-error` for an unknown ledger or quest. Write rules such as "2 of these 5 quests" or "a balance of 100 or one of these quests" in `evaluate` with them.

  `EvaluationMember` is now `@compat sealed`: Relay and the testing kit create it, and a 1.x minor release can add members to it.

  The testing kit answers the readers from `facts.member.balances` and `facts.member.completions`, filtering and ordering completions as Relay does. Like Telegram calls, their calls take sequence numbers but are not listed in a run's `calls`.

* 7ff5984: Evaluator failures are coded. A quest bundle that throws while loading, an `evaluate` that throws anything without an escaped capability failure in it, and one that returns no decision now fail the attempt once with code `evaluator-error` and the thrown error's name and message (at most 1,000 characters), instead of retrying and ending with "Execution failed after retries". A retryable capability failure that escapes, rethrown or wrapped as a `cause`, is still retried.

  An evaluation run's ninth capability call throws `CapabilityError` with the new code `capability-limit`, never retryable, without reaching Relay; before, quest code saw an opaque failure. Like `integration-required`, it is a code `capabilityErrorCodes` does not list. `reservedAttemptCodes` now includes `evaluator-error` and `capability-limit`, so a reject code cannot use them.

  The testing kit reports both the same way: the ninth call throws the `capability-limit` `CapabilityError` instead of failing the run with a `TestSetupError`, and `runQuest` rejects with the new `EvaluatorError` (code `evaluator-error`, the same message, and what quest code threw as `cause`) for anything quest code throws other than a capability failure it received. A test that expected the thrown error itself reads it from `cause`. `maxCapabilityCalls` is exported from `@domino-sdk/relay/testing`.

* 6cc08fa: A member can greet themselves by first name. Each entry of `relay.profiles.mine().options` gains `givenName`: the first name Google sent at sign-in (OIDC `given_name`), null for X, Discord and accounts without one. Older servers' answers still parse, with `givenName: null`. Only the member's own profile carries it; profile reads by ID and leaderboard rows don't. Existing Google accounts get it on their next sign-in. Operator member reads are unchanged.

* 921e9d7: Add `dominoHosting({ command })` to `@domino-sdk/relay/vite`. It returns `cloudflare`, options for the app's own `cloudflare()` call from `@cloudflare/vite-plugin` (the Worker in Vite's `server` environment, and, only when serving, the variables `domino dev` sets forwarded to the local Worker, empty when unset), and `server`, Vite's server settings (port from `DOMINO_APP_PORT`, 5173 by default, and the `DOMINO_PREVIEW_ORIGIN` host with HTTPS hot reload when it is set). The SDK does not depend on the Cloudflare plugin.

  Add `domino build --hosting-artifacts`, which writes the quest and catalog artifacts the hosting builder reads to `.domino-build/quests.json` and `.domino-build/catalog.json` beside `relay.json`.

  The starters use both: their `vite.config.ts` is their own plugins plus `dominoHosting()`, and `scripts/build-domino.mjs` gives way to `vite build && domino build --hosting-artifacts`. Existing apps keep working unchanged.

* 6156dec: Automatic attempts that failed with `integration-required` are admitted again. When a project's Discord connection becomes ready, on first connection or after a repair from needs attention, Relay admits a new attempt in the background for each member of an active automatic quest without a tracker that needs Discord whose latest attempt failed that way. `isRetryableAutomaticOutcome` now also returns `true` for an outcome whose linked attempt is `failed` with code `integration-required`, so automatic catch-up (`management.quests.previewAutomaticCatchUp` and `catchUpAutomatic`) accepts these outcomes too. Its signature is unchanged. Console marks published quests that need Discord while it is missing, unavailable or needs attention.

* a801a1d: Leaderboard settlements can split a pool prize by score. `settle.pool` is `{ amount, unit, ledger?, minScore? }`: `amount` is the whole pool in base units as a decimal string, and `unit` is `"points"` or `{ code, decimals }` for a token or currency. A points pool names the `ledger` it pays into (a ledger handle or key in `defineLeaderboard`) and holds at most 2,147,483,647 points; other units take no ledger. With a pool, `settle.prizes` may be empty (`defineLeaderboard` lets you leave it out), and a board can have both. When the window freezes, Relay splits the pool over the eligible frozen rows in proportion to score, in exact integer math: members scoring 0 or less, or at or below `minScore`, get nothing, each pool share rounds down, and the leftover base units go one each to the largest remainders, earlier positions first, so the shares sum exactly to the pool. With no eligible score the whole pool stays unallocated.

  Settlement reads (`settlement`, `settlementReview`) gain `pool`, with `allocated` and `unallocated`, and each row gains `share` in base units; both are absent from settlements without a pool. Approval pays a points pool's shares as points in each member's prize completion, and revoking the completion reverses them. Relay pays no other unit: `management.leaderboards.settlementExport(id)` (`GET /management/v1/leaderboard-settlements/:id/export`, `read` permission) and `domino leaderboard-settlements export SETTLEMENT` (CSV, or `--json`) list each member's score, rank, share in base units and whole units, and verified linked wallets, as the operator's payout list. Console shows each winner's pool share before approval.

* cbdcdc0: Add the plan limits in test and setup, where plans apply to the organization. A sign-in that would add a real person to a test environment that already has its plan's number of real people fails with `limit-reached` (403, `limit: "real-people-in-test"`, `count`, `maximum`); people who already joined, test members and live sign-ins are unaffected. Creating a project, or transferring a Free project in, fails with `limit-reached` (403, `limit: "free-projects"`) when the organization already holds 10 Free projects. Adding a custom domain needs the Growth plan or above and otherwise fails with `plan-required` (403, `feature: "custom-domain"`). `management.usage.realPeopleInTest()` and `domino usage real-people-in-test` (`GET /management/v1/usage/real-people-in-test`, `read` permission) return the test environment's `realPeople`, the project's `plan`, its `limit` and whether it is `enforced`; `realPeopleInTestUsageSchema` is exported from `@domino-sdk/relay/server`, with a reader in `managementResponses`. The CLI's error JSON keeps a `limit-reached` or `plan-required` refusal's detail in `details`.

* 823a691: Member accounts that Relay reads in the background with the member's own OAuth tokens now report when those tokens stop working. In management member reads (`management.members.get`, `list`, `lookup`, and the `person` on attempts) such an account carries `status: "connected"`, or `status: "needs-reconnect"` with a `reason` of `revoked`, `expired` or `missing-scope`; accounts Relay keeps no tokens for carry neither field. `domino members get` and the other CLI results include the same optional fields. The participant client's `relay.auth.accounts()` and `useAccounts()` report the account's `status` as `"reconnect"` until the member links it again. Relay refreshes kept tokens before they expire. No provider reads accounts in the background yet.

* cc459ef: Add `questActionState(quest)` and `useQuestAction(view)` to `@domino-sdk/relay/react`. `questActionState` takes a `useQuest()` result and returns `open` (show the quest's input: the participant can submit now, or is starting a new submission after discarding a failed one on an active quest), `counted` (the submission just sent was accepted on a repeatable quest) and `automatic` (the server owns every run). `useQuestAction(view)` returns `useQuest(view)` with those three fields added.

  Add `signedInMember(request, env, { apiUrl })` to `@domino-sdk/relay/portal-proxy`. A hosted app's server route calls it to learn the signed-in member's ID, or null for a visitor or a session that is not a member's. It asks Domino's `/v1/auth/session` through the app's participant proxy with the request's cookies.

  The starters use `questActionState` instead of their own copies of these rules, and the classic starter's `server/participant.ts` gives way to `signedInMember`.

* a4c93bc: Add Stripe billing for a project. `management.billing.checkout({ plan, interval })` and `domino billing checkout --plan launch|growth|scale [--interval month|year]` (`POST /management/v1/billing/checkout`) return a Stripe Checkout URL that subscribes the project; `management.billing.portal()` and `domino billing portal` (`POST /management/v1/billing/portal`) return the organization's Stripe customer portal. Both need an organization administrator, not an API key (403 `permission-required`). While billing is staff-only, every organization but Domino's own is refused with 403 `billing-staff-only`. A project whose subscription holds its plan is refused another checkout with 409 `subscription-exists`, and the portal answers 409 `no-customer` before the organization's first checkout; a deployment without Stripe answers 503 `not-configured`. `management.billing.read()` and `domino billing show` (`GET /management/v1/billing`, `read` permission) return whether billing is open to the caller (`available`, `reason`) and, when it is, the project's `subscription`: `plan`, `interval`, Stripe's `status`, `currentPeriodEnd`, `cancelAtPeriodEnd`, `lastInvoice` and `livemode`. `billingCheckoutInputSchema`, `billingSessionSchema`, `billingSubscriptionSchema` and `projectBillingSchema` are exported from `@domino-sdk/relay/server`, with readers in `managementResponses`.

* b8348fd: Change a project's Stripe plan, cancel and resume. `management.billing.changePlan({ plan, interval })` and `domino billing change --plan launch|growth|scale [--interval month|year]` (`POST /management/v1/billing/plan`) move a paying project's subscription: a higher plan or annual billing applies now with the prorated difference invoiced at once, and a lower plan or monthly billing is held for the renewal. The answer is `billingPlanChangeSchema`: `timing` (`now`, `renewal` or `none`) and `invoiceUrl`, set when the immediate charge needs the customer and nothing changes until it is paid. `management.billing.cancel()` and `management.billing.resume()` (`domino billing cancel`, `domino billing resume`; `POST /management/v1/billing/cancel|resume`) set the subscription to end when its period does, or take that back, and return the project's billing. A project without a subscription that holds its plan is refused with 409 `no-subscription`, and a plan change while the subscription is set to cancel with 409 `subscription-canceling`. The subscription now carries `scheduledChange`, the plan, interval and date of a change held for the renewal, or null. The customer portal no longer changes or cancels plans; it keeps cards, invoices and billing details.

* c055c43: A project's Stripe subscription follows the project. Deleting a paying project sets its subscription to end with its period, and restoring it resumes the subscription; purging ends it at once. A transferred project keeps its plan until the old organization's period ends, and `management.billing.checkout()` (`domino billing checkout`) subscribes it in the receiving organization with no charge before then. The subscription now carries `endedFor`: `project-deleted`, `project-transferred` or null.

* f89ea5d: Buy AI builder credit, and see an overdue subscription. `management.billing.topup({ quantity })` and `domino billing topup [--quantity 1-10]` (`POST /management/v1/billing/topup`) return a Stripe Checkout URL buying `quantity` packs of $10 AI builder credit for the project, on any plan; the credit is added once the payment is in, once per purchase. `billingTopupInputSchema` and `billingTopupMaxQuantity` are exported from `@domino-sdk/relay/server`. The subscription now carries `unpaid` while an invoice is overdue: `since` (when it first failed), `promotionsRefusedAt` (14 days later, when the organization's new promotions are refused with `plan-required` and `reason: "unpaid"`) and `offlineAt` (45 days later, when the project's live goes offline until it is paid), or null.

* 8a5b7d3: Add `settings.amount({ label, default, decimals? })` and `settings.chain({ label, default })` to `@domino-sdk/relay/authoring`. An amount setting holds a non-negative base-unit integer string such as `"1000000000000000000"`; with `decimals`, Console shows and edits it in whole tokens. A tracker filter bound may name an amount setting (`gte: { setting: "minimum" }`) and compares it exactly, so 18-decimal thresholds fit. A chain setting holds `"solana"`, Solana mainnet's CAIP-2 ID or an EVM chain ID such as `"eip155:8453"`, typed as the `ctx.wallet.balance()` query's `chain`. Both are text settings (`picker: "amount"` or `"chain"`; text fields gain an optional `decimals`), and publication and Console edits refuse other values. `@domino-sdk/relay/schema` exports `isAmount`, `isBalanceChain`, `isBoundSetting`, `textSettingSchema` and the `AmountSetting`, `BoundSettings` and `BalanceChain` types. The `token-holder` example and snippet use both settings and need no `WalletBalanceQuery` cast; their minimum is now in base units.

* c656f61: A quest's `completion` can be a function of the policy builders: `completion: ({ keyed }) => keyed(({ tracker, settings }) => tracker.record.data.messageId)`. Its key callbacks, `keyed(...)`'s key and `progress(...)`'s `distinct`, read a `QuestKeyedContext` typed from the quest: `tracker` is non-null and typed for a tracker trigger, and `settings` has the quest's settings, so the `tracker!` cast is no longer needed. The builders it receives are `once`, `keyed`, `streak` and `progress`. `completion: keyed(...)` built inline keeps working and keeps its `KeyedContext`. Key callbacks now also receive the quest's parsed `settings` at runtime.

  `runQuest` reports `run.diagnostics` (and `createTestContext` its `diagnostics`): a `completion-key-ignored` entry when the evaluator returns a `completionKey` the completion policy ignores. Only `keyed(...)` uses it; `once()`, `streak(...)`, `progress(...)` and content quests key completions themselves. The decision is unchanged, and hosted attempts record nothing new. The Domino skill's authoring and tracker references describe the builder form and which policies use `completionKey`.

* 4aaea1a: X tracker refresh policy. Tracker status gains `pauseReason` (`operator`, `x-budget-exhausted` or `test-window-ended`; null when not paused), `resumesAt` (when a tracker paused on its X budget resumes on its own) and `effectiveRefreshSeconds` (the interval a scheduled tracker actually runs at: `refreshSeconds`, but no shorter than the project's plan allows, 6 hours below Enterprise). All three are optional, so older servers' answers still parse. Known X posts are re-read every 6 hours for 48 hours after they were posted, daily until day 7, then never. A deployment whose X search tracker asks for less than the plan's minimum warns `tracker-refresh-below-plan-minimum` and still publishes. A spent monthly X budget pauses X trackers with `x-budget-exhausted` (live only while plan limits are enforced, test always) until the budget renews, and resuming one meanwhile answers 409 `x-budget-exhausted`; a test X tracker pauses with `test-window-ended` after 14 days until resumed. `domino trackers create|update --refresh-seconds` help names the plan's minimum.

#### Patch Changes [#patch-changes-6]

* 5a8a0a0: CLI paper cuts. `domino stage --commit` accepts a short or uppercase SHA: a short one resolves to the full commit in the checkout, failing with Git's reason when it is unknown or ambiguous; a `--request-id` resume still takes the full SHA the build request printed. `domino api` strips one `/management/v1` the path already starts with, so `domino api POST /management/v1/staging/link` reaches `/staging/link` (a one-line note on stderr says so outside `--json`). A reader that closes stdout early, as in `domino --help | head`, ends the CLI with status 0 instead of an unhandled EPIPE. `domino test-members act --wait [--timeout SECONDS]` reads the attempt until it is no longer queued, evaluating or deferred and prints it with its status, reason and code; without `--wait` it prints the queued attempt as before. The deprecation notes of bare `domino status`, `quests` and `releases` name the typed read for each use, the help says that `--json` stdout carries exactly one JSON value while progress events go to stderr as NDJSON, and `domino build --help` recommends `--out FILE --json` for a summary. Both packages now ship their `CHANGELOG.md`.

### 1.54.0 [#1540]

#### Minor Changes [#minor-changes-12]

* ee9e5f1: Add `management.analytics.read({ range, ledger })` and `domino analytics get [--range 7d|30d|90d|all] [--ledger KEY]`, which read a project's activity in one environment from `GET /management/v1/analytics` with `read` permission. A range ends now and is bucketed by project-calendar day (`7d`, `30d`) or Monday week (`90d`, `all`, which starts on the day of the earliest dated record: the first membership, or an older imported completion). Each metric (sign-ups, active members, attempts, completions, rejections, attempts sent to staff review, one point ledger's points awarded, spent and net and members earning, fulfilled offer redemptions, accepted and qualified referrals) has a total, a value per bucket and the previous range's total. Referrals also report `acceptedQualified`, how many of the range's accepted referrals have qualified since, for a share that never exceeds 100%. Each quest also has its decisions, rejection rate, top 5 rejection reasons and rejections by source. Offers and referrals appear only when the project has them, and a test environment section only in test. `domino analytics get` prints a readable summary, and the read with `--json`; `analyticsReadSchema` and `analyticsQuerySchema` are exported from `@domino-sdk/relay/server`.

  `managementResponses` from `@domino-sdk/relay/server` now builds each reader on first use, as `managementResponseSchemas` does, so importing the entry retains about 3.5 MiB less heap. Each reader is built once and stays writable.

* 63f7a14: Custom domains. An organization admin can attach a domain they control, such as `quests.example.com`, to a project's live hosted app. Domino shows a CNAME to `cname.domino.page` and a `_domino.` TXT record that proves the domain, issues its certificate, and serves the live app there once both are in place. While the domain is active, page loads on `<project-slug>.domino.page` redirect to it.

  * `domino domains add|list|get|recheck|remove` and Console **Settings → Domains** attach, check and remove the domain, and show each DNS record against what public DNS returned and Cloudflare's statuses word for word.
  * `management.domains.list()`, `.get(id)`, `.add(hostname)`, `.recheck(id)` and `.remove(id)`.
  * Root domains, wildcards, IP addresses and Domino's own domains are refused with `invalid-hostname`. A project has one custom domain.
  * The `hosted-custom-domains` capability is now supported.

* 50246d7: `getHostEnvironment()` from `@domino-sdk/relay/browser` tells a hosted app where it is running: it resolves to `{ environment: "test" | "live", kind: "staging" | "preview" | "live" }` from the host's `/__domino/environment`, which Domino answers itself on staging (behind staging access), previews and the live app. It asks once per page load, and resolves to `null` where the host does not answer, such as a local dev server, a host outside Domino, or server rendering. Use it instead of hostname patterns, which are not a supported signal.

* f5bd728: Test members can accept a referral invitation: `management.testMembers.acceptReferral(member, { code })` or `{ inviter }` (another test member, whose invitation code is used), and `domino test-members accept-referral MEMBER --code CODE` or `--inviter MEMBER`. The acceptance runs as a signed-in member's would, with the same refusals, qualification and rewards, in the test environment only, and the audit names the operator.

### 1.53.1 [#1531]

No changes in this release.

### 1.53.0 [#1530]

*Not published to npm. Its changes first reached npm in 1.53.1.*

#### Minor Changes [#minor-changes-13]

* 1b789ce: Test members can hold real X and Discord accounts. `management.testMembers.linkAccount(member, { actionId, provider, id })`, or `{ actionId, provider: "x", username }` resolved through X, links a public account to a test member without OAuth, and `unlinkAccount(member, { actionId, provider, id })` unlinks and frees it (`domino test-members link-account` and `unlink-account`). Quest code's `x.profile()`, `follows()`, `reposted()` and Discord role reads then answer for the real account, tracker records it authors are attributed to the member, and `testMembers.record` posts as it instead of refusing with `conflict`. An account another member holds is refused with `account-linked-elsewhere`; when its owner signs in with it or connects it, it moves to them. Reset unlinks it. Relay never writes to a test-linked account: a Discord role reward for one creates no delivery and is never sent, and the attempt's `history` entry carries `rewardsNotSent: [{ reason }]`. Test member results include an optional `accounts` list; managed member identities and `members.oauthAccounts` mark these accounts with an optional `testLinked` flag.

### 1.52.1 [#1521]

No changes in this release.

### 1.52.0 [#1520]

#### Minor Changes [#minor-changes-14]

* 9fadb80: Evaluator rejections take an optional code. `decision.reject({ reason, code })` from `@domino-sdk/relay/authoring` names the cause with a stable key, such as `wrong-answers`, that stays the same when the reason's wording changes. Relay stores it as the attempt's `code`, returned by `management.attempts.get` and `management.attempts.list`, `domino attempts list|get` and Console's attempt detail beside the reason; responses to members carry `code: null` for an evaluator-rejected attempt. A code uses lowercase letters, digits and hyphens, at most 64 characters, and cannot be one of the codes Relay writes itself, now exported as `reservedAttemptCodes` from `@domino-sdk/relay/schema`; an invalid one fails the evaluation, and `runQuest`, with an error naming the rule. A content quest's `performance.evaluate` accepts the same `code`. Rejections without a code are unchanged.

### 1.51.0 [#1510]

#### Minor Changes [#minor-changes-15]

* 4dddeb0: `@domino-sdk/relay/server` exports `customDomainSchema`, `customDomainListSchema`, `customDomainRecordSchema`, `customDomainStateSchema` and `customDomainInputSchema`, the shapes of the management API's custom domain routes (`/management/v1/domains`).

### 1.50.0 [#1500]

#### Minor Changes [#minor-changes-16]

* 2ecde12: An operator can pass a quest for a test member without running its check, or accept a test member's rejected attempt anyway, to reach the quests behind a check that can't be satisfied in test.

  * New `management.testMembers.pass(member, { actionId, quest, completionKey? })` admits an attempt on a manual quest as a submission would, with no input, then accepts it as the operator. Prerequisites, availability, enrollment and the retry cooldown still refuse it, and the completion policy, budgets and rewards apply as for an accepted check: a progress quest counts one step, and a keyed quest without `completionKey` gets a fresh key. From the CLI, `domino test-members pass MEMBER QUEST [--completion-key KEY]`.
  * New `management.testMembers.acceptAnyway(member, { actionId, attempt })` accepts a rejected attempt as the operator, such as a tracker-triggered attempt its check rejected. The attempt keeps its rejection in its history, and when it is the member's latest attempt on the quest, its retry cooldown ends. Any other status is refused with 409 `attempt-not-rejected`. From the CLI, `domino test-members accept-anyway MEMBER ATTEMPT`.
  * Both answer the decided attempt and record a `test-member-decision` event with the operator as actor, listed in the `test-members` audit category. `testMembers.history` attempts an operator decided carry `operatorDecision: { kind, actor, at }`.

  Both need `publish` permission in the test environment, like the other test-member methods; live refuses them.

### 1.49.1 [#1491]

No changes in this release.

### 1.49.0 [#1490]

#### Minor Changes [#minor-changes-17]

* c262df2: `management.attempts.list` takes `quest` to list one quest's attempts, by its ID, alone or with `q` and `status` (`managedAttemptQuerySchema.quest`). `domino attempts list --quest <id>` does the same from the CLI.
* 41a3fe9: Hosted apps go live. Approving a promotion that carries a hosted app build deploys that build as the project's public live app at `https://<project-slug>.domino.page`, with live's own management key (`DOMINO_ENVIRONMENT` is `live`), app secrets, KV and SQL stores, and schedule signing key. The previous live build serves until the new one switches in; a failed deployment keeps it, and promoting the build again retries. Before the first such promotion the address answers 404 "Nothing is live here yet".

  * `domino secrets`, `domino storage` and `domino schedules` take `--environment live` to manage the live app's secrets, stores and jobs. Without the flag they use test, as before; they ignore `RELAY_ENVIRONMENT` and the environment in `relay.json`.
  * A CLI sign-in session can set and delete app secrets in live. Other live writes still need Console or an API key.
  * `management.liveApp.get()` returns the live app's address and state (`none`, `deploying`, `live` or `failed`, with the failure). Console shows it as the **Live app** card in **Deployments**.
  * The `hosted-live-app` capability is now supported.

### 1.48.0 [#1480]

*Not published to npm. Its changes first reached npm in 1.49.0.*

#### Minor Changes [#minor-changes-18]

* 3213a87: Add group membership quest rewards. `groupMembership(group)` from `@domino-sdk/relay/authoring` takes a `defineGroup` handle and adds the member to that group when the completion's rewards are delivered, recording the completion as the reason in the audit history and checking prerequisite-only automatic quests. Publishing refuses a reward for a group that does not exist or is archived. Revoking the completion removes the membership unless an operator's add or another unrevoked completion also backs it; `CompletionRevocation.groups` reports each group with `stillMember`. Archiving a group that a quest rewards fails with `group-in-use`, naming the quests. SDK 1.x keeps the existing `kind: "tier"` reward envelope with an optional `reward.group` key (`groupReward` and `groupRewardSchema` in `@domino-sdk/relay/schema`), and quest views add an optional `reward.group` with the group's key and title.

### 1.47.0 [#1470]

*Not published to npm. Its changes first reached npm in 1.49.0.*

#### Minor Changes [#minor-changes-19]

* 7db52a2: Reward X posts only for approved creators. X does not allow apps that reward users for posting, so a deployment that publishes a quest rewarding X posts without an `inGroup` prerequisite now returns the warning `x-post-rewards-ungated` in `warnings`, naming the quests by key. It applies to an X tracker trigger that pays the post's author and to a content quest on an X tracker, when the quest declares rewards or an engagement rule credits it; `beneficiary: "inReplyTo"` quests are not affected. The deployment still publishes, `domino deploy` prints the warning, and Console shows it on the deployment with a link to **Groups**. Add `requires: [inGroup(group)]` with a group of approved creators you manage in Console **Groups**, with `domino groups`, or with `management.groups`.

  The posting-streak and original-post SDK examples, the tracker guides and the CLI skill references now gate X post rewards on an `approved-creators` group.

### 1.46.0 [#1460]

#### Minor Changes [#minor-changes-20]

* fc0fc19: One deployment holds up to 1,000 quests and 200 quest types (was 50 each). `projectDeploymentSchema.releases`, `batchReleaseSchema.releases`, `questDraftChangesSchema` quests and collections, and `catalogDeploymentSchema` collections accept up to 1,000; the limits are exported as `questScaleLimits`. `domino build`, `deploy` and `dev` accept up to 1,000 quests and 200 quest types in one manifest. Other resource kinds keep their limits.

  SDK clients older than this release check drafts against 50 quests and 50 collections, so they cannot read a draft that holds more. Update the SDK before building drafts larger than that.

### 1.45.0 [#1450]

#### Minor Changes [#minor-changes-21]

* 8bb0a7b: Quest and quest type bundles import the SDK instead of carrying it. The SDK package ships its runtime (`dist/runtime`), and `buildQuest(entry, { runtime: true })` from `@domino-sdk/relay/build` leaves `@domino-sdk/relay/authoring`, `@domino-sdk/relay/schema` and, at the matching version, `zod` out of the bundle, returning the `runtime` the code imports. `domino build`, `deploy`, `dev` and hosted builds use it: releases and types record a `runtime` reference and the deployment carries each runtime once in `runtimes`. A quest bundle shrinks from about 170 KB to a few kilobytes. Apps on an SDK without a runtime keep building self-contained bundles. A Relay that predates runtimes refuses such a deployment, and the CLI says to update it. See [Shared SDK runtime](https://github.com/GetDomino/relay/blob/main/docs/public/concepts/quests.md#shared-sdk-runtime).

### 1.44.0 [#1440]

#### Minor Changes [#minor-changes-22]

* f114841: `management.audit.list` accepts `category: "test-members"`, which lists operators acting as test members in test: creating, submitting a quest as, advancing the clock of, giving and removing wallets for, sending synthetic records as, signing a staging app in and out as, and resetting a test member. Listing every category now includes these records too. The new `auditListQuerySchema` export validates this query and `AuditQuery` is its input type; `auditQuerySchema` keeps the three earlier categories and is deprecated in its favor. Console's **Audit history** shows these records under a new **Test members** filter.

#### Patch Changes [#patch-changes-7]

* f114841: `management.testMembers.reset` and `domino test-members reset` now unlink the accounts a test member connected in your app while a staging site acted as it: X, Discord, Telegram, Google and Apple accounts and wallets. Before, such an account made reset refuse with `test-member-reset-blocked`. Only the member's link is removed; the account itself is untouched and anyone can link it afterwards. Reset still refuses a member with an account that signs in, an app user ID, or an X account a member import attached. See [Reset and create another member](https://console.domino.run/docs/use/quests#6-reset-and-create-another-member).

### 1.43.0 [#1430]

#### Minor Changes [#minor-changes-23]

* 64e8df3: Test members can post on X, send Discord messages and add Discord reactions, so quests on managed X and Discord trackers can be tried in test without real accounts.

  * New `management.testMembers.record(member, { actionId, tracker, data?, as? })` writes one synthetic record into a managed X or Discord tracker. `data` uses the stored record's field paths (`content` and `likeCount` for X, `channelId` and `text` for messages, `emoji` for reactions), so it exercises quests' `where` filters; Relay sets IDs and real time. `as: "messageAuthor"` makes the member the reacted-to message's author. The answer is `{ tracker, record, account, stored, admissions }`. From the CLI, `domino test-members record MEMBER --tracker TRACKER [--data JSON] [--as author|messageAuthor]`.
  * The first record for X or Discord links the test member a synthetic account of that kind, which attribution, member `identities` and `x.connection()` read like a real link. Reset removes it with the member's synthetic records.
  * Tracker record reads mark these records `synthetic: true`. Collected and ingested records never carry it.
  * Console's **Try** tab sends posts, messages and reactions for X and Discord quests, starting from values that match the quest's filter, and the tracker's record list marks synthetic records.

  It needs `publish` permission in the test environment, like the other test-member methods; live refuses it.

### 1.42.0 [#1420]

#### Minor Changes [#minor-changes-24]

* b7156c7: Releases and quest types can reference a shared SDK runtime instead of bundling the SDK. `releaseSchema`, `publishedReleaseSchema`, quest type versions and catalog deployment types take an optional `runtime` reference (`{ digest, sdkVersion }`), and project deployments take `runtimes`, each runtime's modules sent once. The new `@domino-sdk/relay/schema` exports are `sdkRuntimeSchema`, `sdkRuntimeReferenceSchema`, `sdkRuntimeModulesSchema`, `sdkRuntimeModulePathSchema`, `sdkRuntimeLimits` and `sdkRuntimeDigest`. Existing self-contained bundles work unchanged.

### 1.41.0 [#1410]

*Not published to npm. Its changes first reached npm in 1.42.0.*

#### Minor Changes [#minor-changes-25]

* c8e46ee: `RelayProvider` reads the session and every query again when `window` receives a `domino:test-state-changed` event. Domino's staging test toolbar fires it after it changes test data under the page, such as moving a test member to the next day or sending a tracker record, so a staging app shows the new state without a reload. Apps that read Domino without `RelayProvider` can listen for the same event. See [trying quests on your staging site](https://console.domino.run/docs/use/quests#try-quests-on-your-staging-site).

### 1.40.0 [#1400]

*Not published to npm. Its changes first reached npm in 1.42.0.*

#### Minor Changes [#minor-changes-26]

* 68bf9eb: Promote test to live as one reviewed request ([ADR 0017](https://github.com/GetDomino/relay/blob/main/docs/adr/0017-live-changes-only-through-promotion.md)). A promotion makes live match test for every definition, code-defined and Console-authored, matched by authoring key, and carries the hosted app's active staged build. It lists environment bindings live lacks (integrations, webhooks, participant sign-in, app secrets) as warnings and never copies them.

  * `management.promotions.prepare()` (test), `history()`, `get(id)`, `approve(id)` and `reject(id, { reason })` (live, Console session), and `rollback(id)` (live): `POST /promotions`, `GET /promotions/history`, `GET /promotions/:promotion`, and `POST /promotions/:promotion/approve`, `/reject` and `/rollback`.
  * `domino promote --json` prepares one from test; `domino promotions list` and `domino promotions get` read them.
  * New error codes: `promotion-empty` (live already matches), `promotion-changed` (test or live changed since the promotion was prepared; approval changed nothing) and `promotion-decided`.

  `management.deployments.promote(id)` and `domino deploy --promote` keep their 1.x request and response shapes. Once the test deployment is published they now prepare a promotion of all of test, answered as a pending deployment record with no resources whose first warning, code `environment-promotion`, names the promotion. Approve it as a promotion: `deployments.get` and `deployments.publish` do not accept its ID.

  Live now refuses direct code deployments. `domino deploy --environment live`, with or without `--preview`, and `management.deployments.preview` with a client for live fail with 422 `promotion-required` before the bundle is checked, whatever the credential; the CLI reports it as `ENVIRONMENT_UNSUPPORTED` (exit 1) and no longer submits anything. Automation that deployed to live should deploy to test, then run `domino promote` (or `management.promotions.prepare()`) and have a person approve the promotion in Console. Live deployments submitted before this change can still be submitted, approved, rejected and rolled back. Request and response shapes are unchanged, so like the other live refusals this ships in a minor release.

### 1.39.1 [#1391]

#### Patch Changes [#patch-changes-8]

* 08f24a3: An operator who signed in to a project's staging app through Console and holds `publish` can now sign that app in as a test member, through the staging host's `/__domino/toolbar/*` endpoints. The test member gets an ordinary 8-hour participant session. Issuing and ending it is audited as the operator, and that audit survives reset. Reset now ends a test member's participant sessions instead of refusing. Live, members that aren't test members, password staging sessions, API keys, hosted keys and CLI sessions can't start one. A password staging session gets the new refusal code `operator-required`, which the CLI skill's error reference now lists.

### 1.39.0 [#1390]

#### Minor Changes [#minor-changes-27]

* a9b11a8: Participants read the point entries behind their score on a leaderboard. `relay.me.leaderboardHistory({ board, cursor? })` (`GET /v1/me/leaderboards/:board/history`) and the React hook `useLeaderboardHistory(board)` page through the signed-in member's own entries that the board counts: its ledger filtered by the board's window, quests and calculation, so every page together sums to the member's score. Entries have the same shape as `relay.me.pointHistory()`, newest recorded first, and the page's `period` is the window the first page counted. A member the board excludes, or one outside its group, gets no entries.

### 1.38.1 [#1381]

#### Patch Changes [#patch-changes-9]

* 9b02ade: `questConfiguration` from `@domino-sdk/relay/server` accepts any release-shaped value with `configuration`, `fields` and `values`, so callers no longer need the release's code to read its configuration.

### 1.38.0 [#1380]

#### Minor Changes [#minor-changes-28]

* 167a840: A quest can make a member wait before trying again after a rejection. Set `retryCooldownMs` (1,000 to 604,800,000) on a quest members submit, or **Retry cooldown** on a quest made in Console, where a new quiz starts at five minutes. After the evaluator or a reviewer rejects the member's latest attempt, a new attempt is refused with 409 `retry-cooldown` until `retryAt` (also `RelayError.retryAt`). Quest progress and `QuestView` carry `retryAt` while the cooldown runs; `questStatus` reports `retry-cooldown`, `canSubmit` is false until then, `isCoolingDown(view)` is new, and `useQuest` renders again when it ends. Catalog quest instances take an optional `retryCooldownMs` (`null` for none). A quest without the field keeps today's behavior: no cooldown.

### 1.37.1 [#1371]

No changes in this release.

### 1.37.0 [#1370]

*Not published to npm. Its changes first reached npm in 1.37.1.*

#### Minor Changes [#minor-changes-29]

* 701a0d8: Leaderboards can list every member. `defineLeaderboard({ includeUnscored: true })` adds the board's eligible members who have no qualifying points to its standings, `me`, `nearby` and `placement` reads, with a score of 0. They share one rank after every member who scored and are ordered by member ID; the board's group and exclusion group still apply. Settlements leave them out, so they win no prizes. Boards without the option rank as before.
* e1b3c70: A points reward can pay up to 2,147,483,647 points, the largest 32-bit signed integer, instead of 10,000. The new limit applies wherever `points()` and `pointsRewardSchema` are used: quest rewards, evaluator decisions, leaderboard prizes and referral rewards. Offer prices (`offerPriceSchema`) take the same maximum and still start at 1. Code that declares a larger amount needs this SDK version, and apps that read one need SDK 1.10.0 or later, whose response readers ignore these maximums.
* ff024c7: Scheduled jobs can report what a run did. A job route answers with `scheduleResult({ outcome, summary?, metrics?, diagnostics? })` from `@domino-sdk/relay/server`, and Relay records it with the run as `result`. An `outcome` of `partial` or `failed` records the run as failed even when the HTTP status is 2xx. Relay reads any `application/json` answer up to 16,384 bytes. An invalid or larger one leaves the status alone and sets the run's `error` to `Result ignored: …`. Each schedule keeps its latest 20 runs: `management.schedules.runs(name)` (`GET /schedules/:name/runs`, `domino schedules runs NAME`, Console **Run history**) returns them newest first, and needs `read` permission.
* ff024c7: Read a hosted app's SQL value in full where a rows page cuts it at 2,000 characters. `management.storage.cell(name, table, { row, column, cursor? })` (`GET /storage/sql/:name/cell?table=&row=&column=`, `domino storage cell DB TABLE ROW COLUMN [--all]`) returns up to 100,000 characters per page with the value's `type` and `length`, and a `nextCursor` for the rest; `--all` follows it. `row` is the row's position in storage order, the rows page's `offset` plus its index on the page. A BLOB answers with its size and no text. It needs `publish` permission, like the other storage reads. Console's row view has **Show full value**.

### 1.36.0 [#1360]

#### Minor Changes [#minor-changes-30]

* 5da9393: Participants read their own email address. `relay.me.get()` (`GET /v1/me`) and `useMe()` now return `emails`, the verified addresses that sign the member in (each email-code address, then the verified email of each Google or Apple account enabled for sign-in), and `email`, the first of them for "signed in as". A member who signs in only with a wallet, X, Discord, Telegram or an app login has none: `email` is null and `emails` is empty. Participant routes never return another member's email. The classic starter shows the address in its account menu and Settings page.

### 1.35.0 [#1350]

*Not published to npm. Its changes first reached npm in 1.36.0.*

#### Minor Changes [#minor-changes-31]

* f7ba8d6: Create and change X search trackers without a deployment. `management.trackers.create({ key, title, query, window, refreshSeconds?, maxPages? })` (`POST /trackers`, `domino trackers create`) creates one as data in test, with the same validation as `defineTracker`; its first collection searches from `window.start`. `management.trackers.update(tracker, changes)` (`POST /trackers/:tracker/update`, `domino trackers update`) changes any field but the key, and a new query or window never searches the past. Both need `publish` permission, and live refuses them with `promotion-required`: a data tracker reaches live through a promotion from test, which is not available yet. Console **Trackers** creates and edits them too.

  A key belongs to code or to data. Tracker status reports it as `origin` (`code` or `data`). A deployment that declares a data tracker's key is refused with 409 `tracker-owned-by-data`, and creating or updating a key a deployment publishes with 409 `tracker-owned-by-code`.

### 1.34.0 [#1340]

#### Minor Changes [#minor-changes-32]

* 81d0037: Definitions change in test and reach live only through a promotion ([ADR 0017](https://github.com/GetDomino/relay/blob/main/docs/adr/0017-live-changes-only-through-promotion.md)). In live, every definition write fails with 422 `promotion-required`, whatever the credential, Console sessions included. These 1.x management routes refuse live:

  * `POST /releases`, `/releases/batch`, `/releases/settings`, `/releases/lifecycle`, and `/quests/:quest/archive` and `/unarchive`
  * `POST /catalog/deploy`, `/catalog/quests`, `/catalog/drafts` and `/catalog/publish`
  * `POST /engagement/publish` and `/engagement/change`
  * `POST /point-ledgers`, `/point-ledgers/update`, `/point-ledgers/:ledger/update`, `/archive` and `/unarchive`
  * `POST /calendar` and `/profile-settings`
  * `POST /badges`, `/badges/:badge/update`, `/archive` and `/unarchive`
  * `POST /pickup-inventory` and `/leaderboards/:board/exclusion-group`
  * `POST /stock-items`, `/stock-items/:item/rename`, `/archive` and `/unarchive`
  * `POST /resources/lifecycle`, and `/archive` and `/unarchive` under `/trackers`, `/referral-programs`, `/leaderboards`, `/engagement/rules`, `/collections`, `/quest-types` and `/images`

  API keys used to get 403 `console-approval-required` on most of these. Pickup policy, exclusion-group bindings and stock item creation, renames and archive state accepted API keys in live. Operations keep working in live under the caller's permissions: pauses, points, groups, moderation, reviews, entitlements, redemptions and stock adjustments. Code deployments still reach live through deployment review in Console.

  This ships as a minor release as an accepted exception to the compatibility policy: no request or response shape changes, the refusal is one new error code, and no project had written a definition to live in production. The CLI reports the refusal as `ENVIRONMENT_UNSUPPORTED` with `apiCode: "promotion-required"`.

### 1.33.0 [#1330]

#### Minor Changes [#minor-changes-33]

* 3706733: Add member imports: bring members, opening point balances and quest progress from another platform into a project and environment. `domino import --file members.jsonl --source NAME --dry-run` uploads a JSONL file, checks every row and prints what applying would create, link, set and refuse; without `--dry-run` it applies the import in test. `domino imports list|get|rows|apply|cancel` and `management.memberImports.create|addRows|check|apply|cancel|get|rows|list` reach the same `/management/v1/member-imports` routes. A live import is applied only from Console. Imported opening balances are `adjust` entries that name their import (`PointsEntry.import`, and `imported` in a member's own point history); they emit no `points.awarded` and count on balance leaderboards only. Imported progress pays no rewards. Accounts an import vouches for carry `imported: true` on `ManagedWallet`, `ManagedOAuthAccount` and `ManagedMemberIdentity`.

### 1.32.0 [#1320]

#### Minor Changes [#minor-changes-34]

* d6ea67b: Service keys give a self-hosted app's backend a management credential that no person owns. An organization admin creates one in Console under **Settings → API keys** for one project and environment, with chosen permissions. It never expires unless an expiry is chosen, and it keeps every live fence a personal key has. `management.serviceKeys` lists, creates and revokes them from an admin's Console session. Rotation overlaps: `addSecret` issues a second secret while the first keeps working, and `revokeSecret` retires the old one. Each secret reports `lastUsedAt`.

  Identity integration keys follow the same policy. New keys never expire unless `expiresInDays` is given, and existing keys keep their expiry until an admin removes it with `identityIntegrations.removeSecretExpiry`. `identityIntegrations.addSecret` and `revokeSecret` rotate a key without signing members out. Integrations now list their `secrets`. `expiresAt` is deprecated: it reports the latest secret expiry, or 8640000000000000 when a secret never expires. `identityIntegrations.rotate` is deprecated too. It now replaces every secret with one that never expires, instead of extending the key by 90 days.

### 1.31.0 [#1310]

#### Minor Changes [#minor-changes-35]

* 07802d2: Reward deliveries, redemptions and deployments page by `cursor` and `nextCursor`, so long histories no longer stop silently.

  * `management.rewardDeliveries({ member, cursor, limit })` and `relay.me.rewardDeliveries({ cursor, limit })` answer `nextCursor` beside `deliveries`. A page holds up to 500 deliveries, 500 by default, so calls without a cursor read what they did before. `management.rewardDeliveries(member)` still works.
  * New `management.redemptions.history({ member, cursor, limit })` answers `{ items, nextCursor }`, 25 a page by default and at most 100, and replaces `management.offers.redemptions`, which answers every redemption at once. From the CLI, `domino redemptions history [--member] [--cursor] [--limit]` replaces `domino redemptions list`.
  * New `management.deployments.history({ cursor, limit })` answers `{ items, published, nextCursor }`, 25 a page by default and at most 100. `published` holds the two newest published deployments by revision on every page: the live one and the one `rollback` returns to. It replaces `management.deployments.list`, which stops at the 100 newest.

  The deprecated forms keep working through 1.x and are removed in 2.0.0.

#### Patch Changes [#patch-changes-10]

* caf698a: To prepare project slugs for hosts on `domino.page`, the server now refuses a slug change to a name Domino reserves (409 `Project slug is reserved`) or to a slug shorter than 3 characters (400), and a new project's derived slug gets a short suffix instead of taking either. Reserved names match exactly: `console` is reserved, `console-tools` is not. `projectSchema` still parses every existing slug; its `slug` field documents the rule, and the agent references and `domino capabilities` describe it.

### 1.30.1 [#1301]

No changes in this release.

### 1.30.0 [#1300]

#### Minor Changes [#minor-changes-36]

* cd6d78c: Add `management.x.lookup({ username })` and `domino x lookup HANDLE`, which resolve an X handle (with or without `@`) to the account that holds it, `{ id, username, name }` (read through a cache up to a day old), from `GET /management/v1/x/users/lookup`. Trackers, `--author` filters, custom `x` identities and `ctx.x.follows({ userId })` take the stable `id`, which no longer has to be copied from a post in Preview X search. The lookup needs `read` permission, fails with 404 `not-found` when no account holds the handle, and allows 30 lookups a minute per user and 300 an hour per organization. It resolves the handle only: it does not show who owns the account.

### 1.29.0 [#1290]

#### Minor Changes [#minor-changes-37]

* 272a255: Add `management.members.progress(member)` and `domino members progress MEMBER`, which read a member's progress on each active quest, 25 most recent attempts and point balances from `GET /management/v1/members/:member/progress`. They leave out the quest releases and evaluator code that `members.get` returns beside them. In a project with many quests, member detail runs to megabytes; every list in this read is bounded by the project's configuration, so it suits scheduled jobs and other reads under a deadline. `members.get` is unchanged.

### 1.28.0 [#1280]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Minor Changes [#minor-changes-38]

* 932e3c1: Add kind-keyed operator identity commands. `management.members.identityStates.list(member)` reads every identity a member proved (wallets, X, Discord and Telegram accounts, and `app:<name>` identities) with its link, revision, sign-in and allowed actions. `identityStates.unlink` and `relink` take an `identity` selector, an `expectedRevision` and a reason, and `identityStates.setSignIn` turns a wallet's sign-in on or off on its own. Each kind keeps its rules: wallet and account unlinks keep sign-in, app identity unlinks end that identity's sessions, and a Telegram account can now be relinked. The CLI adds `domino members identities`, `unlink-identity`, `relink-identity` and `identity-sign-in`.

  Add `management.discordRoles.preserve(delivery, { reason })`, the Discord role reward kind's preservation operation. A delivery of a reward kind without that operation is refused with 409.

  Deprecated, still supported through 1.x: `members.oauthAccounts`, `unlinkOAuthAccount`, `telegramAccount`, `unlinkTelegramAccount`, `unlinkWallet`, `setWalletSignIn`, `unlinkIdentity`, `relinkIdentity` and `identityLinks` (use `members.identityStates`), `management.preserveRewardRole` (use `management.discordRoles.preserve`), and the CLI's `members identity-links`, `members relink` and `members oauth-accounts`.

### 1.27.1 [#1271]

*Not published to npm. Its changes first reached npm in 1.29.0.*

No changes in this release.

### 1.27.0 [#1270]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Minor Changes [#minor-changes-39]

* 072493d: Leaderboard exclusions are a group (DOM-205):

  * A board leaves out the members of its exclusion group. Each board gets its own group, keyed `domino--exclusions--<board ID>`, on its first exclusion. Existing exclusions and their history move into these groups, so standings and settlements leave out the same members at every cutoff.
  * `management.leaderboards.exclusionGroup(board)` returns `{ leaderboard, group, generated, boundAt }`, and `management.leaderboards.bindExclusionGroup(board, { group, reason })` binds the board to another unarchived group, such as one several boards share, or back to its own. Settlements read the group bound at their cutoff.
  * `management.leaderboards.exclude` and `exclusions` keep their inputs, responses, permissions and audit events. They now change and read the board's exclusion group, which `management.groups` can change too.
  * Archiving a group that a leaderboard uses as its exclusion group fails with 409 `group-in-use`, naming the boards in `leaderboards`. Group keys starting with `domino--exclusions--` are reserved: creating one fails with 409 `already-exists`.
  * CLI: `domino leaderboards exclusion-group BOARD` and `domino leaderboards bind-exclusion-group BOARD GROUP --reason …`.

### 1.26.0 [#1260]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Minor Changes [#minor-changes-40]

* 91d7ad0: Relay keeps quest submissions (DOM-268). See [ADR 0014](https://github.com/GetDomino/relay/blob/main/docs/adr/0014-relay-keeps-quest-submissions.md).

  * Submission handles (`me.submission`) and quest controllers (`quests.open`) save each submission in Relay instead of the browser. A lost answer, a reload, another tab or another device signed in as the same member recovers it from Relay, and submitting again returns the same attempt. The public API is unchanged. Submissions no longer need IndexedDB or Web Locks, so `createRelayClient` without a `submissionStore` submits too. `SubmissionStore` and `browserSubmissionStore` are used only to finish a submission an older SDK version saved in the browser.
  * A member has at most one unfinished attempt per quest. Another claim, or a retry of a failed attempt, is refused with `attempt-pending` until the previous one is decided, including while it waits for human review. The same action sent twice still replays.
  * New participant routes: `GET`/`POST /v1/me/quests/:quest/submission`, `GET`/`POST /v1/me/submissions/:submission`, and `POST /v1/me/submissions/:submission/submit`, `/discard` and `/evidence` (a photo uploaded into the submission). A submit refused because the quest changed keeps the draft with `reviewRequired`, and the handle reports `needs-review`. `@domino-sdk/relay/schema` exports `questSubmissionSchema`, `questSubmissionUpdateSchema`, `questSubmissionResultSchema` and `QuestSubmissionStatus`. A draft expires 7 days after its last change. Refusal code `submission-closed` means the submission was already submitted, discarded or expired.
  * The management client has `quests.submissions(member)` and `quests.discardSubmission(member, submission)`. Console shows a member's open quest submissions and lets reviewers and staff discard one.

### 1.25.1 [#1251]

*Not published to npm. Its changes first reached npm in 1.29.0.*

No changes in this release.

### 1.25.0 [#1250]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Minor Changes [#minor-changes-41]

* 55d08ae: Client SDK paper cuts (DOM-160):

  * `createRelayClient` takes `getToken: async () => token | null`, read before every attempt, as the management client does. A failing `getToken` rejects with code `credential` before anything is sent. `token` and `getToken` together are refused.
  * `createRelayClient` and `createManagementClient` take `fetch`, which replaces the global `fetch` for every call, and `headers`, added to every call. The client's own headers win over one of the same name; an invalid header is refused with `invalid-input` when the client is created.
  * Submission handles (`me.submission`) and quest controllers (`quests.open`) take `{ signal?, timeout? }` on `submit`, `resume`, `status` and `discard`. Each request the call sends (session read, upload, claim, retry, and a controller's read of the latest quest after `quest-updated`) gets the signal and what is left of one deadline for the whole call. A cancelled or timed-out call keeps its saved progress for `resume()`. The quiz's `submit(answers, { release?, signal?, timeout? })` makes `release` optional.
  * `auth.start({ provider, mode })` and `auth.emailVerify({ challenge, code })` take an options object, which refuses unknown fields. The positional forms still work and are deprecated, to be removed in 2.0.0.
  * `paginate(read)` from `@domino-sdk/relay` iterates every item of a paged read with `for await`, following `nextCursor`.
  * `RelayErrorCode` lists `OperationRetired` (`operation-retired`) and `Credential` (`credential`).
  * `offers.redeem`, `profiles.badges`, `me.activity.list`, `me.upload`, `auth.telegram.miniApp.connect` and `auth.emailStart` reject invalid input with a `RelayError` of code `invalid-input`, instead of throwing a raw `ZodError` (`offers.redeem`) or throwing synchronously.
  * The browser bundle no longer includes every Zod locale: `createBrowserRelayClient` drops from about 412 KB to 198 KB minified (90 KB to 56 KB gzipped).

### 1.24.1 [#1241]

No changes in this release.

### 1.24.0 [#1240]

#### Minor Changes [#minor-changes-42]

* f6e030a: Webhook endpoint creation and secret rotation can be recovered after a lost answer. `management.webhooks.create` takes an optional `actionId` (new `webhookCreateInputSchema`), and `management.webhooks.rotateSecret(id, { actionId })` takes one too (new `webhookRotateInputSchema`). The client generates an ID when you omit it. A retry with the same `actionId` returns the same endpoint and signing secret instead of creating a second endpoint, or being refused during the 24-hour overlap. Once the secret is rotated again, the earlier action's retry is refused with `secret-superseded`. `domino webhooks create` and `domino webhooks rotate-secret` are now saved operations with `--operation-id`; resume one with `domino operations resume <id>`.

  Relay now refuses webhook subscriptions to event types it never sends, such as `completion.create`, with `unknown-activity-type`; the message lists the valid types. This narrows what the API accepts, but the CLI and SDK already report it as a refusal. An endpoint update can keep a type the endpoint subscribed to before. Event payload `type` stays an open string, so receivers must still accept or ignore types they do not know.

### 1.23.2 [#1232]

No changes in this release.

### 1.23.1 [#1231]

#### Patch Changes [#patch-changes-11]

* fd5f211: Members now belong to one project. A person who signs in to two projects of the same organization is a separate member in each, with their own identities, linked X, Discord and Telegram accounts, and wallets; unlinking, relinking and wallet sign-in control act within the project. Project transfer now moves a project with its participants instead of refusing it. No API shape changes: `MemberIdentityLink.scope` keeps the value `"organization-environment"` for OAuth links, which now names a link in the member's project and environment.

### 1.23.0 [#1230]

#### Minor Changes [#minor-changes-43]

* 88c7c00: Add Telegram chats, membership checks and reaction trackers. Operators connect Telegram groups and channels to a project through Domino's bot in Console (**Integrations → Telegram → Add chat**), or with `management.telegram.connect()`, `chats()` and `remove(chat)`. See [Telegram](https://console.domino.run/docs/build/telegram).

  * `ctx.telegram.member({ chat })` reads whether the member's linked Telegram account is in a connected chat: `{ userId, username, status, isMember, isAdmin, untilDate }`, or null. It needs no `integrations` entry. `ctx.telegram` is optional in `EvaluationContext`, so code that builds contexts keeps compiling. `settings.telegramChat()` adds a setting that Console fills from the connected chats. The testing kit takes `facts.telegram.member`.
  * `telegram.reactions()` is a tracker source of the reactions members add in connected groups. Each record carries Telegram accounts for the reactor (`author`) and the message's author (`messageAuthor`). `data` is `TelegramReactionData`, which includes `reactorStatus` and `reactorIsAdmin` for a trigger's `where`. In SDK 1.x it is a custom source on the reserved namespace `telegram-reactions`, which Relay alone writes.
  * Text settings can carry `picker`, which Console uses to offer a list.
  * The `telegram-membership` snippet is a join quest.

### 1.22.0 [#1220]

#### Minor Changes [#minor-changes-44]

* 81beec3: Personal API keys and `domino login` CLI sessions can now use test members, including giving them wallet addresses, not only Console sessions: the caller needs `publish` permission in the test environment, and hosted app keys stay refused. Add `domino test-members create|list|history|act|advance|link-wallet|unlink-wallet|reset`, which always address the test environment and save their writes as resumable operations, so a coding agent can simulate a campaign end to end.

### 1.21.0 [#1210]

#### Minor Changes [#minor-changes-45]

* 5db81c1: Test members can hold wallet addresses. `management.testMembers.linkWallet(member, { actionId, chain, address, network? })` gives a test member a Solana, EVM or Cosmos address without a signature, and `unlinkWallet(member, { actionId, chain, address })` removes and frees one. Quest code reads these addresses through `ctx.wallet` like a member's linked wallets, so balance-gated quests can be tried in the test environment. When the wallet's owner signs in with it or connects it, the address moves to them. Test member results include an optional `wallets` list. The CLI's capability map and Domino skill reference describe the new methods.

### 1.20.0 [#1200]

#### Minor Changes [#minor-changes-46]

* 2e1eff4: Add Telegram sign-in. Telegram Login signs participants in on the web where Domino configures it, and Mini App sign-in signs them in inside a project's own Telegram Mini App once the project names its bot ID in Console (**Settings → Sign-in**). Both reach one Telegram account. See [Telegram](https://console.domino.run/docs/build/telegram).

  Telegram is not an `AuthProvider` in SDK 1.x, so code that switches over providers keeps working: `AuthProvider`, `relay.auth.accounts()`, `useAccounts()` and `useSignInOptions().options` never include it. Telegram has its own additions beside them:

  * `relay.auth.providers()` returns `telegram: { login, miniApp }`.
  * `relay.auth.telegram` has `start(mode)`, `account()`, `disconnect()`, and `miniApp.signIn(initData)` and `miniApp.connect(initData)`. `telegramMiniAppInitData()` reads the Mini App's launch data.
  * `useSignInOptions()` and `signInOptions()` return the Telegram Login option as `telegram`, beside `options`. `useSignIn()` adds `withTelegram()` and `withTelegramMiniApp(initData)`, and `useTelegramAccount()` connects and disconnects the member's Telegram account.
  * In the management client, `participantSignIn()` returns `telegram: { login, miniAppBotId }`, and `configureParticipantSignIn()` takes `telegram: { miniAppBotId }`, which keeps the current bot when left out. `members.telegramAccount(member)` and `members.unlinkTelegramAccount(member, { subject, revision, reason })` read and unlink a member's Telegram account.

### 1.19.0 [#1190]

#### Minor Changes [#minor-changes-47]

* 37a57df: Deploying a quest that needs an unconnected integration, such as Discord, no longer fails. The deployment preview and result include `warnings` (`deploymentWarningSchema`) naming the quests with code `integration-required`, and `domino deploy` prints them. Their attempts still fail with `integration-required` until the integration is connected.

### 1.18.1 [#1181]

#### Patch Changes [#patch-changes-12]

* 0a6ebf1: Allow management deployment responses to include additional fields in direct, promotion and rollback sources. This closes the recorded response compatibility gap without changing known fields or source kinds.

### 1.18.0 [#1180]

#### Minor Changes [#minor-changes-48]

* 7000d41: Add project badge definitions, quest badge rewards, audited manual awards and revokes, participant and profile reads, and React hooks. Author badges with `defineBadge`, include them in `relay.json`, and attach them with `badge(...)`. SDK 1.x retains the existing reward envelope with optional badge metadata. The CLI adds `domino badges` catalog and member commands.

### 1.17.0 [#1170]

*Not published to npm. Its changes first reached npm in 1.18.0.*

#### Minor Changes [#minor-changes-49]

* d8a5731: Add ordered quest presentation steps with HTTPS link actions, images and alt text, and account or wallet hints. Collection declarations now accept description, image and alt text. Participant reads and React hooks return these optional fields in the published collection order. Console can edit and reset quest presentation and edit collection details. Presentation changes do not change verification.

### 1.16.0 [#1160]

#### Minor Changes [#minor-changes-50]

* b9d767f: Run background jobs on active hosted Worker builds in test. Declare `app.schedules` in relay.json with an interval or UTC cron, verify signed route calls with `verifyScheduledRequest(request, env.DOMINO_SCHEDULE_KEY)`, and inspect or control jobs with `management.schedules` or `domino schedules`.

  Jobs have a five-minute minimum cadence, ten schedules per app and a 30-second timeout. Relay makes one attempt per tick; uncertain completion blocks future runs until an operator confirms recovery. See [Run scheduled jobs](https://console.domino.run/docs/build/schedules) for custom tracker ingestion and recovery.

### 1.15.1 [#1151]

*Not published to npm. Its changes first reached npm in 1.16.0.*

No changes in this release.

### 1.15.0 [#1150]

No changes in this release.

### 1.14.0 [#1140]

#### Minor Changes [#minor-changes-51]

* 0c394de: Replay captured test-environment attempts locally. `management.attempts.capture(attempt)` (`publish` permission) returns what one evaluation run of an attempt read: the release's settings and rewards, the attempt's input, frozen clock, seed and calendar, each captured capability answer, and what quest code decided. `replayAttempt(quest, capture)` from `@domino-sdk/relay/testing` evaluates current quest code against it and reports the decision beside the recorded one, whether it changed (kind, reason, rewards or completion key), and the first call the capture could not answer. `domino quests replay <attempt>` fetches, saves (`--out`) and replays a capture. Captures are served from the test environment only: live attempts are refused with 422 `test-environment-required`, and an attempt that has not been evaluated with 409 `attempt-not-evaluated`.
* 580a641: Add release history to the management client. `management.releases.list({ quest?, limit?, cursor? })` (`GET /management/v1/releases/list`) pages every release the project's quests have had, newest first, optionally for one quest by ID or key, and `management.releases.get(release)` (`GET /management/v1/releases/:release`) reads one current or historical release. Both need `read` and `publish` permission. They replace the deprecated `page("releases")` and its `detail`.

#### Patch Changes [#patch-changes-13]

* cb252b6: The CLI now sends its management requests through the management client's own transport, so `domino` and `createManagementClient` share one implementation of deadlines, retries and failure classification. CLI exit codes, error codes and results stay the same; some error messages change wording, and network errors no longer carry `details.cause`. A management client write whose connection was refused, never resolved, could not reach the network or host, timed out while connecting, or failed TLS now fails with `uncertain: false`, because nothing was sent, and a 204 answer reads as `null`.
* 3ccb72a: `Person` records (`members.people()`, and the `people` of attempt, entitlement and page reads) now carry `test` and `name` for Console test members in the test environment, the same fields member reads already had. Console names test members by that name and marks them as test members instead of showing their member ID.

### 1.13.0 [#1130]

*Not published to npm. Its changes first reached npm in 1.14.0.*

#### Minor Changes [#minor-changes-52]

* c3601dd: `RelayProvider` now follows the signed-in member's activity feed instead of polling the whole quest experience every 1.5 seconds. Each event reads again only the hooks it changes: an accepted attempt reads the experience, outcomes, rewards, balances, leaderboards, profiles, referral progress and offers; a points award reads balances, leaderboards, offers and deliveries; and so on. The whole experience is read every 30 seconds for changes no event announces, and as the fallback when the feed is unavailable. An idle signed-in tab drops from about 124 requests a minute to about 14. Pass `polling={{ activityMs, snapshotMs }}` to change either interval. `useEntitlements` and `useDeliveries` now poll every 30 seconds instead of 5, since feed events refresh them.

  `quests.observe`, `me.observeProgress` and `me.activity.observe` make one request per poll: they send the session with each read, and read it again only when Relay refuses the session as changed. A shown tab resumes polling on schedule instead of reading at once. `me.activity.list` and `me.activity.observe` accept `start: "latest"` to follow only new events.

  New React query options: `enabled` and `null` keys for dependent queries, `staleMs`, `select`, `suspense`, and `initialData` for public reads (rendered on the server and while hydrating). `useSetQueryData()` changes a cached value at once, for optimistic updates.

  Mutation hooks share one shape: `mutate`, `mutateAsync`, `status`, `isPending`, `data`, `error`, `reset` and `settle`, built on `useRelayAction`'s retained keys. `useRelayMutation(mutation, { invalidates })` gives your own writes that shape, and `useRedeemOffer` and `useSignOut` now have it. `useRelayAction` adds `status`, `isPending`, `data` and `reset`, and its failed result carries `error`. `useSignIn` and `useSignInOptions` add `isPending` and `reset`; `useAccounts`, `useWallets` and `useMyProfile` add `isPending` and `resetAction`; `useQuest` adds `isPending`.

  Deprecated, still working: `busy` on every hook that now has `isPending`; `useRedeemOffer`'s `redeem` and `retry` (use `mutate`, which replays a retained redemption); `useSignOut`'s `signOut` (use `mutateAsync`, which rejects the same way, or `mutate`).

### 1.12.0 [#1120]

*Not published to npm. Its changes first reached npm in 1.14.0.*

#### Minor Changes [#minor-changes-53]

* 80f823a: Add `@domino-sdk/relay/testing`, a kit for unit-testing quests locally. `runQuest(quest, { input, tracker, settings, facts, now, seed })` evaluates a quest the way Relay's hosted runtime does: its published settings, rewards and review switch, a frozen clock, seeded randomness, fake answers for `ctx.member`, `ctx.wallet`, `ctx.x`, `ctx.discord`, `ctx.trackers`, `ctx.images` and `ctx.review` checked against Relay's capability contracts, and the reward bounds Relay enforces. `trackerObservation` and `trackerPage` build typed tracker input, `expectAccept`, `expectReject`, `expectReview`, `expectDefer` and `expectProgress` assert decisions, and `createTestContext` returns the host context `quest.evaluate` takes. `buildQuest` now refuses a quest bundle that imports the testing kit. Starters include quest unit tests in `tests/quests.test.ts`.

### 1.11.2 [#1112]

*Not published to npm. Its changes first reached npm in 1.14.0.*

#### Patch Changes [#patch-changes-14]

* 7160db6: Include points received before a leaderboard cutoff even when their transaction commits just afterward. New point receipts use database time, and freeze waits for in-flight point writers. Existing frozen settlements remain unchanged.

### 1.11.1 [#1111]

#### Patch Changes [#patch-changes-15]

* 90e5141: Build tolerant response readers on first use so SDK imports and backend startup retain less heap. Preserve concrete Zod schema APIs, shared readers, and unsupported-variant response envelopes.

### 1.11.0 [#1110]

#### Minor Changes [#minor-changes-54]

* 76e7a45: Add operator quest withdrawal, inventory rename/archive/unarchive/stock correction, webhook deletion with retained history, settlement rejection, and kind-keyed wallet/OAuth relink. Existing 1.x methods and response shapes remain supported. Inventory archive refuses retained offer and pickup references with named blockers; relink applies the current quest's historical policy while preserving admitted attempts.

### 1.10.1 [#1101]

#### Patch Changes [#patch-changes-16]

* fa55eff: Share converted response schema nodes to avoid excessive startup heap allocation in SDK 1.10.0. Tolerant response parsing and the SDK API are unchanged.

### 1.10.0 [#1100]

#### Minor Changes [#minor-changes-55]

* f2724f9: Read added response fields at every depth without relaxing request validation. Add explicit supported/unsupported response schemas and `client.response(...)` for extensible results. Unsupported entities retain their raw data; entity lists retain positions and report an unsupported count. Existing 1.x method return types stay compatible.

  SDK and CLI calls identify their package, version and runtime with `X-Domino-Client`. Tolerant SDK calls also identify their per-call response capability with `X-Domino-Response: tolerant-v1`. Older and legacy callers still require compatible server wire shapes. See the [participant response reference](https://console.domino.run/docs/reference/participant-api#read-evolving-responses) for the opt-in API.

### 1.9.0 [#190]

#### Minor Changes [#minor-changes-56]

* 4ae54f9: Add `relay.action()`, `createRelayAction()`, and `useRelayAction()` to retain a mutation key and its original callback across uncertain failures, with explicit replay-safe retries and acknowledgment of confirmed reconciliation and a synchronous guard against duplicate clicks. `useRedeemOffer` now reuses the original redemption key and exposes `retry`.

  All previously required participant and management mutation action IDs are optional; omitted IDs are generated once per call. Only reusing a key protects separate calls from double application. Evidence upload now generates an action ID; avatar upload accepts `actionId` alongside the existing `uploadId` alias and still requires reconciliation after uncertainty.

  See [retry after an uncertain answer](https://console.domino.run/docs/reference/participant-api#retry-after-an-uncertain-answer) for helper lifetime, retry behavior, and reload recovery. Existing explicit keys and upload aliases continue to work.

### 1.8.0 [#180]

*Not published to npm. Its changes first reached npm in 1.9.0.*

#### Minor Changes [#minor-changes-57]

* f19da0d: Add canonical archive, unarchive, pause and resume methods and CLI commands for management resources. Keep older lifecycle and feature-specific publication forms working as deprecated 1.x APIs.

### 1.7.0 [#170]

#### Minor Changes [#minor-changes-58]

* 5662b50: Expose protocol validation helpers through `@domino-sdk/relay/server` for server adapters: identity integration inputs and exchange, participant routes, quest configuration and shape validation, sequence cursors, image extraction, tracker data validation, and referral reward checks. Existing exports and wire formats are unchanged.

### 1.6.0 [#160]

#### Minor Changes [#minor-changes-59]

* 4651887: Add an optional `window` detail to `outside-window` availability, errors, and automatic outcomes so clients can distinguish tracker and quest windows without branching on reason text.

* 547600e: Every refusal from the participant and management APIs now carries a machine-readable `code` beside its unchanged message, so clients can branch without matching text. `RelayError.code` and `ManagementError.code` (and the CLI's `apiCode`) are set for every API answer: a specific code when the client can act on it, such as `catalog-changed`, `draft-changed`, `prerequisites-incomplete`, `permission-required` or `not-configured`, or the status's own code otherwise (`not-found`, `conflict`, `forbidden`, …). Structured detail, such as the `missing` prerequisites, comes with the code on every route. The [API error codes](https://console.domino.run/docs/reference/errors) reference lists them.

  * `RelayErrorCode` adds `PrerequisitesIncomplete`, `QuestClosed`, `QuestPaused`, `ProfileLocked`, `InvitationNotFound`, `InvalidCode`, `CodeExpired`, `ChallengeExpired` and `RateLimited`.
  * New `managementErrorSchema` (`@domino-sdk/relay/server`) parses the management API's error body, for raw `api.raw()` answers.

  Messages keep their text, and `code` stays an open string, so code written against 1.2 keeps working.

  The participant proxy now codes its own routing, configuration and authentication refusals. During application identity exchange it also keeps Relay's safe upstream code and `Retry-After` while continuing to replace non-account-conflict messages with its existing generic text.

* 6c80795: Groups are their own resource. A group is a named set of members that `inGroup` prerequisites, a leaderboard's `group` and a referral program's `inviterGroup` read by key; before, groups existed only as leaderboard group memberships. Existing memberships and their history move over unchanged.

  * `management.groups` lists, gets, creates, renames, archives and unarchives groups, and lists, adds and removes members with a reason for the audit history. Lists take `{ cursor, limit }` and return `{ items, nextCursor }`. The schemas and `ManagedGroup`, `GroupPage`, `GroupMember` and `GroupMemberPage` types are exported from `@domino-sdk/relay/schema` and `@domino-sdk/relay/server`.
  * `domino groups list|show|create|update|archive|unarchive|members|add|remove` does the same from the CLI.
  * Groups are archived, never deleted. An archived group's members still count, but cannot change until it is unarchived.
  * `management.leaderboards.group(group, { member, included, reason })` and `leaderboardGroupSchema` are deprecated and removed in 2.0.0. They keep working, and still create a group the first time they add a member to it. The group name must now be a group key (letters, digits, `_` and `-`, at most 100 characters), as definitions already required; a longer name answers 400. Replace them with `management.groups.create({ key, title })` once, then `management.groups.addMember(group, { member, reason })` and `management.groups.removeMember(group, member, { reason })`.

  `defineGroup` and `inGroup` are unchanged. See [groups](https://console.domino.run/docs/concepts/groups).

* 6088922: Every paged management read now takes `cursor` and answers `nextCursor`, as participant reads already did: pass a page's `nextCursor` back as `cursor` until it is `null`.

  * `management.points.history(member, ledger, { cursor })` and `management.referrals.participants({ cursor })` take an options object; their answers add `nextCursor`. Passing the numeric `before` (a page's `next`) still works and is deprecated.
  * `management.webhooks.deliveries(id, { cursor, limit })` and `domino webhooks deliveries <endpoint> --cursor <cursor>` page by the answer's `nextCursor`. `before`, `--before` and `next` are deprecated.
  * `management.storage.rows(name, table, { cursor })` and `domino storage rows NAME TABLE --cursor C` page by `nextCursor`; `offset` and `--offset` are deprecated. `management.storage.keys` answers `nextCursor`; its `cursor` field is deprecated.
  * New `management.trackers.triggerOutcomes(tracker, { ...filters, cursor })` answers `{ items, nextCursor }` and replaces `management.trackers.outcomes`, whose `before` and `next` stay as they were.
  * New `management.engagement.ruleVersions(rule, { cursor })` answers `{ items, nextCursor }` and replaces `management.engagement.versions({ rule, before })`.
  * `resourceOptionsSchema` (`@domino-sdk/relay/schema`) adds `nextCursor`; its `cursor` field is deprecated.

  The deprecated forms keep working through 1.x and are removed in 2.0.0. Responses gain only fields, so code written against 1.2 keeps compiling.

* 6fa9cd5: New `management.members.list({ q, needsReview, limit, cursor })` (`GET /management/v1/members/list`) and `domino members list [--q TEXT] [--needs-review] [--limit N] [--cursor C]` list a project's members, most recently joined first, with their accounts and attempt, completion and reward counts. The answer is `{ items, total, nextCursor }`: pass `nextCursor` back as `cursor` until it is `null`. `q` finds members by part of their ID, profile name, or an account's subject, name or email, as Console's member search does. Use it instead of `page("members", { q })`.

  `memberListSchema`, `memberPageSchema` and `MemberPage` (`@domino-sdk/relay/schema`), which no route used, are deprecated in favour of `managedMemberQuerySchema`, `managedMemberPageSchema` and `ManagedMemberPage` (`@domino-sdk/relay/server`), and are removed in 2.0.0.

* e3a050e: Management commands on an existing resource now name it in the path, and the client methods take it as their first argument:

  * `management.deployments.publish(id)`, `submit(id)`, `reject(id, { reason })`, `promote(id, { actionId? })` and `rollback(id, { actionId? })` call `/deployments/:deployment/<verb>`. The `{ id, ... }` forms still work and are deprecated. `domino deploy` uses the new routes.
  * `management.pointLedgers.update(ledger, { name, description })` calls `/point-ledgers/:ledger/update` and replaces the deprecated `pointLedgers.save({ id, name, description })`.
  * `management.referrals.suspend(member, { suspended, reason })`, `qualify(program, member, { qualified, reason })` and `resolveReward(entitlement, { reason })` replace the deprecated `referrals.control({ kind, ... })`. Each generates its `actionId` when omitted.

  Duplicate and misleading names have clear replacements; the old names stay as deprecated aliases:

  * `management.points.record(input)` awards, spends, adjusts, reverses or sets a balance, replacing `points.update`.
  * `management.pointLedgers.list()` and `pointLedgers.create(input)` replace `points.ledgers()` and `points.createLedger(input)`.

  The `{ id, ... }` input schemas (`publishDeploymentSchema`, `submitDeploymentSchema`, `rejectDeploymentSchema`, `promoteDeploymentSchema`, `rollbackDeploymentSchema` and `referralControlSchema`) are deprecated beside them; `deploymentRejectionSchema`, `deploymentActionSchema`, `referralSuspensionSchema`, `referralQualificationSchema` and `referralRewardResolutionSchema` describe the new bodies.

  The routes that took these IDs in the body keep answering released clients. Deprecated forms are removed in 2.0.0.

* b145e16: The management client reads a project's state as resources, which Console now composes its pages from:

  * `management.metrics.read()`: the project's counts and each quest's completions (`completionsByQuest`).
  * `management.quests.list()`: each quest's current release. `management.operations.list()`: paused and resumed operations. `management.inventory.list()`: stock items with their counts and the offers that sell them.
  * `management.attempts.list({ q, status, limit, cursor })` and `management.attempts.get(attempt)`; `management.entitlements.list({ q, status, limit, cursor })` and `management.entitlements.get(entitlement)`.
  * `management.audit.list({ category, q, limit, cursor })` and, for organization administrators, `management.audit.access({ q, limit, cursor })`.

  The paged attempt, entitlement and audit reads answer `{ items, total, nextCursor }`; attempt pages also carry the releases and members (`people`) they name, and entitlement pages carry their members. Pass `nextCursor` back as `cursor` until it is `null`.

  `management.page()`, `pageSchema`, `ManagementPage`, `querySchema` and `ListQuery`, Console's former page-shaped reads, are deprecated: compose the reads above instead. The page routes keep answering, unchanged, through 1.x; they are removed in 2.0.0.

* b3d87d0: `points.awarded` webhook events now carry `source`, `{ kind, id }`, naming what paid the award: `quest` or `leaderboard` with the quest or board ID, `engagement` with the engagement rule's key, or `manual` with `id: null` for an operator's point award. `kind` is an open string that can gain values; `PayoutSourceKind` from `@domino-sdk/relay/server` names the known ones. A completion revocation result (`CompletionRevocation`, from `management.completions.revoke`) gains the same optional `source` as completion records, so a revoked leaderboard prize can be told apart from a quest completion; `quest` keeps carrying the board ID for a prize. See the [webhook reference](https://console.domino.run/docs/reference/webhooks).

* f322ced: `ctx.review.photo()` is deprecated and will be removed in 2.0.0. It still works, and nothing about how it reviews photos changes. In new quest code, review an uploaded photo with `ctx.images.review({ images: [{ evidence: ctx.input.photo }], criteria })` instead. It returns the same `pass`, `fail` or `unclear` result, and it can also review X images, reference images and up to four images at once. See [check images](https://console.domino.run/docs/build/images#review-images-with-ai).

  The agent references and capability map now list tracker-restricted ingestion keys (`ingestTrackers` on `management.tokens.create` and `management.tokens.scope`) as supported.

* 5ddcb62: Approving a leaderboard settlement now sends a `completion.created` webhook event for each winner's completion. Before, only quest attempts sent it. Every `completion.created` event now carries `source` (`{ kind, id }`, where `kind` is `quest` or `leaderboard`) and `key`, the occurrence the completion consumed. A prize event has `attempt: null`, the board ID in `quest` and `source.id`, and the settlement ID in `key`. If your receiver treats every `completion.created` as a quest completion, branch on `source.kind`. Completion records in management snapshots gain the same optional `source`, and `CompletionSourceKind` is exported from `@domino-sdk/relay/server` and `@domino-sdk/relay/schema`. See the [webhook reference](https://console.domino.run/docs/reference/webhooks).

#### Patch Changes [#patch-changes-17]

* 75fa01c: Prerequisite-only automatic quests now try an uncompleted occurrence again after a claim-time budget rejection when a later member event passes current eligibility and budget checks. Evaluator, staff and revoked-occurrence rejections stay final. Automatic outcome reads can include optional `linkedAttempt` details; the legacy `attempt` ID remains available in 1.x but is deprecated. Operators can preview and confirm budget recovery through the existing automatic catch-up API and Console flow.

* 3831c73: `proxyParticipant` and `createParticipantBackend` (`@domino-sdk/relay/portal-proxy`) now forward each participant route only with the method Relay registers for it, from the same route list Relay's API admits hosted apps by. Before, some routes (sign-in and session, `/v1/me/progress`, `/v1/me/experience`, `/v1/me/attempts`, `/v1/me/evidence`, `/v1/me/collection-pass`, the wallet routes and quest retries) were forwarded with either GET or POST, and Relay answered the wrong one with 405. The proxy now answers it with 404 "Route not found". No client method changes behavior.

  `relay.evidence.download` is deprecated and will be removed in 2.0.0. No participant gate admits `GET /v1/evidence/:id`, so it has always failed with 404; members do not read evidence back, and reviewers download it through the management API.

* f493c48: Referral corrections and operator reversals now consume one shared remainder for each positive payout, so either order settles the payout once. Historical corrections use the referral account's settled amount as a conservative cap. New referral definitions refuse Discord role rewards with a clear error. Stored legacy programs remain readable and recoverable, and skip an unsupported role with an Audit history reason instead of failing qualification. See [referral corrections](https://console.domino.run/docs/build/referrals#how-corrections-work).

* 6e9b120: Completion revocation now keeps any points it could not take back visible after the command returns. The result and audit event report the remainder for each point ledger. Console shows the remainder on the member and attempt, and operators can open the existing point adjustment to settle it explicitly. Relay never records member debt for the remainder.

* 0ec4d7d: `RelayClient` and `ManagementClient` are now named interfaces that `createRelayClient`, `createBrowserRelayClient`, `createManagementClient` and `hostedManagementClient` return, so declaration files name them instead of repeating the whole client. `ManagementClient.testMembers` is no longer optional: every client these factories create has it. The clients and the error classes (`RequestError`, `RelayError`, `QuestUpdatedError`, `ManagementError`) are not for implementation or subclassing; minor releases add methods and fields to them. A `@compat` tag in a type's doc comment now says how it may grow in a minor release ([Upgrade to Relay SDK v1](https://console.domino.run/docs/reference/upgrade-to-v1#supported-consumers-and-compatibility)).

* f6b705a: Revoking a completion now reads the member's recorded tier grants instead of every completion event in the project, so it stays fast as a project grows. `409 completion-grant-history-missing` remains only for older completions whose original record could not be read; contact Domino support to restore their history. See [Revoke a completion](https://console.domino.run/docs/concepts/quests#revoke-a-completion).

* 65a9765: Deprecated three schema members that Relay no longer uses. They stay in 1.x and are removed in 2.0.0:

  * `faultAfterReview` on `submitSchema` (`SubmitInput`). Relay refuses a submission that sets it to `true`, so stop sending it.
  * The `fixture-*` values of `storedReviewModeSchema`. They name the retired simulated review providers, and only releases and quest types published before the retirement carry them. Publication already accepts only `workers-ai`.
  * `reservation` on `Redemption`. A redemption is fulfilled in the request that makes it, so the ID names no separate hold. Use `id` to refer to a redemption.

  Offer redemptions no longer briefly hold the price before spending it, and their response is unchanged. The photo, quiz and staff-confirmation snippet guides no longer suggest `fixture-*` providers, which Relay refuses at publication.

### 1.5.2 [#152]

No changes in this release.

### 1.5.1 [#151]

No changes in this release.

### 1.5.0 [#150]

#### Minor Changes [#minor-changes-60]

* 625dcbc: Expose project creator IDs and resolved operator details in project access and audit responses. Validate enriched profile IDs against the recorded creator or actor while accepting older unenriched responses. Add `updateProject` for Console organization admins to rename projects or change their slug with name confirmation. Hosted build links follow the current slug without a rebuild, including older builds that used repository-based addresses. Links switch only after the new hostname is confirmed.

  Slug changes preserve app secrets and hosted stores. New staging addresses are registered before the change commits; old and prepared slugs remain reserved until project purge, so interrupted changes can be retried safely. Reserved slugs cannot be claimed by older servers during a rolling upgrade.

### 1.4.0 [#140]

#### Minor Changes [#minor-changes-61]

* da46e7b: Add `management.testMembers.advance(member, { actionId, days }, call?)` and `TestMember.offsetDays` for Console sessions in test. Move one member forward 1–30 project-calendar days per write, up to 90 total, to try calendar rules through the ledger. See [test-member clocks](https://console.domino.run/docs/use/quests#3-advance-the-members-clock) for record timestamps, replay, and real-time workflow scheduling.

  The exported `offsetDays` field and broader `ManagementClient.testMembers.advance` method are optional for compatibility with existing clients. Factory clients always provide `advance`. Shared records retain real timestamps, beneficiary attempts pin their projected activity, and history defaults to the member’s last fourteen days through member now.

  Test members also expose optional `today?: string` (an ISO calendar date). Each advance captures a fixed duration shift, preserving timestamp order and freshness across DST. Evaluator tracker reads use that frozen shift; observation identity keeps raw timestamps.

### 1.3.0 [#130]

#### Minor Changes [#minor-changes-62]

* 86114a5: Add `management.testMembers.history(member, { since }?)` for Console sessions in the test environment. Read up to 500 attempts with ledger-credited activity times and 500 skipped tracker outcomes, with a completeness flag. The default window covers the last 14 days.

### 1.2.0 [#120]

#### Minor Changes [#minor-changes-63]

* 7f69653: Add test-environment KV and D1 storage for Domino-hosted Worker apps through `relay.json` `app.storage`. Stores receive native Worker bindings and retain data when declarations are removed. SQL migration files run before staging activation, with Wrangler-compatible tracking and per-file rollback and safe retries after interrupted responses.

  Inspect declarations and retained stores with `domino storage list`. Connected development and staging warn when local Wrangler bindings differ from hosted declarations. See [hosted storage and migration recovery](https://console.domino.run/docs/build/storage).

* cdea4a0: Browse hosted app storage read-only and delete stores no serving build declares. `domino storage tables`, `rows`, `keys` and `get`, and `management.storage` in the SDK, read SQL tables and KV values with `publish`; `domino storage delete` removes an undeclared store and its data. See [browse and delete stores](https://console.domino.run/docs/build/storage#browse-your-data).

### 1.1.1 [#111]

#### Patch Changes [#patch-changes-18]

* 1d2a0cc: Recover saved submissions after the server permanently retires an operation. Preserve the terminal state across reloads, expose it for participant review, and allow explicit discard without silently submitting another claim. Starters show a separate restart action; unknown conflicts and lost responses still resume the original action. See the [participant recovery reference](https://console.domino.run/docs/reference/participant-api).

### 1.1.0 [#110]

#### Minor Changes [#minor-changes-64]

* ead35b5: Add Console-session-only management methods to create, list, act as, and reset test members in the test environment. Member reads expose the test marker and display name. Document the CLI exclusion and reset restrictions.

### 1.0.0 [#100]

#### Major Changes [#major-changes]

* ad91102: Record prerequisite-only automatic decisions, expose participant and operator outcomes, and add previewed catch-up for paused events with review permission and audited reasons. Point-entry changes schedule current eligibility asynchronously after commit, including refunds and referral or leaderboard awards. Operators can inspect and retry failed scheduling.

  Remove the permanent silent pause block. Outcome pagination now merges prerequisite and tracker decisions; reset saved cursors and use the `automatic` identity on rows without a tracker record. See the [v1 upgrade guide](https://domino.run/docs/reference/upgrade-to-v1#prerequisite-automatic-outcomes).

* 4b10080: Add the protocol-2 `ctx.calendar.day(at?)` evaluator helper for replay-safe project-calendar keys and day bounds. Protocol-1 bundles remain executable. See the [v1 evaluator protocol upgrade guide](https://domino.run/docs/reference/upgrade-to-v1#evaluator-protocol).

* 4205914: Add code-authored campaigns with half-open availability windows and durable campaign-wide enrollment; typed completion, live balance and project-group prerequisites; typed availability blockers; and live quest-budget capacity on participant views. Participant and management clients can enroll a member in a required campaign, and Console shows campaign rules and member enrollment.

  `completed(quest)` now returns a discriminated completion prerequisite instead of `{ quest }`, and `prerequisiteSchema` accepts only typed prerequisite definitions. Stored legacy releases remain readable. See [Prerequisites are typed eligibility rules](https://console.domino.run/docs/reference/upgrade-to-v1#prerequisites-are-typed-eligibility-rules) in the v1 upgrade guide.

* 6d66f96: Add reversible campaign enrollment. Participants can withdraw and re-enroll themselves; trusted servers and Console operators can withdraw a member with an audited reason and restore them. Withdrawal blocks new admissions while preserving the first enrollment time, admitted attempts, completions, and rewards.

* a803b30: Evaluators now replay deterministically. Hosted evaluation freezes the clock for each attempt and seeds randomness from the attempt, so a retried or replayed attempt makes the same decision and `keyed()` completion key. This applies to every evaluation Domino runs, including releases published with earlier SDK versions.

  * `ctx.now` is new: the attempt's evaluation time in epoch milliseconds, the same on every run of the attempt. `keyed()` receives it too. It is distinct from `ctx.eligibleAt`, which is when the activity happened.
  * `Date.now()`, `new Date()` and `Date()` return `ctx.now` and do not advance, even across awaited capability calls. `performance.now()` returns 0.
  * `Math.random()`, `crypto.getRandomValues()` and `crypto.randomUUID()` repeat the same sequence for one attempt and differ between attempts.

  **Who should act.** Evaluators that measure elapsed time or rely on unpredictable random values. Use `ctx.now` for "as of evaluation" checks and `ctx.eligibleAt` for when the activity happened. See [Evaluators see a frozen clock and seeded randomness](https://console.domino.run/docs/reference/upgrade-to-v1#evaluators-see-a-frozen-clock-and-seeded-randomness) in the v1 upgrade guide.

* e1cba88: Observations are removed, and every automatic activity arrives as a tracker record. `automatic({ source, type })`, `ctx.observation`, `management.observations.submit`, `POST /management/v1/observations`, the `Observation` type, `observationSchema`, `observationInputSchema`, `observationReceiptSchema`, `observationTriggerSchema`, `discordReactionSchema`, `QuestView.trigger.observation` (and the release trigger's `observation`) and the `observation` field of the `automatic` availability are gone. `questStatus` and `runsSilently` from `@domino-sdk/relay/react` now label every tracker-triggered quest "Automatic", not only observation and keyed quests. A server that reported observations sends records to a custom tracker with `management.trackers.ingest` and triggers quests with `automatic({ tracker })`. Ingestion never joins a member to a project: a tracker trigger skips a member who has not joined with `not-joined`. Quests published with an observation trigger leave the active set when Relay upgrades. Republish them from code on a tracker; the quest key can stay the same.

  Discord messages and reactions are two managed tracker sources, `discord.messages()` (with `{ text: true }` to store message text) and `discord.reactions()`, on the `discord` object from `@domino-sdk/relay/authoring`. Domino's Discord gateway writes each new message or reaction into every tracker of that kind in the projects connected to the server, and keeps a record only when one of its accounts is a Discord account a member has linked. Their record data is typed (`DiscordMessageData`, `DiscordMessageTextData`, `DiscordReactionData`), so `where` filters and `ctx.tracker.record.data` are checked. A reaction record names the message's author as the `messageAuthor` account: `automatic({ tracker: reactions, beneficiary: "messageAuthor" })` replaces `reaction.received`, and Relay skips reactions to one's own message with `beneficiary-is-author`. Discord trackers refuse ingestion (403) and refresh (422). Streaks work on these triggers.

  The `observe` permission is removed. Ingesting needs `ingest`, and enrolling a member from a server (`management.quests.enroll`) needs the new `enroll` permission. Editors hold `read`, `publish`, `inventory`, `points`, `ingest` and `enroll`; hosted-app keys hold `read`, `points` and `ingest` and cannot enroll. A personal API key issued with `observe` keeps its other permissions, and `observe` grants nothing: reissue it with **Ingest** or **Enroll**. The `observation-quests` capability is removed from `capabilities.json`; Discord tracker triggers are part of the `trackers` capability.

  To upgrade, see [Observation triggers become tracker triggers](https://console.domino.run/docs/reference/upgrade-to-v1#observation-triggers-become-tracker-triggers) and [The `observe` permission is replaced by `enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#the-observe-permission-is-replaced-by-enroll).

* 23c85ba: `defineEngagementReward` takes `budgets`, a list of `{ scope: "project" | "member", period: "day" | "week" | "month" | "lifetime", limit }`, instead of `budget: { projectDaily, memberDaily, projectLifetime }`. Periods are those of the project calendar at settlement time, so a rule can now cap weekly or monthly payouts, and accounts settle once per project calendar day instead of once per UTC day. A definition with `budget` fails validation; rules already published keep their limits in the new form.

  See [Engagement budgets are a list of periods](https://console.domino.run/docs/reference/upgrade-to-v1#engagement-budgets-are-a-list-of-periods) in the v1 upgrade guide.

* 6cd0ba7: Make protocol 2 the first stable evaluator contract, including the required pinned project calendar. Remove pre-release protocol 1 from publication and execution, and preserve an SDK-built protocol-2 compatibility fixture. Rebuild and republish pre-v1 bundles during the approved cutover; changing only their protocol field is insufficient. See the [evaluator protocol upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#evaluator-protocol).

* 3a38259: Run content qualification and performance gates on custom content sources as well as X. Declare each custom source's body schema and metric names, use `content({ tracker })`, and read the captured body through `ctx.tracker.content`. Content quests imply one completion per item. Replace retired content admission and approval calls with quest outcomes, claims and tracker outcome retries.

  Upgrade steps: [Content stages](https://console.domino.run/docs/reference/upgrade-to-v1#content-stages).

* 4626004: Identity kinds are open. Besides `wallet`, `x`, `discord` and `member`, a custom tracker can declare your app's own kinds, `app:<name>` such as `app:telegram`, and records name your app's user IDs. An identity integration created with `identityKind: "app:<name>"` (set only at creation, kept by rotation) links each subject it signs in to the member as an identity of that kind, and tracker triggers attribute records through it, with `{ provider: "app:<name>", subject, revision, linkedAt }` as `ctx.tracker.attribution.account`.

  A kind belongs to the integration that first claimed it, also after a disconnect, which keeps its links attributing. A new integration takes a kind that has identities only by adopting them: `adoptIdentitiesFrom` (the old integration's ID, from the new `management.identityIntegrations.kinds()`) with a `reason` the audit log keeps; otherwise it is refused with 409. Reviewers unlink and relink a member's app identity (`management.members.unlinkIdentity` and `relinkIdentity`, and Console's **Tracker identities**), each audited with a reason: unlinking stops attribution, ends the member's sessions for that subject, and refuses its sign-in with 409 `APP_IDENTITY_UNLINKED`, which `createParticipantBackend` passes on, until it is relinked. A subject the integration already signs in as another member answers 409 `APP_IDENTITY_CONFLICT`.

  Evaluators read the member's selected X and Discord accounts and app identities with `ctx.member.identities()`, pinned once per attempt and `null` when there are none. Management member reads return them as `identities`, and Console lists them on the member under **Tracker identities**.

  Published tracker definitions in status, list, record-page, summary, and deployment responses use `trackerDefinitionResponseSchema` and `TrackerDefinitionResponse`. Authoring keeps the strict `trackerDefinitionSchema` and `TrackerDefinition`.

  Responses and evaluator contexts read identity kinds as an open set, `OpenIdentityKind`, and ignore fields they do not know, so a kind a later Relay adds no longer fails them.

  **Breaking:** `IdentityKind` is no longer a closed union, `identityKindSchema` is no longer a `z.enum`, response kinds are `OpenIdentityKind`, `trackerRecordSchema` is a separate response schema, `trackerAttributionSchema`'s account is a plain union with app and generic variants, and `ctx.member` is `{ id, identities() }`. See [Identity kinds are open, and evaluators read `ctx.member.identities()`](https://console.domino.run/docs/reference/upgrade-to-v1#identity-kinds-are-open-and-evaluators-read-ctxmemberidentities).

* 2149973: Read published trackers with future source kinds without rejecting a tracker list. Preserve unknown source payloads and expose `isKnownTrackerSource` for supported-source narrowing. Tracker authoring remains closed. Update code that assumes every response source is supported as described in the [v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#handle-future-tracker-sources).

* 54fcf41: The participant client follows one convention for paging, sessions and errors.

  * Every method takes an optional last argument, `{ session?, signal? }`. `session` replaces the positional session argument of `wallets.challenge`, `wallets.verify` and `me.collectionPass`. Calls Relay binds to a session (wallet linking, collection passes, `quests.retry`) read the current session when `session` is omitted.
  * Every paged read takes `{ cursor? }` and returns `{ items, nextCursor }`: `referrals.history`, `quests.outcomes`, `content.qualifications`, `content.performance`, `engagement.page` and `engagement.boundaries` changed from `before`, `after` and `next`. The participant routes answer `nextCursor` and read `cursor`, and so do the management routes and client methods that share them (`management.referrals.history(member, { cursor })`, `management.content.*`, `management.engagement.page` and `boundaries`).
  * Every rejection is a `RelayError`, which extends the new shared `RequestError`: `kind` (`http`, `network`, `timeout`, `aborted`, `invalid-response`, `client`), `uncertain` (true only for a write that may have taken effect: a failure after sending, a cut-off body, a 500, 502 or 504, or an unreadable success answer), `status` whenever response headers arrived, an open string `code` (known ones in `RelayErrorCode`), `operationId`, and `recovery` (`replay` with an `operationId`, else `reconcile`) when uncertain. `isRelayError` and `isRequestError` recognize these errors across duplicate copies of the package. `RelayError.status` is now `number | undefined`. Invalid input rejects with `invalid-input` instead of a `ZodError`, a success answer this SDK cannot read with `invalid-response`, a body cut off after its headers with kind `network` and its status, and a wallet signer's own failure with `wallet-signer`. The error body is read field by field, so Relay's message and a code the SDK does not know are always kept. Relay now sends `code: "not-signed-in"` and `code: "stale-session"` on those participant errors, and every paged read answers a malformed cursor with 400, code `invalid-cursor`, and one message.
  * Removed: `submit`, `retry(id)`, `progress(member)`, `upload(member, file)`, `auth.connections`, `me.linkPass`, the `wallet.*` namespace, and the staff calls the participant API refused (`lookupMember`, `snapshot`, `publish`, `publishBatch`, `beginHandover`, `confirmHandover`, `decide`, `journeyTemplate`, `template`).
  * Parameters take Zod input types, so defaulted fields are optional, and cursors are opaque strings the server validates. The session the client returns (`ParticipantSession`) accepts fields and principal kinds a newer Relay adds; `sessionMember(session)` reads its member.
  * Hosted proxies no longer forward the 0.x single-wallet routes (`/v1/me/wallet`, `/challenge`, `/verify`).

  Upgrade: [One paging, session and error convention in the participant client](https://console.domino.run/docs/reference/upgrade-to-v1#one-paging-session-and-error-convention-in-the-participant-client) in the SDK v1 upgrade guide maps every method and has search commands.

* c832384: Replace raw authoring resource keys with typed quest, ledger, tracker, and group handles. Authoring constructors and nested budget, tier, milestone, and bonus definitions now use `key`; ledger declarations publish from modules; point-ledger settings return typed references. Stored releases and management, Console, and participant wire IDs remain compatible. See [Authoring references use typed handles](https://console.domino.run/docs/reference/upgrade-to-v1#primitive-handles) in the v1 upgrade guide.

* 23c85ba: Each project environment has one project calendar: a timezone, UTC until you choose one, that sets where every day, week and month begins. Streak days, quest budgets, engagement budgets and calendar leaderboard windows all follow it, so a weekly budget and a weekly leaderboard cover the same week.

  * Set it with `"calendar": { "timeZone": "Europe/Budapest" }` in relay.json, in Console **Settings → General → Project calendar**, or with the new `management.calendar.read()` and `management.calendar.configure({ timeZone, reason })` (`GET` and `POST /calendar`, `publish` permission; in live only from Console). A change takes effect at the next midnight in the current timezone: the open day, week or month finishes in the old zone, and streaks stay consecutive. A deployment applies relay.json's timezone only when it differs from the last one deployed, and the deployment preview's new `calendar` field shows the change it schedules.
  * `streak()` and calendar leaderboard windows no longer take a `timeZone`; both throw if given one. Streaks and boards already published with one still read.
  * Budgets reset at midnight in the project timezone instead of UTC, and accept `period: "month"`.
  * One validator, `timeZoneSchema`, accepts named IANA zones only. Fixed offsets such as `+02:00` and `Etc/GMT-2` are rejected, including in leaderboards, which used to accept them. The shared `calendarPeriodSchema` is `lifetime | day | week | month`.

  **Who should act.** Projects that pass `timeZone` to `streak()` or a calendar leaderboard window, and projects that rely on budgets resetting at UTC midnight. See [Set the timezone once, on the project calendar](https://console.domino.run/docs/reference/upgrade-to-v1#set-the-timezone-once-on-the-project-calendar) in the v1 upgrade guide.

* 2528cec: Every tracker trigger shares the admission rules of ADR 0006: enrollment, the earliest activity counted, and who releases historical activity, with skips decided again when the member event that clears them happens.

  * **Admission options on `automatic({ tracker })`.** `enrollment: "required"` counts only members who enrolled in the quest, and `ctx.tracker.enrolledAt` carries their enrollment time. `since` is `"activation"` (the default), `"window"`, or, with required enrollment, `"enrollment"`. `historical` is `"staff"` (the default), `"claim"` or `"automatic"`.
  * **Historical activity waits.** A record from before the member linked the account it pays, or with an unknown link time, is skipped as `historical` until a reviewer retries it, unless the quest sets `historical: "claim"` or `"automatic"`. Before, it was admitted. See [Tracker triggers hold activity from before an account was linked](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-triggers-hold-activity-from-before-an-account-was-linked).
  * **Skips are decided again by member events.** Linking an account, removing a conflicting link, joining the project and enrolling decide the matching skips again, through a durable workflow.
  * **Every selected observation has one outcome.** New skip codes `outside-window`, `future-record`, `already-completed`, `not-enrolled`, `before-enrollment` and `historical`; `TrackerSkipCode` is an open string, with `KnownTrackerSkipCode` for the codes this SDK knows. A prerequisite completed after the record occurred no longer counts for it.
  * **Participant APIs.** `relay.quests.enroll({ actionId, quest })`, `relay.quests.outcomes({ quest?, state?, code?, cursor? })` and `relay.quests.claim({ actionId, quest, tracker, record, observedAt })`, and `useQuestOutcomes()` in React. `relay.content.enroll` and `management.content.enroll` are replaced by `relay.quests.enroll` and `management.quests.enroll`. See [Enroll in quests with `relay.quests.enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#enroll-in-quests-with-relayquestsenroll).
  * **Console.** A tracker-triggered quest shows its enrollment, `since` and historical policy, a `not-enrolled` skip offers **Enroll the member**, and a member's claim shows as **Claimed**.

  **Who should act.** Apps that enroll members or read trigger outcomes, and quests whose members link accounts after the activity they expect to be paid for.

* c919948: Follow-ups to the React hooks' shared query layer.

  * `completionCountLabel(view)` is renamed `completionCountText(view)`, so the name says it returns English like `questStatusText`. For your own words, read `view.completionCount`.
  * `questStatus(view)` returns `QuestStatus`, whose `code` is the open `QuestStatusCode`, and `roleDeliveryStatus(deliveries, quest)` returns the open `RoleDeliveryStatus | null`. A `switch` that lists every code needs a `default`, so a newer SDK can add codes without breaking the signature.
  * `useReferralSummary({ intervalMs })` reads the summary again every 60 seconds by default while the page is visible, so friends who qualify show up without an app-side timer.
  * `useSignOut()` drops the signed-out member's cached values as soon as Relay confirms the sign-out, even when the next session read fails.
  * A `useMyProfile()` change that answers after another member signed in no longer writes into the new member's cache.
  * `useSignIn`, `useSignOut`, `useSignInOptions`, `useInvitation` and `useQuest` return objects and actions that keep their identity until something in them changes; `useQuest`'s actions act on the latest view. `useExperience().deliveries` is a `readonly RoleDelivery[]`, and keeps one frozen empty list while there are none.
  * `progressOf(quests)` is documented: `total` counts locked and paused quests and leaves out archived and repeatable ones.

  Upgrade: [React hooks share one cache and return one state shape](https://console.domino.run/docs/reference/upgrade-to-v1#react-hooks-share-one-cache-and-return-one-state-shape) in the SDK v1 upgrade guide.

* c2390b3: The React hooks in `@domino-sdk/relay/react` share one query cache and return one state shape.

  * Every read hook returns `{ status, data, error, refresh }`: `status` is `loading`, `ready` or `error`, plus `signed-out` for hooks that need a member; `data` holds the value; `error` is always a `RelayError`, and while `ready` it says why the latest refresh failed and the last value stays. `useMe` and `useSignInMethods` return this shape instead of a bare value, `useMe` no longer swallows errors, and the profile hooks report the uniform 404 as `error` with status 404 instead of `missing`.
  * `<RelayProvider>` keeps one keyed cache. Components that read the same thing share one request. A read that is replaced, invalidated, or belongs to a previous participant or client is cancelled, and its late answer is dropped. Polling, including the quest experience observer and the Discord role-delivery watcher, pauses while the tab is hidden and catches up when it is shown. Reads wait for the first session read, public ones included, and a sign-in attempt that does not change the session drops nothing.
  * Changes invalidate what they affect: every new completion the experience shows, submitted or automatic, reads balances, point history, leaderboards, profiles and referral progress again; an accepted invitation, the referral summary and referral progress; wallet, account and profile changes, their own reads. The `PROFILE_CHANGED` window event is removed. `useInvalidate()` and `relayKeys` invalidate by key prefix after a change made with `useRelay()`.
  * The cache is scoped to the client, organization, project, environment, member and session. Signing in or out or switching accounts, projects or environments drops it, so one member never sees another's cached values. A new `client` or `baseUrl` passed to `RelayProvider` now takes effect: the old observer stops, reads in flight are cancelled, and the hooks start from an empty cache.
  * `useSession()` is new: who is signed in, independent of the experience read, so a failing `/me/experience` no longer blanks the profile, wallet, account and `useMe` hooks. `useExperience()` returns the shared shape with `data.quests` and `data.collections` and `deliveries`; its `member` moved to `useSession()`. `Experience` is replaced by `ExperienceData`, which `questGroups` takes.
  * `useBalances()` returns the `{ items }` answer in `data`, and `usePointHistory(ledger)` returns `data.items` with `loadMore()`; an unknown ledger or a member who has not joined is `error` with code `point-ledger-not-found` or `not-joined` instead of the `missing` and `not-joined` statuses. `BalancesState` becomes `MemberQuery` and `PointHistoryState` becomes `MemberPagedQuery`.
  * `useRelayQuery`, `useMemberQuery`, `useRelayPages` and `useMemberPages` put an app's own reads on the same cache. `Page` is the root package's type; `PagedQuery` (public pages) and `MemberPagedQuery` (member pages) add `loadMore`, `loadingMore` and `loadMoreError`, which belong to one key and participant.
  * Every error the hooks report is a `RelayError`: `useQuest().error`, `useSignIn().error`, `useSignInOptions().error` and `methodsError`, `SignInMethods.error` (also from `signInOptions()` in `@domino-sdk/relay/browser`), and a rejected or failed invitation's `error`, which replaces `reason`.
  * `completionMark`, `hasPending`, `sanitizeDeliveries`, `watchRoleDeliveries`, `PENDING_POLL_MS`, `RETRY_DELAYS_MS`, `runsSilently` and `visibleQuests` are no longer exported from `@domino-sdk/relay/react`.
  * `questStatus(view)` returns `{ tone, code, reason? }`, a stable code to localize (`QuestStatusCode`, an open string like `RelayErrorCode`); `questStatusText(view)` adds the English `label` and `detail`. `roleDeliveryStatus(deliveries, quest)` returns `pending`, `failed`, `granted` or null, and `roleDeliveryLabel(status)` gives its English label.

  Upgrade: [React hooks share one cache and return one state shape](https://console.domino.run/docs/reference/upgrade-to-v1#react-hooks-share-one-cache-and-return-one-state-shape) in the SDK v1 upgrade guide maps every hook.

* 1084288: `@domino-sdk/relay` now exports only the participant runtime client: `createRelayClient`, `RelayError`, `QuestUpdatedError`, `formatUnits`, `parseUnits`, and the types the client returns. Schemas, builders and avatar helpers moved to their own entry points, a few implementation helpers are no longer public, and two are no longer exported at all, so an app that imports the client no longer loads authoring builders, deployment and management schemas, or DiceBear.

  * Zod schemas (and helpers such as `identifier`, `caip10` and `xPostId`) come from `@domino-sdk/relay/schema`.
  * Builders (`defineTracker`, `defineLeaderboard`, `defineReferral`, `defineEngagementReward`, `content`, `streak`, `settings`, `discord`, `pointLedgers`, `x`, `customSource`) and evaluator reader types (`TrackerReader`, `XReader`, `ImageReader`) come only from `@domino-sdk/relay/authoring`.
  * `avatarSrc`, `defaultAvatar`, `defaultAvatarSvg` and `walletAvatarSeed` come from `@domino-sdk/relay/avatar`, and no longer from `@domino-sdk/relay` or `@domino-sdk/relay/react`.
  * No longer public: `identityIntegrationSchema`, `identityIntegrationInputSchema`, `identityIntegrationSecretSchema`, `participantExchangeSchema`, `questConfiguration`, `parseImageResult`, `parseImageExtraction`, `isContentParticipantRoute`, `isProfileParticipantRoute`, `isWalletParticipantRoute` and `isWalletSignInRoute` are exported only from `@domino-sdk/relay/internal`, which Relay's own services share and which is not a supported API.
  * No longer exported: `questActions` and `reconcileQuestDraft`. Open a quest controller with `relay.quests.open(view)`; its `quest-updated` result already carries the reconciled `draft`.
  * The package declares `sideEffects: false`.
  * `esbuild` is an optional peer dependency, needed only by `@domino-sdk/relay/build`. `@domino-sdk/relay-cli` depends on it directly, so `domino build` and `domino deploy` work as before. Code that calls `buildQuest` without the CLI must install `esbuild`.

  Upgrade: [Runtime entry exports only the client](https://console.domino.run/docs/reference/upgrade-to-v1#runtime-entry-exports-only-the-client) in the SDK v1 upgrade guide has the import mapping and search commands.

* 5d4ec3e: Release Relay SDK and CLI 1.0.0. Install the SDK and CLI together at the same version. This release removes pre-v1 APIs; follow the [SDK v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1) before replacing a 0.x installation.

  Stable APIs follow semantic versioning throughout the supported v1 lifetime. Compatibility exemptions apply only to explicitly experimental entrypoints.

* ae047ca: Remove the pre-v1 runtime, release, single-wallet and tracker compatibility shims. Published quests must carry an explicit evaluator protocol; protocol 2 is the first stable contract, and all pre-v1 bundles must be rebuilt and republished. Participant and management reads now expose only canonical wallet lists, and tracker policies use canonical identity roles and an explicit observation mode. See [Wallet reads return every linked wallet](https://console.domino.run/docs/reference/upgrade-to-v1#wallet-reads-return-every-linked-wallet) in the v1 upgrade guide.

* bdb89a4: Evaluator readers follow one rule: a missing subject is `null`, a failure throws `CapabilityError`, linked accounts are pinned once per evaluation run, and tracker reads are point in time. Retries replay the same run; scheduled deferred evaluations get fresh captures. Pre-v1 releases must be rebuilt and republished during cutover.

  * **Missing is `null`.** `ctx.x.profile()`, `follows()` and `reposted()` return `null` without a linked X account instead of throwing; `ctx.x.post()` returns `null` for a post that does not exist and needs no linked account. `ctx.discord.member()` returns `null` without a linked Discord account. `ctx.wallet.list()` returns `null` instead of `[]`, and `ctx.wallet.balance()` returns `null` instead of `total: "0"` when the member has no wallet of that family. `ctx.trackers.content()` returns `null` for a missing record or observation. See [Readers return `null` when an account is not linked or a subject is missing](https://console.domino.run/docs/reference/upgrade-to-v1#readers-return-null-when-an-account-is-not-linked-or-a-subject-is-missing).
  * **Failures throw `CapabilityError`**, new in `@domino-sdk/relay/authoring` (its wire shape, `capabilityFailureSchema`, is in `@domino-sdk/relay/schema`), with `code` (`"not-linked"`, `"provider-unavailable"`, `"author-error"`) and `retryable`. Invalid capability input is `author-error`; an image check of `{ x: "profile" }` without a linked X account is `not-linked`. Failures that are not retryable are captured and replay. A retryable failure that quest code handles and moves past is final for that evaluation run; one that escapes the evaluation is released, along with every fact captured after it, and retried, so the retry asks the provider and the later calls again. Rethrow a caught failure, or wrap it with `{ cause: error }`, to let it be retried. The failure shape is open: a newer Relay can add codes, and the SDK treats an unknown code as not retryable. See [Capability failures throw `CapabilityError`](https://console.domino.run/docs/reference/upgrade-to-v1#capability-failures-throw-capabilityerror).
  * **Tracker reads are point in time.** `ctx.trackers.records()` and `summary()` take `asOf`, defaulting to `ctx.eligibleAt`, and every tracker read of an evaluation run sees the observations stored at its first tracker read. Pass `asOf: ctx.now` to count everything stored by then. See [Tracker reads are point in time and pinned per attempt](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-reads-are-point-in-time-and-pinned-per-attempt).
  * **Linked accounts stay pinned.** X and Discord readers pin the account on the evaluation run's first read and replay that pin on retries, as wallets already did, so a link change no longer fails the attempt with "X account changed" or "Account or integration changed". See [Linked X and Discord accounts stay pinned for the attempt](https://console.domino.run/docs/reference/upgrade-to-v1#linked-x-and-discord-accounts-stay-pinned-for-the-attempt).

  **Who should act.** Evaluators that read `ctx.x`, `ctx.discord`, `ctx.wallet` or `ctx.trackers`, or that match capability error messages. The changed return types point TypeScript at each read to update. The `discord-membership`, `token-holder` and `community-screenshot` snippets and the Discord starter's `join-the-server` quest are updated.

#### Minor Changes [#minor-changes-65]

* afe1128: Operators can revoke an accepted completion with a reason: `management.completions.revoke({ completion, reason, freeOccurrence })` (`POST /management/v1/completions/<id>/revoke`, `review` permission). In one transaction the completion stops counting (progress, prerequisites of new attempts, counts, profiles and referral qualification), its point awards are reversed as far as the member's available points allow, tiered rewards it granted are voided or lowered to the tier another completion grants (collected ones are flagged), Discord role grants not yet delivered are stopped while delivered roles are flagged for the operator to remove in Discord (Relay never removes roles; automatic removal is DOM-99), referral programs that qualified on it are re-evaluated, and a streak milestone it paid is revoked while its days stay. `freeOccurrence` lets the member complete the quest again for the same completion key; by default later attempts for it are rejected. The result reports each part of the cascade. API keys with `review` permission can revoke in live. Console offers it on member detail and accepted attempts, and the attempt history and Audit history show the revoke and its reason. `domino capabilities --json` now marks `completion-revoke` as native.

  Breaking: a reward delivery's `action` is an open string (`RewardDeliveryAction` lists `"grant-role"`), the worker fails a delivery whose action it does not know, and a grant still pending at the revoke fails. The revoke result's `outcome` fields are open strings with known values in `RevokedEntitlementOutcome`, `RevokedDeliveryOutcome` and `RevokedReferralOutcome`. To upgrade, see [Reward delivery actions are open strings](https://console.domino.run/docs/reference/upgrade-to-v1#reward-delivery-actions-are-open-strings).

  Revoking preserves percentage bonuses from other valid completions and queues a new automatic attempt when a freed occurrence still meets its prerequisites. Delivery responses add optional `dispatchedAt` and `revokedAt` fields. Dispatch history survives retries, revoke fences failed generations, and late provider success stays visible without reopening delivery. Console uses the revoke field to stop retries and shows unknown delivered actions by their generic status.

  Revocation refuses missing or unreadable grant history before changing member state. Collected entitlements carry a persistent recovery flag in ordinary reads and Console. Correcting collection clears the flag and voids an unearned reward instead of reopening it; a surviving grant reopens only its earned tier. Reused-completion attempt timelines include the shared completion’s revoke event.

* b0bffa0: Add native distinct-action progress counters with calendar and rolling windows, durable achievements, participant summaries and audited operator correction. Participant state follows settlement order for late-arriving repeat batches, and achievement alerts remain scoped to their calendar window when the next window reopens. Add qualified-referral and continuous-membership examples. See [counter rules](https://console.domino.run/docs/concepts/quests#distinct-action-counters). Update custom attempt renderers using the [counter upgrade steps](https://console.domino.run/docs/reference/upgrade-to-v1#counter-progress).

* bac9ca4: Declare provider modules to enable evaluator readers, use `discord.grantRole()` for role rewards, and replace Solana-only wallet readers with `wallet.list()` and `wallet.balance()`. See the [upgrade guide](/docs/reference/upgrade-to-v1#declared-integrations).

* e7f81ff: Add signed outbound webhooks for operator backends, with scoped subscriptions, bounded retries, delivery logs, replay, and overlapping secret rotation. Expose `management.webhooks`, `verifyWebhook`, shared activity constants, and `domino webhooks` commands. This is additive; existing integrations need no changes. See [Receive outbound webhooks](https://console.domino.run/docs/build/webhooks) for an optional polling migration.

* 8465d40: Operators can correct a member's tiered reward, at the revision they saw and with an audited reason. `management.handover.cancel(entitlement, { revision, reason })` (`POST /management/v1/entitlements/<id>/cancel`, `handover` permission) returns a handover in progress to `available` and puts its pickup size back into stock. `management.entitlements.correctCollection` (`…/correct`, `review`) does the same for a collection recorded by mistake, and `management.entitlements.void` (`…/void`, `review`) voids an available entitlement. A stale revision fails with 409 `entitlement-changed`, the wrong starting status with 409 `entitlement-status`. API keys with the permission can make these corrections in live. Console Rewards and member detail offer them, and Audit history lists each one with its reason. `domino capabilities --json` lists them as `entitlement-corrections`.

  Breaking: `Entitlement` gains the status `"void"`, with `voidedAt`. A voided entitlement cannot be collected; a quest that grants the same reward to the member again makes it `available`, as a fresh grant at that quest's tier; a voided referral reward stays void. SDKs from before this release fail the whole progress read once a member has a voided entitlement. Reads now parse entitlements with a tolerant `entitlementSchema` (fields a newer server adds are kept, and an unknown status reads as `"unsupported"` with `reportedStatus`); the strict stored shape is `entitlementStateSchema`, and the management client's `entitlementRecordSchema` is removed in favor of `entitlementSchema`. To upgrade, see [Entitlements can be `void`, and reads tolerate statuses they do not know](https://console.domino.run/docs/reference/upgrade-to-v1#entitlements-can-be-void-and-reads-tolerate-statuses-they-do-not-know).

* 84feaf7: Declare evaluator protocol 2 on authored bundles and descriptions, including the pinned project calendar. The host selects a versioned adapter, derives readers from the capability registry and preserves captured capability requests for replay. Custom bundle builders must keep both protocol declarations. See the [v1 evaluator protocol upgrade guide](https://domino.run/docs/reference/upgrade-to-v1#evaluator-protocol).

* 71cf912: Add reviewable leaderboard settlements. Fixed and calendar earned/net leaderboards can declare point and tiered-reward prizes, choose shared-rank or earliest-score tie handling, and freeze each ended window with its eligibility and exclusions at cutoff. Entries received after the cutoff stay out even when backdated, and a definition with a changed window advances past an obsolete scheduled cutoff. Operators review and approve delivery in Console or with `management.leaderboards`, where the approval reason remains visible, and can correct an individual winner through completion revocation without awarding that settlement again.

* f2f1b75: Breaking: the management client moves from `@domino-sdk/relay-cli` to the new `@domino-sdk/relay/server` entrypoint, with `createManagementClient`, `hostedManagementClient`, `ManagementError`, the management schemas and types, and `starterTemplates`. Server code, including a hosted app's Worker, now needs only `@domino-sdk/relay`, without the CLI's command-line parser and TypeScript compiler. `@domino-sdk/relay-cli` is only the `domino` executable and its agent skills; it has nothing to import. `serverBoundary()` also fails a browser build that imports `@domino-sdk/relay/server`. To upgrade, see [Import the management client from `@domino-sdk/relay/server`](https://console.domino.run/docs/reference/upgrade-to-v1#import-the-management-client-from-domino-sdkrelayserver).

  The capability catalog (`domino capabilities --json`) adds staff confirmation, staff handover, stock, point corrections, deploy and stage, operator member reads and lookup, project access, and the management API with `domino api`. It marks webhooks, bulk data export, completion revocation and tracker-scoped keys as unsupported, and lists the management methods it leaves out on purpose in `excludedManagementMethods`.

* 8c06912: Add operator reads and audited unlink for current X and Discord quest accounts. Reviewers provide the subject and selection revision to prevent a stale review from unlinking a newer connection. Identity ownership, sign-in grants and existing rewards remain unchanged. Console exposes the action on each member's Quest accounts section.

* 6c30583: Add member-only activity pages, `me.activity.observe()` and `useActivity({ onEvent })` for committed asynchronous outcomes. Cursor reads preserve concurrent commits, and observers pause while hidden and reset on session changes. Events begin after the additive activity-feed migration; existing snapshots remain authoritative for earlier state.

* 710e520: Participants read their own point balances and history. `relay.me.balances()` (`GET /v1/me/points`) returns `{ items }`: the signed-in member's `balance` and `available` points (after reservations) in every ledger of the project, each with its `ledger` reference. `relay.me.pointHistory({ ledger, cursor? })` (`GET /v1/me/points/:ledger/history`) pages through the member's own entries in one ledger, by key or ID, most recently recorded first, as `{ items, nextCursor }`. Each entry is `{ id, amount, kind, at, quest, reverses }`, without the operator's reason or actor. `kind` is an open string; the `PointEntryKind` constants (`award`, `spend`, `adjust`, `reverse`) name the kinds this SDK knows, and referral payouts are `adjust`. An unknown ledger answers 404 with code `point-ledger-not-found` and a member who has not joined the project 404 with `not-joined`, both added to `RelayErrorCode`. Neither read takes a member, and both are scoped to the session's project and environment. React apps use `useBalances()`, which reads again after a quest completes, and `usePointHistory(ledger)`. The participant proxy and identity integrations admit both routes. New schemas and types: `pointBalanceSchema`, `pointBalancesSchema`, `pointEntrySchema`, `pointHistoryQuerySchema`, `pointHistoryPageSchema`, `PointBalance`, `PointBalances`, `PointEntry`, `PointEntryKind`, `KnownPointEntryKind`, `PointHistoryQuery` and `PointHistoryPage`.

  The classic starter's header now reads the balance with `useBalances()` instead of its `/api/me/points` server route, which is gone.

* 6c30583: Expose participant earned rewards through `rewards.entitlements()` and `useEntitlements()`, and include entitlements in experience reads. Add `rewards.deliveries()` and `useDeliveries()` for points, tiers and integration deliveries. Preserve unknown delivery providers, kinds and states in reads while keeping backend delivery writes validated. Role displays use current reconciliation state after revocation.

  Keep private operator revocation reasons out of participant reward, progress and experience reads while retaining visible collection recovery state.

* c320586: A content quest's `performance.evaluate` can no longer select rewards. It gates the rewards the content evaluation already selected, and it always did: a `rewards` value in its result was silently ignored. Its type is now `PerformanceOutcome` (`accept`, `review` or `reject` with a reason only), so `decision.accept({ rewards })` and `decision.review({ reason, rewards })` are type errors there, and a runtime result that carries `rewards` fails the evaluation with "A performance evaluator cannot select rewards". To change what a post earns, select rewards in the quest's `evaluate`.

  See [Performance evaluators no longer select rewards](/docs/reference/upgrade-to-v1#performance-evaluators-no-longer-select-rewards) in the upgrade guide.

* c320586: Replace untyped prerequisite objects with typed rules. `completed(questHandle)` creates a completion rule with `scope: "ever"`; `completed(questHandle, { scope: "period", period: "day" })` checks the current project-calendar period. Balance and group requirements use `balanceAtLeast(ledgerHandle, minimum)` and `inGroup(groupHandle)`. Persisted rules carry an explicit `kind`; bare `{ quest }` and string prerequisites are not canonical v1 input.

  See [Prerequisites are typed eligibility rules](https://console.domino.run/docs/reference/upgrade-to-v1#prerequisites-are-typed-eligibility-rules) and [Authoring references use typed handles](https://console.domino.run/docs/reference/upgrade-to-v1#primitive-handles) in the upgrade guide.

* 569989e: Reconcile Discord role membership across projects and environments, preserve external assignments, and expose operator recovery plus current membership alongside delivery history. Revoking the last accepted grant now removes Relay-owned assignments. Read the [SDK v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#discord-role-ownership-and-reconciliation) before upgrading.

  Treat forbidden Discord membership reads as permanent delivery failures so operators can fix permissions and retry immediately.

  Publish initial pending delivery events in the grant transaction so observers can see queued rewards before reconciliation starts.

* 8a8870e: Add points-priced offers with declared stock, participant redemption hooks, automatic or staff-handover fulfillment, and operator cancellation and refund controls.

* 11ab1d8: Add explicit archive and restore for code-declared resources through the management client, durable CLI operations and Console. Keep resource identities and history while stopping new activity, and preserve archive state across publication. See the [resource lifecycle](https://console.domino.run/docs/concepts/quests#resource-lifecycle).

* ddfc526: The participant client, the management client and the CLI share one set of deadline, cancellation and retry rules.

  * Each call has one deadline covering every attempt, the waits between them and the answer's body: 15 seconds for the participant client and 30 seconds for the management client and the CLI. `createRelayClient({ timeout })` and the new per-call `{ timeout }` option of every participant method set it, as `timeout` already does for the management client. A timeout is never retried.
  * A call is sent again, at most three times in all, only when the server applies a repeat at most once: reads; participant writes with an action ID, submission evidence by its upload ID, `wallets.verify` (by its challenge) and `wallets.challenge`, but not avatar uploads, whose upload ID has no receipt; typed management writes whose input has an `actionId`; and the CLI's `deploy` publication, `stage` build request and `quests archive`/`unarchive`. Every attempt sends the same payload and session. Retries follow a lost answer, including a body that breaks off after successful headers, and HTTP 500, 502, 503 and 504, with a short jittered backoff. Invalid or unexpected answers, cancellations, rejected input, other refusals and every other write are sent once.
  * A 429 is retried only when its `Retry-After` fits in the time left, and every wait honours `Retry-After`. The new `retryAfter` field of `RequestError` (and so `RelayError` and `ManagementError`) carries the wait the server asked for, in milliseconds; the CLI reports it as `details.retryAfter` on `RATE_LIMITED` and `SERVER_ERROR`.
  * A session-bound participant call that reads the current session first shares one deadline with that read, and `auth.session()` honours `timeout`.
  * An error status whose body breaks off, times out or is cancelled is now a `network`, `timeout` or `aborted` failure that keeps its `status` and `retryAfter`, in all three clients; an error body that is not JSON stays `http`.
  * Cancelling with `signal` stops a retry wait at once. A write any attempt of which may have been applied stays `uncertain`, with its `operationId` and `recovery`, whatever later attempts answer.
  * `management.request` retries only GET, and `management.raw` sends once. `useInvitation()` keeps a rate-limited code for a retry instead of discarding it as refused.

  Avatar failures report `recovery: "reconcile"` even when they have an upload ID. Wallet address and signature waits reject when the caller cancels. See [Bounded requests and avatar recovery](https://console.domino.run/docs/reference/upgrade-to-v1#bounded-requests-and-avatar-recovery) for the recovery-handler change.

  Generic management writes report `recovery: "reconcile"` even with a caller-supplied action ID. Uncertain `domino api` writes expose `details.recovery: "reconcile"`. See [Generic management writes require reconciliation](https://console.domino.run/docs/reference/upgrade-to-v1#generic-management-writes-require-reconciliation) for the recovery-handler change.

* 89ea6ba: Add reviewed live project publication. The management client can list deployment records, submit and reject previews, promote a published test deployment, and prepare a rollback to the previous live deployment. Promotion and rollback accept an action UUID, and the management client generates one when omitted so retries return the same deployment record. `domino deploy --environment live` now returns `status: "pending"`, and `--promote` submits test source for live review; only a person in Console can approve, reject, or prepare rollback. The new `pending` deploy status is a breaking addition for exhaustive automation. See [Live deploys return a pending review](https://console.domino.run/docs/reference/upgrade-to-v1#live-deploys-return-a-pending-review).

* 67d0d8c: Add bounded `decision.defer` and absolute `decision.progress` results, fresh evaluation runs scheduled by durable timers, participant progress and deferred status, and consistent completion-key authoring types. Reviewers can reject a deferred attempt to stop it.

  Attempt response statuses accept future server values; use known `AttemptStatus` constants with a fallback. New manual submissions while deferred return `409 attempt-pending`. Custom decision/status switches must handle the new variants. See [Handle deferred evaluator decisions and progress](https://console.domino.run/docs/reference/upgrade-to-v1#handle-deferred-evaluator-decisions-and-progress) for old and new code, capture semantics, and search patterns.

* d3b6261: Restrict personal ingestion keys to selected custom trackers, inspect and edit their scope without changing the secret, and return TRACKER\_SCOPE\_DENIED for disallowed writes. Human CLI sign-ins can manage their own keys; project-scoped playground sessions and API keys cannot. Existing unrestricted keys retain project-wide ingestion.

* d7f9fe4: `streak(...)` completes manual and tracker-triggered quests. Each accepted attempt counts the project-calendar day of its trusted occurrence time, exposed as `ctx.eligibleAt`: a tracker observation's `occurredAt`, or the admission time of a manual submission. A rejected manual submission preserves committed streak progress and displays its reason. Once the streak milestone pays, later tracker observations do not pay it again.

  Content-stage quests and prerequisite-only `automatic()` quests cannot use streak completion. A published quest cannot change between manual and tracker triggers under the same key. Observation triggers are removed; migrate them to trackers. See [Observation triggers become tracker triggers](https://console.domino.run/docs/reference/upgrade-to-v1#observation-triggers-become-tracker-triggers).

* 0a89168: Operators can archive point ledgers and correct points from Console member detail. `management.pointLedgers.list()` includes nullable `archivedAt`, and `management.pointLedgers.archive(id)` archives a ledger. Archived ledgers remain in balance and history reads and existing published rewards keep working, but new reward pickers and published definitions reject them.

* cf5b02c: Custom tracker records have a declared data schema. `customSource(namespace, { data })` requires a zod object schema (`z` is exported by `@domino-sdk/relay/authoring`), stores its JSON Schema form with the tracker, and throws, naming the field, for what ingestion cannot check (string formats and patterns, tuples, refinements, transforms, dates, bigints). Publication refuses a custom tracker without one. Ingestion rejects a batch whose record data does not match with 422 and `{ error, code: "invalid-record-data", record, path, reason }`, one too costly to validate with `record-data-too-complex`, and records for a tracker published without a schema with `tracker-schema-required`. Schemas are bounded (32 KB, 16 levels, 256 properties, required keys, enum values or key names per node, 16 branches per union, no wide union nested in another union's branch), and validation charges every unit of work to a per-record and per-batch step budget. A deployment whose new schema breaks a live quest's `where` filter, Console quests included, is refused. Stored records are not rechecked: tracker triggers skip non-matching records with the new skip code `invalid-record-data`, evaluators reject pinned records that do not match the schema the quest was built with, and `trackers.records(handle)` leaves them out and counts them in `excluded`. The schema can change under the same tracker key and applies to later records.

  The data type flows into quests: `ctx.tracker` is non-null in a tracker-triggered quest's evaluator with `record.data` of the tracker's type (content triggers expose their captured body through `ctx.tracker.content`), `trackers.records(tracker)` returns typed records, and `where` paths, `equals` values and `{ setting }` bounds are type-checked against the data and the quest's integer settings. `where` also accepts a list of conditions that must all hold, and `automatic()` and publication check filter paths against the stored schema. `settings.object` and `defineQuest` accept only `settings.*` builders; a plain zod setting is a type error and `defineQuest` throws `Setting "<key>" is not a quest setting`.

  See [Custom sources declare their record data](/docs/reference/upgrade-to-v1#custom-sources-declare-their-record-data) and [Quest settings come only from settings builders](/docs/reference/upgrade-to-v1#quest-settings-come-only-from-settings-builders) in the upgrade guide.

* 1e23d8f: Type quest completion and prerequisites from the trigger, preserve source-specific beneficiary roles, and share shape validation with publication. Reusable prerequisite-only types can receive prerequisites from their instances. See the [upgrade guide](/docs/reference/upgrade-to-v1#typed-trigger-policies).

#### Patch Changes [#patch-changes-19]

* ed4988c: Read Cosmos Hub and Osmosis bank balances with `ctx.wallet.balance()`, pinned to one verified block and the member's wallets on the exact Cosmos network.
* c320586: `defineQuest` throws at build time for two shapes that only failed when publishing: a bare `automatic()` quest with no `requires` entry, and a bare `automatic()` quest with `keyed()` completion. A bare `automatic()` runs once its prerequisites are complete, so it needs at least one typed completion, balance or group prerequisite and completes once. The backend still checks both when it publishes.
* 1141af4: Fix invalid action IDs in the tracker and leaderboard documentation examples. Validate action ID literals during documentation checks so copied examples use the identifier format the API accepts.
* 83c4426: Update Discord message tracker records when a message is edited or deleted. Turning text off or deleting a message now erases retained message text.
* c320586: `manual({ actor: "staff", input })` throws instead of dropping the actor. Its types never allowed the combination, but untyped callers got a quest that members could submit evidence to, not one staff confirm. Staff-captured evidence is not supported: use `manual({ actor: "staff" })` for a staff confirmation, or `manual({ input })` for evidence the participant submits.
* 7e37049: The SDK's optional React peer dependency is now `^19.0.0` instead of `>=19`, matching the React versions the React hooks are verified against. React is still optional: the runtime, browser and server entrypoints do not need it. Apps on React 19 need no change.
* 909f343: The browser submission store recovers from storage failures instead of caching them. A temporary IndexedDB opening error, a blocked open, a `versionchange` from another tab, or a connection the browser closes no longer fails every later call on the same client: the next `status()`, `resume()` or `submit()` opens storage again, and the saved operation and its operation ID are kept. A blocked open waits briefly for the other tab, then rejects with `submission-storage`, and the pending request is adopted if it later succeeds. Full browser storage rejects with the new `RelayErrorCode.SubmissionStorageQuota` (`submission-storage-quota`); an upgrade by a newer page asks for a reload. Both are `client` errors that are not `uncertain`, and a failed read never looks like an empty store, so a new claim is never started over a saved one. See [Storage failures](/docs/reference/participant-api#storage-failures) in the participant API reference.
* 8b706af: An attempt now records when its evaluator asked for human review on a quest with review off. `decision.review({ reason })` was already a rejection with the review reason in that case. The attempt now also has `requestedReview: true`, and Console attempt detail says "Would have gone to review; human review is off". A reject the evaluator returned itself keeps `requestedReview: false`. The `review` option, the quests concept page and the authoring reference describe it.
* e1e701b: Read added trigger and availability fields in participant progress and quest views without weakening publication input validation. Verify unknown delivery providers, actions and statuses through the wire compatibility fixtures.

### 1.0.0-next.34 [#100-next34]

#### Major Changes [#major-changes-1]

* ad91102: Record prerequisite-only automatic decisions, expose participant and operator outcomes, and add previewed catch-up for paused events with review permission and audited reasons. Point-entry changes schedule current eligibility asynchronously after commit, including refunds and referral or leaderboard awards. Operators can inspect and retry failed scheduling.

  Remove the permanent silent pause block. Outcome pagination now merges prerequisite and tracker decisions; reset saved cursors and use the `automatic` identity on rows without a tracker record. See the [v1 upgrade guide](https://domino.run/docs/reference/upgrade-to-v1#prerequisite-automatic-outcomes).

* 4b10080: Add the protocol-2 `ctx.calendar.day(at?)` evaluator helper for replay-safe project-calendar keys and day bounds. Protocol-1 bundles remain executable. See the [v1 evaluator protocol upgrade guide](https://domino.run/docs/reference/upgrade-to-v1#evaluator-protocol).

* 4205914: Add code-authored campaigns with half-open availability windows and durable campaign-wide enrollment; typed completion, live balance and project-group prerequisites; typed availability blockers; and live quest-budget capacity on participant views. Participant and management clients can enroll a member in a required campaign, and Console shows campaign rules and member enrollment.

  `completed(quest)` now returns a discriminated completion prerequisite instead of `{ quest }`, and `prerequisiteSchema` accepts only typed prerequisite definitions. Stored legacy releases remain readable. See [Prerequisites are typed eligibility rules](https://console.domino.run/docs/reference/upgrade-to-v1#prerequisites-are-typed-eligibility-rules) in the v1 upgrade guide.

* 6d66f96: Add reversible campaign enrollment. Participants can withdraw and re-enroll themselves; trusted servers and Console operators can withdraw a member with an audited reason and restore them. Withdrawal blocks new admissions while preserving the first enrollment time, admitted attempts, completions, and rewards.

* 6cd0ba7: Make protocol 2 the first stable evaluator contract, including the required pinned project calendar. Remove pre-release protocol 1 from publication and execution, and preserve an SDK-built protocol-2 compatibility fixture. Rebuild and republish pre-v1 bundles during the approved cutover; changing only their protocol field is insufficient. See the [evaluator protocol upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#evaluator-protocol).

* 3a38259: Run content qualification and performance gates on custom content sources as well as X. Declare each custom source's body schema and metric names, use `content({ tracker })`, and read the captured body through `ctx.tracker.content`. Content quests imply one completion per item. Replace retired content admission and approval calls with quest outcomes, claims and tracker outcome retries.

  Upgrade steps: [Content stages](https://console.domino.run/docs/reference/upgrade-to-v1#content-stages).

* c832384: Replace raw authoring resource keys with typed quest, ledger, tracker, and group handles. Authoring constructors and nested budget, tier, milestone, and bonus definitions now use `key`; ledger declarations publish from modules; point-ledger settings return typed references. Stored releases and management, Console, and participant wire IDs remain compatible. See [Authoring references use typed handles](https://console.domino.run/docs/reference/upgrade-to-v1#primitive-handles) in the v1 upgrade guide.

* 2528cec: Every tracker trigger shares the admission rules of ADR 0006: enrollment, the earliest activity counted, and who releases historical activity, with skips decided again when the member event that clears them happens.

  * **Admission options on `automatic({ tracker })`.** `enrollment: "required"` counts only members who enrolled in the quest, and `ctx.tracker.enrolledAt` carries their enrollment time. `since` is `"activation"` (the default), `"window"`, or, with required enrollment, `"enrollment"`. `historical` is `"staff"` (the default), `"claim"` or `"automatic"`.
  * **Historical activity waits.** A record from before the member linked the account it pays, or with an unknown link time, is skipped as `historical` until a reviewer retries it, unless the quest sets `historical: "claim"` or `"automatic"`. Before, it was admitted. See [Tracker triggers hold activity from before an account was linked](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-triggers-hold-activity-from-before-an-account-was-linked).
  * **Skips are decided again by member events.** Linking an account, removing a conflicting link, joining the project and enrolling decide the matching skips again, through a durable workflow.
  * **Every selected observation has one outcome.** New skip codes `outside-window`, `future-record`, `already-completed`, `not-enrolled`, `before-enrollment` and `historical`; `TrackerSkipCode` is an open string, with `KnownTrackerSkipCode` for the codes this SDK knows. A prerequisite completed after the record occurred no longer counts for it.
  * **Participant APIs.** `relay.quests.enroll({ actionId, quest })`, `relay.quests.outcomes({ quest?, state?, code?, cursor? })` and `relay.quests.claim({ actionId, quest, tracker, record, observedAt })`, and `useQuestOutcomes()` in React. `relay.content.enroll` and `management.content.enroll` are replaced by `relay.quests.enroll` and `management.quests.enroll`. See [Enroll in quests with `relay.quests.enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#enroll-in-quests-with-relayquestsenroll).
  * **Console.** A tracker-triggered quest shows its enrollment, `since` and historical policy, a `not-enrolled` skip offers **Enroll the member**, and a member's claim shows as **Claimed**.

  **Who should act.** Apps that enroll members or read trigger outcomes, and quests whose members link accounts after the activity they expect to be paid for.

* ae047ca: Remove the pre-v1 runtime, release, single-wallet and tracker compatibility shims. Published quests must carry an explicit evaluator protocol; protocol 1 remains supported, while protocol-less bundles must be republished. Participant and management reads now expose only canonical wallet lists, and tracker policies use canonical identity roles and an explicit observation mode. See [Wallet reads return every linked wallet](https://console.domino.run/docs/reference/upgrade-to-v1#wallet-reads-return-every-linked-wallet) in the v1 upgrade guide.

#### Minor Changes [#minor-changes-66]

* b0bffa0: Add native distinct-action progress counters with calendar and rolling windows, durable achievements, participant summaries and audited operator correction. Participant state follows settlement order for late-arriving repeat batches, and achievement alerts remain scoped to their calendar window when the next window reopens. Add qualified-referral and continuous-membership examples. See [counter rules](https://console.domino.run/docs/concepts/quests#distinct-action-counters). Update custom attempt renderers using the [counter upgrade steps](https://console.domino.run/docs/reference/upgrade-to-v1#counter-progress).

* bac9ca4: Declare provider modules to enable evaluator readers, use `discord.grantRole()` for role rewards, and replace Solana-only wallet readers with `wallet.list()` and `wallet.balance()`. See the [upgrade guide](/docs/reference/upgrade-to-v1#declared-integrations).

* 84feaf7: Declare evaluator protocol 1 on authored bundles and descriptions. The host selects a versioned adapter, derives readers from the capability registry and preserves captured capability requests for replay. Custom bundle builders must keep both protocol declarations. See the [v1 evaluator protocol upgrade guide](https://domino.run/docs/reference/upgrade-to-v1#evaluator-protocol).

* 71cf912: Add reviewable leaderboard settlements. Fixed and calendar earned/net leaderboards can declare point and tiered-reward prizes, choose shared-rank or earliest-score tie handling, and freeze each ended window with its eligibility and exclusions at cutoff. Entries received after the cutoff stay out even when backdated, and a definition with a changed window advances past an obsolete scheduled cutoff. Operators review and approve delivery in Console or with `management.leaderboards`, where the approval reason remains visible, and can correct an individual winner through completion revocation without awarding that settlement again.

* 6c30583: Add member-only activity pages, `me.activity.observe()` and `useActivity({ onEvent })` for committed asynchronous outcomes. Cursor reads preserve concurrent commits, and observers pause while hidden and reset on session changes. Events begin after the additive activity-feed migration; existing snapshots remain authoritative for earlier state.

* 6c30583: Expose participant earned rewards through `rewards.entitlements()` and `useEntitlements()`, and include entitlements in experience reads. Add `rewards.deliveries()` and `useDeliveries()` for points, tiers and integration deliveries. Preserve unknown delivery providers, kinds and states in reads while keeping backend delivery writes validated. Role displays use current reconciliation state after revocation.

  Keep private operator revocation reasons out of participant reward, progress and experience reads while retaining visible collection recovery state.

* 8a8870e: Add points-priced offers with declared stock, participant redemption hooks, automatic or staff-handover fulfillment, and operator cancellation and refund controls.

* 11ab1d8: Add explicit archive and restore for code-declared resources through the management client, durable CLI operations and Console. Keep resource identities and history while stopping new activity, and preserve archive state across publication. See the [resource lifecycle](https://console.domino.run/docs/concepts/quests#resource-lifecycle).

* 0a89168: Operators can archive point ledgers and correct points from Console member detail. `management.pointLedgers.list()` includes nullable `archivedAt`, and `management.pointLedgers.archive(id)` archives a ledger. Archived ledgers remain in balance and history reads and existing published rewards keep working, but new reward pickers and published definitions reject them.

* 1e23d8f: Type quest completion and prerequisites from the trigger, preserve source-specific beneficiary roles, and share shape validation with publication. Reusable prerequisite-only types can receive prerequisites from their instances. See the [upgrade guide](/docs/reference/upgrade-to-v1#typed-trigger-policies).

#### Patch Changes [#patch-changes-20]

* ed4988c: Read Cosmos Hub and Osmosis bank balances with `ctx.wallet.balance()`, pinned to one verified block and the member's wallets on the exact Cosmos network.
* e1e701b: Read added trigger and availability fields in participant progress and quest views without weakening publication input validation. Verify unknown delivery providers, actions and statuses through the wire compatibility fixtures.

### 1.0.0-next.33 [#100-next33]

#### Minor Changes [#minor-changes-67]

* 89ea6ba: Add reviewed live project publication. The management client can list deployment records, submit and reject previews, promote a published test deployment, and prepare a rollback to the previous live deployment. Promotion and rollback accept an action UUID, and the management client generates one when omitted so retries return the same deployment record. `domino deploy --environment live` now returns `status: "pending"`, and `--promote` submits test source for live review; only a person in Console can approve, reject, or prepare rollback. The new `pending` deploy status is a breaking addition for exhaustive automation. See [Live deploys return a pending review](https://console.domino.run/docs/reference/upgrade-to-v1#live-deploys-return-a-pending-review).

### 1.0.0-next.32 [#100-next32]

#### Minor Changes [#minor-changes-68]

* 6c30583: Add member-only activity pages, `me.activity.observe()` and `useActivity({ onEvent })` for committed asynchronous outcomes. Cursor reads preserve concurrent commits, and observers pause while hidden and reset on session changes. Events begin after the additive activity-feed migration; existing snapshots remain authoritative for earlier state.
* 6c30583: Expose participant earned rewards through `rewards.entitlements()` and `useEntitlements()`, and include entitlements in experience reads. Add `rewards.deliveries()` and `useDeliveries()` for points, tiers and integration deliveries. Preserve unknown delivery providers, kinds and states in reads while keeping backend delivery writes validated. Role displays use current reconciliation state after revocation.

  Keep private operator revocation reasons out of participant reward, progress and experience reads while retaining visible collection recovery state.

### 1.0.0-next.31 [#100-next31]

#### Minor Changes [#minor-changes-69]

* 569989e: Reconcile Discord role membership across projects and environments, preserve external assignments, and expose operator recovery plus current membership alongside delivery history. Revoking the last accepted grant now removes Relay-owned assignments. Read the [SDK v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#discord-role-ownership-and-reconciliation) before upgrading.

  Treat forbidden Discord membership reads as permanent delivery failures so operators can fix permissions and retry immediately.

  Publish initial pending delivery events in the grant transaction so observers can see queued rewards before reconciliation starts.

### 1.0.0-next.30 [#100-next30]

#### Minor Changes [#minor-changes-70]

* afe1128: Operators can revoke an accepted completion with a reason: `management.completions.revoke({ completion, reason, freeOccurrence })` (`POST /management/v1/completions/<id>/revoke`, `review` permission). In one transaction the completion stops counting (progress, prerequisites of new attempts, counts, profiles and referral qualification), its point awards are reversed as far as the member's available points allow, tiered rewards it granted are voided or lowered to the tier another completion grants (collected ones are flagged), Discord role grants not yet delivered are stopped while delivered roles are flagged for the operator to remove in Discord (Relay never removes roles; automatic removal is DOM-99), referral programs that qualified on it are re-evaluated, and a streak milestone it paid is revoked while its days stay. `freeOccurrence` lets the member complete the quest again for the same completion key; by default later attempts for it are rejected. The result reports each part of the cascade. API keys with `review` permission can revoke in live. Console offers it on member detail and accepted attempts, and the attempt history and Audit history show the revoke and its reason. `domino capabilities --json` now marks `completion-revoke` as native.

  Breaking: a reward delivery's `action` is an open string (`RewardDeliveryAction` lists `"grant-role"`), the worker fails a delivery whose action it does not know, and a grant still pending at the revoke fails. The revoke result's `outcome` fields are open strings with known values in `RevokedEntitlementOutcome`, `RevokedDeliveryOutcome` and `RevokedReferralOutcome`. To upgrade, see [Reward delivery actions are open strings](https://console.domino.run/docs/reference/upgrade-to-v1#reward-delivery-actions-are-open-strings).

  Revoking preserves percentage bonuses from other valid completions and queues a new automatic attempt when a freed occurrence still meets its prerequisites. Delivery responses add optional `dispatchedAt` and `revokedAt` fields. Dispatch history survives retries, revoke fences failed generations, and late provider success stays visible without reopening delivery. Console uses the revoke field to stop retries and shows unknown delivered actions by their generic status.

  Revocation refuses missing or unreadable grant history before changing member state. Collected entitlements carry a persistent recovery flag in ordinary reads and Console. Correcting collection clears the flag and voids an unearned reward instead of reopening it; a surviving grant reopens only its earned tier. Reused-completion attempt timelines include the shared completion’s revoke event.

### 1.0.0-next.29 [#100-next29]

#### Minor Changes [#minor-changes-71]

* 67d0d8c: Add bounded `decision.defer` and absolute `decision.progress` results, fresh evaluation runs scheduled by durable timers, participant progress and deferred status, and consistent completion-key authoring types. Reviewers can reject a deferred attempt to stop it.

  Attempt response statuses accept future server values; use known `AttemptStatus` constants with a fallback. New manual submissions while deferred return `409 attempt-pending`. Custom decision/status switches must handle the new variants. See [Handle deferred evaluator decisions and progress](https://console.domino.run/docs/reference/upgrade-to-v1#handle-deferred-evaluator-decisions-and-progress) for old and new code, capture semantics, and search patterns. This ships in the `1.0.0-next` prerelease series.

* d3b6261: Restrict personal ingestion keys to selected custom trackers, inspect and edit their scope without changing the secret, and return TRACKER\_SCOPE\_DENIED for disallowed writes. Human CLI sign-ins can manage their own keys; project-scoped playground sessions and API keys cannot. Existing unrestricted keys retain project-wide ingestion.

### 1.0.0-next.28 [#100-next28]

#### Patch Changes [#patch-changes-21]

* 83c4426: Update Discord message tracker records when a message is edited or deleted. Turning text off or deleting a message now erases retained message text.

### 1.0.0-next.27 [#100-next27]

#### Minor Changes [#minor-changes-72]

* e7f81ff: Add signed outbound webhooks for operator backends, with scoped subscriptions, bounded retries, delivery logs, replay, and overlapping secret rotation. Expose `management.webhooks`, `verifyWebhook`, shared activity constants, and `domino webhooks` commands. This is additive; existing integrations need no changes. See [Receive outbound webhooks](https://console.domino.run/docs/build/webhooks) for an optional polling migration.

### 1.0.0-next.26 [#100-next26]

#### Minor Changes [#minor-changes-73]

* 8c06912: Add operator reads and audited unlink for current X and Discord quest accounts. Reviewers provide the subject and selection revision to prevent a stale review from unlinking a newer connection. Identity ownership, sign-in grants and existing rewards remain unchanged. Console exposes the action on each member's Quest accounts section.

### 1.0.0-next.25 [#100-next25]

No changes in this release.

### 1.0.0-next.24 [#100-next24]

#### Major Changes [#major-changes-2]

* 2528cec: Every tracker trigger shares the admission rules of ADR 0006: enrollment, the earliest activity counted, and who releases historical activity, with skips decided again when the member event that clears them happens.

  * **Admission options on `automatic({ tracker })`.** `enrollment: "required"` counts only members who enrolled in the quest, and `ctx.tracker.enrolledAt` carries their enrollment time. `since` is `"activation"` (the default), `"window"`, or, with required enrollment, `"enrollment"`. `historical` is `"staff"` (the default), `"claim"` or `"automatic"`.
  * **Historical activity waits.** A record from before the member linked the account it pays, or with an unknown link time, is skipped as `historical` until a reviewer retries it, unless the quest sets `historical: "claim"` or `"automatic"`. Before, it was admitted. See [Tracker triggers hold activity from before an account was linked](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-triggers-hold-activity-from-before-an-account-was-linked).
  * **Skips are decided again by member events.** Linking an account, removing a conflicting link, joining the project and enrolling decide the matching skips again, through a durable workflow.
  * **Every selected observation has one outcome.** New skip codes `outside-window`, `future-record`, `already-completed`, `not-enrolled`, `before-enrollment` and `historical`; `TrackerSkipCode` is an open string, with `KnownTrackerSkipCode` for the codes this SDK knows. A prerequisite completed after the record occurred no longer counts for it.
  * **Participant APIs.** `relay.quests.enroll({ actionId, quest })`, `relay.quests.outcomes({ quest?, state?, code?, cursor? })` and `relay.quests.claim({ actionId, quest, tracker, record, observedAt })`, and `useQuestOutcomes()` in React. `relay.content.enroll` and `management.content.enroll` are replaced by `relay.quests.enroll` and `management.quests.enroll`. See [Enroll in quests with `relay.quests.enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#enroll-in-quests-with-relayquestsenroll).
  * **Console.** A tracker-triggered quest shows its enrollment, `since` and historical policy, a `not-enrolled` skip offers **Enroll the member**, and a member's claim shows as **Claimed**.

  **Who should act.** Apps that enroll members or read trigger outcomes, and quests whose members link accounts after the activity they expect to be paid for.

### 1.0.0-next.23 [#100-next23]

#### Major Changes [#major-changes-3]

* 2149973: Read published trackers with future source kinds without rejecting a tracker list. Preserve unknown source payloads and expose `isKnownTrackerSource` for supported-source narrowing. Tracker authoring remains closed. Update code that assumes every response source is supported as described in the [v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#handle-future-tracker-sources).

### 1.0.0-next.22 [#100-next22]

#### Major Changes [#major-changes-4]

* 4626004: Identity kinds are open. Besides `wallet`, `x`, `discord` and `member`, a custom tracker can declare your app's own kinds, `app:<name>` such as `app:telegram`, and records name your app's user IDs. An identity integration created with `identityKind: "app:<name>"` (set only at creation, kept by rotation) links each subject it signs in to the member as an identity of that kind, and tracker triggers attribute records through it, with `{ provider: "app:<name>", subject, revision, linkedAt }` as `ctx.tracker.attribution.account`.

  A kind belongs to the integration that first claimed it, also after a disconnect, which keeps its links attributing. A new integration takes a kind that has identities only by adopting them: `adoptIdentitiesFrom` (the old integration's ID, from the new `management.identityIntegrations.kinds()`) with a `reason` the audit log keeps; otherwise it is refused with 409. Reviewers unlink and relink a member's app identity (`management.members.unlinkIdentity` and `relinkIdentity`, and Console's **Tracker identities**), each audited with a reason: unlinking stops attribution, ends the member's sessions for that subject, and refuses its sign-in with 409 `APP_IDENTITY_UNLINKED`, which `createParticipantBackend` passes on, until it is relinked. A subject the integration already signs in as another member answers 409 `APP_IDENTITY_CONFLICT`.

  Evaluators read the member's selected X and Discord accounts and app identities with `ctx.member.identities()`, pinned once per attempt and `null` when there are none. Management member reads return them as `identities`, and Console lists them on the member under **Tracker identities**.

  Published tracker definitions in status, list, record-page, summary, and deployment responses use `trackerDefinitionResponseSchema` and `TrackerDefinitionResponse`. Authoring keeps the strict `trackerDefinitionSchema` and `TrackerDefinition`.

  Responses and evaluator contexts read identity kinds as an open set, `OpenIdentityKind`, and ignore fields they do not know, so a kind a later Relay adds no longer fails them.

  **Breaking:** `IdentityKind` is no longer a closed union, `identityKindSchema` is no longer a `z.enum`, response kinds are `OpenIdentityKind`, `trackerRecordSchema` is a separate response schema, `trackerAttributionSchema`'s account is a plain union with app and generic variants, and `ctx.member` is `{ id, identities() }`. See [Identity kinds are open, and evaluators read `ctx.member.identities()`](https://console.domino.run/docs/reference/upgrade-to-v1#identity-kinds-are-open-and-evaluators-read-ctxmemberidentities).

### 1.0.0-next.21 [#100-next21]

#### Minor Changes [#minor-changes-74]

* ddfc526: The participant client, the management client and the CLI share one set of deadline, cancellation and retry rules.

  * Each call has one deadline covering every attempt, the waits between them and the answer's body: 15 seconds for the participant client and 30 seconds for the management client and the CLI. `createRelayClient({ timeout })` and the new per-call `{ timeout }` option of every participant method set it, as `timeout` already does for the management client. A timeout is never retried.
  * A call is sent again, at most three times in all, only when the server applies a repeat at most once: reads; participant writes with an action ID, submission evidence by its upload ID, `wallets.verify` (by its challenge) and `wallets.challenge`, but not avatar uploads, whose upload ID has no receipt; typed management writes whose input has an `actionId`; and the CLI's `deploy` publication, `stage` build request and `quests archive`/`unarchive`. Every attempt sends the same payload and session. Retries follow a lost answer, including a body that breaks off after successful headers, and HTTP 500, 502, 503 and 504, with a short jittered backoff. Invalid or unexpected answers, cancellations, rejected input, other refusals and every other write are sent once.
  * A 429 is retried only when its `Retry-After` fits in the time left, and every wait honours `Retry-After`. The new `retryAfter` field of `RequestError` (and so `RelayError` and `ManagementError`) carries the wait the server asked for, in milliseconds; the CLI reports it as `details.retryAfter` on `RATE_LIMITED` and `SERVER_ERROR`.
  * A session-bound participant call that reads the current session first shares one deadline with that read, and `auth.session()` honours `timeout`.
  * An error status whose body breaks off, times out or is cancelled is now a `network`, `timeout` or `aborted` failure that keeps its `status` and `retryAfter`, in all three clients; an error body that is not JSON stays `http`.
  * Cancelling with `signal` stops a retry wait at once. A write any attempt of which may have been applied stays `uncertain`, with its `operationId` and `recovery`, whatever later attempts answer.
  * `management.request` retries only GET, and `management.raw` sends once. `useInvitation()` keeps a rate-limited code for a retry instead of discarding it as refused.

  Avatar failures report `recovery: "reconcile"` even when they have an upload ID. Wallet address and signature waits reject when the caller cancels. See [Bounded requests and avatar recovery](https://console.domino.run/docs/reference/upgrade-to-v1#bounded-requests-and-avatar-recovery) for the recovery-handler change.

  Generic management writes report `recovery: "reconcile"` even with a caller-supplied action ID. Uncertain `domino api` writes expose `details.recovery: "reconcile"`. See [Generic management writes require reconciliation](https://console.domino.run/docs/reference/upgrade-to-v1#generic-management-writes-require-reconciliation) for the recovery-handler change.

### 1.0.0-next.20 [#100-next20]

No changes in this release.

### 1.0.0-next.19 [#100-next19]

#### Patch Changes [#patch-changes-22]

* 1141af4: Fix invalid action IDs in the tracker and leaderboard documentation examples. Validate action ID literals during documentation checks so copied examples use the identifier format the API accepts.

### 1.0.0-next.18 [#100-next18]

#### Minor Changes [#minor-changes-75]

* 8465d40: Operators can correct a member's tiered reward, at the revision they saw and with an audited reason. `management.handover.cancel(entitlement, { revision, reason })` (`POST /management/v1/entitlements/<id>/cancel`, `handover` permission) returns a handover in progress to `available` and puts its pickup size back into stock. `management.entitlements.correctCollection` (`…/correct`, `review`) does the same for a collection recorded by mistake, and `management.entitlements.void` (`…/void`, `review`) voids an available entitlement. A stale revision fails with 409 `entitlement-changed`, the wrong starting status with 409 `entitlement-status`. API keys with the permission can make these corrections in live. Console Rewards and member detail offer them, and Audit history lists each one with its reason. `domino capabilities --json` lists them as `entitlement-corrections`.

  Breaking: `Entitlement` gains the status `"void"`, with `voidedAt`. A voided entitlement cannot be collected; a quest that grants the same reward to the member again makes it `available`, as a fresh grant at that quest's tier; a voided referral reward stays void. SDKs from before this release fail the whole progress read once a member has a voided entitlement. Reads now parse entitlements with a tolerant `entitlementSchema` (fields a newer server adds are kept, and an unknown status reads as `"unsupported"` with `reportedStatus`); the strict stored shape is `entitlementStateSchema`, and the management client's `entitlementRecordSchema` is removed in favor of `entitlementSchema`. To upgrade, see [Entitlements can be `void`, and reads tolerate statuses they do not know](https://console.domino.run/docs/reference/upgrade-to-v1#entitlements-can-be-void-and-reads-tolerate-statuses-they-do-not-know).

### 1.0.0-next.17 [#100-next17]

#### Major Changes [#major-changes-5]

* c919948: Follow-ups to the React hooks' shared query layer.

  * `completionCountLabel(view)` is renamed `completionCountText(view)`, so the name says it returns English like `questStatusText`. For your own words, read `view.completionCount`.
  * `questStatus(view)` returns `QuestStatus`, whose `code` is the open `QuestStatusCode`, and `roleDeliveryStatus(deliveries, quest)` returns the open `RoleDeliveryStatus | null`. A `switch` that lists every code needs a `default`, so a newer SDK can add codes without breaking the signature.
  * `useReferralSummary({ intervalMs })` reads the summary again every 60 seconds by default while the page is visible, so friends who qualify show up without an app-side timer.
  * `useSignOut()` drops the signed-out member's cached values as soon as Relay confirms the sign-out, even when the next session read fails.
  * A `useMyProfile()` change that answers after another member signed in no longer writes into the new member's cache.
  * `useSignIn`, `useSignOut`, `useSignInOptions`, `useInvitation` and `useQuest` return objects and actions that keep their identity until something in them changes; `useQuest`'s actions act on the latest view. `useExperience().deliveries` is a `readonly RoleDelivery[]`, and keeps one frozen empty list while there are none.
  * `progressOf(quests)` is documented: `total` counts locked and paused quests and leaves out archived and repeatable ones.

  Upgrade: [React hooks share one cache and return one state shape](https://console.domino.run/docs/reference/upgrade-to-v1#react-hooks-share-one-cache-and-return-one-state-shape) in the SDK v1 upgrade guide.

### 1.0.0-next.16 [#100-next16]

#### Major Changes [#major-changes-6]

* bdb89a4: Evaluator readers follow one rule: a missing subject is `null`, a failure throws `CapabilityError`, linked accounts are pinned once per attempt, and tracker reads are point in time. This applies to every evaluation Domino runs, including releases published with earlier SDK versions.

  * **Missing is `null`.** `ctx.x.profile()`, `follows()` and `reposted()` return `null` without a linked X account instead of throwing; `ctx.x.post()` returns `null` for a post that does not exist and needs no linked account. `ctx.discord.member()` returns `null` without a linked Discord account. `ctx.wallet.list()` returns `null` instead of `[]`, and `ctx.wallet.balance()` returns `null` instead of `total: "0"` when the member has no wallet of that family. `ctx.trackers.content()` returns `null` for a missing record or observation. See [Readers return `null` when an account is not linked or a subject is missing](https://console.domino.run/docs/reference/upgrade-to-v1#readers-return-null-when-an-account-is-not-linked-or-a-subject-is-missing).
  * **Failures throw `CapabilityError`**, new in `@domino-sdk/relay/authoring` (its wire shape, `capabilityFailureSchema`, is in `@domino-sdk/relay/schema`), with `code` (`"not-linked"`, `"provider-unavailable"`, `"author-error"`) and `retryable`. Invalid capability input is `author-error`; an image check of `{ x: "profile" }` without a linked X account is `not-linked`. Failures that are not retryable are captured and replay. A retryable failure that quest code handles and moves past is final for the attempt; one that escapes the evaluation is released, along with every fact captured after it, and retried, so the retry asks the provider and the later calls again. Rethrow a caught failure, or wrap it with `{ cause: error }`, to let it be retried. The failure shape is open: a newer Relay can add codes, and the SDK treats an unknown code as not retryable. See [Capability failures throw `CapabilityError`](https://console.domino.run/docs/reference/upgrade-to-v1#capability-failures-throw-capabilityerror).
  * **Tracker reads are point in time.** `ctx.trackers.records()` and `summary()` take `asOf`, defaulting to `ctx.eligibleAt`, and every tracker read of an attempt sees the observations stored at its first tracker read. Pass `asOf: ctx.now` to count everything stored by then. See [Tracker reads are point in time and pinned per attempt](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-reads-are-point-in-time-and-pinned-per-attempt).
  * **Linked accounts stay pinned.** X and Discord readers pin the account at the attempt's first read and never recheck it, as wallets already did, so a link change no longer fails the attempt with "X account changed" or "Account or integration changed". See [Linked X and Discord accounts stay pinned for the attempt](https://console.domino.run/docs/reference/upgrade-to-v1#linked-x-and-discord-accounts-stay-pinned-for-the-attempt).

  **Who should act.** Evaluators that read `ctx.x`, `ctx.discord`, `ctx.wallet` or `ctx.trackers`, or that match capability error messages. The changed return types point TypeScript at each read to update. The `discord-membership`, `token-holder` and `community-screenshot` snippets and the Discord starter's `join-the-server` quest are updated.

### 1.0.0-next.15 [#100-next15]

#### Major Changes [#major-changes-7]

* e1cba88: Observations are removed, and every automatic activity arrives as a tracker record. `automatic({ source, type })`, `ctx.observation`, `management.observations.submit`, `POST /management/v1/observations`, the `Observation` type, `observationSchema`, `observationInputSchema`, `observationReceiptSchema`, `observationTriggerSchema`, `discordReactionSchema`, `QuestView.trigger.observation` (and the release trigger's `observation`) and the `observation` field of the `automatic` availability are gone. `questStatus` and `runsSilently` from `@domino-sdk/relay/react` now label every tracker-triggered quest "Automatic", not only observation and keyed quests. A server that reported observations sends records to a custom tracker with `management.trackers.ingest` and triggers quests with `automatic({ tracker })`. Ingestion never joins a member to a project: a tracker trigger skips a member who has not joined with `not-joined`. Quests published with an observation trigger leave the active set when Relay upgrades. Republish them from code on a tracker; the quest key can stay the same.

  Discord messages and reactions are two managed tracker sources, `discord.messages()` (with `{ text: true }` to store message text) and `discord.reactions()`, on the `discord` object from `@domino-sdk/relay/authoring`. Domino's Discord gateway writes each new message or reaction into every tracker of that kind in the projects connected to the server, and keeps a record only when one of its accounts is a Discord account a member has linked. Their record data is typed (`DiscordMessageData`, `DiscordMessageTextData`, `DiscordReactionData`), so `where` filters and `ctx.tracker.record.data` are checked. A reaction record names the message's author as the `messageAuthor` account: `automatic({ tracker: reactions, beneficiary: "messageAuthor" })` replaces `reaction.received`, and Relay skips reactions to one's own message with `beneficiary-is-author`. Discord trackers refuse ingestion (403) and refresh (422). Streaks work on these triggers.

  The `observe` permission is removed. Ingesting needs `ingest`, and enrolling a member from a server (`management.content.enroll`) needs the new `enroll` permission. Editors hold `read`, `publish`, `inventory`, `points`, `ingest` and `enroll`; hosted-app keys hold `read`, `points` and `ingest` and cannot enroll. A personal API key issued with `observe` keeps its other permissions, and `observe` grants nothing: reissue it with **Ingest** or **Enroll**. The `observation-quests` capability is removed from `capabilities.json`; Discord tracker triggers are part of the `trackers` capability.

  To upgrade, see [Observation triggers become tracker triggers](https://console.domino.run/docs/reference/upgrade-to-v1#observation-triggers-become-tracker-triggers) and [The `observe` permission is replaced by `enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#the-observe-permission-is-replaced-by-enroll).

### 1.0.0-next.14 [#100-next14]

#### Major Changes [#major-changes-8]

* c2390b3: The React hooks in `@domino-sdk/relay/react` share one query cache and return one state shape.

  * Every read hook returns `{ status, data, error, refresh }`: `status` is `loading`, `ready` or `error`, plus `signed-out` for hooks that need a member; `data` holds the value; `error` is always a `RelayError`, and while `ready` it says why the latest refresh failed and the last value stays. `useMe` and `useSignInMethods` return this shape instead of a bare value, `useMe` no longer swallows errors, and the profile hooks report the uniform 404 as `error` with status 404 instead of `missing`.
  * `<RelayProvider>` keeps one keyed cache. Components that read the same thing share one request. A read that is replaced, invalidated, or belongs to a previous participant or client is cancelled, and its late answer is dropped. Polling, including the quest experience observer and the Discord role-delivery watcher, pauses while the tab is hidden and catches up when it is shown. Reads wait for the first session read, public ones included, and a sign-in attempt that does not change the session drops nothing.
  * Changes invalidate what they affect: every new completion the experience shows, submitted or automatic, reads balances, point history, leaderboards, profiles and referral progress again; an accepted invitation, the referral summary and referral progress; wallet, account and profile changes, their own reads. The `PROFILE_CHANGED` window event is removed. `useInvalidate()` and `relayKeys` invalidate by key prefix after a change made with `useRelay()`.
  * The cache is scoped to the client, organization, project, environment, member and session. Signing in or out or switching accounts, projects or environments drops it, so one member never sees another's cached values. A new `client` or `baseUrl` passed to `RelayProvider` now takes effect: the old observer stops, reads in flight are cancelled, and the hooks start from an empty cache.
  * `useSession()` is new: who is signed in, independent of the experience read, so a failing `/me/experience` no longer blanks the profile, wallet, account and `useMe` hooks. `useExperience()` returns the shared shape with `data.quests` and `data.collections` and `deliveries`; its `member` moved to `useSession()`. `Experience` is replaced by `ExperienceData`, which `questGroups` takes.
  * `useBalances()` returns the `{ items }` answer in `data`, and `usePointHistory(ledger)` returns `data.items` with `loadMore()`; an unknown ledger or a member who has not joined is `error` with code `point-ledger-not-found` or `not-joined` instead of the `missing` and `not-joined` statuses. `BalancesState` and `PointHistoryState` are replaced by `MemberQuery` and `PagedQuery`.
  * `useRelayQuery`, `useMemberQuery`, `useRelayPages` and `useMemberPages` put an app's own reads on the same cache. `Page` is the root package's type; `PagedQuery` (public pages) and `MemberPagedQuery` (member pages) add `loadMore`, `loadingMore` and `loadMoreError`, which belong to one key and participant.
  * Every error the hooks report is a `RelayError`: `useQuest().error`, `useSignIn().error`, `useSignInOptions().error` and `methodsError`, `SignInMethods.error` (also from `signInOptions()` in `@domino-sdk/relay/browser`), and a rejected or failed invitation's `error`, which replaces `reason`.
  * `completionMark`, `hasPending`, `sanitizeDeliveries`, `watchRoleDeliveries`, `PENDING_POLL_MS`, `RETRY_DELAYS_MS`, `runsSilently` and `visibleQuests` are no longer exported from `@domino-sdk/relay/react`.
  * `questStatus(view)` returns `{ tone, code, reason? }`, a stable code to localize (`QuestStatusCode`, an open string like `RelayErrorCode`); `questStatusText(view)` adds the English `label` and `detail`. `roleDeliveryStatus(deliveries, quest)` returns `pending`, `failed`, `granted` or null, and `roleDeliveryLabel(status)` gives its English label.

  Upgrade: [React hooks share one cache and return one state shape](https://console.domino.run/docs/reference/upgrade-to-v1#react-hooks-share-one-cache-and-return-one-state-shape) in the SDK v1 upgrade guide maps every hook.

### 1.0.0-next.13 [#100-next13]

#### Major Changes [#major-changes-9]

* 23c85ba: `defineEngagementReward` takes `budgets`, a list of `{ scope: "project" | "member", period: "day" | "week" | "month" | "lifetime", limit }`, instead of `budget: { projectDaily, memberDaily, projectLifetime }`. Periods are those of the project calendar at settlement time, so a rule can now cap weekly or monthly payouts, and accounts settle once per project calendar day instead of once per UTC day. A definition with `budget` fails validation; rules already published keep their limits in the new form.

  See [Engagement budgets are a list of periods](https://console.domino.run/docs/reference/upgrade-to-v1#engagement-budgets-are-a-list-of-periods) in the v1 upgrade guide.

* 23c85ba: Each project environment has one project calendar: a timezone, UTC until you choose one, that sets where every day, week and month begins. Streak days, quest budgets, engagement budgets and calendar leaderboard windows all follow it, so a weekly budget and a weekly leaderboard cover the same week.

  * Set it with `"calendar": { "timeZone": "Europe/Budapest" }` in relay.json, in Console **Settings → General → Project calendar**, or with the new `management.calendar.read()` and `management.calendar.configure({ timeZone, reason })` (`GET` and `POST /calendar`, `publish` permission; in live only from Console). A change takes effect at the next midnight in the current timezone: the open day, week or month finishes in the old zone, and streaks stay consecutive. A deployment applies relay.json's timezone only when it differs from the last one deployed, and the deployment preview's new `calendar` field shows the change it schedules.
  * `streak()` and calendar leaderboard windows no longer take a `timeZone`; both throw if given one. Streaks and boards already published with one still read.
  * Budgets reset at midnight in the project timezone instead of UTC, and accept `period: "month"`.
  * One validator, `timeZoneSchema`, accepts named IANA zones only. Fixed offsets such as `+02:00` and `Etc/GMT-2` are rejected, including in leaderboards, which used to accept them. The shared `calendarPeriodSchema` is `lifetime | day | week | month`.

  **Who should act.** Projects that pass `timeZone` to `streak()` or a calendar leaderboard window, and projects that rely on budgets resetting at UTC midnight. See [Set the timezone once, on the project calendar](https://console.domino.run/docs/reference/upgrade-to-v1#set-the-timezone-once-on-the-project-calendar) in the v1 upgrade guide.

### 1.0.0-next.12 [#100-next12]

#### Minor Changes [#minor-changes-76]

* 710e520: Participants read their own point balances and history. `relay.me.balances()` (`GET /v1/me/points`) returns `{ items }`: the signed-in member's `balance` and `available` points (after reservations) in every ledger of the project, each with its `ledger` reference. `relay.me.pointHistory({ ledger, cursor? })` (`GET /v1/me/points/:ledger/history`) pages through the member's own entries in one ledger, by key or ID, most recently recorded first, as `{ items, nextCursor }`. Each entry is `{ id, amount, kind, at, quest, reverses }`, without the operator's reason or actor. `kind` is an open string; the `PointEntryKind` constants (`award`, `spend`, `adjust`, `reverse`) name the kinds this SDK knows, and referral payouts are `adjust`. An unknown ledger answers 404 with code `point-ledger-not-found` and a member who has not joined the project 404 with `not-joined`, both added to `RelayErrorCode`. Neither read takes a member, and both are scoped to the session's project and environment. React apps use `useBalances()`, which reads again after a quest completes, and `usePointHistory(ledger)`. The participant proxy and identity integrations admit both routes. New schemas and types: `pointBalanceSchema`, `pointBalancesSchema`, `pointEntrySchema`, `pointHistoryQuerySchema`, `pointHistoryPageSchema`, `PointBalance`, `PointBalances`, `PointEntry`, `PointEntryKind`, `KnownPointEntryKind`, `PointHistoryQuery` and `PointHistoryPage`.

  The classic starter's header now reads the balance with `useBalances()` instead of its `/api/me/points` server route, which is gone.

### 1.0.0-next.11 [#100-next11]

#### Minor Changes [#minor-changes-77]

* f2f1b75: Breaking: the management client moves from `@domino-sdk/relay-cli` to the new `@domino-sdk/relay/server` entrypoint, with `createManagementClient`, `hostedManagementClient`, `ManagementError`, the management schemas and types, and `starterTemplates`. Server code, including a hosted app's Worker, now needs only `@domino-sdk/relay`, without the CLI's command-line parser and TypeScript compiler. `@domino-sdk/relay-cli` is only the `domino` executable and its agent skills; it has nothing to import. `serverBoundary()` also fails a browser build that imports `@domino-sdk/relay/server`. To upgrade, see [Import the management client from `@domino-sdk/relay/server`](https://console.domino.run/docs/reference/upgrade-to-v1#import-the-management-client-from-domino-sdkrelayserver).

  The capability catalog (`domino capabilities --json`) adds staff confirmation, staff handover, stock, point corrections, deploy and stage, operator member reads and lookup, project access, and the management API with `domino api`. It marks webhooks, bulk data export, completion revocation and tracker-scoped keys as unsupported, and lists the management methods it leaves out on purpose in `excludedManagementMethods`.

#### Patch Changes [#patch-changes-23]

* 909f343: The browser submission store recovers from storage failures instead of caching them. A temporary IndexedDB opening error, a blocked open, a `versionchange` from another tab, or a connection the browser closes no longer fails every later call on the same client: the next `status()`, `resume()` or `submit()` opens storage again, and the saved operation and its operation ID are kept. A blocked open waits briefly for the other tab, then rejects with `submission-storage`, and the pending request is adopted if it later succeeds. Full browser storage rejects with the new `RelayErrorCode.SubmissionStorageQuota` (`submission-storage-quota`); an upgrade by a newer page asks for a reload. Both are `client` errors that are not `uncertain`, and a failed read never looks like an empty store, so a new claim is never started over a saved one. See [Storage failures](/docs/reference/participant-api#storage-failures) in the participant API reference.

### 1.0.0-next.10 [#100-next10]

No changes in this release.

### 1.0.0-next.9 [#100-next9]

#### Minor Changes [#minor-changes-78]

* cf5b02c: Custom tracker records have a declared data schema. `customSource(namespace, { data })` requires a zod object schema (`z` is exported by `@domino-sdk/relay/authoring`), stores its JSON Schema form with the tracker, and throws, naming the field, for what ingestion cannot check (string formats and patterns, tuples, refinements, transforms, dates, bigints). Publication refuses a custom tracker without one. Ingestion rejects a batch whose record data does not match with 422 and `{ error, code: "invalid-record-data", record, path, reason }`, one too costly to validate with `record-data-too-complex`, and records for a tracker published without a schema with `tracker-schema-required`. Schemas are bounded (32 KB, 16 levels, 256 properties, required keys, enum values or key names per node, 16 branches per union, no wide union nested in another union's branch), and validation charges every unit of work to a per-record and per-batch step budget. A deployment whose new schema breaks a live quest's `where` filter, Console quests included, is refused. Stored records are not rechecked: tracker triggers skip non-matching records with the new skip code `invalid-record-data`, evaluators reject pinned records that do not match the schema the quest was built with, and `trackers.records(handle)` leaves them out and counts them in `excluded`. The schema can change under the same tracker key and applies to later records.

  The data type flows into quests: `ctx.tracker` is non-null in a tracker-triggered quest's evaluator with `record.data` of the tracker's type (and `ctx.content` and `ctx.observation` are non-null for the triggers that supply them, `null` otherwise), `trackers.records(tracker)` returns typed records, and `where` paths, `equals` values and `{ setting }` bounds are type-checked against the data and the quest's integer settings. `where` also accepts a list of conditions that must all hold, and `automatic()` and publication check filter paths against the stored schema. `settings.object` and `defineQuest` accept only `settings.*` builders; a plain zod setting is a type error and `defineQuest` throws `Setting "<key>" is not a quest setting`.

  See [Custom sources declare their record data](/docs/reference/upgrade-to-v1#custom-sources-declare-their-record-data) and [Quest settings come only from settings builders](/docs/reference/upgrade-to-v1#quest-settings-come-only-from-settings-builders) in the upgrade guide.

### 1.0.0-next.8 [#100-next8]

#### Major Changes [#major-changes-10]

* 54fcf41: The participant client follows one convention for paging, sessions and errors.

  * Every method takes an optional last argument, `{ session?, signal? }`. `session` replaces the positional session argument of `wallets.challenge`, `wallets.verify` and `me.collectionPass`. Calls Relay binds to a session (wallet linking, collection passes, `quests.retry`) read the current session when `session` is omitted.
  * Every paged read takes `{ cursor? }` and returns `{ items, nextCursor }`: `referrals.history`, `content.admissions`, `content.qualifications`, `content.performance`, `engagement.page` and `engagement.boundaries` changed from `before`, `after` and `next`. The participant routes answer `nextCursor` and read `cursor`, and so do the management routes and client methods that share them (`management.referrals.history(member, { cursor })`, `management.content.*`, `management.engagement.page` and `boundaries`).
  * Every rejection is a `RelayError`, which extends the new shared `RequestError`: `kind` (`http`, `network`, `timeout`, `aborted`, `invalid-response`, `client`), `uncertain` (true only for a write that may have taken effect: a failure after sending, a cut-off body, a 500, 502 or 504, or an unreadable success answer), `status` whenever response headers arrived, an open string `code` (known ones in `RelayErrorCode`), `operationId`, and `recovery` (`replay` with an `operationId`, else `reconcile`) when uncertain. `isRelayError` and `isRequestError` recognize these errors across duplicate copies of the package. `RelayError.status` is now `number | undefined`. Invalid input rejects with `invalid-input` instead of a `ZodError`, a success answer this SDK cannot read with `invalid-response`, a body cut off after its headers with kind `network` and its status, and a wallet signer's own failure with `wallet-signer`. The error body is read field by field, so Relay's message and a code the SDK does not know are always kept. Relay now sends `code: "not-signed-in"` and `code: "stale-session"` on those participant errors, and every paged read answers a malformed cursor with 400, code `invalid-cursor`, and one message.
  * Removed: `submit`, `retry(id)`, `progress(member)`, `upload(member, file)`, `auth.connections`, `me.linkPass`, the `wallet.*` namespace, and the staff calls the participant API refused (`lookupMember`, `snapshot`, `publish`, `publishBatch`, `beginHandover`, `confirmHandover`, `decide`, `journeyTemplate`, `template`).
  * Parameters take Zod input types, so defaulted fields are optional, and cursors are opaque strings the server validates. The session the client returns (`ParticipantSession`) accepts fields and principal kinds a newer Relay adds; `sessionMember(session)` reads its member.
  * Hosted proxies no longer forward the 0.x single-wallet routes (`/v1/me/wallet`, `/challenge`, `/verify`).

  Upgrade: [One paging, session and error convention in the participant client](https://console.domino.run/docs/reference/upgrade-to-v1#one-paging-session-and-error-convention-in-the-participant-client) in the SDK v1 upgrade guide maps every method and has search commands.

### 1.0.0-next.7 [#100-next7]

#### Major Changes [#major-changes-11]

* 1084288: `@domino-sdk/relay` now exports only the participant runtime client: `createRelayClient`, `RelayError`, `QuestUpdatedError`, `formatUnits`, `parseUnits`, and the types the client returns. Schemas, builders and avatar helpers moved to their own entry points, a few implementation helpers are no longer public, and two are no longer exported at all, so an app that imports the client no longer loads authoring builders, deployment and management schemas, or DiceBear.

  * Zod schemas (and helpers such as `identifier`, `caip10` and `xPostId`) come from `@domino-sdk/relay/schema`.
  * Builders (`defineTracker`, `defineLeaderboard`, `defineReferral`, `defineEngagementReward`, `content`, `perContent`, `streak`, `settings`, `discord`, `pointLedgers`, `x`, `customSource`) and evaluator reader types (`TrackerReader`, `XReader`, `ImageReader`) come only from `@domino-sdk/relay/authoring`.
  * `avatarSrc`, `defaultAvatar`, `defaultAvatarSvg` and `walletAvatarSeed` come from `@domino-sdk/relay/avatar`, and no longer from `@domino-sdk/relay` or `@domino-sdk/relay/react`.
  * No longer public: `identityIntegrationSchema`, `identityIntegrationInputSchema`, `identityIntegrationSecretSchema`, `participantExchangeSchema`, `questConfiguration`, `parseImageResult`, `parseImageExtraction`, `isContentParticipantRoute`, `isProfileParticipantRoute`, `isWalletParticipantRoute` and `isWalletSignInRoute` are exported only from `@domino-sdk/relay/internal`, which Relay's own services share and which is not a supported API.
  * No longer exported: `questActions` and `reconcileQuestDraft`. Open a quest controller with `relay.quests.open(view)`; its `quest-updated` result already carries the reconciled `draft`.
  * The package declares `sideEffects: false`.
  * `esbuild` is an optional peer dependency, needed only by `@domino-sdk/relay/build`. `@domino-sdk/relay-cli` depends on it directly, so `domino build` and `domino deploy` work as before. Code that calls `buildQuest` without the CLI must install `esbuild`.

  Upgrade: [Runtime entry exports only the client](https://console.domino.run/docs/reference/upgrade-to-v1#runtime-entry-exports-only-the-client) in the SDK v1 upgrade guide has the import mapping and search commands.

### 1.0.0-next.6 [#100-next6]

#### Patch Changes [#patch-changes-24]

* 8b706af: An attempt now records when its evaluator asked for human review on a quest with review off. `decision.review({ reason })` was already a rejection with the review reason in that case. The attempt now also has `requestedReview: true`, and Console attempt detail says "Would have gone to review; human review is off". A reject the evaluator returned itself keeps `requestedReview: false`. The `review` option, the quests concept page and the authoring reference describe it.

### 1.0.0-next.5 [#100-next5]

No changes in this release.

### 1.0.0-next.4 [#100-next4]

#### Minor Changes [#minor-changes-79]

* d7f9fe4: `streak(...)` now completes manual quests and tracker-triggered quests (`automatic({ tracker })`), not only automatic observation quests. Each accepted attempt counts the community day of its trusted occurrence time, the time `ctx.eligibleAt` reports: a tracker record observation's `occurredAt` (such as an X post's publication time), or, for a manual quest, when Relay admitted the submission, so a manual streak counts submission days. `defineQuest` no longer throws "Streaks require an automatic observation trigger" or "Tracker triggers complete once() or keyed()", and publication accepts these quests. Content quests and prerequisite-only `automatic()` quests still cannot be streaks, and a streak quest keeps its trigger kind (manual, observation or tracker): changing it needs a new quest key. Once a member's tracker streak milestone pays, the trigger stops admitting that member's observations for the quest, as for a completed `once()` quest. `questStatus` shows a member's own streak submission first while it is checking, in review or failed; a rejected submission keeps the "N / M days" progress and gives the reason as its detail. The new `packages/sdk/examples/posting-streak-quest.ts` pays members who post on X five days in a row. Nothing existing needs to change, so the v1 upgrade guide has no section for this.

### 1.0.0-next.3 [#100-next3]

#### Major Changes [#major-changes-12]

* a803b30: Evaluators now replay deterministically. Hosted evaluation freezes the clock for each attempt and seeds randomness from the attempt, so a retried or replayed attempt makes the same decision and `keyed()` completion key. This applies to every evaluation Domino runs, including releases published with earlier SDK versions.

  * `ctx.now` is new: the attempt's evaluation time in epoch milliseconds, the same on every run of the attempt. `keyed()` receives it too. It is distinct from `ctx.eligibleAt`, which is when the activity happened.
  * `Date.now()`, `new Date()` and `Date()` return `ctx.now` and do not advance, even across awaited capability calls. `performance.now()` returns 0.
  * `Math.random()`, `crypto.getRandomValues()` and `crypto.randomUUID()` repeat the same sequence for one attempt and differ between attempts.

  **Who should act.** Evaluators that measure elapsed time or rely on unpredictable random values. Use `ctx.now` for "as of evaluation" checks and `ctx.eligibleAt` for when the activity happened. See [Evaluators see a frozen clock and seeded randomness](https://console.domino.run/docs/reference/upgrade-to-v1#evaluators-see-a-frozen-clock-and-seeded-randomness) in the v1 upgrade guide.

#### Patch Changes [#patch-changes-25]

* 7e37049: The SDK's optional React peer dependency is now `^19.0.0` instead of `>=19`, matching the React versions the React hooks are verified against. React is still optional: the runtime, browser and server entrypoints do not need it. Apps on React 19 need no change.

### 1.0.0-next.2 [#100-next2]

#### Minor Changes [#minor-changes-80]

* c320586: A content quest's `performance.evaluate` can no longer select rewards. It gates the rewards the content evaluation already selected, and it always did: a `rewards` value in its result was silently ignored. Its type is now `PerformanceOutcome` (`accept`, `review` or `reject` with a reason only), so `decision.accept({ rewards })` and `decision.review({ reason, rewards })` are type errors there, and a runtime result that carries `rewards` fails the evaluation with "A performance evaluator cannot select rewards". To change what a post earns, select rewards in the quest's `evaluate`.

  See [Performance evaluators no longer select rewards](/docs/reference/upgrade-to-v1#performance-evaluators-no-longer-select-rewards) in the upgrade guide.

* c320586: A prerequisite is the quest key alone. `completed(quest)` returned `{ quest, scope: "ever" }`, and `scope` was declared but never read: a prerequisite has always meant a required quest completion from any prior occurrence, with no same-period variant. `completed()`, the quest description schema (`requires[]`) and published releases no longer carry `scope`. Releases stored with it still read, and the field is dropped. Code that reads `requires[].scope`, or builds `{ quest, scope: "ever" }` by hand, should use `{ quest }` or `completed(quest)`.

  See [Prerequisites are typed eligibility rules](/docs/reference/upgrade-to-v1#prerequisites-are-typed-eligibility-rules) in the upgrade guide.

#### Patch Changes [#patch-changes-26]

* c320586: `defineQuest` throws at build time for two shapes that only failed when publishing: a bare `automatic()` quest with no `requires` entry, and a bare `automatic()` quest with `keyed()` completion. A bare `automatic()` runs once its prerequisites are complete, so it needs at least one `completed(quest)` and completes once. The backend still checks both when it publishes.
* c320586: `manual({ actor: "staff", input })` throws instead of dropping the actor. Its types never allowed the combination, but untyped callers got a quest that members could submit evidence to, not one staff confirm. Staff-captured evidence is not supported: use `manual({ actor: "staff" })` for a staff confirmation, or `manual({ input })` for evidence the participant submits.

### 1.0.0-next.1 [#100-next1]

No changes in this release.

### 1.0.0-next.0 [#100-next0]

#### Major Changes [#major-changes-13]

* 5d4ec3e: Start the SDK v1 prerelease. Relay SDK and CLI v1 is one breaking release, 1.0.0; until it ships, breaking changes publish as `1.0.0-next.N` on the npm `next` dist-tag. `latest` stays on the 0.x release, so `npm install @domino-sdk/relay` and new starters keep using 0.x.

  **Who should upgrade.** Install `@domino-sdk/relay@next` and `@domino-sdk/relay-cli@next` together only to try v1 before it is stable. Prereleases can break each other: pin an exact `1.0.0-next.N` version with `npm install --save-exact @domino-sdk/relay@next @domino-sdk/relay-cli@next` (or `pnpm add -E @domino-sdk/relay@next @domino-sdk/relay-cli@next`), and upgrade the SDK and CLI to the same version.

  **Upgrade steps.** Every breaking change in the prerelease adds a section to the SDK v1 upgrade guide in the Domino docs, and its changelog entry links to that section. Once 1.0.0 is published, stable APIs follow semantic versioning; experimental APIs are only in namespaces marked experimental.

### 0.31.1 [#0311]

No changes in this release.

### 0.31.0 [#0310]

#### Minor Changes [#minor-changes-81]

* 3d0bee0: Tracker triggers are composable primitives: a firing mode, an optional data filter, and the account role a quest pays, over records whose accounts can be any verified identity.

  * `automatic({ tracker, on?, where?, beneficiary? })`. `on` is `"new-record"` (default for X trackers), `"new-observation"` (default for custom trackers) or `"data-changed"` (a new record, or `data` that differs from the previous observation). `where` is one condition on a path in the record's `data`: `{ path, equals }`, or `{ path, gt?, gte?, lt?, lte? }` comparing a number or an integer string exactly, with bounds that are numbers or `{ setting: "name" }` (an integer setting of the quest). Observations the mode or filter does not select record nothing; `ingest` counts those the filter dropped in `admissions.filtered`, present when nonzero.
  * `beneficiary` is an account role: `"author"` (default), `"inReplyTo"` on X trackers, or a role a custom source declares. A record never pays its author's member through another role: when the beneficiary account is the author's, or both belong to the same member (the same wallet on another chain, or another account the member linked), the observation is skipped with `beneficiary-is-author`.
  * `customSource(namespace, { identities, accounts? })`: record authors and named accounts are verified identities of the listed kinds, `"wallet"`, `"x"`, `"discord"` or `"member"`. Records carry `author: { id, kind? }` and `accounts: { <role>: { kind?, id } }`; `kind` may be left out when the source lists one kind. Record reads return them as ingested, and the records `member` filter matches authors of every declared kind. The ingesting server names the accounts, so keep ingest keys on trusted servers.
  * Trigger outcomes carry a stable skip `code` (`trackerSkipCodes`) beside the human `reason`, and `trackers.outcomes` filters by `code`. An X post without an author is no longer reported as "Post is not a reply".
  * `triggerMode(policy)`, `beneficiaryRole(policy)` and `sourceIdentities(source)` read the mode, role and identity kinds of published policies and trackers, including those stored by earlier SDKs.

  **Compatibility.** Existing data keeps its stored shape on the wire, so SDKs and CLIs 0.25 to 0.27, and backend pods of the previous release during a rollout, keep reading it. New keys appear only where an author uses a new option:

  * `automatic()` leaves `on` out when it is the source's default, so default quests publish the same policy as before. `on` (non-default), `where`, roles other than `author` and `inReplyTo`, `identities` on new trackers, `accounts`, `author.kind`, and `discord` or `member` attribution are rejected by older SDKs, whose schemas are strict: upgrade apps that read those trackers and quests.
  * Trackers published with `authors: "wallet"` read back as stored. Republishing one as `identities: ["wallet"]` is the same tracker: it keeps its key and stored definition.
  * Reply quests published with `beneficiary: "replied-to"` keep it: reads return it as stored, and their evaluators keep receiving `beneficiary: "replied-to"`, so they keep working until republished. Republishing with this SDK switches them to `inReplyTo`.
  * Trigger outcomes always carry `code`: `trackers.outcomes` needs this SDK.

  **Breaking changes.** For code written against the tracker triggers of 0.25.0 to 0.27.0:

  * Replace `customSource(namespace, { authors: "wallet" })` with `customSource(namespace, { identities: ["wallet"] })`. `authors` stays readable on published trackers and is deprecated.
  * Replace `automatic({ tracker, beneficiary: "replied-to" })` with `beneficiary: "inReplyTo"`.
  * `TrackerBeneficiary` and `trackerBeneficiarySchema` are removed; the beneficiary is a role string, or `"replied-to"` on releases published before roles.

  **Upgrading.** Deploy the backend (migration `0074_tracker_trigger_primitives`) before publishing quests that use the new options; older servers refuse them.

### 0.30.0 [#0300]

#### Minor Changes [#minor-changes-82]

* c6afc57: The EVM chains `ctx.wallet.balance()` reads are the platform's list, not an SDK export. Adding a chain no longer needs an SDK release.

  * `WalletBalanceQuery.chain` for EVM is any CAIP-2 ID, typed `` `eip155:${number}` `` (new `EvmChainId` type). New `isEvmChainId(value)` (also from `@domino-sdk/relay/authoring`) and `evmChainIdSchema` check the syntax (`eip155:` and a chain ID in decimal, no leading zeros); the evaluator sandbox refuses anything else before reading.
  * The platform decides which chains it reads: the EVM networks `management.walletBalanceChains()` lists, generated from dRPC's chain list. On any other EVM chain the evaluation fails with 422 "Relay cannot read balances on eip155:<chain ID>: it is not a supported EVM chain. …".
  * New `management.walletBalanceChains()` (`GET /management/v1/wallet-balance-chains`, `read` permission) returns `{ chains: [{ chain, name, testnet }] }`: Solana mainnet, then each EVM chain the platform reads, by chain ID. Console names the chains of a member's balance checks from it.

  **Breaking.** `evmChains`, `evmChainSchema`, `EvmChain` and `EvmChainInfo` are no longer exported from `@domino-sdk/relay`.

  **Upgrading.** Quest code that passes a string literal such as `"eip155:8453"` compiles unchanged. Replace `EvmChain` with `EvmChainId`, `evmChainSchema` with `evmChainIdSchema` (it checks syntax, not support), and chain-name lookups in `evmChains` with `management.walletBalanceChains()` from your server.

* c6afc57: `ingest` is a normal management permission: it sends records to custom trackers (`management.trackers.ingest`) and allows nothing else.

  * Editors hold `ingest`, and organization administrators hold every permission, so `management.access()` lists it for them. `rolePermissions.editor` includes it.
  * Personal API keys can carry it. In Console, **Settings → Keys → Personal API keys** now lets you choose a new key's permissions (all of yours by default) and lists each key's permissions; select only **Ingest** for an indexer. `POST /management/v1/tokens` accepts `ingest` for callers who hold it.
  * Hosted-app keys (`dhk_`) now list `ingest` in the minted key, the development key and `access()`, alongside `read` and `points`. They always held it.
  * `observe` still allows ingestion, so keys issued with only `observe` keep ingesting. A caller with neither gets 403 "Missing ingest permission" (before: "Missing observe permission").
  * `domino dev` needs your `read`, `points` and `ingest` permissions for its development key; editors have all three.
  * `tokenSchema` (the personal API key list) drops permissions this release does not know instead of rejecting the list, as `access()` already does.

  **Upgrading.** The `domino` CLI reads `/access` and its development key without checking permission names, so every CLI version keeps working. Code that calls `management.access()` from `@domino-sdk/relay-cli` before 0.25 rejects a response that lists `ingest`, which now happens for every editor and administrator: upgrade `@domino-sdk/relay-cli` there. 0.25 and later drop permissions they do not know.

### 0.29.0 [#0290]

#### Minor Changes [#minor-changes-83]

* 28f8bcf: Participant profiles are primitives: Domino serves the profile and separate reads keyed by it, and your app composes the profile page. Each read checks the profile's visibility and then its own resource's access.

  * `relay.profiles.get(id)` returns the profile only: `id`, `name` and `avatar` (`profileSummarySchema`).
  * `relay.leaderboards.placement(board, profileId)` (`GET /v1/leaderboards/:board/profiles/:profile`) returns `rank`, `score` and the ranked `total` on one board, with `rank` and `score` null while the member is unranked. It needs the board's access, like `standings`. New `leaderboardPlacementSchema`.
  * `relay.profiles.completions(id, { limit, cursor })` (`GET /v1/profiles/:profile/completions`) returns completions grouped by quest, most recent first, paged with `nextCursor`. Only quests that opt in appear: `defineQuest({ profile: { visible: true } })`, hidden by default; the quest's active release decides, and archived quests do not appear. New `profileCompletionPageSchema`, `profileCompletionQuerySchema` and `questProfileSchema`; quest descriptions gain an optional `profile`, which `defineQuest` includes only when the quest sets it.
  * `relay.profiles.referral(id, program)` (`GET /v1/profiles/:profile/referrals/:program`) returns `invited`, `qualified` and `milestones` in one program, for signed-in viewers (401 when signed out). New `profileReferralSchema`.
  * React: `useLeaderboardPlacement(board, id)`, `useProfileCompletions(id, { limit })` with `loadMore()`, and `useProfileReferral(id, program)`. `useProfile(id)` returns the profile only.
  * Profile settings: `defaults` is an ordered list of the accounts that fill in names and pictures, `"x"` and `"discord"` (`[]` for generated). `profileDefaultProviderSchema` lists them.
  * Generated avatars are seeded by an HMAC of the member under the project's secret (`relay.me.get().avatarSeed`), no longer by the member's first wallet: linking a wallet never changes an avatar.
  * `proxyParticipant` and the API's scope selector admit the new routes (`isProfileParticipantRoute`).
  * Console: **Participant profiles** settings show who sees profiles and the default accounts, with arrow buttons to order them. The member's **Community profile** appears only when profiles are visible or the member or an operator changed the profile.

  **Upgrading.** Breaking for apps, servers and CLIs on 0.25 to 0.27 that use profiles:

  * `relay.profiles.get(id)` no longer returns `self`, `leaderboards`, `quests` or `referrals`, and `publicProfileSchema` and `PublicProfile` are removed. Read each part with the calls above. For `self`, compare `id` with `relay.profiles.mine().id`. `useProfile(id)`'s `ready` state holds a `ProfileSummary`.
  * Quest completions no longer appear anywhere until the quest opts in: add `profile: { visible: true }` to the quests whose completions profiles may show, and publish.
  * Referral progress is per program and needs a signed-in viewer. `attributed` is now `invited`, returned with each program; it still counts the member's invitations across the whole project, not only that program's.
  * `management.profiles.configure()` takes `defaults: ["x", "discord"]` instead of `"accounts"`, and `[]` instead of `"generated"`; `sections` is removed and refused. `settings()` returns the list. Stored settings migrate: `accounts` becomes `["x", "discord"]`, `generated` becomes `[]`.
  * Members who linked a wallet get a new generated avatar, the one members without a wallet already had. Uploaded and account pictures do not change.
  * SDK and CLI 0.25 to 0.27 cannot parse the new profile and settings responses: `relay.profiles.get()` and `management.profiles.settings()` throw until the app and CLI upgrade. Deploy the backend first; its migration only adds the new setting, so pods of the previous release keep working during the rollout.
  * Upgrade the CLI and finish rolling out the backend before publishing a quest with `profile: { visible: true }`. Only quests that set `profile` carry the key in their release. CLIs from 0.25 to 0.27 refuse deploy and preview responses that include such a release (`Unrecognized key: profile`), and pods of the previous backend release cannot read it, including after a rollback. Quests without it publish and roll back as before.

### 0.28.0 [#0280]

#### Minor Changes [#minor-changes-84]

* 30dda1d: Add `useSignInOptions({ cosmos })` to `@domino-sdk/relay/react`: every way a participant can sign in, as one flat list. It holds the configured OAuth providers, the installed wallets of the families the project enabled, and email. Each option has an `id`, a `label`, a `kind` and an `icon` hint. OAuth and wallet options sign in with `start()`, and the email option with `sendCode(email)` then `verifyCode(code)`. Cosmos wallets are offered only on the chains the app passes as `cosmos`. `unavailable` explains the gaps with a machine-readable `reason`: `cosmos-chains-missing` when the app named no chains, `cosmos-chains-invalid` when a chain in `cosmos` is invalid (such as `"cosmoshub-4"` without `cosmos:`; the other methods still load), and `no-wallet-installed` with a message for the participant. OAuth and email are offered as soon as the methods are read; installed wallets join them when discovery finishes. The options keep their identity across renders, and their actions never reject: failures land in `error`. `methodsError` says when the methods could not be read and the list fell back to email. Apps without React get the same list from `signInOptions(relay, { cosmos })` in `@domino-sdk/relay/browser`, whose actions reject on failure; `@domino-sdk/relay/browser` also exports the option types, `discoverSignInWallets`, `CosmosChains` and `SignInWallet`.

  `useSignIn()` actions now keep their identity across renders, and `verifyCode` always uses the latest code sent; `sendCode` forgets the previous code first. `useSignInMethods()` returns `error` when it fell back to email. Installed wallets with the same name and family get a number in their label. `cosmosChain()` now rejects a chain object without a string `chainId`.

  The starter apps now build their sign-in on the hook, and no longer export `COSMOS_CHAINS`. To offer Cosmos wallets in an app made from a starter, pass your chains to the `useSignInOptions()` call in `src/components/sign-in.tsx`. In development the starters show a note when the project takes Cosmos wallets and the app names no chains, or an invalid one, and no longer tell participants the project has no sign-in method in that case. `useSignInMethods()`, `useSignIn()` and `useSignInWallets()` keep working.

  Management member accounts now carry `kind`: `wallet` for wallet identities and `account` for the rest. Responses from older servers read as `account`.

### 0.27.0 [#0270]

#### Minor Changes [#minor-changes-85]

* 9ef2629: Cosmos wallets work on any Cosmos SDK chain your app chooses. Relay no longer ships a list of Cosmos chains.

  * A Cosmos wallet is `cosmos:<chain ID>` (a CAIP-2 reference: 1 to 32 letters, digits, `-` or `_`) and a bech32 address with any prefix, checked for its checksum and stored lowercase. `caip10()` and wallet tracker authors accept any Cosmos chain ID. A Cosmos wallet is its address: a Cosmos account ID is attributed to the member who links that address, and `trackers.records({ member })` returns the member's Cosmos records under any chain ID. The same key on chains with different prefixes is separate wallets; five Cosmos wallets per member and project, across all chains. 32-byte module and contract addresses cannot link or sign in. Unlinking a wallet and changing its sign-in accept a bech32 address in either case.
  * A Cosmos wallet's `network` is the chain the app requested when linking it. Relay does not verify it: any app can link an address under any `cosmos:<chain ID>`. Quests and servers should gate on the address, such as its bech32 prefix against their own list of chains, not on `network`.
  * Browser: `cosmosSigner(provider, chain)` and `discoverCosmosWallets(chain)` take the app's chain as `{ chainId, chainInfo? }` or `"cosmos:<chain ID>"`. When the wallet answers that it does not know the chain, the signer suggests it with `experimentalSuggestChain(chainInfo)` and retries. Without `chainInfo`, or with a wallet that has no `experimentalSuggestChain`, it throws an error naming the chain. Other errors, such as a rejected request, pass through. New `CosmosChain` type and `cosmosChain()` helper.
  * React: `useWalletDiscovery({ cosmos })` and `useSignInWallets(families, { cosmos })` take the app's chains (one or an array).
  * Wallet sign-in is enabled per family: `"solana"`, `"eip155"` and `"cosmos"`. `relay.auth.providers().wallets`, `participantSignIn()` and `configureParticipantSignIn()` use families, and a per-network value such as `cosmos:cosmoshub-4` is accepted and read as `cosmos`. The rule that refused a Cosmos testnet in the live environment is gone with the chain list. New `walletSignInFamilies` and `walletSignInNetworksSchema`.
  * Console: **Participant sign-in** offers **Cosmos SDK chains**, and member wallets show the chain ID, such as **Cosmos · cosmoshub-4**. Starters offer Cosmos wallets for the chains in `COSMOS_CHAINS` in `src/components/sign-in.tsx`, and say sign-in is not set up when the project enables only Cosmos wallets and `COSMOS_CHAINS` is empty.

  **Upgrading.** Deploy the backend, then the API Worker right after it. The backend's migration turns stored per-network sign-in settings into `cosmos`. Until the API Worker is updated, it checks `ctx.wallet.list()` results against the previous release's two Cosmos networks, so a quest evaluation fails when the member has a wallet linked on any other Cosmos chain.

  * **Projects that enabled Cosmos sign-in must upgrade their app to this release immediately after the backend deploys.** SDK 0.25 and 0.26 read `relay.auth.providers().wallets` against a fixed list of values. After the migration such a project answers `["cosmos"]`, which that list does not contain, so `relay.auth.providers()` throws for every visitor. `useSignInMethods()` then falls back to email alone: every OAuth and wallet sign-in button disappears, not only Cosmos wallets, and the email form appears even where email sign-in is off.

  * CLI and management SDK 0.25 and 0.26 reject the `cosmos` family in `participantSignIn()` and `configureParticipantSignIn()` responses. SDK 0.25 and 0.26 reject a linked wallet on any other Cosmos chain in `relay.wallets.list()` and `management.member()`. Upgrade the CLI with the app.

  * Apps made from a starter declare `NETWORK_NAMES: Record<WalletSignInNetwork, string>` in `src/components/sign-in.tsx` with a key per Cosmos network. After upgrading, `tsc` fails on those keys. Built anyway, the sign-in form of a project that enables Cosmos wallets lists no Cosmos wallet, and crashes when it shows the "Wallet sign-in accepts …" hint, because `NETWORK_NAMES.cosmos` is undefined. Replace the per-network keys with one `cosmos` key, declare your chains, and pass them to `useSignInWallets`:

    ```tsx
    import type { CosmosChain, WalletSignInNetwork } from "@domino-sdk/relay"

    /** The Cosmos SDK chains this app offers wallet sign-in on. */
    export const COSMOS_CHAINS: CosmosChain[] = [{ chainId: "cosmoshub-4" }]

    const NETWORK_NAMES: Record<WalletSignInNetwork, string> = {
      solana: "Solana",
      eip155: "EVM",
      cosmos: "Cosmos",
    }

    // In the sign-in component:
    const wallets = useSignInWallets(methods?.wallets, {
      cosmos: COSMOS_CHAINS,
    })
    ```

  * `cosmosNetworks` is removed, and `CosmosNetwork` is any `cosmos:<chain ID>`. Pass your chain to the adapters instead of looking it up.

  * `useWalletDiscovery()` no longer lists Cosmos wallets on a default chain: pass `{ cosmos: { chainId: "…" } }`. `useSignInWallets(networks)` lists Cosmos wallets only with `{ cosmos }`.

  * `WalletSignInNetwork` is now `"solana" | "eip155" | "cosmos"`; a `SignInWallet`'s `network` is its family, and a Cosmos wallet's chain is `signer.network`.

### 0.26.0 [#0260]

No changes in this release.

### 0.25.1 [#0251]

No changes in this release.

### 0.25.0 [#0250]

#### Minor Changes [#minor-changes-86]

* 6634992: Participants have project profiles: a display name and an avatar that other participants see on leaderboards and profile pages, where the project enables them.

  * `relay.profiles.mine()`, `update({ displayName, avatar, hidden })`, `uploadAvatar(file, { uploadId })` and `get(profileId)`. Uploads accept JPEG, PNG or WebP up to 5 MB and are stored as 512×512 WebP without metadata.
  * React: `useMyProfile()` and `useProfile(id)`; `avatarSrc(avatar)` renders any profile avatar. Generated avatars use the same seed as `relay.me.get().avatarSeed`.
  * Contract: `profileSummarySchema`, `ownProfileSchema`, `publicProfileSchema`, `profileUpdateSchema`, `displayNameSchema`, `profileSettingsSchema`, `profileModerationSchema`, `managedProfileSchema` and `isProfileParticipantRoute()`. Leaderboard rows gain `profile` (null unless the viewer can see it; older servers omit it and it reads as null).
  * Management: `profiles.settings()`, `profiles.configure({ visibility, defaults, sections, expectedRevision, reason })` (publish; in live only from Console) and `profiles.moderate(member, { resetName, removeAvatar, lock, reason })` (review). `member(id)` returns `profile`, and account profiles gain `username`. Console has **Settings → Access → Participant profiles** and a Community profile section on the member.

  **Upgrading.** Nothing is shown until an operator enables profiles: leaderboards keep their aliases and profile pages answer 404. Deploy the backend first; the database migration runs automatically and dates earlier completions. Apps must upgrade the SDK so their `/relay` proxy (`proxyParticipant` or `createParticipantBackend`) admits `/v1/me/profile`, `/v1/me/profile/avatar` and `/v1/profiles/:id`; older proxies answer 404 for them. X and Discord names and pictures fill in when members next sign in or connect those accounts. No breaking changes.

* 531f41e: Members can link several Solana, EVM and Provenance wallets to a project and unlink them.

  * `relay.wallets.list()`, `link(signer)`, `unlink(wallet)`, and the lower-level `challenge()` and `verify()`.
  * Dependency-free signer adapters and discovery in `@domino-sdk/relay/browser`: `evmSigner`, `solanaSigner`, `walletStandardSolanaSigner`, `cosmosSigner`, `discoverEvmWallets`, `discoverSolanaWallets`, `discoverCosmosWallets`.
  * React: `useWallets()` and `useWalletDiscovery()`.
  * Evaluators: `ctx.wallet.list()` returns the member's linked wallets, pinned once per attempt.
  * Management: `unlinkMemberWallet(member, { chain, address, reason })` (requires `review`), and `wallets` in the member detail.
  * Contract helpers: `caip10()`, `cosmosNetworks`, `isWalletParticipantRoute()`, and the `LinkedWallet`, `WalletSigner` and related schemas.

  **Upgrading.** Nothing breaks. `relay.wallet.connection()/challenge()/verify()` and `ctx.wallet.connection()/solanaBalance()` keep working and use the newest linked Solana wallet; linking through `relay.wallet.verify()` still replaces it, while wallets linked through `relay.wallets` sit alongside older ones. The legacy challenge text now uses the sign-in message format, and rate-limited wallet challenges answer 429 instead of 400. Deploy the backend and API Worker before publishing apps that use the new APIs. Apps that proxy through `proxyParticipant` forward `/v1/me/wallets*` after upgrading the SDK. The database migration runs automatically.

* c71712f: Quests can trigger from custom tracker records attributed to members' verified wallets.

  * `customSource(namespace, { authors: "wallet" })`: each record's author is a canonical CAIP-10 account ID, attributed to the member who links that wallet. Ingestion refuses missing or non-canonical authors.
  * `automatic({ tracker })` admits one attempt per new record observation for the attributed member. Evaluators and `keyed()` receive the pinned observation and attribution as `ctx.tracker`; `ctx.eligibleAt` is the record's occurrence time.
  * `management.trackers.outcomes(tracker, query)` explains each admission or skip with its reason; `management.trackers.retryOutcome(tracker, input)` (requires `review`) retries a skipped admission.
  * `management.trackers.records(tracker, { member })` works on wallet trackers, returning the member's linked wallets' records. `trackers.ingest` results include `admissions` counts.
  * New `ingest` permission, granted to hosted-app keys: it allows custom tracker ingestion only. People keep ingesting with `observe`.

  **Upgrading.** Nothing breaks for existing projects. Once a project publishes a tracker-triggered quest, release and progress reads include `trigger.tracker`, which older SDKs reject because the trigger schema is strict: upgrade participant apps before publishing one. Hosted-app keys hold `ingest`, but responses (the minted key, `access()`) keep listing only `read` and `points`, so older CLIs, API Workers and builders still parse them. Permission lists in `access()` projects and hosted-key responses now drop values this release does not know instead of rejecting the response. The database migration runs automatically.

* ea5d69e: Quests can check token balances of a member's linked wallets.

  * Evaluators: `ctx.wallet.balance({ chain, token?, address? })` reads SOL and SPL tokens (including Token-2022) on Solana mainnet, and native and ERC-20 balances on any EVM chain in `evmChains` (the EVM networks dRPC serves, by CAIP-2 ID such as `"eip155:8453"`). It reads all of the member's pinned linked wallets of that family, or one of them, and returns integer amounts with the token's decimals, captured for replay. Each read is one of the evaluation's eight capability calls.
  * Authoring: `parseUnits()` and `formatUnits()` convert whole-token amounts, and `WalletBalanceQuery` and `WalletBalance` type the call.
  * Contract: `evmChains`, `evmChainSchema` and `walletBalanceSchema`.
  * Management: the member detail's `balanceChecks` lists the member's recent recorded balance reads; Console shows them under the member's wallets.
  * Snippet: `token-holder` checks that a member's linked wallets hold at least a minimum.

  **Upgrading.** Nothing breaks. Deploy the backend and API Worker before publishing quests that call `ctx.wallet.balance()`. EVM chains need the platform's `DRPC_API_KEY`; without it, EVM balance reads fail the verification with "EVM balance checks are not configured". Solana reads need no new configuration. `evmChains` is generated from dRPC's chain list: a later release that removes a chain breaks quests that read it, and its changeset will say so.

* e7853fb: Participants can sign in, and sign up, with a Solana, EVM or Provenance wallet where a project enables it, and apps can draw generated avatars.

  * `relay.auth.wallet.signIn(signer)`, and its two steps `start(input)` and `verify(proof)`. `relay.auth.providers()` returns `wallets`, the networks the project enabled.
  * React: `useSignIn().withWallet(signer)`, `useSignInMethods().wallets`, `useSignInWallets(networks)` (the installed wallets that can sign in on those networks), and `useMe()`. The starters offer installed wallets next to email and OAuth.
  * `relay.me.get()` returns the signed-in `member` and an `avatarSeed` derived from their first wallet without revealing it. `defaultAvatar(seed)` and `defaultAvatarSvg(seed)` draw a deterministic avatar (DiceBear Shapes, CC0); `walletAvatarSeed(wallet)` seeds a member's own wallet list.
  * Contract: `walletSignInNetworkSchema`, `walletSignInNetwork()`, `isWalletSignInRoute()` and `memberSummarySchema`.
  * Management: `participantSignIn()` and `configureParticipantSignIn({ wallets })` (publish) read and set the project's wallet sign-in per environment; `setMemberWalletSignIn(member, { chain, address, enabled, reason })` (review) turns sign-in with a member's wallet on or off. Console has both under Settings → Access and on the member.

  **Upgrading.** Nothing changes until a project enables wallet sign-in. Deploy the backend and API Worker first; the database migration runs automatically. `/v1/auth/providers` gains `wallets`, which older SDKs ignore. Apps that proxy through `proxyParticipant` forward `/v1/auth/wallet/*` and `/v1/me` after upgrading the SDK. A platform without OAuth providers or email no longer answers 503 for projects that enable wallet sign-in.

* b22ead4: X trackers can trigger quests, and budgets can reset weekly.

  * `automatic({ tracker })` accepts an X tracker. The quest runs once for each post the tracker collects for the first time; refreshes, metric changes and edits never run it again. `beneficiary: "author"` (the default) pays the member whose selected, verified X account wrote the post; `beneficiary: "replied-to"` pays the member whose selected, verified X account the post replies to. `"replied-to"` on a custom tracker throws.
  * `ctx.tracker.x` carries the captured post (`content` and `evidence`) for X trackers, and `ctx.tracker.attribution.account` can be `{ provider: "x", issuer, subject, revision, linkedAt }`.
  * `budget({ period: "week" })` limits completions or points per ISO week in UTC, starting Monday, by the attempt's eligibility time.
  * New trigger outcome reasons: "Post is not a reply", "Reply is to the author's own post", "No member has linked this X account" and "Captured post is unavailable".

  **Upgrading.** Nothing breaks for existing projects. Release and progress reads that include an X tracker trigger (`source: "x-search"`, `beneficiary: "replied-to"`) or a `period: "week"` budget are rejected by older SDKs, whose schemas are strict: upgrade participant apps before publishing either.

### 0.24.0 [#0240]

No changes in this release.

### 0.23.1 [#0231]

No changes in this release.

### 0.23.0 [#0230]

*Not published to npm. Its changes first reached npm in 0.23.1.*

#### Minor Changes [#minor-changes-87]

* 49fa0fd: Apps can now match the ledger and tiered reward IDs that reads return to the keys they declared.

  * `relay.pointLedgers.list()` (`GET /v1/point-ledgers`) and the React hook `usePointLedgers()` return each ledger's `id`, `key`, and `name`.
  * `relay.tieredRewards.list()` (`GET /v1/tiered-rewards`) and `useTieredRewards()` return each tiered reward's `id` and `key`.
  * Both reads are open to signed-out visitors. The hooks return the list in `items`. `key` is null for resources created in Console without one.

  Reads name ledgers and tiered rewards by ID. Comparing a referral reward's, account's, or history entry's `balance` with a key such as `"credits"`, or an entitlement's `reward.id` with the key given to `defineTieredReward`, never matched, and apps could show "200 ledger\_… points". Find the resource by key, compare IDs, and display its name or title.

  Quest view rewards are now typed: `{ kind: "points", label, amount, ledger: { id, key, name } }`, `{ kind: "tier", label, reward: { id, key, title }, tier }`, or `{ kind: "integration", label }`. Labels read "10 XP" instead of "10 ledger\_…". Tier labels show the reward title, followed by the tier title when the reward has several tiers, instead of the tier ID.

  Upgrade notes:

  * Code that reads `label` keeps working, and older SDKs ignore the new fields. Replace label parsing with `amount`. If you compared a tier reward's `label` with its tier ID, compare `tier` instead.
  * TypeScript code that builds quest views, such as test fixtures, must add `kind` and that kind's fields.
  * Deploy the updated backend and API Worker before upgrading the SDK: this SDK expects `kind` on quest view rewards. Apps that proxy participant requests with `proxyParticipant` or `handleHostedApp` forward the new routes once they upgrade.
  * No database migration is required.

### 0.22.0 [#0220]

*Not published to npm. Its changes first reached npm in 0.23.1.*

#### Minor Changes [#minor-changes-88]

* 2614ad4: Add `ctx.images` for quest evaluators: `inspect`, `compare` (perceptual hash distances), `review` (AI judgement over up to four images), `extract` (typed fields read from a screenshot) and `seen` (other participants' near-duplicate images). Images are named by reference: the attempt's photo evidence, the linked X profile picture, an X post's attached image, or a reference image listed under the new `images` key in `relay.json` (JPEG, PNG or WebP, up to 5 MiB each and 10 per deployment) and published with `domino deploy`, which uploads new files directly to storage and sends only their hashes. New snippets `x-profile-picture`, `unique-photo` and `community-screenshot` install complete quests with operator-tunable similarity thresholds. `ctx.x.post()` results now include attached `media`.

  Additive and backward compatible. Existing quests, including `ctx.review.photo()`, keep working. To use `ctx.images`, rebuild quests with this SDK version; bundles built with earlier versions do not pass it to `evaluate`. Deploying `images` needs a server with this release; older servers ignore the key, and quests that reference those images then fail verification with "Image … is not published".

### 0.21.0 [#0210]

*Not published to npm. Its changes first reached npm in 0.23.1.*

No changes in this release.

### 0.20.0 [#0200]

*Not published to npm. Its changes first reached npm in 0.23.1.*

#### Minor Changes [#minor-changes-89]

* 2ff82fb: Remove `reviews.list` and `members.list` from `createRelayClient` and their `ReviewListOptions` and `MemberListOptions` types. Since the 0.10.0 release the participant API no longer serves `GET /v1/reviews` or `GET /v1/members`, so these calls fail with every SDK version; removing the methods turns that runtime failure into a type error.

  Upgrade notes: working projects are unaffected, because these calls could not succeed. If your code still references them, delete the calls. List members from a server with the management client: `createManagementClient(...).page("members", { q, limit })`, or `member(id)` for one member. Review pending attempts in the Domino Console **Activity** page. Members can still download their own evidence with `evidence.download`.

### 0.19.0 [#0190]

No changes in this release.

### 0.18.4 [#0184]

No changes in this release.

### 0.18.3 [#0183]

No changes in this release.

### 0.18.2 [#0182]

No changes in this release.

### 0.18.1 [#0181]

#### Patch Changes [#patch-changes-27]

* 2186fe3: `domino dev` no longer breaks participant requests when `--api-url` is Console's management proxy (`https://console.domino.run/relay`). The CLI used to pass that URL to the app as `DOMINO_PARTICIPANT_API`. The participant proxy refuses any upstream with a path, so every `/relay/*` request answered 503 "Participant API is not configured".

  The API now reports its participant origin in `/preflight` (`participantApi`). `domino dev` sends participant requests there and gives the development management key the same origin, as deployed builds receive it. With an older API, the CLI uses `--api-url` if it is already an origin, then relay.json's `apiUrl`. If neither is, it stops with instructions instead of starting a broken preview.

  `domino doctor --remote` has a new `participant-api` check, and reports the resolved origin as `scope.participantApi`. The check fails when no participant origin can be found, or when relay.json `apiUrl` is not that origin, which staged builds rely on. The proxy's 503 message now says to use the API origin rather than a management URL.

  Upgrade note: if relay.json `apiUrl` is set to a management proxy URL, change it to the participant API origin (`https://relay.domino.run`).

### 0.18.0 [#0180]

#### Minor Changes [#minor-changes-90]

* bf84375: Add `trackers.content(reference, { id, observation })` to hosted quest evaluators. The read returns a retained X body and verified account attribution to the attempt's member, captured together for replay. Engagement counters, mutable author profiles, usernames, and verification badges do not determine ownership. Explicit disconnects and ambiguous legacy connections return unlinked attribution.

  Management clients expose the same read with a required `member` in the query and `read` permission. Deploy the updated backend and API runtime before using this method. Existing tracker methods and response shapes remain unchanged. This adds evidence reads, not automatic admission or engagement rewards.

* ac63da2: Add automatic X content quests with `content(tracker, policy)` and `perContent()`, pinned content context, quest enrollment, historical claims and staff approvals, and paginated admission explanations. Existing trigger and execution contracts remain readable. Upgrade the backend and collector workers before publishing content quests; update participant apps to use the new SDK proxy paths. Existing tracker matches are not backfilled, and edits after admission do not reevaluate or reverse prior rewards.

* 0a0ac9a: Add independent daily engagement rewards for qualified X content. Author immutable cumulative formulas with `defineEngagementReward`, publish bundled code with `management.engagement.publish`, and read settlement history through `engagement.page`. Partial budgets retain paid and permanently excluded growth separately.

  Deploy backend migration 0060, the API formula runtime, and workers before publishing engagement rules. Existing quests keep their contracts. This first version supports new discoveries and historical initial baselines; CLI manifest deployment, formula replacement, explicit backfill, enrollment baselines, and automated reversals remain unsupported.

* 40c9761: Add opt-in X evidence references to tracker records. Pass `includeEvidence: true` to receive a stable content revision and a separate provider observation identity. Engagement and author profile refreshes keep the content revision unchanged. Custom records return `evidence: null` when requested; default responses keep their existing shape.

  Deploy backend migration 0056 and the updated API before requesting evidence from the new SDK. The migration backfills retained X observations and captures writes from previous backend versions during rolling deployment. Existing SDK callers need no changes. Automatic content qualification and engagement rewards remain unimplemented.

* b596a0a: Add content-only performance gates with declared engagement metrics and a configurable freshness limit. Content approval stays cached while new samples run performance. A body edit or change to the verified account selection requires fresh content approval. Missing or stale metrics wait without running the callback. The first passing performance decision resolves the content-approved reward selection; later metric changes cannot duplicate awards or retry budget exclusions.

  Use `content.performance({ quest, content, member?, before? })` to read current eligibility and retained metric evaluations. Existing content qualification history remains content-only. Performance callbacks receive the declared metrics, sample time, pinned content, member, and settings. They gate rewards but cannot replace the content-approved selection.

  Apply migration `0059_content_performance` and upgrade the backend, API evaluator, and workers before publishing quests with this stage. Older evaluator runtimes cannot dispatch performance safely. Existing quests need no source changes and omit the new optional fields. Upgrade the SDK to author performance stages or read their history. Automatic reversals and daily engagement settlement remain unsupported.

* e5b7091: Reevaluate admitted X posts after body edits and expose `content.qualifications({ quest, member?, content?, before? })` in participant and management clients. History keeps the original release, beneficiary, and evidence, with qualification state separate from the one-time reward outcome. Stale evaluations and reviews cannot award rewards; later qualifying edits cannot repeat rewards or retry a budget exclusion. Existing admission APIs retain their response shapes. Removal and rejection do not reverse prior awards.

  Apply migration `0058_content_qualification` and upgrade backend and workers together before enabling content quests. Older decision workers cannot enforce stale-content guards during mixed-version deployment. Existing admissions are retained and backfilled; no quest source changes are needed. Upgrade clients to use the new history method.

* 243d9e7: Add explicit `engagement: [{ entry: "engagement.ts" }]` manifest entries, with optional `exportName`, to `domino build` and `domino deploy`. Preview and publish engagement rules with their tracker, qualifying quest, and points balance in one project transaction. Previews report new, unchanged, and changed formulas; formula changes preserve past earnings and budget spend through observed boundaries.

  Resume the saved deployment ID to publish a reviewed preview or recover a committed result. Stale engagement changes reject the entire deployment. Missing entries do not delete rules, and unchanged code does not create another scoring version. New rules use actual publication time as their discovery cutoff.

  Upgrade the backend and settlement workers before deploying engagement entries. Manifests and SDK requests without engagement entries keep their request shape, and existing hosted-build activation receipts remain replayable. CLI publication remains test-only; live previews remain available. Changes to metrics, budgets, or qualifying quests remain unsupported.

* 4cea332: Add preview/apply management workflows for engagement formula changes, immutable version history, and participant boundary history. Each post settles its governing formula through an available fresh eligible observation before the latest requested formula starts at the same counters. Existing budget spend and exclusions survive; superseded unapplied formulas do not invent intermediate earnings.

  Preview totals describe retained accounting, not promised future payouts. Available observations may predate the request by up to the rule's freshness limit. Quest, balance, metrics, baseline policy, freshness policy, and budgets cannot change in this workflow.

  Deploy migration 0061 and upgrade/drain all settlement workers before requesting a formula change. Older workers do not enforce version boundaries. Existing settlement response shapes remain compatible; histories may now include an additional formula-boundary settlement in the same UTC day.

  Engagement creation and formula apply now follow the existing live-publication policy: management keys can publish in test, while live publication requires a Console session.

* 2a7ca88: Add an optional `content` filter to `content.admissions({ quest, content })` for direct post lookup. Management and participant reads retain their existing project and member isolation, response shapes, and unfiltered pagination behavior. Upgrade the backend before sending the new filter; older servers reject it. Existing callers need no changes.

### 0.17.2 [#0172]

#### Patch Changes [#patch-changes-28]

* 664eb80: Share current X post content, author verification, and engagement across matching trackers in the same project and environment. Each tracker keeps its own matches and first-seen times. Concurrent collectors coordinate known-post refreshes, and captured quest reads retain their original results.

  Pausing a tracker stops its collection; another active tracker can still update their shared posts. Custom datasets remain isolated per tracker. Existing tracker definitions and SDK calls need no changes.

  Requires backend migration `0055_shared_tracker_content` and the matching backend deployment. The migration retains the previous release's write format for rolling upgrades and preserves existing observation history. Removing the legacy payload columns requires a later contract release after all old workers have stopped. Old workers may still perform duplicate refreshes until the rollout finishes.

### 0.17.1 [#0171]

No changes in this release.

### 0.17.0 [#0170]

No changes in this release.

### 0.16.1 [#0161]

No changes in this release.

### 0.16.0 [#0160]

*Not published to npm. Its changes first reached npm in 0.16.1.*

#### Minor Changes [#minor-changes-91]

* abc6358: Add `ctx.review.text({ text, criteria })` with typed pass, fail, and unclear results for authored quest decisions. Review verified content such as X tracker posts using the existing backend AI configuration. Validated results are captured and reused across retries; provider failures remain errors.

  The capability limits text to 16,000 JavaScript string units and criteria to 2,000, with a 12-second provider timeout. Review text is treated as untrusted evidence. Credentials stay server-side. This additive API requires a backend and evaluator bridge deployment that supports text review before publishing quests that use it. Existing photo quests remain compatible. Combine this capability with `evidence.text()` for submitted text or `evidence.url()` plus verified X post reads.

* 3280aa1: Add required or optional text and HTTP/HTTPS URL submissions with configurable text lengths and exact allowed hosts. Evaluators receive captured strings through ctx.input.value. Browser handles and campaign controls preserve the original values across interrupted delivery and evaluation retries. Backend admission enforces the published contract and returns correctable validation errors.

  Submitted URLs never trigger automatic fetching. Use `xPostId` and `ctx.x.post({ postId })` to verify a post through the configured X provider; reads are captured for retries. Participants link X before verification. Compare the returned author ID with `userId` from `ctx.x.connection()` to establish authorship.

  Existing photo and quiz request shapes and saved photo records remain compatible. Upgrade the SDK and CLI together before authoring text or URL quests. Existing participant apps must add controls and declare text and url in supportedInteractions; newly exported campaign starters include both. Deploy the updated backend and API Worker before publishing these quests. Text and URL storage need no additional migration; the integrated X capability requires the X execution-snapshot migration.

* 074c292: Add captured X verification to authored quest evaluators: `x.connection()`, `x.profile()`, `x.post({ postId })`, `x.follows({ userId })`, and `x.reposted({ postId })`.

  Participants link their X accounts through the existing account flow. Platform operators configure the backend's `X_API_WRAPPER_API_KEY`, shared with trackers. Account changes invalidate unfinished reads; retries reuse captured evidence. Existing quests remain compatible. Apply the additive X execution-snapshot migration and deploy the updated backend and evaluator Worker before publishing quests that use the new methods, and update the SDK and installed CLI references.

### 0.15.0 [#0150]

#### Minor Changes [#minor-changes-92]

* 6c309ea: New projects can start from a choice of starters: Campaign tour (the default), Classic questboard, Waitlist, Referral program, Discord activation, and API. In Console, choose one under **Start from** when creating something new. In a terminal, run `domino create NAME --template waitlist`, or answer the new prompt. Scripts without `--template` still get the campaign tour. `starterTemplates` and `starterIdSchema` in `@domino-sdk/relay-cli` list the offered starters. Projects now report the chosen starter as a top-level `starter` field, and `POST /repository` accepts any offered starter ID. Omit it to use the starter chosen at setup. At creation, Domino writes the project name to the starter's `src/project.json` and places the brief where the starter's `CAMPAIGN.md` marks it.

  `@domino-sdk/relay/react` provides the React layer the starters share:

  * `RelayProvider`, `useExperience`, `questGroups`, `useQuest`, and the quest status helpers
  * `useSignIn`, `useSignInMethods`, `useSignOut`, and `useAccounts`
  * `useLeaderboard`, `useReferralSummary`, `useInvitation`, and `useRelay`

  React 19 is an optional peer dependency. `@domino-sdk/relay/vite` adds `serverBoundary()`, which fails a Vite build when browser code imports `server/` or `@domino-sdk/relay-cli`. `@domino-sdk/relay/portal-proxy` adds `hostedAppRequest` and `handleHostedApp` for the Domino routes of a hosted app's Worker.

  Upgrade notes: nothing breaks for existing projects. Apps built from the earlier campaign starter keep their own `src/relay` copies and can switch to `@domino-sdk/relay/react` hook by hook. Wrap the app in `RelayProvider`, which replaces `ExperienceProvider` and `src/relay/client.ts`, then change imports. The hook names and behavior are unchanged, except that `useRoleDeliveries` is now internal and `useExperience()` also returns `member`.

### 0.14.1 [#0141]

No changes in this release.

### 0.14.0 [#0140]

#### Minor Changes [#minor-changes-93]

* 11abaf5: Add a bounded X search preview for exploring tracker queries before defining them in code. `management.trackers.preview({ query, since })` returns at most 100 matching posts from 3 search pages, up to 30 days back, with author verification retained and `more` set when X had further matches. It requires `publish` permission, is rate limited, stores nothing, and never publishes a tracker or starts collection. Console's Trackers page uses it to show a sample and copy a tracker definition or coding-agent prompt.

  Requires the matching backend deployment. Existing clients and manifests are unaffected.

### 0.13.0 [#0130]

#### Minor Changes [#minor-changes-94]

* e969f1e: Add maintained tracker datasets with a managed X search source, preserved author verification, read-time filters, and custom indexer ingestion. Publish definitions through relay.json and the existing deployment preview flow. Query datasets from the management SDK or captured read-only capabilities in hosted quest evaluators.

  Requires the matching backend migration and deployment before publishing or reading trackers. X collection requires the platform's X\_API\_WRAPPER\_API\_KEY credential. Existing project manifests remain valid. Custom evaluator test contexts that explicitly implement EvaluationContext must add a trackers reader. Collection does not automatically issue or reverse rewards.

### 0.12.0 [#0120]

#### Minor Changes [#minor-changes-95]

* 9fdc113: Reward deliveries now end. `rewardDeliverySchema.status` adds a terminal `failed` state, reached when Discord permanently rejects a role grant (unknown role, member left the server, missing permissions) or transient errors persist for about a day. The management client adds `retryRewardDelivery(id)`, which moves a failed delivery back to `pending` and starts a new delivery round. Clients that parse deliveries with an older SDK reject `failed` records, so upgrade before reading deliveries.

### 0.11.0 [#0110]

#### Minor Changes [#minor-changes-96]

* 5337971: Add `streak({ days, timeZone })` for automatic observation quests. Verified actions count once per community day, including daylight-saving boundaries. Late evidence repairs continuity; milestone rewards commit once with progress. Participant reads expose current/best runs, qualifying days, deadlines and reward status. Timezone changes take effect at the next community boundary without rewriting history.

  Each quest awards one lifetime milestone. Configure separate quests for separate goals. All streak quests in a project/environment share one timezone. An existing goal length is immutable, and budget-exhausted milestones keep their evidence without later automatic payout.

  Apply the backend migration and deploy the updated backend, SDK and participant app before publishing streak quests. Existing once/keyed quests retain their behavior. Older SDKs cannot parse the new `streak` completion kind or `progressed` attempt status; upgrade clients that read a project containing streak quests.

### 0.10.0 [#0100]

#### Minor Changes [#minor-changes-97]

* 8a81fde: `proxyParticipant` now sends the app origin to the shared API by default, as `hosted: true` did. The shared API has no deployment-wide participant project, so calls without it failed with 503 and now get 404 “Participant requests must come through a registered app or identity integration”. Existing code that passes `hosted: true` keeps working and can drop the option. Pass `hosted: false` only for an upstream that applies its own project scope.

#### Patch Changes [#patch-changes-29]

* d5d530d: Mark the staff and operator methods on `createRelayClient` as deprecated: `reviews.list`, `members.list`, `lookupMember`, `snapshot`, `template`, `journeyTemplate`, `publish`, `publishBatch`, `beginHandover`, `confirmHandover`, `decide` and `me.linkPass`. The participant API accepts only member sessions, so these calls already fail. No upgrade is needed; move any remaining calls to the management API, `domino deploy`, or a server-side pass verifier that calls `POST /management/v1/passes/verify`.

### 0.9.1 [#091]

No changes in this release.

### 0.9.0 [#090]

#### Minor Changes [#minor-changes-98]

* 6a5b6d3: Remove the offline development runtime and the `dev --offline`, `dev --review-mode`, and `doctor --offline` options. Use `domino dev` with the hosted test environment and real sign-in. New campaign exports include only the SDK and CLI packages.

  New release and catalog publication accept only Workers AI. Historical provider identifiers remain readable for retained records, but cannot select simulated evaluation.

### 0.8.0 [#080]

#### Minor Changes [#minor-changes-99]

* 25ea110: Assign persistent resource IDs on the server. Authoring declarations now use stable keys, project and draft creation use separate retry keys, and point ledgers must be created or declared before use. Existing IDs remain valid. Legacy authoring `id` remains supported as a key alias. Use `domino migrate` to preview a repeatable source and manifest upgrade, apply with `--write`, and inspect actionable diagnostics with `domino check --json`. Creation helpers generate retry keys, retry uncertain responses once, and expose the retry key on `CreationError`; raw HTTP creation still requires an action ID. Consume returned IDs for newly created resources.

### 0.7.0 [#070]

#### Minor Changes [#minor-changes-100]

* 0bbefc1: Add point ledger metadata with stable IDs, editable names and descriptions, management list/save methods, and a pointLedger setting for Console pickers. Existing balance keys retain their accounting history.

### 0.6.0 [#060]

#### Minor Changes [#minor-changes-101]

* 5cd39a2: Add observation-triggered quests, keyed completions, visibility, shared atomic budgets, and backend observation submission. Include an automatic Discord announcement reaction snippet and participant guidance for repeatable quests.
* 5cd39a2: Add Discord message contributions, moderator recognition with separate actor and beneficiary identities, richer member and scoped message reads, and durable Discord role rewards with reusable role selectors and delivery status. Include three distinct capability snippets and updated authoring guidance.
* 5cd39a2: Add reusable integration resource selectors and date/time settings. Include Discord channel selection, scoped resource lookup, publication validation, and paused publication for unconfigured resource settings.

### 0.5.0 [#050]

#### Minor Changes [#minor-changes-102]

* 85bb8aa: Add compact prefixed resource ID generation and expose globally unique project slugs. Hosted builds carry the slug used for stable staging addresses; development previews use the slug and a shorter workspace hash. Existing resource IDs remain valid.

* c24042e: Remove the Phantom demo sign-in helper and campaign-specific API routes from the platform. Local authentication defaults to the generic local/campaign test scope. Event pass admission requires a verification integration instead of accepting Breakpoint-formatted codes.

  Expose a generic management operation for app-owned pass verification. It requires confirmation permission and keeps unique pass ownership and retry-safe entry submission in Relay.

* 990c756: Support compiled Cloudflare Worker apps in connected previews and cloud staging, including SSR, server actions, and API routes. Add the worker app configuration and private module artifacts while preserving static hosting.

  Add opt-in `app.routing="transparent"`: app requests and responses pass through unchanged, with no reserved routes. Move the starter participant proxy into application code and add an explicit hosted participant integration option. Omitted routing retains legacy behavior for existing projects, deployed builds, preview sessions, and older CLI requests. New starters opt in; existing apps migrate explicitly after adding their own participant handler.

### 0.4.0 [#040]

#### Minor Changes [#minor-changes-103]

* d8a2572: Add project integrations with managed Discord bot installation in Console. Quests declare integrations and read membership through ctx.discord.member() without a server ID. Participants use basic Discord identity sign-in or linking. Capture identity and integration revisions for recovery, provide starter identity controls and failed-submission discard, and include the membership snippet and operator/agent guides.
* 8ead9ee: Add project-scoped points leaderboards with code-defined calculations, reviewed publication, participant rankings, and audited points updates. Support period windows, shared ranks, participation groups, and board exclusions.
* 1fb2edd: Add code-defined referral programs, session-bound invitation attribution, qualification rewards, milestones, and selective inviter point bonuses with correction recovery. Include participant and management APIs, project publication, Console controls, and agent references.

#### Patch Changes [#patch-changes-30]

* c475d40: Use the complete non-quest deployment schema for hosted build artifacts, preserving leaderboard and referral modules during preview activation and starter verification.
* 7204e04: Discover default-exported quests and quest types using patterns in relay.json. New projects include discovery patterns, and explicit entries can override deployment settings. Reject missing selected exports while bundling. Add editable photo, quiz, and staff-confirmation recipes through the public snippets registry.

### 0.3.0 [#030]

#### Minor Changes [#minor-changes-104]

* 53d6aef: Support existing application logins through a server participant adapter and project-scoped identity integration keys. Add Console provisioning, rotation and revocation, trusted participant routing, and an agent integration reference.

### 0.2.0 [#020]

No changes in this release.

### 0.1.0 [#010]

#### Minor Changes [#minor-changes-105]

* d46d47c: Introduce the Relay SDK, Domino CLI with bundled agent skills, and standalone local runtime.

## CLI [#cli]

### 1.66.1 [#1661-1]

#### Patch Changes [#patch-changes-31]

* d77e195: &#x2A;*Repeated refused credentials and unknown paths from one address are now refused with 429.** After an API server has refused one address (an IPv4 address or IPv6 /64) 60 times within 10 minutes for a missing or refused credential, a Git path that names no repository, or a path the API does not serve, it answers that address's management API, MCP and Git requests and unknown paths with 429 `rate-limited`, message "Too many refused requests. Try again later.", and `Retry-After` until the 10 minutes end, before checking any credential. A personal key or MCP token counts only when Clerk does not recognize it. Successful requests, Git's first credential-less request of a fetch or push, an MCP request without an MCP token, and a hosted app's own server calls never count. The CLI reports the wait as `details.retryAfter`; the troubleshooting reference explains how to recover.
* @domino-sdk/relay\@1.66.1

### 1.66.0 [#1660-1]

#### Minor Changes [#minor-changes-106]

* 165ca99: Harder sign-in.

  * **`domino login` codes now expire after 15 minutes, not an hour.** Console's approval page shows when the request was made, the country of the IP it came from and the CLI's version and OS, and warns to approve only a code from your own terminal. The CLI sends that description as its `User-Agent`. One client network (an IPv4 address, or an IPv6 /48) holds at most 20 pending sign-ins.
  * **Email sign-in codes name the app.** The email says which app and host the code is for, and to enter it only there. Only the latest code for an address works: an earlier one answers `code-expired`. Ten wrong codes for an address on a project in an hour, or twenty in a day, refuse `emailVerify` with `rate-limited` and `Retry-After` until the window ends, whatever new codes it gets. An address gets at most ten codes an hour from every app together; past that, `emailStart` answers `rate-limited`.
  * **Plugin OAuth connections finish in Console, as the person who started them (experimental).** The vendor's callback no longer connects by itself: it sends the browser to Console with a one-time `oauthCode`, and the new `management.connections.completeAuthorization(plugin, { code })` redeems it within five minutes, for the same person, project and environment only (403 `scope-mismatch` for anyone else, 409 `oauth-flow-expired` for a spent or expired code). Without `returnTo`, the browser returns to Console's Integrations page for the project. `authorize` now refuses automation, service and hosted app keys with 403 `credential-not-allowed`. New experimental exports: `connectionAuthorizationCompleteInputSchema` and `ConnectionAuthorizationCompleteInput`.

#### Patch Changes [#patch-changes-32]

* Updated dependencies \[165ca99]
  * @domino-sdk/relay\@1.66.0

### 1.65.0 [#1650-1]

#### Minor Changes [#minor-changes-107]

* 9a37fe5: Clear test data in hosted app storage. `domino storage clear sql DB --table tickets` (or `--all-tables --keep settings`) empties SQL tables and keeps their schema and `d1_migrations`; `domino storage clear kv PHOTOS --prefix photo:` (or `--all-keys`) deletes KV keys. `--dry-run` counts first, and without `--yes` the command asks in a terminal and refuses unattended. The management API has the same operation as `POST /management/v1/storage/{kind}/{name}/clear`, and `createManagementClient` as `storage.clear(kind, name, input)`. It works only in the test environment: live refuses with `test-environment-required`. It needs `publish`, and each clear is audited. A KV clear deletes at most `limits.apps.storageClearKeys` (10,000) keys a call and answers `complete: false` while keys remain; the CLI repeats until none do. Test member reset, purge and erase still leave app storage alone.

#### Patch Changes [#patch-changes-33]

* fabbf60: `domino doctor` labels a Worker app's preview by its kind. When `relay.json` declares `app.kind: "worker"`, the `--json` result now has `preview: "worker-app-configured"` instead of `"static-app-configured"`. Static apps and projects without an `app` keep the value they had.
* 7820fec: The Domino skill's campaign testing reference explains **Hide toolbar** in the staging toolbar's `⋯` menu: it hides the toolbar for the browser tab, leaving a small restore button in the corner, and while it is hidden `--domino-staging-toolbar-inset-bottom` is `0px` and `<html>` has no `data-domino-staging-toolbar`, so screenshots and layout checks see the live layout.
* a09e24b: The skill's schedule dispatch guarantees say what a run's `error` now records. A run whose build has no Worker fails, so the next tick still runs. An uncertain run names what Relay saw: the dispatch bridge's HTTP status and cause, no answer within the timeout, or the error that interrupted the send. An exception the app throws before answering is recorded there too, trimmed to 300 characters, so keep secrets out of thrown messages.
* Updated dependencies \[9a37fe5]
  * @domino-sdk/relay\@1.65.0

### 1.64.0 [#1640-1]

#### Minor Changes [#minor-changes-108]

* 0a4d580: Domino now deletes the evidence members submit (photos, text, links and quiz answers) 90 days after the attempt is decided, in test and live, unless you change it. Nothing was deleted when this shipped: evidence that already existed is first deleted 90 days after the change reached Domino, on 18 January 2027 for projects that existed on 20 October 2026, and 90 days after creation for newer projects. Attempts waiting for a decision keep their evidence. Each project's date shows in Console's project settings and in `noticeEndsAt`.

  To keep evidence longer, or until you delete it, change the period in Console's project settings, with `management.attempts.setEvidenceRetention({ evidenceRetention: { days } })` (1 to 3650 days) or `{ evidenceRetention: "keep" }`, or with `domino attempts set-evidence-retention --days N` or `--keep`. Changing it needs `publish`; shortening it, or switching from keep to days, also needs an organization admin or a service key. `management.attempts.evidenceRetention()` and `domino attempts evidence-retention` read it. Both methods are experimental. Each change is recorded in the project's access history, and each deletion pass as an `evidence-redacted-before` event with no actor and no quest. To delete sooner, or per quest, keep using `attempts.redactEvidenceBefore`.

* 0b1f8de: Operators can ban a member, and optionally take back what they earned. While banned, a member's attempts, completions and invitation acceptances are refused with 403 `member-banned`, their invitation code answers `invitation-not-found`, attempts waiting for a decision are rejected, tracker records that would count for them are skipped with `member-banned`, leaderboards and settlements leave them out, and referral payouts to and from them stop. `relay.me.get()` now answers `eligible`, false while the member is banned (true from older Relay versions).

  `management.members.ban(member, { reason, rollback })` bans, where `rollback` can revoke all their completions, reverse the referral payouts they received, and reverse manual awards, imported opening balances and engagement payouts, a step at a time in the background. `management.members.previewBan(member, { rollback })` previews it per point ledger, and `management.members.unban(member, { reason, restoreCompletions })` lifts it. Banning and unbanning need `review` permission; reversing referral payouts or point awards also needs `publish`. `members.get` answers the member's latest ban as `ban`, and member lists answer `bannedAt`. The CLI adds `domino members ban` (with `--rollback` and `--dry-run`) and `domino members unban`. Member bans are experimental. Member imports refuse rows naming a banned member with `member-banned`.

#### Patch Changes [#patch-changes-34]

* 166254b: App login now works on a private staging address. `createParticipantBackend` no longer sends `X-Relay-App-Origin` with its proxied calls, and Relay selects a request that carries an app login key by the key even when it also names an app origin, so a Test key registered for `https://<slug>--staging.domino.page` signs members in there without a workaround. Apps on SDK 1.63.0 work without upgrading. The `staging-access-required` error reference says a request with an app login key needs no staging pass.
* 28acc4c: The management skill reference says Console's Integrations page shows a card per plugin that opens a panel with its connection form.
* 59cf5db: `management.identityIntegrations.createIntoAppSecret(input, name)` and `addSecretIntoAppSecret(id, name)` create an app login key, or add a secret to one, and store the new secret as the hosted app secret `name` in the key's environment instead of returning it. The response has `appSecret` and no `token`. They need publish permission, and like every app login key change they work only from an organization administrator's Console session. When the app secret is refused, no key or secret is created. The CLI skill reference and capabilities describe Console's matching "Also save it as an app secret" option.
* 15b249d: Agents can act as test members on a `domino dev` preview, before staging. `POST /management/v1/staging/link` takes an optional `preview`, the preview URL `domino dev` printed, and returns a one-minute link on that preview that opens the test toolbar for the same operators as a staging link. The session works only on that preview host and only while it runs; the dev server never receives its cookie, and other visitors to the preview see no change. A URL that is not a running preview of the project answers 404 `preview-not-found`. The Domino skill's campaign testing reference adds the steps under "Check participant flows on staging", and the hosted reference points to them for checking the participant flow before staging.
* Updated dependencies \[166254b]
* Updated dependencies \[ff6c976]
* Updated dependencies \[0a4d580]
* Updated dependencies \[59cf5db]
* Updated dependencies \[7bd9a79]
* Updated dependencies \[0b1f8de]
  * @domino-sdk/relay\@1.64.0

### 1.63.0 [#1630-1]

#### Minor Changes [#minor-changes-109]

* 8c4b0c0: `domino checkout` accepts a project's slug, as `domino whoami --json` lists it, as well as its ID; a slug two organizations share needs `--organization`, and an unknown project now fails with `SCOPE_NOT_FOUND` before any repository request. Without `--json`, checkout shows Git's fetch progress, and a fetch that moves under 1 KB/s for 60 seconds fails with `CHECKOUT_GIT_FAILED` instead of waiting on a dead connection. Without `--json`, an invalid `relay.json` now lists each invalid field and its message under the error, as `domino doctor` does; the `--json` error is unchanged.

* 052744c: `inspectIntegrationKey({ baseUrl, integrationKey })` from `@domino-sdk/relay/portal-proxy` answers which app login key a `di_` key is: its `id`, `name`, `organization`, `project`, `environment`, `origin`, `identityKind`, `vouchesAccounts`, `secretExpiresAt` and `appOrigins`, the addresses Domino serves the project's app from in that environment. It calls the new `GET /v1/auth/integration`, never returns a secret, and does not count as a use of the key. An unknown, expired or revoked key throws `ParticipantExchangeError` with 401 `invalid-credential`. `identityIntegrationKeySchema` and the `IdentityIntegrationKey` type are exported from `@domino-sdk/relay/server`.

  Behavior change: `domino secrets set` now checks a value that starts with `di_` before storing it. It refuses, storing nothing and exiting 2 with `INVALID_ARGUMENT`, a key that is unknown, expired or revoked, or that belongs to another project or environment, such as a Live key set as Test's secret. It stores a key whose origin is not the app's own address for that environment with a warning, and a key it could not check (network failure, or a Relay without the route) with a warning. A checked key is named in the result's new `identityIntegration` field. Other values are stored as before.

* eaaae2a: Test members that `reset` refuses because they redeemed an offer, hold a reservation or a started reward handover, or took part in a referral can now be purged: `management.testMembers.purge({ actionId, members, reason })` and `domino test-members purge MEMBER... --reason TEXT` delete up to 500 test members in one transaction, in the test environment only. The stock unit each redemption, reservation and pickup handover took goes back to its stock item, so the offer's stock and redeemed count stay consistent, and the members' referrals and referral accounting go with them. List both sides of every referral: one with a member outside the list refuses the call with `test-member-reset-blocked` (`referrals affect another member`, naming the unlisted member in `outside`). Every other reason reset refuses still applies, and a refused purge changes nothing. The audit records one event with the operator, the members, the reason and the stock returned. `reset` is unchanged. See [Purge members reset refuses](https://console.domino.run/docs/use/quests#purge-members-reset-refuses).

* 69e017d: Erase a member on the person's request: `management.members.erase(member, { reason, actionId? })` and `domino members erase MEMBER --reason TEXT [--yes]` (`POST /management/v1/members/:member/erase`, `publish` permission, live and test). It can't be undone. The member's profile and uploaded avatar, sign-in identities, linked accounts and their tokens, app identities, wallets, sessions, consent, leaderboard aliases, referral code, the evidence of every attempt and the stored content of the tracker records their accounts authored are deleted; reasons people typed on the rows that stay read `[erased]`. The member ID keeps its points, completions, rewards and referrals. The result is `{ member, erasedAt, deleted }`; while an attempt waits for a decision or a reward handover is in progress it is refused with 409 `member-erase-blocked`. See [Erase a member on request](https://console.domino.run/docs/use/member-erasure).

  Member reads (`members.get`, `members.list`) carry a new `erasedAt` on an erased member, and `members.get` answers `profile: null` for it. After erasure, writes naming the member (points, badges, groups, campaigns, consent, profile changes) are refused with the new 409 code `member-erased`, records naming its ID attribute to no one, and the same person signing in again becomes a new member. A member import's revert passes over an erased member's row and reports it in `keptLinks` with reason `member-erased`. The CLI command asks for confirmation in a terminal and needs `--yes` otherwise.

* bbc3623: Klaviyo is the first destination plugin: connected through Klaviyo's OAuth, it keeps opted-in members' Klaviyo profiles current with `domino_` properties (points, ranks, quests completed, unredeemed rewards, campaigns), subscribes and unsubscribes them as their email consent changes (a backfill or resync never resubscribes someone Klaviyo has unsubscribed), and records Domino's events as `Domino …` metrics. It never calls Klaviyo's send, campaign or flow APIs. See the new guide, "Send reminders from Klaviyo".

  `management.connections` gains, experimental like the rest of it:

  * `connect(plugin, { config, dryRun: true })` connects a destination that supports a dry run, in the test environment, without secrets or the vendor's account. It records what it would have sent instead of sending it. `Connection.dryRun` and `PluginInfo.dryRun` say which. Refusals: 409 `dry-run-not-supported`, `dry-run-test-only`.
  * `activity(plugin)` reads a destination's delivery record: last sync, failing state, members synced and skipped per reason, the latest backfill or resync with its progress, and the latest 20 errors.
  * `resync(plugin)` sends every member's profile and attributes again, never past events; one runs at a time.
  * `dryRunLog(plugin, { cursor, limit })` pages through a dry run's requests, newest first.

  These answer 409 `not-a-destination` for other plugins. The CLI adds `domino connections connect --dry-run`, `connections activity`, `connections resync` and `connections log`.

* 9cb68e2: A text setting can be secret: `settings.text({ label, secret: true })`. Operators set its value in Console or with `management.quests.updateSettings`, and the quest's code reads it as usual, but no management response returns it. Releases leave secret settings out of `values` and `configuration`, and list the ones that hold a value in the new optional `withheldSettings`. So `domino deploy --json`, `--preview --json`, `quests list`, `releases get`, deployments, attempts and member reads no longer print a shared code a guest must not see. Console shows a secret as **Set** or **Not set**, with a field to replace it and **Clear**. A secret setting takes no default: one with a non-empty `default`, or a value from a deploy entry's `settings`, is refused, and reusable quest types refuse secret settings. A secret's entry in a release's `fields` says `secret: true`. Settings without `secret` behave as before. Withholding follows each release's own fields, so releases and deployments from before a setting became secret still show its old value: set a new one after marking an existing setting secret.

* 8ee88a9: Test members can redeem an offer: `management.testMembers.redeem(member, { offer })`, with the offer's key or ID, and `domino test-members redeem MEMBER OFFER`. The redemption runs as a signed-in member's would, with the same price, stock, per-member limit, requirements and refusals, writes the same redemption and entitlement, answers the redemption, and replays by action ID. It works in the test environment only, and the audit names the operator. A test member with a redemption can't be reset.

* 6fe6f22: The testing kit lists and replays every capability reader, and a few small defaults change.

  * **`capabilityCalls`.** `runQuest`, `createTestContext` and `replayAttempt` results gain `capabilityCalls`: every capability call as `{ sequence, request, outcome }`, with an open `CapabilityCallRequest` whose `kind` names the reader. It includes the calls `calls` left out: Telegram, NFT, contract call, delegation, wallet event and transaction reads, and the member's state. `calls` is deprecated.
  * **Replay divergences of any reader.** `replayAttempt` reports a call of any reader the capture cannot answer in the new `capabilityDivergence`, and stops there unless `facts` answers it. Before, such a call of a newer reader failed the replay with `TestSetupError`; now the replay resolves with `decision: null`. `divergence` is deprecated, and is `null` when the first divergence is a newer reader's call.
  * **Tolerant replay.** A captured answer with a field or an enum value a later Relay added, such as a new field on a Discord message, now replays instead of failing. Answers in `facts` are still checked strictly.
  * **Reader fakes.** `fakeWalletReader(facts)` and `fakeTelegramReader(facts)` from `@domino-sdk/relay/testing` build readers with every method Relay supplies, for tests that assemble their own evaluation context. The readers added after 1.0 stay optional in their types; quest code calls them with a non-null assertion, as the snippets now do (`ctx.wallet.nfts!(…)`, `ctx.telegram!.connection!()`), instead of guarding against a runtime that lacks them.
  * **Behavior change: Telegram trackers fire once per record.** `automatic()` on a `telegram.reactions()` or `telegram.messages()` tracker now defaults to `on: "new-record"` instead of `"new-observation"`, so a reaction removed and added again no longer runs the quest again. Published quests keep the mode they were published with; republishing one with this SDK switches it unless it sets `on`.
  * **`domino check` warns `TRIGGER_REFIRES_ON_REFRESH`** for a non-content quest on a YouTube Shorts or TikTok tracker that runs on every observation: each refresh evaluates it again for every video. Use `content({ tracker })` with `performance` for a metric threshold.
  * **`RelayProvider` takes `followWallet`.** `followWallet={false}` keeps a wallet sign-in's session when the wallet switches account or locks. The default, `true`, signs out as before.
  * **Behavior change: `unauthenticated` ends the session.** A participant backend's (`createParticipantBackend`) 401 `unauthenticated` now ends the member's session in the SDK's experience, progress and activity observers, as `not-signed-in` does, instead of reporting an error. `RelayErrorCode.Unauthenticated` names it.
  * **`enrollment` on `not-enrolled` skips.** Tracker trigger outcomes, `relay.quests.outcomes()` and automatic outcomes carry an optional `enrollment` on `not-enrolled` skips: `"quest"` for the quest's own enrollment, `"campaign"` for its campaign's. A campaign skip on a tracker trigger now says "Member has not enrolled in the quest's campaign".
  * **Naming.** A leaderboard's `balance` is the ledger it ranks and its points pool's `ledger` the one the pool pays into; both are documented on the type. New fields that name a ledger are called `ledger`; existing `balance` fields keep their name in 1.x.

* 561f4ab: Tiered rewards (`defineTieredReward`) can be listed, archived and restored. `management.resources.list()` and `domino resources list` list them in a new `tieredRewards` array (`managedTieredRewardSchema`), beside `offers`, and `domino tiered-rewards list` lists them alone. The experimental `management.tieredRewards.archive` and `unarchive` (`POST /management/v1/tiered-rewards/REWARD/archive|unarchive`) and `domino tiered-rewards archive|unarchive KEY --reason` archive and restore one in test, by key or ID, with `publish` permission; a promotion carries it to live, and Console **Settings → Resources** lists them with the same controls. Each returns `{ resource, changed }`, typed by `tieredRewardLifecycleResultSchema`. 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`, naming it in `reward`. What already granted it keeps granting it, its entitlements and handover keep working, and its key stays taken. Console **Rewards** leaves archived tiered rewards out of its rewards. Promotions now list tiered rewards as definitions of kind `tiered-reward`, and a rollback archives one the earlier promotion did not carry.

* 8d79fd6: Four operations that had two public paths with different results now agree, or say how they differ:

  * `management.deployments.promote`, `submit` and `reject` are deprecated, removed in 2.0.0. `promote` prepares a promotion of all of test and publishes nothing; its JSDoc now says so, and that the answered `id` is the promotion's, read with `promotions.get` and rejected by `deployments.get` and `publish`. Use `management.promotions.prepare()` with a client for test, and `promotions.reject` to decline. `submit` and `reject` act only on live deployments made before live refused them. `domino deploy --promote` is deprecated in favor of `domino promote --environment test`, and prints a warning naming it.
  * `management.testMembers.acceptAnyway(member, { attempt, rules: "live", reason })` applies live `attempts.acceptAnyway`'s rules in test: `reason` is required, and an archived quest, a superseded attempt, a completion cooldown, an exhausted budget, an existing or revoked completion and a used key are refused with live's 409 codes, leaving the attempt and balances unchanged. The default (`rules: "test"`) still accepts those, with no reason, so a tester can get a test member unstuck. A `reason` is recorded with the decision either way. `domino test-members accept-anyway MEMBER ATTEMPT --live-rules --reason TEXT` sends it.
  * `GET /v1/reward-deliveries` and `GET /management/v1/reward-deliveries` answer `items` beside `deliveries`, with the same rows, so `paginate((cursor) => relay.me.rewardDeliveries({ cursor }))` and `management.rewardDeliveries({ cursor })` read every page. Both clients fill `items` from `deliveries` when an older Relay sends only `deliveries`. `useExperience().deliveries` reads the role deliveries again on each `delivery.updated` activity event, so a role an operator revoked shows without waiting for focus.
  * `defineLeaderboard` prints a warning, and `domino check` reports a `LEADERBOARD_TIES_DIFFER` warning, when a board's live `ties` differs from its `settle.ties`, including a board that sets only `settle.ties: "earliest"`, whose standings share ranks while its prizes pay first-to-reach. `leaderboardTiesWarning(board)` from `@domino-sdk/relay/authoring` returns that message, or null. `domino check`'s `diagnostics` now include warnings from data definitions beside its errors; only errors fail it.

#### Patch Changes [#patch-changes-35]

* a7ede97: The Domino skill references link to the new [access](https://console.domino.run/docs/concepts/access) page, which lists every kind of credential, the roles and permissions behind it, and what each may do in live, instead of restating the CLI's live limits in the error codes reference. The analytics, on-chain actions and campaign-testing references link to their matching guides. No command or behavior changes.

* d5fe3db: `domino dev` prints "Hosted test quests are up to date." when a quest update succeeds after a failed one without publishing anything new, for example after reverting the change that broke the build. Console's Playground reads it to clear the build error its Map tab shows.

* 1fa78d2: `limits` reads Relay's platform limits as `number`, so a 1.x release can raise one or add one (released SDKs keep checking the values they shipped with, so Relay answers them within those): `limits.quests`, `limits.questTypes`, `limits.collections`, `limits.sdkRuntime`, `limits.memberCompletions`, `limits.dashboardPanels`, `limits.dashboards`, `limits.defer`, `limits.retryCooldown`, `limits.analyticsQueryRows`, `limits.apps` (secrets, KV and SQL stores, migrations, storage browsing and schedules), `limits.stagedDeployment`, `limits.capabilityCalls`, `limits.trackerData`, `limits.trackerPreview`, `limits.walletsPerChain`, `limits.walletEventLogs` and `limits.walletTransactions`. It is exported from `@domino-sdk/relay/schema`, `/authoring`, `/server` and `/testing`, with its `Limits` type.

  Deprecated, with unchanged values and types: the literal-typed limit constants `limits` replaces, which can never change in 1.x. They are `questScaleLimits`, `sdkRuntimeLimits`, `maxMemberCompletions`, `dashboardLimits`, `deferPolicy`, `retryCooldownPolicy`, `analyticsQueryLimit`, `APP_SECRET_LIMIT`, `APP_KV_LIMIT`, `APP_SQL_LIMIT`, `APP_MIGRATION_LIMIT`, `APP_MIGRATION_FILE_BYTES`, `APP_MIGRATION_TOTAL_BYTES`, `APP_STORAGE_ROW_LIMIT`, `APP_STORAGE_PREVIEW_CHARACTERS`, `APP_STORAGE_CELL_CHARACTERS`, `stagedDeploymentLimits`, `APP_SCHEDULE_LIMIT`, `APP_SCHEDULE_MIN_INTERVAL`, `APP_SCHEDULE_TIMEOUT`, `APP_SCHEDULE_RESULT_BYTES`, `maxCapabilityCalls`, `trackerDataMaxBranches`, `trackerDataMaxEntries`, `trackerPreviewLimits`, `WALLETS_PER_CHAIN`, `maxWalletEventLogs` and `maxWalletTransactions`.

  Deprecated and removed in 2.0.0: Relay's own implementation helpers, which apps and quests have no use for. From `@domino-sdk/relay/server`: `participantRoutes`, `participantRoutePattern`, `sequenceCursorSchema`, `sourceDataSchema`, `trackerDataIssue`, `trackerDataIssues`, `whereSchemaIssue`, `validateQuestShape`, `containsReferralIntegrationReward`, `REFERRAL_INTEGRATION_REWARD_ERROR` and `nextScheduleRun`. From `@domino-sdk/relay/schema`: `dashboardIssue`, `normalizeDashboard`, `compatibleSettingKinds`, `isBoundSetting`, `whereSettings` and `sourceContent`. They keep working until then.

  `z` from `@domino-sdk/relay/authoring` stays zod 4 for all of 1.x.

* 4842e30: Wording only. The app identity kind example is now `app:game`, including the validation message "Name an app identity kind like app:game: …", since a Telegram account is vouched for rather than modelled as an app identity. The JSDoc of `progress()`, `quests.claim`, `me.claim` and the quest interactions says which meaning of "progress" and "claim" each uses, and `domino test-members pass` help calls `progress(...)` quests counter quests. Examples use the `@XDevelopers` X account. The changelogs mark the versions that were never published to npm.

* 425e4df: Internal: the "HTTPS, or plain HTTP on a loopback host" check for API URLs, sign-in URLs and portal proxy origins now has one implementation, shared through `@domino-sdk/relay/internal`. Published behavior is unchanged.

* 41f63cb: More CLI commands now call the SDK management client instead of building management routes by hand: `groups`, `domains`, `webhooks`, `schedules`, `secrets`, `storage`, `stock-items`, `leaderboard-settlements`, `members identities` and `identity-links`, `quests enrollments`, `quests replay`, and every read of the signed-in credential's access. Their responses are checked against the SDK's schemas, and a malformed answer fails with `INVALID_RESPONSE` instead of being printed. `--json` output is unchanged.

  A few things behave differently:

  * `domino groups list` and `domino groups members` refuse a `--limit` outside 1–100 with `INVALID_ARGUMENT` (exit 2), as the other paged commands do. Before, any whole number was sent to the API.
  * An ID, cursor or body that the SDK rejects now fails with `INVALID_ARGUMENT` (exit 2) before anything is sent, where the API used to answer 400 or 404.
  * `domino webhooks replay`, `pause` and `resume` send a generated action ID when you don't pass `--action-id`, so a retry after a transient failure is applied once.
  * Paged reads always send the route's default `limit`.

* e71245d: `createParticipantBackend` accepts `onExchangeError(error, request)`, called with each refused sign-in exchange as Domino answered it (a `ParticipantExchangeError` with `status`, `code`, Domino's `message` and `retryAfter`) before the browser gets its answer, so the app's server can log why sign-in failed. The browser's answer is unchanged, also when the callback throws.

  Behavior change: creating an app login key for one of Domino's own app addresses in the environment that address does not serve is now refused with 400 `invalid-request`, and the message names the right environment. A `<project-slug>--staging.domino.page` or preview address belongs in Test, a live `<project-slug>.domino.page` address in Live. Other origins, custom domains included, can still use either environment, and existing keys keep working. An origin that is a URL but not an exact HTTPS origin is refused with a message that names the origin it accepts ("Use the exact origin [https://app.example.com](https://app.example.com), without a path, query or trailing slash.") or "Use an HTTPS origin." instead of "Use an exact HTTPS application origin"; over HTTP the management API now answers that message instead of "Invalid management request". The app login guide and the Domino skill references gain a troubleshooting section with the exchange refusal codes.

* ff2e96d: The CLI skill references and the `RELAY.md` that `domino init` writes now show commands that run as written: the wallet-balance tracker example passes its key, title and start (`domino trackers create KEY --title T --source wallet-balances --chain C [--token T] --start MS`), the stocked-offer test reads redemptions with `domino redemptions history` instead of the deprecated `domino redemptions list`, `RELAY.md` says `domino quests list` and `domino releases list` instead of the deprecated bare `domino quests` and `domino releases`, and the authoring reference lists every warning `domino check` reports (`LEDGER_REFERENCE_DYNAMIC`, `LEDGER_DECLARATION_MISSING`, `AUTHORING_IDENTITY_DYNAMIC` and `LEADERBOARD_TIES_DIFFER` were missing).

* 8278803: Relay now refuses a malformed personal API key ID with 400 `invalid-request` ("Use the key's ID, such as ak\_…") on `DELETE /management/v1/tokens/:id` and `PUT /management/v1/tokens/:id/scope`. A key ID is `ak_` followed by letters, digits or `_`. IDs from `tokens.list()` and `domino tokens list` already have that form, so no working call changes: an unknown `ak_…` ID still revokes as a silent success, and a key that belongs to someone else is still refused with 403.

* eb8572b: The testing kit's participant API answers an activity cursor it cannot read as Relay does: 400 `invalid-cursor`, "Invalid cursor. Start from the first page.", instead of `invalid-request`.

* Updated dependencies \[cc04dae]

* Updated dependencies \[1fa78d2]

* Updated dependencies \[4842e30]

* Updated dependencies \[425e4df]

* Updated dependencies \[8515f3f]

* Updated dependencies \[052744c]

* Updated dependencies \[e71245d]

* Updated dependencies \[eaaae2a]

* Updated dependencies \[69e017d]

* Updated dependencies \[bbc3623]

* Updated dependencies \[8278803]

* Updated dependencies \[9cb68e2]

* Updated dependencies \[8ee88a9]

* Updated dependencies \[6fe6f22]

* Updated dependencies \[eb8572b]

* Updated dependencies \[561f4ab]

* Updated dependencies \[8d79fd6]
  * @domino-sdk/relay\@1.63.0

### 1.62.0 [#1620-1]

#### Minor Changes [#minor-changes-110]

* a341b16: Analytics counts Relay's own `key-used` rejection, of an attempt whose completion key another member's completion holds, as its own rejection source. `rejectionsBySource` gains `keyUsed`, and `analytics.query`'s `source` breakdown a `keyUsed` row; `evaluator` no longer includes them, while an evaluator's own `key-used` still counts there. Each `topReasons` item carries `codeSource` (`relay`, `evaluator`, or null without a code), and Relay's `key-used` and an evaluator's are separate rows, in `topReasons` and in the `reason` breakdown. Both fields are optional in the schema, since older servers don't send them. `domino analytics get` marks the code of a top reason Relay wrote, such as `[relay key-used]` or `[relay budget-exhausted]`.

* 5233be5: The experimental billing results read the plan as `BillingPlan` (`KnownBillingPlan | (string & {})`): `subscription.plan` and `scheduledChange.plan` in `management.billing.read()`, `cancel()`, `resume()` and `organization()`, `billingSubscriptionSchema`, `projectBillingSchema` and `organizationBillingSchema`, and the `--json` results of `domino billing show`, `billing organization`, `billing cancel` and `billing resume`, so a plan Relay adds no longer fails to parse. Handle a plan name this SDK does not know. The stable `management.usage.plan()` and `domino usage plan` are unchanged: the `subscription` they carry keeps the plans this SDK knows (`launch`, `growth`, `scale`).

* 3665a4e: Billing is experimental while it is open to Domino staff only: the `management.billing` methods, the billing schemas and types in `@domino-sdk/relay/server`, and the `domino billing` commands may change in a minor release, plan names included. `domino billing` now appears under **Experimental commands**. `management.billing.checkout()` and `changePlan()` take any plan name (`BillingPlan`, `KnownBillingPlan | (string & {})`), and `domino billing checkout` and `billing change` take any `--plan`; Relay refuses a plan it does not offer with 400, as before. `management.usage.plan()` and `domino usage plan` stay stable.

  The CLI's JSON result schemas now leave objects open, as documented, so a JSON Schema validator such as Ajv accepts fields a later release adds; only the variants of a `oneOf` stay closed. `webhooks deliveries`, `storage rows` and `storage keys` results declare the optional `nextCursor` the API already sent. `secrets`, `storage` and `schedules` results include `environment`, and these commands warn on stderr when `RELAY_ENVIRONMENT` or `relay.json` names live while they use test. `contract.json` marks deprecated options with `deprecated`, as it does commands, and `capabilities.json` no longer lists deprecated commands. `management.referrals.control` and `referralControlSchema` now point to `pause` and `resume` instead of the deprecated `suspend`.

* f03828d: `domino deploy --only <quest-key>` (repeatable) deploys just the named quests, with the SDK runtimes they import, the reference images their code names and `supportedInteractions`. Every other quest keeps its current release and every other resource stays as published, so parallel worktrees can 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.

* 361a5b1: A deployment keeps the active release of a quest it doesn't change, instead of publishing an equal one with a new ID. Deployment previews and results carry `questChanges`: each quest by key, `created`, `updated` (with the release it replaces) or `unchanged`. `domino deploy` lists the created and updated quests in its confirmation and in the `--preview --json` summary, so a deploy that would replace a quest you didn't touch shows before it publishes.

* 68efdac: `domino stage` warns when the project has leaderboards (declared in `relay.json` or published in test) but its test profile settings are private or name no default account, so every leaderboard row shows a generated name such as `Participant 1a2b3c4d`. The warning goes to stderr and, as `generated-leaderboard-names` with the `domino profiles configure` command that shows members' names, into the JSON result's `warnings`; the build still stages. `domino doctor --remote` reports the same as its `leaderboard-names` check. The Domino skill now says where leaderboard names come from and to ask the owner before showing them.

* 90f9465: `domino integrations discord get` reads the project's Discord connection, matching the `get` name other single reads use. `domino integrations discord status` still works, prints a deprecation warning outside `--json`, and is removed in 2.0.0. `domino collections list` lists the project's collection slots in the environment, archived ones included, as `{ collections: [{ id, key, title, archivedAt }] }`.

* 98ad384: A member import carries its real state in a new `state` field: `open`, `checking`, `checked`, `applying`, `applied`, `stopped`, `reverting`, `reverted` or `canceled`, typed open (`KnownMemberImportState | (string & {})`, with `MemberImportState` listing the known values), so Relay may add states. `status` is unchanged and still reads `applied` for a stopped, reverting or reverted import, so code filtering on `status === "applied"` counts those too; read `state` instead. Import rows gain `reverted`, true once the import's revert went through the row. Both fields are optional in the schema, since older servers don't send them. `domino import` and `domino imports` results include `state`, and the CLI polls on it.

  `MemberSummary` (`relay.me()`, `useMe()`) now types `email`, `emails` and `clock` as required, since Relay always sends them: code no longer needs to check them for `undefined`. The SDK reads a `/v1/me` response from a Relay older than these fields with `email: null`, `emails: []` and a `clock` at real time (offset 0), the same as code treating them as absent did. `memberTime()` still accepts an object without `clock`.

  `MemberSummary`, `LeaderboardPage`, `LeaderboardHistoryPage`, `Badge`, `MemberBadge` and `MemberBadges` are marked as response types Relay may add properties to.

* 4bf82e5: CLI results and options follow the management API conventions more closely:

  * Every command in `contract.json` has a `recovery`: `resume`, `replay`, `reconcile`, or `null` for a command that changes nothing in the API. It says what to do when the command exits 3 with `OUTCOME_UNCERTAIN`; `recoveries` in `contract.json` describes each value. Every `OUTCOME_UNCERTAIN` error that names no saved operation now carries `details.recovery: "reconcile"`, as the automation reference promised; before, only `domino api` and `tokens create` set it.
  * `offers pause|resume`, `quests pause|resume`, `redemptions pause|resume`, `redemptions cancel` and `entitlements restore` take `--expected-revision N`. `--revision N` is its deprecated name, removed in 2.0.0, and keeps working; passing both with different numbers exits 2 with `INVALID_ARGUMENT`.
  * `webhooks replay` takes `--action-id <uuid>`. Relay keeps a receipt for it, so a retry with the same ID answers the delivery the first replay queued instead of queuing another, and the CLI sends it again after a lost answer.
  * `tokens list --json` answers the keys in `items` as well as `tokens`.
  * `test-members advance --to` also takes epoch milliseconds.

* ea2c9be: A retention job can delete the evidence of a quest's attempts decided before a cutoff: `management.attempts.redactEvidenceBefore({ quest, decidedBefore, reason })` (`review` permission; in live it also requires an organization administrator's Console session or personal key, or a service key, and refuses any other caller with `403 permission-required`, because the deletion can't be undone) deletes up to 100 attempts' evidence per call, oldest decision first, exactly as `attempts.redactEvidence` does for one, and answers `more: true` while others remain. Attempts still waiting for a decision are skipped, and a `decidedBefore` in the future is refused with `400 invalid-request`. Each call records one `evidence-redacted-before` audit event. `domino attempts redact-evidence-before --quest QUEST --decided-before MS --reason TEXT` repeats the call until nothing is left and prints the total.

* e58c2ae: Name a member's account the way a member import does, everywhere an operator command takes one: `{ kind: "x" | "discord" | "telegram", subject }` with the numeric user ID, `{ kind: "app:<name>", subject }`, or `{ kind: "wallet", chain, address }` (`identityReferenceSchema`, `IdentityReference`).

  * `members.identityStates.unlink` and `relink` take it as `identity` (`memberIdentityStateChangeInputSchema`, `MemberIdentityStateChangeInput`); `setSignIn` also takes a Cosmos wallet with its `network`. An X or Discord subject names the member's selected account with that subject, else the only one they hold. A subject the member holds through several issuers, none of them selected, fails with 409 `identity-ambiguous`; name it by `issuer` then.
  * `members.lookup` takes it, and sends it as the `provider` and `subject` every Relay version reads.
  * `testMembers.linkAccount` and `unlinkAccount` take `{ actionId, kind, subject }` for an X or Discord account.
  * The CLI takes `--kind` and `--subject` on `domino members lookup` (with `--chain` and `--address` for a wallet) and on `domino test-members link-account` and `unlink-account`.

  Deprecated, removed in 2.0.0: the `{ kind: "oauth", provider, issuer, subject }` and `{ kind: "app", appKind, subject }` input selectors, `memberIdentityLinkChangeInputSchema` and `MemberIdentityLinkChangeInput`, `members.lookup({ provider, subject })`, the test-member `{ provider, id }`, and the CLI's `members lookup --provider` and `test-members link-account|unlink-account --provider|--id`. They keep working through 1.x; `contract.json` marks the deprecated options with `deprecated`.

  Identity reads gain an optional `source`: `member`, `import`, `app-login` or `test-link`, an open string (`IdentitySource`, `KnownIdentitySource`), and `import`, the ID of the member import that attached the identity when Relay recorded it. Identity states of every kind carry them, and so do `members.get`'s `wallets` and `identities` and the deprecated `oauthAccounts` and `telegramAccount`. `imported` is unchanged.

* 3a3feef: A leaderboard can rank its live standings first-to-reach. `defineLeaderboard({ ..., ties: "earliest" })` orders equal scores by each member's latest qualifying entry, earliest first, then by member ID, and gives every member who scored their own rank: the order a settlement with `settle.ties: "earliest"` freezes. Standings pages, `me`, `nearby`, placement, and the management and CLI standings reads all follow it, so a tie-broken board is one paged read.

  `ties` is optional. Omitted or `"shared"`, equal scores share a rank as before, so published boards rank unchanged. It is independent of `settle.ties`; set both on a board that settles so members see the order prizes follow. Members listed through `includeUnscored` still share one rank after every member who scored.

* 10826dc: An organization administrator's CLI session lists the project's identity integrations (app login keys) and their claimed kinds: `domino identity-integrations list` and `domino identity-integrations kinds`, or `management.identityIntegrations.list()` and `kinds()`. They return each key's origin, identity kind and secrets' expiry and last use, never a secret. Creating, rotating and revoking keys and their secrets stays in Console; non-admin CLI sessions and every key type are refused.

* 80e09c2: Experimental: operators read a member's marketing consent. `management.members.consent(member)` lists the member's current consent, one entry per channel they chose on, and `management.members.consentHistory(member, { cursor, limit })` pages through every change on every channel, newest first, as `{ items, nextCursor }`; each entry is `{ member, channel, state, at, wording, source }`, and `memberConsentHistorySchema` and `memberConsentHistoryQuerySchema` are exported from `@domino-sdk/relay/schema`. Both need `read` permission (`GET /management/v1/members/:member/consent` and `/consent/history`); a member who never chose reads as empty, and an unknown member fails with 404 `not-found`. `domino members consent MEMBER` prints both, with `--limit` and `--cursor` for the history. The Console member page shows the same, read-only. There is no management write: only the member grants or withdraws consent. The consent API is `@experimental` and can change in a minor release.

* 1c61073: `domino tokens list|create|scope|revoke` manage your personal API keys from a `domino login` session, as `management.tokens` does. `domino tokens create --name NAME --permission P [--ingest-tracker T] [--days N]` issues a key for the selected project and environment and prints its one-time `token` and expiry; `--json` prints the SDK's answer, so `domino tokens create … --json | jq -r .token | domino secrets set NAME` stores it. A personal key expires after `days`, 1 to 90, default 30; `--days 91` is refused before anything is sent. `domino tokens list` shows each key's expiry. API keys still cannot manage keys (`credential-not-allowed`), and a credential that must not expire is a service key, which an organization admin creates in Console.

* 423b6fc: Plugin connections, experimental. `management.connections` (`list`, `get`, `connect`, `update`, `disconnect`) connects a project to the plugins Domino writes, separately in test and live; a promotion never copies a connection. A connection's config is checked against the plugin's JSON Schema (422 `invalid-config` names the failing `path`), its secrets are write-only and sealed at rest, every change takes an `actionId`, and `update` and `disconnect` take an `expectedRevision` (409 `connection-changed`). The new exports (`connectionSchema`, `connectionPageSchema`, `Connection`, `ConnectionStatus` and the connect, update and disconnect inputs) are experimental and may change in a minor release. The CLI adds the experimental `domino connections list|get|connect|update|disconnect`, test unless `--environment live`, reading secrets from stdin as JSON or at hidden prompts, never from arguments.

* 2a83792: Plugin OAuth connections, experimental. A plugin whose `plugins.list()` entry has `oauth` connects through its vendor's OAuth instead of entered secrets: `management.connections.authorize(plugin, { config, returnTo?, actionId? })` answers `{ authorizationUrl, expiresAt }`, a person opens the URL and grants access, and the vendor's callback makes the connection `ready`. The URL works once, for ten minutes. Domino refreshes the tokens before they expire and never answers them; when the vendor refuses a refresh, the connection is `attention` with the new `reconnectReason` (`revoked` or `expired`), and authorizing again reconnects it. Disconnecting deletes the tokens, then revokes them at the vendor; a failed revocation is logged. `connect` refuses an OAuth 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` (`oauth.configured` is false). New experimental exports: `connectionAuthorizeInputSchema`, `connectionAuthorizationSchema`, `ConnectionAuthorizeInput`, `ConnectionAuthorization`, `ConnectionReconnectReason`, `KnownConnectionReconnectReason` and `knownConnectionReconnectReasons`. The CLI adds the experimental `domino connections authorize <plugin> --config-json …`, which prints the authorization URL.

* d64625c: `domino quests list --lifecycle <state>` prints only the quests in that lifecycle state: `active`, `paused` or `archived`. Repeat the flag for more than one state. Any other value exits 2 with `INVALID_ARGUMENT` before a request is sent. Without the flag, the list is unchanged; each quest in it carries `lifecycle`.

* b2b8127: A full deployment to test warns `quests-not-in-bundle` with the keys of quests published from code that it leaves out, such as a starter's sample quests whose files were deleted: they stay as published until archived with `domino quests archive <key> --reason "…"`. Archived quests and quests created in Console are not listed. A deployment input with `partial: true` doesn't warn, and `domino deploy --only` sends it. `domino stage` now prints the warnings of the deployment its build published, and its `--json` result carries them in `warnings`; a build's status (`GET /builds/{id}`) names that deployment in `deployment`.

* a381dde: Staff can delete the evidence a member submitted for a decided attempt: `management.attempts.redactEvidence({ attempt, reason })` and `domino attempts redact-evidence ATTEMPT --reason TEXT` (`review` permission, test and live). The photo, text, URL or quiz answers, and what the quest's capabilities read from them, are deleted; the attempt keeps its outcome, completion and rewards. Attempts that submitted the same photo are redacted together. `management.attempts.get` answers the new `redaction` (`{ at, actor, reason }`). Undecided attempts are refused with `attempt-not-decided`, and `attempts.retry` refuses a redacted attempt with `evidence-redacted`.

* c8452bc: Add `domino starters export <template> --directory <dir>`, which writes the starter that `domino create` uses today, including `.domino/starter.json`, into a new or empty directory so an existing hosted project can move to a newer starter.

* 7fa2a06: Test members can submit photo quests. `management.testMembers.uploadEvidence(member, file, { actionId })` uploads a JPEG or PNG of at most 5 MB as the test member's photo, and `act` submits its `id` as `evidence`. `domino test-members act MEMBER QUEST --photo FILE` does both, so the quest's real image check and the review queue can be proven from the CLI. The campaign-testing reference now points photo evaluator tests at `facts.images.review`.

* 4391bad: Every `domino test-members` change takes `--action-id ID`: any action ID the management client accepts, sent as the change's `actionId` without saving an operation, so the CLI and the management client can retry the same action.

#### Patch Changes [#patch-changes-36]

* 8439ff6: The Domino skill references and `capabilities.json` now match shipped behavior. Console shows dashboards on Analytics. Webhooks list all nine event types. A new `quest-announcements` capability covers quest completion announcements. The trackers reference lists `pause`, `resume`, `refresh` and `triggerOutcomes`, says the plan's minimum refresh interval applies to every collected source, and explains that a spent quest budget rejects the attempt instead of skipping the observation. Deprecated methods are no longer recommended: use `useRedeemOffer().mutate` and `members.identityStates.relink`.

* ee3f488: Bulk management writes (`catalog.deploy`, catalog previews and drafts, `deployments.preview`, `releases.publishBatch`, tracker record ingest and `memberImports.addRows`) refuse a body with more than 2,000,000 JSON values with 413 `payload-too-large` before parsing it, whatever its byte size; other management writes refuse one past 500,000. A server handles one body over 4 MiB or 500,000 values at a time and answers another 503 with `Retry-After` when it has waited 10 seconds. `memberImports.addRows` now takes at most 64,000,000 bytes, like the other bulk writes, so a single batch above 64 MB from an older CLI gets 413. `domino import` now splits rows into batches of at most 500 rows, 16 MB and 1,000,000 JSON values, sends a row larger than that in a batch of its own, and refuses a row too large for one call (63 MB or 1,990,000 values) before uploading anything, naming its line.

  The management client, the CLI and the participant client now send any call again after a 503 with `Retry-After`, within its deadline and attempts, including writes without an action ID: Relay answers such a 503 only to a request it refused before doing anything. Before, those writes were sent once.

* f13cfb6: A `domino login` CLI session creates and scopes personal API keys in test only. `management.tokens.create` with `environment: "live"`, and `management.tokens.scope` on a live key, now answer 403 `console-approval-required` from a CLI session, whatever `X-Relay-Environment` selects: a live key would carry the live operations (points, attempt confirmations, reviews, pauses) that the CLI session itself is refused, and would outlive `domino logout`. Create and change live API keys in Console (**Settings → API keys**). A CLI session still lists its keys and revokes any of them, live included, and Console sessions are unchanged.

  This changes a 1.x behaviour, an exception Peter Szoke granted on 2026-10-15: no request or response shape changes, and the refusal is a typed 403 that says where to go. `domino tokens create --environment live` prints that refusal, and `domino tokens --help` says live keys come from Console.

* c391c86: `domino deploy --promote <id> --environment live` runs in the checkout's scope, from `relay.json` like `domino promote`, instead of failing with `SCOPE_REQUIRED`. It starts a new operation rather than resuming one; a promotion whose response was lost is still recovered with `domino operations resume <id>`, in the scope it journaled. `domino deploy --resume` and `domino stage --request-id` still require every scope key as a flag or `RELAY_*` variable.

* 154af63: Discord message and reaction records name a thread's parent channel: `DiscordMessageData` and `DiscordReactionData` gain `parentChannelId`, the channel a thread belongs to (a forum post's forum), or `null` outside a thread. It is absent when it is unknown, because Discord's channel read failed. `channelId` stays the thread's own ID. A trigger's `where: { path: "parentChannelId", equals }` selects a channel's threads; to count a channel and its threads, compare both fields in the evaluator. `management.testMembers.record` and `domino test-members record --data` take `parentChannelId` for Discord records.

* 550b610: `domino doctor --remote`'s `participant-api` check, its `scope.participantApi`, and `domino dev` read the `apiUrl` written in relay.json, not the management URL a checkout scope (`domino checkout --api-url`) keeps in the Git directory. A checkout made with `--api-url https://console.domino.run/relay` and a relay.json pointing at `https://relay.domino.run` is now healthy instead of reporting relay.json as wrong.

* b83512e: `domino deploy` reports a timeout or lost connection while preparing its preview as `TIMEOUT` or `NETWORK_ERROR` (exit 1) and says nothing was published, instead of `OUTCOME_UNCERTAIN` with no operation to resume.

* 5e635dc: The CLI requires Node 24.5.0 or later (`engines.node` is `>=24.5.0 <25`), because its first request loads system certificate authorities with `tls.setDefaultCACertificates`, which Node 24.5.0 added. On an earlier Node 24 release `domino login` and every other command now exit 1 with `Domino CLI needs Node 24.5.0 or newer` instead of failing the first request with `Could not reach the API: setDefaultCACertificates is not a function`. The `NODE_VERSION_UNSUPPORTED` error adds `details.minimumVersion` (such as `"24.5.0"`); `details.minimum` stays the major version.

* 986f989: `domino login` says when its code expires, and the `--json` `browser-login` event carries `expiresAt` (epoch milliseconds). A sign-in request now lasts 60 minutes instead of 10, so the person approving it can come back to the link later.

* 9ebd5dc: `domino create` waits for a slow repository setup instead of stopping at the 30-second request deadline. When a step times out, loses its connection, or gets a 502, 503 or 504, it resumes the same saved operation for up to 5 minutes and then prints the normal result. Without `--json` it says once on stderr that it is still setting up the project. Past 5 minutes it fails as before, with the operation to resume and a message in plain words: Domino may still be setting up the project, or the API stayed unavailable.

* f204520: `domino stage` ends its human output with who can open the staging URL: staging is private and runs on test data, reviewers outside Console open it with a staging password (Console **Settings → Staging access**, or `domino api PUT /staging/access`), and members see the app only once a promotion makes it live. `--json` output is unchanged. The Domino skill's hosted reference shows the `PUT /staging/access` body and tells agents to ask the owner before setting or changing the password.

* 44a083c: The Discord capability tells agents that a Console administrator without Manage Server can send a one-time install link (Send to your server admin, valid seven days) to whoever manages the server, who needs no Domino account.

* acad848: `domino doctor` outside a project reports setup health only: no relay.json is a `manifest` warning instead of an error, the skill loader check and the `agents` field are left out, and a machine with nothing else wrong reports `healthy: true` and exits 0. Inside a project nothing changes. Human output labels the loader field "Skill loaders", since it lists the Domino skill loaders installed in the project, not which agent is running doctor.

* 8b95b72: The Domino skill's `trackers` reference now covers declaring trackers: maintained X datasets, rewarding X posts, tracker triggers, content admission and performance gates, daily engagement points, and the Discord, Telegram, YouTube Shorts, TikTok and wallet balance sources. They moved there from the `authoring` reference, which links to them, so `domino reference trackers` serves them and an agent writing a quest no longer loads them.

* 2112bd9: The Domino skill has a new `verification` reference for checking what a member did: the evaluator reader rules (null results, errors and pinning), declared integrations, Discord membership, X, linked wallets, wallet balances, NFT holdings, contract state, Cosmos delegations, on-chain actions, text and URL submissions, AI text review, image checks, staff confirmation, and entry after app-owned pass verification. These sections moved there unchanged from the `authoring` reference, which links to them, so `domino reference verification` serves them and an agent writing a quest loads them only when it needs them.

* a06d381: The Domino skill has a new `quest-rules` reference for how a quest decides and repeats: campaigns, windows and prerequisites, automatic and repeatable quests and republishing them, prerequisite automatic outcomes and test members, reject codes, deferred evaluation and progress reports, distinct-action progress counters, retry and completion cooldowns, evaluation time and randomness, calendar-day streaks, and testing quests locally. These sections moved there unchanged from the `authoring` reference, which links to them, so `domino reference quest-rules` serves them and an agent loads them only when it shapes quest behavior.

* d820a34: The Domino skill has a new `management` reference for operating published resources: the management client and its error contract, project identity and settings, resource lifecycle, canonical lifecycle commands, recovering a quest lifecycle change and its keys, operator undo commands, and scheduled hosted jobs. These sections moved there unchanged from the `authoring` reference, which now covers writing and publishing quests, rewards and presentation and links to the trackers, verification, quest rules and management references, so `domino reference management` serves them and an agent loads them only when it operates a project.

* 56a0601: `fakeEvaluationMember(methods)` from `@domino-sdk/relay/testing` builds an `EvaluationMember` for a test that assembles its own evaluation context. Pass the methods your code reads; the rest answer for a member with no linked accounts, completions, groups or enrollments, and `balance` and `referrals` fail with a `CapabilityError` of code `author-error`. A member written by hand stops compiling when a minor release adds a method; one from `fakeEvaluationMember` does not.

  The CLI's automation contract (`cli/contract/contract.json`) lists the JSON Schemas of `relay.json` and the checkout's `domino.json` under `files`, so a later release is checked to accept every file an older CLI wrote.

* 5cc0d4d: Revoking a completion with `freeOccurrence: true` now returns the budget it used. Each quest budget the completion was charged gets that charge back, in the same transaction and in the budget period it was charged to, so the freed occurrence can be completed again within the cap. A point-priced budget gets back its charge less the points the revoke couldn't reverse. `CompletionRevocation` reports this in a new optional `budgets` (`budget`, `released`). Revoking without freeing the occurrence leaves budgets spent, as before. Completions recorded before this release give no budget back and report `budgets: []`.

  Restoring such a revocation charges that budget again and reports it in a new optional `CompletionRestoration.budgets` (`budget`, `charged`). The charge is checked against the budget's current limit, or the limit it was first charged under if the quest removed the budget or changed its period or cost. Restore now also refuses with 409 `budget-exhausted`, naming the `budget`, when a budget has no room left for the charge, and then changes nothing. This is a behaviour change: until now, budgets stayed spent after any revoke.

* dcc32da: `ctx.wallet.events()` takes `newest`, from 1 to 1,000: it returns at most that many of the newest matching events, reading back from the newest block, and stops at the 3,000,000-block or 1,000-log cap instead of throwing `author-error`. `WalletEvents` gains an optional `complete`, `false` when a `newest` read left older matching events out or stopped before `since`. A busy wallet, or a long window on a fast chain such as Arbitrum One (3,000,000 blocks is about 9 days there), no longer has to fail the attempt. A call without `newest` behaves as before: it returns every matching event, now with `complete: true`, or throws `author-error`. A read whose start-block estimate overshot 3,000,000 blocks while the span itself fit is no longer refused. The `onchain-action` snippet reads with `newest: maxWalletEventLogs`.

* 980e6ca: Sign-in rate limits count an IPv6 client by its /64, so every address in one /64 shares the per-IP limits on email, wallet, OAuth and Telegram sign-in starts, `domino login` device starts and staging password attempts. An IPv4-mapped IPv6 address counts as its IPv4 address. Per-address limits now count per client: three sign-in codes per email address and ten wallet challenges per wallet address from one IP address every ten minutes, so another client hitting them no longer refuses that address's own sign-in, on this project or another. Each project and environment also caps an address at ten sign-in codes and thirty wallet challenges every ten minutes from all clients together.

* d683d00: Sign-in rate limits per IP address count each member of a Domino-hosted app on their own. Hosted apps reach the API from Cloudflare's shared Worker address, so until now every member of every hosted app shared one set of per-IP limits. Domino now attests the visitor's IP on every request a hosted app's server sends to the API, with no app change or rebuild. A request from Cloudflare's shared Worker address without Domino's attestation, such as from an app you run on your own Cloudflare Workers, is counted per project and environment instead of with every other Worker.

* 081806c: Personal keys (`ak_…`) and MCP OAuth tokens that Clerk does not recognize now count against the client that presented them, on the management API, `git` and the MCP server: after 30 in 10 minutes from one IPv4 address or IPv6 /64, counted by each API server on its own, every `ak_` and MCP token from it answers 429 `rate-limited` with `Retry-After` until the window ends. Expired, revoked and other refused credentials Clerk knows are not counted, and neither is Clerk refusing Domino's own secret key. A refused value presented again within a minute can be refused without asking Clerk, and a value no Clerk credential can be (under 16 characters after its prefix, over 4096, or with a character a bearer token cannot hold) is refused with 401 `invalid-credential` without asking Clerk. When Domino is checking many new credentials at once, a key or token it has not accepted recently can answer 503 with `Retry-After: 1`. Clerk's own rate limit now answers 503 with Clerk's `Retry-After`.

* 114e0c7: Telegram and Discord install links now tell whoever opens them which Domino project they connect to. The Telegram bot's reply to a link, before the chat picker, names the organization, the project's name and slug, and the environment, and so does its `Connected …` reply. A Discord install link sent to a server's administrator (**Send to your server admin**) first opens a page naming the same, with a **Continue to Discord** link; the page changes nothing, so a link preview that fetches it stays harmless, and the page shown after connecting names the project too. Creating either link (`management.telegram.connect()`, `POST /management/v1/integrations/discord/links`) now reads the organization's name from Clerk, and answers 503 rather than issue a link that names nobody when Clerk can't answer. Links made before this change still work and name no project. Console's own Discord installation is unchanged.

* 92ee725: Connecting a Discord server or Telegram chat where Domino's shared bot already holds rights now takes someone who holds those rights too. In Discord, installing the bot (Console's **Connect Discord** or a shared install link) refuses a user who is neither the server's owner nor an Administrator when the bot already has a permission there they lack, such as Manage Roles another organization gave it; the install page says "Domino's bot already has permissions in this server that you don't have. Ask a server administrator to connect it." and nothing is connected. A fresh install, where the bot holds nothing beyond `@everyone`, still needs only Manage Server. In Telegram, an administrator who lacks an administrator right the bot holds in the chat, such as posting messages, is refused with a reply naming the missing rights; the chat's creator always connects it. Reusing a Discord server the organization already connected is unchanged, and existing connections are not revisited.

* d7df1d9: The automation skill reference documents management API rate limits: no general quota per key, project or organization, the calls that carry their own limits, and how to honor 429 or 503 `Retry-After` and pace a long run.

* dd7ced3: Experimental: a member import row can carry the member's marketing consent from the platform they come from. The optional `consent` list on `memberImportRowSchema` takes one `{ channel, state: "granted" | "withdrawn", at, wording? }` per channel; applying the row records it with its original time and wording, marked `source: "imported"`, in the row's transaction, unless the member chose on that channel in Relay (their choice always stays) or an earlier import recorded consent as recent or more recent. A check refuses the row with `future-action` for a time in the future, `unsupported-consent-channel` for a channel other than `email`, `consent-wording-required`, `consent-wording-too-long` or `consent-wording-invalid` for the wording, and `duplicate-entry` for a channel named twice. `KnownConsentSource` gains `imported`. The CLI imports reference describes the field.

* 5a34b36: Experimental: members record their marketing consent per channel. `relay.consent.get()` lists the signed-in member's current consent, one entry per channel they chose on (`member`, `channel`, `state`, `at`, `wording`, `source`), and `relay.consent.set({ channel, granted, wording, actionId? })` grants or withdraws one channel. The `useConsent()` React hook returns the query with `grant(channel, wording)` and `withdraw(channel, wording?)`. Relay stamps the time and stores the wording verbatim; a repeat of the current state and wording records nothing. Only `email` is accepted for now: other channels are refused with `unsupported-consent-channel`, a grant without wording with `consent-wording-required`, wording over 2,000 characters with `consent-wording-too-long`, and wording with a NUL or a lone surrogate with `consent-wording-invalid`. Channel, state and source are open strings in results. The consent API is `@experimental` and can change in a minor release. The participant API reference and `capabilities.json` describe it.

* f463bdf: The campaign-testing skill reference explains how to pace a live test-member rehearsal: a driver process outside the hosted app, a deterministic `actionId` or `--operation-id` per step, bounded concurrency, and clock moves instead of real-time waits.

* 172c3b5: Experimental: a `viewer` project role and grant end dates, to share a project's dashboards with a sponsor or client. A viewer holds one permission, `dashboards`, which reads `dashboards.list` and `dashboards.read` and nothing else: no members, evidence, quests or settings. Every other role holds it too, and `read` keeps reading dashboards, so no existing person or key loses access.

  `management.grants.set` takes `projectGrantSchema`: `roles` may name `viewer`, and an optional `expiresAt` (epoch ms, or null for no end) ends the grant's access without anyone removing it. An ended grant stays in `grants.list()`, and the access history records each end date. Released SDKs keep reading grants: a grant's `roles` keeps the roles 1.x names, so a viewer's is empty, and the new `projectRoles` lists every role. Likewise a project in `management.access()` keeps `dashboards` out of `permissions` and lists it in the new `projectPermissions`. `projectRoleSchema`, `projectPermissionSchema`, `projectRolePermissions`, `projectGrantSchema` and these fields are experimental.

  Console grants the viewer role and an end date under **Settings → Access → Team**, and shows an ended grant as **Expired**. A viewer's Console shows only **Analytics**, opened on the project's first dashboard. The CLI's agent references describe the role.

* aaf28f9: Quest submissions are limited to 120 an hour per member in each project environment, counted together across `POST /v1/me/attempts`, `POST /v1/attempts`, `POST /v1/me/submissions/:id/submit` and `POST /v1/attempts/:id/retry`. Past it they answer 429 `rate-limited` with `Retry-After`, before the request is read. The quest controllers keep the saved draft, so `resume()` continues it. The participant reference in the Domino skill states the limit.

* 9f39ea4: An operator's reversal of a referral payout stays reversed. Reversing all or part of a referral payout (Console point history **Reverse**, or `management.points.record({ kind: "reverse", entry })`) writes those points off on the referral account, so no later reconcile pays them again; earlier, the inviter's next invitee activity or a new acceptance of their code paid the reversed amount back. Later bonus growth still pays, and restoring a revoked qualification does not repay the reversed points. `earned` in the referral summary and the management referral accounts excludes the reversed points, so a reversed payout no longer reads as earned.

* 10df298: When no credential exists for the selected API URL but `domino login` saved one for another, `AUTH_REQUIRED` names that URL and the fix (`--api-url`, a checkout that uses it, or `domino login`), and `details.savedApiUrls` lists the saved URLs so automation can retry. A saved token is still sent only to the API URL it was saved for. With no other saved sign-ins, the error is unchanged.

* 1cdc64b: Staging a commit the project's hosted repository doesn't have (a mistyped SHA, or a commit not pushed yet) now answers a certain 422 `commit-not-found` before any build is reserved, instead of an uncertain 502 `upstream-failed`. `domino stage` reports it as `REJECTED` with `apiCode: "commit-not-found"`, saves no operation to resume and prints no retry command: push the commit, then stage again. Other GitHub failures during the commit check still answer 502 and stay uncertain.

* 3d2f654: Staging's participant API is now private, like staging itself. A participant request that names a staging address (`<slug>--staging.domino.page`) in `X-Relay-App-Origin` answers only when it carries the staging pass, which the staging gateway gives browsers signed in to staging; anything else is refused with 403 `staging-access-required`. Apps on staging keep working with no change: the gateway adds the pass to the requests it hands the app and to legacy `/relay/*` calls, and `proxyParticipant` forwards it with the browser's other cookies. Only callers that sent a staging origin straight to the API, outside the staging address, see the new refusal. The retired `staging-<slug>.domino.run` origin now selects nothing (403 "Application origin is not registered or is offline"). Previews, live apps and identity integrations are unchanged.

* 2e8f383: The Domino skill's campaign testing reference explains the staging toolbar's bottom inset: while the toolbar is mounted, `<html>` carries `data-domino-staging-toolbar` and `--domino-staging-toolbar-inset-bottom`, so an app's fixed bottom bar can sit above the collapsed toolbar instead of under it. When Playwright reports "`<domino-staging-toolbar>` intercepts pointer events", the app should use the inset.

* 75efef0: The skill's references list the participant reads that return a member's submitted text, URL or quiz input, and warn against text or URL quests for secrets. The trackers reference shows the pattern to use instead: the app's server checks the code, rate-limits guesses, and ingests a record for the member into a custom tracker that triggers the quest.

* b9270ff: Telegram Login, the member's Telegram account and its unlink now go through the generic sign-in surfaces, and Telegram's own members for them are deprecated. They keep working, with their routes, through SDK 1.x and are removed in 2.0.0:

  * `relay.auth.telegram.start(mode)` → `relay.auth.start({ provider: "telegram", mode })`.
  * `relay.auth.telegram.account()` → the `"telegram"` entry of `relay.auth.signInAccounts()`.
  * `relay.auth.telegram.disconnect()` → `relay.auth.disconnect("telegram")`.
  * `useTelegramAccount()` → `useAccount("telegram")`. Inside a Mini App, link with `relay.auth.telegram.miniApp.connect(initData)`, then call `invalidate([relayKeys.accounts, relayKeys.experience])` from `useInvalidate()`.
  * `useSignIn().withTelegram()` → `useSignIn().withProvider("telegram")`; `withProvider` now takes any `SignInProvider`.
  * `SignInOptions.telegram` and `useSignInOptions().telegram`, `TelegramSignInOption` and `SignInActions.telegram` → `signInProviders`, below.
  * `relay.auth.providers().telegram.login` and `management.participantSignIn().telegram.login` → the `"telegram"` entry of `signInProviders`. `TelegramSignInMethods` and the new `ParticipantSignInTelegram` are written-out types, so editors show these two deprecations.
  * `management.members.telegramAccount(member)` and `unlinkTelegramAccount(member, input)` → `members.accounts(member)` and `members.unlinkAccount(member, { provider: "telegram", subject, expectedRevision, reason })`.

  `useSignInOptions()` and `signInOptions(relay)` add `signInProviders`, the open sibling of the `oauth` entries of `options`: every sign-in provider Domino configured, Telegram included, in Relay's order, each a `ProviderSignInOption` (`{ kind: "provider", id, label, icon, start() }`). A newer Relay may list a provider this SDK does not know; its label is its name. Against a Relay without `signInProviders`, the list holds its providers and Telegram Login. `SignInMethods` adds `signInProviders` and `SignInActions` an optional `provider(provider)` action. The starters render their provider buttons from `signInProviders`.

  Mini App sign-in (`relay.auth.telegram.miniApp`, `useSignIn().withTelegramMiniApp`, `providers().telegram.miniApp`, `participantSignIn().telegram.miniAppBotId`) is not deprecated.

* 90a20c6: Telegram is now a plugin: `management.connections.list()` and `domino connections get telegram` include it, `ready` while the environment has a connected chat, `attention` with the chat's issue while Domino's bot can't read one, and `disconnected` without chats. Chats still connect only through the bot (`management.telegram.connect()`, unchanged), so `connections.connect`, `update` and `disconnect` answer 409 `plugin-managed` for Telegram and for any plugin that connects through its own flow. `knownPluginSlots` adds `announcement`, the slot Telegram posts quest announcements through. `management.plugins.list()` items gain `console`: `{ component }` when Console shows the plugin's own component instead of the generated card, otherwise null; the schema defaults it to null, since older servers don't send it.

* 8e8e24a: Telegram Mini App sign-in has its own entrypoint, `@domino-sdk/relay/telegram` (experimental): `withTelegram(relay)` adds `relay.telegram` to a client and answers the same client, `relay.telegram.miniApp.signIn(initData)` and `.connect(initData)` do what `relay.auth.telegram.miniApp` did, and `telegramMiniAppInitData()` reads the launch data. Apps that don't import it don't carry it. `relay.auth.telegram.miniApp` and the `telegramMiniAppInitData` exports of `@domino-sdk/relay/browser` and `@domino-sdk/relay/react` are deprecated, keep working through 1.x and are removed in 2.0.0.

  Telegram Login and Mini App sign-in now run in the Telegram plugin, with no change to what they accept or answer: members linked before keep signing in as the same member, and `/v1/auth/telegram/mini-app` takes the same requests. `management.plugins.list()` and `domino connections get telegram` list one more slot for Telegram, which `knownPluginSlots` adds: `identity` (members sign in with it).

* 52ed037: Telegram's trackers and `ctx.telegram` now come from the Telegram plugin, with no change to what they record or read: `telegram.messages()` and `telegram.reactions()` trackers, the bot's webhook at `/v1/integrations/telegram/updates`, and `ctx.telegram.connection()` and `member()` behave as before, and published quests keep working. `management.plugins.list()` and `domino connections get telegram` list two more slots for Telegram, which `knownPluginSlots` adds: `tracker-source` (the plugin writes its trackers' records) and `evaluator-capability` (quest code reads it as `ctx.<plugin>`). A quest may now declare the `telegram` integration module, `{ provider: "telegram" }`, and publishes; before, publishing refused it with `unknown-integration-module`. Declaring it is optional, since every quest gets `ctx.telegram`.

* Updated dependencies \[a341b16]

* Updated dependencies \[bd622ae]

* Updated dependencies \[76be180]

* Updated dependencies \[5233be5]

* Updated dependencies \[ee3f488]

* Updated dependencies \[3665a4e]

* Updated dependencies \[f13cfb6]

* Updated dependencies \[361a5b1]

* Updated dependencies \[154af63]

* Updated dependencies \[79b7c68]

* Updated dependencies \[eed1808]

* Updated dependencies \[56a0601]

* Updated dependencies \[5cc0d4d]

* Updated dependencies \[94ca2b5]

* Updated dependencies \[f9c9f34]

* Updated dependencies \[dcc32da]

* Updated dependencies \[98ad384]

* Updated dependencies \[4bf82e5]

* Updated dependencies \[4bf82e5]

* Updated dependencies \[4bf82e5]

* Updated dependencies \[d4fcade]

* Updated dependencies \[8064983]

* Updated dependencies \[980e6ca]

* Updated dependencies \[d683d00]

* Updated dependencies \[081806c]

* Updated dependencies \[114e0c7]

* Updated dependencies \[92ee725]

* Updated dependencies \[ea2c9be]

* Updated dependencies \[2de5d71]

* Updated dependencies \[e58c2ae]

* Updated dependencies \[3a3feef]

* Updated dependencies \[10826dc]

* Updated dependencies \[c391c86]

* Updated dependencies \[7a93f76]

* Updated dependencies \[1a70098]

* Updated dependencies \[dd7ced3]

* Updated dependencies \[5a34b36]

* Updated dependencies \[4b7c062]

* Updated dependencies \[de99154]

* Updated dependencies \[80e09c2]

* Updated dependencies \[1c61073]

* Updated dependencies \[423b6fc]

* Updated dependencies \[01b789e]

* Updated dependencies \[2a83792]

* Updated dependencies \[172c3b5]

* Updated dependencies \[aaf28f9]

* Updated dependencies \[b2b8127]

* Updated dependencies \[a381dde]

* Updated dependencies \[9f39ea4]

* Updated dependencies \[b866593]

* Updated dependencies \[1cdc64b]

* Updated dependencies \[3d2f654]

* Updated dependencies \[4443124]

* Updated dependencies \[b9270ff]

* Updated dependencies \[90a20c6]

* Updated dependencies \[8e8e24a]

* Updated dependencies \[52ed037]

* Updated dependencies \[2fe2d72]

* Updated dependencies \[7fa2a06]

* Updated dependencies \[06046f1]
  * @domino-sdk/relay\@1.62.0

### 1.61.0 [#1610-1]

#### Minor Changes [#minor-changes-111]

* e71f881: `domino check` runs each badge, point ledger, offer, leaderboard, referral, dashboard and tracker entry in a separate Node process and parses its export with the schema Relay parses it with at deployment. A definition that fails, such as an offer without `stock`, reports a `DEFINITION_INVALID` error diagnostic with its `file`, `export`, field `path` and message, and fails the check instead of failing later in staging.
  `defineOffer`, `defineBadge`, `defineLedger`, `defineLeaderboard`, `defineReferral`, `defineDashboard` and `defineTracker` still throw a `ZodError` for invalid input, and its message now names the kind, key and fields, for example `Offer "ladder" is invalid: stock: Invalid input: expected number, received undefined`. Staging shows that message when its data setup fails.
  A deployment, preview or staging activation with an inline or described definition that fails its schema now answers 422 `validation-failed` with that message, where it answered 400 `Invalid request`.

* b369a75: A project connects a Discord server that another project of its organization already connects, ready, in the same environment, without installing Domino's bot again. Any credential with `publish` permission can: an automation key, a CLI session, a personal API key or Console (**Integrations**, **Or reuse**). Installing the bot into a new server stays with an organization administrator in Console.

  * `management.discord.servers()` and `domino integrations discord servers` list the servers the project can connect.
  * `management.discord.connect(server)` and `domino integrations discord connect --server <id>` connect one, once the bot still sees it. They refuse with 409 `server-not-connected-in-organization` for any other server, 409 `integration-connected` when the project already has a Discord connection, and 422 `server-not-reachable` when the bot lost the server. Connecting the server the project already connects changes nothing.
  * `management.discord.connection()` and `domino integrations discord status` read the project's Discord connection.

  The project's audit records each connection as `discord-server-connected`, with who connected it and the project whose connection it reused. An automation key and a CLI session connect only in test.

* d657954: `domino stage` warns when the app signs participants in with a wallet but the test environment has no wallet family enabled, which leaves the app's wallet sign-in doing nothing. The warning goes to stderr and, as `wallet-sign-in-off` with the enabling `domino api PUT /participant-sign-in` command, into the JSON result's `warnings`; the build still stages. `domino doctor --remote` reports the same as its `wallet-sign-in` check. The CLI reads wallet sign-in from each of the app's source files: `relay.auth.wallet.signIn`, `useSignIn().withWallet`, `useSignInWallets`, Cosmos chains passed to `useSignInOptions`, a wallet option picked with `find`, or wallet options filtered without OAuth, email or Telegram ones. The starters' sign-in, which renders every option the project offers, does not count.

#### Patch Changes [#patch-changes-37]

* 97b71e2: The `domino` skill's error reference no longer lists `organization-not-provisioned`: Relay serves every organization, so no request is refused with it.
* Updated dependencies \[e71f881]
* Updated dependencies \[f0ce453]
* Updated dependencies \[b369a75]
  * @domino-sdk/relay\@1.61.0

### 1.60.0 [#1600-1]

#### Minor Changes [#minor-changes-112]

* d258795: A budget can cap what one person gives, not only what one member receives. `budget({ key, scope: "author", period, limit, cost })` counts per tracker record author: the account that caused the record, such as a reaction's reactor, whether or not a member linked it. Put it in a tracker quest's `budgets` beside a `"member"` budget to cap both sides, for example a tip paid to a message's author with `beneficiary: "messageAuthor"`: at most 20 tips a day per receiver and 10 a day per reactor. A tip over either cap is rejected with `budget-exhausted`, permanently; other reactors' tips keep paying. It works on `telegram.reactions()`, `discord.reactions()`, kudos recipients and X `inReplyTo` replies. Author budgets share keys, calendar periods and the atomic commit with the other budgets, and imported completions don't count toward them.

  An author budget needs a tracker trigger. `domino check` reports `AUTHOR_BUDGET_TRACKER_REQUIRED` on another quest, `domino build` and `domino deploy` refuse it, and Relay refuses its publication.

  The types SDK 1.x code reads are unchanged: `Budget` keeps `scope: "member" | "project"`, and a quest's description lists author budgets in the new optional `authorBudgets` (`AuthorBudget`, `authorBudgetSchema` from `@domino-sdk/relay/schema`), apart from `budgets`. `budget()` returns an `AuthorBudget` for `scope: "author"`. A member's quest `budgets` (`QuestBudgetView`) leave author budgets out, since they cap someone else's giving.

* c619bf3: New `domino deploy rollback [deployment]` publishes the previous published test deployment again, undoing the latest `domino deploy`. It names both deployments and asks before publishing (`--yes` skips that), and it saves the change under an operation ID, so `domino operations resume` finishes it once after an uncertain response. Only the previous published deployment can be rolled back to. Only what it contains goes back, and the staged build does not change; once a hosted app build has published to test since that deployment, the rollback is refused with 409 `conflict`. In live it fails with `ENVIRONMENT_UNSUPPORTED` and `apiCode: "promotion-required"`. `management.deployments.rollback` now works with a test client too, publishing at once; in live it still submits the rollback for review.

* e899f47: Member imports carry referrals. A row's optional `referral` names the member's inviter (another row's `ref`, or the sign-in identity of an existing member), when they accepted, and the referral programs they already qualified in (`qualified: [{ program, at }]`). Applying records the referral and the qualifications without paying any referral reward, milestone or bonus; they count toward the inviter's qualified friends, and the friend's later points pay the program's bonus. An inviter's row applies before the rows that name it. New refusal codes: `unknown-inviter`, `self-referral`, `inviter-already-accepted`, `referral-cycle`, `inviter-refused` and `unknown-referral-program`. The check summary adds `referrals` (`accepted`, and `qualified` by program), `management.referrals.participants()` names the import that recorded a relationship or qualification in `import`, and `management.members.get()` returns the member's accepted referral in an optional `referral` (`inviter`, `acceptedAt`, `suspended`, `import`).

* 8ee2000: A member import vouches for Telegram and Discord accounts too. A row's `accounts` takes `{ kind: "telegram", subject }` and `{ kind: "discord", subject }`, each a numeric user ID, at most one of each per member, from `domino import` and `management.memberImports.addRows`. They work like a vouched X account: Telegram and Discord tracker records, quest checks and Discord role checks trust them from when the import attached them, nobody else can link them, and the member's own Telegram or Discord link replaces them. They never sign in: a Telegram or Discord sign-in with one is refused with `sign-in-method-disabled`. A row naming a second account of a kind, or another one than the member's, is refused with the new `MemberImportRefusalCode.TelegramAccountSelected` (`telegram-account-selected`) or `DiscordAccountSelected` (`discord-account-selected`).

  `@domino-sdk/relay/server` adds `memberImportVouchedAccountSchema`, `memberImportFullRowSchema` and `memberImportFullRowsInputSchema`, with the types `MemberImportVouchedAccount`, `MemberImportFullRow`, `MemberImportFullRowInput` and `MemberImportFullRowsInput`, for rows with every kind of vouched account; `addRows` takes `MemberImportFullRowInput[]`. `memberImportAccountSchema`, `memberImportRowSchema` and `memberImportRowsInputSchema` stay the X-and-wallet form. Member identity states carry `imported: true` for an X, Discord or Telegram account an import attached, and so does a connected `ManagedTelegramAccount`.

  An imported X, Telegram or Discord account now pays tracker records from the moment it was imported. Before, a vouched X account had no link time, so its records were skipped as historical. Members imported before this release keep that unknown link time until they link their X account themselves.

* f0d5b8e: Offers can be archived and restored. `management.offers.archive` and `unarchive` (`POST /management/v1/offers/OFFER/archive|unarchive`), `domino offers archive|unarchive OFFER --reason`, Console **Settings → Resources** and the offer's card in Console **Rewards** archive one in test, and a promotion carries it to live. Each returns `{ resource, changed }`, typed by the new `offerLifecycleResultSchema`, with `resource.kind` `offer`. An archived offer leaves member offer reads and refuses redemption with `offer-paused`; management reads keep it, with the new `archivedAt` field on `Offer`, and its redemptions can still be canceled. `management.resources.list()` and `domino resources list` list offers in a new `offers` array (`managedResourceListSchema`), beside the existing kinds. A rollback now archives an offer the earlier promotion did not carry.

* ea1f033: Revert an applied or stopped member import: `management.memberImports.revert(id, { reason })` starts it, and `domino imports revert IMPORT --reason TEXT` starts it and waits. In the background, one row at a time, it reverses the applied rows' opening balances (taking only points members still have; `unreversedPoints` lists the rest), revokes their imported completions (reason `import-reverted`, freeing the occurrence) and removes the wallets, accounts and identities they linked unless the member signed in with one since (`keptLinks`). Members stay in the project. A reverting or reverted import reads as `applied` with `revert` set, and is reverted once; the same `actionId` answers the import again. Rows applied before imports recorded their links keep them (`linksUntracked`). Reverting needs `publish` permission and works in live from API keys and CLI sessions too. Console shows **Revert import** on applied and stopped imports.
  `MemberImport` gains `revert`: the reason, who asked and when, rows reverted so far, then `revertedAt` and the `outcome`.

* 9a7ad11: Stop a member import while it applies: `management.memberImports.stop(id)` and `domino imports stop IMPORT` let the row being applied finish, apply no more rows, and end the import: it reads as `applied` with `appliedAt` null and `stoppedAt` and `stoppedBy` set, and its remaining rows stay `ready`. A stopped import can't be applied again; import the remaining rows in a new import. Stopping needs `publish` permission and works in live from API keys and CLI sessions too. Console shows **Stop import** on an applying import.
  `MemberImport` gains `stoppedAt` and `stoppedBy`; check `stoppedAt` to tell a stopped import from a fully applied one.

* df3a08c: A test member's clock moves by hours and minutes, or to an instant, so time-of-day rules can be tested: block edges, or midnight in the project's time zone. `management.testMembers.advance(member, input)` takes `{ actionId }` with one of `days` (1–30, unchanged: project-calendar days that keep the time of day), `hours` (1–720), `minutes` (1–43200) or `to`, an ISO 8601 instant with an offset that must be later than the member's clock (else 422 `test-member-clock-past`). From the CLI, `domino test-members advance MEMBER` takes one of `--days`, `--hours`, `--minutes` or `--to`. The 90-day limit holds across units (422 `test-member-clock-limit`): `days` still add up to at most 90 and land no later than the 90th date, and hours, minutes and `to` never move the clock past 90 days from now. `offsetDays` still counts the days advanced with `days`; hours, minutes and `to` leave it unchanged, so read the member's date from `today`. `@domino-sdk/relay/server` exports `testMemberClockAdvanceSchema` and `TestMemberClockAdvanceInput` for every form; `testMemberAdvanceSchema` and `TestMemberAdvanceInput` stay the `days` form.

#### Patch Changes [#patch-changes-38]

* 6b5228b: Quests can announce a completion in a connected Telegram chat or Discord channel. List `telegram.postMessage({ chat, text, replyToTriggeringMessage })` or `discord.postMessage({ channel, text, replyToTriggeringMessage })` in the new optional `announcements` of `defineQuest`, beside `rewards` (at most three; an array or a function of the settings). The text is a template with the placeholders `{beneficiary}`, `{author}`, `{points}`, `{rewards}` and `{quest}` (`announcementPlaceholders`); people are mentioned through their linked account on that platform. Relay posts after the rewards commit, as a reward delivery with the new `RewardDeliveryAction.PostMessage` action and an empty `roleId`, retried with backoff and paced per chat; a failed post never undoes the rewards, and `management.retryRewardDelivery(id)` retries one. Descriptions carry them in the new optional `announcements` field (`announcementSchema`). `discord.channels({ postable: true })` lists channels the bot can't post in as unavailable, and `TelegramChat` gains an optional `canPost`. The CLI's authoring reference documents announcements.
* Updated dependencies \[d258795]
* Updated dependencies \[d41f5da]
* Updated dependencies \[e899f47]
* Updated dependencies \[8ee2000]
* Updated dependencies \[f0d5b8e]
* Updated dependencies \[6b5228b]
* Updated dependencies \[ea1f033]
* Updated dependencies \[9a7ad11]
* Updated dependencies \[df3a08c]
  * @domino-sdk/relay\@1.60.0

### 1.59.0 [#1590-1]

#### Minor Changes [#minor-changes-113]

* 1d5a195: `management.attempts.acceptAnyway({ attempt, reason })` (`POST /attempts/<id>/accept-anyway`, `review` permission) accepts a rejected attempt anyway, for any member, reversing a reviewer's or an evaluator's rejection. The acceptance pays as a reviewer's approval would, with the normal `completion.created` webhook; the rejection stays in the attempt's events beside a `staff-decision` with `decision: "accept-anyway"` and the reason, and the retry cooldown ends. Only the member's latest attempt on a quest that isn't archived qualifies (409 `attempt-superseded`, `quest-archived`), and a cap the acceptance would break refuses it instead of rejecting it again (409 `completion-cooldown`, `budget-exhausted`, `completion-revoked`, `already-completed`). `management.attempts.get` answers `supersededBy`, the member's newer attempt on the quest, or null. The CLI adds `domino attempts accept-anyway ATTEMPT --reason TEXT`.
* a0c77e4: Archiving or unarchiving a group, pausing or resuming a tracker, and changing the project's schedule state take an optional reason, which the audit log records: `management.groups.archive(group, { reason })` and `unarchive(group, { reason })`, `management.trackers.pause(reference, { actionId, reason })` and `resume`, and `management.schedules.setState(state, { reason })`. A reason is a trimmed string of 1–500 characters, like the other lifecycle reasons; leaving it out works as before. The CLI adds `--reason TEXT` to `domino groups archive|unarchive`, `domino trackers pause|resume` and `domino schedules pause|archive|resume`. Console asks for a reason in a dialog for each of these, and the project schedule menu no longer applies a state as soon as you pick it.
* 424863f: Operators can restore an entitlement they voided. `management.entitlements.restore(entitlement, { revision, reason })` (`POST /management/v1/entitlements/<id>/restore`, `review` permission) and `domino entitlements restore <id> --revision <n> --reason <text>` make a `void` entitlement `available` again at the tier an accepted completion still earns, and bring back any referral review the void settled. Restoring is refused with 409 `entitlement-status` when a completion revocation voided the entitlement or no accepted completion still earns it (the body names the `completion` to restore instead), when canceling an offer redemption voided it, or when a referral reward was voided before voids kept their reviews. Console Rewards and member detail offer **Restore reward** on a voided reward, and Audit history lists each restore with its reason.
* 790aa5d: Operators can restore a completion they revoked by mistake. `management.completions.restore({ completion, reason })` (`POST /management/v1/completions/<id>/restore`, `review` permission) and `domino completions restore <completion> --reason <text>` re-apply what the revocation recorded, not a fresh reward evaluation: the points it reversed are awarded again (points it could not reverse are not), its badges, group memberships, tiered rewards, Discord role grants, referral qualifications and streak milestone come back, and budgets stay spent. The result (`CompletionRestoration`) lists what came back. Restoring is refused with 409 `completion-not-revoked` when the completion isn't revoked, and with 409 `occurrence-taken` (naming the `completion` that holds it) when the revocation freed the occurrence and the member completed it again. `members.get(id).revokedCompletions` lists up to 25 revoked completions, each with its revoke's reason and result. Console member detail and attempt detail offer **Restore completion**, and the attempt history and Audit history show the restore beside the revoke.
* 95cb8d8: On-chain action checks on Solana. `ctx.wallet.transactions({ program, since, afterActivation?, mint?, address? })` reads the successful, finalized transactions of the member's linked Solana wallets since `since` that invoked `program` in a top-level or inner instruction, oldest first. It returns `{ program, chain, mint, since, at, complete, transactions: [{ wallet, signature, slot, timestamp, programs, balanceChanges: [{ mint, delta }] }], observedAt }` (`WalletTransactions`, `WalletTransaction`, `WalletBalanceChange`, `walletTransactionsSchema`), with `mint` `"SOL"` for lamports (rent included, the fee the wallet paid left out) and `delta` a signed integer string, or null when the member has no linked Solana wallet, and follows `ctx.wallet.balance()`'s capture, `author-error` and `provider-unavailable` rules. With `mint`, only each wallet's associated token account for that mint is read. `since` may be at most 30 days before the evaluation (`maxWalletTransactionLookback`); each wallet's read fetches at most 1,000 transactions (`maxWalletTransactions`), and a wallet with more returns its newest ones with `complete: false` instead of throwing. `WalletReader.transactions` is optional in the type so that existing test doubles compile; Relay supplies it to every evaluation.

  The program comes from the new `settings.solanaProgram({ label, default })` setting, a base58 address (`SolanaProgram`, `isSolanaProgram`) stored as a text setting with `picker: "solana-program"`. Publication, Console settings edits and unarchiving check that the account exists on Solana mainnet and is executable.

  New pure helpers in `@domino-sdk/relay/authoring` read one transaction: `received(tx, mint)` and `spent(tx, mint)` return the wallet's gain or loss as an integer string, or `"0"`; `swapped(tx, { into?, from? })` checks that one mint went down and a different one went up; `invoked(tx, program)`. The testing kit answers `ctx.wallet.transactions()` from `facts.wallet.transactions`.

#### Patch Changes [#patch-changes-39]

* aaa07ad: The SDK and CLI depend on Zod 4.6.5, whose schemas keep their methods on a shared prototype instead of binding about 7 KB of them to every schema instance. Importing the SDK takes far less memory: a Relay backend process that loads the server entry starts with about half the heap. Quest `settings.datetime()` values, member-import times and X post `createdAt` values (including hand-written testing fixtures) still accept minute-precision instants such as `2026-11-01T00:00Z`, which Zod 4.6 alone rejects. An X post read without a `media` field still gives an empty list.

  One tracker data schema change after a rebuild: a field declared as `z.number().min(0).int()` now stores `minimum: 0`, so Relay refuses negative values at ingestion that the app's own schema already refused. Zod 4.3 stored the safe-integer minimum for that order of checks.

* Updated dependencies \[1d5a195]

* Updated dependencies \[a0c77e4]

* Updated dependencies \[424863f]

* Updated dependencies \[790aa5d]

* Updated dependencies \[850a252]

* Updated dependencies \[7ac13ef]

* Updated dependencies \[5b94440]

* Updated dependencies \[95cb8d8]

* Updated dependencies \[aaa07ad]
  * @domino-sdk/relay\@1.59.0

### 1.58.0 [#1580-1]

*Not published to npm. Its changes first reached npm in 1.59.0.*

#### Minor Changes [#minor-changes-114]

* 88776b6: The operations journal no longer keeps webhook signing secrets. `domino webhooks create` and `domino webhooks rotate-secret` still save the secret while the operation is incomplete, so `domino operations resume` returns the same one, but once the CLI has written it to stdout the saved result has `secretRedacted: true` instead, `domino operations show` adds a `note` saying to rotate it, and repeating the command with the same `--operation-id` fails with `CONFLICT`. A secret an older CLI saved is printed by the next `show` or `resume`, then removed. New `domino operations forget <id>` removes one saved operation, and `domino operations prune` removes completed ones last changed more than `--older-than` days ago (default 30). `domino quests replay --out` now writes the capture to a new file with mode 0600 that replaces the destination, since it holds raw provider answers.

#### Patch Changes [#patch-changes-40]

* Updated dependencies \[321763b]
* Updated dependencies \[a2318dd]
  * @domino-sdk/relay\@1.58.0

### 1.57.0 [#1570-1]

*Not published to npm. Its changes first reached npm in 1.59.0.*

#### Minor Changes [#minor-changes-115]

* 7012d1c: Contract state and Cosmos staking reads in quest evaluators.

  `ctx.wallet.call({ call, args?, address? })` calls one `view` or `pure` EVM contract function once per linked EVM wallet, every call at one block, through dRPC on the chains `management.walletBalanceChains()` lists: state a contract keeps per address, such as `stakedOf(address)`, `getVotes(address)` or `userInfo(uint256, address)`. `{ wallet: true }` in `args` stands for each wallet's address. It returns `{ call, chain, contract, returns, total, wallets: [{ address, value }], at, observedAt }` (`WalletCall`, `walletCallSchema`), with integers as decimal strings and `total` their sum (null for `bool` and `address` returns), or null when the member has no linked EVM wallet. A revert is an `author-error` carrying the contract's reason. The call comes from the new `settings.contractCall({ label, default })` setting (`picker: "contract-call"`): the contract's CAIP-10 ID and a human-readable signature, such as `eip155:8453:0x… function stakedOf(address) view returns (uint256)` (`ContractCall`, `isContractCall`, `parseContractCall`, `parseContractFunction`). Only `view` or `pure` functions with elementary parameters that return one `uint`, `int`, `bool` or `address` are accepted; publication, Console settings edits and unarchiving also check the chain.

  `ctx.wallet.delegations({ chain, validator?, address? })` reads the native staking delegations of the member's wallets linked on a Cosmos SDK chain Relay lists, at one block, in base units of the chain's staking denomination: `{ chain, denom, validator, total, wallets, validators, at, observedAt }` (`WalletDelegations`, `walletDelegationsSchema`), optionally only to one validator, or null when the member has no wallet on that chain.

  Both follow `ctx.wallet.balance()`'s capture, `author-error` and `provider-unavailable` rules. `WalletReader.call` and `WalletReader.delegations` are optional in the type so that existing test doubles compile; Relay supplies them to every evaluation, and the testing kit answers them from `facts.wallet.call` and `facts.wallet.delegations`. New `contract-state` and `cosmos-staker` snippets accept members whose view call reaches a minimum, or who delegate at least a minimum, optionally to a configured validator.

* 59a01d8: On-chain action checks on EVM chains. `ctx.wallet.events({ event, account, since, afterActivation?, where?, address? })` reads the events of one contract whose indexed address argument `account` is one of the member's linked EVM wallets, from `since` to the chain's finalized block (its safe block on a chain without finality), through dRPC `eth_getLogs` on the chains `management.walletBalanceChains()` lists. It returns `{ event, chain, contract, since, at, finality, logs: [{ wallet, block, timestamp, tx, logIndex, args }], observedAt }` (`WalletEvents`, `walletEventsSchema`) with decoded arguments, or null when the member has no linked EVM wallet, and follows `ctx.wallet.balance()`'s capture, `author-error` and `provider-unavailable` rules. `where` holds equality or `{ gte }` conditions on other arguments (`WalletEventCondition`); `afterActivation` skips events before the quest's activation. `since` may be at most 30 days before the evaluation (`maxWalletEventLookback`), and a read over more than 3,000,000 blocks or 1,000 logs (`maxWalletEventLogs`) throws `author-error`. `WalletReader.events` is optional in the type so that existing test doubles compile; Relay supplies it to every evaluation.

  The event comes from the new `settings.contractEvent({ label, default })` setting: a CAIP-10 contract, a space and the event's Solidity signature with argument names and `indexed` markers, such as `eip155:8453:0x… Transfer(address indexed from, address indexed to, uint256 value)` (`ContractEvent`, `isContractEvent`, `parseContractEvent`, `ContractEventParts`), stored as a text setting with `picker: "contract-event"`. An event without an indexed address argument is refused. Publication, Console settings edits and unarchiving also check that the chain reports a finalized or safe block and that the address holds a contract.

  The testing kit answers `ctx.wallet.events()` from `facts.wallet.events`. A new `onchain-action` snippet rewards each new matching event since the later of the quest's activation and the wallet's link, with an optional minimum amount, keyed on `chain:tx:logIndex`.

* 956b721: NFT holding checks on EVM chains and Solana. `ctx.wallet.nfts({ collection, address? })` counts the NFTs of one collection the member's linked wallets of its family hold: on EVM, an ERC-721 contract's `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 whose Metaplex metadata names the collection with `verified: true` (SPL Token and Token-2022 accounts holding exactly 1 of a 0-decimal, supply-1 mint; compressed NFTs and Metaplex Core assets are not counted). It returns `{ collection, chain, standard, total, wallets: [{ address, count }], at, observedAt }` (`WalletNfts`, `walletNftsSchema`; `standard` is `"erc721"`, `"erc1155"` or `"metaplex"`), or null when the member has no linked wallet of that family, and follows `ctx.wallet.balance()`'s capture, `author-error` and `provider-unavailable` rules. `WalletReader.nfts` is optional in the type so that existing test doubles compile; Relay supplies it to every evaluation.

  The collection comes from the new `settings.nft({ label, default })` setting, a CAIP-19 asset type such as `eip155:1/erc721:0x…`, `eip155:8453/erc1155:0x…/1` or `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/nft:<collection mint>` (`NftCollection`, `isNftCollection`, `parseNftCollection`, `NftCollectionParts`), stored as a text setting with `picker: "nft"`. Publication and Console settings edits check it: an EVM chain and that the contract reports the standard through ERC-165 `supportsInterface`, so a token contract or the wrong standard is refused when it is set; a Solana mint must be a Metaplex collection NFT, so a fungible mint, a mint without Metaplex metadata, a print edition or a member NFT is refused. Unarchiving checks it too, and a platform without its dRPC key refuses every EVM collection. `ctx.wallet.balance()` pointed at an ERC-721 or ERC-1155 contract now throws `author-error` naming `ctx.wallet.nfts()` instead of reading a token count; it asks ERC-165 alongside `decimals()` on every token read.

  The testing kit answers `ctx.wallet.nfts()` from `facts.wallet.nfts`. A new `nft-holder` snippet accepts members whose linked wallets hold at least one NFT from the configured collection, on EVM or Solana.

* 1770ada: A seventh starter, `loyalty`: members join a campaign, earn points from its steps, join a members' group through a quest's group membership reward, and spend points on offers, one of them limited to the group with `inGroup`. Console offers it and `domino create --template loyalty` creates it.

  Starters are now an open list, so adding one no longer changes the SDK's types. `starters` lists every starter as a `Starter` with a plain string `id`, `offeredStarterSchema` checks an ID against it, and `projectCreateInputSchema` and `repositoryCreateSchema` accept any offered starter; `management.createProject` uses the first. `starterTemplates`, `StarterTemplate`, `StarterId`, `starterIdSchema`, `projectInputSchema` and `createRepositorySchema` are deprecated: they keep the first six starters and are removed in 2.0.0. To upgrade, replace `starterTemplates` with `starters` and `starterIdSchema` with `offeredStarterSchema`, and type a starter ID as `string`.

* cec850f: Create and change a tracker of every managed source without a deployment, not only X search. `management.trackers.create` and `management.trackers.update` (`POST /trackers`, `POST /trackers/:tracker/update`) take a `source`: `{ kind: "x-search", query }`, `{ kind: "discord-messages", text? }`, `{ kind: "discord-reactions" }`, `{ kind: "telegram-messages" }`, `{ kind: "telegram-reactions" }`, `{ kind: "youtube-shorts" }` or `{ kind: "tiktok-videos" }`. Each is stored as the source its builder makes, such as `telegram.reactions()`, so quests read a data tracker as they read the same tracker in code. `query` alone still creates an X search tracker; create takes exactly one of `query` or `source`. An update's `source` replaces the source's settings but never its kind, which is refused with 422 `validation-failed`. Turning Discord message text off erases the text already stored. `managedTrackerCreateSchema` validates the new input, while `trackerCreateSchema` keeps its X-only shape. `managedTrackerSource(source)` reads a published source by these names, and `managedTrackerSourceKinds` lists them.

  `domino trackers create KEY --source <kind>` creates one from the CLI, with `--text on` for Discord messages; `domino trackers update TRACKER --text on|off` turns message text on or off. Console **Trackers → New tracker** offers every source.

* 94726d8: Organization automation keys (`dak_`): a credential for CI and headless coding agents that no person owns. An organization administrator creates one in Console Settings → API keys for every project or a chosen list, with chosen permissions and an optional permission to create projects. It works as `RELAY_MANAGEMENT_TOKEN`, with `domino login --token-stdin` and the Git credential helper, in the test environment only: live writes, administration and minting other credentials are refused.

  The management client adds `automationKeys` (`list`, `create`, `addSecret`, `revokeSecret`, `revoke`) with `automationKeyInputSchema`, `automationKeySchema`, `issuedAutomationKeySchema` and `automationKeyPermissionSchema`. `accessSchema` gains an optional `credential` that `/access` and `domino whoami --json` fill for an automation key with its kind and scope.

* 6d9f0de: `telegram.messages()` is a tracker source of the messages members post in the project's connected Telegram groups and supergroups, one record per message with the poster's Telegram account as `author`. `data` is `TelegramMessageData`: `chatId`, `chatType`, `messageId`, `replyToMessageId` and `threadId`, never the text. Bots', service and channel messages are not recorded. In SDK 1.x it is a custom source on the reserved namespace `telegram-messages`, which Relay alone writes. The new `telegram-contribution` snippet rewards posting once per member per day.

* 35b445c: Unattended agent mode. Whoever launches an agent with nobody present sets `DOMINO_AGENT_MODE=unattended`, and `domino doctor --json` and `domino whoami --json` report it as `agentMode`. In that mode browser `domino login` refuses with the new `UNATTENDED_LOGIN_UNAVAILABLE` code, so remote work uses `RELAY_MANAGEMENT_TOKEN`, and `domino feedback` appends each report to `./domino-feedback.jsonl` (or `DOMINO_FEEDBACK_FILE`) instead of sending it; `--submit` or `DOMINO_FEEDBACK=submit` sends it. The Domino skill builds from the supplied brief without waiting, records the gaps it fills in CAMPAIGN.md, and ends with a report.

* 8d11f64: `wallet.balances({ chain, token })` is a tracker source of daily balance samples that Relay writes itself. Once per project-calendar day, at a time members can't predict, Relay reads one token's balance in every wallet linked by a member who joined the project, every wallet of a run at one pinned EVM block or Cosmos height (on Solana, every read must reach the run's pinned slot). `chain` and `token` are those of `ctx.wallet.balance()`. Each record is one wallet, keyed by its CAIP-10 account on the sampled chain, which is also its `author`; each sample is a new observation whose `occurredAt` is the sample time and whose `data` is `WalletBalanceSample` (`amount` in base units, `decimals`, `block`). A provider outage retries the same block later that day and never writes a zero, so a day with no successful sample breaks a `streak({ days })`. Reward holding for days with `automatic({ tracker, where: { path: "amount", gte: { setting: "threshold" } } })` and `streak({ days })`. In SDK 1.x it is a custom source on the reserved namespace `wallet-balances`, with the chain and token in the new optional `balance` field of custom sources (`walletBalanceSourceSchema`); ingestion refuses the namespace, and a project and environment can have at most 5 such trackers (`limit-reached`).

  `management.trackers.create` and Console **New tracker** take `{ kind: "wallet-balances", chain, token }` (`managedTrackerSourceSchema`), and `domino trackers create --source wallet-balances` takes `--chain` and `--token`. The new `token-hold-streak` snippet rewards holding a token for 7 days in a row.

#### Patch Changes [#patch-changes-41]

* dbffafb: On a Node release older than 24 the CLI now prints `Domino CLI needs Node 24 or newer. You have <version>.` and exits 1, or the `NODE_VERSION_UNSUPPORTED` error with `--json`, instead of crashing with a `SyntaxError`. A `relay.json` that still has a starter's placeholder organization (`local`) and no checkout scope is refused with the new `SCOPE_PLACEHOLDER` code, which says to run `domino checkout` or pass `--organization` and `--project`; `domino doctor` reports it as a `scope` error. `whoami`, `login`, `logout`, `create`, `checkout`, `init` and `feedback` ignore the placeholder instead of sending it.
* 1e2de20: Agents can act as test members on staging. A staging link (`domino api POST /staging/link`) minted by a `domino login` session, or by a personal API key or organization automation key that holds `publish`, now opens the test toolbar, acting as that person or as `automation:<id>`. Domino checks the key again on every toolbar request, so revoking it ends access. The Domino skill's campaign testing reference adds "Check participant flows on staging": mint the link, open it, sign the page in as a test member, and walk the app's own screens.
* Updated dependencies \[7012d1c]
* Updated dependencies \[59a01d8]
* Updated dependencies \[956b721]
* Updated dependencies \[1770ada]
* Updated dependencies \[cec850f]
* Updated dependencies \[94726d8]
* Updated dependencies \[6d9f0de]
* Updated dependencies \[8d11f64]
  * @domino-sdk/relay\@1.57.0

### 1.56.0 [#1560-1]

#### Minor Changes [#minor-changes-116]

* 9ec9f2f: Publishing creates the groups code names. A deployment whose quest `groupMembership(group)` reward or `inGroup(group)` prerequisite, offer `inGroup(group)` requirement, leaderboard `group` or referral `inviterGroup` names a group the environment doesn't have now creates it, with no members and the `defineGroup` handle's title (the key when only a leaderboard or referral program names it), instead of refusing with 422 `validation-failed`. Approving a promotion creates the groups live lacks the same way. The deployment preview and result, and the prepared promotion, list each such group as a `group-created` warning, and the audit history records `group-created` with the deployment or promotion as its reason. An existing group keeps its title and members, and publishing never unarchives one: a `groupMembership` reward naming an archived group still refuses. A project whose code uses groups no longer needs `domino groups create` before its first deployment or promotion.
* fbe8e64: Offers take prerequisites. `defineOffer({ ..., requires })` accepts the prerequisites quests do, `completed(quest)`, `balanceAtLeast(ledger, minimum)` and `inGroup(group)`, at most 30; `offerDefinitionSchema` and `offerAuthoringSchema` gain an optional `requires`. Publication refuses a required quest or ledger the deployment doesn't know. Redeeming as a member who misses one fails with 409 `prerequisites-incomplete` and `missing`, the shape quest admission uses, before any points or stock are spent. `Offer` gains `requires` and `missing`: for a signed-in member, `relay.offers.list()` and `useOffers()` list the prerequisites they have not met, empty for a read without a member or an offer without `requires`. `availability` keeps its four kinds. Console Rewards shows an offer's requirements on its card, and a promotion carries them to live.
* cba70f4: Add the organization's billing read. `management.billing.organization()` and `domino billing organization` (`GET /management/v1/organization/billing`) are open to any member of the organization and need no project. They list the projects the caller may read, each with its plan, `live` and `liveState` as `management.usage.plan()` reports them, and its Stripe subscription. They also return the organization's Free projects against their limit (`freeProjects`), the free AI allowance its Free projects share (`freeAi`), the monthly AI allowance across the organization, the trial, whether plan limits are enforced, and whether the caller may open the customer portal (`billing`). `management.billing.organizationPortal()` (`POST /management/v1/organization/billing/portal`) opens the customer portal as `management.billing.portal()` does, returning to Console's organization billing page. `organizationBillingSchema` and the `OrganizationBilling` type are exported from `@domino-sdk/relay/server`, with a reader in `managementResponses`.
* 6458ebe: Add the project's plan read. `management.usage.plan()` and `domino usage plan` (`GET /management/v1/usage/plan`, `read` permission) return the project's plan and where it came from (`source`), whether it may serve live and why (`live`), live's notice or offline state (`liveState`), the organization's one live trial (`trial`), whether the plan includes a custom domain, the AI builder balance by pool with when it renews (`ai`), the Stripe subscription as `management.billing.read()` reports it, and whether plan limits are enforced. Unlike `management.billing`, it is open to anyone who can read the project. `projectPlanSchema` and the `ProjectPlan` type are exported from `@domino-sdk/relay/server`, with a reader in `managementResponses`.
* 62c3791: Add TikTok creator accounts and the `tiktok.videos()` tracker source. A signed-in member links their TikTok account through TikTok's consent screen with `relay.auth.creatorAccounts.connect("tiktok")` (`useCreatorAccounts()` in React); the account's subject is its `open_id`, its profile a display name and avatar, and `knownCreatorAccountProviders` now lists `"tiktok"`. Operators read and unlink it like a YouTube channel. `tiktok.videos()` from `@domino-sdk/relay/authoring` reads the public videos on the accounts members linked, only with each member's own token, every `refreshSeconds` of the tracker, as content with a `TikTokVideoBody` (`title`, `description`, `durationSeconds`, `width`, `height`, `createdAt`, `shareUrl`, `isAigc`) and the metrics `views`, `likes`, `comments` and `shares`, so `content()` quests, `performance` and engagement rules use it. A known video the account no longer returns is a content removal; an account whose token needs reconnecting is not read, as `provider-unavailable`. It is a custom source on the reserved namespace `tiktok-videos`, which ingestion refuses. Both are inert until Domino configures TikTok. The `tiktok-videos` capability and the participant and authoring references describe them.
* a533f93: Add creator accounts and the `youtube.shorts()` tracker source. A signed-in member links the YouTube channel they publish from as a creator account: `relay.auth.creatorAccounts.list()` (`GET /v1/auth/creator-accounts`), `connect(provider)` and `disconnect(provider)`, and `useCreatorAccounts()` in React. A creator account is never a sign-in method, linking another channel replaces the current one, and the account's subject is the channel ID picked on Google's consent screen. Each entry mirrors `AccountConnection` with the channel's `profile`; `provider` is an open string, with `knownCreatorAccountProviders` in `@domino-sdk/relay/schema` listing `"youtube"`, so `CreatorAccount`, `CreatorAccounts` and their schemas sit beside SDK 1.x's closed provider lists. Operators read them on `management.members.get(member).creatorAccounts` and `management.members.creatorAccounts(member)`, and `management.members.unlinkCreatorAccount(member, { provider, subject, revision, reason })` unlinks one with `review` permission; `managedCreatorAccountSchema`, `managedCreatorAccountsSchema` and `memberCreatorAccountUnlinkInputSchema` are exported from `@domino-sdk/relay/server`. `youtube.shorts()` from `@domino-sdk/relay/authoring` reads the Shorts on the channels members linked, every `refreshSeconds` of the tracker, as content with a `YouTubeShortBody` (`title`, `description`, `tags`, `durationSeconds`, `publishedAt`) and the metrics `views`, `likes` and `comments`, so `content()` quests, `performance` and engagement rules use it. It is a custom source on the reserved namespace `youtube-shorts`, which ingestion refuses. Both are inert until Domino configures YouTube. The `creator-accounts` and `youtube-shorts` capabilities and the participant and authoring references describe them.

#### Patch Changes [#patch-changes-42]

* 42784fb: The hosted-app skill reference notes that every `domino.page` address answers `/.well-known/security.txt` itself with Domino's abuse contact, so an app never receives that path there; a custom domain still forwards it.
* a88ba4d: The capability map and the Domino skill's automation reference describe Domino's MCP server at `https://mcp.domino.run`, which operators connect to Claude or ChatGPT: reads of projects, quests, campaigns, submissions, members, leaderboards, offers, usage, analytics and audit history in test and live, and test-member and review tools that write in test only. See `domino capabilities mcp-server --json` and the "Claude and ChatGPT" section of `domino reference automation`.
* Updated dependencies \[9ec9f2f]
* Updated dependencies \[fbe8e64]
* Updated dependencies \[cba70f4]
* Updated dependencies \[6458ebe]
* Updated dependencies \[62c3791]
* Updated dependencies \[a533f93]
  * @domino-sdk/relay\@1.56.0

### 1.55.0 [#1550-1]

#### Minor Changes [#minor-changes-117]

* 5c70548: Add monthly active member usage. `management.usage.activeMembers()` and `domino usage active-members` (`GET /management/v1/usage/active-members`, `read` permission) return the project's live active members this UTC month (`month`, `activeMembers`), its plan and the plan's `limit` (null when the plan has none), and the six earlier months in `history`, newest first. An active member was active in the month on a UTC day after the day they joined the project: a signed-in participant API request, which includes their attempts and redemptions. Imports, server-reported tracker records and test members never count. `activeMemberUsageSchema` and `activeMemberMonthSchema` are exported from `@domino-sdk/relay/server`, with a reader in `managementResponses`.

* 65f3a9a: Add four metrics to the analytics catalog. `groups.joins` and `groups.leaves` count members added to and removed from a group, by when the change happened, without repeats that left membership as it was or memberships recorded without a date; their `group` breakdown and filter mean the change's group. `trackers.records` counts tracker trigger outcomes by when the record was observed, as each outcome is now, with an `outcome` (`admitted`, `skipped`) or `tracker` breakdown and a `tracker` filter that keeps the tracker's own outcomes. `leaderboards.settledParticipants` counts the rows of pending and approved leaderboard settlements by each settlement's cutoff, with a `leaderboard` breakdown and filter, and draws as `number`, `bar` or `table`. `analyticsBreakdowns` gains `group`, `tracker`, `outcome` and `leaderboard`, and `analyticsFilters` gains `leaderboard`. A dashboard panel naming a leaderboard the environment doesn't have fails the deployment with `unknown-reference`.

* 8f12977: Add the closed metric catalog. `management.analytics.catalog()` and `domino analytics metrics` (`GET /management/v1/analytics/catalog`) list every metric with its definition, kind (`count`, `distinct` or `rate`), the breakdowns and filters it accepts, its allowed charts and whether it needs a point ledger; `analyticsCatalog` and the `AnalyticsMetricId`, `AnalyticsBreakdown`, `AnalyticsFilter` and `AnalyticsChart` types are exported from `@domino-sdk/relay/server`. `management.analytics.query({ metric, range, breakdown?, filter?, ledger?, limit? })` and `domino analytics query --metric ID [--range] [--breakdown] [--filter name=a,b]... [--ledger] [--limit]` (`GET /management/v1/analytics/query`, `read` permission) read one metric bucketed like `analytics.read`, and with no breakdown or filter return the same numbers. A breakdown by quest, offer, referral program, rejection source or rejection reason has a row per matching resource (archived ones only with activity), sorted by total and capped by `limit` (1 to 100, default 100) with the rest summed into `other`. Filters narrow to quests, offers or programs by key, the quests a tracker triggers, or members now in a group. The new rates `quests.rejectionRate` and `referrals.qualificationRate` are `null` below 10 in the denominator. A query the catalog doesn't accept is a 422 with `unknown-metric`, `breakdown-not-accepted`, `filter-not-accepted`, `ledger-required`, `ledger-not-accepted`, `invalid-limit` or `unknown-reference`, and a missing group is `group-not-found`. `analyticsCatalogSchema`, `analyticsMetricQuerySchema` and `analyticsMetricResultSchema` are exported, with readers in `managementResponses`.

* c91ba35: A repeatable quest can space out a member's completions. Set `completionCooldownMs` (1,000 to 604,800,000) on a quest members submit that completes more than once, or **Completion cooldown** on a quest made in Console. After a completion of any key is accepted, a new attempt is refused with 409 `completion-cooldown` (`RelayErrorCode.CompletionCooldown`) until `retryAt`; when the retry cooldown also runs, the later one is named. A quest that completes once refuses the field. Quest progress and `QuestView` carry `nextAvailableAt`, when the member may next submit: the latest of the retry cooldown's end, the completion cooldown's end and an exhausted budget's next reset, omitted when nothing will reopen the quest. `questStatus` reports `completion-cooldown`, `canSubmit` is false until then, and `useQuest` renders again when it ends. Catalog quest instances take an optional `completionCooldownMs` (`null` for none). A quest without the field keeps today's behavior.

* 6b946d8: Add dashboards: a `relay.json` definition kind built from the metric catalog. Default-export `defineDashboard({ key, title, description?, defaultRange?, position?, sections })` from `@domino-sdk/relay/authoring`, typed so each panel accepts only the breakdowns, filters and charts its metric allows, and list it under `dashboards: [{ entry, exportName? }]` in relay.json. A deployment refuses a dashboard with a coded 422 (`unknown-metric`, `breakdown-not-accepted`, `breakdown-required`, `filter-not-accepted`, `chart-not-accepted`, `ledger-required`, `ledger-not-accepted`, `unknown-reference`, `referral-program-required`, `duplicate-key`, `too-many-panels` or `too-many-dashboards`), publishes a new version only when the definition changed, and reports each one in the preview's `dashboards` as `created`, `updated` or `unchanged`. `management.dashboards.list({ includeArchived? })` and `domino dashboards list` (`GET /management/v1/dashboards`) list them; `management.dashboards.read({ key, range? })` and `domino dashboards get KEY [--range]` (`GET /management/v1/dashboards/KEY`) return the definition and every panel's `analytics.query` result, with a failing panel's own `error` (`group-not-found`, `panel-timeout`) beside the others. `management.dashboards.archive|unarchive(key, { reason })` and `domino dashboards archive|unarchive KEY --reason` (`POST /management/v1/dashboards/KEY/archive|unarchive`) archive and restore one in test, returning `{ dashboard, changed }`, and promotions carry dashboards to live by key as changes of kind `dashboard`. The deployment preview's `dashboards` is optional, so values built for older SDKs still fit. `dashboardDefinitionSchema`, `dashboardAuthoringSchema`, `normalizeDashboard`, `dashboardIssue` and `dashboardPreviewSchema` are exported from `@domino-sdk/relay/schema`, and `dashboardListSchema`, `dashboardReadSchema` and `dashboardLifecycleResultSchema` from `@domino-sdk/relay/server`, with readers in `managementResponses`.

* 3a8a888: Engagement points land in the leaderboard window they were measured in. Each engagement settlement's point entries now take effect when the content sample that measured their growth was observed, instead of when the daily settlement paid them, and budgets charge each entry to the period of its `effectiveAt`. When the growth between two samples spans a leaderboard window or budget period boundary, Relay splits it there in proportion to time (rounded down, the remainder to the later part), one entry per side. Before, every earned board over engagement points ran about a day late.

  A settling leaderboard whose score counts an engagement rule's points now takes a closing measurement before it freezes. While it closes, its settlement reads as `pending` with no rows in the review reads (`settlementReviews`, `settlementReview`), with a new optional `closing` field holding `startedAt` and `deadline`: Relay asks scheduled trackers to resample the content, a sample observed within 30 minutes after the cutoff counts as measured at it, and the settlement freezes when everything is measured or the wait ends. Settlements gain `unmeasured`: content frozen at its last good sample, with `content`, `member` and `observedAt`. `management.leaderboards.retrySettlementMeasurement(id, { actionId? })` and `domino leaderboard-settlements retry-measurement SETTLEMENT` (`POST /management/v1/leaderboard-settlements/:id/retry-measurement`, `review` permission) close such a settlement again for another 30 minutes. Approving a closing settlement fails with `settlement-closing`; retrying one with nothing unmeasured fails with `settlement-measured`. The older `settlements` and `settlement` reads omit closing settlements.

* 6cc08fa: A member can greet themselves by first name. Each entry of `relay.profiles.mine().options` gains `givenName`: the first name Google sent at sign-in (OIDC `given_name`), null for X, Discord and accounts without one. Older servers' answers still parse, with `givenName: null`. Only the member's own profile carries it; profile reads by ID and leaderboard rows don't. Existing Google accounts get it on their next sign-in. Operator member reads are unchanged.

* 921e9d7: Add `dominoHosting({ command })` to `@domino-sdk/relay/vite`. It returns `cloudflare`, options for the app's own `cloudflare()` call from `@cloudflare/vite-plugin` (the Worker in Vite's `server` environment, and, only when serving, the variables `domino dev` sets forwarded to the local Worker, empty when unset), and `server`, Vite's server settings (port from `DOMINO_APP_PORT`, 5173 by default, and the `DOMINO_PREVIEW_ORIGIN` host with HTTPS hot reload when it is set). The SDK does not depend on the Cloudflare plugin.

  Add `domino build --hosting-artifacts`, which writes the quest and catalog artifacts the hosting builder reads to `.domino-build/quests.json` and `.domino-build/catalog.json` beside `relay.json`.

  The starters use both: their `vite.config.ts` is their own plugins plus `dominoHosting()`, and `scripts/build-domino.mjs` gives way to `vite build && domino build --hosting-artifacts`. Existing apps keep working unchanged.

* a801a1d: Leaderboard settlements can split a pool prize by score. `settle.pool` is `{ amount, unit, ledger?, minScore? }`: `amount` is the whole pool in base units as a decimal string, and `unit` is `"points"` or `{ code, decimals }` for a token or currency. A points pool names the `ledger` it pays into (a ledger handle or key in `defineLeaderboard`) and holds at most 2,147,483,647 points; other units take no ledger. With a pool, `settle.prizes` may be empty (`defineLeaderboard` lets you leave it out), and a board can have both. When the window freezes, Relay splits the pool over the eligible frozen rows in proportion to score, in exact integer math: members scoring 0 or less, or at or below `minScore`, get nothing, each pool share rounds down, and the leftover base units go one each to the largest remainders, earlier positions first, so the shares sum exactly to the pool. With no eligible score the whole pool stays unallocated.

  Settlement reads (`settlement`, `settlementReview`) gain `pool`, with `allocated` and `unallocated`, and each row gains `share` in base units; both are absent from settlements without a pool. Approval pays a points pool's shares as points in each member's prize completion, and revoking the completion reverses them. Relay pays no other unit: `management.leaderboards.settlementExport(id)` (`GET /management/v1/leaderboard-settlements/:id/export`, `read` permission) and `domino leaderboard-settlements export SETTLEMENT` (CSV, or `--json`) list each member's score, rank, share in base units and whole units, and verified linked wallets, as the operator's payout list. Console shows each winner's pool share before approval.

* cbdcdc0: Add the plan limits in test and setup, where plans apply to the organization. A sign-in that would add a real person to a test environment that already has its plan's number of real people fails with `limit-reached` (403, `limit: "real-people-in-test"`, `count`, `maximum`); people who already joined, test members and live sign-ins are unaffected. Creating a project, or transferring a Free project in, fails with `limit-reached` (403, `limit: "free-projects"`) when the organization already holds 10 Free projects. Adding a custom domain needs the Growth plan or above and otherwise fails with `plan-required` (403, `feature: "custom-domain"`). `management.usage.realPeopleInTest()` and `domino usage real-people-in-test` (`GET /management/v1/usage/real-people-in-test`, `read` permission) return the test environment's `realPeople`, the project's `plan`, its `limit` and whether it is `enforced`; `realPeopleInTestUsageSchema` is exported from `@domino-sdk/relay/server`, with a reader in `managementResponses`. The CLI's error JSON keeps a `limit-reached` or `plan-required` refusal's detail in `details`.

* 823a691: Member accounts that Relay reads in the background with the member's own OAuth tokens now report when those tokens stop working. In management member reads (`management.members.get`, `list`, `lookup`, and the `person` on attempts) such an account carries `status: "connected"`, or `status: "needs-reconnect"` with a `reason` of `revoked`, `expired` or `missing-scope`; accounts Relay keeps no tokens for carry neither field. `domino members get` and the other CLI results include the same optional fields. The participant client's `relay.auth.accounts()` and `useAccounts()` report the account's `status` as `"reconnect"` until the member links it again. Relay refreshes kept tokens before they expire. No provider reads accounts in the background yet.

* a4c93bc: Add Stripe billing for a project. `management.billing.checkout({ plan, interval })` and `domino billing checkout --plan launch|growth|scale [--interval month|year]` (`POST /management/v1/billing/checkout`) return a Stripe Checkout URL that subscribes the project; `management.billing.portal()` and `domino billing portal` (`POST /management/v1/billing/portal`) return the organization's Stripe customer portal. Both need an organization administrator, not an API key (403 `permission-required`). While billing is staff-only, every organization but Domino's own is refused with 403 `billing-staff-only`. A project whose subscription holds its plan is refused another checkout with 409 `subscription-exists`, and the portal answers 409 `no-customer` before the organization's first checkout; a deployment without Stripe answers 503 `not-configured`. `management.billing.read()` and `domino billing show` (`GET /management/v1/billing`, `read` permission) return whether billing is open to the caller (`available`, `reason`) and, when it is, the project's `subscription`: `plan`, `interval`, Stripe's `status`, `currentPeriodEnd`, `cancelAtPeriodEnd`, `lastInvoice` and `livemode`. `billingCheckoutInputSchema`, `billingSessionSchema`, `billingSubscriptionSchema` and `projectBillingSchema` are exported from `@domino-sdk/relay/server`, with readers in `managementResponses`.

* b8348fd: Change a project's Stripe plan, cancel and resume. `management.billing.changePlan({ plan, interval })` and `domino billing change --plan launch|growth|scale [--interval month|year]` (`POST /management/v1/billing/plan`) move a paying project's subscription: a higher plan or annual billing applies now with the prorated difference invoiced at once, and a lower plan or monthly billing is held for the renewal. The answer is `billingPlanChangeSchema`: `timing` (`now`, `renewal` or `none`) and `invoiceUrl`, set when the immediate charge needs the customer and nothing changes until it is paid. `management.billing.cancel()` and `management.billing.resume()` (`domino billing cancel`, `domino billing resume`; `POST /management/v1/billing/cancel|resume`) set the subscription to end when its period does, or take that back, and return the project's billing. A project without a subscription that holds its plan is refused with 409 `no-subscription`, and a plan change while the subscription is set to cancel with 409 `subscription-canceling`. The subscription now carries `scheduledChange`, the plan, interval and date of a change held for the renewal, or null. The customer portal no longer changes or cancels plans; it keeps cards, invoices and billing details.

* c055c43: A project's Stripe subscription follows the project. Deleting a paying project sets its subscription to end with its period, and restoring it resumes the subscription; purging ends it at once. A transferred project keeps its plan until the old organization's period ends, and `management.billing.checkout()` (`domino billing checkout`) subscribes it in the receiving organization with no charge before then. The subscription now carries `endedFor`: `project-deleted`, `project-transferred` or null.

* f89ea5d: Buy AI builder credit, and see an overdue subscription. `management.billing.topup({ quantity })` and `domino billing topup [--quantity 1-10]` (`POST /management/v1/billing/topup`) return a Stripe Checkout URL buying `quantity` packs of $10 AI builder credit for the project, on any plan; the credit is added once the payment is in, once per purchase. `billingTopupInputSchema` and `billingTopupMaxQuantity` are exported from `@domino-sdk/relay/server`. The subscription now carries `unpaid` while an invoice is overdue: `since` (when it first failed), `promotionsRefusedAt` (14 days later, when the organization's new promotions are refused with `plan-required` and `reason: "unpaid"`) and `offlineAt` (45 days later, when the project's live goes offline until it is paid), or null.

* 8a5b7d3: Add `settings.amount({ label, default, decimals? })` and `settings.chain({ label, default })` to `@domino-sdk/relay/authoring`. An amount setting holds a non-negative base-unit integer string such as `"1000000000000000000"`; with `decimals`, Console shows and edits it in whole tokens. A tracker filter bound may name an amount setting (`gte: { setting: "minimum" }`) and compares it exactly, so 18-decimal thresholds fit. A chain setting holds `"solana"`, Solana mainnet's CAIP-2 ID or an EVM chain ID such as `"eip155:8453"`, typed as the `ctx.wallet.balance()` query's `chain`. Both are text settings (`picker: "amount"` or `"chain"`; text fields gain an optional `decimals`), and publication and Console edits refuse other values. `@domino-sdk/relay/schema` exports `isAmount`, `isBalanceChain`, `isBoundSetting`, `textSettingSchema` and the `AmountSetting`, `BoundSettings` and `BalanceChain` types. The `token-holder` example and snippet use both settings and need no `WalletBalanceQuery` cast; their minimum is now in base units.

* 4aaea1a: X tracker refresh policy. Tracker status gains `pauseReason` (`operator`, `x-budget-exhausted` or `test-window-ended`; null when not paused), `resumesAt` (when a tracker paused on its X budget resumes on its own) and `effectiveRefreshSeconds` (the interval a scheduled tracker actually runs at: `refreshSeconds`, but no shorter than the project's plan allows, 6 hours below Enterprise). All three are optional, so older servers' answers still parse. Known X posts are re-read every 6 hours for 48 hours after they were posted, daily until day 7, then never. A deployment whose X search tracker asks for less than the plan's minimum warns `tracker-refresh-below-plan-minimum` and still publishes. A spent monthly X budget pauses X trackers with `x-budget-exhausted` (live only while plan limits are enforced, test always) until the budget renews, and resuming one meanwhile answers 409 `x-budget-exhausted`; a test X tracker pauses with `test-window-ended` after 14 days until resumed. `domino trackers create|update --refresh-seconds` help names the plan's minimum.

#### Patch Changes [#patch-changes-43]

* 283a1a2: The Domino skill has an analytics reference (`domino reference analytics`): how to answer "how is it going" from `domino analytics get` and `domino analytics query`, choose catalog metrics from a project's goals, and build, check and explain dashboards. The `project-analytics` and `dashboards` capabilities point to it. The referral starter ships a Referrals dashboard with sign-ups, qualified referrals by program and the qualification rate.

* 21a4386: `domino check`, `domino build` and `domino deploy` refuse quests that declare one budget key with a different scope, period, limit or cost, naming both quests and the fields that differ. `check` reports it as the error diagnostic `BUDGET_DECLARATION_CONFLICT`; `build` and `deploy` fail with `BUNDLE_FAILED`. Relay also compares `cost` when it checks a deployment's shared budgets.

* dbc4e34: `domino` starts faster. `domino --version` and `--help` drop from about 0.4 s to under 0.1 s, and commands that call the management API through the CLI's own transport (`domino api`, `domino builds`, `domino whoami`) from about 0.44 s to about 0.29 s. The CLI now loads the SDK, zod and the system certificate store only when a command needs them. Output, exit statuses and `--json` results are unchanged.

* 5a8a0a0: CLI paper cuts. `domino stage --commit` accepts a short or uppercase SHA: a short one resolves to the full commit in the checkout, failing with Git's reason when it is unknown or ambiguous; a `--request-id` resume still takes the full SHA the build request printed. `domino api` strips one `/management/v1` the path already starts with, so `domino api POST /management/v1/staging/link` reaches `/staging/link` (a one-line note on stderr says so outside `--json`). A reader that closes stdout early, as in `domino --help | head`, ends the CLI with status 0 instead of an unhandled EPIPE. `domino test-members act --wait [--timeout SECONDS]` reads the attempt until it is no longer queued, evaluating or deferred and prints it with its status, reason and code; without `--wait` it prints the queued attempt as before. The deprecation notes of bare `domino status`, `quests` and `releases` name the typed read for each use, the help says that `--json` stdout carries exactly one JSON value while progress events go to stderr as NDJSON, and `domino build --help` recommends `--out FILE --json` for a summary. Both packages now ship their `CHANGELOG.md`.

* 9e5c5c2: Going live needs a plan where plans apply to the organization. Approving a promotion for a project without a plan starts the organization's one 14-day live trial when it is unused, and otherwise fails with `plan-required` (403), whose `trial` says where the trial is. When a trial ends without a plan, live keeps serving for 7 days, then goes offline: participant requests to live fail with `live-offline` (403) and the live app answers a "This app is offline" page, until the project gets a plan. The error reference describes both codes.

* a242e19: The errors reference lists `ai-allowance-exhausted`: a 402 when a project's AI builder balance is empty and the Playground cannot start.

* ea4814c: Add the `trackers` reference to the Domino skill, so `domino reference trackers` serves tracker collection options, record filters and attribution, tracker trigger rules and outcome codes, Discord record fields, and engagement accounting.

* c656f61: A quest's `completion` can be a function of the policy builders: `completion: ({ keyed }) => keyed(({ tracker, settings }) => tracker.record.data.messageId)`. Its key callbacks, `keyed(...)`'s key and `progress(...)`'s `distinct`, read a `QuestKeyedContext` typed from the quest: `tracker` is non-null and typed for a tracker trigger, and `settings` has the quest's settings, so the `tracker!` cast is no longer needed. The builders it receives are `once`, `keyed`, `streak` and `progress`. `completion: keyed(...)` built inline keeps working and keeps its `KeyedContext`. Key callbacks now also receive the quest's parsed `settings` at runtime.

  `runQuest` reports `run.diagnostics` (and `createTestContext` its `diagnostics`): a `completion-key-ignored` entry when the evaluator returns a `completionKey` the completion policy ignores. Only `keyed(...)` uses it; `once()`, `streak(...)`, `progress(...)` and content quests key completions themselves. The decision is unchanged, and hosted attempts record nothing new. The Domino skill's authoring and tracker references describe the builder form and which policies use `completionKey`.

* Updated dependencies \[5c70548]

* Updated dependencies \[65f3a9a]

* Updated dependencies \[8f12977]

* Updated dependencies \[5a8a0a0]

* Updated dependencies \[c91ba35]

* Updated dependencies \[6b946d8]

* Updated dependencies \[3a8a888]

* Updated dependencies \[5ae31db]

* Updated dependencies \[d463895]

* Updated dependencies \[7ff5984]

* Updated dependencies \[6cc08fa]

* Updated dependencies \[921e9d7]

* Updated dependencies \[6156dec]

* Updated dependencies \[a801a1d]

* Updated dependencies \[cbdcdc0]

* Updated dependencies \[823a691]

* Updated dependencies \[cc459ef]

* Updated dependencies \[a4c93bc]

* Updated dependencies \[b8348fd]

* Updated dependencies \[c055c43]

* Updated dependencies \[f89ea5d]

* Updated dependencies \[8a5b7d3]

* Updated dependencies \[c656f61]

* Updated dependencies \[4aaea1a]
  * @domino-sdk/relay\@1.55.0

### 1.54.0 [#1540-1]

#### Minor Changes [#minor-changes-118]

* ee9e5f1: Add `management.analytics.read({ range, ledger })` and `domino analytics get [--range 7d|30d|90d|all] [--ledger KEY]`, which read a project's activity in one environment from `GET /management/v1/analytics` with `read` permission. A range ends now and is bucketed by project-calendar day (`7d`, `30d`) or Monday week (`90d`, `all`, which starts on the day of the earliest dated record: the first membership, or an older imported completion). Each metric (sign-ups, active members, attempts, completions, rejections, attempts sent to staff review, one point ledger's points awarded, spent and net and members earning, fulfilled offer redemptions, accepted and qualified referrals) has a total, a value per bucket and the previous range's total. Referrals also report `acceptedQualified`, how many of the range's accepted referrals have qualified since, for a share that never exceeds 100%. Each quest also has its decisions, rejection rate, top 5 rejection reasons and rejections by source. Offers and referrals appear only when the project has them, and a test environment section only in test. `domino analytics get` prints a readable summary, and the read with `--json`; `analyticsReadSchema` and `analyticsQuerySchema` are exported from `@domino-sdk/relay/server`.

  `managementResponses` from `@domino-sdk/relay/server` now builds each reader on first use, as `managementResponseSchemas` does, so importing the entry retains about 3.5 MiB less heap. Each reader is built once and stays writable.

* 63f7a14: Custom domains. An organization admin can attach a domain they control, such as `quests.example.com`, to a project's live hosted app. Domino shows a CNAME to `cname.domino.page` and a `_domino.` TXT record that proves the domain, issues its certificate, and serves the live app there once both are in place. While the domain is active, page loads on `<project-slug>.domino.page` redirect to it.

  * `domino domains add|list|get|recheck|remove` and Console **Settings → Domains** attach, check and remove the domain, and show each DNS record against what public DNS returned and Cloudflare's statuses word for word.
  * `management.domains.list()`, `.get(id)`, `.add(hostname)`, `.recheck(id)` and `.remove(id)`.
  * Root domains, wildcards, IP addresses and Domino's own domains are refused with `invalid-hostname`. A project has one custom domain.
  * The `hosted-custom-domains` capability is now supported.

* f5bd728: Test members can accept a referral invitation: `management.testMembers.acceptReferral(member, { code })` or `{ inviter }` (another test member, whose invitation code is used), and `domino test-members accept-referral MEMBER --code CODE` or `--inviter MEMBER`. The acceptance runs as a signed-in member's would, with the same refusals, qualification and rewards, in the test environment only, and the audit names the operator.

#### Patch Changes [#patch-changes-44]

* e735ab8: `domino migrate` renames `id` to `key` in referral bonuses and milestones, tiers, and `budget()` calls, and `balance` to `ledger` in referral bonuses, leaderboards, and budget costs. Strings where v1 takes a handle (`quests`, `qualifiedBy`, `ledger`, prerequisites) get a `HANDLE_REFERENCE_REQUIRED` error naming the field instead of passing silently, and migrate no longer says "No automatic changes needed" while such errors remain.

* 50246d7: Hosted app builds no longer set `VITE_DOMINO_STAGING`. It was `true` in every build, including the builds the live app serves, so an app that gated test tools on it showed them in live. Builds now see only `CI=true`. To tell staging from live, call `getHostEnvironment()` from `@domino-sdk/relay/browser` in the browser (static and Worker apps), or read `DOMINO_ENVIRONMENT` in a Worker's server code.

  Staging moved from `staging-<project-slug>.domino.run` to `<project-slug>--staging.domino.page`. The old staging addresses, and development previews on `domino.run`, now answer `410 Gone` with a page naming the new address; they do not redirect. Links printed with an old staging address, such as QR codes, need the live address instead.

* Updated dependencies \[ee9e5f1]

* Updated dependencies \[63f7a14]

* Updated dependencies \[50246d7]

* Updated dependencies \[f5bd728]
  * @domino-sdk/relay\@1.54.0

### 1.53.1 [#1531-1]

#### Patch Changes [#patch-changes-45]

* 500f752: `domino check` and `domino migrate` resolve `defineLedger` handles, defined in the same file or imported, to their keys, so `points(handle, n)` no longer warns `LEDGER_REFERENCE_DYNAMIC`. They also check v1 `ledger:` references in leaderboards, referral bonuses and `budget({ cost: { ledger } })`, and count a ledger published in `relay.json` `pointLedgers` as `{ "entry", "exportName" }` as declared, so `domino migrate` no longer proposes an inline duplicate for it. A reference that really needs code to resolve suggests publishing a `defineLedger` export.
* @domino-sdk/relay\@1.53.1

### 1.53.0 [#1530-1]

*Not published to npm. Its changes first reached npm in 1.53.1.*

#### Minor Changes [#minor-changes-119]

* 1b789ce: Test members can hold real X and Discord accounts. `management.testMembers.linkAccount(member, { actionId, provider, id })`, or `{ actionId, provider: "x", username }` resolved through X, links a public account to a test member without OAuth, and `unlinkAccount(member, { actionId, provider, id })` unlinks and frees it (`domino test-members link-account` and `unlink-account`). Quest code's `x.profile()`, `follows()`, `reposted()` and Discord role reads then answer for the real account, tracker records it authors are attributed to the member, and `testMembers.record` posts as it instead of refusing with `conflict`. An account another member holds is refused with `account-linked-elsewhere`; when its owner signs in with it or connects it, it moves to them. Reset unlinks it. Relay never writes to a test-linked account: a Discord role reward for one creates no delivery and is never sent, and the attempt's `history` entry carries `rewardsNotSent: [{ reason }]`. Test member results include an optional `accounts` list; managed member identities and `members.oauthAccounts` mark these accounts with an optional `testLinked` flag.

#### Patch Changes [#patch-changes-46]

* Updated dependencies \[1b789ce]
  * @domino-sdk/relay\@1.53.0

### 1.52.1 [#1521-1]

#### Patch Changes [#patch-changes-47]

* b857dc5: The member imports skill reference sends live imports to Console's **Members** > **Imports**, where member imports now open. Console no longer has a separate Member imports page.
* @domino-sdk/relay\@1.52.1

### 1.52.0 [#1520-1]

#### Minor Changes [#minor-changes-120]

* 9fadb80: Evaluator rejections take an optional code. `decision.reject({ reason, code })` from `@domino-sdk/relay/authoring` names the cause with a stable key, such as `wrong-answers`, that stays the same when the reason's wording changes. Relay stores it as the attempt's `code`, returned by `management.attempts.get` and `management.attempts.list`, `domino attempts list|get` and Console's attempt detail beside the reason; responses to members carry `code: null` for an evaluator-rejected attempt. A code uses lowercase letters, digits and hyphens, at most 64 characters, and cannot be one of the codes Relay writes itself, now exported as `reservedAttemptCodes` from `@domino-sdk/relay/schema`; an invalid one fails the evaluation, and `runQuest`, with an error naming the rule. A content quest's `performance.evaluate` accepts the same `code`. Rejections without a code are unchanged.

#### Patch Changes [#patch-changes-48]

* Updated dependencies \[9fadb80]
  * @domino-sdk/relay\@1.52.0

### 1.51.0 [#1510-1]

#### Patch Changes [#patch-changes-49]

* Updated dependencies \[4dddeb0]
  * @domino-sdk/relay\@1.51.0

### 1.50.0 [#1500-1]

#### Minor Changes [#minor-changes-121]

* 2ecde12: An operator can pass a quest for a test member without running its check, or accept a test member's rejected attempt anyway, to reach the quests behind a check that can't be satisfied in test.

  * New `management.testMembers.pass(member, { actionId, quest, completionKey? })` admits an attempt on a manual quest as a submission would, with no input, then accepts it as the operator. Prerequisites, availability, enrollment and the retry cooldown still refuse it, and the completion policy, budgets and rewards apply as for an accepted check: a progress quest counts one step, and a keyed quest without `completionKey` gets a fresh key. From the CLI, `domino test-members pass MEMBER QUEST [--completion-key KEY]`.
  * New `management.testMembers.acceptAnyway(member, { actionId, attempt })` accepts a rejected attempt as the operator, such as a tracker-triggered attempt its check rejected. The attempt keeps its rejection in its history, and when it is the member's latest attempt on the quest, its retry cooldown ends. Any other status is refused with 409 `attempt-not-rejected`. From the CLI, `domino test-members accept-anyway MEMBER ATTEMPT`.
  * Both answer the decided attempt and record a `test-member-decision` event with the operator as actor, listed in the `test-members` audit category. `testMembers.history` attempts an operator decided carry `operatorDecision: { kind, actor, at }`.

  Both need `publish` permission in the test environment, like the other test-member methods; live refuses them.

#### Patch Changes [#patch-changes-50]

* Updated dependencies \[2ecde12]
  * @domino-sdk/relay\@1.50.0

### 1.49.1 [#1491-1]

#### Patch Changes [#patch-changes-51]

* 3239667: Tracker trigger outcomes always carry a skip code. A skip recorded before Relay stored codes, whose reason matches no rule, now reads with the code `legacy`; reading such outcomes no longer fails. `TrackerSkipCode` is already an open string, so no client change is needed: show `legacy` by its `reason`, and retry it if it should count. The CLI's authoring reference names the code.
* @domino-sdk/relay\@1.49.1

### 1.49.0 [#1490-1]

#### Minor Changes [#minor-changes-122]

* c262df2: `management.attempts.list` takes `quest` to list one quest's attempts, by its ID, alone or with `q` and `status` (`managedAttemptQuerySchema.quest`). `domino attempts list --quest <id>` does the same from the CLI.
* 41a3fe9: Hosted apps go live. Approving a promotion that carries a hosted app build deploys that build as the project's public live app at `https://<project-slug>.domino.page`, with live's own management key (`DOMINO_ENVIRONMENT` is `live`), app secrets, KV and SQL stores, and schedule signing key. The previous live build serves until the new one switches in; a failed deployment keeps it, and promoting the build again retries. Before the first such promotion the address answers 404 "Nothing is live here yet".

  * `domino secrets`, `domino storage` and `domino schedules` take `--environment live` to manage the live app's secrets, stores and jobs. Without the flag they use test, as before; they ignore `RELAY_ENVIRONMENT` and the environment in `relay.json`.
  * A CLI sign-in session can set and delete app secrets in live. Other live writes still need Console or an API key.
  * `management.liveApp.get()` returns the live app's address and state (`none`, `deploying`, `live` or `failed`, with the failure). Console shows it as the **Live app** card in **Deployments**.
  * The `hosted-live-app` capability is now supported.

#### Patch Changes [#patch-changes-52]

* Updated dependencies \[c262df2]
* Updated dependencies \[41a3fe9]
  * @domino-sdk/relay\@1.49.0

### 1.48.0 [#1480-1]

*Not published to npm. Its changes first reached npm in 1.49.0.*

#### Minor Changes [#minor-changes-123]

* 3213a87: Add group membership quest rewards. `groupMembership(group)` from `@domino-sdk/relay/authoring` takes a `defineGroup` handle and adds the member to that group when the completion's rewards are delivered, recording the completion as the reason in the audit history and checking prerequisite-only automatic quests. Publishing refuses a reward for a group that does not exist or is archived. Revoking the completion removes the membership unless an operator's add or another unrevoked completion also backs it; `CompletionRevocation.groups` reports each group with `stillMember`. Archiving a group that a quest rewards fails with `group-in-use`, naming the quests. SDK 1.x keeps the existing `kind: "tier"` reward envelope with an optional `reward.group` key (`groupReward` and `groupRewardSchema` in `@domino-sdk/relay/schema`), and quest views add an optional `reward.group` with the group's key and title.

#### Patch Changes [#patch-changes-53]

* Updated dependencies \[3213a87]
  * @domino-sdk/relay\@1.48.0

### 1.47.0 [#1470-1]

*Not published to npm. Its changes first reached npm in 1.49.0.*

#### Minor Changes [#minor-changes-124]

* 7db52a2: Reward X posts only for approved creators. X does not allow apps that reward users for posting, so a deployment that publishes a quest rewarding X posts without an `inGroup` prerequisite now returns the warning `x-post-rewards-ungated` in `warnings`, naming the quests by key. It applies to an X tracker trigger that pays the post's author and to a content quest on an X tracker, when the quest declares rewards or an engagement rule credits it; `beneficiary: "inReplyTo"` quests are not affected. The deployment still publishes, `domino deploy` prints the warning, and Console shows it on the deployment with a link to **Groups**. Add `requires: [inGroup(group)]` with a group of approved creators you manage in Console **Groups**, with `domino groups`, or with `management.groups`.

  The posting-streak and original-post SDK examples, the tracker guides and the CLI skill references now gate X post rewards on an `approved-creators` group.

#### Patch Changes [#patch-changes-54]

* Updated dependencies \[7db52a2]
  * @domino-sdk/relay\@1.47.0

### 1.46.0 [#1460-1]

#### Minor Changes [#minor-changes-125]

* fc0fc19: One deployment holds up to 1,000 quests and 200 quest types (was 50 each). `projectDeploymentSchema.releases`, `batchReleaseSchema.releases`, `questDraftChangesSchema` quests and collections, and `catalogDeploymentSchema` collections accept up to 1,000; the limits are exported as `questScaleLimits`. `domino build`, `deploy` and `dev` accept up to 1,000 quests and 200 quest types in one manifest. Other resource kinds keep their limits.

  SDK clients older than this release check drafts against 50 quests and 50 collections, so they cannot read a draft that holds more. Update the SDK before building drafts larger than that.

#### Patch Changes [#patch-changes-55]

* Updated dependencies \[fc0fc19]
  * @domino-sdk/relay\@1.46.0

### 1.45.0 [#1450-1]

#### Minor Changes [#minor-changes-126]

* 8bb0a7b: Quest and quest type bundles import the SDK instead of carrying it. The SDK package ships its runtime (`dist/runtime`), and `buildQuest(entry, { runtime: true })` from `@domino-sdk/relay/build` leaves `@domino-sdk/relay/authoring`, `@domino-sdk/relay/schema` and, at the matching version, `zod` out of the bundle, returning the `runtime` the code imports. `domino build`, `deploy`, `dev` and hosted builds use it: releases and types record a `runtime` reference and the deployment carries each runtime once in `runtimes`. A quest bundle shrinks from about 170 KB to a few kilobytes. Apps on an SDK without a runtime keep building self-contained bundles. A Relay that predates runtimes refuses such a deployment, and the CLI says to update it. See [Shared SDK runtime](https://github.com/GetDomino/relay/blob/main/docs/public/concepts/quests.md#shared-sdk-runtime).

#### Patch Changes [#patch-changes-56]

* Updated dependencies \[8bb0a7b]
  * @domino-sdk/relay\@1.45.0

### 1.44.0 [#1440-1]

#### Patch Changes [#patch-changes-57]

* f114841: `management.audit.list` accepts `category: "test-members"`, which lists operators acting as test members in test: creating, submitting a quest as, advancing the clock of, giving and removing wallets for, sending synthetic records as, signing a staging app in and out as, and resetting a test member. Listing every category now includes these records too. The new `auditListQuerySchema` export validates this query and `AuditQuery` is its input type; `auditQuerySchema` keeps the three earlier categories and is deprecated in its favor. Console's **Audit history** shows these records under a new **Test members** filter.
* f114841: `management.testMembers.reset` and `domino test-members reset` now unlink the accounts a test member connected in your app while a staging site acted as it: X, Discord, Telegram, Google and Apple accounts and wallets. Before, such an account made reset refuse with `test-member-reset-blocked`. Only the member's link is removed; the account itself is untouched and anyone can link it afterwards. Reset still refuses a member with an account that signs in, an app user ID, or an X account a member import attached. See [Reset and create another member](https://console.domino.run/docs/use/quests#7-reset-and-create-another-member).
* Updated dependencies \[f114841]
* Updated dependencies \[f114841]
  * @domino-sdk/relay\@1.44.0

### 1.43.0 [#1430-1]

#### Minor Changes [#minor-changes-127]

* 64e8df3: Test members can post on X, send Discord messages and add Discord reactions, so quests on managed X and Discord trackers can be tried in test without real accounts.

  * New `management.testMembers.record(member, { actionId, tracker, data?, as? })` writes one synthetic record into a managed X or Discord tracker. `data` uses the stored record's field paths (`content` and `likeCount` for X, `channelId` and `text` for messages, `emoji` for reactions), so it exercises quests' `where` filters; Relay sets IDs and real time. `as: "messageAuthor"` makes the member the reacted-to message's author. The answer is `{ tracker, record, account, stored, admissions }`. From the CLI, `domino test-members record MEMBER --tracker TRACKER [--data JSON] [--as author|messageAuthor]`.
  * The first record for X or Discord links the test member a synthetic account of that kind, which attribution, member `identities` and `x.connection()` read like a real link. Reset removes it with the member's synthetic records.
  * Tracker record reads mark these records `synthetic: true`. Collected and ingested records never carry it.
  * Console's **Try** tab sends posts, messages and reactions for X and Discord quests, starting from values that match the quest's filter, and the tracker's record list marks synthetic records.

  It needs `publish` permission in the test environment, like the other test-member methods; live refuses it.

#### Patch Changes [#patch-changes-58]

* Updated dependencies \[64e8df3]
  * @domino-sdk/relay\@1.43.0

### 1.42.0 [#1420-1]

#### Minor Changes [#minor-changes-128]

* b7156c7: Releases and quest types can reference a shared SDK runtime instead of bundling the SDK. `releaseSchema`, `publishedReleaseSchema`, quest type versions and catalog deployment types take an optional `runtime` reference (`{ digest, sdkVersion }`), and project deployments take `runtimes`, each runtime's modules sent once. The new `@domino-sdk/relay/schema` exports are `sdkRuntimeSchema`, `sdkRuntimeReferenceSchema`, `sdkRuntimeModulesSchema`, `sdkRuntimeModulePathSchema`, `sdkRuntimeLimits` and `sdkRuntimeDigest`. Existing self-contained bundles work unchanged.

#### Patch Changes [#patch-changes-59]

* Updated dependencies \[b7156c7]
  * @domino-sdk/relay\@1.42.0

### 1.41.0 [#1410-1]

*Not published to npm. Its changes first reached npm in 1.42.0.*

#### Patch Changes [#patch-changes-60]

* Updated dependencies \[c8e46ee]
  * @domino-sdk/relay\@1.41.0

### 1.40.0 [#1400-1]

*Not published to npm. Its changes first reached npm in 1.42.0.*

#### Minor Changes [#minor-changes-129]

* 68bf9eb: Promote test to live as one reviewed request ([ADR 0017](https://github.com/GetDomino/relay/blob/main/docs/adr/0017-live-changes-only-through-promotion.md)). A promotion makes live match test for every definition, code-defined and Console-authored, matched by authoring key, and carries the hosted app's active staged build. It lists environment bindings live lacks (integrations, webhooks, participant sign-in, app secrets) as warnings and never copies them.

  * `management.promotions.prepare()` (test), `history()`, `get(id)`, `approve(id)` and `reject(id, { reason })` (live, Console session), and `rollback(id)` (live): `POST /promotions`, `GET /promotions/history`, `GET /promotions/:promotion`, and `POST /promotions/:promotion/approve`, `/reject` and `/rollback`.
  * `domino promote --json` prepares one from test; `domino promotions list` and `domino promotions get` read them.
  * New error codes: `promotion-empty` (live already matches), `promotion-changed` (test or live changed since the promotion was prepared; approval changed nothing) and `promotion-decided`.

  `management.deployments.promote(id)` and `domino deploy --promote` keep their 1.x request and response shapes. Once the test deployment is published they now prepare a promotion of all of test, answered as a pending deployment record with no resources whose first warning, code `environment-promotion`, names the promotion. Approve it as a promotion: `deployments.get` and `deployments.publish` do not accept its ID.

  Live now refuses direct code deployments. `domino deploy --environment live`, with or without `--preview`, and `management.deployments.preview` with a client for live fail with 422 `promotion-required` before the bundle is checked, whatever the credential; the CLI reports it as `ENVIRONMENT_UNSUPPORTED` (exit 1) and no longer submits anything. Automation that deployed to live should deploy to test, then run `domino promote` (or `management.promotions.prepare()`) and have a person approve the promotion in Console. Live deployments submitted before this change can still be submitted, approved, rejected and rolled back. Request and response shapes are unchanged, so like the other live refusals this ships in a minor release.

#### Patch Changes [#patch-changes-61]

* Updated dependencies \[68bf9eb]
  * @domino-sdk/relay\@1.40.0

### 1.39.1 [#1391-1]

#### Patch Changes [#patch-changes-62]

* 08f24a3: An operator who signed in to a project's staging app through Console and holds `publish` can now sign that app in as a test member, through the staging host's `/__domino/toolbar/*` endpoints. The test member gets an ordinary 8-hour participant session. Issuing and ending it is audited as the operator, and that audit survives reset. Reset now ends a test member's participant sessions instead of refusing. Live, members that aren't test members, password staging sessions, API keys, hosted keys and CLI sessions can't start one. A password staging session gets the new refusal code `operator-required`, which the CLI skill's error reference now lists.
* Updated dependencies \[08f24a3]
  * @domino-sdk/relay\@1.39.1

### 1.39.0 [#1390-1]

#### Patch Changes [#patch-changes-63]

* Updated dependencies \[a9b11a8]
  * @domino-sdk/relay\@1.39.0

### 1.38.1 [#1381-1]

#### Patch Changes [#patch-changes-64]

* 9b02ade: `questConfiguration` from `@domino-sdk/relay/server` accepts any release-shaped value with `configuration`, `fields` and `values`, so callers no longer need the release's code to read its configuration.
* Updated dependencies \[9b02ade]
  * @domino-sdk/relay\@1.38.1

### 1.38.0 [#1380-1]

#### Minor Changes [#minor-changes-130]

* 167a840: A quest can make a member wait before trying again after a rejection. Set `retryCooldownMs` (1,000 to 604,800,000) on a quest members submit, or **Retry cooldown** on a quest made in Console, where a new quiz starts at five minutes. After the evaluator or a reviewer rejects the member's latest attempt, a new attempt is refused with 409 `retry-cooldown` until `retryAt` (also `RelayError.retryAt`). Quest progress and `QuestView` carry `retryAt` while the cooldown runs; `questStatus` reports `retry-cooldown`, `canSubmit` is false until then, `isCoolingDown(view)` is new, and `useQuest` renders again when it ends. Catalog quest instances take an optional `retryCooldownMs` (`null` for none). A quest without the field keeps today's behavior: no cooldown.

#### Patch Changes [#patch-changes-65]

* Updated dependencies \[167a840]
  * @domino-sdk/relay\@1.38.0

### 1.37.1 [#1371-1]

#### Patch Changes [#patch-changes-66]

* 2f18822: Hosted apps moved to `domino.page`. New staged builds serve at `https://<project-slug>--staging.domino.page`, and `domino dev` prints `https://<project-slug>--<workspace-hash>.domino.page`. `domino capabilities` describes the new staging address. The CLI shows whatever host the API returns, so no upgrade is needed. Staging links on `staging-<project-slug>.domino.run` from earlier builds keep working; stage again to get the new address.
* @domino-sdk/relay\@1.37.1

### 1.37.0 [#1370-1]

*Not published to npm. Its changes first reached npm in 1.37.1.*

#### Minor Changes [#minor-changes-131]

* 701a0d8: Leaderboards can list every member. `defineLeaderboard({ includeUnscored: true })` adds the board's eligible members who have no qualifying points to its standings, `me`, `nearby` and `placement` reads, with a score of 0. They share one rank after every member who scored and are ordered by member ID; the board's group and exclusion group still apply. Settlements leave them out, so they win no prizes. Boards without the option rank as before.
* e1b3c70: A points reward can pay up to 2,147,483,647 points, the largest 32-bit signed integer, instead of 10,000. The new limit applies wherever `points()` and `pointsRewardSchema` are used: quest rewards, evaluator decisions, leaderboard prizes and referral rewards. Offer prices (`offerPriceSchema`) take the same maximum and still start at 1. Code that declares a larger amount needs this SDK version, and apps that read one need SDK 1.10.0 or later, whose response readers ignore these maximums.
* ff024c7: Scheduled jobs can report what a run did. A job route answers with `scheduleResult({ outcome, summary?, metrics?, diagnostics? })` from `@domino-sdk/relay/server`, and Relay records it with the run as `result`. An `outcome` of `partial` or `failed` records the run as failed even when the HTTP status is 2xx. Relay reads any `application/json` answer up to 16,384 bytes. An invalid or larger one leaves the status alone and sets the run's `error` to `Result ignored: …`. Each schedule keeps its latest 20 runs: `management.schedules.runs(name)` (`GET /schedules/:name/runs`, `domino schedules runs NAME`, Console **Run history**) returns them newest first, and needs `read` permission.
* ff024c7: Read a hosted app's SQL value in full where a rows page cuts it at 2,000 characters. `management.storage.cell(name, table, { row, column, cursor? })` (`GET /storage/sql/:name/cell?table=&row=&column=`, `domino storage cell DB TABLE ROW COLUMN [--all]`) returns up to 100,000 characters per page with the value's `type` and `length`, and a `nextCursor` for the rest; `--all` follows it. `row` is the row's position in storage order, the rows page's `offset` plus its index on the page. A BLOB answers with its size and no text. It needs `publish` permission, like the other storage reads. Console's row view has **Show full value**.

#### Patch Changes [#patch-changes-67]

* Updated dependencies \[701a0d8]
* Updated dependencies \[e1b3c70]
* Updated dependencies \[ff024c7]
* Updated dependencies \[ff024c7]
  * @domino-sdk/relay\@1.37.0

### 1.36.0 [#1360-1]

#### Patch Changes [#patch-changes-68]

* 5da9393: Participants read their own email address. `relay.me.get()` (`GET /v1/me`) and `useMe()` now return `emails`, the verified addresses that sign the member in (each email-code address, then the verified email of each Google or Apple account enabled for sign-in), and `email`, the first of them for "signed in as". A member who signs in only with a wallet, X, Discord, Telegram or an app login has none: `email` is null and `emails` is empty. Participant routes never return another member's email. The classic starter shows the address in its account menu and Settings page.
* Updated dependencies \[5da9393]
  * @domino-sdk/relay\@1.36.0

### 1.35.0 [#1350-1]

*Not published to npm. Its changes first reached npm in 1.36.0.*

#### Minor Changes [#minor-changes-132]

* f7ba8d6: Create and change X search trackers without a deployment. `management.trackers.create({ key, title, query, window, refreshSeconds?, maxPages? })` (`POST /trackers`, `domino trackers create`) creates one as data in test, with the same validation as `defineTracker`; its first collection searches from `window.start`. `management.trackers.update(tracker, changes)` (`POST /trackers/:tracker/update`, `domino trackers update`) changes any field but the key, and a new query or window never searches the past. Both need `publish` permission, and live refuses them with `promotion-required`: a data tracker reaches live through a promotion from test, which is not available yet. Console **Trackers** creates and edits them too.

  A key belongs to code or to data. Tracker status reports it as `origin` (`code` or `data`). A deployment that declares a data tracker's key is refused with 409 `tracker-owned-by-data`, and creating or updating a key a deployment publishes with 409 `tracker-owned-by-code`.

#### Patch Changes [#patch-changes-69]

* Updated dependencies \[f7ba8d6]
  * @domino-sdk/relay\@1.35.0

### 1.34.0 [#1340-1]

#### Minor Changes [#minor-changes-133]

* 81d0037: Definitions change in test and reach live only through a promotion ([ADR 0017](https://github.com/GetDomino/relay/blob/main/docs/adr/0017-live-changes-only-through-promotion.md)). In live, every definition write fails with 422 `promotion-required`, whatever the credential, Console sessions included. These 1.x management routes refuse live:

  * `POST /releases`, `/releases/batch`, `/releases/settings`, `/releases/lifecycle`, and `/quests/:quest/archive` and `/unarchive`
  * `POST /catalog/deploy`, `/catalog/quests`, `/catalog/drafts` and `/catalog/publish`
  * `POST /engagement/publish` and `/engagement/change`
  * `POST /point-ledgers`, `/point-ledgers/update`, `/point-ledgers/:ledger/update`, `/archive` and `/unarchive`
  * `POST /calendar` and `/profile-settings`
  * `POST /badges`, `/badges/:badge/update`, `/archive` and `/unarchive`
  * `POST /pickup-inventory` and `/leaderboards/:board/exclusion-group`
  * `POST /stock-items`, `/stock-items/:item/rename`, `/archive` and `/unarchive`
  * `POST /resources/lifecycle`, and `/archive` and `/unarchive` under `/trackers`, `/referral-programs`, `/leaderboards`, `/engagement/rules`, `/collections`, `/quest-types` and `/images`

  API keys used to get 403 `console-approval-required` on most of these. Pickup policy, exclusion-group bindings and stock item creation, renames and archive state accepted API keys in live. Operations keep working in live under the caller's permissions: pauses, points, groups, moderation, reviews, entitlements, redemptions and stock adjustments. Code deployments still reach live through deployment review in Console.

  This ships as a minor release as an accepted exception to the compatibility policy: no request or response shape changes, the refusal is one new error code, and no project had written a definition to live in production. The CLI reports the refusal as `ENVIRONMENT_UNSUPPORTED` with `apiCode: "promotion-required"`.

#### Patch Changes [#patch-changes-70]

* Updated dependencies \[81d0037]
  * @domino-sdk/relay\@1.34.0

### 1.33.0 [#1330-1]

#### Minor Changes [#minor-changes-134]

* 3706733: Add member imports: bring members, opening point balances and quest progress from another platform into a project and environment. `domino import --file members.jsonl --source NAME --dry-run` uploads a JSONL file, checks every row and prints what applying would create, link, set and refuse; without `--dry-run` it applies the import in test. `domino imports list|get|rows|apply|cancel` and `management.memberImports.create|addRows|check|apply|cancel|get|rows|list` reach the same `/management/v1/member-imports` routes. A live import is applied only from Console. Imported opening balances are `adjust` entries that name their import (`PointsEntry.import`, and `imported` in a member's own point history); they emit no `points.awarded` and count on balance leaderboards only. Imported progress pays no rewards. Accounts an import vouches for carry `imported: true` on `ManagedWallet`, `ManagedOAuthAccount` and `ManagedMemberIdentity`.

#### Patch Changes [#patch-changes-71]

* Updated dependencies \[3706733]
  * @domino-sdk/relay\@1.33.0

### 1.32.0 [#1320-1]

#### Minor Changes [#minor-changes-135]

* d6ea67b: Service keys give a self-hosted app's backend a management credential that no person owns. An organization admin creates one in Console under **Settings → API keys** for one project and environment, with chosen permissions. It never expires unless an expiry is chosen, and it keeps every live fence a personal key has. `management.serviceKeys` lists, creates and revokes them from an admin's Console session. Rotation overlaps: `addSecret` issues a second secret while the first keeps working, and `revokeSecret` retires the old one. Each secret reports `lastUsedAt`.

  Identity integration keys follow the same policy. New keys never expire unless `expiresInDays` is given, and existing keys keep their expiry until an admin removes it with `identityIntegrations.removeSecretExpiry`. `identityIntegrations.addSecret` and `revokeSecret` rotate a key without signing members out. Integrations now list their `secrets`. `expiresAt` is deprecated: it reports the latest secret expiry, or 8640000000000000 when a secret never expires. `identityIntegrations.rotate` is deprecated too. It now replaces every secret with one that never expires, instead of extending the key by 90 days.

#### Patch Changes [#patch-changes-72]

* Updated dependencies \[d6ea67b]
  * @domino-sdk/relay\@1.32.0

### 1.31.0 [#1310-1]

#### Minor Changes [#minor-changes-136]

* 07802d2: Reward deliveries, redemptions and deployments page by `cursor` and `nextCursor`, so long histories no longer stop silently.

  * `management.rewardDeliveries({ member, cursor, limit })` and `relay.me.rewardDeliveries({ cursor, limit })` answer `nextCursor` beside `deliveries`. A page holds up to 500 deliveries, 500 by default, so calls without a cursor read what they did before. `management.rewardDeliveries(member)` still works.
  * New `management.redemptions.history({ member, cursor, limit })` answers `{ items, nextCursor }`, 25 a page by default and at most 100, and replaces `management.offers.redemptions`, which answers every redemption at once. From the CLI, `domino redemptions history [--member] [--cursor] [--limit]` replaces `domino redemptions list`.
  * New `management.deployments.history({ cursor, limit })` answers `{ items, published, nextCursor }`, 25 a page by default and at most 100. `published` holds the two newest published deployments by revision on every page: the live one and the one `rollback` returns to. It replaces `management.deployments.list`, which stops at the 100 newest.

  The deprecated forms keep working through 1.x and are removed in 2.0.0.

#### Patch Changes [#patch-changes-73]

* caf698a: To prepare project slugs for hosts on `domino.page`, the server now refuses a slug change to a name Domino reserves (409 `Project slug is reserved`) or to a slug shorter than 3 characters (400), and a new project's derived slug gets a short suffix instead of taking either. Reserved names match exactly: `console` is reserved, `console-tools` is not. `projectSchema` still parses every existing slug; its `slug` field documents the rule, and the agent references and `domino capabilities` describe it.
* Updated dependencies \[caf698a]
* Updated dependencies \[07802d2]
  * @domino-sdk/relay\@1.31.0

### 1.30.1 [#1301-1]

#### Patch Changes [#patch-changes-74]

* bb09650: The participant API reference's `useRelayMutation` and server-rendered `useRelayQuery` examples are now complete components that compile, instead of fragments.
* @domino-sdk/relay\@1.30.1

### 1.30.0 [#1300-1]

#### Minor Changes [#minor-changes-137]

* cd6d78c: Add `management.x.lookup({ username })` and `domino x lookup HANDLE`, which resolve an X handle (with or without `@`) to the account that holds it, `{ id, username, name }` (read through a cache up to a day old), from `GET /management/v1/x/users/lookup`. Trackers, `--author` filters, custom `x` identities and `ctx.x.follows({ userId })` take the stable `id`, which no longer has to be copied from a post in Preview X search. The lookup needs `read` permission, fails with 404 `not-found` when no account holds the handle, and allows 30 lookups a minute per user and 300 an hour per organization. It resolves the handle only: it does not show who owns the account.

#### Patch Changes [#patch-changes-75]

* Updated dependencies \[cd6d78c]
  * @domino-sdk/relay\@1.30.0

### 1.29.0 [#1290-1]

#### Minor Changes [#minor-changes-138]

* 272a255: Add `management.members.progress(member)` and `domino members progress MEMBER`, which read a member's progress on each active quest, 25 most recent attempts and point balances from `GET /management/v1/members/:member/progress`. They leave out the quest releases and evaluator code that `members.get` returns beside them. In a project with many quests, member detail runs to megabytes; every list in this read is bounded by the project's configuration, so it suits scheduled jobs and other reads under a deadline. `members.get` is unchanged.

#### Patch Changes [#patch-changes-76]

* Updated dependencies \[272a255]
  * @domino-sdk/relay\@1.29.0

### 1.28.0 [#1280-1]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Minor Changes [#minor-changes-139]

* 932e3c1: Add kind-keyed operator identity commands. `management.members.identityStates.list(member)` reads every identity a member proved (wallets, X, Discord and Telegram accounts, and `app:<name>` identities) with its link, revision, sign-in and allowed actions. `identityStates.unlink` and `relink` take an `identity` selector, an `expectedRevision` and a reason, and `identityStates.setSignIn` turns a wallet's sign-in on or off on its own. Each kind keeps its rules: wallet and account unlinks keep sign-in, app identity unlinks end that identity's sessions, and a Telegram account can now be relinked. The CLI adds `domino members identities`, `unlink-identity`, `relink-identity` and `identity-sign-in`.

  Add `management.discordRoles.preserve(delivery, { reason })`, the Discord role reward kind's preservation operation. A delivery of a reward kind without that operation is refused with 409.

  Deprecated, still supported through 1.x: `members.oauthAccounts`, `unlinkOAuthAccount`, `telegramAccount`, `unlinkTelegramAccount`, `unlinkWallet`, `setWalletSignIn`, `unlinkIdentity`, `relinkIdentity` and `identityLinks` (use `members.identityStates`), `management.preserveRewardRole` (use `management.discordRoles.preserve`), and the CLI's `members identity-links`, `members relink` and `members oauth-accounts`.

#### Patch Changes [#patch-changes-77]

* Updated dependencies \[932e3c1]
  * @domino-sdk/relay\@1.28.0

### 1.27.1 [#1271-1]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Patch Changes [#patch-changes-78]

* 4746d5a: Corrected the agent references: `domino capabilities` now describes X account linking (OAuth with PKCE, stable X user ID) and profile badge reads, and the participant API reference says apps using their own login can still connect X and Discord accounts and link wallets.
* @domino-sdk/relay\@1.27.1

### 1.27.0 [#1270-1]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Minor Changes [#minor-changes-140]

* 072493d: Leaderboard exclusions are a group (DOM-205):

  * A board leaves out the members of its exclusion group. Each board gets its own group, keyed `domino--exclusions--<board ID>`, on its first exclusion. Existing exclusions and their history move into these groups, so standings and settlements leave out the same members at every cutoff.
  * `management.leaderboards.exclusionGroup(board)` returns `{ leaderboard, group, generated, boundAt }`, and `management.leaderboards.bindExclusionGroup(board, { group, reason })` binds the board to another unarchived group, such as one several boards share, or back to its own. Settlements read the group bound at their cutoff.
  * `management.leaderboards.exclude` and `exclusions` keep their inputs, responses, permissions and audit events. They now change and read the board's exclusion group, which `management.groups` can change too.
  * Archiving a group that a leaderboard uses as its exclusion group fails with 409 `group-in-use`, naming the boards in `leaderboards`. Group keys starting with `domino--exclusions--` are reserved: creating one fails with 409 `already-exists`.
  * CLI: `domino leaderboards exclusion-group BOARD` and `domino leaderboards bind-exclusion-group BOARD GROUP --reason …`.

#### Patch Changes [#patch-changes-79]

* Updated dependencies \[072493d]
  * @domino-sdk/relay\@1.27.0

### 1.26.0 [#1260-1]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Patch Changes [#patch-changes-80]

* 91d7ad0: Relay keeps quest submissions (DOM-268). See [ADR 0014](https://github.com/GetDomino/relay/blob/main/docs/adr/0014-relay-keeps-quest-submissions.md).

  * Submission handles (`me.submission`) and quest controllers (`quests.open`) save each submission in Relay instead of the browser. A lost answer, a reload, another tab or another device signed in as the same member recovers it from Relay, and submitting again returns the same attempt. The public API is unchanged. Submissions no longer need IndexedDB or Web Locks, so `createRelayClient` without a `submissionStore` submits too. `SubmissionStore` and `browserSubmissionStore` are used only to finish a submission an older SDK version saved in the browser.
  * A member has at most one unfinished attempt per quest. Another claim, or a retry of a failed attempt, is refused with `attempt-pending` until the previous one is decided, including while it waits for human review. The same action sent twice still replays.
  * New participant routes: `GET`/`POST /v1/me/quests/:quest/submission`, `GET`/`POST /v1/me/submissions/:submission`, and `POST /v1/me/submissions/:submission/submit`, `/discard` and `/evidence` (a photo uploaded into the submission). A submit refused because the quest changed keeps the draft with `reviewRequired`, and the handle reports `needs-review`. `@domino-sdk/relay/schema` exports `questSubmissionSchema`, `questSubmissionUpdateSchema`, `questSubmissionResultSchema` and `QuestSubmissionStatus`. A draft expires 7 days after its last change. Refusal code `submission-closed` means the submission was already submitted, discarded or expired.
  * The management client has `quests.submissions(member)` and `quests.discardSubmission(member, submission)`. Console shows a member's open quest submissions and lets reviewers and staff discard one.

* Updated dependencies \[91d7ad0]
  * @domino-sdk/relay\@1.26.0

### 1.25.1 [#1251-1]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Patch Changes [#patch-changes-81]

* 07b7bc1: `management.attempts.confirm` (`POST /management/v1/attempts`) and `management.passes.verify` (`POST /management/v1/passes/verify`) now need a member that already exists in the project's environment. An unknown member ID is refused with 404 `not-found` and creates nothing; before, Relay created a member with no sign-in identity, who then collected the quest's rewards. Members who exist but have not joined the project still join it, and test members keep working. The CLI's skill references describe the rule.
* @domino-sdk/relay\@1.25.1

### 1.25.0 [#1250-1]

*Not published to npm. Its changes first reached npm in 1.29.0.*

#### Patch Changes [#patch-changes-82]

* 55d08ae: Client SDK paper cuts (DOM-160):

  * `createRelayClient` takes `getToken: async () => token | null`, read before every attempt, as the management client does. A failing `getToken` rejects with code `credential` before anything is sent. `token` and `getToken` together are refused.
  * `createRelayClient` and `createManagementClient` take `fetch`, which replaces the global `fetch` for every call, and `headers`, added to every call. The client's own headers win over one of the same name; an invalid header is refused with `invalid-input` when the client is created.
  * Submission handles (`me.submission`) and quest controllers (`quests.open`) take `{ signal?, timeout? }` on `submit`, `resume`, `status` and `discard`. Each request the call sends (session read, upload, claim, retry, and a controller's read of the latest quest after `quest-updated`) gets the signal and what is left of one deadline for the whole call. A cancelled or timed-out call keeps its saved progress for `resume()`. The quiz's `submit(answers, { release?, signal?, timeout? })` makes `release` optional.
  * `auth.start({ provider, mode })` and `auth.emailVerify({ challenge, code })` take an options object, which refuses unknown fields. The positional forms still work and are deprecated, to be removed in 2.0.0.
  * `paginate(read)` from `@domino-sdk/relay` iterates every item of a paged read with `for await`, following `nextCursor`.
  * `RelayErrorCode` lists `OperationRetired` (`operation-retired`) and `Credential` (`credential`).
  * `offers.redeem`, `profiles.badges`, `me.activity.list`, `me.upload`, `auth.telegram.miniApp.connect` and `auth.emailStart` reject invalid input with a `RelayError` of code `invalid-input`, instead of throwing a raw `ZodError` (`offers.redeem`) or throwing synchronously.
  * The browser bundle no longer includes every Zod locale: `createBrowserRelayClient` drops from about 412 KB to 198 KB minified (90 KB to 56 KB gzipped).

* Updated dependencies \[55d08ae]
  * @domino-sdk/relay\@1.25.0

### 1.24.1 [#1241-1]

#### Patch Changes [#patch-changes-83]

* 2fd54a5: `domino reference` now serves every reference the skill ships, including `existing-app`, `hosted`, `webhooks` and `errors`, so each capability's `reference` resolves. The references now say the CLI sends a request at most three times in all, and that the 50 app-secret limit applies to each project environment. Capability entries explain why referral milestones and external qualification are not compositions.
* @domino-sdk/relay\@1.24.1

### 1.24.0 [#1240-1]

#### Minor Changes [#minor-changes-141]

* f6e030a: Webhook endpoint creation and secret rotation can be recovered after a lost answer. `management.webhooks.create` takes an optional `actionId` (new `webhookCreateInputSchema`), and `management.webhooks.rotateSecret(id, { actionId })` takes one too (new `webhookRotateInputSchema`). The client generates an ID when you omit it. A retry with the same `actionId` returns the same endpoint and signing secret instead of creating a second endpoint, or being refused during the 24-hour overlap. Once the secret is rotated again, the earlier action's retry is refused with `secret-superseded`. `domino webhooks create` and `domino webhooks rotate-secret` are now saved operations with `--operation-id`; resume one with `domino operations resume <id>`.

  Relay now refuses webhook subscriptions to event types it never sends, such as `completion.create`, with `unknown-activity-type`; the message lists the valid types. This narrows what the API accepts, but the CLI and SDK already report it as a refusal. An endpoint update can keep a type the endpoint subscribed to before. Event payload `type` stays an open string, so receivers must still accept or ignore types they do not know.

#### Patch Changes [#patch-changes-84]

* Updated dependencies \[f6e030a]
  * @domino-sdk/relay\@1.24.0

### 1.23.2 [#1232-1]

#### Patch Changes [#patch-changes-85]

* f049af8: Agent references and capabilities now say that Discord and Telegram trackers keep activity only for accounts a member of the tracker's own project linked, now that members belong to one project.
* @domino-sdk/relay\@1.23.2

### 1.23.1 [#1231-1]

#### Patch Changes [#patch-changes-86]

* fd5f211: Members now belong to one project. A person who signs in to two projects of the same organization is a separate member in each, with their own identities, linked X, Discord and Telegram accounts, and wallets; unlinking, relinking and wallet sign-in control act within the project. Project transfer now moves a project with its participants instead of refusing it. No API shape changes: `MemberIdentityLink.scope` keeps the value `"organization-environment"` for OAuth links, which now names a link in the member's project and environment.
* Updated dependencies \[fd5f211]
  * @domino-sdk/relay\@1.23.1

### 1.23.0 [#1230-1]

#### Minor Changes [#minor-changes-142]

* 88c7c00: Add Telegram chats, membership checks and reaction trackers. Operators connect Telegram groups and channels to a project through Domino's bot in Console (**Integrations → Telegram → Add chat**), or with `management.telegram.connect()`, `chats()` and `remove(chat)`. See [Telegram](https://console.domino.run/docs/build/telegram).

  * `ctx.telegram.member({ chat })` reads whether the member's linked Telegram account is in a connected chat: `{ userId, username, status, isMember, isAdmin, untilDate }`, or null. It needs no `integrations` entry. `ctx.telegram` is optional in `EvaluationContext`, so code that builds contexts keeps compiling. `settings.telegramChat()` adds a setting that Console fills from the connected chats. The testing kit takes `facts.telegram.member`.
  * `telegram.reactions()` is a tracker source of the reactions members add in connected groups. Each record carries Telegram accounts for the reactor (`author`) and the message's author (`messageAuthor`). `data` is `TelegramReactionData`, which includes `reactorStatus` and `reactorIsAdmin` for a trigger's `where`. In SDK 1.x it is a custom source on the reserved namespace `telegram-reactions`, which Relay alone writes.
  * Text settings can carry `picker`, which Console uses to offer a list.
  * The `telegram-membership` snippet is a join quest.

#### Patch Changes [#patch-changes-87]

* Updated dependencies \[88c7c00]
  * @domino-sdk/relay\@1.23.0

### 1.22.0 [#1220-1]

#### Minor Changes [#minor-changes-143]

* 81beec3: Personal API keys and `domino login` CLI sessions can now use test members, including giving them wallet addresses, not only Console sessions: the caller needs `publish` permission in the test environment, and hosted app keys stay refused. Add `domino test-members create|list|history|act|advance|link-wallet|unlink-wallet|reset`, which always address the test environment and save their writes as resumable operations, so a coding agent can simulate a campaign end to end.

#### Patch Changes [#patch-changes-88]

* 07f3a99: Add the `campaign-testing` agent reference (`domino reference campaign-testing`): how to simulate a campaign over several days with test members, read the results, fix and replay, and what to watch for, such as odd characters, thresholds, missing provider fields, day boundaries, referral chains and balance gates.
* Updated dependencies \[81beec3]
  * @domino-sdk/relay\@1.22.0

### 1.21.0 [#1210-1]

#### Minor Changes [#minor-changes-144]

* 5db81c1: Test members can hold wallet addresses. `management.testMembers.linkWallet(member, { actionId, chain, address, network? })` gives a test member a Solana, EVM or Cosmos address without a signature, and `unlinkWallet(member, { actionId, chain, address })` removes and frees one. Quest code reads these addresses through `ctx.wallet` like a member's linked wallets, so balance-gated quests can be tried in the test environment. When the wallet's owner signs in with it or connects it, the address moves to them. Test member results include an optional `wallets` list. The CLI's capability map and Domino skill reference describe the new methods.

#### Patch Changes [#patch-changes-89]

* Updated dependencies \[5db81c1]
  * @domino-sdk/relay\@1.21.0

### 1.20.0 [#1200-1]

#### Minor Changes [#minor-changes-145]

* 2e1eff4: Add Telegram sign-in. Telegram Login signs participants in on the web where Domino configures it, and Mini App sign-in signs them in inside a project's own Telegram Mini App once the project names its bot ID in Console (**Settings → Sign-in**). Both reach one Telegram account. See [Telegram](https://console.domino.run/docs/build/telegram).

  Telegram is not an `AuthProvider` in SDK 1.x, so code that switches over providers keeps working: `AuthProvider`, `relay.auth.accounts()`, `useAccounts()` and `useSignInOptions().options` never include it. Telegram has its own additions beside them:

  * `relay.auth.providers()` returns `telegram: { login, miniApp }`.
  * `relay.auth.telegram` has `start(mode)`, `account()`, `disconnect()`, and `miniApp.signIn(initData)` and `miniApp.connect(initData)`. `telegramMiniAppInitData()` reads the Mini App's launch data.
  * `useSignInOptions()` and `signInOptions()` return the Telegram Login option as `telegram`, beside `options`. `useSignIn()` adds `withTelegram()` and `withTelegramMiniApp(initData)`, and `useTelegramAccount()` connects and disconnects the member's Telegram account.
  * In the management client, `participantSignIn()` returns `telegram: { login, miniAppBotId }`, and `configureParticipantSignIn()` takes `telegram: { miniAppBotId }`, which keeps the current bot when left out. `members.telegramAccount(member)` and `members.unlinkTelegramAccount(member, { subject, revision, reason })` read and unlink a member's Telegram account.

#### Patch Changes [#patch-changes-90]

* Updated dependencies \[2e1eff4]
  * @domino-sdk/relay\@1.20.0

### 1.19.0 [#1190-1]

#### Minor Changes [#minor-changes-146]

* 37a57df: Deploying a quest that needs an unconnected integration, such as Discord, no longer fails. The deployment preview and result include `warnings` (`deploymentWarningSchema`) naming the quests with code `integration-required`, and `domino deploy` prints them. Their attempts still fail with `integration-required` until the integration is connected.

#### Patch Changes [#patch-changes-91]

* Updated dependencies \[37a57df]
  * @domino-sdk/relay\@1.19.0

### 1.18.1 [#1181-1]

#### Patch Changes [#patch-changes-92]

* 0a6ebf1: Allow management deployment responses to include additional fields in direct, promotion and rollback sources. This closes the recorded response compatibility gap without changing known fields or source kinds.
* Updated dependencies \[0a6ebf1]
  * @domino-sdk/relay\@1.18.1

### 1.18.0 [#1180-1]

#### Minor Changes [#minor-changes-147]

* 7000d41: Add project badge definitions, quest badge rewards, audited manual awards and revokes, participant and profile reads, and React hooks. Author badges with `defineBadge`, include them in `relay.json`, and attach them with `badge(...)`. SDK 1.x retains the existing reward envelope with optional badge metadata. The CLI adds `domino badges` catalog and member commands.

#### Patch Changes [#patch-changes-93]

* Updated dependencies \[7000d41]
  * @domino-sdk/relay\@1.18.0

### 1.17.0 [#1170-1]

*Not published to npm. Its changes first reached npm in 1.18.0.*

#### Minor Changes [#minor-changes-148]

* d8a5731: Add ordered quest presentation steps with HTTPS link actions, images and alt text, and account or wallet hints. Collection declarations now accept description, image and alt text. Participant reads and React hooks return these optional fields in the published collection order. Console can edit and reset quest presentation and edit collection details. Presentation changes do not change verification.

#### Patch Changes [#patch-changes-94]

* Updated dependencies \[d8a5731]
  * @domino-sdk/relay\@1.17.0

### 1.16.0 [#1160-1]

#### Minor Changes [#minor-changes-149]

* b9d767f: Run background jobs on active hosted Worker builds in test. Declare `app.schedules` in relay.json with an interval or UTC cron, verify signed route calls with `verifyScheduledRequest(request, env.DOMINO_SCHEDULE_KEY)`, and inspect or control jobs with `management.schedules` or `domino schedules`.

  Jobs have a five-minute minimum cadence, ten schedules per app and a 30-second timeout. Relay makes one attempt per tick; uncertain completion blocks future runs until an operator confirms recovery. See [Run scheduled jobs](https://console.domino.run/docs/build/schedules) for custom tracker ingestion and recovery.

#### Patch Changes [#patch-changes-95]

* Updated dependencies \[b9d767f]
  * @domino-sdk/relay\@1.16.0

### 1.15.1 [#1151-1]

*Not published to npm. Its changes first reached npm in 1.16.0.*

#### Patch Changes [#patch-changes-96]

* 54a725e: Document non-retryable Discord configuration failures, their `integration-required` attempt code, publication prerequisites, and repair-before-retry guidance in the installed agent references.
* @domino-sdk/relay\@1.15.1

### 1.15.0 [#1150-1]

#### Minor Changes [#minor-changes-150]

* e6849ac: Add typed `domino <noun> list|get|…` commands for every management primitive, built on the SDK management client: `points get|list|award|spend|adjust|set|reverse`, `point-ledgers list|create|update`, `trackers list|get|records|summary|content|ingest|outcomes|retry-outcome`, `leaderboards list|standings|history|exclusions|exclude|include`, `leaderboard-settlements approve`, `referral-programs list|qualify`, `referral-relationships list|history`, `referral-rewards resolve`, `offers list`, `redemptions list|cancel`, `profiles settings|configure|moderate`, `calendar get|configure`, `members get|lookup|oauth-accounts|counters|moderate-counter`, `groups get|member`, `quests list|get`, `releases list|get`, `attempts list|get`, `metrics get` and `pauses list`. Each prints a typed result in the CLI contract. Every change except `point-ledgers update`, which sets a state, is saved before it is sent and takes `--operation-id`, which becomes the request's `actionId`, so an uncertain answer resumes with `domino operations resume`. The existing archive, unarchive, pause, resume, refresh, settlement reject, quest enroll and withdraw, and identity relink commands are saved the same way; their `--action-id` still sends the change unsaved. The bare `status`, `quests` and `releases` page reads and `groups show` are deprecated in favor of these commands and are removed in 2.0.0; `contract.json` marks each deprecated command with a `deprecated` note. Each capability in `domino capabilities` now names its commands in `cli`. See [resource commands](https://console.domino.run/docs/reference/automation#resource-commands).

#### Patch Changes [#patch-changes-97]

* @domino-sdk/relay\@1.15.0

### 1.14.0 [#1140-1]

#### Minor Changes [#minor-changes-151]

* 0c394de: Replay captured test-environment attempts locally. `management.attempts.capture(attempt)` (`publish` permission) returns what one evaluation run of an attempt read: the release's settings and rewards, the attempt's input, frozen clock, seed and calendar, each captured capability answer, and what quest code decided. `replayAttempt(quest, capture)` from `@domino-sdk/relay/testing` evaluates current quest code against it and reports the decision beside the recorded one, whether it changed (kind, reason, rewards or completion key), and the first call the capture could not answer. `domino quests replay <attempt>` fetches, saves (`--out`) and replays a capture. Captures are served from the test environment only: live attempts are refused with 422 `test-environment-required`, and an attempt that has not been evaluated with 409 `attempt-not-evaluated`.

#### Patch Changes [#patch-changes-98]

* cb252b6: The CLI now sends its management requests through the management client's own transport, so `domino` and `createManagementClient` share one implementation of deadlines, retries and failure classification. CLI exit codes, error codes and results stay the same; some error messages change wording, and network errors no longer carry `details.cause`. A management client write whose connection was refused, never resolved, could not reach the network or host, timed out while connecting, or failed TLS now fails with `uncertain: false`, because nothing was sent, and a 204 answer reads as `null`.
* 3ccb72a: `Person` records (`members.people()`, and the `people` of attempt, entitlement and page reads) now carry `test` and `name` for Console test members in the test environment, the same fields member reads already had. Console names test members by that name and marks them as test members instead of showing their member ID.
* Updated dependencies \[0c394de]
* Updated dependencies \[580a641]
* Updated dependencies \[cb252b6]
* Updated dependencies \[3ccb72a]
  * @domino-sdk/relay\@1.14.0

### 1.13.0 [#1130-1]

*Not published to npm. Its changes first reached npm in 1.14.0.*

#### Patch Changes [#patch-changes-99]

* Updated dependencies \[c3601dd]
  * @domino-sdk/relay\@1.13.0

### 1.12.0 [#1120-1]

*Not published to npm. Its changes first reached npm in 1.14.0.*

#### Minor Changes [#minor-changes-152]

* 80f823a: Add `@domino-sdk/relay/testing`, a kit for unit-testing quests locally. `runQuest(quest, { input, tracker, settings, facts, now, seed })` evaluates a quest the way Relay's hosted runtime does: its published settings, rewards and review switch, a frozen clock, seeded randomness, fake answers for `ctx.member`, `ctx.wallet`, `ctx.x`, `ctx.discord`, `ctx.trackers`, `ctx.images` and `ctx.review` checked against Relay's capability contracts, and the reward bounds Relay enforces. `trackerObservation` and `trackerPage` build typed tracker input, `expectAccept`, `expectReject`, `expectReview`, `expectDefer` and `expectProgress` assert decisions, and `createTestContext` returns the host context `quest.evaluate` takes. `buildQuest` now refuses a quest bundle that imports the testing kit. Starters include quest unit tests in `tests/quests.test.ts`.

#### Patch Changes [#patch-changes-100]

* Updated dependencies \[80f823a]
  * @domino-sdk/relay\@1.12.0

### 1.11.2 [#1112-1]

*Not published to npm. Its changes first reached npm in 1.14.0.*

#### Patch Changes [#patch-changes-101]

* Updated dependencies \[7160db6]
  * @domino-sdk/relay\@1.11.2

### 1.11.1 [#1111-1]

#### Patch Changes [#patch-changes-102]

* Updated dependencies \[90e5141]
  * @domino-sdk/relay\@1.11.1

### 1.11.0 [#1110-1]

#### Minor Changes [#minor-changes-153]

* 76e7a45: Add operator quest withdrawal, inventory rename/archive/unarchive/stock correction, webhook deletion with retained history, settlement rejection, and kind-keyed wallet/OAuth relink. Existing 1.x methods and response shapes remain supported. Inventory archive refuses retained offer and pickup references with named blockers; relink applies the current quest's historical policy while preserving admitted attempts.

#### Patch Changes [#patch-changes-103]

* Updated dependencies \[76e7a45]
  * @domino-sdk/relay\@1.11.0

### 1.10.1 [#1101-1]

#### Patch Changes [#patch-changes-104]

* Updated dependencies \[fa55eff]
  * @domino-sdk/relay\@1.10.1

### 1.10.0 [#1100-1]

#### Patch Changes [#patch-changes-105]

* f2724f9: Read added response fields at every depth without relaxing request validation. Add explicit supported/unsupported response schemas and `client.response(...)` for extensible results. Unsupported entities retain their raw data; entity lists retain positions and report an unsupported count. Existing 1.x method return types stay compatible.

  SDK and CLI calls identify their package, version and runtime with `X-Domino-Client`. Tolerant SDK calls also identify their per-call response capability with `X-Domino-Response: tolerant-v1`. Older and legacy callers still require compatible server wire shapes. See the [participant response reference](https://console.domino.run/docs/reference/participant-api#read-evolving-responses) for the opt-in API.

* Updated dependencies \[f2724f9]
  * @domino-sdk/relay\@1.10.0

### 1.9.0 [#190-1]

#### Patch Changes [#patch-changes-106]

* Updated dependencies \[4ae54f9]
  * @domino-sdk/relay\@1.9.0

### 1.8.0 [#180-1]

*Not published to npm. Its changes first reached npm in 1.9.0.*

#### Minor Changes [#minor-changes-154]

* f19da0d: Add canonical archive, unarchive, pause and resume methods and CLI commands for management resources. Keep older lifecycle and feature-specific publication forms working as deprecated 1.x APIs.

#### Patch Changes [#patch-changes-107]

* Updated dependencies \[f19da0d]
  * @domino-sdk/relay\@1.8.0

### 1.7.0 [#170-1]

#### Patch Changes [#patch-changes-108]

* Updated dependencies \[5662b50]
  * @domino-sdk/relay\@1.7.0

### 1.6.0 [#160-1]

#### Minor Changes [#minor-changes-155]

* 4651887: Add an optional `window` detail to `outside-window` availability, errors, and automatic outcomes so clients can distinguish tracker and quest windows without branching on reason text.

* 547600e: Every refusal from the participant and management APIs now carries a machine-readable `code` beside its unchanged message, so clients can branch without matching text. `RelayError.code` and `ManagementError.code` (and the CLI's `apiCode`) are set for every API answer: a specific code when the client can act on it, such as `catalog-changed`, `draft-changed`, `prerequisites-incomplete`, `permission-required` or `not-configured`, or the status's own code otherwise (`not-found`, `conflict`, `forbidden`, …). Structured detail, such as the `missing` prerequisites, comes with the code on every route. The [API error codes](https://console.domino.run/docs/reference/errors) reference lists them.

  * `RelayErrorCode` adds `PrerequisitesIncomplete`, `QuestClosed`, `QuestPaused`, `ProfileLocked`, `InvitationNotFound`, `InvalidCode`, `CodeExpired`, `ChallengeExpired` and `RateLimited`.
  * New `managementErrorSchema` (`@domino-sdk/relay/server`) parses the management API's error body, for raw `api.raw()` answers.

  Messages keep their text, and `code` stays an open string, so code written against 1.2 keeps working.

  The participant proxy now codes its own routing, configuration and authentication refusals. During application identity exchange it also keeps Relay's safe upstream code and `Retry-After` while continuing to replace non-account-conflict messages with its existing generic text.

* 6c80795: Groups are their own resource. A group is a named set of members that `inGroup` prerequisites, a leaderboard's `group` and a referral program's `inviterGroup` read by key; before, groups existed only as leaderboard group memberships. Existing memberships and their history move over unchanged.

  * `management.groups` lists, gets, creates, renames, archives and unarchives groups, and lists, adds and removes members with a reason for the audit history. Lists take `{ cursor, limit }` and return `{ items, nextCursor }`. The schemas and `ManagedGroup`, `GroupPage`, `GroupMember` and `GroupMemberPage` types are exported from `@domino-sdk/relay/schema` and `@domino-sdk/relay/server`.
  * `domino groups list|show|create|update|archive|unarchive|members|add|remove` does the same from the CLI.
  * Groups are archived, never deleted. An archived group's members still count, but cannot change until it is unarchived.
  * `management.leaderboards.group(group, { member, included, reason })` and `leaderboardGroupSchema` are deprecated and removed in 2.0.0. They keep working, and still create a group the first time they add a member to it. The group name must now be a group key (letters, digits, `_` and `-`, at most 100 characters), as definitions already required; a longer name answers 400. Replace them with `management.groups.create({ key, title })` once, then `management.groups.addMember(group, { member, reason })` and `management.groups.removeMember(group, member, { reason })`.

  `defineGroup` and `inGroup` are unchanged. See [groups](https://console.domino.run/docs/concepts/groups).

* 6088922: Every paged management read now takes `cursor` and answers `nextCursor`, as participant reads already did: pass a page's `nextCursor` back as `cursor` until it is `null`.

  * `management.points.history(member, ledger, { cursor })` and `management.referrals.participants({ cursor })` take an options object; their answers add `nextCursor`. Passing the numeric `before` (a page's `next`) still works and is deprecated.
  * `management.webhooks.deliveries(id, { cursor, limit })` and `domino webhooks deliveries <endpoint> --cursor <cursor>` page by the answer's `nextCursor`. `before`, `--before` and `next` are deprecated.
  * `management.storage.rows(name, table, { cursor })` and `domino storage rows NAME TABLE --cursor C` page by `nextCursor`; `offset` and `--offset` are deprecated. `management.storage.keys` answers `nextCursor`; its `cursor` field is deprecated.
  * New `management.trackers.triggerOutcomes(tracker, { ...filters, cursor })` answers `{ items, nextCursor }` and replaces `management.trackers.outcomes`, whose `before` and `next` stay as they were.
  * New `management.engagement.ruleVersions(rule, { cursor })` answers `{ items, nextCursor }` and replaces `management.engagement.versions({ rule, before })`.
  * `resourceOptionsSchema` (`@domino-sdk/relay/schema`) adds `nextCursor`; its `cursor` field is deprecated.

  The deprecated forms keep working through 1.x and are removed in 2.0.0. Responses gain only fields, so code written against 1.2 keeps compiling.

* 6fa9cd5: New `management.members.list({ q, needsReview, limit, cursor })` (`GET /management/v1/members/list`) and `domino members list [--q TEXT] [--needs-review] [--limit N] [--cursor C]` list a project's members, most recently joined first, with their accounts and attempt, completion and reward counts. The answer is `{ items, total, nextCursor }`: pass `nextCursor` back as `cursor` until it is `null`. `q` finds members by part of their ID, profile name, or an account's subject, name or email, as Console's member search does. Use it instead of `page("members", { q })`.

  `memberListSchema`, `memberPageSchema` and `MemberPage` (`@domino-sdk/relay/schema`), which no route used, are deprecated in favour of `managedMemberQuerySchema`, `managedMemberPageSchema` and `ManagedMemberPage` (`@domino-sdk/relay/server`), and are removed in 2.0.0.

* e3a050e: Management commands on an existing resource now name it in the path, and the client methods take it as their first argument:

  * `management.deployments.publish(id)`, `submit(id)`, `reject(id, { reason })`, `promote(id, { actionId? })` and `rollback(id, { actionId? })` call `/deployments/:deployment/<verb>`. The `{ id, ... }` forms still work and are deprecated. `domino deploy` uses the new routes.
  * `management.pointLedgers.update(ledger, { name, description })` calls `/point-ledgers/:ledger/update` and replaces the deprecated `pointLedgers.save({ id, name, description })`.
  * `management.referrals.suspend(member, { suspended, reason })`, `qualify(program, member, { qualified, reason })` and `resolveReward(entitlement, { reason })` replace the deprecated `referrals.control({ kind, ... })`. Each generates its `actionId` when omitted.

  Duplicate and misleading names have clear replacements; the old names stay as deprecated aliases:

  * `management.points.record(input)` awards, spends, adjusts, reverses or sets a balance, replacing `points.update`.
  * `management.pointLedgers.list()` and `pointLedgers.create(input)` replace `points.ledgers()` and `points.createLedger(input)`.

  The `{ id, ... }` input schemas (`publishDeploymentSchema`, `submitDeploymentSchema`, `rejectDeploymentSchema`, `promoteDeploymentSchema`, `rollbackDeploymentSchema` and `referralControlSchema`) are deprecated beside them; `deploymentRejectionSchema`, `deploymentActionSchema`, `referralSuspensionSchema`, `referralQualificationSchema` and `referralRewardResolutionSchema` describe the new bodies.

  The routes that took these IDs in the body keep answering released clients. Deprecated forms are removed in 2.0.0.

* b3d87d0: `points.awarded` webhook events now carry `source`, `{ kind, id }`, naming what paid the award: `quest` or `leaderboard` with the quest or board ID, `engagement` with the engagement rule's key, or `manual` with `id: null` for an operator's point award. `kind` is an open string that can gain values; `PayoutSourceKind` from `@domino-sdk/relay/server` names the known ones. A completion revocation result (`CompletionRevocation`, from `management.completions.revoke`) gains the same optional `source` as completion records, so a revoked leaderboard prize can be told apart from a quest completion; `quest` keeps carrying the board ID for a prize. See the [webhook reference](https://console.domino.run/docs/reference/webhooks).

* 5ddcb62: Approving a leaderboard settlement now sends a `completion.created` webhook event for each winner's completion. Before, only quest attempts sent it. Every `completion.created` event now carries `source` (`{ kind, id }`, where `kind` is `quest` or `leaderboard`) and `key`, the occurrence the completion consumed. A prize event has `attempt: null`, the board ID in `quest` and `source.id`, and the settlement ID in `key`. If your receiver treats every `completion.created` as a quest completion, branch on `source.kind`. Completion records in management snapshots gain the same optional `source`, and `CompletionSourceKind` is exported from `@domino-sdk/relay/server` and `@domino-sdk/relay/schema`. See the [webhook reference](https://console.domino.run/docs/reference/webhooks).

#### Patch Changes [#patch-changes-109]

* 75fa01c: Prerequisite-only automatic quests now try an uncompleted occurrence again after a claim-time budget rejection when a later member event passes current eligibility and budget checks. Evaluator, staff and revoked-occurrence rejections stay final. Automatic outcome reads can include optional `linkedAttempt` details; the legacy `attempt` ID remains available in 1.x but is deprecated. Operators can preview and confirm budget recovery through the existing automatic catch-up API and Console flow.

* 8d049d4: The authoring reference now says that `ctx.wallet.balance()` reads any Cosmos SDK chain `management.walletBalanceChains()` lists, not only Cosmos Hub and Osmosis. Domino configures these chains and their endpoints; quest code and project settings can't supply an RPC or REST URL. For an unlisted chain or other on-chain state, send records from your own indexer to a custom tracker source. Code changes aren't needed. See [token balances](https://console.domino.run/docs/build/wallets#check-token-balances).

* 6f10a51: Engagement rule payouts now earn referral percentage bonuses. Each daily engagement settlement counts as earnings of the rule's qualifying quest, so a bonus in the same ledger that covers that quest, or names no quests, pays the member's inviter in the same commit. Previously these payouts never earned a bonus. Bonuses apply from the next settlement; earlier engagement payouts are not backfilled. The agent referrals reference describes the rule. See [percentage bonuses](https://console.domino.run/docs/build/referrals#percentage-bonuses).

* f322ced: `ctx.review.photo()` is deprecated and will be removed in 2.0.0. It still works, and nothing about how it reviews photos changes. In new quest code, review an uploaded photo with `ctx.images.review({ images: [{ evidence: ctx.input.photo }], criteria })` instead. It returns the same `pass`, `fail` or `unclear` result, and it can also review X images, reference images and up to four images at once. See [check images](https://console.domino.run/docs/build/images#review-images-with-ai).

  The agent references and capability map now list tracker-restricted ingestion keys (`ingestTrackers` on `management.tokens.create` and `management.tokens.scope`) as supported.

* 89f742b: Leaderboard prize points no longer earn referral percentage bonuses. Before, a prize award counted as a quest award, so a bonus that named no quests paid the winner's inviter on it, while a bonus with a `quests` list never did. The points that won the prize already earned their bonuses. Bonuses already paid on earlier prizes stay, and revoking such a prize still corrects them. Quest, engagement and opted-in external awards are unchanged. The agent referrals and authoring references describe the rule. See [percentage bonuses](https://console.domino.run/docs/build/referrals#percentage-bonuses).

* f493c48: Referral corrections and operator reversals now consume one shared remainder for each positive payout, so either order settles the payout once. Historical corrections use the referral account's settled amount as a conservative cap. New referral definitions refuse Discord role rewards with a clear error. Stored legacy programs remain readable and recoverable, and skip an unsupported role with an Audit history reason instead of failing qualification. See [referral corrections](https://console.domino.run/docs/build/referrals#how-corrections-work).

* 6e9b120: Completion revocation now keeps any points it could not take back visible after the command returns. The result and audit event report the remainder for each point ledger. Console shows the remainder on the member and attempt, and operators can open the existing point adjustment to settle it explicitly. Relay never records member debt for the remainder.

* 0ec4d7d: `RelayClient` and `ManagementClient` are now named interfaces that `createRelayClient`, `createBrowserRelayClient`, `createManagementClient` and `hostedManagementClient` return, so declaration files name them instead of repeating the whole client. `ManagementClient.testMembers` is no longer optional: every client these factories create has it. The clients and the error classes (`RequestError`, `RelayError`, `QuestUpdatedError`, `ManagementError`) are not for implementation or subclassing; minor releases add methods and fields to them. A `@compat` tag in a type's doc comment now says how it may grow in a minor release ([Upgrade to Relay SDK v1](https://console.domino.run/docs/reference/upgrade-to-v1#supported-consumers-and-compatibility)).

* f6b705a: Revoking a completion now reads the member's recorded tier grants instead of every completion event in the project, so it stays fast as a project grows. `409 completion-grant-history-missing` remains only for older completions whose original record could not be read; contact Domino support to restore their history. See [Revoke a completion](https://console.domino.run/docs/concepts/quests#revoke-a-completion).

* Updated dependencies \[4651887]

* Updated dependencies \[75fa01c]

* Updated dependencies \[547600e]

* Updated dependencies \[6c80795]

* Updated dependencies \[6088922]

* Updated dependencies \[6fa9cd5]

* Updated dependencies \[e3a050e]

* Updated dependencies \[b145e16]

* Updated dependencies \[3831c73]

* Updated dependencies \[b3d87d0]

* Updated dependencies \[f322ced]

* Updated dependencies \[5ddcb62]

* Updated dependencies \[f493c48]

* Updated dependencies \[6e9b120]

* Updated dependencies \[0ec4d7d]

* Updated dependencies \[f6b705a]

* Updated dependencies \[65a9765]
  * @domino-sdk/relay\@1.6.0

### 1.5.2 [#152-1]

#### Patch Changes [#patch-changes-110]

* 68a30f4: `domino dev` keeps the app running when the process pauses for longer than its 90-second preview lease, as when a laptop sleeps or a playground VM is paused. On resume it claims the preview again and reconnects the tunnel without restarting the dev server, so the preview URL and port stay the same. A dropped lease renewal or a tunnel connector that exits now retries with backoff instead of stopping the app. `domino dev` still stops when another run has taken over the checkout's preview, when Domino refuses the preview (for example after publish permission is revoked), or when the API stays unreachable for about two and a half minutes.
* @domino-sdk/relay\@1.5.2

### 1.5.1 [#151-1]

#### Patch Changes [#patch-changes-111]

* 9fe0ec9: Point the moved-project message, the app-secrets capability, the bundled agent references and the starters' "no sign-in method" notice at Console's new Settings pages: **API keys**, **App login keys**, **App secrets**, **Sign-in**, **Profiles** and **Staging access**.
* @domino-sdk/relay\@1.5.1

### 1.5.0 [#150-1]

#### Minor Changes [#minor-changes-156]

* 625dcbc: Expose project creator IDs and resolved operator details in project access and audit responses. Validate enriched profile IDs against the recorded creator or actor while accepting older unenriched responses. Add `updateProject` for Console organization admins to rename projects or change their slug with name confirmation. Hosted build links follow the current slug without a rebuild, including older builds that used repository-based addresses. Links switch only after the new hostname is confirmed.

  Slug changes preserve app secrets and hosted stores. New staging addresses are registered before the change commits; old and prepared slugs remain reserved until project purge, so interrupted changes can be retried safely. Reserved slugs cannot be claimed by older servers during a rolling upgrade.

#### Patch Changes [#patch-changes-112]

* Updated dependencies \[625dcbc]
  * @domino-sdk/relay\@1.5.0

### 1.4.0 [#140-1]

#### Patch Changes [#patch-changes-113]

* Updated dependencies \[da46e7b]
  * @domino-sdk/relay\@1.4.0

### 1.3.0 [#130-1]

#### Patch Changes [#patch-changes-114]

* Updated dependencies \[86114a5]
  * @domino-sdk/relay\@1.3.0

### 1.2.0 [#120-1]

#### Minor Changes [#minor-changes-157]

* 7f69653: Add test-environment KV and D1 storage for Domino-hosted Worker apps through `relay.json` `app.storage`. Stores receive native Worker bindings and retain data when declarations are removed. SQL migration files run before staging activation, with Wrangler-compatible tracking and per-file rollback and safe retries after interrupted responses.

  Inspect declarations and retained stores with `domino storage list`. Connected development and staging warn when local Wrangler bindings differ from hosted declarations. See [hosted storage and migration recovery](https://console.domino.run/docs/build/storage).

* cdea4a0: Browse hosted app storage read-only and delete stores no serving build declares. `domino storage tables`, `rows`, `keys` and `get`, and `management.storage` in the SDK, read SQL tables and KV values with `publish`; `domino storage delete` removes an undeclared store and its data. See [browse and delete stores](https://console.domino.run/docs/build/storage#browse-your-data).

#### Patch Changes [#patch-changes-115]

* Updated dependencies \[7f69653]
* Updated dependencies \[cdea4a0]
  * @domino-sdk/relay\@1.2.0

### 1.1.1 [#111-1]

#### Patch Changes [#patch-changes-116]

* 1d2a0cc: Recover saved submissions after the server permanently retires an operation. Preserve the terminal state across reloads, expose it for participant review, and allow explicit discard without silently submitting another claim. Starters show a separate restart action; unknown conflicts and lost responses still resume the original action. See the [participant recovery reference](https://console.domino.run/docs/reference/participant-api).
* Updated dependencies \[1d2a0cc]
  * @domino-sdk/relay\@1.1.1

### 1.1.0 [#110-1]

#### Minor Changes [#minor-changes-158]

* ead35b5: Add Console-session-only management methods to create, list, act as, and reset test members in the test environment. Member reads expose the test marker and display name. Document the CLI exclusion and reset restrictions.

#### Patch Changes [#patch-changes-117]

* Updated dependencies \[ead35b5]
  * @domino-sdk/relay\@1.1.0

### 1.0.0 [#100-1]

#### Major Changes [#major-changes-14]

* ad91102: Record prerequisite-only automatic decisions, expose participant and operator outcomes, and add previewed catch-up for paused events with review permission and audited reasons. Point-entry changes schedule current eligibility asynchronously after commit, including refunds and referral or leaderboard awards. Operators can inspect and retry failed scheduling.

  Remove the permanent silent pause block. Outcome pagination now merges prerequisite and tracker decisions; reset saved cursors and use the `automatic` identity on rows without a tracker record. See the [v1 upgrade guide](https://domino.run/docs/reference/upgrade-to-v1#prerequisite-automatic-outcomes).

* 4205914: Add code-authored campaigns with half-open availability windows and durable campaign-wide enrollment; typed completion, live balance and project-group prerequisites; typed availability blockers; and live quest-budget capacity on participant views. Participant and management clients can enroll a member in a required campaign, and Console shows campaign rules and member enrollment.

  `completed(quest)` now returns a discriminated completion prerequisite instead of `{ quest }`, and `prerequisiteSchema` accepts only typed prerequisite definitions. Stored legacy releases remain readable. See [Prerequisites are typed eligibility rules](https://console.domino.run/docs/reference/upgrade-to-v1#prerequisites-are-typed-eligibility-rules) in the v1 upgrade guide.

* 6d66f96: Add reversible campaign enrollment. Participants can withdraw and re-enroll themselves; trusted servers and Console operators can withdraw a member with an audited reason and restore them. Withdrawal blocks new admissions while preserving the first enrollment time, admitted attempts, completions, and rewards.

* fa16503: The CLI's automation contract is frozen for v1. Every failure has a stable error `code`, and the exit status says what kind of failure it was: 0 success, 1 failure, 2 usage error (was 1), 3 a change that may have taken effect without confirmation (was 1), and 130 cancelled by SIGINT or a prompt, 143 by SIGTERM (was 1). With `--json`, the error on stderr's last line always has `error`, `code` and `outcome`, plus `status`, `apiCode`, `details` or `operation` when they apply; Commander's `commander.*` codes are gone. An uncertain `deploy` publication or `stage` build reports an `operation` whose `resume` arguments pin the full scope and finish it from any directory, announced as a JSON `operation` event on stderr right before the request is sent. `deploy --resume` and `stage --request-id` now require that scope as flags and need no checkout. `doctor`, `check` and `migrate` also write their failure to stderr. The undocumented `domino GET /path` spelling is removed; use `domino api GET /path`. `domino help` with an unknown command fails with `UNKNOWN_COMMAND`. `domino dev` is now listed under **Experimental commands**. Result and error JSON Schemas, fixtures, a command inventory and `cli/contract/index.mjs` (the zod schemas by name) ship in `cli/contract/`, and `domino reference automation` explains them. To upgrade, see [CLI errors have stable codes and exit statuses](https://console.domino.run/docs/reference/upgrade-to-v1#cli-errors-have-stable-codes-and-exit-statuses) and [Call the management API with `domino api`](https://console.domino.run/docs/reference/upgrade-to-v1#call-the-management-api-with-domino-api).

* a803b30: Evaluators now replay deterministically. Hosted evaluation freezes the clock for each attempt and seeds randomness from the attempt, so a retried or replayed attempt makes the same decision and `keyed()` completion key. This applies to every evaluation Domino runs, including releases published with earlier SDK versions.

  * `ctx.now` is new: the attempt's evaluation time in epoch milliseconds, the same on every run of the attempt. `keyed()` receives it too. It is distinct from `ctx.eligibleAt`, which is when the activity happened.
  * `Date.now()`, `new Date()` and `Date()` return `ctx.now` and do not advance, even across awaited capability calls. `performance.now()` returns 0.
  * `Math.random()`, `crypto.getRandomValues()` and `crypto.randomUUID()` repeat the same sequence for one attempt and differ between attempts.

  **Who should act.** Evaluators that measure elapsed time or rely on unpredictable random values. Use `ctx.now` for "as of evaluation" checks and `ctx.eligibleAt` for when the activity happened. See [Evaluators see a frozen clock and seeded randomness](https://console.domino.run/docs/reference/upgrade-to-v1#evaluators-see-a-frozen-clock-and-seeded-randomness) in the v1 upgrade guide.

* e1cba88: Observations are removed, and every automatic activity arrives as a tracker record. `automatic({ source, type })`, `ctx.observation`, `management.observations.submit`, `POST /management/v1/observations`, the `Observation` type, `observationSchema`, `observationInputSchema`, `observationReceiptSchema`, `observationTriggerSchema`, `discordReactionSchema`, `QuestView.trigger.observation` (and the release trigger's `observation`) and the `observation` field of the `automatic` availability are gone. `questStatus` and `runsSilently` from `@domino-sdk/relay/react` now label every tracker-triggered quest "Automatic", not only observation and keyed quests. A server that reported observations sends records to a custom tracker with `management.trackers.ingest` and triggers quests with `automatic({ tracker })`. Ingestion never joins a member to a project: a tracker trigger skips a member who has not joined with `not-joined`. Quests published with an observation trigger leave the active set when Relay upgrades. Republish them from code on a tracker; the quest key can stay the same.

  Discord messages and reactions are two managed tracker sources, `discord.messages()` (with `{ text: true }` to store message text) and `discord.reactions()`, on the `discord` object from `@domino-sdk/relay/authoring`. Domino's Discord gateway writes each new message or reaction into every tracker of that kind in the projects connected to the server, and keeps a record only when one of its accounts is a Discord account a member has linked. Their record data is typed (`DiscordMessageData`, `DiscordMessageTextData`, `DiscordReactionData`), so `where` filters and `ctx.tracker.record.data` are checked. A reaction record names the message's author as the `messageAuthor` account: `automatic({ tracker: reactions, beneficiary: "messageAuthor" })` replaces `reaction.received`, and Relay skips reactions to one's own message with `beneficiary-is-author`. Discord trackers refuse ingestion (403) and refresh (422). Streaks work on these triggers.

  The `observe` permission is removed. Ingesting needs `ingest`, and enrolling a member from a server (`management.quests.enroll`) needs the new `enroll` permission. Editors hold `read`, `publish`, `inventory`, `points`, `ingest` and `enroll`; hosted-app keys hold `read`, `points` and `ingest` and cannot enroll. A personal API key issued with `observe` keeps its other permissions, and `observe` grants nothing: reissue it with **Ingest** or **Enroll**. The `observation-quests` capability is removed from `capabilities.json`; Discord tracker triggers are part of the `trackers` capability.

  To upgrade, see [Observation triggers become tracker triggers](https://console.domino.run/docs/reference/upgrade-to-v1#observation-triggers-become-tracker-triggers) and [The `observe` permission is replaced by `enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#the-observe-permission-is-replaced-by-enroll).

* 23c85ba: `defineEngagementReward` takes `budgets`, a list of `{ scope: "project" | "member", period: "day" | "week" | "month" | "lifetime", limit }`, instead of `budget: { projectDaily, memberDaily, projectLifetime }`. Periods are those of the project calendar at settlement time, so a rule can now cap weekly or monthly payouts, and accounts settle once per project calendar day instead of once per UTC day. A definition with `budget` fails validation; rules already published keep their limits in the new form.

  See [Engagement budgets are a list of periods](https://console.domino.run/docs/reference/upgrade-to-v1#engagement-budgets-are-a-list-of-periods) in the v1 upgrade guide.

* 6cd0ba7: Make protocol 2 the first stable evaluator contract, including the required pinned project calendar. Remove pre-release protocol 1 from publication and execution, and preserve an SDK-built protocol-2 compatibility fixture. Rebuild and republish pre-v1 bundles during the approved cutover; changing only their protocol field is insufficient. See the [evaluator protocol upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#evaluator-protocol).

* 3a38259: Run content qualification and performance gates on custom content sources as well as X. Declare each custom source's body schema and metric names, use `content({ tracker })`, and read the captured body through `ctx.tracker.content`. Content quests imply one completion per item. Replace retired content admission and approval calls with quest outcomes, claims and tracker outcome retries.

  Upgrade steps: [Content stages](https://console.domino.run/docs/reference/upgrade-to-v1#content-stages).

* 4626004: Identity kinds are open. Besides `wallet`, `x`, `discord` and `member`, a custom tracker can declare your app's own kinds, `app:<name>` such as `app:telegram`, and records name your app's user IDs. An identity integration created with `identityKind: "app:<name>"` (set only at creation, kept by rotation) links each subject it signs in to the member as an identity of that kind, and tracker triggers attribute records through it, with `{ provider: "app:<name>", subject, revision, linkedAt }` as `ctx.tracker.attribution.account`.

  A kind belongs to the integration that first claimed it, also after a disconnect, which keeps its links attributing. A new integration takes a kind that has identities only by adopting them: `adoptIdentitiesFrom` (the old integration's ID, from the new `management.identityIntegrations.kinds()`) with a `reason` the audit log keeps; otherwise it is refused with 409. Reviewers unlink and relink a member's app identity (`management.members.unlinkIdentity` and `relinkIdentity`, and Console's **Tracker identities**), each audited with a reason: unlinking stops attribution, ends the member's sessions for that subject, and refuses its sign-in with 409 `APP_IDENTITY_UNLINKED`, which `createParticipantBackend` passes on, until it is relinked. A subject the integration already signs in as another member answers 409 `APP_IDENTITY_CONFLICT`.

  Evaluators read the member's selected X and Discord accounts and app identities with `ctx.member.identities()`, pinned once per attempt and `null` when there are none. Management member reads return them as `identities`, and Console lists them on the member under **Tracker identities**.

  Published tracker definitions in status, list, record-page, summary, and deployment responses use `trackerDefinitionResponseSchema` and `TrackerDefinitionResponse`. Authoring keeps the strict `trackerDefinitionSchema` and `TrackerDefinition`.

  Responses and evaluator contexts read identity kinds as an open set, `OpenIdentityKind`, and ignore fields they do not know, so a kind a later Relay adds no longer fails them.

  **Breaking:** `IdentityKind` is no longer a closed union, `identityKindSchema` is no longer a `z.enum`, response kinds are `OpenIdentityKind`, `trackerRecordSchema` is a separate response schema, `trackerAttributionSchema`'s account is a plain union with app and generic variants, and `ctx.member` is `{ id, identities() }`. See [Identity kinds are open, and evaluators read `ctx.member.identities()`](https://console.domino.run/docs/reference/upgrade-to-v1#identity-kinds-are-open-and-evaluators-read-ctxmemberidentities).

* 2149973: Read published trackers with future source kinds without rejecting a tracker list. Preserve unknown source payloads and expose `isKnownTrackerSource` for supported-source narrowing. Tracker authoring remains closed. Update code that assumes every response source is supported as described in the [v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#handle-future-tracker-sources).

* 54fcf41: The participant client follows one convention for paging, sessions and errors.

  * Every method takes an optional last argument, `{ session?, signal? }`. `session` replaces the positional session argument of `wallets.challenge`, `wallets.verify` and `me.collectionPass`. Calls Relay binds to a session (wallet linking, collection passes, `quests.retry`) read the current session when `session` is omitted.
  * Every paged read takes `{ cursor? }` and returns `{ items, nextCursor }`: `referrals.history`, `quests.outcomes`, `content.qualifications`, `content.performance`, `engagement.page` and `engagement.boundaries` changed from `before`, `after` and `next`. The participant routes answer `nextCursor` and read `cursor`, and so do the management routes and client methods that share them (`management.referrals.history(member, { cursor })`, `management.content.*`, `management.engagement.page` and `boundaries`).
  * Every rejection is a `RelayError`, which extends the new shared `RequestError`: `kind` (`http`, `network`, `timeout`, `aborted`, `invalid-response`, `client`), `uncertain` (true only for a write that may have taken effect: a failure after sending, a cut-off body, a 500, 502 or 504, or an unreadable success answer), `status` whenever response headers arrived, an open string `code` (known ones in `RelayErrorCode`), `operationId`, and `recovery` (`replay` with an `operationId`, else `reconcile`) when uncertain. `isRelayError` and `isRequestError` recognize these errors across duplicate copies of the package. `RelayError.status` is now `number | undefined`. Invalid input rejects with `invalid-input` instead of a `ZodError`, a success answer this SDK cannot read with `invalid-response`, a body cut off after its headers with kind `network` and its status, and a wallet signer's own failure with `wallet-signer`. The error body is read field by field, so Relay's message and a code the SDK does not know are always kept. Relay now sends `code: "not-signed-in"` and `code: "stale-session"` on those participant errors, and every paged read answers a malformed cursor with 400, code `invalid-cursor`, and one message.
  * Removed: `submit`, `retry(id)`, `progress(member)`, `upload(member, file)`, `auth.connections`, `me.linkPass`, the `wallet.*` namespace, and the staff calls the participant API refused (`lookupMember`, `snapshot`, `publish`, `publishBatch`, `beginHandover`, `confirmHandover`, `decide`, `journeyTemplate`, `template`).
  * Parameters take Zod input types, so defaulted fields are optional, and cursors are opaque strings the server validates. The session the client returns (`ParticipantSession`) accepts fields and principal kinds a newer Relay adds; `sessionMember(session)` reads its member.
  * Hosted proxies no longer forward the 0.x single-wallet routes (`/v1/me/wallet`, `/challenge`, `/verify`).

  Upgrade: [One paging, session and error convention in the participant client](https://console.domino.run/docs/reference/upgrade-to-v1#one-paging-session-and-error-convention-in-the-participant-client) in the SDK v1 upgrade guide maps every method and has search commands.

* c832384: Replace raw authoring resource keys with typed quest, ledger, tracker, and group handles. Authoring constructors and nested budget, tier, milestone, and bonus definitions now use `key`; ledger declarations publish from modules; point-ledger settings return typed references. Stored releases and management, Console, and participant wire IDs remain compatible. See [Authoring references use typed handles](https://console.domino.run/docs/reference/upgrade-to-v1#primitive-handles) in the v1 upgrade guide.

* 23c85ba: Each project environment has one project calendar: a timezone, UTC until you choose one, that sets where every day, week and month begins. Streak days, quest budgets, engagement budgets and calendar leaderboard windows all follow it, so a weekly budget and a weekly leaderboard cover the same week.

  * Set it with `"calendar": { "timeZone": "Europe/Budapest" }` in relay.json, in Console **Settings → General → Project calendar**, or with the new `management.calendar.read()` and `management.calendar.configure({ timeZone, reason })` (`GET` and `POST /calendar`, `publish` permission; in live only from Console). A change takes effect at the next midnight in the current timezone: the open day, week or month finishes in the old zone, and streaks stay consecutive. A deployment applies relay.json's timezone only when it differs from the last one deployed, and the deployment preview's new `calendar` field shows the change it schedules.
  * `streak()` and calendar leaderboard windows no longer take a `timeZone`; both throw if given one. Streaks and boards already published with one still read.
  * Budgets reset at midnight in the project timezone instead of UTC, and accept `period: "month"`.
  * One validator, `timeZoneSchema`, accepts named IANA zones only. Fixed offsets such as `+02:00` and `Etc/GMT-2` are rejected, including in leaderboards, which used to accept them. The shared `calendarPeriodSchema` is `lifetime | day | week | month`.

  **Who should act.** Projects that pass `timeZone` to `streak()` or a calendar leaderboard window, and projects that rely on budgets resetting at UTC midnight. See [Set the timezone once, on the project calendar](https://console.domino.run/docs/reference/upgrade-to-v1#set-the-timezone-once-on-the-project-calendar) in the v1 upgrade guide.

* 2528cec: Every tracker trigger shares the admission rules of ADR 0006: enrollment, the earliest activity counted, and who releases historical activity, with skips decided again when the member event that clears them happens.

  * **Admission options on `automatic({ tracker })`.** `enrollment: "required"` counts only members who enrolled in the quest, and `ctx.tracker.enrolledAt` carries their enrollment time. `since` is `"activation"` (the default), `"window"`, or, with required enrollment, `"enrollment"`. `historical` is `"staff"` (the default), `"claim"` or `"automatic"`.
  * **Historical activity waits.** A record from before the member linked the account it pays, or with an unknown link time, is skipped as `historical` until a reviewer retries it, unless the quest sets `historical: "claim"` or `"automatic"`. Before, it was admitted. See [Tracker triggers hold activity from before an account was linked](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-triggers-hold-activity-from-before-an-account-was-linked).
  * **Skips are decided again by member events.** Linking an account, removing a conflicting link, joining the project and enrolling decide the matching skips again, through a durable workflow.
  * **Every selected observation has one outcome.** New skip codes `outside-window`, `future-record`, `already-completed`, `not-enrolled`, `before-enrollment` and `historical`; `TrackerSkipCode` is an open string, with `KnownTrackerSkipCode` for the codes this SDK knows. A prerequisite completed after the record occurred no longer counts for it.
  * **Participant APIs.** `relay.quests.enroll({ actionId, quest })`, `relay.quests.outcomes({ quest?, state?, code?, cursor? })` and `relay.quests.claim({ actionId, quest, tracker, record, observedAt })`, and `useQuestOutcomes()` in React. `relay.content.enroll` and `management.content.enroll` are replaced by `relay.quests.enroll` and `management.quests.enroll`. See [Enroll in quests with `relay.quests.enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#enroll-in-quests-with-relayquestsenroll).
  * **Console.** A tracker-triggered quest shows its enrollment, `since` and historical policy, a `not-enrolled` skip offers **Enroll the member**, and a member's claim shows as **Claimed**.

  **Who should act.** Apps that enroll members or read trigger outcomes, and quests whose members link accounts after the activity they expect to be paid for.

* 89ea6ba: Add reviewed live project publication. The management client can list deployment records, submit and reject previews, promote a published test deployment, and prepare a rollback to the previous live deployment. Promotion and rollback accept an action UUID, and the management client generates one when omitted so retries return the same deployment record. `domino deploy --environment live` now returns `status: "pending"`, and `--promote` submits test source for live review; only a person in Console can approve, reject, or prepare rollback. The new `pending` deploy status is a breaking addition for exhaustive automation. See [Live deploys return a pending review](https://console.domino.run/docs/reference/upgrade-to-v1#live-deploys-return-a-pending-review).

* 5d4ec3e: Release Relay SDK and CLI 1.0.0. Install the SDK and CLI together at the same version. This release removes pre-v1 APIs; follow the [SDK v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1) before replacing a 0.x installation.

  Stable APIs follow semantic versioning throughout the supported v1 lifetime. Compatibility exemptions apply only to explicitly experimental entrypoints.

* ae047ca: Remove the pre-v1 runtime, release, single-wallet and tracker compatibility shims. Published quests must carry an explicit evaluator protocol; protocol 2 is the first stable contract, and all pre-v1 bundles must be rebuilt and republished. Participant and management reads now expose only canonical wallet lists, and tracker policies use canonical identity roles and an explicit observation mode. See [Wallet reads return every linked wallet](https://console.domino.run/docs/reference/upgrade-to-v1#wallet-reads-return-every-linked-wallet) in the v1 upgrade guide.

* bdb89a4: Evaluator readers follow one rule: a missing subject is `null`, a failure throws `CapabilityError`, linked accounts are pinned once per evaluation run, and tracker reads are point in time. Retries replay the same run; scheduled deferred evaluations get fresh captures. Pre-v1 releases must be rebuilt and republished during cutover.

  * **Missing is `null`.** `ctx.x.profile()`, `follows()` and `reposted()` return `null` without a linked X account instead of throwing; `ctx.x.post()` returns `null` for a post that does not exist and needs no linked account. `ctx.discord.member()` returns `null` without a linked Discord account. `ctx.wallet.list()` returns `null` instead of `[]`, and `ctx.wallet.balance()` returns `null` instead of `total: "0"` when the member has no wallet of that family. `ctx.trackers.content()` returns `null` for a missing record or observation. See [Readers return `null` when an account is not linked or a subject is missing](https://console.domino.run/docs/reference/upgrade-to-v1#readers-return-null-when-an-account-is-not-linked-or-a-subject-is-missing).
  * **Failures throw `CapabilityError`**, new in `@domino-sdk/relay/authoring` (its wire shape, `capabilityFailureSchema`, is in `@domino-sdk/relay/schema`), with `code` (`"not-linked"`, `"provider-unavailable"`, `"author-error"`) and `retryable`. Invalid capability input is `author-error`; an image check of `{ x: "profile" }` without a linked X account is `not-linked`. Failures that are not retryable are captured and replay. A retryable failure that quest code handles and moves past is final for that evaluation run; one that escapes the evaluation is released, along with every fact captured after it, and retried, so the retry asks the provider and the later calls again. Rethrow a caught failure, or wrap it with `{ cause: error }`, to let it be retried. The failure shape is open: a newer Relay can add codes, and the SDK treats an unknown code as not retryable. See [Capability failures throw `CapabilityError`](https://console.domino.run/docs/reference/upgrade-to-v1#capability-failures-throw-capabilityerror).
  * **Tracker reads are point in time.** `ctx.trackers.records()` and `summary()` take `asOf`, defaulting to `ctx.eligibleAt`, and every tracker read of an evaluation run sees the observations stored at its first tracker read. Pass `asOf: ctx.now` to count everything stored by then. See [Tracker reads are point in time and pinned per attempt](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-reads-are-point-in-time-and-pinned-per-attempt).
  * **Linked accounts stay pinned.** X and Discord readers pin the account on the evaluation run's first read and replay that pin on retries, as wallets already did, so a link change no longer fails the attempt with "X account changed" or "Account or integration changed". See [Linked X and Discord accounts stay pinned for the attempt](https://console.domino.run/docs/reference/upgrade-to-v1#linked-x-and-discord-accounts-stay-pinned-for-the-attempt).

  **Who should act.** Evaluators that read `ctx.x`, `ctx.discord`, `ctx.wallet` or `ctx.trackers`, or that match capability error messages. The changed return types point TypeScript at each read to update. The `discord-membership`, `token-holder` and `community-screenshot` snippets and the Discord starter's `join-the-server` quest are updated.

#### Minor Changes [#minor-changes-159]

* afe1128: Operators can revoke an accepted completion with a reason: `management.completions.revoke({ completion, reason, freeOccurrence })` (`POST /management/v1/completions/<id>/revoke`, `review` permission). In one transaction the completion stops counting (progress, prerequisites of new attempts, counts, profiles and referral qualification), its point awards are reversed as far as the member's available points allow, tiered rewards it granted are voided or lowered to the tier another completion grants (collected ones are flagged), Discord role grants not yet delivered are stopped while delivered roles are flagged for the operator to remove in Discord (Relay never removes roles; automatic removal is DOM-99), referral programs that qualified on it are re-evaluated, and a streak milestone it paid is revoked while its days stay. `freeOccurrence` lets the member complete the quest again for the same completion key; by default later attempts for it are rejected. The result reports each part of the cascade. API keys with `review` permission can revoke in live. Console offers it on member detail and accepted attempts, and the attempt history and Audit history show the revoke and its reason. `domino capabilities --json` now marks `completion-revoke` as native.

  Breaking: a reward delivery's `action` is an open string (`RewardDeliveryAction` lists `"grant-role"`), the worker fails a delivery whose action it does not know, and a grant still pending at the revoke fails. The revoke result's `outcome` fields are open strings with known values in `RevokedEntitlementOutcome`, `RevokedDeliveryOutcome` and `RevokedReferralOutcome`. To upgrade, see [Reward delivery actions are open strings](https://console.domino.run/docs/reference/upgrade-to-v1#reward-delivery-actions-are-open-strings).

  Revoking preserves percentage bonuses from other valid completions and queues a new automatic attempt when a freed occurrence still meets its prerequisites. Delivery responses add optional `dispatchedAt` and `revokedAt` fields. Dispatch history survives retries, revoke fences failed generations, and late provider success stays visible without reopening delivery. Console uses the revoke field to stop retries and shows unknown delivered actions by their generic status.

  Revocation refuses missing or unreadable grant history before changing member state. Collected entitlements carry a persistent recovery flag in ordinary reads and Console. Correcting collection clears the flag and voids an unearned reward instead of reopening it; a surviving grant reopens only its earned tier. Reused-completion attempt timelines include the shared completion’s revoke event.

* b0bffa0: Add native distinct-action progress counters with calendar and rolling windows, durable achievements, participant summaries and audited operator correction. Participant state follows settlement order for late-arriving repeat batches, and achievement alerts remain scoped to their calendar window when the next window reopens. Add qualified-referral and continuous-membership examples. See [counter rules](https://console.domino.run/docs/concepts/quests#distinct-action-counters). Update custom attempt renderers using the [counter upgrade steps](https://console.domino.run/docs/reference/upgrade-to-v1#counter-progress).

* e7f81ff: Add signed outbound webhooks for operator backends, with scoped subscriptions, bounded retries, delivery logs, replay, and overlapping secret rotation. Expose `management.webhooks`, `verifyWebhook`, shared activity constants, and `domino webhooks` commands. This is additive; existing integrations need no changes. See [Receive outbound webhooks](https://console.domino.run/docs/build/webhooks) for an optional polling migration.

* 903ffad: Persist recoverable CLI mutations before sending and add `operations list`, `show` and `resume`. Creation resumes through repository and checkout failures; quest lifecycle changes retain their action ID and scoped input. Errors for known partial failures may now include an operation to resume. See the [saved CLI mutation recovery upgrade guide](/docs/reference/upgrade-to-v1#saved-cli-mutation-recovery).

* 8465d40: Operators can correct a member's tiered reward, at the revision they saw and with an audited reason. `management.handover.cancel(entitlement, { revision, reason })` (`POST /management/v1/entitlements/<id>/cancel`, `handover` permission) returns a handover in progress to `available` and puts its pickup size back into stock. `management.entitlements.correctCollection` (`…/correct`, `review`) does the same for a collection recorded by mistake, and `management.entitlements.void` (`…/void`, `review`) voids an available entitlement. A stale revision fails with 409 `entitlement-changed`, the wrong starting status with 409 `entitlement-status`. API keys with the permission can make these corrections in live. Console Rewards and member detail offer them, and Audit history lists each one with its reason. `domino capabilities --json` lists them as `entitlement-corrections`.

  Breaking: `Entitlement` gains the status `"void"`, with `voidedAt`. A voided entitlement cannot be collected; a quest that grants the same reward to the member again makes it `available`, as a fresh grant at that quest's tier; a voided referral reward stays void. SDKs from before this release fail the whole progress read once a member has a voided entitlement. Reads now parse entitlements with a tolerant `entitlementSchema` (fields a newer server adds are kept, and an unknown status reads as `"unsupported"` with `reportedStatus`); the strict stored shape is `entitlementStateSchema`, and the management client's `entitlementRecordSchema` is removed in favor of `entitlementSchema`. To upgrade, see [Entitlements can be `void`, and reads tolerate statuses they do not know](https://console.domino.run/docs/reference/upgrade-to-v1#entitlements-can-be-void-and-reads-tolerate-statuses-they-do-not-know).

* d1ef769: Relay answers 503 only when it provably did nothing, so a write it reports as failed with 503 is safe to send again under a new action ID:

  * An unexpected server error is 500, not 503. It can follow a commit, and the SDK, the management client and the CLI already treat 500 on a write as `uncertain`.
  * A lost or unusable answer from an internal service is 502, or 504 when it timed out, where it was 503: publication, the Discord integration service, the preview service, Clerk API key changes, and the cloud builder ("Could not start cloud build. Retry with the same request ID." is now 502 or 504).
  * 503 stays for refusals before any work: missing configuration, a dependency check that fails before the transaction, and a data space closed for maintenance, which now sends `Retry-After: 60`.
  * Rate-limited answers (429) send `Retry-After` with the seconds until the limit's window resets: sign-in starts, email codes, wallet sign-in and wallet linking, CLI device login and approval, identity exchange, profile and avatar changes, playground starts, preview starts, development keys, tracker previews and feedback.

  The `participant-api` and `hosted` agent references describe the 503 rule and the 502 or 504 build start.

  Upgrade custom retry handling using [Treat failed writes as uncertain when Relay cannot prove refusal](https://console.domino.run/docs/reference/upgrade-to-v1#treat-failed-writes-as-uncertain-when-relay-cannot-prove-refusal). Evidence and avatar PUTs, email delivery, Clerk cleanup, and activation failures after a commit now preserve uncertainty. Staging password limits also carry `Retry-After`.

* 71cf912: Add reviewable leaderboard settlements. Fixed and calendar earned/net leaderboards can declare point and tiered-reward prizes, choose shared-rank or earliest-score tie handling, and freeze each ended window with its eligibility and exclusions at cutoff. Entries received after the cutoff stay out even when backdated, and a definition with a changed window advances past an obsolete scheduled cutoff. Operators review and approve delivery in Console or with `management.leaderboards`, where the approval reason remains visible, and can correct an individual winner through completion revocation without awarding that settlement again.

* f2f1b75: Breaking: the management client moves from `@domino-sdk/relay-cli` to the new `@domino-sdk/relay/server` entrypoint, with `createManagementClient`, `hostedManagementClient`, `ManagementError`, the management schemas and types, and `starterTemplates`. Server code, including a hosted app's Worker, now needs only `@domino-sdk/relay`, without the CLI's command-line parser and TypeScript compiler. `@domino-sdk/relay-cli` is only the `domino` executable and its agent skills; it has nothing to import. `serverBoundary()` also fails a browser build that imports `@domino-sdk/relay/server`. To upgrade, see [Import the management client from `@domino-sdk/relay/server`](https://console.domino.run/docs/reference/upgrade-to-v1#import-the-management-client-from-domino-sdkrelayserver).

  The capability catalog (`domino capabilities --json`) adds staff confirmation, staff handover, stock, point corrections, deploy and stage, operator member reads and lookup, project access, and the management API with `domino api`. It marks webhooks, bulk data export, completion revocation and tracker-scoped keys as unsupported, and lists the management methods it leaves out on purpose in `excludedManagementMethods`.

* 8c06912: Add operator reads and audited unlink for current X and Discord quest accounts. Reviewers provide the subject and selection revision to prevent a stale review from unlinking a newer connection. Identity ownership, sign-in grants and existing rewards remain unchanged. Console exposes the action on each member's Quest accounts section.

* c320586: A content quest's `performance.evaluate` can no longer select rewards. It gates the rewards the content evaluation already selected, and it always did: a `rewards` value in its result was silently ignored. Its type is now `PerformanceOutcome` (`accept`, `review` or `reject` with a reason only), so `decision.accept({ rewards })` and `decision.review({ reason, rewards })` are type errors there, and a runtime result that carries `rewards` fails the evaluation with "A performance evaluator cannot select rewards". To change what a post earns, select rewards in the quest's `evaluate`.

  See [Performance evaluators no longer select rewards](/docs/reference/upgrade-to-v1#performance-evaluators-no-longer-select-rewards) in the upgrade guide.

* c320586: Replace untyped prerequisite objects with typed rules. `completed(questHandle)` creates a completion rule with `scope: "ever"`; `completed(questHandle, { scope: "period", period: "day" })` checks the current project-calendar period. Balance and group requirements use `balanceAtLeast(ledgerHandle, minimum)` and `inGroup(groupHandle)`. Persisted rules carry an explicit `kind`; bare `{ quest }` and string prerequisites are not canonical v1 input.

  See [Prerequisites are typed eligibility rules](https://console.domino.run/docs/reference/upgrade-to-v1#prerequisites-are-typed-eligibility-rules) and [Authoring references use typed handles](https://console.domino.run/docs/reference/upgrade-to-v1#primitive-handles) in the upgrade guide.

* 569989e: Reconcile Discord role membership across projects and environments, preserve external assignments, and expose operator recovery plus current membership alongside delivery history. Revoking the last accepted grant now removes Relay-owned assignments. Read the [SDK v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#discord-role-ownership-and-reconciliation) before upgrading.

  Treat forbidden Discord membership reads as permanent delivery failures so operators can fix permissions and retry immediately.

  Publish initial pending delivery events in the grant transaction so observers can see queued rewards before reconciliation starts.

* 8a8870e: Add points-priced offers with declared stock, participant redemption hooks, automatic or staff-handover fulfillment, and operator cancellation and refund controls.

* 11ab1d8: Add explicit archive and restore for code-declared resources through the management client, durable CLI operations and Console. Keep resource identities and history while stopping new activity, and preserve archive state across publication. See the [resource lifecycle](https://console.domino.run/docs/concepts/quests#resource-lifecycle).

* ddfc526: The participant client, the management client and the CLI share one set of deadline, cancellation and retry rules.

  * Each call has one deadline covering every attempt, the waits between them and the answer's body: 15 seconds for the participant client and 30 seconds for the management client and the CLI. `createRelayClient({ timeout })` and the new per-call `{ timeout }` option of every participant method set it, as `timeout` already does for the management client. A timeout is never retried.
  * A call is sent again, at most three times in all, only when the server applies a repeat at most once: reads; participant writes with an action ID, submission evidence by its upload ID, `wallets.verify` (by its challenge) and `wallets.challenge`, but not avatar uploads, whose upload ID has no receipt; typed management writes whose input has an `actionId`; and the CLI's `deploy` publication, `stage` build request and `quests archive`/`unarchive`. Every attempt sends the same payload and session. Retries follow a lost answer, including a body that breaks off after successful headers, and HTTP 500, 502, 503 and 504, with a short jittered backoff. Invalid or unexpected answers, cancellations, rejected input, other refusals and every other write are sent once.
  * A 429 is retried only when its `Retry-After` fits in the time left, and every wait honours `Retry-After`. The new `retryAfter` field of `RequestError` (and so `RelayError` and `ManagementError`) carries the wait the server asked for, in milliseconds; the CLI reports it as `details.retryAfter` on `RATE_LIMITED` and `SERVER_ERROR`.
  * A session-bound participant call that reads the current session first shares one deadline with that read, and `auth.session()` honours `timeout`.
  * An error status whose body breaks off, times out or is cancelled is now a `network`, `timeout` or `aborted` failure that keeps its `status` and `retryAfter`, in all three clients; an error body that is not JSON stays `http`.
  * Cancelling with `signal` stops a retry wait at once. A write any attempt of which may have been applied stays `uncertain`, with its `operationId` and `recovery`, whatever later attempts answer.
  * `management.request` retries only GET, and `management.raw` sends once. `useInvitation()` keeps a rate-limited code for a retry instead of discarding it as refused.

  Avatar failures report `recovery: "reconcile"` even when they have an upload ID. Wallet address and signature waits reject when the caller cancels. See [Bounded requests and avatar recovery](https://console.domino.run/docs/reference/upgrade-to-v1#bounded-requests-and-avatar-recovery) for the recovery-handler change.

  Generic management writes report `recovery: "reconcile"` even with a caller-supplied action ID. Uncertain `domino api` writes expose `details.recovery: "reconcile"`. See [Generic management writes require reconciliation](https://console.domino.run/docs/reference/upgrade-to-v1#generic-management-writes-require-reconciliation) for the recovery-handler change.

* 67d0d8c: Add bounded `decision.defer` and absolute `decision.progress` results, fresh evaluation runs scheduled by durable timers, participant progress and deferred status, and consistent completion-key authoring types. Reviewers can reject a deferred attempt to stop it.

  Attempt response statuses accept future server values; use known `AttemptStatus` constants with a fallback. New manual submissions while deferred return `409 attempt-pending`. Custom decision/status switches must handle the new variants. See [Handle deferred evaluator decisions and progress](https://console.domino.run/docs/reference/upgrade-to-v1#handle-deferred-evaluator-decisions-and-progress) for old and new code, capture semantics, and search patterns.

* d3b6261: Restrict personal ingestion keys to selected custom trackers, inspect and edit their scope without changing the secret, and return TRACKER\_SCOPE\_DENIED for disallowed writes. Human CLI sign-ins can manage their own keys; project-scoped playground sessions and API keys cannot. Existing unrestricted keys retain project-wide ingestion.

* d7f9fe4: `streak(...)` completes manual and tracker-triggered quests. Each accepted attempt counts the project-calendar day of its trusted occurrence time, exposed as `ctx.eligibleAt`: a tracker observation's `occurredAt`, or the admission time of a manual submission. A rejected manual submission preserves committed streak progress and displays its reason. Once the streak milestone pays, later tracker observations do not pay it again.

  Content-stage quests and prerequisite-only `automatic()` quests cannot use streak completion. A published quest cannot change between manual and tracker triggers under the same key. Observation triggers are removed; migrate them to trackers. See [Observation triggers become tracker triggers](https://console.domino.run/docs/reference/upgrade-to-v1#observation-triggers-become-tracker-triggers).

* 0a89168: Operators can archive point ledgers and correct points from Console member detail. `management.pointLedgers.list()` includes nullable `archivedAt`, and `management.pointLedgers.archive(id)` archives a ledger. Archived ledgers remain in balance and history reads and existing published rewards keep working, but new reward pickers and published definitions reject them.

* 0052894: The management client has typed methods for the routes Console and scripts called by path: `attempts.confirm`, `attempts.review` and `attempts.retry`, `passes.verify`, `handover.begin` and `handover.confirm`, `operations.configure`, `inventory.addStock`, `quests.updateSettings`, `tokens`, `grants`, `team` and `identityIntegrations`. The new `members.lookup({ provider, subject })` (`GET /management/v1/members/lookup`, `read` permission) finds the one member of the project holding exactly that account, and fails with 404 `member-not-found` or 409 `account-ambiguous`.

  Breaking: every failed call rejects with a `ManagementError` whose `kind` (`http`, `network`, `timeout`, `aborted`, `invalid-response`, `client`) separates a server refusal from a lost or unreadable answer and from input rejected before sending. It keeps the server's `message` and any `code`, including codes this version does not know, and reports `uncertain`, with `operationId` (the `actionId` it sent) and `recovery`, when a write may have been applied. It extends the SDK's shared `RequestError`; `isManagementError()` recognizes it. `CreationError` is removed. Calls end after 30 seconds by default; every method takes `{ signal, timeout }` as its last argument, and the client takes a default `timeout`. To upgrade, see [Catch `ManagementError` instead of `CreationError` or a status property](https://console.domino.run/docs/reference/upgrade-to-v1#catch-managementerror-instead-of-creationerror-or-a-status-property).

  Breaking: `member`, `people`, `unlinkMemberWallet` and `setMemberWalletSignIn` move to `members.get`, `members.people`, `members.unlinkWallet` and `members.setWalletSignIn`. See [Member methods moved under `management.members`](https://console.domino.run/docs/reference/upgrade-to-v1#member-methods-moved-under-managementmembers).

* cf5b02c: Custom tracker records have a declared data schema. `customSource(namespace, { data })` requires a zod object schema (`z` is exported by `@domino-sdk/relay/authoring`), stores its JSON Schema form with the tracker, and throws, naming the field, for what ingestion cannot check (string formats and patterns, tuples, refinements, transforms, dates, bigints). Publication refuses a custom tracker without one. Ingestion rejects a batch whose record data does not match with 422 and `{ error, code: "invalid-record-data", record, path, reason }`, one too costly to validate with `record-data-too-complex`, and records for a tracker published without a schema with `tracker-schema-required`. Schemas are bounded (32 KB, 16 levels, 256 properties, required keys, enum values or key names per node, 16 branches per union, no wide union nested in another union's branch), and validation charges every unit of work to a per-record and per-batch step budget. A deployment whose new schema breaks a live quest's `where` filter, Console quests included, is refused. Stored records are not rechecked: tracker triggers skip non-matching records with the new skip code `invalid-record-data`, evaluators reject pinned records that do not match the schema the quest was built with, and `trackers.records(handle)` leaves them out and counts them in `excluded`. The schema can change under the same tracker key and applies to later records.

  The data type flows into quests: `ctx.tracker` is non-null in a tracker-triggered quest's evaluator with `record.data` of the tracker's type (content triggers expose their captured body through `ctx.tracker.content`), `trackers.records(tracker)` returns typed records, and `where` paths, `equals` values and `{ setting }` bounds are type-checked against the data and the quest's integer settings. `where` also accepts a list of conditions that must all hold, and `automatic()` and publication check filter paths against the stored schema. `settings.object` and `defineQuest` accept only `settings.*` builders; a plain zod setting is a type error and `defineQuest` throws `Setting "<key>" is not a quest setting`.

  See [Custom sources declare their record data](/docs/reference/upgrade-to-v1#custom-sources-declare-their-record-data) and [Quest settings come only from settings builders](/docs/reference/upgrade-to-v1#quest-settings-come-only-from-settings-builders) in the upgrade guide.

#### Patch Changes [#patch-changes-118]

* 1019218: Console **Audit history** now records every point ledger created in Console or with `management.points.createLedger`, as **Point ledger created**, with the ledger's key and name, the environment and who created it. Before, only edits were audited.

* ed4988c: Read Cosmos Hub and Osmosis bank balances with `ctx.wallet.balance()`, pinned to one verified block and the member's wallets on the exact Cosmos network.

* c320586: `defineQuest` throws at build time for two shapes that only failed when publishing: a bare `automatic()` quest with no `requires` entry, and a bare `automatic()` quest with `keyed()` completion. A bare `automatic()` runs once its prerequisites are complete, so it needs at least one typed completion, balance or group prerequisite and completes once. The backend still checks both when it publishes.

* 83c4426: Update Discord message tracker records when a message is edited or deleted. Turning text off or deleting a message now erases retained message text.

* bd960a4: Only a person signed in to Console can create or edit a point ledger in live. `management.points.createLedger` and `management.pointLedgers.save` with an API key or a CLI sign-in now get 403 "Approve live publication in the console" in live, as publication already did; in test they work as before. To upgrade, see [test and live](https://console.domino.run/docs/concepts/environments). Live operator controls stay open to a personal API key with `publish`: operation pauses (`POST /management/v1/operations`), `management.referrals.control`, `management.leaderboards.exclude` and `group`, and `management.trackers.control`. Console **Audit history** now lists leaderboard exclusion, group and tracker control changes with the key's owner as the actor. The agent references and `capabilities.json` describe both rules.

* c320586: `manual({ actor: "staff", input })` throws instead of dropping the actor. Its types never allowed the combination, but untyped callers got a quest that members could submit evidence to, not one staff confirm. Staff-captured evidence is not supported: use `manual({ actor: "staff" })` for a staff confirmation, or `manual({ input })` for evidence the participant submits.

* 2f78766: Fix `domino check` and `domino migrate` when a valid manifest omits either discovery array. Omitted arrays use the same empty defaults as `domino build`, and explicit resources remain included.

* 6c30583: Add member-only activity pages, `me.activity.observe()` and `useActivity({ onEvent })` for committed asynchronous outcomes. Cursor reads preserve concurrent commits, and observers pause while hidden and reset on session changes. Events begin after the additive activity-feed migration; existing snapshots remain authoritative for earlier state.

* 710e520: Participants read their own point balances and history. `relay.me.balances()` (`GET /v1/me/points`) returns `{ items }`: the signed-in member's `balance` and `available` points (after reservations) in every ledger of the project, each with its `ledger` reference. `relay.me.pointHistory({ ledger, cursor? })` (`GET /v1/me/points/:ledger/history`) pages through the member's own entries in one ledger, by key or ID, most recently recorded first, as `{ items, nextCursor }`. Each entry is `{ id, amount, kind, at, quest, reverses }`, without the operator's reason or actor. `kind` is an open string; the `PointEntryKind` constants (`award`, `spend`, `adjust`, `reverse`) name the kinds this SDK knows, and referral payouts are `adjust`. An unknown ledger answers 404 with code `point-ledger-not-found` and a member who has not joined the project 404 with `not-joined`, both added to `RelayErrorCode`. Neither read takes a member, and both are scoped to the session's project and environment. React apps use `useBalances()`, which reads again after a quest completes, and `usePointHistory(ledger)`. The participant proxy and identity integrations admit both routes. New schemas and types: `pointBalanceSchema`, `pointBalancesSchema`, `pointEntrySchema`, `pointHistoryQuerySchema`, `pointHistoryPageSchema`, `PointBalance`, `PointBalances`, `PointEntry`, `PointEntryKind`, `KnownPointEntryKind`, `PointHistoryQuery` and `PointHistoryPage`.

  The classic starter's header now reads the balance with `useBalances()` instead of its `/api/me/points` server route, which is gone.

* 6c30583: Expose participant earned rewards through `rewards.entitlements()` and `useEntitlements()`, and include entitlements in experience reads. Add `rewards.deliveries()` and `useDeliveries()` for points, tiers and integration deliveries. Preserve unknown delivery providers, kinds and states in reads while keeping backend delivery writes validated. Role displays use current reconciliation state after revocation.

  Keep private operator revocation reasons out of participant reward, progress and experience reads while retaining visible collection recovery state.

* 1084288: `@domino-sdk/relay` now exports only the participant runtime client: `createRelayClient`, `RelayError`, `QuestUpdatedError`, `formatUnits`, `parseUnits`, and the types the client returns. Schemas, builders and avatar helpers moved to their own entry points, a few implementation helpers are no longer public, and two are no longer exported at all, so an app that imports the client no longer loads authoring builders, deployment and management schemas, or DiceBear.

  * Zod schemas (and helpers such as `identifier`, `caip10` and `xPostId`) come from `@domino-sdk/relay/schema`.
  * Builders (`defineTracker`, `defineLeaderboard`, `defineReferral`, `defineEngagementReward`, `content`, `streak`, `settings`, `discord`, `pointLedgers`, `x`, `customSource`) and evaluator reader types (`TrackerReader`, `XReader`, `ImageReader`) come only from `@domino-sdk/relay/authoring`.
  * `avatarSrc`, `defaultAvatar`, `defaultAvatarSvg` and `walletAvatarSeed` come from `@domino-sdk/relay/avatar`, and no longer from `@domino-sdk/relay` or `@domino-sdk/relay/react`.
  * No longer public: `identityIntegrationSchema`, `identityIntegrationInputSchema`, `identityIntegrationSecretSchema`, `participantExchangeSchema`, `questConfiguration`, `parseImageResult`, `parseImageExtraction`, `isContentParticipantRoute`, `isProfileParticipantRoute`, `isWalletParticipantRoute` and `isWalletSignInRoute` are exported only from `@domino-sdk/relay/internal`, which Relay's own services share and which is not a supported API.
  * No longer exported: `questActions` and `reconcileQuestDraft`. Open a quest controller with `relay.quests.open(view)`; its `quest-updated` result already carries the reconciled `draft`.
  * The package declares `sideEffects: false`.
  * `esbuild` is an optional peer dependency, needed only by `@domino-sdk/relay/build`. `@domino-sdk/relay-cli` depends on it directly, so `domino build` and `domino deploy` work as before. Code that calls `buildQuest` without the CLI must install `esbuild`.

  Upgrade: [Runtime entry exports only the client](https://console.domino.run/docs/reference/upgrade-to-v1#runtime-entry-exports-only-the-client) in the SDK v1 upgrade guide has the import mapping and search commands.

* e635fe2: Troubleshooting reference: when a build or typecheck fails on an API that no longer exists after an upgrade, read the Upgrade to Relay SDK v1 guide.

* Updated dependencies \[ad91102]

* Updated dependencies \[4b10080]

* Updated dependencies \[4205914]

* Updated dependencies \[6d66f96]

* Updated dependencies \[afe1128]

* Updated dependencies \[ed4988c]

* Updated dependencies \[b0bffa0]

* Updated dependencies \[bac9ca4]

* Updated dependencies \[c320586]

* Updated dependencies \[a803b30]

* Updated dependencies \[e1cba88]

* Updated dependencies \[1141af4]

* Updated dependencies \[e7f81ff]

* Updated dependencies \[23c85ba]

* Updated dependencies \[8465d40]

* Updated dependencies \[84feaf7]

* Updated dependencies \[6cd0ba7]

* Updated dependencies \[3a38259]

* Updated dependencies \[83c4426]

* Updated dependencies \[71cf912]

* Updated dependencies \[f2f1b75]

* Updated dependencies \[c320586]

* Updated dependencies \[4626004]

* Updated dependencies \[2149973]

* Updated dependencies \[8c06912]

* Updated dependencies \[6c30583]

* Updated dependencies \[54fcf41]

* Updated dependencies \[710e520]

* Updated dependencies \[6c30583]

* Updated dependencies \[c320586]

* Updated dependencies \[c320586]

* Updated dependencies \[c832384]

* Updated dependencies \[23c85ba]

* Updated dependencies \[2528cec]

* Updated dependencies \[569989e]

* Updated dependencies \[8a8870e]

* Updated dependencies \[11ab1d8]

* Updated dependencies \[7e37049]

* Updated dependencies \[c919948]

* Updated dependencies \[c2390b3]

* Updated dependencies \[909f343]

* Updated dependencies \[ddfc526]

* Updated dependencies \[8b706af]

* Updated dependencies \[89ea6ba]

* Updated dependencies \[67d0d8c]

* Updated dependencies \[d3b6261]

* Updated dependencies \[1084288]

* Updated dependencies \[5d4ec3e]

* Updated dependencies \[d7f9fe4]

* Updated dependencies \[ae047ca]

* Updated dependencies \[0a89168]

* Updated dependencies \[e1e701b]

* Updated dependencies \[cf5b02c]

* Updated dependencies \[1e23d8f]

* Updated dependencies \[bdb89a4]
  * @domino-sdk/relay\@1.0.0

### 1.0.0-next.34 [#100-next34-1]

#### Major Changes [#major-changes-15]

* ad91102: Record prerequisite-only automatic decisions, expose participant and operator outcomes, and add previewed catch-up for paused events with review permission and audited reasons. Point-entry changes schedule current eligibility asynchronously after commit, including refunds and referral or leaderboard awards. Operators can inspect and retry failed scheduling.

  Remove the permanent silent pause block. Outcome pagination now merges prerequisite and tracker decisions; reset saved cursors and use the `automatic` identity on rows without a tracker record. See the [v1 upgrade guide](https://domino.run/docs/reference/upgrade-to-v1#prerequisite-automatic-outcomes).

* 4205914: Add code-authored campaigns with half-open availability windows and durable campaign-wide enrollment; typed completion, live balance and project-group prerequisites; typed availability blockers; and live quest-budget capacity on participant views. Participant and management clients can enroll a member in a required campaign, and Console shows campaign rules and member enrollment.

  `completed(quest)` now returns a discriminated completion prerequisite instead of `{ quest }`, and `prerequisiteSchema` accepts only typed prerequisite definitions. Stored legacy releases remain readable. See [Prerequisites are typed eligibility rules](https://console.domino.run/docs/reference/upgrade-to-v1#prerequisites-are-typed-eligibility-rules) in the v1 upgrade guide.

* 6d66f96: Add reversible campaign enrollment. Participants can withdraw and re-enroll themselves; trusted servers and Console operators can withdraw a member with an audited reason and restore them. Withdrawal blocks new admissions while preserving the first enrollment time, admitted attempts, completions, and rewards.

* 6cd0ba7: Make protocol 2 the first stable evaluator contract, including the required pinned project calendar. Remove pre-release protocol 1 from publication and execution, and preserve an SDK-built protocol-2 compatibility fixture. Rebuild and republish pre-v1 bundles during the approved cutover; changing only their protocol field is insufficient. See the [evaluator protocol upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#evaluator-protocol).

* 3a38259: Run content qualification and performance gates on custom content sources as well as X. Declare each custom source's body schema and metric names, use `content({ tracker })`, and read the captured body through `ctx.tracker.content`. Content quests imply one completion per item. Replace retired content admission and approval calls with quest outcomes, claims and tracker outcome retries.

  Upgrade steps: [Content stages](https://console.domino.run/docs/reference/upgrade-to-v1#content-stages).

* c832384: Replace raw authoring resource keys with typed quest, ledger, tracker, and group handles. Authoring constructors and nested budget, tier, milestone, and bonus definitions now use `key`; ledger declarations publish from modules; point-ledger settings return typed references. Stored releases and management, Console, and participant wire IDs remain compatible. See [Authoring references use typed handles](https://console.domino.run/docs/reference/upgrade-to-v1#primitive-handles) in the v1 upgrade guide.

* 2528cec: Every tracker trigger shares the admission rules of ADR 0006: enrollment, the earliest activity counted, and who releases historical activity, with skips decided again when the member event that clears them happens.

  * **Admission options on `automatic({ tracker })`.** `enrollment: "required"` counts only members who enrolled in the quest, and `ctx.tracker.enrolledAt` carries their enrollment time. `since` is `"activation"` (the default), `"window"`, or, with required enrollment, `"enrollment"`. `historical` is `"staff"` (the default), `"claim"` or `"automatic"`.
  * **Historical activity waits.** A record from before the member linked the account it pays, or with an unknown link time, is skipped as `historical` until a reviewer retries it, unless the quest sets `historical: "claim"` or `"automatic"`. Before, it was admitted. See [Tracker triggers hold activity from before an account was linked](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-triggers-hold-activity-from-before-an-account-was-linked).
  * **Skips are decided again by member events.** Linking an account, removing a conflicting link, joining the project and enrolling decide the matching skips again, through a durable workflow.
  * **Every selected observation has one outcome.** New skip codes `outside-window`, `future-record`, `already-completed`, `not-enrolled`, `before-enrollment` and `historical`; `TrackerSkipCode` is an open string, with `KnownTrackerSkipCode` for the codes this SDK knows. A prerequisite completed after the record occurred no longer counts for it.
  * **Participant APIs.** `relay.quests.enroll({ actionId, quest })`, `relay.quests.outcomes({ quest?, state?, code?, cursor? })` and `relay.quests.claim({ actionId, quest, tracker, record, observedAt })`, and `useQuestOutcomes()` in React. `relay.content.enroll` and `management.content.enroll` are replaced by `relay.quests.enroll` and `management.quests.enroll`. See [Enroll in quests with `relay.quests.enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#enroll-in-quests-with-relayquestsenroll).
  * **Console.** A tracker-triggered quest shows its enrollment, `since` and historical policy, a `not-enrolled` skip offers **Enroll the member**, and a member's claim shows as **Claimed**.

  **Who should act.** Apps that enroll members or read trigger outcomes, and quests whose members link accounts after the activity they expect to be paid for.

* ae047ca: Remove the pre-v1 runtime, release, single-wallet and tracker compatibility shims. Published quests must carry an explicit evaluator protocol; protocol 1 remains supported, while protocol-less bundles must be republished. Participant and management reads now expose only canonical wallet lists, and tracker policies use canonical identity roles and an explicit observation mode. See [Wallet reads return every linked wallet](https://console.domino.run/docs/reference/upgrade-to-v1#wallet-reads-return-every-linked-wallet) in the v1 upgrade guide.

#### Minor Changes [#minor-changes-160]

* b0bffa0: Add native distinct-action progress counters with calendar and rolling windows, durable achievements, participant summaries and audited operator correction. Participant state follows settlement order for late-arriving repeat batches, and achievement alerts remain scoped to their calendar window when the next window reopens. Add qualified-referral and continuous-membership examples. See [counter rules](https://console.domino.run/docs/concepts/quests#distinct-action-counters). Update custom attempt renderers using the [counter upgrade steps](https://console.domino.run/docs/reference/upgrade-to-v1#counter-progress).
* 71cf912: Add reviewable leaderboard settlements. Fixed and calendar earned/net leaderboards can declare point and tiered-reward prizes, choose shared-rank or earliest-score tie handling, and freeze each ended window with its eligibility and exclusions at cutoff. Entries received after the cutoff stay out even when backdated, and a definition with a changed window advances past an obsolete scheduled cutoff. Operators review and approve delivery in Console or with `management.leaderboards`, where the approval reason remains visible, and can correct an individual winner through completion revocation without awarding that settlement again.
* 8a8870e: Add points-priced offers with declared stock, participant redemption hooks, automatic or staff-handover fulfillment, and operator cancellation and refund controls.
* 11ab1d8: Add explicit archive and restore for code-declared resources through the management client, durable CLI operations and Console. Keep resource identities and history while stopping new activity, and preserve archive state across publication. See the [resource lifecycle](https://console.domino.run/docs/concepts/quests#resource-lifecycle).
* 0a89168: Operators can archive point ledgers and correct points from Console member detail. `management.pointLedgers.list()` includes nullable `archivedAt`, and `management.pointLedgers.archive(id)` archives a ledger. Archived ledgers remain in balance and history reads and existing published rewards keep working, but new reward pickers and published definitions reject them.

#### Patch Changes [#patch-changes-119]

* ed4988c: Read Cosmos Hub and Osmosis bank balances with `ctx.wallet.balance()`, pinned to one verified block and the member's wallets on the exact Cosmos network.

* 2f78766: Fix `domino check` and `domino migrate` when a valid manifest omits either discovery array. Omitted arrays use the same empty defaults as `domino build`, and explicit resources remain included.

* 6c30583: Add member-only activity pages, `me.activity.observe()` and `useActivity({ onEvent })` for committed asynchronous outcomes. Cursor reads preserve concurrent commits, and observers pause while hidden and reset on session changes. Events begin after the additive activity-feed migration; existing snapshots remain authoritative for earlier state.

* 6c30583: Expose participant earned rewards through `rewards.entitlements()` and `useEntitlements()`, and include entitlements in experience reads. Add `rewards.deliveries()` and `useDeliveries()` for points, tiers and integration deliveries. Preserve unknown delivery providers, kinds and states in reads while keeping backend delivery writes validated. Role displays use current reconciliation state after revocation.

  Keep private operator revocation reasons out of participant reward, progress and experience reads while retaining visible collection recovery state.

* Updated dependencies \[ad91102]

* Updated dependencies \[4b10080]

* Updated dependencies \[4205914]

* Updated dependencies \[6d66f96]

* Updated dependencies \[ed4988c]

* Updated dependencies \[b0bffa0]

* Updated dependencies \[bac9ca4]

* Updated dependencies \[84feaf7]

* Updated dependencies \[6cd0ba7]

* Updated dependencies \[3a38259]

* Updated dependencies \[71cf912]

* Updated dependencies \[6c30583]

* Updated dependencies \[6c30583]

* Updated dependencies \[c832384]

* Updated dependencies \[2528cec]

* Updated dependencies \[8a8870e]

* Updated dependencies \[11ab1d8]

* Updated dependencies \[ae047ca]

* Updated dependencies \[0a89168]

* Updated dependencies \[e1e701b]

* Updated dependencies \[1e23d8f]
  * @domino-sdk/relay\@1.0.0-next.34

### 1.0.0-next.33 [#100-next33-1]

#### Major Changes [#major-changes-16]

* 89ea6ba: Add reviewed live project publication. The management client can list deployment records, submit and reject previews, promote a published test deployment, and prepare a rollback to the previous live deployment. Promotion and rollback accept an action UUID, and the management client generates one when omitted so retries return the same deployment record. `domino deploy --environment live` now returns `status: "pending"`, and `--promote` submits test source for live review; only a person in Console can approve, reject, or prepare rollback. The new `pending` deploy status is a breaking addition for exhaustive automation. See [Live deploys return a pending review](https://console.domino.run/docs/reference/upgrade-to-v1#live-deploys-return-a-pending-review).

#### Patch Changes [#patch-changes-120]

* Updated dependencies \[89ea6ba]
  * @domino-sdk/relay\@1.0.0-next.33

### 1.0.0-next.32 [#100-next32-1]

#### Patch Changes [#patch-changes-121]

* 6c30583: Add member-only activity pages, `me.activity.observe()` and `useActivity({ onEvent })` for committed asynchronous outcomes. Cursor reads preserve concurrent commits, and observers pause while hidden and reset on session changes. Events begin after the additive activity-feed migration; existing snapshots remain authoritative for earlier state.

* 6c30583: Expose participant earned rewards through `rewards.entitlements()` and `useEntitlements()`, and include entitlements in experience reads. Add `rewards.deliveries()` and `useDeliveries()` for points, tiers and integration deliveries. Preserve unknown delivery providers, kinds and states in reads while keeping backend delivery writes validated. Role displays use current reconciliation state after revocation.

  Keep private operator revocation reasons out of participant reward, progress and experience reads while retaining visible collection recovery state.

* Updated dependencies \[6c30583]

* Updated dependencies \[6c30583]
  * @domino-sdk/relay\@1.0.0-next.32

### 1.0.0-next.31 [#100-next31-1]

#### Minor Changes [#minor-changes-161]

* 569989e: Reconcile Discord role membership across projects and environments, preserve external assignments, and expose operator recovery plus current membership alongside delivery history. Revoking the last accepted grant now removes Relay-owned assignments. Read the [SDK v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#discord-role-ownership-and-reconciliation) before upgrading.

  Treat forbidden Discord membership reads as permanent delivery failures so operators can fix permissions and retry immediately.

  Publish initial pending delivery events in the grant transaction so observers can see queued rewards before reconciliation starts.

#### Patch Changes [#patch-changes-122]

* Updated dependencies \[569989e]
  * @domino-sdk/relay\@1.0.0-next.31

### 1.0.0-next.30 [#100-next30-1]

#### Minor Changes [#minor-changes-162]

* afe1128: Operators can revoke an accepted completion with a reason: `management.completions.revoke({ completion, reason, freeOccurrence })` (`POST /management/v1/completions/<id>/revoke`, `review` permission). In one transaction the completion stops counting (progress, prerequisites of new attempts, counts, profiles and referral qualification), its point awards are reversed as far as the member's available points allow, tiered rewards it granted are voided or lowered to the tier another completion grants (collected ones are flagged), Discord role grants not yet delivered are stopped while delivered roles are flagged for the operator to remove in Discord (Relay never removes roles; automatic removal is DOM-99), referral programs that qualified on it are re-evaluated, and a streak milestone it paid is revoked while its days stay. `freeOccurrence` lets the member complete the quest again for the same completion key; by default later attempts for it are rejected. The result reports each part of the cascade. API keys with `review` permission can revoke in live. Console offers it on member detail and accepted attempts, and the attempt history and Audit history show the revoke and its reason. `domino capabilities --json` now marks `completion-revoke` as native.

  Breaking: a reward delivery's `action` is an open string (`RewardDeliveryAction` lists `"grant-role"`), the worker fails a delivery whose action it does not know, and a grant still pending at the revoke fails. The revoke result's `outcome` fields are open strings with known values in `RevokedEntitlementOutcome`, `RevokedDeliveryOutcome` and `RevokedReferralOutcome`. To upgrade, see [Reward delivery actions are open strings](https://console.domino.run/docs/reference/upgrade-to-v1#reward-delivery-actions-are-open-strings).

  Revoking preserves percentage bonuses from other valid completions and queues a new automatic attempt when a freed occurrence still meets its prerequisites. Delivery responses add optional `dispatchedAt` and `revokedAt` fields. Dispatch history survives retries, revoke fences failed generations, and late provider success stays visible without reopening delivery. Console uses the revoke field to stop retries and shows unknown delivered actions by their generic status.

  Revocation refuses missing or unreadable grant history before changing member state. Collected entitlements carry a persistent recovery flag in ordinary reads and Console. Correcting collection clears the flag and voids an unearned reward instead of reopening it; a surviving grant reopens only its earned tier. Reused-completion attempt timelines include the shared completion’s revoke event.

#### Patch Changes [#patch-changes-123]

* Updated dependencies \[afe1128]
  * @domino-sdk/relay\@1.0.0-next.30

### 1.0.0-next.29 [#100-next29-1]

#### Minor Changes [#minor-changes-163]

* 67d0d8c: Add bounded `decision.defer` and absolute `decision.progress` results, fresh evaluation runs scheduled by durable timers, participant progress and deferred status, and consistent completion-key authoring types. Reviewers can reject a deferred attempt to stop it.

  Attempt response statuses accept future server values; use known `AttemptStatus` constants with a fallback. New manual submissions while deferred return `409 attempt-pending`. Custom decision/status switches must handle the new variants. See [Handle deferred evaluator decisions and progress](https://console.domino.run/docs/reference/upgrade-to-v1#handle-deferred-evaluator-decisions-and-progress) for old and new code, capture semantics, and search patterns. This ships in the `1.0.0-next` prerelease series.

* d3b6261: Restrict personal ingestion keys to selected custom trackers, inspect and edit their scope without changing the secret, and return TRACKER\_SCOPE\_DENIED for disallowed writes. Human CLI sign-ins can manage their own keys; project-scoped playground sessions and API keys cannot. Existing unrestricted keys retain project-wide ingestion.

#### Patch Changes [#patch-changes-124]

* Updated dependencies \[67d0d8c]
* Updated dependencies \[d3b6261]
  * @domino-sdk/relay\@1.0.0-next.29

### 1.0.0-next.28 [#100-next28-1]

#### Patch Changes [#patch-changes-125]

* 83c4426: Update Discord message tracker records when a message is edited or deleted. Turning text off or deleting a message now erases retained message text.
* Updated dependencies \[83c4426]
  * @domino-sdk/relay\@1.0.0-next.28

### 1.0.0-next.27 [#100-next27-1]

#### Minor Changes [#minor-changes-164]

* e7f81ff: Add signed outbound webhooks for operator backends, with scoped subscriptions, bounded retries, delivery logs, replay, and overlapping secret rotation. Expose `management.webhooks`, `verifyWebhook`, shared activity constants, and `domino webhooks` commands. This is additive; existing integrations need no changes. See [Receive outbound webhooks](https://console.domino.run/docs/build/webhooks) for an optional polling migration.

#### Patch Changes [#patch-changes-126]

* Updated dependencies \[e7f81ff]
  * @domino-sdk/relay\@1.0.0-next.27

### 1.0.0-next.26 [#100-next26-1]

#### Minor Changes [#minor-changes-165]

* 8c06912: Add operator reads and audited unlink for current X and Discord quest accounts. Reviewers provide the subject and selection revision to prevent a stale review from unlinking a newer connection. Identity ownership, sign-in grants and existing rewards remain unchanged. Console exposes the action on each member's Quest accounts section.

#### Patch Changes [#patch-changes-127]

* Updated dependencies \[8c06912]
  * @domino-sdk/relay\@1.0.0-next.26

### 1.0.0-next.25 [#100-next25-1]

#### Minor Changes [#minor-changes-166]

* 903ffad: Persist recoverable CLI mutations before sending and add `operations list`, `show` and `resume`. Creation resumes through repository and checkout failures; quest lifecycle changes retain their action ID and scoped input. Errors for known partial failures may now include an operation to resume. See the [saved CLI mutation recovery upgrade guide](/docs/reference/upgrade-to-v1#saved-cli-mutation-recovery).

#### Patch Changes [#patch-changes-128]

* @domino-sdk/relay\@1.0.0-next.25

### 1.0.0-next.24 [#100-next24-1]

#### Major Changes [#major-changes-17]

* 2528cec: Every tracker trigger shares the admission rules of ADR 0006: enrollment, the earliest activity counted, and who releases historical activity, with skips decided again when the member event that clears them happens.

  * **Admission options on `automatic({ tracker })`.** `enrollment: "required"` counts only members who enrolled in the quest, and `ctx.tracker.enrolledAt` carries their enrollment time. `since` is `"activation"` (the default), `"window"`, or, with required enrollment, `"enrollment"`. `historical` is `"staff"` (the default), `"claim"` or `"automatic"`.
  * **Historical activity waits.** A record from before the member linked the account it pays, or with an unknown link time, is skipped as `historical` until a reviewer retries it, unless the quest sets `historical: "claim"` or `"automatic"`. Before, it was admitted. See [Tracker triggers hold activity from before an account was linked](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-triggers-hold-activity-from-before-an-account-was-linked).
  * **Skips are decided again by member events.** Linking an account, removing a conflicting link, joining the project and enrolling decide the matching skips again, through a durable workflow.
  * **Every selected observation has one outcome.** New skip codes `outside-window`, `future-record`, `already-completed`, `not-enrolled`, `before-enrollment` and `historical`; `TrackerSkipCode` is an open string, with `KnownTrackerSkipCode` for the codes this SDK knows. A prerequisite completed after the record occurred no longer counts for it.
  * **Participant APIs.** `relay.quests.enroll({ actionId, quest })`, `relay.quests.outcomes({ quest?, state?, code?, cursor? })` and `relay.quests.claim({ actionId, quest, tracker, record, observedAt })`, and `useQuestOutcomes()` in React. `relay.content.enroll` and `management.content.enroll` are replaced by `relay.quests.enroll` and `management.quests.enroll`. See [Enroll in quests with `relay.quests.enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#enroll-in-quests-with-relayquestsenroll).
  * **Console.** A tracker-triggered quest shows its enrollment, `since` and historical policy, a `not-enrolled` skip offers **Enroll the member**, and a member's claim shows as **Claimed**.

  **Who should act.** Apps that enroll members or read trigger outcomes, and quests whose members link accounts after the activity they expect to be paid for.

#### Patch Changes [#patch-changes-129]

* Updated dependencies \[2528cec]
  * @domino-sdk/relay\@1.0.0-next.24

### 1.0.0-next.23 [#100-next23-1]

#### Major Changes [#major-changes-18]

* 2149973: Read published trackers with future source kinds without rejecting a tracker list. Preserve unknown source payloads and expose `isKnownTrackerSource` for supported-source narrowing. Tracker authoring remains closed. Update code that assumes every response source is supported as described in the [v1 upgrade guide](https://console.domino.run/docs/reference/upgrade-to-v1#handle-future-tracker-sources).

#### Patch Changes [#patch-changes-130]

* Updated dependencies \[2149973]
  * @domino-sdk/relay\@1.0.0-next.23

### 1.0.0-next.22 [#100-next22-1]

#### Major Changes [#major-changes-19]

* 4626004: Identity kinds are open. Besides `wallet`, `x`, `discord` and `member`, a custom tracker can declare your app's own kinds, `app:<name>` such as `app:telegram`, and records name your app's user IDs. An identity integration created with `identityKind: "app:<name>"` (set only at creation, kept by rotation) links each subject it signs in to the member as an identity of that kind, and tracker triggers attribute records through it, with `{ provider: "app:<name>", subject, revision, linkedAt }` as `ctx.tracker.attribution.account`.

  A kind belongs to the integration that first claimed it, also after a disconnect, which keeps its links attributing. A new integration takes a kind that has identities only by adopting them: `adoptIdentitiesFrom` (the old integration's ID, from the new `management.identityIntegrations.kinds()`) with a `reason` the audit log keeps; otherwise it is refused with 409. Reviewers unlink and relink a member's app identity (`management.members.unlinkIdentity` and `relinkIdentity`, and Console's **Tracker identities**), each audited with a reason: unlinking stops attribution, ends the member's sessions for that subject, and refuses its sign-in with 409 `APP_IDENTITY_UNLINKED`, which `createParticipantBackend` passes on, until it is relinked. A subject the integration already signs in as another member answers 409 `APP_IDENTITY_CONFLICT`.

  Evaluators read the member's selected X and Discord accounts and app identities with `ctx.member.identities()`, pinned once per attempt and `null` when there are none. Management member reads return them as `identities`, and Console lists them on the member under **Tracker identities**.

  Published tracker definitions in status, list, record-page, summary, and deployment responses use `trackerDefinitionResponseSchema` and `TrackerDefinitionResponse`. Authoring keeps the strict `trackerDefinitionSchema` and `TrackerDefinition`.

  Responses and evaluator contexts read identity kinds as an open set, `OpenIdentityKind`, and ignore fields they do not know, so a kind a later Relay adds no longer fails them.

  **Breaking:** `IdentityKind` is no longer a closed union, `identityKindSchema` is no longer a `z.enum`, response kinds are `OpenIdentityKind`, `trackerRecordSchema` is a separate response schema, `trackerAttributionSchema`'s account is a plain union with app and generic variants, and `ctx.member` is `{ id, identities() }`. See [Identity kinds are open, and evaluators read `ctx.member.identities()`](https://console.domino.run/docs/reference/upgrade-to-v1#identity-kinds-are-open-and-evaluators-read-ctxmemberidentities).

#### Patch Changes [#patch-changes-131]

* Updated dependencies \[4626004]
  * @domino-sdk/relay\@1.0.0-next.22

### 1.0.0-next.21 [#100-next21-1]

#### Minor Changes [#minor-changes-167]

* ddfc526: The participant client, the management client and the CLI share one set of deadline, cancellation and retry rules.

  * Each call has one deadline covering every attempt, the waits between them and the answer's body: 15 seconds for the participant client and 30 seconds for the management client and the CLI. `createRelayClient({ timeout })` and the new per-call `{ timeout }` option of every participant method set it, as `timeout` already does for the management client. A timeout is never retried.
  * A call is sent again, at most three times in all, only when the server applies a repeat at most once: reads; participant writes with an action ID, submission evidence by its upload ID, `wallets.verify` (by its challenge) and `wallets.challenge`, but not avatar uploads, whose upload ID has no receipt; typed management writes whose input has an `actionId`; and the CLI's `deploy` publication, `stage` build request and `quests archive`/`unarchive`. Every attempt sends the same payload and session. Retries follow a lost answer, including a body that breaks off after successful headers, and HTTP 500, 502, 503 and 504, with a short jittered backoff. Invalid or unexpected answers, cancellations, rejected input, other refusals and every other write are sent once.
  * A 429 is retried only when its `Retry-After` fits in the time left, and every wait honours `Retry-After`. The new `retryAfter` field of `RequestError` (and so `RelayError` and `ManagementError`) carries the wait the server asked for, in milliseconds; the CLI reports it as `details.retryAfter` on `RATE_LIMITED` and `SERVER_ERROR`.
  * A session-bound participant call that reads the current session first shares one deadline with that read, and `auth.session()` honours `timeout`.
  * An error status whose body breaks off, times out or is cancelled is now a `network`, `timeout` or `aborted` failure that keeps its `status` and `retryAfter`, in all three clients; an error body that is not JSON stays `http`.
  * Cancelling with `signal` stops a retry wait at once. A write any attempt of which may have been applied stays `uncertain`, with its `operationId` and `recovery`, whatever later attempts answer.
  * `management.request` retries only GET, and `management.raw` sends once. `useInvitation()` keeps a rate-limited code for a retry instead of discarding it as refused.

  Avatar failures report `recovery: "reconcile"` even when they have an upload ID. Wallet address and signature waits reject when the caller cancels. See [Bounded requests and avatar recovery](https://console.domino.run/docs/reference/upgrade-to-v1#bounded-requests-and-avatar-recovery) for the recovery-handler change.

  Generic management writes report `recovery: "reconcile"` even with a caller-supplied action ID. Uncertain `domino api` writes expose `details.recovery: "reconcile"`. See [Generic management writes require reconciliation](https://console.domino.run/docs/reference/upgrade-to-v1#generic-management-writes-require-reconciliation) for the recovery-handler change.

#### Patch Changes [#patch-changes-132]

* Updated dependencies \[ddfc526]
  * @domino-sdk/relay\@1.0.0-next.21

### 1.0.0-next.20 [#100-next20-1]

#### Minor Changes [#minor-changes-168]

* d1ef769: Relay answers 503 only when it provably did nothing, so a write it reports as failed with 503 is safe to send again under a new action ID:

  * An unexpected server error is 500, not 503. It can follow a commit, and the SDK, the management client and the CLI already treat 500 on a write as `uncertain`.
  * A lost or unusable answer from an internal service is 502, or 504 when it timed out, where it was 503: publication, the Discord integration service, the preview service, Clerk API key changes, and the cloud builder ("Could not start cloud build. Retry with the same request ID." is now 502 or 504).
  * 503 stays for refusals before any work: missing configuration, a dependency check that fails before the transaction, and a data space closed for maintenance, which now sends `Retry-After: 60`.
  * Rate-limited answers (429) send `Retry-After` with the seconds until the limit's window resets: sign-in starts, email codes, wallet sign-in and wallet linking, CLI device login and approval, identity exchange, profile and avatar changes, playground starts, preview starts, development keys, tracker previews and feedback.

  The `participant-api` and `hosted` agent references describe the 503 rule and the 502 or 504 build start.

  Upgrade custom retry handling using [Treat failed writes as uncertain when Relay cannot prove refusal](https://console.domino.run/docs/reference/upgrade-to-v1#treat-failed-writes-as-uncertain-when-relay-cannot-prove-refusal). Evidence and avatar PUTs, email delivery, Clerk cleanup, and activation failures after a commit now preserve uncertainty. Staging password limits also carry `Retry-After`.

#### Patch Changes [#patch-changes-133]

* @domino-sdk/relay\@1.0.0-next.20

### 1.0.0-next.19 [#100-next19-1]

#### Patch Changes [#patch-changes-134]

* Updated dependencies \[1141af4]
  * @domino-sdk/relay\@1.0.0-next.19

### 1.0.0-next.18 [#100-next18-1]

#### Minor Changes [#minor-changes-169]

* 8465d40: Operators can correct a member's tiered reward, at the revision they saw and with an audited reason. `management.handover.cancel(entitlement, { revision, reason })` (`POST /management/v1/entitlements/<id>/cancel`, `handover` permission) returns a handover in progress to `available` and puts its pickup size back into stock. `management.entitlements.correctCollection` (`…/correct`, `review`) does the same for a collection recorded by mistake, and `management.entitlements.void` (`…/void`, `review`) voids an available entitlement. A stale revision fails with 409 `entitlement-changed`, the wrong starting status with 409 `entitlement-status`. API keys with the permission can make these corrections in live. Console Rewards and member detail offer them, and Audit history lists each one with its reason. `domino capabilities --json` lists them as `entitlement-corrections`.

  Breaking: `Entitlement` gains the status `"void"`, with `voidedAt`. A voided entitlement cannot be collected; a quest that grants the same reward to the member again makes it `available`, as a fresh grant at that quest's tier; a voided referral reward stays void. SDKs from before this release fail the whole progress read once a member has a voided entitlement. Reads now parse entitlements with a tolerant `entitlementSchema` (fields a newer server adds are kept, and an unknown status reads as `"unsupported"` with `reportedStatus`); the strict stored shape is `entitlementStateSchema`, and the management client's `entitlementRecordSchema` is removed in favor of `entitlementSchema`. To upgrade, see [Entitlements can be `void`, and reads tolerate statuses they do not know](https://console.domino.run/docs/reference/upgrade-to-v1#entitlements-can-be-void-and-reads-tolerate-statuses-they-do-not-know).

#### Patch Changes [#patch-changes-135]

* Updated dependencies \[8465d40]
  * @domino-sdk/relay\@1.0.0-next.18

### 1.0.0-next.17 [#100-next17-1]

#### Patch Changes [#patch-changes-136]

* Updated dependencies \[c919948]
  * @domino-sdk/relay\@1.0.0-next.17

### 1.0.0-next.16 [#100-next16-1]

#### Major Changes [#major-changes-20]

* bdb89a4: Evaluator readers follow one rule: a missing subject is `null`, a failure throws `CapabilityError`, linked accounts are pinned once per attempt, and tracker reads are point in time. This applies to every evaluation Domino runs, including releases published with earlier SDK versions.

  * **Missing is `null`.** `ctx.x.profile()`, `follows()` and `reposted()` return `null` without a linked X account instead of throwing; `ctx.x.post()` returns `null` for a post that does not exist and needs no linked account. `ctx.discord.member()` returns `null` without a linked Discord account. `ctx.wallet.list()` returns `null` instead of `[]`, and `ctx.wallet.balance()` returns `null` instead of `total: "0"` when the member has no wallet of that family. `ctx.trackers.content()` returns `null` for a missing record or observation. See [Readers return `null` when an account is not linked or a subject is missing](https://console.domino.run/docs/reference/upgrade-to-v1#readers-return-null-when-an-account-is-not-linked-or-a-subject-is-missing).
  * **Failures throw `CapabilityError`**, new in `@domino-sdk/relay/authoring` (its wire shape, `capabilityFailureSchema`, is in `@domino-sdk/relay/schema`), with `code` (`"not-linked"`, `"provider-unavailable"`, `"author-error"`) and `retryable`. Invalid capability input is `author-error`; an image check of `{ x: "profile" }` without a linked X account is `not-linked`. Failures that are not retryable are captured and replay. A retryable failure that quest code handles and moves past is final for the attempt; one that escapes the evaluation is released, along with every fact captured after it, and retried, so the retry asks the provider and the later calls again. Rethrow a caught failure, or wrap it with `{ cause: error }`, to let it be retried. The failure shape is open: a newer Relay can add codes, and the SDK treats an unknown code as not retryable. See [Capability failures throw `CapabilityError`](https://console.domino.run/docs/reference/upgrade-to-v1#capability-failures-throw-capabilityerror).
  * **Tracker reads are point in time.** `ctx.trackers.records()` and `summary()` take `asOf`, defaulting to `ctx.eligibleAt`, and every tracker read of an attempt sees the observations stored at its first tracker read. Pass `asOf: ctx.now` to count everything stored by then. See [Tracker reads are point in time and pinned per attempt](https://console.domino.run/docs/reference/upgrade-to-v1#tracker-reads-are-point-in-time-and-pinned-per-attempt).
  * **Linked accounts stay pinned.** X and Discord readers pin the account at the attempt's first read and never recheck it, as wallets already did, so a link change no longer fails the attempt with "X account changed" or "Account or integration changed". See [Linked X and Discord accounts stay pinned for the attempt](https://console.domino.run/docs/reference/upgrade-to-v1#linked-x-and-discord-accounts-stay-pinned-for-the-attempt).

  **Who should act.** Evaluators that read `ctx.x`, `ctx.discord`, `ctx.wallet` or `ctx.trackers`, or that match capability error messages. The changed return types point TypeScript at each read to update. The `discord-membership`, `token-holder` and `community-screenshot` snippets and the Discord starter's `join-the-server` quest are updated.

#### Patch Changes [#patch-changes-137]

* Updated dependencies \[bdb89a4]
  * @domino-sdk/relay\@1.0.0-next.16

### 1.0.0-next.15 [#100-next15-1]

#### Major Changes [#major-changes-21]

* e1cba88: Observations are removed, and every automatic activity arrives as a tracker record. `automatic({ source, type })`, `ctx.observation`, `management.observations.submit`, `POST /management/v1/observations`, the `Observation` type, `observationSchema`, `observationInputSchema`, `observationReceiptSchema`, `observationTriggerSchema`, `discordReactionSchema`, `QuestView.trigger.observation` (and the release trigger's `observation`) and the `observation` field of the `automatic` availability are gone. `questStatus` and `runsSilently` from `@domino-sdk/relay/react` now label every tracker-triggered quest "Automatic", not only observation and keyed quests. A server that reported observations sends records to a custom tracker with `management.trackers.ingest` and triggers quests with `automatic({ tracker })`. Ingestion never joins a member to a project: a tracker trigger skips a member who has not joined with `not-joined`. Quests published with an observation trigger leave the active set when Relay upgrades. Republish them from code on a tracker; the quest key can stay the same.

  Discord messages and reactions are two managed tracker sources, `discord.messages()` (with `{ text: true }` to store message text) and `discord.reactions()`, on the `discord` object from `@domino-sdk/relay/authoring`. Domino's Discord gateway writes each new message or reaction into every tracker of that kind in the projects connected to the server, and keeps a record only when one of its accounts is a Discord account a member has linked. Their record data is typed (`DiscordMessageData`, `DiscordMessageTextData`, `DiscordReactionData`), so `where` filters and `ctx.tracker.record.data` are checked. A reaction record names the message's author as the `messageAuthor` account: `automatic({ tracker: reactions, beneficiary: "messageAuthor" })` replaces `reaction.received`, and Relay skips reactions to one's own message with `beneficiary-is-author`. Discord trackers refuse ingestion (403) and refresh (422). Streaks work on these triggers.

  The `observe` permission is removed. Ingesting needs `ingest`, and enrolling a member from a server (`management.content.enroll`) needs the new `enroll` permission. Editors hold `read`, `publish`, `inventory`, `points`, `ingest` and `enroll`; hosted-app keys hold `read`, `points` and `ingest` and cannot enroll. A personal API key issued with `observe` keeps its other permissions, and `observe` grants nothing: reissue it with **Ingest** or **Enroll**. The `observation-quests` capability is removed from `capabilities.json`; Discord tracker triggers are part of the `trackers` capability.

  To upgrade, see [Observation triggers become tracker triggers](https://console.domino.run/docs/reference/upgrade-to-v1#observation-triggers-become-tracker-triggers) and [The `observe` permission is replaced by `enroll`](https://console.domino.run/docs/reference/upgrade-to-v1#the-observe-permission-is-replaced-by-enroll).

#### Patch Changes [#patch-changes-138]

* Updated dependencies \[e1cba88]
  * @domino-sdk/relay\@1.0.0-next.15

### 1.0.0-next.14 [#100-next14-1]

#### Patch Changes [#patch-changes-139]

* Updated dependencies \[c2390b3]
  * @domino-sdk/relay\@1.0.0-next.14

### 1.0.0-next.13 [#100-next13-1]

#### Major Changes [#major-changes-22]

* 23c85ba: `defineEngagementReward` takes `budgets`, a list of `{ scope: "project" | "member", period: "day" | "week" | "month" | "lifetime", limit }`, instead of `budget: { projectDaily, memberDaily, projectLifetime }`. Periods are those of the project calendar at settlement time, so a rule can now cap weekly or monthly payouts, and accounts settle once per project calendar day instead of once per UTC day. A definition with `budget` fails validation; rules already published keep their limits in the new form.

  See [Engagement budgets are a list of periods](https://console.domino.run/docs/reference/upgrade-to-v1#engagement-budgets-are-a-list-of-periods) in the v1 upgrade guide.

* 23c85ba: Each project environment has one project calendar: a timezone, UTC until you choose one, that sets where every day, week and month begins. Streak days, quest budgets, engagement budgets and calendar leaderboard windows all follow it, so a weekly budget and a weekly leaderboard cover the same week.

  * Set it with `"calendar": { "timeZone": "Europe/Budapest" }` in relay.json, in Console **Settings → General → Project calendar**, or with the new `management.calendar.read()` and `management.calendar.configure({ timeZone, reason })` (`GET` and `POST /calendar`, `publish` permission; in live only from Console). A change takes effect at the next midnight in the current timezone: the open day, week or month finishes in the old zone, and streaks stay consecutive. A deployment applies relay.json's timezone only when it differs from the last one deployed, and the deployment preview's new `calendar` field shows the change it schedules.
  * `streak()` and calendar leaderboard windows no longer take a `timeZone`; both throw if given one. Streaks and boards already published with one still read.
  * Budgets reset at midnight in the project timezone instead of UTC, and accept `period: "month"`.
  * One validator, `timeZoneSchema`, accepts named IANA zones only. Fixed offsets such as `+02:00` and `Etc/GMT-2` are rejected, including in leaderboards, which used to accept them. The shared `calendarPeriodSchema` is `lifetime | day | week | month`.

  **Who should act.** Projects that pass `timeZone` to `streak()` or a calendar leaderboard window, and projects that rely on budgets resetting at UTC midnight. See [Set the timezone once, on the project calendar](https://console.domino.run/docs/reference/upgrade-to-v1#set-the-timezone-once-on-the-project-calendar) in the v1 upgrade guide.

#### Patch Changes [#patch-changes-140]

* Updated dependencies \[23c85ba]
* Updated dependencies \[23c85ba]
  * @domino-sdk/relay\@1.0.0-next.13

### 1.0.0-next.12 [#100-next12-1]

#### Patch Changes [#patch-changes-141]

* 710e520: Participants read their own point balances and history. `relay.me.balances()` (`GET /v1/me/points`) returns `{ items }`: the signed-in member's `balance` and `available` points (after reservations) in every ledger of the project, each with its `ledger` reference. `relay.me.pointHistory({ ledger, cursor? })` (`GET /v1/me/points/:ledger/history`) pages through the member's own entries in one ledger, by key or ID, most recently recorded first, as `{ items, nextCursor }`. Each entry is `{ id, amount, kind, at, quest, reverses }`, without the operator's reason or actor. `kind` is an open string; the `PointEntryKind` constants (`award`, `spend`, `adjust`, `reverse`) name the kinds this SDK knows, and referral payouts are `adjust`. An unknown ledger answers 404 with code `point-ledger-not-found` and a member who has not joined the project 404 with `not-joined`, both added to `RelayErrorCode`. Neither read takes a member, and both are scoped to the session's project and environment. React apps use `useBalances()`, which reads again after a quest completes, and `usePointHistory(ledger)`. The participant proxy and identity integrations admit both routes. New schemas and types: `pointBalanceSchema`, `pointBalancesSchema`, `pointEntrySchema`, `pointHistoryQuerySchema`, `pointHistoryPageSchema`, `PointBalance`, `PointBalances`, `PointEntry`, `PointEntryKind`, `KnownPointEntryKind`, `PointHistoryQuery` and `PointHistoryPage`.

  The classic starter's header now reads the balance with `useBalances()` instead of its `/api/me/points` server route, which is gone.

* Updated dependencies \[710e520]
  * @domino-sdk/relay\@1.0.0-next.12

### 1.0.0-next.11 [#100-next11-1]

#### Minor Changes [#minor-changes-170]

* f2f1b75: Breaking: the management client moves from `@domino-sdk/relay-cli` to the new `@domino-sdk/relay/server` entrypoint, with `createManagementClient`, `hostedManagementClient`, `ManagementError`, the management schemas and types, and `starterTemplates`. Server code, including a hosted app's Worker, now needs only `@domino-sdk/relay`, without the CLI's command-line parser and TypeScript compiler. `@domino-sdk/relay-cli` is only the `domino` executable and its agent skills; it has nothing to import. `serverBoundary()` also fails a browser build that imports `@domino-sdk/relay/server`. To upgrade, see [Import the management client from `@domino-sdk/relay/server`](https://console.domino.run/docs/reference/upgrade-to-v1#import-the-management-client-from-domino-sdkrelayserver).

  The capability catalog (`domino capabilities --json`) adds staff confirmation, staff handover, stock, point corrections, deploy and stage, operator member reads and lookup, project access, and the management API with `domino api`. It marks webhooks, bulk data export, completion revocation and tracker-scoped keys as unsupported, and lists the management methods it leaves out on purpose in `excludedManagementMethods`.

#### Patch Changes [#patch-changes-142]

* Updated dependencies \[f2f1b75]
* Updated dependencies \[909f343]
  * @domino-sdk/relay\@1.0.0-next.11

### 1.0.0-next.10 [#100-next10-1]

#### Minor Changes [#minor-changes-171]

* 0052894: The management client has typed methods for the routes Console and scripts called by path: `attempts.confirm`, `attempts.review` and `attempts.retry`, `passes.verify`, `handover.begin` and `handover.confirm`, `operations.configure`, `inventory.addStock`, `quests.updateSettings`, `tokens`, `grants`, `team` and `identityIntegrations`. The new `members.lookup({ provider, subject })` (`GET /management/v1/members/lookup`, `read` permission) finds the one member of the project holding exactly that account, and fails with 404 `member-not-found` or 409 `account-ambiguous`.

  Breaking: every failed call rejects with a `ManagementError` whose `kind` (`http`, `network`, `timeout`, `aborted`, `invalid-response`, `client`) separates a server refusal from a lost or unreadable answer and from input rejected before sending. It keeps the server's `message` and any `code`, including codes this version does not know, and reports `uncertain`, with `operationId` (the `actionId` it sent) and `recovery`, when a write may have been applied. It extends the SDK's shared `RequestError`; `isManagementError()` recognizes it. `CreationError` is removed. Calls end after 30 seconds by default; every method takes `{ signal, timeout }` as its last argument, and the client takes a default `timeout`. To upgrade, see [Catch `ManagementError` instead of `CreationError` or a status property](https://console.domino.run/docs/reference/upgrade-to-v1#catch-managementerror-instead-of-creationerror-or-a-status-property).

  Breaking: `member`, `people`, `unlinkMemberWallet` and `setMemberWalletSignIn` move to `members.get`, `members.people`, `members.unlinkWallet` and `members.setWalletSignIn`. See [Member methods moved under `management.members`](https://console.domino.run/docs/reference/upgrade-to-v1#member-methods-moved-under-managementmembers).

#### Patch Changes [#patch-changes-143]

* @domino-sdk/relay\@1.0.0-next.10

### 1.0.0-next.9 [#100-next9-1]

#### Minor Changes [#minor-changes-172]

* cf5b02c: Custom tracker records have a declared data schema. `customSource(namespace, { data })` requires a zod object schema (`z` is exported by `@domino-sdk/relay/authoring`), stores its JSON Schema form with the tracker, and throws, naming the field, for what ingestion cannot check (string formats and patterns, tuples, refinements, transforms, dates, bigints). Publication refuses a custom tracker without one. Ingestion rejects a batch whose record data does not match with 422 and `{ error, code: "invalid-record-data", record, path, reason }`, one too costly to validate with `record-data-too-complex`, and records for a tracker published without a schema with `tracker-schema-required`. Schemas are bounded (32 KB, 16 levels, 256 properties, required keys, enum values or key names per node, 16 branches per union, no wide union nested in another union's branch), and validation charges every unit of work to a per-record and per-batch step budget. A deployment whose new schema breaks a live quest's `where` filter, Console quests included, is refused. Stored records are not rechecked: tracker triggers skip non-matching records with the new skip code `invalid-record-data`, evaluators reject pinned records that do not match the schema the quest was built with, and `trackers.records(handle)` leaves them out and counts them in `excluded`. The schema can change under the same tracker key and applies to later records.

  The data type flows into quests: `ctx.tracker` is non-null in a tracker-triggered quest's evaluator with `record.data` of the tracker's type (and `ctx.content` and `ctx.observation` are non-null for the triggers that supply them, `null` otherwise), `trackers.records(tracker)` returns typed records, and `where` paths, `equals` values and `{ setting }` bounds are type-checked against the data and the quest's integer settings. `where` also accepts a list of conditions that must all hold, and `automatic()` and publication check filter paths against the stored schema. `settings.object` and `defineQuest` accept only `settings.*` builders; a plain zod setting is a type error and `defineQuest` throws `Setting "<key>" is not a quest setting`.

  See [Custom sources declare their record data](/docs/reference/upgrade-to-v1#custom-sources-declare-their-record-data) and [Quest settings come only from settings builders](/docs/reference/upgrade-to-v1#quest-settings-come-only-from-settings-builders) in the upgrade guide.

#### Patch Changes [#patch-changes-144]

* Updated dependencies \[cf5b02c]
  * @domino-sdk/relay\@1.0.0-next.9

### 1.0.0-next.8 [#100-next8-1]

#### Major Changes [#major-changes-23]

* 54fcf41: The participant client follows one convention for paging, sessions and errors.

  * Every method takes an optional last argument, `{ session?, signal? }`. `session` replaces the positional session argument of `wallets.challenge`, `wallets.verify` and `me.collectionPass`. Calls Relay binds to a session (wallet linking, collection passes, `quests.retry`) read the current session when `session` is omitted.
  * Every paged read takes `{ cursor? }` and returns `{ items, nextCursor }`: `referrals.history`, `content.admissions`, `content.qualifications`, `content.performance`, `engagement.page` and `engagement.boundaries` changed from `before`, `after` and `next`. The participant routes answer `nextCursor` and read `cursor`, and so do the management routes and client methods that share them (`management.referrals.history(member, { cursor })`, `management.content.*`, `management.engagement.page` and `boundaries`).
  * Every rejection is a `RelayError`, which extends the new shared `RequestError`: `kind` (`http`, `network`, `timeout`, `aborted`, `invalid-response`, `client`), `uncertain` (true only for a write that may have taken effect: a failure after sending, a cut-off body, a 500, 502 or 504, or an unreadable success answer), `status` whenever response headers arrived, an open string `code` (known ones in `RelayErrorCode`), `operationId`, and `recovery` (`replay` with an `operationId`, else `reconcile`) when uncertain. `isRelayError` and `isRequestError` recognize these errors across duplicate copies of the package. `RelayError.status` is now `number | undefined`. Invalid input rejects with `invalid-input` instead of a `ZodError`, a success answer this SDK cannot read with `invalid-response`, a body cut off after its headers with kind `network` and its status, and a wallet signer's own failure with `wallet-signer`. The error body is read field by field, so Relay's message and a code the SDK does not know are always kept. Relay now sends `code: "not-signed-in"` and `code: "stale-session"` on those participant errors, and every paged read answers a malformed cursor with 400, code `invalid-cursor`, and one message.
  * Removed: `submit`, `retry(id)`, `progress(member)`, `upload(member, file)`, `auth.connections`, `me.linkPass`, the `wallet.*` namespace, and the staff calls the participant API refused (`lookupMember`, `snapshot`, `publish`, `publishBatch`, `beginHandover`, `confirmHandover`, `decide`, `journeyTemplate`, `template`).
  * Parameters take Zod input types, so defaulted fields are optional, and cursors are opaque strings the server validates. The session the client returns (`ParticipantSession`) accepts fields and principal kinds a newer Relay adds; `sessionMember(session)` reads its member.
  * Hosted proxies no longer forward the 0.x single-wallet routes (`/v1/me/wallet`, `/challenge`, `/verify`).

  Upgrade: [One paging, session and error convention in the participant client](https://console.domino.run/docs/reference/upgrade-to-v1#one-paging-session-and-error-convention-in-the-participant-client) in the SDK v1 upgrade guide maps every method and has search commands.

#### Patch Changes [#patch-changes-145]

* Updated dependencies \[54fcf41]
  * @domino-sdk/relay\@1.0.0-next.8

### 1.0.0-next.7 [#100-next7-1]

#### Patch Changes [#patch-changes-146]

* 1084288: `@domino-sdk/relay` now exports only the participant runtime client: `createRelayClient`, `RelayError`, `QuestUpdatedError`, `formatUnits`, `parseUnits`, and the types the client returns. Schemas, builders and avatar helpers moved to their own entry points, a few implementation helpers are no longer public, and two are no longer exported at all, so an app that imports the client no longer loads authoring builders, deployment and management schemas, or DiceBear.

  * Zod schemas (and helpers such as `identifier`, `caip10` and `xPostId`) come from `@domino-sdk/relay/schema`.
  * Builders (`defineTracker`, `defineLeaderboard`, `defineReferral`, `defineEngagementReward`, `content`, `perContent`, `streak`, `settings`, `discord`, `pointLedgers`, `x`, `customSource`) and evaluator reader types (`TrackerReader`, `XReader`, `ImageReader`) come only from `@domino-sdk/relay/authoring`.
  * `avatarSrc`, `defaultAvatar`, `defaultAvatarSvg` and `walletAvatarSeed` come from `@domino-sdk/relay/avatar`, and no longer from `@domino-sdk/relay` or `@domino-sdk/relay/react`.
  * No longer public: `identityIntegrationSchema`, `identityIntegrationInputSchema`, `identityIntegrationSecretSchema`, `participantExchangeSchema`, `questConfiguration`, `parseImageResult`, `parseImageExtraction`, `isContentParticipantRoute`, `isProfileParticipantRoute`, `isWalletParticipantRoute` and `isWalletSignInRoute` are exported only from `@domino-sdk/relay/internal`, which Relay's own services share and which is not a supported API.
  * No longer exported: `questActions` and `reconcileQuestDraft`. Open a quest controller with `relay.quests.open(view)`; its `quest-updated` result already carries the reconciled `draft`.
  * The package declares `sideEffects: false`.
  * `esbuild` is an optional peer dependency, needed only by `@domino-sdk/relay/build`. `@domino-sdk/relay-cli` depends on it directly, so `domino build` and `domino deploy` work as before. Code that calls `buildQuest` without the CLI must install `esbuild`.

  Upgrade: [Runtime entry exports only the client](https://console.domino.run/docs/reference/upgrade-to-v1#runtime-entry-exports-only-the-client) in the SDK v1 upgrade guide has the import mapping and search commands.

* Updated dependencies \[1084288]
  * @domino-sdk/relay\@1.0.0-next.7

### 1.0.0-next.6 [#100-next6-1]

#### Patch Changes [#patch-changes-147]

* 1019218: Console **Audit history** now records every point ledger created in Console or with `management.points.createLedger`, as **Point ledger created**, with the ledger's key and name, the environment and who created it. Before, only edits were audited.
* Updated dependencies \[8b706af]
  * @domino-sdk/relay\@1.0.0-next.6

### 1.0.0-next.5 [#100-next5-1]

#### Major Changes [#major-changes-24]

* fa16503: The CLI's automation contract is frozen for v1. Every failure has a stable error `code`, and the exit status says what kind of failure it was: 0 success, 1 failure, 2 usage error (was 1), 3 a change that may have taken effect without confirmation (was 1), and 130 cancelled by SIGINT or a prompt, 143 by SIGTERM (was 1). With `--json`, the error on stderr's last line always has `error`, `code` and `outcome`, plus `status`, `apiCode`, `details` or `operation` when they apply; Commander's `commander.*` codes are gone. An uncertain `deploy` publication or `stage` build reports an `operation` whose `resume` arguments pin the full scope and finish it from any directory, announced as a JSON `operation` event on stderr right before the request is sent. `deploy --resume` and `stage --request-id` now require that scope as flags and need no checkout. `doctor`, `check` and `migrate` also write their failure to stderr. The undocumented `domino GET /path` spelling is removed; use `domino api GET /path`. `domino help` with an unknown command fails with `UNKNOWN_COMMAND`. `domino dev` is now listed under **Experimental commands**. Result and error JSON Schemas, fixtures, a command inventory and `cli/contract/index.mjs` (the zod schemas by name) ship in `cli/contract/`, and `domino reference automation` explains them. To upgrade, see [CLI errors have stable codes and exit statuses](https://console.domino.run/docs/reference/upgrade-to-v1#cli-errors-have-stable-codes-and-exit-statuses) and [Call the management API with `domino api`](https://console.domino.run/docs/reference/upgrade-to-v1#call-the-management-api-with-domino-api).

#### Patch Changes [#patch-changes-148]

* @domino-sdk/relay\@1.0.0-next.5

### 1.0.0-next.4 [#100-next4-1]

#### Minor Changes [#minor-changes-173]

* d7f9fe4: `streak(...)` now completes manual quests and tracker-triggered quests (`automatic({ tracker })`), not only automatic observation quests. Each accepted attempt counts the community day of its trusted occurrence time, the time `ctx.eligibleAt` reports: a tracker record observation's `occurredAt` (such as an X post's publication time), or, for a manual quest, when Relay admitted the submission, so a manual streak counts submission days. `defineQuest` no longer throws "Streaks require an automatic observation trigger" or "Tracker triggers complete once() or keyed()", and publication accepts these quests. Content quests and prerequisite-only `automatic()` quests still cannot be streaks, and a streak quest keeps its trigger kind (manual, observation or tracker): changing it needs a new quest key. Once a member's tracker streak milestone pays, the trigger stops admitting that member's observations for the quest, as for a completed `once()` quest. `questStatus` shows a member's own streak submission first while it is checking, in review or failed; a rejected submission keeps the "N / M days" progress and gives the reason as its detail. The new `packages/sdk/examples/posting-streak-quest.ts` pays members who post on X five days in a row. Nothing existing needs to change, so the v1 upgrade guide has no section for this.

#### Patch Changes [#patch-changes-149]

* Updated dependencies \[d7f9fe4]
  * @domino-sdk/relay\@1.0.0-next.4

### 1.0.0-next.3 [#100-next3-1]

#### Major Changes [#major-changes-25]

* a803b30: Evaluators now replay deterministically. Hosted evaluation freezes the clock for each attempt and seeds randomness from the attempt, so a retried or replayed attempt makes the same decision and `keyed()` completion key. This applies to every evaluation Domino runs, including releases published with earlier SDK versions.

  * `ctx.now` is new: the attempt's evaluation time in epoch milliseconds, the same on every run of the attempt. `keyed()` receives it too. It is distinct from `ctx.eligibleAt`, which is when the activity happened.
  * `Date.now()`, `new Date()` and `Date()` return `ctx.now` and do not advance, even across awaited capability calls. `performance.now()` returns 0.
  * `Math.random()`, `crypto.getRandomValues()` and `crypto.randomUUID()` repeat the same sequence for one attempt and differ between attempts.

  **Who should act.** Evaluators that measure elapsed time or rely on unpredictable random values. Use `ctx.now` for "as of evaluation" checks and `ctx.eligibleAt` for when the activity happened. See [Evaluators see a frozen clock and seeded randomness](https://console.domino.run/docs/reference/upgrade-to-v1#evaluators-see-a-frozen-clock-and-seeded-randomness) in the v1 upgrade guide.

#### Patch Changes [#patch-changes-150]

* bd960a4: Only a person signed in to Console can create or edit a point ledger in live. `management.points.createLedger` and `management.pointLedgers.save` with an API key or a CLI sign-in now get 403 "Approve live publication in the console" in live, as publication already did; in test they work as before. To upgrade, see [test and live](https://console.domino.run/docs/concepts/environments). Live operator controls stay open to a personal API key with `publish`: operation pauses (`POST /management/v1/operations`), `management.referrals.control`, `management.leaderboards.exclude` and `group`, and `management.trackers.control`. Console **Audit history** now lists leaderboard exclusion, group and tracker control changes with the key's owner as the actor. The agent references and `capabilities.json` describe both rules.
* Updated dependencies \[a803b30]
* Updated dependencies \[7e37049]
  * @domino-sdk/relay\@1.0.0-next.3

### 1.0.0-next.2 [#100-next2-1]

#### Minor Changes [#minor-changes-174]

* c320586: A content quest's `performance.evaluate` can no longer select rewards. It gates the rewards the content evaluation already selected, and it always did: a `rewards` value in its result was silently ignored. Its type is now `PerformanceOutcome` (`accept`, `review` or `reject` with a reason only), so `decision.accept({ rewards })` and `decision.review({ reason, rewards })` are type errors there, and a runtime result that carries `rewards` fails the evaluation with "A performance evaluator cannot select rewards". To change what a post earns, select rewards in the quest's `evaluate`.

  See [Performance evaluators no longer select rewards](/docs/reference/upgrade-to-v1#performance-evaluators-no-longer-select-rewards) in the upgrade guide.

* c320586: A prerequisite is the quest key alone. `completed(quest)` returned `{ quest, scope: "ever" }`, and `scope` was declared but never read: a prerequisite has always meant a required quest completion from any prior occurrence, with no same-period variant. `completed()`, the quest description schema (`requires[]`) and published releases no longer carry `scope`. Releases stored with it still read, and the field is dropped. Code that reads `requires[].scope`, or builds `{ quest, scope: "ever" }` by hand, should use `{ quest }` or `completed(quest)`.

  See [Prerequisites are typed eligibility rules](/docs/reference/upgrade-to-v1#prerequisites-are-typed-eligibility-rules) in the upgrade guide.

#### Patch Changes [#patch-changes-151]

* c320586: `defineQuest` throws at build time for two shapes that only failed when publishing: a bare `automatic()` quest with no `requires` entry, and a bare `automatic()` quest with `keyed()` completion. A bare `automatic()` runs once its prerequisites are complete, so it needs at least one `completed(quest)` and completes once. The backend still checks both when it publishes.
* c320586: `manual({ actor: "staff", input })` throws instead of dropping the actor. Its types never allowed the combination, but untyped callers got a quest that members could submit evidence to, not one staff confirm. Staff-captured evidence is not supported: use `manual({ actor: "staff" })` for a staff confirmation, or `manual({ input })` for evidence the participant submits.
* Updated dependencies \[c320586]
* Updated dependencies \[c320586]
* Updated dependencies \[c320586]
* Updated dependencies \[c320586]
  * @domino-sdk/relay\@1.0.0-next.2

### 1.0.0-next.1 [#100-next1-1]

#### Patch Changes [#patch-changes-152]

* e635fe2: Troubleshooting reference: when a build or typecheck fails on an API that no longer exists after an upgrade, read the Upgrade to Relay SDK v1 guide.
* @domino-sdk/relay\@1.0.0-next.1

### 1.0.0-next.0 [#100-next0-1]

#### Major Changes [#major-changes-26]

* 5d4ec3e: Start the SDK v1 prerelease. Relay SDK and CLI v1 is one breaking release, 1.0.0; until it ships, breaking changes publish as `1.0.0-next.N` on the npm `next` dist-tag. `latest` stays on the 0.x release, so `npm install @domino-sdk/relay` and new starters keep using 0.x.

  **Who should upgrade.** Install `@domino-sdk/relay@next` and `@domino-sdk/relay-cli@next` together only to try v1 before it is stable. Prereleases can break each other: pin an exact `1.0.0-next.N` version with `npm install --save-exact @domino-sdk/relay@next @domino-sdk/relay-cli@next` (or `pnpm add -E @domino-sdk/relay@next @domino-sdk/relay-cli@next`), and upgrade the SDK and CLI to the same version.

  **Upgrade steps.** Every breaking change in the prerelease adds a section to the SDK v1 upgrade guide in the Domino docs, and its changelog entry links to that section. Once 1.0.0 is published, stable APIs follow semantic versioning; experimental APIs are only in namespaces marked experimental.

#### Patch Changes [#patch-changes-153]

* Updated dependencies \[5d4ec3e]
  * @domino-sdk/relay\@1.0.0-next.0

### 0.31.1 [#0311-1]

#### Patch Changes [#patch-changes-154]

* f76abb2: Update the agent references and capability descriptions for verified identities, account-reply and kudos tracker quests, and ranking an external score with a dedicated point ledger.
* @domino-sdk/relay\@0.31.1

### 0.31.0 [#0310-1]

#### Minor Changes [#minor-changes-175]

* 3d0bee0: Tracker triggers are composable primitives: a firing mode, an optional data filter, and the account role a quest pays, over records whose accounts can be any verified identity.

  * `automatic({ tracker, on?, where?, beneficiary? })`. `on` is `"new-record"` (default for X trackers), `"new-observation"` (default for custom trackers) or `"data-changed"` (a new record, or `data` that differs from the previous observation). `where` is one condition on a path in the record's `data`: `{ path, equals }`, or `{ path, gt?, gte?, lt?, lte? }` comparing a number or an integer string exactly, with bounds that are numbers or `{ setting: "name" }` (an integer setting of the quest). Observations the mode or filter does not select record nothing; `ingest` counts those the filter dropped in `admissions.filtered`, present when nonzero.
  * `beneficiary` is an account role: `"author"` (default), `"inReplyTo"` on X trackers, or a role a custom source declares. A record never pays its author's member through another role: when the beneficiary account is the author's, or both belong to the same member (the same wallet on another chain, or another account the member linked), the observation is skipped with `beneficiary-is-author`.
  * `customSource(namespace, { identities, accounts? })`: record authors and named accounts are verified identities of the listed kinds, `"wallet"`, `"x"`, `"discord"` or `"member"`. Records carry `author: { id, kind? }` and `accounts: { <role>: { kind?, id } }`; `kind` may be left out when the source lists one kind. Record reads return them as ingested, and the records `member` filter matches authors of every declared kind. The ingesting server names the accounts, so keep ingest keys on trusted servers.
  * Trigger outcomes carry a stable skip `code` (`trackerSkipCodes`) beside the human `reason`, and `trackers.outcomes` filters by `code`. An X post without an author is no longer reported as "Post is not a reply".
  * `triggerMode(policy)`, `beneficiaryRole(policy)` and `sourceIdentities(source)` read the mode, role and identity kinds of published policies and trackers, including those stored by earlier SDKs.

  **Compatibility.** Existing data keeps its stored shape on the wire, so SDKs and CLIs 0.25 to 0.27, and backend pods of the previous release during a rollout, keep reading it. New keys appear only where an author uses a new option:

  * `automatic()` leaves `on` out when it is the source's default, so default quests publish the same policy as before. `on` (non-default), `where`, roles other than `author` and `inReplyTo`, `identities` on new trackers, `accounts`, `author.kind`, and `discord` or `member` attribution are rejected by older SDKs, whose schemas are strict: upgrade apps that read those trackers and quests.
  * Trackers published with `authors: "wallet"` read back as stored. Republishing one as `identities: ["wallet"]` is the same tracker: it keeps its key and stored definition.
  * Reply quests published with `beneficiary: "replied-to"` keep it: reads return it as stored, and their evaluators keep receiving `beneficiary: "replied-to"`, so they keep working until republished. Republishing with this SDK switches them to `inReplyTo`.
  * Trigger outcomes always carry `code`: `trackers.outcomes` needs this SDK.

  **Breaking changes.** For code written against the tracker triggers of 0.25.0 to 0.27.0:

  * Replace `customSource(namespace, { authors: "wallet" })` with `customSource(namespace, { identities: ["wallet"] })`. `authors` stays readable on published trackers and is deprecated.
  * Replace `automatic({ tracker, beneficiary: "replied-to" })` with `beneficiary: "inReplyTo"`.
  * `TrackerBeneficiary` and `trackerBeneficiarySchema` are removed; the beneficiary is a role string, or `"replied-to"` on releases published before roles.

  **Upgrading.** Deploy the backend (migration `0074_tracker_trigger_primitives`) before publishing quests that use the new options; older servers refuse them.

#### Patch Changes [#patch-changes-155]

* Updated dependencies \[3d0bee0]
  * @domino-sdk/relay\@0.31.0

### 0.30.0 [#0300-1]

#### Minor Changes [#minor-changes-176]

* c6afc57: The EVM chains `ctx.wallet.balance()` reads are the platform's list, not an SDK export. Adding a chain no longer needs an SDK release.

  * `WalletBalanceQuery.chain` for EVM is any CAIP-2 ID, typed `` `eip155:${number}` `` (new `EvmChainId` type). New `isEvmChainId(value)` (also from `@domino-sdk/relay/authoring`) and `evmChainIdSchema` check the syntax (`eip155:` and a chain ID in decimal, no leading zeros); the evaluator sandbox refuses anything else before reading.
  * The platform decides which chains it reads: the EVM networks `management.walletBalanceChains()` lists, generated from dRPC's chain list. On any other EVM chain the evaluation fails with 422 "Relay cannot read balances on eip155:<chain ID>: it is not a supported EVM chain. …".
  * New `management.walletBalanceChains()` (`GET /management/v1/wallet-balance-chains`, `read` permission) returns `{ chains: [{ chain, name, testnet }] }`: Solana mainnet, then each EVM chain the platform reads, by chain ID. Console names the chains of a member's balance checks from it.

  **Breaking.** `evmChains`, `evmChainSchema`, `EvmChain` and `EvmChainInfo` are no longer exported from `@domino-sdk/relay`.

  **Upgrading.** Quest code that passes a string literal such as `"eip155:8453"` compiles unchanged. Replace `EvmChain` with `EvmChainId`, `evmChainSchema` with `evmChainIdSchema` (it checks syntax, not support), and chain-name lookups in `evmChains` with `management.walletBalanceChains()` from your server.

* c6afc57: `ingest` is a normal management permission: it sends records to custom trackers (`management.trackers.ingest`) and allows nothing else.

  * Editors hold `ingest`, and organization administrators hold every permission, so `management.access()` lists it for them. `rolePermissions.editor` includes it.
  * Personal API keys can carry it. In Console, **Settings → Keys → Personal API keys** now lets you choose a new key's permissions (all of yours by default) and lists each key's permissions; select only **Ingest** for an indexer. `POST /management/v1/tokens` accepts `ingest` for callers who hold it.
  * Hosted-app keys (`dhk_`) now list `ingest` in the minted key, the development key and `access()`, alongside `read` and `points`. They always held it.
  * `observe` still allows ingestion, so keys issued with only `observe` keep ingesting. A caller with neither gets 403 "Missing ingest permission" (before: "Missing observe permission").
  * `domino dev` needs your `read`, `points` and `ingest` permissions for its development key; editors have all three.
  * `tokenSchema` (the personal API key list) drops permissions this release does not know instead of rejecting the list, as `access()` already does.

  **Upgrading.** The `domino` CLI reads `/access` and its development key without checking permission names, so every CLI version keeps working. Code that calls `management.access()` from `@domino-sdk/relay-cli` before 0.25 rejects a response that lists `ingest`, which now happens for every editor and administrator: upgrade `@domino-sdk/relay-cli` there. 0.25 and later drop permissions they do not know.

#### Patch Changes [#patch-changes-156]

* Updated dependencies \[c6afc57]
* Updated dependencies \[c6afc57]
  * @domino-sdk/relay\@0.30.0

### 0.29.0 [#0290-1]

#### Minor Changes [#minor-changes-177]

* 28f8bcf: Participant profiles are primitives: Domino serves the profile and separate reads keyed by it, and your app composes the profile page. Each read checks the profile's visibility and then its own resource's access.

  * `relay.profiles.get(id)` returns the profile only: `id`, `name` and `avatar` (`profileSummarySchema`).
  * `relay.leaderboards.placement(board, profileId)` (`GET /v1/leaderboards/:board/profiles/:profile`) returns `rank`, `score` and the ranked `total` on one board, with `rank` and `score` null while the member is unranked. It needs the board's access, like `standings`. New `leaderboardPlacementSchema`.
  * `relay.profiles.completions(id, { limit, cursor })` (`GET /v1/profiles/:profile/completions`) returns completions grouped by quest, most recent first, paged with `nextCursor`. Only quests that opt in appear: `defineQuest({ profile: { visible: true } })`, hidden by default; the quest's active release decides, and archived quests do not appear. New `profileCompletionPageSchema`, `profileCompletionQuerySchema` and `questProfileSchema`; quest descriptions gain an optional `profile`, which `defineQuest` includes only when the quest sets it.
  * `relay.profiles.referral(id, program)` (`GET /v1/profiles/:profile/referrals/:program`) returns `invited`, `qualified` and `milestones` in one program, for signed-in viewers (401 when signed out). New `profileReferralSchema`.
  * React: `useLeaderboardPlacement(board, id)`, `useProfileCompletions(id, { limit })` with `loadMore()`, and `useProfileReferral(id, program)`. `useProfile(id)` returns the profile only.
  * Profile settings: `defaults` is an ordered list of the accounts that fill in names and pictures, `"x"` and `"discord"` (`[]` for generated). `profileDefaultProviderSchema` lists them.
  * Generated avatars are seeded by an HMAC of the member under the project's secret (`relay.me.get().avatarSeed`), no longer by the member's first wallet: linking a wallet never changes an avatar.
  * `proxyParticipant` and the API's scope selector admit the new routes (`isProfileParticipantRoute`).
  * Console: **Participant profiles** settings show who sees profiles and the default accounts, with arrow buttons to order them. The member's **Community profile** appears only when profiles are visible or the member or an operator changed the profile.

  **Upgrading.** Breaking for apps, servers and CLIs on 0.25 to 0.27 that use profiles:

  * `relay.profiles.get(id)` no longer returns `self`, `leaderboards`, `quests` or `referrals`, and `publicProfileSchema` and `PublicProfile` are removed. Read each part with the calls above. For `self`, compare `id` with `relay.profiles.mine().id`. `useProfile(id)`'s `ready` state holds a `ProfileSummary`.
  * Quest completions no longer appear anywhere until the quest opts in: add `profile: { visible: true }` to the quests whose completions profiles may show, and publish.
  * Referral progress is per program and needs a signed-in viewer. `attributed` is now `invited`, returned with each program; it still counts the member's invitations across the whole project, not only that program's.
  * `management.profiles.configure()` takes `defaults: ["x", "discord"]` instead of `"accounts"`, and `[]` instead of `"generated"`; `sections` is removed and refused. `settings()` returns the list. Stored settings migrate: `accounts` becomes `["x", "discord"]`, `generated` becomes `[]`.
  * Members who linked a wallet get a new generated avatar, the one members without a wallet already had. Uploaded and account pictures do not change.
  * SDK and CLI 0.25 to 0.27 cannot parse the new profile and settings responses: `relay.profiles.get()` and `management.profiles.settings()` throw until the app and CLI upgrade. Deploy the backend first; its migration only adds the new setting, so pods of the previous release keep working during the rollout.
  * Upgrade the CLI and finish rolling out the backend before publishing a quest with `profile: { visible: true }`. Only quests that set `profile` carry the key in their release. CLIs from 0.25 to 0.27 refuse deploy and preview responses that include such a release (`Unrecognized key: profile`), and pods of the previous backend release cannot read it, including after a rollback. Quests without it publish and roll back as before.

#### Patch Changes [#patch-changes-157]

* Updated dependencies \[28f8bcf]
  * @domino-sdk/relay\@0.29.0

### 0.28.0 [#0280-1]

#### Minor Changes [#minor-changes-178]

* 30dda1d: Add `useSignInOptions({ cosmos })` to `@domino-sdk/relay/react`: every way a participant can sign in, as one flat list. It holds the configured OAuth providers, the installed wallets of the families the project enabled, and email. Each option has an `id`, a `label`, a `kind` and an `icon` hint. OAuth and wallet options sign in with `start()`, and the email option with `sendCode(email)` then `verifyCode(code)`. Cosmos wallets are offered only on the chains the app passes as `cosmos`. `unavailable` explains the gaps with a machine-readable `reason`: `cosmos-chains-missing` when the app named no chains, `cosmos-chains-invalid` when a chain in `cosmos` is invalid (such as `"cosmoshub-4"` without `cosmos:`; the other methods still load), and `no-wallet-installed` with a message for the participant. OAuth and email are offered as soon as the methods are read; installed wallets join them when discovery finishes. The options keep their identity across renders, and their actions never reject: failures land in `error`. `methodsError` says when the methods could not be read and the list fell back to email. Apps without React get the same list from `signInOptions(relay, { cosmos })` in `@domino-sdk/relay/browser`, whose actions reject on failure; `@domino-sdk/relay/browser` also exports the option types, `discoverSignInWallets`, `CosmosChains` and `SignInWallet`.

  `useSignIn()` actions now keep their identity across renders, and `verifyCode` always uses the latest code sent; `sendCode` forgets the previous code first. `useSignInMethods()` returns `error` when it fell back to email. Installed wallets with the same name and family get a number in their label. `cosmosChain()` now rejects a chain object without a string `chainId`.

  The starter apps now build their sign-in on the hook, and no longer export `COSMOS_CHAINS`. To offer Cosmos wallets in an app made from a starter, pass your chains to the `useSignInOptions()` call in `src/components/sign-in.tsx`. In development the starters show a note when the project takes Cosmos wallets and the app names no chains, or an invalid one, and no longer tell participants the project has no sign-in method in that case. `useSignInMethods()`, `useSignIn()` and `useSignInWallets()` keep working.

  Management member accounts now carry `kind`: `wallet` for wallet identities and `account` for the rest. Responses from older servers read as `account`.

#### Patch Changes [#patch-changes-158]

* Updated dependencies \[30dda1d]
  * @domino-sdk/relay\@0.28.0

### 0.27.0 [#0270-1]

#### Minor Changes [#minor-changes-179]

* 9ef2629: Cosmos wallets work on any Cosmos SDK chain your app chooses. Relay no longer ships a list of Cosmos chains.

  * A Cosmos wallet is `cosmos:<chain ID>` (a CAIP-2 reference: 1 to 32 letters, digits, `-` or `_`) and a bech32 address with any prefix, checked for its checksum and stored lowercase. `caip10()` and wallet tracker authors accept any Cosmos chain ID. A Cosmos wallet is its address: a Cosmos account ID is attributed to the member who links that address, and `trackers.records({ member })` returns the member's Cosmos records under any chain ID. The same key on chains with different prefixes is separate wallets; five Cosmos wallets per member and project, across all chains. 32-byte module and contract addresses cannot link or sign in. Unlinking a wallet and changing its sign-in accept a bech32 address in either case.
  * A Cosmos wallet's `network` is the chain the app requested when linking it. Relay does not verify it: any app can link an address under any `cosmos:<chain ID>`. Quests and servers should gate on the address, such as its bech32 prefix against their own list of chains, not on `network`.
  * Browser: `cosmosSigner(provider, chain)` and `discoverCosmosWallets(chain)` take the app's chain as `{ chainId, chainInfo? }` or `"cosmos:<chain ID>"`. When the wallet answers that it does not know the chain, the signer suggests it with `experimentalSuggestChain(chainInfo)` and retries. Without `chainInfo`, or with a wallet that has no `experimentalSuggestChain`, it throws an error naming the chain. Other errors, such as a rejected request, pass through. New `CosmosChain` type and `cosmosChain()` helper.
  * React: `useWalletDiscovery({ cosmos })` and `useSignInWallets(families, { cosmos })` take the app's chains (one or an array).
  * Wallet sign-in is enabled per family: `"solana"`, `"eip155"` and `"cosmos"`. `relay.auth.providers().wallets`, `participantSignIn()` and `configureParticipantSignIn()` use families, and a per-network value such as `cosmos:cosmoshub-4` is accepted and read as `cosmos`. The rule that refused a Cosmos testnet in the live environment is gone with the chain list. New `walletSignInFamilies` and `walletSignInNetworksSchema`.
  * Console: **Participant sign-in** offers **Cosmos SDK chains**, and member wallets show the chain ID, such as **Cosmos · cosmoshub-4**. Starters offer Cosmos wallets for the chains in `COSMOS_CHAINS` in `src/components/sign-in.tsx`, and say sign-in is not set up when the project enables only Cosmos wallets and `COSMOS_CHAINS` is empty.

  **Upgrading.** Deploy the backend, then the API Worker right after it. The backend's migration turns stored per-network sign-in settings into `cosmos`. Until the API Worker is updated, it checks `ctx.wallet.list()` results against the previous release's two Cosmos networks, so a quest evaluation fails when the member has a wallet linked on any other Cosmos chain.

  * **Projects that enabled Cosmos sign-in must upgrade their app to this release immediately after the backend deploys.** SDK 0.25 and 0.26 read `relay.auth.providers().wallets` against a fixed list of values. After the migration such a project answers `["cosmos"]`, which that list does not contain, so `relay.auth.providers()` throws for every visitor. `useSignInMethods()` then falls back to email alone: every OAuth and wallet sign-in button disappears, not only Cosmos wallets, and the email form appears even where email sign-in is off.

  * CLI and management SDK 0.25 and 0.26 reject the `cosmos` family in `participantSignIn()` and `configureParticipantSignIn()` responses. SDK 0.25 and 0.26 reject a linked wallet on any other Cosmos chain in `relay.wallets.list()` and `management.member()`. Upgrade the CLI with the app.

  * Apps made from a starter declare `NETWORK_NAMES: Record<WalletSignInNetwork, string>` in `src/components/sign-in.tsx` with a key per Cosmos network. After upgrading, `tsc` fails on those keys. Built anyway, the sign-in form of a project that enables Cosmos wallets lists no Cosmos wallet, and crashes when it shows the "Wallet sign-in accepts …" hint, because `NETWORK_NAMES.cosmos` is undefined. Replace the per-network keys with one `cosmos` key, declare your chains, and pass them to `useSignInWallets`:

    ```tsx
    import type { CosmosChain, WalletSignInNetwork } from "@domino-sdk/relay"

    /** The Cosmos SDK chains this app offers wallet sign-in on. */
    export const COSMOS_CHAINS: CosmosChain[] = [{ chainId: "cosmoshub-4" }]

    const NETWORK_NAMES: Record<WalletSignInNetwork, string> = {
      solana: "Solana",
      eip155: "EVM",
      cosmos: "Cosmos",
    }

    // In the sign-in component:
    const wallets = useSignInWallets(methods?.wallets, {
      cosmos: COSMOS_CHAINS,
    })
    ```

  * `cosmosNetworks` is removed, and `CosmosNetwork` is any `cosmos:<chain ID>`. Pass your chain to the adapters instead of looking it up.

  * `useWalletDiscovery()` no longer lists Cosmos wallets on a default chain: pass `{ cosmos: { chainId: "…" } }`. `useSignInWallets(networks)` lists Cosmos wallets only with `{ cosmos }`.

  * `WalletSignInNetwork` is now `"solana" | "eip155" | "cosmos"`; a `SignInWallet`'s `network` is its family, and a Cosmos wallet's chain is `signer.network`.

#### Patch Changes [#patch-changes-159]

* Updated dependencies \[9ef2629]
  * @domino-sdk/relay\@0.27.0

### 0.26.0 [#0260-1]

#### Minor Changes [#minor-changes-180]

* 81693c8: Archive and unarchive quests published from code. Deleting a quest file never archived the published quest, and there was no way to do it.

  * CLI: `domino quests archive <key> --reason "…"` and `domino quests unarchive <key> --reason "…"` (optional `--expected-release <id>`, `--json`). Test environment only; live changes need Console.
  * Management: `quests.archive(quest, { reason, expectedRelease?, actionId? })` and `quests.unarchive(...)` call `POST /management/v1/releases/lifecycle` with `publish` permission, returning `{ quest, key, title, release, lifecycle, changed }`. New `questLifecycleInputSchema` and `questLifecycleSchema`.
  * An archived quest admits no new attempts, leaves collections and no longer counts toward `supportedInteractions`; completions, attempts and rewards stay. Deploying its code again keeps it archived. Unarchiving re-checks the app's supported interactions and the prerequisite graph, and leaves the quest paused while a resource setting is empty.
  * `domino deploy` release summaries now include each release's `lifecycle`.

  **Upgrading.** Deploy the backend first; older servers answer 404 for the new route. No breaking changes.

#### Patch Changes [#patch-changes-160]

* @domino-sdk/relay\@0.26.0

### 0.25.1 [#0251-1]

#### Patch Changes [#patch-changes-161]

* 7420088: Hosted builds with many quests no longer fail with "Build artifact exceeds 20 MB". Quest and catalog code from `.domino-build` is now compressed and staged beside the build artifact. Its limits are 64 MB of JSON and 10 MB compressed, and the dependencies each quest bundles compress well when they repeat. The 20 MB limit now covers only Worker modules and public assets.

  * Contract: `buildArtifactsSchema` accepts `format: 2` artifacts, which carry a `deployment` reference (`stagedDeploymentSchema`) and the catalog's `images` in place of inline `quests` and `catalog`. Format 1 artifacts still parse. `stagedDeploymentContentSchema`, `stagedDeploymentLimits` and `stagedDeploymentKey()` describe the staged object.

  No changes are needed in apps. No breaking changes.

* @domino-sdk/relay\@0.25.1

### 0.25.0 [#0250-1]

#### Minor Changes [#minor-changes-181]

* 6634992: Participants have project profiles: a display name and an avatar that other participants see on leaderboards and profile pages, where the project enables them.

  * `relay.profiles.mine()`, `update({ displayName, avatar, hidden })`, `uploadAvatar(file, { uploadId })` and `get(profileId)`. Uploads accept JPEG, PNG or WebP up to 5 MB and are stored as 512×512 WebP without metadata.
  * React: `useMyProfile()` and `useProfile(id)`; `avatarSrc(avatar)` renders any profile avatar. Generated avatars use the same seed as `relay.me.get().avatarSeed`.
  * Contract: `profileSummarySchema`, `ownProfileSchema`, `publicProfileSchema`, `profileUpdateSchema`, `displayNameSchema`, `profileSettingsSchema`, `profileModerationSchema`, `managedProfileSchema` and `isProfileParticipantRoute()`. Leaderboard rows gain `profile` (null unless the viewer can see it; older servers omit it and it reads as null).
  * Management: `profiles.settings()`, `profiles.configure({ visibility, defaults, sections, expectedRevision, reason })` (publish; in live only from Console) and `profiles.moderate(member, { resetName, removeAvatar, lock, reason })` (review). `member(id)` returns `profile`, and account profiles gain `username`. Console has **Settings → Access → Participant profiles** and a Community profile section on the member.

  **Upgrading.** Nothing is shown until an operator enables profiles: leaderboards keep their aliases and profile pages answer 404. Deploy the backend first; the database migration runs automatically and dates earlier completions. Apps must upgrade the SDK so their `/relay` proxy (`proxyParticipant` or `createParticipantBackend`) admits `/v1/me/profile`, `/v1/me/profile/avatar` and `/v1/profiles/:id`; older proxies answer 404 for them. X and Discord names and pictures fill in when members next sign in or connect those accounts. No breaking changes.

* 531f41e: Members can link several Solana, EVM and Provenance wallets to a project and unlink them.

  * `relay.wallets.list()`, `link(signer)`, `unlink(wallet)`, and the lower-level `challenge()` and `verify()`.
  * Dependency-free signer adapters and discovery in `@domino-sdk/relay/browser`: `evmSigner`, `solanaSigner`, `walletStandardSolanaSigner`, `cosmosSigner`, `discoverEvmWallets`, `discoverSolanaWallets`, `discoverCosmosWallets`.
  * React: `useWallets()` and `useWalletDiscovery()`.
  * Evaluators: `ctx.wallet.list()` returns the member's linked wallets, pinned once per attempt.
  * Management: `unlinkMemberWallet(member, { chain, address, reason })` (requires `review`), and `wallets` in the member detail.
  * Contract helpers: `caip10()`, `cosmosNetworks`, `isWalletParticipantRoute()`, and the `LinkedWallet`, `WalletSigner` and related schemas.

  **Upgrading.** Nothing breaks. `relay.wallet.connection()/challenge()/verify()` and `ctx.wallet.connection()/solanaBalance()` keep working and use the newest linked Solana wallet; linking through `relay.wallet.verify()` still replaces it, while wallets linked through `relay.wallets` sit alongside older ones. The legacy challenge text now uses the sign-in message format, and rate-limited wallet challenges answer 429 instead of 400. Deploy the backend and API Worker before publishing apps that use the new APIs. Apps that proxy through `proxyParticipant` forward `/v1/me/wallets*` after upgrading the SDK. The database migration runs automatically.

* c71712f: Quests can trigger from custom tracker records attributed to members' verified wallets.

  * `customSource(namespace, { authors: "wallet" })`: each record's author is a canonical CAIP-10 account ID, attributed to the member who links that wallet. Ingestion refuses missing or non-canonical authors.
  * `automatic({ tracker })` admits one attempt per new record observation for the attributed member. Evaluators and `keyed()` receive the pinned observation and attribution as `ctx.tracker`; `ctx.eligibleAt` is the record's occurrence time.
  * `management.trackers.outcomes(tracker, query)` explains each admission or skip with its reason; `management.trackers.retryOutcome(tracker, input)` (requires `review`) retries a skipped admission.
  * `management.trackers.records(tracker, { member })` works on wallet trackers, returning the member's linked wallets' records. `trackers.ingest` results include `admissions` counts.
  * New `ingest` permission, granted to hosted-app keys: it allows custom tracker ingestion only. People keep ingesting with `observe`.

  **Upgrading.** Nothing breaks for existing projects. Once a project publishes a tracker-triggered quest, release and progress reads include `trigger.tracker`, which older SDKs reject because the trigger schema is strict: upgrade participant apps before publishing one. Hosted-app keys hold `ingest`, but responses (the minted key, `access()`) keep listing only `read` and `points`, so older CLIs, API Workers and builders still parse them. Permission lists in `access()` projects and hosted-key responses now drop values this release does not know instead of rejecting the response. The database migration runs automatically.

* ea5d69e: Quests can check token balances of a member's linked wallets.

  * Evaluators: `ctx.wallet.balance({ chain, token?, address? })` reads SOL and SPL tokens (including Token-2022) on Solana mainnet, and native and ERC-20 balances on any EVM chain in `evmChains` (the EVM networks dRPC serves, by CAIP-2 ID such as `"eip155:8453"`). It reads all of the member's pinned linked wallets of that family, or one of them, and returns integer amounts with the token's decimals, captured for replay. Each read is one of the evaluation's eight capability calls.
  * Authoring: `parseUnits()` and `formatUnits()` convert whole-token amounts, and `WalletBalanceQuery` and `WalletBalance` type the call.
  * Contract: `evmChains`, `evmChainSchema` and `walletBalanceSchema`.
  * Management: the member detail's `balanceChecks` lists the member's recent recorded balance reads; Console shows them under the member's wallets.
  * Snippet: `token-holder` checks that a member's linked wallets hold at least a minimum.

  **Upgrading.** Nothing breaks. Deploy the backend and API Worker before publishing quests that call `ctx.wallet.balance()`. EVM chains need the platform's `DRPC_API_KEY`; without it, EVM balance reads fail the verification with "EVM balance checks are not configured". Solana reads need no new configuration. `evmChains` is generated from dRPC's chain list: a later release that removes a chain breaks quests that read it, and its changeset will say so.

* e7853fb: Participants can sign in, and sign up, with a Solana, EVM or Provenance wallet where a project enables it, and apps can draw generated avatars.

  * `relay.auth.wallet.signIn(signer)`, and its two steps `start(input)` and `verify(proof)`. `relay.auth.providers()` returns `wallets`, the networks the project enabled.
  * React: `useSignIn().withWallet(signer)`, `useSignInMethods().wallets`, `useSignInWallets(networks)` (the installed wallets that can sign in on those networks), and `useMe()`. The starters offer installed wallets next to email and OAuth.
  * `relay.me.get()` returns the signed-in `member` and an `avatarSeed` derived from their first wallet without revealing it. `defaultAvatar(seed)` and `defaultAvatarSvg(seed)` draw a deterministic avatar (DiceBear Shapes, CC0); `walletAvatarSeed(wallet)` seeds a member's own wallet list.
  * Contract: `walletSignInNetworkSchema`, `walletSignInNetwork()`, `isWalletSignInRoute()` and `memberSummarySchema`.
  * Management: `participantSignIn()` and `configureParticipantSignIn({ wallets })` (publish) read and set the project's wallet sign-in per environment; `setMemberWalletSignIn(member, { chain, address, enabled, reason })` (review) turns sign-in with a member's wallet on or off. Console has both under Settings → Access and on the member.

  **Upgrading.** Nothing changes until a project enables wallet sign-in. Deploy the backend and API Worker first; the database migration runs automatically. `/v1/auth/providers` gains `wallets`, which older SDKs ignore. Apps that proxy through `proxyParticipant` forward `/v1/auth/wallet/*` and `/v1/me` after upgrading the SDK. A platform without OAuth providers or email no longer answers 503 for projects that enable wallet sign-in.

* b22ead4: X trackers can trigger quests, and budgets can reset weekly.

  * `automatic({ tracker })` accepts an X tracker. The quest runs once for each post the tracker collects for the first time; refreshes, metric changes and edits never run it again. `beneficiary: "author"` (the default) pays the member whose selected, verified X account wrote the post; `beneficiary: "replied-to"` pays the member whose selected, verified X account the post replies to. `"replied-to"` on a custom tracker throws.
  * `ctx.tracker.x` carries the captured post (`content` and `evidence`) for X trackers, and `ctx.tracker.attribution.account` can be `{ provider: "x", issuer, subject, revision, linkedAt }`.
  * `budget({ period: "week" })` limits completions or points per ISO week in UTC, starting Monday, by the attempt's eligibility time.
  * New trigger outcome reasons: "Post is not a reply", "Reply is to the author's own post", "No member has linked this X account" and "Captured post is unavailable".

  **Upgrading.** Nothing breaks for existing projects. Release and progress reads that include an X tracker trigger (`source: "x-search"`, `beneficiary: "replied-to"`) or a `period: "week"` budget are rejected by older SDKs, whose schemas are strict: upgrade participant apps before publishing either.

#### Patch Changes [#patch-changes-162]

* 0b5ee21: `domino dev` runs against a Domino API that reports direct preview transport, such as Domino's local development stack, without cloudflared: the preview routes straight to the app's port, so the CLI starts no connector and skips the public reachability check. On such a stack without Workers AI credentials, `domino dev` keeps running on the quests already published instead of stopping. `developmentPreflightSchema` gains `development.transport` (`"tunnel"` or `"direct"`), defaulting to `"tunnel"` for APIs that do not report it, so existing behavior is unchanged.
* Updated dependencies \[6634992]
* Updated dependencies \[531f41e]
* Updated dependencies \[c71712f]
* Updated dependencies \[ea5d69e]
* Updated dependencies \[e7853fb]
* Updated dependencies \[b22ead4]
  * @domino-sdk/relay\@0.25.0

### 0.24.0 [#0240-1]

#### Minor Changes [#minor-changes-182]

* 305ca73: Hosted apps can now have secrets. Add `domino secrets set NAME`, `domino secrets list`, and `domino secrets delete NAME` to manage them in the project's test environment. `set` reads the value from stdin or a hidden terminal prompt, never from arguments, and never prints it. Worker builds (`app.kind: "worker"`) staged after a change receive every secret as a secret binding, replacing an app `vars` entry of the same name; run `domino stage` again to apply a change. Static apps and `domino dev` receive none.

  * The management client adds `secrets.list()`, `secrets.set(name, value)`, and `secrets.delete(name)`, for `GET /management/v1/secrets`, `PUT /management/v1/secrets/:name` with `{ value }`, and `DELETE /management/v1/secrets/:name`. Values are write-only: no route returns them.
  * Worker apps declare the secrets they read in `relay.json` `app.secrets`: `{ "APP_API_KEY": { "description": "…", "required": true } }`. `required` defaults to true. A name cannot be both a `vars` entry and a declared secret, and static apps cannot declare any. `domino stage` warns on stderr about each required declared secret that is not set, with the command that sets it, and adds `missingSecrets` to its result; the build still stages. `domino secrets list` returns the set secrets, the declared ones (the active build's merged with this checkout's `relay.json`), and `missing`. `GET /management/v1/secrets` includes the active build's `declared` secrets.
  * New exports: `appSecretNameSchema`, `appSecretValueSchema`, `appSecretInputSchema`, `appSecretSchema`, `appSecretListSchema`, `appSecretDeclarationSchema`, `appSecretDeclarationsSchema`, the `AppSecretDeclaration` type, and `APP_SECRET_LIMIT`.
  * Names use uppercase letters, digits, and underscores (at most 64 characters, not starting with a digit); `DOMINO_` names and `ASSETS` are reserved. Values are 1 byte to 5 KiB, and a project holds at most 50. Setting and deleting need `publish`, listing needs `read`. Other environments answer 422.

  Upgrade notes:

  * Additive. Existing commands, client methods, and `vars` keep working. Older CLIs reject a `relay.json` that declares `app.secrets`; upgrade the CLI before adding declarations.
  * The commands need a server with this release; older servers answer 404. A server without app secret storage configured answers 503.
  * Move credentials you placed in `relay.json` `app.vars` into secrets and remove them from the repository. The API starter now declares `APP_API_KEY`; set it this way (`domino secrets set APP_API_KEY`, then `domino stage`) to open its protected routes on the staged preview.
  * Refresh installed agent guidance with `domino agents install` after upgrading.

#### Patch Changes [#patch-changes-163]

* @domino-sdk/relay\@0.24.0

### 0.23.1 [#0231-1]

#### Patch Changes [#patch-changes-164]

* e4d0a4f: Fix `domino dev` missing rapid, same-size source edits on filesystems that report identical modification timestamps. Source polling now compares file contents as well as metadata. No configuration or migration is required.
* @domino-sdk/relay\@0.23.1

### 0.23.0 [#0230-1]

*Not published to npm. Its changes first reached npm in 0.23.1.*

#### Minor Changes [#minor-changes-183]

* 49fa0fd: Apps can now match the ledger and tiered reward IDs that reads return to the keys they declared.

  * `relay.pointLedgers.list()` (`GET /v1/point-ledgers`) and the React hook `usePointLedgers()` return each ledger's `id`, `key`, and `name`.
  * `relay.tieredRewards.list()` (`GET /v1/tiered-rewards`) and `useTieredRewards()` return each tiered reward's `id` and `key`.
  * Both reads are open to signed-out visitors. The hooks return the list in `items`. `key` is null for resources created in Console without one.

  Reads name ledgers and tiered rewards by ID. Comparing a referral reward's, account's, or history entry's `balance` with a key such as `"credits"`, or an entitlement's `reward.id` with the key given to `defineTieredReward`, never matched, and apps could show "200 ledger\_… points". Find the resource by key, compare IDs, and display its name or title.

  Quest view rewards are now typed: `{ kind: "points", label, amount, ledger: { id, key, name } }`, `{ kind: "tier", label, reward: { id, key, title }, tier }`, or `{ kind: "integration", label }`. Labels read "10 XP" instead of "10 ledger\_…". Tier labels show the reward title, followed by the tier title when the reward has several tiers, instead of the tier ID.

  Upgrade notes:

  * Code that reads `label` keeps working, and older SDKs ignore the new fields. Replace label parsing with `amount`. If you compared a tier reward's `label` with its tier ID, compare `tier` instead.
  * TypeScript code that builds quest views, such as test fixtures, must add `kind` and that kind's fields.
  * Deploy the updated backend and API Worker before upgrading the SDK: this SDK expects `kind` on quest view rewards. Apps that proxy participant requests with `proxyParticipant` or `handleHostedApp` forward the new routes once they upgrade.
  * No database migration is required.

#### Patch Changes [#patch-changes-165]

* Updated dependencies \[49fa0fd]
  * @domino-sdk/relay\@0.23.0

### 0.22.0 [#0220-1]

*Not published to npm. Its changes first reached npm in 0.23.1.*

#### Minor Changes [#minor-changes-184]

* 024b075: Add `domino feedback` for humans and agents to report tooling issues to Domino's Linear Triage queue. Submit text directly, from a file, or from stdin; use `--dry-run` to review the report and `--json` for automation. The bundled agent skill now instructs agents to report tooling problems and include the issue link in their handoff.

  At task completion, agents also review and report difficulties that better SDK or CLI support could address, including manual work and workarounds during otherwise successful tasks. They skip duplicates and reports with no tooling difficulty to describe.

  Requires Domino authentication and a backend with feedback delivery configured by the operator. Existing commands remain compatible. Refresh installed agent guidance with `domino agents install` after upgrading.

* 2614ad4: Add `ctx.images` for quest evaluators: `inspect`, `compare` (perceptual hash distances), `review` (AI judgement over up to four images), `extract` (typed fields read from a screenshot) and `seen` (other participants' near-duplicate images). Images are named by reference: the attempt's photo evidence, the linked X profile picture, an X post's attached image, or a reference image listed under the new `images` key in `relay.json` (JPEG, PNG or WebP, up to 5 MiB each and 10 per deployment) and published with `domino deploy`, which uploads new files directly to storage and sends only their hashes. New snippets `x-profile-picture`, `unique-photo` and `community-screenshot` install complete quests with operator-tunable similarity thresholds. `ctx.x.post()` results now include attached `media`.

  Additive and backward compatible. Existing quests, including `ctx.review.photo()`, keep working. To use `ctx.images`, rebuild quests with this SDK version; bundles built with earlier versions do not pass it to `evaluate`. Deploying `images` needs a server with this release; older servers ignore the key, and quests that reference those images then fail verification with "Image … is not published".

#### Patch Changes [#patch-changes-166]

* Updated dependencies \[2614ad4]
  * @domino-sdk/relay\@0.22.0

### 0.21.0 [#0210-1]

*Not published to npm. Its changes first reached npm in 0.23.1.*

#### Minor Changes [#minor-changes-185]

* b605061: Let organization admins delete a project or move it to another organization. Add `deleteProject` (`DELETE /management/v1/project`) and `transferProject` (`POST /management/v1/project/transfer`) to the management client, with the `projectDeletion*` and `projectTransfer*` schemas. Both need the project's name as `confirm`. A deleted project stops answering at once. Domino support can restore it for 30 days, after which its data, hosted app, repository and evidence are erased. Only a project without participants can move, and its hosted app keeps working without a redeploy: a hosted app key now decides its organization, so a stale `X-Relay-Organization` from the app is ignored rather than refused. When a project moved, management routes tell members of its new organization where it went (`PROJECT_MOVED`), and the CLI offers to follow it: it signs in to that organization if needed, updates the checkout and retries. `domino login --organization <id>` does the same without a prompt.

#### Patch Changes [#patch-changes-167]

* @domino-sdk/relay\@0.21.0

### 0.20.0 [#0200-1]

*Not published to npm. Its changes first reached npm in 0.23.1.*

#### Patch Changes [#patch-changes-168]

* Updated dependencies \[2ff82fb]
  * @domino-sdk/relay\@0.20.0

### 0.19.0 [#0190-1]

#### Minor Changes [#minor-changes-186]

* cea3ca7: Require sign-in on every staging app. Visitors sign in with Domino Console and need read access to the project in Test, or enter a password that a project owner sets under **Settings → Staging access**. Console's **Open app** links sign in directly. Add the `stagingAccess*` and `stagingLink` schemas for `GET`/`PUT /management/v1/staging/access` and `POST /management/v1/staging/link`. This is a breaking change for scripts that fetch staging URLs without a session. They should mint a link with `POST /management/v1/staging/link` and send the cookie it sets. Staging now reserves the `/__domino/staging-access` path, including for transparent routing.

#### Patch Changes [#patch-changes-169]

* c2041e7: Fix playground and `domino dev` previews crashing with `ENOSPC: System limit for number of file watchers reached`. Quest synchronization now polls source files about once a second and skips dependency, build, and runtime directories before scanning. App hot reload still uses the app's development server. No configuration changes are required; upgrade the CLI and restart affected previews.
* @domino-sdk/relay\@0.19.0

### 0.18.4 [#0184-1]

#### Patch Changes [#patch-changes-170]

* 68a0429: Use published npm packages for new starters instead of bundled SDK and CLI archives. Exports pin both packages to the same release and include a lockfile. Starter provenance now lives in `.domino/starter.json`, separate from legacy archive metadata.

  Doctor reports installed SDK and CLI versions and no longer requires obsolete starter archives. To migrate an existing project, replace Domino's `file:.domino/*.tgz` dependencies with the same exact published npm version, remove their `pnpm.overrides` entries, install, and run the app checks. Then delete the unused package archives and old `.domino/release.json` archive manifest. Preserve unrelated dependencies and overrides.

  The Playground offers **Upgrade with AI** for CLIs older than 0.18.3 or projects with local package pins before launching the preview. Dependencies change only when the owner selects the action.

* @domino-sdk/relay\@0.18.4

### 0.18.3 [#0183-1]

#### Patch Changes [#patch-changes-171]

* da8c475: Fix previews that fail after Vite reports ready. Local and public startup checks now request HTML, so apps with a browser-only page fallback, including the Classic starter, can start successfully. Upgrade the project's CLI and restart the preview; no app configuration changes are required.
* @domino-sdk/relay\@0.18.3

### 0.18.2 [#0182-1]

#### Patch Changes [#patch-changes-172]

* 0b04a5f: Install small Codex and Claude skill loaders that read guidance from the project-local CLI package. New starters point to the same bundled skill from AGENTS.md, and CLAUDE.md points to AGENTS.md. Guidance now follows dependency upgrades without routine skill reinstallation. Agents must re-read the skill after upgrading, and dependencies must be installed before the loader target is available.

  Existing checkouts should upgrade their project-local CLI, run `domino agents install` once, and commit the instruction changes. The installer migrates known unmodified copies, keeps package-backed redirects for old reference links, and adds marked pointers to AGENTS.md and CLAUDE.md while preserving project instructions. It reports customized files before writing; move their project-specific rules to AGENTS.md, reconcile the reported files with the bundled skill, and retry. `domino doctor` distinguishes missing package guidance from copied or customized instructions. Installation results now include an `updated` file list, and new installations create loaders instead of copying all references.

  The bundled hosted workflow teaches agents to start from current `main`, preserve and reconcile resumed work, and integrate changes into `main` before shared staging. The CLI and backend do not yet enforce that branch policy or merge work automatically.

* @domino-sdk/relay\@0.18.2

### 0.18.1 [#0181-1]

#### Patch Changes [#patch-changes-173]

* 2186fe3: `domino dev` no longer breaks participant requests when `--api-url` is Console's management proxy (`https://console.domino.run/relay`). The CLI used to pass that URL to the app as `DOMINO_PARTICIPANT_API`. The participant proxy refuses any upstream with a path, so every `/relay/*` request answered 503 "Participant API is not configured".

  The API now reports its participant origin in `/preflight` (`participantApi`). `domino dev` sends participant requests there and gives the development management key the same origin, as deployed builds receive it. With an older API, the CLI uses `--api-url` if it is already an origin, then relay.json's `apiUrl`. If neither is, it stops with instructions instead of starting a broken preview.

  `domino doctor --remote` has a new `participant-api` check, and reports the resolved origin as `scope.participantApi`. The check fails when no participant origin can be found, or when relay.json `apiUrl` is not that origin, which staged builds rely on. The proxy's 503 message now says to use the API origin rather than a management URL.

  Upgrade note: if relay.json `apiUrl` is set to a management proxy URL, change it to the participant API origin (`https://relay.domino.run`).

* Updated dependencies \[2186fe3]
  * @domino-sdk/relay\@0.18.1

### 0.18.0 [#0180-1]

#### Minor Changes [#minor-changes-187]

* bf84375: Add `trackers.content(reference, { id, observation })` to hosted quest evaluators. The read returns a retained X body and verified account attribution to the attempt's member, captured together for replay. Engagement counters, mutable author profiles, usernames, and verification badges do not determine ownership. Explicit disconnects and ambiguous legacy connections return unlinked attribution.

  Management clients expose the same read with a required `member` in the query and `read` permission. Deploy the updated backend and API runtime before using this method. Existing tracker methods and response shapes remain unchanged. This adds evidence reads, not automatic admission or engagement rewards.

* ac63da2: Add automatic X content quests with `content(tracker, policy)` and `perContent()`, pinned content context, quest enrollment, historical claims and staff approvals, and paginated admission explanations. Existing trigger and execution contracts remain readable. Upgrade the backend and collector workers before publishing content quests; update participant apps to use the new SDK proxy paths. Existing tracker matches are not backfilled, and edits after admission do not reevaluate or reverse prior rewards.

* 0a0ac9a: Add independent daily engagement rewards for qualified X content. Author immutable cumulative formulas with `defineEngagementReward`, publish bundled code with `management.engagement.publish`, and read settlement history through `engagement.page`. Partial budgets retain paid and permanently excluded growth separately.

  Deploy backend migration 0060, the API formula runtime, and workers before publishing engagement rules. Existing quests keep their contracts. This first version supports new discoveries and historical initial baselines; CLI manifest deployment, formula replacement, explicit backfill, enrollment baselines, and automated reversals remain unsupported.

* 40c9761: Add opt-in X evidence references to tracker records. Pass `includeEvidence: true` to receive a stable content revision and a separate provider observation identity. Engagement and author profile refreshes keep the content revision unchanged. Custom records return `evidence: null` when requested; default responses keep their existing shape.

  Deploy backend migration 0056 and the updated API before requesting evidence from the new SDK. The migration backfills retained X observations and captures writes from previous backend versions during rolling deployment. Existing SDK callers need no changes. Automatic content qualification and engagement rewards remain unimplemented.

* b596a0a: Add content-only performance gates with declared engagement metrics and a configurable freshness limit. Content approval stays cached while new samples run performance. A body edit or change to the verified account selection requires fresh content approval. Missing or stale metrics wait without running the callback. The first passing performance decision resolves the content-approved reward selection; later metric changes cannot duplicate awards or retry budget exclusions.

  Use `content.performance({ quest, content, member?, before? })` to read current eligibility and retained metric evaluations. Existing content qualification history remains content-only. Performance callbacks receive the declared metrics, sample time, pinned content, member, and settings. They gate rewards but cannot replace the content-approved selection.

  Apply migration `0059_content_performance` and upgrade the backend, API evaluator, and workers before publishing quests with this stage. Older evaluator runtimes cannot dispatch performance safely. Existing quests need no source changes and omit the new optional fields. Upgrade the SDK to author performance stages or read their history. Automatic reversals and daily engagement settlement remain unsupported.

* e5b7091: Reevaluate admitted X posts after body edits and expose `content.qualifications({ quest, member?, content?, before? })` in participant and management clients. History keeps the original release, beneficiary, and evidence, with qualification state separate from the one-time reward outcome. Stale evaluations and reviews cannot award rewards; later qualifying edits cannot repeat rewards or retry a budget exclusion. Existing admission APIs retain their response shapes. Removal and rejection do not reverse prior awards.

  Apply migration `0058_content_qualification` and upgrade backend and workers together before enabling content quests. Older decision workers cannot enforce stale-content guards during mixed-version deployment. Existing admissions are retained and backfilled; no quest source changes are needed. Upgrade clients to use the new history method.

* 243d9e7: Add explicit `engagement: [{ entry: "engagement.ts" }]` manifest entries, with optional `exportName`, to `domino build` and `domino deploy`. Preview and publish engagement rules with their tracker, qualifying quest, and points balance in one project transaction. Previews report new, unchanged, and changed formulas; formula changes preserve past earnings and budget spend through observed boundaries.

  Resume the saved deployment ID to publish a reviewed preview or recover a committed result. Stale engagement changes reject the entire deployment. Missing entries do not delete rules, and unchanged code does not create another scoring version. New rules use actual publication time as their discovery cutoff.

  Upgrade the backend and settlement workers before deploying engagement entries. Manifests and SDK requests without engagement entries keep their request shape, and existing hosted-build activation receipts remain replayable. CLI publication remains test-only; live previews remain available. Changes to metrics, budgets, or qualifying quests remain unsupported.

* 4cea332: Add preview/apply management workflows for engagement formula changes, immutable version history, and participant boundary history. Each post settles its governing formula through an available fresh eligible observation before the latest requested formula starts at the same counters. Existing budget spend and exclusions survive; superseded unapplied formulas do not invent intermediate earnings.

  Preview totals describe retained accounting, not promised future payouts. Available observations may predate the request by up to the rule's freshness limit. Quest, balance, metrics, baseline policy, freshness policy, and budgets cannot change in this workflow.

  Deploy migration 0061 and upgrade/drain all settlement workers before requesting a formula change. Older workers do not enforce version boundaries. Existing settlement response shapes remain compatible; histories may now include an additional formula-boundary settlement in the same UTC day.

  Engagement creation and formula apply now follow the existing live-publication policy: management keys can publish in test, while live publication requires a Console session.

* 2a7ca88: Add an optional `content` filter to `content.admissions({ quest, content })` for direct post lookup. Management and participant reads retain their existing project and member isolation, response shapes, and unfiltered pagination behavior. Upgrade the backend before sending the new filter; older servers reject it. Existing callers need no changes.

#### Patch Changes [#patch-changes-174]

* Updated dependencies \[bf84375]
* Updated dependencies \[ac63da2]
* Updated dependencies \[0a0ac9a]
* Updated dependencies \[40c9761]
* Updated dependencies \[b596a0a]
* Updated dependencies \[e5b7091]
* Updated dependencies \[243d9e7]
* Updated dependencies \[4cea332]
* Updated dependencies \[2a7ca88]
  * @domino-sdk/relay\@0.18.0

### 0.17.2 [#0172-1]

#### Patch Changes [#patch-changes-175]

* 664eb80: Share current X post content, author verification, and engagement across matching trackers in the same project and environment. Each tracker keeps its own matches and first-seen times. Concurrent collectors coordinate known-post refreshes, and captured quest reads retain their original results.

  Pausing a tracker stops its collection; another active tracker can still update their shared posts. Custom datasets remain isolated per tracker. Existing tracker definitions and SDK calls need no changes.

  Requires backend migration `0055_shared_tracker_content` and the matching backend deployment. The migration retains the previous release's write format for rolling upgrades and preserves existing observation history. Removing the legacy payload columns requires a later contract release after all old workers have stopped. Old workers may still perform duplicate refreshes until the rollout finishes.

* Updated dependencies \[664eb80]
  * @domino-sdk/relay\@0.17.2

### 0.17.1 [#0171-1]

#### Patch Changes [#patch-changes-176]

* ff2c1ee: Remove the packaged project-maps agent reference. Console now draws the interaction map, user flow, and decision matrix from published project state under **Project map**. `domino agents install` no longer adds `references/project-maps.md`; an installed copy can be deleted.
* @domino-sdk/relay\@0.17.1

### 0.17.0 [#0170-1]

#### Minor Changes [#minor-changes-188]

* 052417c: Add packaged agent guidance for creating evidence-backed interaction maps, user flows, decision matrices, and board-ready project views.

#### Patch Changes [#patch-changes-177]

* @domino-sdk/relay\@0.17.0

### 0.16.1 [#0161-1]

#### Patch Changes [#patch-changes-178]

* aedef4f: Fix classic, waitlist, and referral starter compatibility with text and URL submission types. Quiz recovery now reads only quiz attempts, and classic interaction labels cover the expanded SDK union. Supported interactions remain unchanged.
* @domino-sdk/relay\@0.16.1

### 0.16.0 [#0160-1]

*Not published to npm. Its changes first reached npm in 0.16.1.*

#### Minor Changes [#minor-changes-189]

* 3280aa1: Add required or optional text and HTTP/HTTPS URL submissions with configurable text lengths and exact allowed hosts. Evaluators receive captured strings through ctx.input.value. Browser handles and campaign controls preserve the original values across interrupted delivery and evaluation retries. Backend admission enforces the published contract and returns correctable validation errors.

  Submitted URLs never trigger automatic fetching. Use `xPostId` and `ctx.x.post({ postId })` to verify a post through the configured X provider; reads are captured for retries. Participants link X before verification. Compare the returned author ID with `userId` from `ctx.x.connection()` to establish authorship.

  Existing photo and quiz request shapes and saved photo records remain compatible. Upgrade the SDK and CLI together before authoring text or URL quests. Existing participant apps must add controls and declare text and url in supportedInteractions; newly exported campaign starters include both. Deploy the updated backend and API Worker before publishing these quests. Text and URL storage need no additional migration; the integrated X capability requires the X execution-snapshot migration.

* 074c292: Add captured X verification to authored quest evaluators: `x.connection()`, `x.profile()`, `x.post({ postId })`, `x.follows({ userId })`, and `x.reposted({ postId })`.

  Participants link their X accounts through the existing account flow. Platform operators configure the backend's `X_API_WRAPPER_API_KEY`, shared with trackers. Account changes invalidate unfinished reads; retries reuse captured evidence. Existing quests remain compatible. Apply the additive X execution-snapshot migration and deploy the updated backend and evaluator Worker before publishing quests that use the new methods, and update the SDK and installed CLI references.

#### Patch Changes [#patch-changes-179]

* abc6358: Add `ctx.review.text({ text, criteria })` with typed pass, fail, and unclear results for authored quest decisions. Review verified content such as X tracker posts using the existing backend AI configuration. Validated results are captured and reused across retries; provider failures remain errors.

  The capability limits text to 16,000 JavaScript string units and criteria to 2,000, with a 12-second provider timeout. Review text is treated as untrusted evidence. Credentials stay server-side. This additive API requires a backend and evaluator bridge deployment that supports text review before publishing quests that use it. Existing photo quests remain compatible. Combine this capability with `evidence.text()` for submitted text or `evidence.url()` plus verified X post reads.

* Updated dependencies \[abc6358]

* Updated dependencies \[3280aa1]

* Updated dependencies \[074c292]
  * @domino-sdk/relay\@0.16.0

### 0.15.0 [#0150-1]

#### Minor Changes [#minor-changes-190]

* 6c309ea: Hosted apps can call the management API from their own server code without a pasted credential. Every staged Worker build now receives a platform-managed key as the secret binding `DOMINO_MANAGEMENT_KEY`, plus `DOMINO_API_URL`, `DOMINO_ORGANIZATION`, `DOMINO_PROJECT`, and `DOMINO_ENVIRONMENT`. Build a client with `hostedManagementClient(env)` from `@domino-sdk/relay-cli`, which is safe to bundle into a Worker without `nodejs_compat`. The key (`dhk_…`) has exactly `read` and `points` in the project's test environment, acts as `hosted:<repository ID>` rather than a person, and is revoked when a newer build activates or its own build fails.

  `domino dev` now requests a 12-hour development key (it needs your own `read` and `points` permissions in the test environment), exports the same five variables to the dev server process, and revokes the key when it stops. An app's `vite.config.ts` must forward them to the local Worker in the `cloudflare()` plugin's development `vars`; apps created from earlier starters need that addition.

  Upgrade notes: the five `DOMINO_` names are now reserved; app `vars` with those names are replaced by Domino's values. Deploy the backend (migration `0053_hosted_app_keys`) and API Worker before the builder, because the builder fails a Worker deployment when it cannot obtain a key. Older CLIs keep working and simply do not export a development key; a new CLI against an older API prints that no key is available and continues.

* 6c309ea: New projects can start from a choice of starters: Campaign tour (the default), Classic questboard, Waitlist, Referral program, Discord activation, and API. In Console, choose one under **Start from** when creating something new. In a terminal, run `domino create NAME --template waitlist`, or answer the new prompt. Scripts without `--template` still get the campaign tour. `starterTemplates` and `starterIdSchema` in `@domino-sdk/relay-cli` list the offered starters. Projects now report the chosen starter as a top-level `starter` field, and `POST /repository` accepts any offered starter ID. Omit it to use the starter chosen at setup. At creation, Domino writes the project name to the starter's `src/project.json` and places the brief where the starter's `CAMPAIGN.md` marks it.

  `@domino-sdk/relay/react` provides the React layer the starters share:

  * `RelayProvider`, `useExperience`, `questGroups`, `useQuest`, and the quest status helpers
  * `useSignIn`, `useSignInMethods`, `useSignOut`, and `useAccounts`
  * `useLeaderboard`, `useReferralSummary`, `useInvitation`, and `useRelay`

  React 19 is an optional peer dependency. `@domino-sdk/relay/vite` adds `serverBoundary()`, which fails a Vite build when browser code imports `server/` or `@domino-sdk/relay-cli`. `@domino-sdk/relay/portal-proxy` adds `hostedAppRequest` and `handleHostedApp` for the Domino routes of a hosted app's Worker.

  Upgrade notes: nothing breaks for existing projects. Apps built from the earlier campaign starter keep their own `src/relay` copies and can switch to `@domino-sdk/relay/react` hook by hook. Wrap the app in `RelayProvider`, which replaces `ExperienceProvider` and `src/relay/client.ts`, then change imports. The hook names and behavior are unchanged, except that `useRoleDeliveries` is now internal and `useExperience()` also returns `member`.

#### Patch Changes [#patch-changes-180]

* Updated dependencies \[6c309ea]
  * @domino-sdk/relay\@0.15.0

### 0.14.1 [#0141-1]

#### Patch Changes [#patch-changes-181]

* 9f6c8db: Remove CLI and agent guidance for the retired Console Releases page and JSON imports. Use `domino deploy --environment test` for code-defined behavior and `domino stage` for hosted test apps. Quest Studio publication remains available for Console-authored quests.

  Code-defined live publication through Console is no longer available. Existing live definitions keep running. The CLI continues to reject live publication; `domino deploy --preview --environment live` still validates without publishing. Offline bundle exports remain supported.

* @domino-sdk/relay\@0.14.1

### 0.14.0 [#0140-1]

#### Minor Changes [#minor-changes-191]

* 11abaf5: Add a bounded X search preview for exploring tracker queries before defining them in code. `management.trackers.preview({ query, since })` returns at most 100 matching posts from 3 search pages, up to 30 days back, with author verification retained and `more` set when X had further matches. It requires `publish` permission, is rate limited, stores nothing, and never publishes a tracker or starts collection. Console's Trackers page uses it to show a sample and copy a tracker definition or coding-agent prompt.

  Requires the matching backend deployment. Existing clients and manifests are unaffected.

#### Patch Changes [#patch-changes-182]

* Updated dependencies \[11abaf5]
  * @domino-sdk/relay\@0.14.0

### 0.13.0 [#0130-1]

#### Minor Changes [#minor-changes-192]

* e969f1e: Add maintained tracker datasets with a managed X search source, preserved author verification, read-time filters, and custom indexer ingestion. Publish definitions through relay.json and the existing deployment preview flow. Query datasets from the management SDK or captured read-only capabilities in hosted quest evaluators.

  Requires the matching backend migration and deployment before publishing or reading trackers. X collection requires the platform's X\_API\_WRAPPER\_API\_KEY credential. Existing project manifests remain valid. Custom evaluator test contexts that explicitly implement EvaluationContext must add a trackers reader. Collection does not automatically issue or reverse rewards.

#### Patch Changes [#patch-changes-183]

* Updated dependencies \[e969f1e]
  * @domino-sdk/relay\@0.13.0

### 0.12.0 [#0120-1]

#### Minor Changes [#minor-changes-193]

* 9fdc113: Reward deliveries now end. `rewardDeliverySchema.status` adds a terminal `failed` state, reached when Discord permanently rejects a role grant (unknown role, member left the server, missing permissions) or transient errors persist for about a day. The management client adds `retryRewardDelivery(id)`, which moves a failed delivery back to `pending` and starts a new delivery round. Clients that parse deliveries with an older SDK reject `failed` records, so upgrade before reading deliveries.

#### Patch Changes [#patch-changes-184]

* Updated dependencies \[9fdc113]
  * @domino-sdk/relay\@0.12.0

### 0.11.0 [#0110-1]

#### Minor Changes [#minor-changes-194]

* 5337971: Add `streak({ days, timeZone })` for automatic observation quests. Verified actions count once per community day, including daylight-saving boundaries. Late evidence repairs continuity; milestone rewards commit once with progress. Participant reads expose current/best runs, qualifying days, deadlines and reward status. Timezone changes take effect at the next community boundary without rewriting history.

  Each quest awards one lifetime milestone. Configure separate quests for separate goals. All streak quests in a project/environment share one timezone. An existing goal length is immutable, and budget-exhausted milestones keep their evidence without later automatic payout.

  Apply the backend migration and deploy the updated backend, SDK and participant app before publishing streak quests. Existing once/keyed quests retain their behavior. Older SDKs cannot parse the new `streak` completion kind or `progressed` attempt status; upgrade clients that read a project containing streak quests.

#### Patch Changes [#patch-changes-185]

* Updated dependencies \[5337971]
  * @domino-sdk/relay\@0.11.0

### 0.10.0 [#0100-1]

#### Patch Changes [#patch-changes-186]

* Updated dependencies \[8a81fde]
* Updated dependencies \[d5d530d]
  * @domino-sdk/relay\@0.10.0

### 0.9.1 [#091-1]

#### Patch Changes [#patch-changes-187]

* a024053: Link doctor output and bundled agent guidance to the public package changelog and upgrade instructions. When SDK and server behavior disagree, agents can review intervening release notes, upgrade packages together, and retry the failed action. Guidance is advisory and does not automatically update dependencies or claim server compatibility.
* @domino-sdk/relay\@0.9.1

### 0.9.0 [#090-1]

#### Minor Changes [#minor-changes-195]

* 6a5b6d3: Remove the offline development runtime and the `dev --offline`, `dev --review-mode`, and `doctor --offline` options. Use `domino dev` with the hosted test environment and real sign-in. New campaign exports include only the SDK and CLI packages.

  New release and catalog publication accept only Workers AI. Historical provider identifiers remain readable for retained records, but cannot select simulated evaluation.

#### Patch Changes [#patch-changes-188]

* Updated dependencies \[6a5b6d3]
  * @domino-sdk/relay\@0.9.0

### 0.8.0 [#080-1]

#### Minor Changes [#minor-changes-196]

* 25ea110: Assign persistent resource IDs on the server. Authoring declarations now use stable keys, project and draft creation use separate retry keys, and point ledgers must be created or declared before use. Existing IDs remain valid. Legacy authoring `id` remains supported as a key alias. Use `domino migrate` to preview a repeatable source and manifest upgrade, apply with `--write`, and inspect actionable diagnostics with `domino check --json`. Creation helpers generate retry keys, retry uncertain responses once, and expose the retry key on `CreationError`; raw HTTP creation still requires an action ID. Consume returned IDs for newly created resources.

#### Patch Changes [#patch-changes-189]

* Updated dependencies \[25ea110]
  * @domino-sdk/relay\@0.8.0

### 0.7.0 [#070-1]

#### Minor Changes [#minor-changes-197]

* 0bbefc1: Add point ledger metadata with stable IDs, editable names and descriptions, management list/save methods, and a pointLedger setting for Console pickers. Existing balance keys retain their accounting history.

#### Patch Changes [#patch-changes-190]

* Updated dependencies \[0bbefc1]
  * @domino-sdk/relay\@0.7.0

### 0.6.0 [#060-1]

#### Minor Changes [#minor-changes-198]

* 5cd39a2: Add observation-triggered quests, keyed completions, visibility, shared atomic budgets, and backend observation submission. Include an automatic Discord announcement reaction snippet and participant guidance for repeatable quests.
* 5cd39a2: Add Discord message contributions, moderator recognition with separate actor and beneficiary identities, richer member and scoped message reads, and durable Discord role rewards with reusable role selectors and delivery status. Include three distinct capability snippets and updated authoring guidance.
* d7dd5a4: Show readable command results by default and offer interactive project selection for checkout, init, and inspection commands. Prompt for missing create inputs, agent selection, and recent build selection. Confirm deployment and staging in terminals, with --yes to skip confirmation. Use --json for structured results and --no-interactive to disable terminal prompts.
* 7c88acd: Add `people` to management list reads and a `people(ids)` client method. Both return the sign-in accounts and profiles of members that records name only by ID, so tools can show who a member is outside the Members list.
* 5cd39a2: Add reusable integration resource selectors and date/time settings. Include Discord channel selection, scoped resource lookup, publication validation, and paused publication for unconfigured resource settings.

#### Patch Changes [#patch-changes-191]

* 7216980: Report only staging blockers when staging preflight fails. Do not include optional participant sign-in diagnostics in the error.
* Updated dependencies \[5cd39a2]
* Updated dependencies \[5cd39a2]
* Updated dependencies \[5cd39a2]
  * @domino-sdk/relay\@0.6.0

### 0.5.0 [#050-1]

#### Minor Changes [#minor-changes-199]

* 85bb8aa: Add compact prefixed resource ID generation and expose globally unique project slugs. Hosted builds carry the slug used for stable staging addresses; development previews use the slug and a shorter workspace hash. Existing resource IDs remain valid.
* 990c756: Support compiled Cloudflare Worker apps in connected previews and cloud staging, including SSR, server actions, and API routes. Add the worker app configuration and private module artifacts while preserving static hosting.

  Add opt-in `app.routing="transparent"`: app requests and responses pass through unchanged, with no reserved routes. Move the starter participant proxy into application code and add an explicit hosted participant integration option. Omitted routing retains legacy behavior for existing projects, deployed builds, preview sessions, and older CLI requests. New starters opt in; existing apps migrate explicitly after adding their own participant handler.

#### Patch Changes [#patch-changes-192]

* 8b454af: Show configuration sources and pinned install commands in setup diagnostics, label the bundled capability catalog with release provenance, and preserve Git checkout failure details. Clarify recovery and verification in the shipped agent guidance. Create missing parent directories for deployment build output.
* Updated dependencies \[85bb8aa]
* Updated dependencies \[c24042e]
* Updated dependencies \[990c756]
  * @domino-sdk/relay\@0.5.0

### 0.4.0 [#040-1]

#### Minor Changes [#minor-changes-200]

* d8a2572: Add project integrations with managed Discord bot installation in Console. Quests declare integrations and read membership through ctx.discord.member() without a server ID. Participants use basic Discord identity sign-in or linking. Capture identity and integration revisions for recovery, provide starter identity controls and failed-submission discard, and include the membership snippet and operator/agent guides.
* 8ead9ee: Add project-scoped points leaderboards with code-defined calculations, reviewed publication, participant rankings, and audited points updates. Support period windows, shared ranks, participation groups, and board exclusions.
* 7204e04: Discover default-exported quests and quest types using patterns in relay.json. New projects include discovery patterns, and explicit entries can override deployment settings. Reject missing selected exports while bundling. Add editable photo, quiz, and staff-confirmation recipes through the public snippets registry.
* 1fb2edd: Add code-defined referral programs, session-bound invitation attribution, qualification rewards, milestones, and selective inviter point bonuses with correction recovery. Include participant and management APIs, project publication, Console controls, and agent references.

#### Patch Changes [#patch-changes-193]

* d8a2572: Guide agents to discover recipes from the live snippet catalog before authoring quests. Expose the workflow through `domino reference authoring` and link it from generated project guidance.
* 44e1436: Guide agents to research branding and campaign context, propose a visual direction and campaign brief, and wait for owner confirmation before the first implementation. Preserve confirmed decisions for later sessions.
* 22e5523: Describe the rebuilt starter in the hosted-project agent reference: a static TanStack Router and Tailwind CSS v4 app with a neutral look, sample tour quests and `src/relay` hooks for session, quest state, submission, and recovery.
* Updated dependencies \[d8a2572]
* Updated dependencies \[c475d40]
* Updated dependencies \[8ead9ee]
* Updated dependencies \[7204e04]
* Updated dependencies \[1fb2edd]
  * @domino-sdk/relay\@0.4.0

### 0.3.0 [#030-1]

#### Minor Changes [#minor-changes-201]

* 53d6aef: Support existing application logins through a server participant adapter and project-scoped identity integration keys. Add Console provisioning, rotation and revocation, trusted participant routing, and an agent integration reference.

#### Patch Changes [#patch-changes-194]

* Updated dependencies \[53d6aef]
  * @domino-sdk/relay\@0.3.0

### 0.2.0 [#020-1]

#### Minor Changes [#minor-changes-202]

* 9c037ee: Use hosted test data and leased HTTPS previews for development, with an explicit offline runtime option. Fix unified deployment startup, preflight staging prerequisites, and bundle capability discovery, participant API, referral and troubleshooting references. Record exact starter package hashes and verify the distributed development journey.
* 8aba51d: Save the project setup mode and brief, and include the brief in newly provisioned starter repositories. Console guides new and existing projects through agent handoff with resumable setup.

#### Patch Changes [#patch-changes-195]

* @domino-sdk/relay\@0.2.0

### 0.1.0 [#010-1]

#### Minor Changes [#minor-changes-203]

* d46d47c: Introduce the Relay SDK, Domino CLI with bundled agent skills, and standalone local runtime.

#### Patch Changes [#patch-changes-196]

* Updated dependencies \[d46d47c]
  * @domino-sdk/relay\@0.1.0
