Skip to content

Arete documentation

Steps, permissions, and lifecycle

For the complete documentation index optimized for AI agents, see llms.txt or llms-full.txt. A markdown version of this page is available by appending .md to the URL or sending Accept: text/markdown.

For AI agents: the documentation index is at llms.txt (full corpus: llms-full.txt). A markdown source for this page is /building-protocols/lifecycle.md.

A protocol describes when an action makes sense and who may perform it. A raise can be open for contributions while one wallet has already reached a per-wallet limit; an instance’s state and a participant’s state answer separate questions.

  • create starts an instance. The caller supplies the creation arguments.
  • instance acts on an existing instance, such as closing a deal.
  • participant acts for a wallet in an instance, such as contributing or claiming.

The client supplies an instance id for the latter two scopes. Steps declare their arguments with schemas; shorthand such as { amount: "u64" } makes the argument required. Amounts, public keys, and timestamps are strings with formats such as u64, pubkey, and unixTimestamp.

A guard’s name is the rejection code. Its expression must be true:

guards: {
amount_zero: "args.amount > 0",
not_open: 'instance.state == "open"',
},

Use stable codes your app can translate into helpful text. Conditions can use arguments, parameters, declared reads, and current protocol state. They are checked on prepare. Wallet action discovery cannot finish checks that depend on arguments the caller has not supplied; it reports these in argsDependent.

An action shown as allowed may still fail preparation if state changes or argument-dependent checks fail. Treat rejection as a normal product outcome.

in checks membership in a literal list or in an array value, such as an array resource. Element types must match; a string literal can stand for a public key:

guards: {
mint_not_allowed: "args.mint in resources.allowedMints",
unsupported_side: 'args.side in ["bid", "ask"]',
},

A rejection’s detail carries the operand and the list it was checked against. To pick one entry of an array, index it with an integer literal and read its fields: resources.venues[1].reserve. The checker types the field from the array’s item schema. An index at or past the array’s maxItems fails the check, and so does an index computed from arguments, such as resources.venues[args.i]. Every address a step uses must be decided by fixed names, so use a literal index, or in to accept a value from the list.

Authority identifies the signer: the creator, a participant, or a bound role such as the scaffold’s recipient. Roles are declared in roles and bound to wallets by the create step.

authority: { kind: "role", role: "recipient" },

An admin step can instead be proposed to a team’s Squads v4 multisig. The multisig must come from the deployment config (a resource), never from the instance or the step’s arguments. The optional vaultIndex (an integer from 0 to 255, default 0) picks which vault of that multisig the step acts from, so protocols that share a multisig can keep separate vaults.

authority: { kind: "squadsProposer", multisig: "resources.treasury.multisig", vaultIndex: 1 },

Access determines which credentials may prepare a wallet step. public allows a publishable-key client; secret requires a secret-key session. Create and role steps default to secret; other wallet steps default to public. Declare access explicitly when it is important to your interface.

A publishable key does not authorize signing, and a secret key does not replace the required wallet’s signature. Keeper and Squads proposal steps are not ordinary client-callable wallet steps.

Declare machines under machines.instance and optionally machines.participant. Each has an initial state and named states.

A recorded state changes through declared transitions. A derived state is evaluated from conditions, such as a deadline or chain account field. A time-based rule can declare at as well as when so it is evaluated when the deadline arrives.

instance: {
initial: "open",
states: {
open: "recorded",
expired: { kind: "derived", terminal: true },
closed: { kind: "recorded", terminal: true },
},
derive: [{
from: ["open"],
to: "expired",
when: "now >= params.deadline",
at: "params.deadline",
}],
},

A step can declare transitions: [{ from: ["open"], to: "closed" }]. Preparing or signing it does not itself mean the transition completed: follow the execution outcome and read the instance again.

A terminal instance rejects all further instance and participant steps with instance_terminal. A terminal participant rejects that wallet’s participant steps with participant_terminal; it does not close instance steps. Both include detail.state. A participant step may explicitly declare a transition from that terminal participant state, including an allowed self-loop.

Design the machine around the actions people still need. For example, marking an entire failed raise terminal would also close its participant refund steps. Keep the instance open for refunds if that is part of your workflow, and make the participant terminal after refunding.

Check the right wallet, wrong wallet, before and after deadlines, repeated steps, and terminal states. Extend the tip jar for an example using two roles and a clock-controlled outcome.

Protocol guards govern the Arete API. They do not prevent someone calling the underlying program directly. Use programs whose on-chain permissions and asset rules support your intended product.