---
title: "Your first automation"
description: "Refund every contributor who opted in when a token raise fails, run by a4 dev against your dev environment, then the same handlers in production."
editUrl: true
head: []
template: "doc"
sidebar: {"hidden":false,"attrs":{}}
pagefind: true
draft: false
---

> For the complete documentation index optimized for AI agents, see [llms.txt](https://docs.arete.run/llms.txt) or [llms-full.txt](https://docs.arete.run/llms-full.txt). A markdown version of this page is available at [/automations/quickstart.md](https://docs.arete.run/automations/quickstart.md) or by sending `Accept: text/markdown`.
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](/cli/local-development/), 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

In the protocol's project:

```bash
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.

## Write the handlers

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

## Run it with `a4 dev`

Point `a4 dev` at the module in `arete.toml`:

```toml
[dev.automations]
entry = "automations.ts"
```

```bash
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.

## Try it

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:

   ```bash
   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:

   ```bash
   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:

   ```text
   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

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

```bash
npm install pg
```

```ts
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](/integrating-protocols/events/#receive-named-emissions-through-webhooks).
- **`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`.

```toml
[dev.automations]
forward = "http://localhost:3000/api/arete-webhook"
```

[Moving to mainnet, step by step →](/automations/production/#move-to-mainnet)

## Let a user opt in

In your app, a user opts in by signing a message that states exactly what
you may do:

```ts
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 →](/automations/consent/)

## Next

- Schedule recurring work, or react to live data:
  [triggers](/automations/triggers/).
- Use Privy instead of a dev wallet: [signers](/automations/signing/).
- Take it to production: [running automations](/automations/production/).
