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 covers what the files, the passwords and the policy protect against, and what they do not.
Prerequisites
- The
saucecommand:npm install -g @eco-incorp/sauce(Installation). - A wallet, made with
sauce wallet neworsauce 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:
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 andcast wallet write:
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:--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:
Signers
A command that sends takes one signer:
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).
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 potwithout--deploytakes--walletonly to learn the owner’s address, and asks for no password. With--deployit signs, so it needs the password.sauce simulatetakes--walletthe same way and asks for no password, except with--fund, where it signs the token pulls it simulates.sauce run,sauce reclaimand the other commands that send ask for the password.
sauce tx prints the unsigned transaction and sauce 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:
agent.policy.json
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.
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.
What each limit counts
perTransactioncaps what one transaction moves.perDaycaps 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.nativecounts the chain’s native coin leaving the wallet or the Pot the transaction cooks on. On Arc, the native coin and theUSDCtoken are one balance: native USDC sent and a USDC allowance left count together againstnativeand against aUSDClimit.nativeon 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.tokensare 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.
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 apermitthe wallet signed), which lets the spender take the token wherever its owner approved Permit2 for it. Permit2’slockdownclears 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
Approvalevent, 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,allowanceortransfer(Funding), or approve Permit2 from a wallet without the limit.
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:
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
Batchincluded. A token program’s call the endpoint reports so that an approval in it cannot be read is refused where anativeor 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
nativetoo, 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
nativetoo 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
nativeand against a wrapped-SOL token limit both.
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_simulateV1on 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-20TransferandApprovallogs, 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 runincluded: the wallet signs only a transaction its policy has let go, so an endpoint never holds one it could send itself.
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 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:
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:
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). 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:
--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).
-
An unknown field is an error, so a typo never lifts a limit:
-
A file that others can write is refused, because they could loosen it. Run
chmod 600on it. On Windows the command does not check file permissions. -
sauce wallet listmarks each wallet that has a policy with(policy), and prints a warning for a policy that would be refused:
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:
reservedis written when the transaction passes the policy, before it is sent.spendholds, per limit, what the transaction counts against it, in base units.sentfollows once the transaction is sent, with its hash.releasedfollows instead, withwhy, when the transaction is not sent. A released reservation no longer counts.
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: sign and send a program with--wallet.- MCP wallets: the same wallets and policies, signing for an agent over stdio.
- Production checklist: before a wallet holds real funds.
