.sauce.ts file, compiled for one chain. It takes one of two forms, and everything else in the file (imports, constants, helpers, interfaces) is optional.
A program with main
main’s parameters are the program’s inputs, taken in declaration order. Every value that changes from one run to the next is a parameter.
payout.sauce.ts
main is the entry point. Code in the program cannot call it; see Functions.
How the arguments reach the program
The compiler returns bytecode that does not contain the arguments. The cook carries the arguments after the bytecode, ABI-encoded, so one compiled program serves every value. ABI encoding is the only entry encoding, on EVM chains and on Solana.sauce compile --json reports main’s parameters and the size of the encoded arguments:
size is the bytecode, argumentsSize the two 32-byte words of arguments, and payload.size their sum. A parameter may be an integer (Uint256 or Int256), an address, a bool, bytes, an array or a record, with at most 4 levels of nested arrays and at most 16 KiB in one caller-supplied collection (Arrays). sauce compile lists every output field; Bytecode describes the format.
On Solana, Account parameters are not encoded arguments. They are the accounts the transaction attaches, in the order main declares them. See Types and EVM and Solana.
The return value
main may return a value. The cook returns it to its caller, and sauce simulate prints it after returns, so a program that only reads and returns is a way to read a chain:
pool-address.sauce.ts
Each example was simulated on a local fork of Base at block 52965000 with the current contracts.
sauce simulate also prints the value decoded by its type, such as main returns Int256: -5.
A top-level script
A file with nomain runs its statements in order, after its imports, as if they were the body of main(): void. It takes no inputs and cannot return.
supply-all.sauce.ts
main and take the wallet as a parameter (Who receives what a call produces). Do not use ctx.msgSender() as the owner: for a signed cook it is the submitter (Signed cooks).
A script’s top-level const and let are locals of that entry. A helper function cannot read them: a helper that reads a script’s const limit is refused with `limit` is not a declared variable, and the compiler prints a hint to make limit a parameter of main. Pass the value to the helper as an argument, or declare main and a module constant:
script-const.sauce.ts
pot.<TOKEN> or a token method compiles too: the accounts those need are added to the script’s main, which the compiler gives it. require(pot.USDC >= 1000000, "below 1 USDC"); as a Solana script compiles to a program whose manifest lists the Pot’s USDC account (pot:USDC) and the Pot’s signer slot (pot). A script that names pot is the Pot’s program, so a direct cook on the engine (--via engine), which has no Pot, refuses it and names the line and column of the mention; cook it through the Kitchen.
The header comment
Every program opens with a comment. It states, in order:- what the program does, in one line;
- the
saucecommand that runs it, with--chain <chain>(a program does not name its chain) and--argsnaming everymainparameter in order; - one line per parameter:
name: Type, its meaning and its units, in smallest units (5000000000is 5,000 USDC, which has 6 decimals), with an array or record given as JSON; - one concrete run on a supported chain, the developer’s own inputs as
<placeholders>.
payout.sauce.ts above follows it. A comment changes nothing about the compiled program.
How values reach a program
A program runs onchain and cannot ask the application that sent it for anything. A value reaches it in one of three ways:
A literal or a name changes the bytecode, and so its hash. A
main argument does not, so one compiled program serves every value. On Solana, accounts are a fourth channel: the transaction attaches them.
Module level
Three kinds of declaration sit outside any function: constants, lookup tables and scratch variables. Aconst is a literal or a constant expression over other constants, or a record of them. The compiler folds it:
fee.sauce.ts
9970 in place of KEEP. A const record is read by field name, VENUE.fee for const VENUE = { fee: 500 };, and folds the same way.
A const array is a lookup table. It is read only at a literal index:
fee-tiers.sauce.ts
.length and a write to it are refused, and so is a computed index. FEES[i] with i a main parameter fails with:
fee-tier-at.sauce.ts
let is scratch. It starts as a number, an address or a boolean literal, a negative integer included, and is reset on every run. It is not storage: nothing a let holds survives the cook.
scratch.sauce.ts
let xs = [1, 2]; at module level fails with the message below; declare such a variable inside a function:
evm.sstore and evm.sload on EVM and svm.sstore and svm.sload on Solana. See Builtins.
Imports
Token symbols, protocol namespaces and the
@sauce/token functions need no import: USDC.transfer(to, amount) works as written.
imports.sauce.ts
uniswapV3 is an EVM venue. The router’s Solana arm exports the Solana venues (raydiumClmm, orcaWhirlpool and the others) and not uniswapV3, so the same import fails on Solana with `uniswapV3` is not exported by module "/@eco-incorp/sauce/std/router/router.svm.js".
Import the package itself, never a file inside it. import { transfer } from "@sauce/token/evm"; fails with:
modules. A JSON file imported as data:
config.json
config.sauce.ts
One module, an arm per target
A relative import can resolve to a different file for each target. Name the module without a target (./fee.ts), and the compiler looks first for the arm of the target it compiles for: fee.evm.ts for an EVM chain, fee.svm.ts for Solana. One program then compiles for both, each target with its own code:
fee.evm.ts
fee.svm.ts
pay-fee.sauce.ts
sauce compile pay-fee.sauce.ts --chain base builds it with fee.evm.ts, and --chain solana with fee.svm.ts.
-
The arm keeps the import’s extension.
./fee.tsfindsfee.evm.tsandfee.svm.ts;./fee.jsfindsfee.evm.jsandfee.svm.js. An import with no extension (./fee) finds neither. -
A neutral file is the fallback. With
fee.evm.tsandfee.tsbeside each other, an EVM compile takesfee.evm.tsand a Solana compile takesfee.ts. Write shared code once in the neutral file, and an arm only for the target that differs. -
A target with neither fails at the import. With only
fee.evm.ts, compiling for Solana givesfailed to import module "./fee.ts". -
Each arm is its own module. The arms may export different names and different types. A name only one arm exports fails on the other target:
`bonus` is not exported by module "./fee.svm.ts". -
Import the neutral name, never an arm. Naming an arm is refused:
- It works at any depth. A neutral module can import an arm-split one, and each compile takes the arm for its target all the way down.
-
Through the MCP server, pass each arm in the call’s
modulesunder its own name (fee.evm.ts,fee.svm.ts).
@sauce/token and @sauce/router are built this way: one arm per target, picked by the compiler. See Libraries.
Helpers
A helper is a top-levelfunction declaration. Helpers may call each other and recurse. See Functions for the rules on return paths, annotations and what a call costs.
Annotations
An annotation goes where TypeScript puts one: on a parameter, a return type, or a variable. Without one:- a parameter is a
Uint256; - a local variable takes the type of its initializer, and keeps it;
- a function’s result is a number, even when it returns a comparison.
annotations.sauce.ts
a: uint256 fails with a message listing the valid names. Types lists every type and what is not supported.
Interfaces and inline record types
A record is a group of named fields. Name its type with aninterface, or write the fields inline:
orders.sauce.ts
; or ,. An array or a record passed as a main argument is given to --args as JSON.
Comments
// and /* */ comments are ignored by the compiler. sauce compile reports an error at its line and column. On a line the package rewrote, the error names the rewritten program, main.js; see Errors and reverts.
Next steps
Types
Numbers, bools, addresses, bytes, arrays, records and accounts.
Expressions and control flow
Operators, loops, destructuring and what is rejected.
Functions
Helpers, return paths, recursion and call cost.
Contracts and ABIs
Calling a contract through a namespace or an ABI file.
