Arete documentation
Your first automation
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.mdto the URL or sendingAccept: text/markdown.
For AI agents: the documentation index is at llms.txt (full corpus: llms-full.txt). A markdown source for this page is /automations/quickstart.md.
This guide builds automatic refunds: when a raise fails, every contributor
who opted in is refunded without opening the app. You write the handlers
once. a4 dev up runs them against your
dev environment, where no real funds move, and
your backend runs the same handlers in production. A complete version, with
a webhook endpoint and consent routes, is in the Arete repository under
typescript/automations/examples/token-raise.
You need a protocol with a participant step the user signs and an event that
says when to run it. This guide uses a token-raise protocol as the example: a
contribute step, a refund step, and a raise.failed event its reactions
emit when a raise ends below its soft cap. Use your own protocol’s steps,
event and generated module in their place.
Install
Section titled “Install”In the protocol’s project:
npm install @usearete/automations @usearete/sdknpm install --save-dev tsxa4 dev up deploys the protocol to your dev environment, a fork of mainnet,
and generates its client where a4 install puts it. The module exports the
definition as TOKEN_RAISE_PROTOCOL. tsx lets a4 dev run your
TypeScript.
Write the handlers
Section titled “Write the handlers”import { defineAutomations } from "@usearete/automations";import { TOKEN_RAISE_PROTOCOL } from "./generated/typescript/protocols/token-raise/token-raise.js";
export default defineAutomations( TOKEN_RAISE_PROTOCOL, (automations) => { automations.on("raise.failed", async (event, ctx) => { // Every wallet that agreed to automatic refunds for this raise. const grants = await ctx.consents.list({ step: "refund", instanceId: event.instanceId }); const contributors: string[] = []; for (const grant of grants) { // Only wallets that contributed to this raise have anything to refund. try { await ctx.client.participants!.get({ instance: event.instanceId, wallet: grant.wallet }); contributors.push(grant.wallet); } catch (error) { // Anything but "not a participant" fails the handler, so the // delivery is retried. if ((error as { details?: { status?: number } }).details?.status !== 404) throw error; } } return contributors.map((wallet) => ctx.step("refund", { wallet, instance: event.instanceId })); }); }, { // What may be signed: the programs the steps call, and at most // 0.001 SOL leaving a wallet per transaction. policy: { programs: [RAISE_PROGRAM_ID], maxSolPerTransaction: "0.001" }, },);A handler receives the event and returns the steps to run, one per wallet that opted in and contributed. It never prepares or signs anything itself: each step is checked against the wallet’s consent, the policy and the protocol before it is signed. The definition holds no keys, store or URLs; those come from where it runs.
Run it with a4 dev
Section titled “Run it with a4 dev”Point a4 dev at the module in arete.toml:
[dev.automations]entry = "automations.ts"a4 dev upa4 dev up runs the automations next to the protocol:
- The client is the one it generated for the dev environment’s
deployment, so the automations target
dev. - The signer signs for your dev wallets (
.arete/dev/wallets/), which stand in for users. - The simulator reads the fork through the local RPC
a4 dev upserves. - The store keeps jobs, consent and spending under
.arete/dev/automations/. - Events reach the handlers as signed webhook deliveries. A dev
environment sends no webhooks, so
a4 devfollows the environment’s instances and signs each event the way a production webhook is signed.
When you save automations.ts, or a redeploy regenerates the client, the
automations restart. Jobs, consent and spending survive the restart.
Try it
Section titled “Try it”-
Create a dev wallet and opt it in.
a4 dev consentsigns the consent message with the dev wallet, as a user’s wallet would in your app:Terminal window a4 dev wallet alicea4 dev consent alice --steps refund -
Create a raise with a deadline a few minutes away, leaving time to contribute (any minimum the create step enforces still applies), and a soft cap above the contribution below, so it fails. Contribute from the wallet before the deadline:
Terminal window a4 dev step contribute --instance <raise> --args '{"amount":"1000000"}' --wallet alice -
Wait for the deadline to pass.
a4 dev warpwon’t get you there: it moves the dev environment’s chain clock only, and the raise’s deadline follows real time. Theraise.failedevent arrives, and the refund runs:automations │ completed: refund for <alice> on <raise>: completed
If the outcome is skipped, check that the wallet’s consent covers refund.
If it is refused, the reason names the policy rule or limit that stopped it.
Run the same handlers in production
Section titled “Run the same handlers in production”Your backend builds the runtime and uses the definition. It keeps its state in Postgres:
npm install pgimport { createProtocolClient } from "@usearete/sdk";import pg from "pg";import { createAutomations, definePolicy, rpcSimulator } from "@usearete/automations";import { postgresStore } from "@usearete/automations/postgres";import definition from "./automations";import { TOKEN_RAISE_PROTOCOL } from "./generated/typescript/protocols/token-raise/token-raise.js";
const client = createProtocolClient(TOKEN_RAISE_PROTOCOL, { auth: { secretKey: process.env.ARETE_SECRET_KEY! },});
const store = postgresStore(new pg.Pool({ connectionString: process.env.DATABASE_URL }));await store.migrate(); // never automatic: run it from a deploy step
const automations = createAutomations({ client, signer, // your wallet provider's signer, for example Privy policy: definePolicy({ ...definition.policy!, deployments: [client.deploymentId], clusters: ["mainnet"], // mainnet is allowed only when listed }), simulator: rpcSimulator(process.env.RPC_URL!), store, consent: { domain: "app.example" }, onOutcome: (outcome) => notifyUser(outcome),});await automations.use(definition);
// Events arrive as signed webhooks.export const POST = automations.webhookHandler({ secret: process.env.ARETE_WEBHOOK_SECRET! });
automations.start();- The client uses a secret key, so it runs only on your server. With the
published client (
a4 install --locked) the automations target mainnet. - The policy adds the deployment and mainnet to the definition’s limits.
- The webhook handler checks each delivery’s signature, records it once and answers straight away; set up the endpoint’s subscription as described in events.
start()runs the worker that turns each trigger into steps and each step into a signed, submitted transaction.
To run this server against the dev environment while you build it, set
forward instead of entry: a4 dev up then delivers the events to your
webhook route, signed with the secret in .arete/dev/automations/webhook-secret.
[dev.automations]forward = "http://localhost:3000/api/arete-webhook"Moving to mainnet, step by step →
Let a user opt in
Section titled “Let a user opt in”In your app, a user opts in by signing a message that states exactly what you may do:
import { SOL } from "@usearete/automations";
const fields = { wallet, deploymentId: client.deploymentId, steps: ["refund"], // A refund moves funds into the wallet; only the network fee leaves it. limits: { perExecution: { [SOL]: "10000" } }, expiresAt: new Date("2027-01-01"),};
const proof = await automations.consent.proofMessage(fields);// Your app asks the user's wallet to sign `proof.bytes` (signMessage)...const { grant, warnings } = await automations.consent.grant({ ...fields, nonce: proof.nonce, issuedAt: proof.issuedAt, signature,});What the user signs, and how to present it →
- Schedule recurring work, or react to live data: triggers.
- Use Privy instead of a dev wallet: signers.
- Take it to production: running automations.