On-chain actions

Check EVM contract events and Solana program transactions of a member's linked wallets.

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: missing is null, a failure throws. The wallets guide installs and configures the ready-made quests that use them.

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

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.

On this page