> ## 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.

# Layout of a program

> How a .sauce.ts file is put together, for anyone writing or reading a Sauce program.

A Sauce program is one `.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.

| Form | Inputs | Returns a value |
| - | - | - |
| `function main(...)` | `main`'s parameters, passed with `--args` | yes |
| Top-level script: statements, no `main` | none | no |

## 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.

```ts payout.sauce.ts theme={null}
// Pays each of `recipients` `amount` of the chain's USDC, from the Pot.
//
//   sauce run payout.sauce.ts --chain <chain> --wallet <name> \
//     --args <recipients> <amount>
//
//   recipients: Address[]  who is paid, as a JSON array: '["<address>","<address>"]'
//   amount: Uint256        what each is paid, in USDC's smallest unit (6 decimals):
//                          5000000 is 5 USDC
//
// On Base, 5 USDC to each of two recipients:
//
//   sauce run payout.sauce.ts --chain base --wallet <name> \
//     --args '["<address>","<address>"]' 5000000
function main(recipients: Address[], amount: Uint256) {
  for (const to of recipients) {
    USDC.transfer(to, amount);
  }
}
```

`main` is the entry point. Code in the program cannot call it; see [Functions](https://docs.eco.com/sauce/functions.md#main).

### 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:

```bash theme={null}
sauce compile pay.sauce.ts --chain base --json --args <to> 5000000
```

```json theme={null}
{
  "size": 223,
  "payload": { "size": 287, "argumentsSize": 64 },
  "entryEncoding": "abi-v1",
  "entryParams": [
    { "name": "to", "type": "Address" },
    { "name": "amount", "type": "Uint256" }
  ]
}
```

`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](https://docs.eco.com/sauce/types.md#arrays)). [`sauce compile`](https://docs.eco.com/sauce-sdk/cli/compile) lists every output field; [Bytecode](https://docs.eco.com/sauce/bytecode.md) 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](https://docs.eco.com/sauce/types.md#accounts) and [EVM and Solana](https://docs.eco.com/sauce/targets.md).

### 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:

```ts pool-address.sauce.ts theme={null}
// The Uniswap V3 WETH/USDC pool at the 0.05% fee tier (500 millionths), or 0 where there is none.
//
//   sauce simulate pool-address.sauce.ts --chain base --owner <address>
function main(): Address {
  return UniswapV3.Factory.getPool(WETH, USDC, 500);
}
```

Declare the return type. On an EVM chain the cook returns:

| `main` returns | The cook returns | Example |
| - | - | - |
| `Uint256`, `Address`, `bool` | one 32-byte word | `bool` `true`: `0x…0001` |
| `Int256` | one 32-byte word, two's complement | `-5`: `0xff…fb` |
| a record | its fields, one word each | `{ a: 5, ok: true }`: `0x…0005` then `0x…0001` |
| an array | its elements, one word each, with no length or offset in front | `[5, 6, 7]`: three words |
| `bytes` | the bytes themselves | `tag.concat(x.toBytesBe(2))` with `tag = "ab"`, `x = 5`: `0x61620005` |
| `abi.encode(x, y)` as `bytes` | the standard ABI encoding: two words | `0x…0005`, then the address in the second word |
| `void`, or no return | nothing | `returns 0x` |

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 no `main` runs its statements in order, after its imports, as if they were the body of `main(): void`. It takes no inputs and cannot `return`.

```ts supply-all.sauce.ts theme={null}
// Supplies the Pot's whole USDC balance to Aave V3 for the Pot, so the aTokens stay in the
// Pot until another cook moves them.
//
//   sauce run supply-all.sauce.ts --chain base --wallet <name>
const held = pot.USDC;
require(held >= 1000000, "below 1 USDC");
USDC.approve(AaveV3.Pool, held);
AaveV3.Pool.supply(USDC, held, pot, 0);
```

A script takes no inputs, so it names its recipient in the source. To credit a wallet, declare `main` and take the wallet as a parameter ([Who receives what a call produces](https://docs.eco.com/sauce/contracts-and-abis.md#who-receives-what-a-call-produces)). Do not use `ctx.msgSender()` as the owner: for a signed cook it is the submitter ([Signed cooks](https://docs.eco.com/sauce/pot.md#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:

```ts script-const.sauce.ts theme={null}
// Reverts unless 6 is over the limit the script passes to its helper.
function over(x: Uint256, limit: Uint256): bool {
  return x > limit;
}

const limit = 5;
require(over(6, limit), "no");
```

On Solana a script that uses `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:

1. what the program does, in one line;
2. the `sauce` command that runs it, with `--chain <chain>` (a program does not name its chain) and `--args` naming every `main` parameter in order;
3. one line per parameter: `name: Type`, its meaning and its units, in smallest units (`5000000000` is 5,000 USDC, which has 6 decimals), with an array or record given as JSON;
4. 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:

| Value | Write it as | It arrives |
| - | - | - |
| What the caller chooses per run: tokens, amounts, recipients | a `main` parameter | with the cook, ABI-encoded after the bytecode |
| A value the program fixes: a fee, a pool's fee tier | a literal or a module `const` | folded into the bytecode |
| A value the chain supplies: a token, a protocol address, the chain id | a name (see [Names](https://docs.eco.com/sauce/names.md)) | resolved when compiling for that chain, then folded into the bytecode |

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.

**A `const` is a literal or a constant expression** over other constants, or a record of them. The compiler folds it:

```ts fee.sauce.ts theme={null}
// The amount left after a 0.30% fee, in the token's smallest unit.
const FEE_BPS = 30;
const DENOMINATOR = 10000;
const KEEP = DENOMINATOR - FEE_BPS;

function main(amount: Uint256): Uint256 {
  return (amount * KEEP) / DENOMINATOR;
}
```

This program compiles to the same bytes as one written with `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:

```ts fee-tiers.sauce.ts theme={null}
const FEES = [100, 500, 3000, 10000];

function main(): Uint256 {
  return FEES[2];
}
```

A loop over the table, its `.length` and a write to it are refused, and so is a computed index. `FEES[i]` with `i` a `main` parameter fails with:

```text theme={null}
a module-scope lookup table can only be indexed by a non-negative integer literal or a `const` bound to one (`T[2]`) — the position has to be known as written, so a `let` binding, a loop counter, or a computed index (`-1`, `1n + 1n`) is not accepted. Declare the table inside the function to index it at run time
```

Declare an array you loop over or index at run time inside a function:

```ts fee-tier-at.sauce.ts theme={null}
// Returns the fee tier at index `i` (0 to 3); an index past the end reverts.
function main(i: Uint256): Uint256 {
  const fees = [100, 500, 3000, 10000];
  return fees[i];
}
```

**A `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.

```ts scratch.sauce.ts theme={null}
let calls = 0;

function bump(): void {
  calls = calls + 1;
}

function main(): Uint256 {
  bump();
  bump();
  return calls;
}
```

On a local fork of Base at block 52965000 with the current contracts, this returns 2 on every run. Any other initializer (an array, bytes, a record, an expression, a call) is refused. `let xs = [1, 2];` at module level fails with the message below; declare such a variable inside a function:

```text theme={null}
a module-scope `let xs` must be initialized with a literal — a number, address or boolean literal. Anything computed (an expression, a call, another binding's name) has no place to be evaluated at module scope; declare it inside a function instead
```

Persistent state lives in the Pot's storage, through `evm.sstore` and `evm.sload` on EVM and `svm.sstore` and `svm.sload` on Solana. See [Builtins](https://docs.eco.com/sauce/builtins.md).

## Imports

| Import | Binds |
| - | - |
| `import { transfer } from "@sauce/token"` | token functions, for any token; see [`@sauce/token`](https://docs.eco.com/sauce-libraries/token) |
| `import { uniswapV3 } from "@sauce/router"` | one swap function per venue: the EVM venues on an EVM chain, the Solana venues on Solana; see [`@sauce/router`](https://docs.eco.com/sauce-libraries/router) |
| `import { f, FEE } from "./lib.ts"` (also `.js`, `.mjs`, `.mts`) | exported functions, constants and interfaces from a SauceScript file, or from its arm for the target (`lib.evm.ts`, `lib.svm.ts`); see [One module, an arm per target](#one-module-an-arm-per-target) |
| `import cfg from "./config.json" with { type: "json" }` | JSON data, read as compile-time constants |
| `import ERC20 from "./erc20.abi.json"` | low level: a contract ABI, for a contract no namespace covers; see [Contracts and ABIs](https://docs.eco.com/sauce/contracts-and-abis.md) |

Token symbols, protocol namespaces and the `@sauce/token` functions need no import: `USDC.transfer(to, amount)` works as written.

```ts imports.sauce.ts theme={null}
import { uniswapV3 } from "@sauce/router";

// Swaps `amountIn` USDC for WETH at one Uniswap V3 pool, with no price limit (the 0) and at
// least `minOut` WETH sent to the Pot.
function main(pool: Address, amountIn: Uint256, minOut: Uint256) {
  uniswapV3(pool, USDC, WETH, amountIn, 0, minOut, pot);
}
```

`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:

```text theme={null}
subpath imports are not supported: "@sauce/token/evm" names a subpath of "@sauce/token" — import the package root "@sauce/token" instead
```

An exported function or constant of a relative module is a plain function or constant of the program. A relative import resolves from beside the file; through the MCP server, pass the file in the call's `modules`. A JSON file imported as data:

```json config.json theme={null}
{ "fee": 30 }
```

```ts config.sauce.ts theme={null}
import cfg from "./config.json" with { type: "json" };

function main(amount: Uint256): Uint256 {
  return (amount * cfg.fee) / 10000;
}
```

### 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:

```ts fee.evm.ts theme={null}
// The fee on an EVM chain: 30 basis points.
export function fee(amount: Uint256): Uint256 {
  return (amount * 30) / 10000;
}
```

```ts fee.svm.ts theme={null}
// The fee on Solana: 25 basis points.
export function fee(amount: Uint256): Uint256 {
  return (amount * 25) / 10000;
}
```

```ts pay-fee.sauce.ts theme={null}
import { fee } from "./fee.ts";

function main(amount: Uint256): Uint256 {
  return fee(amount);
}
```

`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.ts` finds `fee.evm.ts` and `fee.svm.ts`; `./fee.js` finds `fee.evm.js` and `fee.svm.js`. An import with no extension (`./fee`) finds neither.

* **A neutral file is the fallback.** With `fee.evm.ts` and `fee.ts` beside each other, an EVM compile takes `fee.evm.ts` and a Solana compile takes `fee.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 gives `failed 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:

  ```text theme={null}
  import the neutral module "./fee.ts", not the target arm "./fee.evm.ts" — the compiler selects the per-target arm for you
  ```

* **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 `modules` under 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](https://docs.eco.com/sauce-libraries/overview.md).

## Helpers

A helper is a top-level `function` declaration. Helpers may call each other and recurse. See [Functions](https://docs.eco.com/sauce/functions.md) 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.

```ts annotations.sauce.ts theme={null}
function half(x: Uint256): Uint256 {
  return x / 2;
}

function main(amount: Uint256, flag: bool): Uint256 {
  let total = 0;
  const limit: Uint256 = 100;
  if (flag) {
    total = half(amount);
  }
  return total < limit ? total : limit;
}
```

A type name from Solidity is not a type: `a: uint256` fails with a message listing the valid names. [Types](https://docs.eco.com/sauce/types.md) 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 an `interface`, or write the fields inline:

```ts orders.sauce.ts theme={null}
interface Order {
  amount: Uint256;
  to: Address;
}

function pay(order: Order): void {
  USDC.transfer(order.to, order.amount);
}

function main(orders: Order[], extra: { amount: Uint256, to: Address }) {
  for (const order of orders) {
    pay(order);
  }
  pay(extra);
}
```

Fields are separated by `;` or `,`. An array or a record passed as a `main` argument is given to `--args` as JSON.

```bash theme={null}
sauce compile orders.sauce.ts --chain base \
  --args '[{"amount":5000000,"to":"<address>"}]' '{"amount":7000000,"to":"<address>"}'
```

## 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](https://docs.eco.com/sauce/errors.md#compile-errors).

## Next steps

> **[Types](https://docs.eco.com/sauce/types.md)**
>
> Numbers, bools, addresses, bytes, arrays, records and accounts.

> **[Expressions and control flow](https://docs.eco.com/sauce/expressions-and-control-flow.md)**
>
> Operators, loops, destructuring and what is rejected.

> **[Functions](https://docs.eco.com/sauce/functions.md)**
>
> Helpers, return paths, recursion and call cost.

> **[Contracts and ABIs](https://docs.eco.com/sauce/contracts-and-abis.md)**
>
> Calling a contract through a namespace or an ABI file.


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