# On-chain actions (/docs/reference/onchain)



Quests check what a member's linked wallet did on chain with two evaluator readers: `ctx.wallet.events()` for EVM contract events and `ctx.wallet.transactions()` for Solana programs. Both follow the [evaluator reader rules](verification.md#evaluator-readers-null-errors-and-pinning): missing is `null`, a failure throws. The [wallets guide](/docs/build/wallets#check-on-chain-actions) installs and configures the ready-made quests that use them.

## EVM events [#evm-events]

`ctx.wallet.events({ event, account, since, afterActivation?, where?, address?, newest? })` reads the events of one EVM contract whose `account` argument is one of the member's pinned linked EVM wallets, from `since` to the chain's `finalized` block (its `safe` block on a chain without a finalized tag). `event` is 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)"` (a leading `event` keyword is accepted): every argument named, at most three indexed, at least one indexed `address`, no tuples or arrays of arrays. Take it from a `settings.contractEvent({ label, description?, default })` setting, typed `ContractEvent`; never from member input. `isContractEvent()` and `parseContractEvent()` from `@domino-sdk/relay/authoring` check the syntax. `account` names the indexed `address` argument matched against the wallets, such as `"to"`. `since` is epoch milliseconds, at most 30 days before the evaluation (`maxWalletEventLookback`); `afterActivation: true` raises it to the quest's first activation in the environment.

It returns `{ event, chain, contract, since, at, finality, complete, logs, observedAt }`: `event` with the contract EIP-55 checksummed, `at` the newest block read (`{ kind: "block", number }`), `finality` `"finalized"` or `"safe"`, `complete` whether `logs` holds every matching event since `since` (absent from results captured before Relay reported it), and `logs` oldest first, each `{ wallet, block, timestamp, tx, logIndex, args }` with `timestamp` the block's time in epoch milliseconds and `args` every argument by name (integers as decimal strings, addresses EIP-55, bytes lowercase hex, an array argument as an array). Key a repeatable completion on `` `${chain}:${tx}:${logIndex}` ``.

