Skip to content

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 .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 /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.

In the protocol’s project:

Terminal window
npm install @usearete/automations @usearete/sdk
npm install --save-dev tsx

a4 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.

automations.ts
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.

Point a4 dev at the module in arete.toml:

[dev.automations]
entry = "automations.ts"
Terminal window
a4 dev up

a4 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 up serves.
  • 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 dev follows 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.

  1. Create a dev wallet and opt it in. a4 dev consent signs the consent message with the dev wallet, as a user’s wallet would in your app:

    Terminal window
    a4 dev wallet alice
    a4 dev consent alice --steps refund
  2. 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
  3. Wait for the deadline to pass. a4 dev warp won’t get you there: it moves the dev environment’s chain clock only, and the raise’s deadline follows real time. The raise.failed event 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.

Your backend builds the runtime and uses the definition. It keeps its state in Postgres:

Terminal window
npm install pg
import { 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 →

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 →