Arete documentation
Signers and safety
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/signing.md.
Your backend signs for users, so it matters what stops it signing the wrong thing. This page covers the checks every step passes, the signers you can use, and which protections still hold if your backend is ever compromised.
The signing policy
Section titled “The signing policy”definePolicy sets what may be signed for any user:
const policy = definePolicy({ deployments: [client.deploymentId], // required clusters: ["dev", "mainnet"], // defaults to ["dev"]; mainnet only when listed programs: [VAULT_PROGRAM_ID], // required: the programs your steps call maxSolPerTransaction: "0.01", // required, in SOL maxTokenAmounts: { [USDC_MINT]: "5000000000" }, // base units per mint});Before each signature, the step’s transactions are simulated against your RPC and checked:
- the deployment and its cluster are allowed (
devfor a clienta4 devgenerated,mainnetfor a published one), and every program the transactions call is on the list (the compute budget, memo, system, token and associated token programs are always allowed); - no transaction hands control of the wallet’s funds to someone else: token approvals, authority changes, account reassignments, nonce, stake and program-upgrade authority changes are always refused, including inside other programs’ calls;
- the SOL and tokens leaving the wallet stay under the caps. Wrapped SOL counts as SOL, and a token account closed to someone else counts too. A mint without a cap may not leave.
A transaction whose effect cannot be simulated is refused. The rules are the same as the MCP signer policy, and both are tested against one shared set of cases.
Exact bytes
Section titled “Exact bytes”A signer may only fill the wallet’s signature. Before signing, the prepared transactions must match what the protocol committed to (each message’s hash). After signing, every message must still match, no other byte may have changed, and the wallet’s signature must verify. A signer that adds an instruction or re-encodes a transaction is refused.
Signers
Section titled “Signers”Keypair, for development
Section titled “Keypair, for development”import { keypairSigner } from "@usearete/automations/keypair";
const signer = keypairSigner(devWalletSecretKey);Use it with dev wallets and tests. A local key has no provider policy behind it.
Privy server wallets let your app sign for a user who has added your app’s authorization key as a signer:
import { PrivyClient } from "@privy-io/node";import { privyPolicyFor, privySigner } from "@usearete/automations/privy";
const signer = privySigner({ privy: new PrivyClient({ appId, appSecret }), walletIdFor: async (address) => lookUpPrivyWalletId(address), // null without server access authorizationPrivateKey: process.env.PRIVY_AUTHORIZATION_KEY!,});
// The provider-side bound for one user's consent: attach it to their wallet in Privy.const { policy: privyPolicy, unenforced } = privyPolicyFor(policy, grant);The signer asks Privy to sign each transaction (never to send it). It
prefers Transaction V1 and falls back to v0; pass
transactionFormats: ['v0'] to sign only v0. When Privy’s policy denies a transaction, the step ends
refused with the rule provider_policy.
privyPolicyFor builds the policy Privy enforces in its own secure
environment. It allows only:
- the programs in your policy;
- System transfers and token
TransferCheckedup to the per-run caps (wrapped SOL up to the SOL cap, since it counts as SOL), checked per instruction: several capped transfers in one transaction, or one transaction after another, can add up to more, so totals remain a check in your backend; - closing token accounts back to the wallet;
- signing until the consent expires.
It never allows key export.
Privy’s policies only see each transaction’s top-level instructions. So for a
protocol step, where the protocol’s program moves the funds, Privy can limit
which programs are called and until when, but not how much moves. unenforced
lists everything it cannot express, so you can show it honestly. Steps with
top-level instructions Privy cannot allow (for example wrapping SOL) are
refused before Privy is asked.
Your own signer
Section titled “Your own signer”Any provider (a KMS, Turnkey) can be a signer by implementing ProtocolSigner:
kind (a short name such as "turnkey"), supportedTransactionFormats,
canSign(wallet) and
signTransactions({ wallet, intent, transactions }), which returns the
signed transactions. Throw a SignerRefusal when the provider declines, so
the step is refused rather than retried. Optionally implement
delegationStatus(wallet) so consent can warn about missing setup.
If your backend is compromised
Section titled “If your backend is compromised”| Protection | Runs in | Holds if your backend is compromised |
|---|---|---|
| The protocol’s on-chain rules | Solana | Yes |
| The wallet provider’s policy | The provider | Yes |
| The signing policy | Your backend | No |
| The user’s consent and limits | Your backend | No |
| Spending windows | Your backend (your store) | No |
| The exact-bytes checks | Your backend | No |
Arete never holds, stores or proxies a user’s key; it prepares transactions, records intents and relays what your backend signed. The consent signature records what the user agreed to, but it cannot stop an attacker who controls your backend. Privy’s caps do not restrict where a transfer goes, so such an attacker can repeat capped transfers to themselves until the delegation expires or is removed: the caps slow a drain, they do not bound it, and revoking a consent in your backend does not change Privy’s policy. Keep authorization keys in a secrets manager, keep delegations short, keep the program allow-list tight, and prefer protocols whose automated steps can only move funds between the user’s own positions.
The threat model in the Arete repository
(typescript/automations/docs/threat-model.md) covers each threat (forged and
replayed triggers, crashes after signing, consent forgery, cap races, wrong
triggers) and what handles it.