---
title: "Actions"
description: "The actions an agent can run in a sandbox (invoke and withdraw), how to swap with invoke, how to preview them, and how to run each exactly once."
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 [/sandboxes/actions.md](https://docs.arete.run/sandboxes/actions.md) or by sending `Accept: text/markdown`.
An agent changes a sandbox's wallet through exactly one tool,
`sandbox_invoke`, with one of two actions: `invoke` or `withdraw`. Each needs a
[grant](/sandboxes/agents/#grants), and each can be previewed first with
`sandbox_preview`, which takes the same arguments.

The loop is: **discover → preview → invoke.**

1. `sandbox_status` and `sandbox_rules`: what the wallet holds, which actions
   this agent holds, and which programs and instructions are allowed.
2. `sandbox_derive`, when the action needs an address: a PDA (computed
   locally, with no network call) or an associated token account.
3. `sandbox_preview`: a dry run. Returns `pass` with what the wallet would
   lose and the transaction it built, or the refusal.
4. `sandbox_invoke`: runs it.

Pass u64 amounts as decimal strings.

## invoke

Named instructions of the programs your rules enable, encoded from each
program's IDL. One to 32 instructions per action, named exactly as
`sandbox_rules` lists them, as long as they fit one transaction: 4096
bytes and 64 accounts for v1 (the default), 1232 bytes for v0. A flow
that needs its setup, refreshes and the action together, such as a
first lending deposit, goes in one action. The sandbox wallet is the
only signer.

```json
{
  "action": "invoke",
  "args": {
    "instructions": [
      {
        "program": "Pump.fun",
        "instruction": "buy",
        "args": { "...": "as sandbox_rules lists them" },
        "accounts": { "...": "only those without a fill" }
      }
    ],
    "computeUnitPriceMicroLamports": "1000"
  }
}
```

| Argument                        | Meaning                                                                        |
| ------------------------------- | ------------------------------------------------------------------------------ |
| `instructions[]`                | `{ program, instruction, args?, accounts?, remainingAccounts? }`               |
| `remainingAccounts`             | `[{ address, writable? }]`, for instructions that take a variable account list |
| `computeUnitLimit`              | Optional compute budget                                                        |
| `computeUnitPriceMicroLamports` | Optional priority fee, per compute unit                                        |
| `transactionFormat`             | `"v1"` (default) or `"v0"`                                                     |

Accounts with a `fill` in `sandbox_rules` can be left out: Arete fills
fixed addresses, the wallet, PDAs, token programs, your associated token
accounts and fields it reads from the accounts you give (a pool's mints and
vaults). A request still missing accounts is refused with
`sandbox-intent-invalid`, `missingAccounts`, and `accountGuidance` that
says how to find each one. See [Finding accounts](/sandboxes/accounts/).

## Swaps

There is no separate swap action. A swap is an `invoke` of the instructions
that make it, so it passes the same rules, limits and checks as any other
instruction:

1. **Resolve the venue.**
   - **A venue's own instructions**, when you know the pool: for example
     PumpSwap (pump-amm) `buy` and `sell` for a coin that has left its
     Pump.fun bonding curve, Pump.fun `buy` and `sell` on the curve, or a
     DEX's own swap instruction (Orca Whirlpool, Raydium, Meteora). This is
     the simplest path.
   - **A Jupiter route**, when you want the best route across venues: quote
     with Jupiter's Swap API, then ask it for `/swap-instructions` with the
     sandbox wallet as the user. Ask the quote for about 50 `maxAccounts`, so
     the route fits a v1 transaction (64 accounts, no lookup tables). The
     route only fills through venues whose programs your rules enable.
2. **Write the `invoke`.** Name each instruction as `sandbox_rules` lists it
   (Jupiter's swap is one of the Jupiter instructions there, such as
   `route_v2` or `shared_accounts_route_v2`), with its `args` decoded from
   the instruction data by the IDL types `sandbox_rules` gives, its named
   accounts, and any further accounts the route passes in
   `remainingAccounts`. Setup Jupiter returns, such as creating a token
   account or wrapping SOL, is baseline plumbing: put it in the same action,
   in order.
3. **`sandbox_preview`** with `action: "invoke"` until it passes, then
   **`sandbox_invoke`** with the same arguments.

```json
{
  "action": "invoke",
  "args": {
    "instructions": [
      {
        "program": "PumpSwap",
        "instruction": "sell",
        "args": { "...": "as sandbox_rules lists them" },
        "accounts": { "...": "only those without a fill" }
      }
    ]
  }
}
```

Tokens the sandbox bought may always be sold, through any route the rules
allow.

## withdraw

SOL to your wallet, the only destination a withdraw can have.

```json
{ "action": "withdraw", "args": { "lamports": "500000000" } }
```

## Transaction format

Actions build Solana v1 transactions (SIMD-0385) by default: up to 4096 bytes
and 64 accounts, no address lookup tables, with the compute budget in the
message itself. `transactionFormat: "v0"` builds the older format instead (up
to 1232 bytes). A transaction too large for its format is refused with rule
`transaction-size`.

## Exactly once

Every `sandbox_invoke` has an idempotency key: the agent's own
(`idempotencyKey`, 1 to 128 characters) or a generated one, returned on
success and on error. A key never runs twice.

- **The same request, the same key**: safe to send again. It returns the
  original result, success or refusal, in the same shape plus
  `replayed: true`. An action still in flight returns its current status. A
  replayed refusal also carries the record's `rejectCode`, which can differ
  from its `code`: go by the replayed `code` and `retryable`. A replayed
  outage is `retryable: true`, not a permanent refusal; retry it with a new
  key. See [Errors and refusals](/sandboxes/errors/).
- **A different request, a used key**: refused, not retryable, with the id of
  the action that has the key (`sandbox-duplicate-action`, or
  `sandbox-idempotency-key-reused`). Use a new key.

After a timeout, the action may have run, so never retry it under a new key.
Retry with the same key, or look it up first:

1. `sandbox_action` with the `idempotencyKey`.
2. If it ran, don't repeat it. If it was refused, retry with a new key. If no
   action has the key, retry with the same key.

Older servers answer every repeat of a key, the same request included, with
`sandbox-duplicate-action` and the action's id; look it up with
`sandbox_action` for its result.

## Results

A landed action returns its id, status, signature and `outflowLamports`:
everything that left the wallet, the network fee included.
`networkFeeLamports` is the network fee alone, null until the landed
transaction is read. `feeLamports`, when present, is something else: the
protocol's own fees. It also names the `guardPipeline` that checked it, such
as `execute-v1`. `sandbox_action` lookups and history items carry
`outflowLamports`, `networkFeeLamports` and `guardPipeline` too (not
`feeLamports`), and previews carry `outflowLamports` and `guardPipeline`.
`guardPipeline` is null or absent on actions recorded before it was kept and
on actions that never reached the guard, such as a rejected one. The full record (decoded
instructions, events, balance changes) is in the
[history](/sandboxes/history/). A refusal is a tool error with `code`,
`rule`, what triggered it, `retryable` and a `hint`; see
[Errors and refusals](/sandboxes/errors/).