* `where` maps argument names to an equality (an address in any case, a decimal integer, `true`/`false`, or hex bytes) or `{ gte: "<decimal>" }` for an integer argument; every condition must hold, at most 10. Equalities on indexed static arguments also narrow the RPC topic filter. A condition on the `account` argument, an unknown argument, an array, an indexed dynamic argument (stored as its hash) or a value of the wrong type throws `author-error`.
* Only events that name the member in an indexed argument can be read: a router or aggregator flow where the member is only `tx.from` cannot. No Cosmos or Snapshot activity; Solana programs are read with [`ctx.wallet.transactions()`](#solana-transactions).
* Reads go through dRPC: `eth_chainId`, the finalized (or safe) block, the first block estimated from block times and checked against its header, then `eth_getLogs` over 10,000-block ranges, several requests at a time, a refused range halved down to 100 blocks (below that, `provider-unavailable`). A read over more than 3,000,000 blocks or more than 1,000 logs (`limits.walletEventLogs`, counted before `where` filters non-indexed arguments) throws `author-error`. 3,000,000 blocks is under 30 days on fast chains; at block times measured in October 2026, about: Robinhood 3 days, Gravity 8, opBNB 8, Arbitrum One 9, Monad 10, Cronos 14, Plume 15, BNB Smart Chain 15, Sei 16, Telos 17, Arc 17, Tempo 19, Playnance 20, Aurora 22, Stable 24, Moca 28. Only finalized blocks are read, so no reorg handling is needed; recent events appear once final (about 13 minutes on Ethereum, longer on rollups).
* `newest: n` (1 to 1,000) returns at most the `n` newest matching events: Relay scans back from the head and stops at either cap instead of throwing, with `complete: false` when it left older matching events out or stopped before `since`. Without `newest`, a read returns every matching event with `complete: true` or throws. Use it for a busy wallet or a long window on a fast chain.
* Publication, Console settings edits and unarchiving check every `settings.contractEvent()` value: the syntax, a chain Relay reads, a chain that reports a `finalized` or `safe` block, and code at the address. Refusals are `422 validation-failed` naming the setting's label; an RPC outage is `503 unavailable`; a platform without a dRPC key refuses every event with `503 not-configured`.
* `address`, `null` and failures follow `ctx.wallet.balance()`: no linked EVM wallet returns `null`; outages throw `provider-unavailable`. Reads are captured, replayed on retries, and count toward the eight capability calls.
* `WalletReader.events` is optional in the type so that older test doubles compile; hosted evaluations always supply it, so call it as `ctx.wallet.events!(…)`. The testing kit answers it from `facts.wallet.events` (called with `{ event, account, since, afterActivation, where, address }`, defaults `false` and `null`, plus `newest` when the quest sets it) without applying `since`, `where` or `newest`, null while `facts.wallet.list` links no EVM wallet; a run's `capabilityCalls` lists event reads as `wallet-events`.
* Anti-sybil: set a minimum with `where` and `gte`, count only logs at or after the wallet's `verifiedAt`, keep `since` recent, and add a member budget.

Snippet `onchain-action` installs a complete quest: a `settings.contractEvent()` event, the participant argument, an optional `settings.amount()` minimum, events since the later of activation and the wallet's `verifiedAt` within 30 days read with `newest: maxWalletEventLogs`, and a `keyed` completion on `chain:tx:logIndex` that skips events already rewarded.

## Solana transactions [#solana-transactions]

`ctx.wallet.transactions({ program, since, afterActivation?, mint?, address? })` reads the successful transactions at `finalized` commitment of the member's pinned linked Solana wallets since `since` that invoked `program` in a top-level or inner instruction (lookup-table addresses resolved), oldest first. Take `program` from a `settings.solanaProgram({ label, description?, default })` setting (base58, `isSolanaProgram()`); never from member input. `since` is epoch milliseconds, at most 30 days before the evaluation (`maxWalletTransactionLookback`); `afterActivation: true` raises it to the quest's first activation. With `mint` (an SPL mint), Relay pages the signatures of each wallet's associated token account for the mint instead of the wallet's: far fewer transactions, but tokens in other token accounts are not seen.

It returns `{ program, chain, mint, since, at, complete, transactions, observedAt }`: `at` the finalized slot read (`{ kind: "slot", number }`), and `transactions` each `{ wallet, signature, slot, timestamp, signer, programs, balanceChanges }`, with `signer` whether that wallet signed it (read from the parsed account keys; `false` in results captured before it existed and in testing-kit facts that omit it), `programs` every invoked program in call order (each top-level instruction's program, then the programs it called) and `balanceChanges` `[{ mint, delta }]` (`mint` `"SOL"` for lamports, rent included and the fee left out when the wallet paid it, wrapped SOL under its own mint; `delta` a signed integer string; unchanged mints omitted). Token changes come from `preTokenBalances`/`postTokenBalances` of accounts the wallet owns, so tokens routed through accounts it does not own do not show. A transaction touching two linked wallets appears once per wallet. A transaction appears when it touched the wallet (or, with `mint`, its token account), whoever signed it: an incoming transfer from someone else's swap counts as received, with `signer` false. Check `signer` for anything that rewards an action. Key a repeatable completion on `signature`.

* Helpers in `@domino-sdk/relay/authoring`, pure over one transaction: `received(tx, mint)` and `spent(tx, mint)` (integer string or `"0"`), `swapped(tx, { into?, from? })` (one mint down, a different one up), `invoked(tx, program)`, `instruction(tx, name)` (first decoded instruction's args, or `null`).
* Instructions: saving a `settings.solanaProgram()` value (publication, Console edit) stores the program's Anchor IDL (legacy or 0.30+) from its on-chain IDL account in the release's `solanaIdls[settingKey]` (`{ program, source: "chain" | "pasted", idl }`); a later on-chain upgrade changes nothing until the next save. For a program without one, an operator pastes it in Console (quest → **Instruction decoding**) or sends `solanaIdls: { [settingKey]: idl | null }` with `POST /releases/settings`; a pasted IDL that is not Anchor, names another `address`, or exceeds 512 KiB is `422`; an on-chain IDL wins at the next save. With an IDL, each transaction has `instructions`: the program's top-level and inner instructions in call order, `{ name, args, accounts }`, matched on the first 8 bytes (the IDL's `discriminator`, else `sha256("global:<snake_case name>")[..8]`); undescribed or undecodable ones are skipped. Names as the IDL spells them. Args: ints up to 32 bits and floats as numbers, 64/128-bit as decimal strings, `pubkey` base58, `bytes`/`vec`/arrays as arrays, absent option `null`, struct as object, enum `{ Variant: fields }`. `accounts` maps IDL account names (composite `group.name`) to addresses. Without an IDL, `instructions` is absent.
* Each wallet's read fetches at most 1,000 successful transactions (`limits.walletTransactions`), newest first; a wallet with more, or whose newest 10,000 signatures don't reach `since`, returns its newest ones with `complete: false` and does not throw.
* Reads use the platform's Solana RPC (dRPC where it has the key): `getSignaturesForAddress` at `finalized`, failed signatures skipped, then `getTransaction` with `jsonParsed` and `maxSupportedTransactionVersion: 1`. A transaction appears about 13 seconds after it lands.
* Publication, Console settings edits and unarchiving check every `settings.solanaProgram()` value: an address with no account or a non-executable account is `422 validation-failed` naming the setting's label; an RPC outage is `503 unavailable`.
* `address`, `null` and failures follow `ctx.wallet.balance()`: no linked Solana wallet returns `null`; outages throw `provider-unavailable`. Reads are captured, replayed on retries, and count toward the eight capability calls.
* `WalletReader.transactions` is optional in the type so that older test doubles compile; hosted evaluations always supply it, so call it as `ctx.wallet.transactions!(…)`. The testing kit answers it from `facts.wallet.transactions` (called with `{ program, since, afterActivation, mint, address }`, defaults `false` and `null`; transactions may carry `instructions`) without applying `program`, `since` or `mint`, null while `facts.wallet.list` links no Solana wallet; a run's `capabilityCalls` lists transaction reads as `wallet-transactions`.
* Anti-sybil: someone else can send tokens to a member's wallet in a transaction that calls the program, so a `received()` check alone rewards transfers; require `tx.signer`, a minimum with `received()` against a `settings.amount()`, count only transactions at or after the wallet's `verifiedAt`, keep `since` recent, and add a member budget.

Snippet `solana-action` installs a complete quest: a `settings.solanaProgram()` program, an optional token mint (an SPL mint, which narrows the read to its associated token accounts, or `"SOL"`) with a `settings.amount()` minimum received, transactions the linked wallet signed since the later of activation and its `verifiedAt` within 30 days, and a `keyed` completion on `signature` that skips transactions already rewarded. Its README shows a `spent()` variant for deposits and staking.
