> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallets, passwords and spending policy

> How the sauce command keeps signing keys and limits what each wallet can send. Read it before you let the command or an agent sign.

The `sauce` command signs with a wallet it keeps on disk, a keystore, a raw key or a Solana keypair file. A policy file beside a wallet limits what that wallet sends through `--wallet`, per transaction and per 24 hours. [Agents, wallets and keys](https://docs.eco.com/sauce/security-considerations.md#agents-wallets-and-keys) covers what the files, the passwords and the policy protect against, and what they do not.

## Prerequisites

* The `sauce` command: `npm install -g @eco-incorp/sauce` ([Installation](https://docs.eco.com/sauce-sdk/installation.md)).
* A wallet, made with [`sauce wallet new`](https://docs.eco.com/sauce-sdk/cli/wallet) or `sauce wallet import`.
* To send from it, the release's engine must be deployed on the chain: `sauce pot --chain <chain> --owner <address>` reports whether it is.

## The wallet directory

Wallets live in `~/.sauce/wallets`, or in the directory `--wallets <dir>` names. The command creates the directory with mode 0700.

Each wallet is one file, `<name>.json`. It is written once, with mode 0600, by linking a temporary file into place, so an existing wallet is never overwritten. A name is up to 64 lowercase letters, digits, `-` and `_`, and starts with a letter or a digit.

Beside a wallet, the command may find:

| File | Purpose |
| - | - |
| `<name>.json` | The encrypted key |
| `<name>.policy.json` | The limits on what the wallet sends. You write it |
| `<name>.ledger.jsonl` | What the wallet has sent, for the 24-hour limits. The command writes it |
| `<name>.lock` | A lock held while a send is checked against the policy and recorded |

These are hot wallets: whoever holds a wallet's file and its password holds the key. Keep small balances in them.

## The file format

An EVM wallet is a Web3 Secret Storage v3 keystore, the format geth and `cast wallet` write:

```json theme={null}
{
  "address": "e1eff146…",
  "crypto": {
    "cipher": "aes-128-ctr",
    "cipherparams": { "iv": "…" },
    "ciphertext": "…",
    "kdf": "scrypt",
    "kdfparams": { "dklen": 32, "n": 262144, "p": 1, "r": 8, "salt": "…" },
    "mac": "…"
  },
  "id": "…",
  "version": 3
}
```

Because the format is standard, other tools open the same file:

```bash theme={null}
cast wallet address --keystore wallets/agent.json --password-file pw.txt
```

```text theme={null}
0xE1Eff146a8463ad592014Bd9cc0D058307364916
```

`sauce … --keystore wallets/agent.json` reads it too, without the policy. A Solana wallet is the same envelope around the 64-byte keypair (seed, then public key, as `solana-keygen` writes it), with `"kind": "solana"` and the base58 public key as `address`. `--keystore` refuses a Solana wallet's file for an EVM transaction.

Each file holds its address in the clear. `sauce wallet list`, `sauce wallet address`, and `sauce pot` with `--wallet` or `--keystore` read the address without a password.

## Password sources

A command reads a password from exactly one source:

| Option | Source |
| - | - |
| `--password-env <NAME>` | The environment variable `NAME`. It is the one environment variable the command reads |
| `--password-file <path>` | The file's text. The file must be readable and writable by you alone: a file whose mode lets the group or others in is refused. Run `chmod 600` on it |
| `--password-command <command>` | What the command prints, such as a secret manager's CLI (`pass show sauce`). It runs through the shell with no stdin. If it fails, the error gives its exit status and never its output |
| none | The prompt, at the terminal, without echo. A new wallet's password is asked twice. With no terminal the command refuses |

`--password-file` and `--password-command` drop one trailing newline. A wallet's password is at least 8 characters.

There is no `--password <value>` option: a value on the command line lands in shell history and the process list. Every key and password a command was given is filtered out of what it prints, error messages included.

A signing command with the password from the environment:

```bash theme={null}
export SAUCE_WALLET_PASSWORD='<password>'
sauce reclaim --chain solana --wallet agent-sol --wallets ./wallets --password-env SAUCE_WALLET_PASSWORD
```

```text theme={null}
note: wallet agent-sol has no policy, so no limits are set on what it sends: wallets/agent-sol.policy.json sets them (per transaction and per day, by chain, by recipient, native and per token)
there is nothing to close now
CEJT6RKEqjgbg7rCpnPZwxNCDie1PmdEWKrRZzqJHyCK has 0 code buffers and 0 Sauce lookup tables open, holding 0 SOL in rent
```

## Signers

A command that sends takes one signer:

| Option | Signer |
| - | - |
| `--wallet <name>` | A wallet from the wallet directory. An EVM wallet signs on Ethereum, Base, Arc and the EVM testnets; a Solana wallet signs on Solana, Solana Devnet and Solana Testnet |
| `--keystore <path>` | A JSON keystore file (v3), as `cast wallet` and geth write. It takes the same password sources, and applies no policy, even to a wallet's own file. EVM chains only |
| `--private-key <hex>` | A raw key. It works, and prints a warning: the key lands in your shell history and the process list |
| `--keypair <path>` | A Solana keypair file: the JSON array of 64 bytes `solana-keygen` writes. Solana only |

For `sauce run` and for a cook built with `sauce tx`, the signer is the Pot's owner, because `cook()` accepts calls only from the owner. On Solana with `--via engine`, which uses no Pot, the signer is the authority the program acts with ([Solana from the CLI](https://docs.eco.com/sauce-sdk/cli/solana.md)).

`sauce pot --deploy` is the exception on EVM chains: deploying a Pot is permissionless, so with `--owner` the signer sends and pays for another owner's Pot, which belongs to that owner alone. On Solana the owner signs `create_pot`, so the signer is the owner.

Which commands ask for the wallet's password:

* `sauce pot` without `--deploy` takes `--wallet` only to learn the owner's address, and asks for no password. With `--deploy` it signs, so it needs the password.
* `sauce simulate` takes `--wallet` the same way and asks for no password, except with `--fund`, where it signs the token pulls it simulates.
* `sauce run`, `sauce reclaim` and the other commands that send ask for the password.

A key that must never reach this machine signs elsewhere: [`sauce tx`](https://docs.eco.com/sauce-sdk/cli/tx) prints the unsigned transaction and [`sauce send`](https://docs.eco.com/sauce-sdk/cli/send) sends the signed one.

Every send from a wallet is held to the supported chains: Ethereum, Base, Arc and Solana, and the testnets Arc Testnet, Base Sepolia, Ethereum Sepolia, Solana Devnet and Solana Testnet. A transaction on a preview chain is refused before it is measured or signed.

## The spending policy

A policy is a file `<name>.policy.json` beside the wallet. Every field is optional:

```json agent.policy.json theme={null}
{
  "chains": ["base", "ethereum"],
  "to": ["0xeD641fe8BC35F8074CFA899a8F4137c270d37dB4"],
  "native": { "perTransaction": "0.01", "perDay": "0.05" },
  "tokens": {
    "USDC": { "perTransaction": "100", "perDay": "500" },
    "WETH": { "perDay": "250000000000000000n" }
  }
}
```

The address in `to` is the wallet's own Pot on Base, as `sauce pot --chain base --wallet agent` prints it: a cook is a transaction to the Pot.

| Field | Type | Limits |
| - | - | - |
| `chains` | List of chain slugs, aliases or ids | The only chains the wallet sends on |
| `to` | List of addresses | The only addresses a transaction may be sent to: its target, not where the program it runs pays. On Solana, the programs its instructions call, the compute-budget program aside |
| `native` | `{ "perTransaction", "perDay" }` | The chain's native coin (ETH, SOL, Arc's USDC) leaving the wallet or the Pot the transaction cooks on |
| `tokens` | Object of token to `{ "perTransaction", "perDay" }` | Each token leaving the wallet or the Pot the transaction cooks on, and what another key is left allowed to take of it |

### Amounts

An amount is a string:

* whole tokens: `"100"` is 100 USDC, `"0.01"` is 0.01 ETH;
* base units, ending in `n`: `"250000000000000000n"` is 0.25 WETH.

A bare integer at or above 10^decimals of a registry token is read as whole tokens, with a note that says so:

```text theme={null}
note: wallets/trader-sol.policy.json: tokens.USDC.perTransaction: "1500000" is 1.5 million USDC; for base units write "1500000n"
```

### What each limit counts

* **`perTransaction`** caps what one transaction moves.
* **`perDay`** caps what moved in the last 24 hours, every chain together, counted from the ledger.
* **`to`.** Deploying the wallet's own Pot through the released Kitchen counts as a transaction to that Pot. A policy that lets the wallet cook on its Pot therefore lets a command deploy that Pot first.
* **`native`** counts the chain's native coin leaving the wallet or the Pot the transaction cooks on. On Arc, the native coin and the `USDC` token are one balance: native USDC sent and a USDC allowance left count together against `native` and against a `USDC` limit.
* **`native` on Solana** counts every account the wallet controls, net: the wallet and the Pot, their token accounts (wrapped SOL among them), durable nonce accounts whose authority is the wallet, and stake accounts whose withdrawer is the wallet. Wrapping SOL into the wallet's own account and unwrapping it count only the fee. Rent paid into the wallet's own token account, or into the Pot a cook creates, stays the wallet's; rent paid into an account the wallet does not control (another key's token account, a code buffer, a lookup table) counts, and so does the fee. A wallet the transaction gives to another program, or makes a durable nonce account another key commands, counts all it holds.
* **`tokens`** are named by registry symbol (in any case) or by address: an ERC-20 or a Solana mint. They count what leaves the wallet or its Pot, and the allowances below. A token the policy does not name is not limited.

### Allowances count as sent

An allowance a transaction leaves another key counts as sent, at the whole allowance, per transaction and in the day's total, together with what the transaction moves of the token: the spender needs no later signature.

* An approval above what a token's limit leaves room for is refused, naming the limit.
* A revoke, an allowance spent within the transaction (it counts as what it moved), and a spender that is the wallet or its own Pot count nothing.
* Each allowance is its own permission, whoever gives it (the wallet or its Pot) and whichever contract it stands on (the token or Permit2). Changing one leaves the others as they are, and they add up.
* Where a limited token's decimals cannot be read, an approval of it is refused, as a transfer of it is.

The rule behind these: an allowance of a limited token to anyone but the wallet or its own Pot counts only where it is measured exactly, and is refused where it is not. On Arc the same holds for USDC under a `native` limit.

**On EVM chains**, each allowance is read as the chain holds it once the transaction is done, one per contract, owner, token and spender:

* An ERC-20's own allowance, and one granted through Permit2 (its `approve`, or a `permit` the wallet signed), which lets the spender take the token wherever its owner approved Permit2 for it. Permit2's `lockdown` clears one. A Permit2 grant counts at its amount, whatever its expiration, and only the registry's Permit2 is read.
* Each allowance is read in a simulation of its own that runs the transaction's calls again on the block the transaction was simulated on, at the same time. The read counts only where that simulation is the transaction's own execution again: the node reports the same block, and every call succeeds with the same logs, return data and gas used. An allowance that cannot be read so (the endpoint or the token does not answer, the endpoint cannot serve the block, or the calls do not run again as they ran) is refused where its token is limited.
* An allowance is seen where the transaction reports it: a token's `Approval` event, or Permit2's own events. A token that sets an allowance with no event is not seen.
* **An approval of Permit2 for a limited token is refused**, from the wallet or from its Pot in a cook. It makes spendable again every Permit2 allowance of that token given before, which the transaction does not report. Under such a limit, fund with `--fund-mode permit`, `allowance` or `transfer` ([Funding](https://docs.eco.com/sauce-sdk/cli/funding.md#permit2-and-the-first-time-approval)), or approve Permit2 from a wallet without the limit.

On a local fork of Base at block 53011315 with the current contracts, a wallet named `agent` whose policy is `{ "tokens": { "USDC": { "perTransaction": "1", "perDay": "1" } } }` ran a program that calls `USDC.approve(Permit2.Permit2, <max>)` and then `Permit2.Permit2.approve(USDC, spender, 10000000, <expiration>)`, with 10 USDC in its Pot:

```text theme={null}
sauce run: refused by the policy of wallet agent (wallets/agent.policy.json): this transaction approves Permit2 (0x000000000022D473030F116dDEE9F6B43aC78BA3) for USDC, which lets each standing Permit2 allowance of it be taken with no further signature, and tokens.USDC limits it
```

The command exited `1` and sent nothing: the wallet's nonce, the Pot's USDC and both allowances were unchanged, and the ledger gained no line.

**The wallet's own Pot.** A transaction counts a Pot as the wallet's own only where it is: a Pot the plan deploys for the wallet, or one whose owner the chain shows to be the wallet. A cook's calldata sent anywhere else, to any other address or to another owner's Pot, is a payment like any other. A cook on a Pot whose owner the endpoint cannot read (the request fails, or the endpoint answers with an error of its own rather than the contract's revert) is refused where a `native` or `tokens` limit applies.

**On Solana**, an allowance is a delegate set or raised (SPL Token `Approve` or `ApproveChecked`) on a token account of the wallet or of a Pot the transaction cooks on:

* An allowance raised and partly spent in the same transaction counts what it spent and what is left, also where it ends where it began. That holds whether the wallet's own instruction raises it or a call a program makes, a Pot's cook or a token `Batch` included. A token program's call the endpoint reports so that an approval in it cannot be read is refused where a `native` or token limit applies.
* Wrapped SOL is measured by its lamports, synced into its amount or not. Wrapped SOL an outside delegate may move counts against `native` too, and SOL the wallet sends into such an account is no longer its own once it is there.
* What the wallet spends as a delegate from a token account it does not own counts as sent, against `native` too for wrapped SOL: from a Pot's account in a transaction with no cook, and from any other key's.
* A token account of the wallet or the Pot handed to another owner is a send of all it holds, a transfer out of it in the same transaction included; so is an account closed and made again at its address for another token. A token account whose close authority goes to another key is refused where its token is limited, and so is a Token-2022 mint whose permanent delegate goes from the wallet to another key.
* A balance counts in its mint's decimals as they were before the transaction, also where the transaction makes the mint again with other decimals and leaves the raw amount as it was. Where a limited token's decimals cannot be read, a transaction moving it is refused; other limits are not affected.
* The measurement errs toward counting: a spend by the account's owner beside an outside allowance that stands may count as if the allowance were raised, and a delegate's spend from another key's account counts though nothing of the wallet's left. Wrapped SOL counts against `native` and against a wrapped-SOL token limit both.

A Token-2022 mint's permanent delegate that the wallet never held moves tokens with no approval from the wallet: no policy limits it. ERC-721 and ERC-1155 transfers and approvals, `setApprovalForAll` included, are not counted either.

### How a send is measured

What a transaction moves is measured, not read from its calldata:

* An EVM transaction is made whole before the policy measures it: the gas it will be signed with (1.2 times its estimate), its fees, its nonce and its chain. It is simulated with `eth_simulateV1` on the chain's head, with native transfers traced, and every allowance read runs with the same gas and fees, since a program can read both. The wallet then signs exactly that transaction. The ERC-20 `Transfer` and `Approval` logs, Permit2's grants and the native transfers out of the wallet and its Pot are what count.
* A Solana transaction is simulated with the accounts it writes reported. The wallet's and its Pots' lamports and token accounts are compared before and after. A Pot is the account each Kitchen cook in the transaction names, read through the transaction's lookup tables when it loads accounts from them, so a Pot at any bump the Kitchen accepts is measured. That includes a Kitchen cook that another program the transaction calls makes for the wallet.
* A Solana transaction is held to the policy of every wallet the command has unlocked that it needs a signature from, whichever of them pays its fee. A planned transaction another tool built must be paid for by the wallet that sends it.
* Every simulation of a transaction a wallet signs is unsigned, a swap's stops in `sauce run` included: the wallet signs only a transaction its policy has let go, so an endpoint never holds one it could send itself.

What cannot be measured is refused wherever a limit needs it: an endpoint without `eth_simulateV1`, a cook whose native transfers are not traced, an allowance no simulation reads exactly, a cook on a Pot whose owner cannot be read, a Solana transaction whose lookup tables cannot be read, and a call to the Kitchen the endpoint reports without its data, its owner or its Pot account.

A send that breaks a limit is refused before anything is signed. The refusal names the wallet, its policy file and the limit, in the form `refused by the policy of wallet <name> (<policy file>): tokens.USDC.perTransaction is 100 USDC, and this transaction sends 250 USDC`.

A simulation shows what a transaction does on the state it ran on: the chain's head, a moment before the transaction is sent. A program that reads the block it lands in (its time, its base fee, what another transaction changed first) may do something else there, and no simulation bounds that. A policy limits what the transaction does as simulated. A bound that must hold in whatever block the transaction lands in is the program's own, such as a `require` on what it sends.

### Signed cooks

[`sauce sign`](https://docs.eco.com/sauce-sdk/cli/sign) signs a cook that another account sends later, on what the chain holds then, so a simulation when you sign cannot measure what it will move. A wallet whose policy sets `native` or any `tokens` limit refuses to sign one, naming the limit:

```text theme={null}
sauce sign: refused by the policy of wallet agent (wallets/agent.policy.json): tokens.USDC is limited, and a signed cook runs when its submitter sends it, any time up to its deadline, on the chain's state then: what it moves cannot be measured when it is signed, nor counted in a day's total. Sign with a wallet whose policy limits only chains and to
```

`chains` and `to` still apply: the cook's chain, and its Pot as the address. A policy with `"chains": ["ethereum"]` refuses a cook on Base (`chains allows ethereum, and this cook is on base`), and one with `"chains": ["base"]` alone signs it. Sign from a wallet without `native` or `tokens` limits, or with the key kept elsewhere.

### Funding a run under a policy

Before `--fund` signs a pull or prepares a funding transaction, the wallet's policy checks the chain and the Pot against `chains` and `to`. A transaction the run sends before the cook is then checked against `to` with its actual destination, the token contract: the `approve` of `--fund-mode allowance`, the `transfer` of `--fund-mode transfer`, and the first approval of Permit2 under `--fund-mode permit2`. A policy whose `to` lists only the Pot refuses all three. On a local fork of Base at block 53116964 with the current contracts, with `"to": ["<the wallet's Pot>"]`, `sauce run keep.sauce.ts --chain base --wallet docs --fund USDC:1 --fund-mode transfer --yes` printed:

```text theme={null}
sauce run: refused by the policy of wallet docs (wallets/docs.policy.json): to allows 1 address(es), and this transaction goes to 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913
```

That address is Base's `USDC`. Adding it to `to` let the `allowance`, `transfer` and `permit2` runs through on the same fork. `--fund-mode permit`, and `permit2` once Permit2 is approved, send nothing before the cook and pass with the Pot alone.

`--fund` signs and sends only for the wallet's own Pot: one the chain shows is the wallet's, or the one the run deploys first ([Funding](https://docs.eco.com/sauce-sdk/cli/funding.md)). What the funding lets the Pot take is spent only by a cook on that Pot, and that cook is measured and held to the `native` and `tokens` limits when it is sent.

Under a token limit, the approval of Permit2 that the default `--fund-mode permit2` sends first is refused. On the fork at block 53011315 above, `sauce run keep.sauce.ts --chain base --wallet agent --fund USDC:0.5 --yes` printed:

```text theme={null}
auto-approved: approve Permit2 for USDC
sauce run: refused by the policy of wallet agent (wallets/agent.policy.json): this transaction approves Permit2 (0x000000000022D473030F116dDEE9F6B43aC78BA3) for USDC, which lets each standing Permit2 allowance of it be taken with no further signature, and tokens.USDC limits it
```

Nothing was sent. The same run with `--fund-mode permit` pulled 0.5 USDC into the Pot and succeeded.

### How the file is checked

Every command that signs with `--wallet` reads the policy first, before it asks for the password. Signing the same file with `--keystore`, `--private-key`, `cast wallet` or geth applies no policy, and anyone who has the file and its password can loosen or ignore it ([Agents, wallets and keys](https://docs.eco.com/sauce/security-considerations.md#agents-wallets-and-keys)).

* An unknown field is an error, so a typo never lifts a limit:

  ```text theme={null}
  sauce reclaim: wallets/agent-sol.policy.json: tokens.USDC.perTx is not a limit: perTransaction or perDay
  ```

* A file that others can write is refused, because they could loosen it. Run `chmod 600` on it. On Windows the command does not check file permissions.

* `sauce wallet list` marks each wallet that has a policy with `(policy)`, and prints a warning for a policy that would be refused:

  ```text theme={null}
  warning: wallets/agent-sol.policy.json: chains names "sol-net", which is not a chain
  ```

With no policy file, sends work, and each signing command notes that no limits are set:

```text theme={null}
note: wallet agent-sol has no policy, so no limits are set on what it sends: wallets/agent-sol.policy.json sets them (per transaction and per day, by chain, by recipient, native and per token)
```

## The ledger

`<name>.ledger.jsonl` records what the wallet spent, one JSON object per line, appended with mode 0600. Each send writes two lines, linked by `id`. Their shape:

```json theme={null}
{ "v": 1, "id": "<uuid>", "state": "reserved", "time": "<ISO time>", "chain": "base", "spend": { "tokens.USDC": { "amount": "100000000", "decimals": 6 } } }
{ "v": 1, "id": "<uuid>", "state": "sent", "time": "<ISO time>", "tx": "<transaction hash>" }
```

* **`reserved`** is written when the transaction passes the policy, before it is sent. `spend` holds, per limit, what the transaction counts against it, in base units.
* **`sent`** follows once the transaction is sent, with its hash.
* **`released`** follows instead, with `why`, when the transaction is not sent. A released reservation no longer counts.

The check and the reservation happen under the wallet's lock file, so two processes cannot both spend the last of a day's limit. `perDay` adds every reservation from the last 24 hours that was not released. Without a policy, nothing is written to the ledger.

A ledger line that is not JSON, such as one cut short by a crash, makes every limited send refuse until you remove that line, because the day's spending can no longer be counted. A missing ledger counts nothing spent, so deleting it restarts every `perDay` count.

## Next steps

* [`sauce run`](https://docs.eco.com/sauce-sdk/cli/run): sign and send a program with `--wallet`.
* [MCP wallets](https://docs.eco.com/sauce-sdk/mcp/wallets.md): the same wallets and policies, signing for an agent over stdio.
* [Production checklist](https://docs.eco.com/sauce-sdk/operating/production-checklist.md): before a wallet holds real funds.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.