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

# Getting started with Sauce

> Install @eco-incorp/sauce, build a first Eco intent that runs a Sauce program on Base, prepare the Executor-owned Pot and the Kitchen fee, fund it on the source chain, run a program directly through your own Pot, and execute on Solana through the Kitchen client.

This guide takes a program from source to a funded Eco intent, then shows the two other ways to execute: directly through your own Pot on the EVM, and through the Kitchen client on Solana. The examples use the APIs documented in the SDK's [bundled manual](https://unpkg.com/@eco-incorp/sauce@0.99.4/docs/README.md).

## Prerequisites

* Node 24 or later
* `npm install --save-exact @eco-incorp/sauce@0.99.4`; the compiler comes with it
* `viem` for chain reads and address formatting
* Access to Sauce, which is closed access: [contact Eco](mailto:partners@eco.com)

**1. Build the intent**

`Base(body, options).reward(reward)` compiles the body for Base and packages it as an Eco intent. The body is read as source and compiled, not run as JavaScript, so it captures nothing: every external value goes through `defines`. Route assets delivered by the solver arrive in the Portal's Executor, and the program spends from the Pot, so a `precalls` transfer moves them across first.

```typescript first-intent.ts theme={null}
import { routes, token } from "@eco-incorp/sauce";
import { hexToBigInt, toHex, type Address } from "viem";

export interface TransferIntentConfig {
  pot: Address;
  engine: Address;
  executionFee: bigint;
  portal: Address;
  sourcePortal: Address;
  creator: Address;
  prover: Address;
  recipient: Address;
  deadline: bigint;
  rewardDeadline: bigint;
  rewardAmount: bigint;
}

// Build after prepareDestinationPot returns ready: true.
// Use Portal and prover addresses from your Eco integration configuration.
export function buildTransferIntent(config: TransferIntentConfig) {
  const baseUsdcValue = routes.registryTokenAddress(Base.chain, "USDC");
  const sourceUsdcValue = routes.registryTokenAddress(Ethereum.chain, "USDC");
  if (baseUsdcValue === undefined || sourceUsdcValue === undefined) {
    throw new Error("Missing USDC registry entry");
  }
  const baseUsdc = toHex(baseUsdcValue, { size: 20 });
  const sourceUsdc = toHex(sourceUsdcValue, { size: 20 });
  const recipient = hexToBigInt(config.recipient);
  const amount = 1_000_000n; // 1 USDC on Base.

  return Base(
    () => {
      USDC.transfer(recipient, amount);
    },
    {
      execution: {
        pot: config.pot,
        engine: config.engine,
        value: config.executionFee,
      },
      nativeAmount: config.executionFee,
      portal: config.portal,
      sourcePortal: config.sourcePortal,
      deadline: config.deadline,
      defines: { recipient, amount },
      tokens: [{ token: baseUsdc, amount }],
      precalls: [token.Token.transfer({
        token: baseUsdc, to: config.pot, amount,
      }).toCall()],
    },
  ).reward({
    source: Ethereum.chain,
    creator: config.creator,
    prover: config.prover,
    deadline: config.rewardDeadline,
    tokens: [{ token: sourceUsdc, amount: config.rewardAmount }],
  });
}
```

Deadlines are absolute Unix seconds. `route.deadline` bounds fulfillment on the destination; `reward.deadline` is when the source funds become refundable, so leave room for fulfillment and proof delivery. The source reward must cover the solver's delivery, fees and gas; the SDK does not price it.

**2. Prepare the destination Pot and fee**

A direct intent route executes through a Pot owned by the destination Portal's **Executor**. Any other owner rejects the call with `NotOwner()`. Every `cook` also pays the Kitchen's current `executionFee()` in native value, including programs that move only ERC-20s. This helper, from the SDK examples, reads the Executor, resolves the release Kitchen and engine, predicts the Pot, returns a deployment request when the Pot does not exist yet, and quotes the fee.

```typescript evm-execution.ts theme={null}
import { routes } from "@eco-incorp/sauce";
import { v12EngineAddress, v12KitchenAddress } from "@eco-incorp/sauce/deployments";
import { v12Kitchen, v12Pot } from "@eco-incorp/sauce/evm/engine";
import { getAddress, parseAbi, type Address, type Hex, type PublicClient } from "viem";

const portalAbi = parseAbi(["function executor() view returns (address)"]);
const feeAbi = parseAbi(["function executionFee() view returns (uint256)"]);
const potKitchenAbi = parseAbi(["function kitchen() view returns (address)"]);

// Call with a client for the destination chain. Importing this example performs no RPC.
export async function prepareDestinationPot(client: PublicClient, portal: Address, salt: Hex) {
  const kitchen = getAddress(v12KitchenAddress());
  const engine = getAddress(v12EngineAddress());
  const [kitchenCode, engineCode] = await Promise.all([
    client.getCode({ address: kitchen }),
    client.getCode({ address: engine }),
  ]);
  if (!kitchenCode || kitchenCode === "0x" || !engineCode || engineCode === "0x") {
    throw new Error("Current Kitchen and engine must be deployed on the destination chain");
  }

  const executor = await client.readContract({
    address: portal,
    abi: portalAbi,
    functionName: "executor",
  });
  const pot = await client.readContract({
    address: kitchen,
    abi: v12Kitchen.abi,
    functionName: "predictPot",
    args: [executor, salt],
  });
  const executionFee = await client.readContract({
    address: kitchen,
    abi: feeAbi,
    functionName: "executionFee",
  });
  const context = { kitchen, engine, executor, pot, executionFee };
  const code = await client.getCode({ address: pot });
  if (!code || code === "0x") {
    const deploy = routes.sauceEvmDeployPotCall({ kitchen, owner: executor, salt });
    // Send this destination-chain request with your signer, wait for a successful receipt,
    // then call prepareDestinationPot again. Do not build a route until ready is true.
    return {
      ...context,
      ready: false as const,
      deployment: { to: kitchen, data: deploy.data, value: 0n },
    };
  }

  const [owner, actualKitchen] = await Promise.all([
    client.readContract({ address: pot, abi: v12Pot.abi, functionName: "owner" }),
    client.readContract({ address: pot, abi: potKitchenAbi, functionName: "kitchen" }),
  ]);
  if (getAddress(owner) !== getAddress(executor) || getAddress(actualKitchen) !== kitchen) {
    throw new Error("Pot must belong to this Portal Executor and the current Kitchen");
  }
  return { ...context, ready: true as const };
}
```

Pass the returned `pot`, `engine` and `executionFee` to `buildTransferIntent` only when `ready` is `true`. Set `execution.value` to the fee plus any native value the program spends, and budget `nativeAmount` for every call value in the route. Read the fee shortly before building: Eco can change it per chain, and an increase can make the cook revert or reduce the native value available to the program.

A Pot owned by the Executor can be called by routes through that Portal. Deliver assets, use them and return leftovers within the same fulfillment; do not leave funds or allowances in it.

**3. Fund on the source chain**

```typescript theme={null}
import type { routes } from "@eco-incorp/sauce";

export function fundingRequests(built: routes.BuiltIntent) {
  return [...built.approvals(), built.publishAndFundCalldata(false)];
}
```

Each request is `{ to, data, value }`. Send them from the funder on `built.source`, in order: the approvals authorize the source Portal to pull the reward into the intent vault, and `publishAndFundCalldata(false)` publishes and fully funds the intent. `built.hash()` returns the intent, route and reward hashes, and `built.vaultCall()`, `hashCall()` and `fundedCall()` return source-chain read requests with decoders. A solver then fulfills the intent on Base, where the Executor runs your program through the Pot.

**4. Run a program directly**

Outside an intent, compile with the SDK's compiler export and call `cook` on a Pot you own. The compiled program is reusable; `main` arguments are ABI-encoded and appended per execution.

```typescript theme={null}
import { compile } from "@eco-incorp/sauce/compiler";
import { v12Pot } from "@eco-incorp/sauce/evm/engine";
import { encodeAbiParameters, hexToBytes, bytesToHex } from "viem";

const source = `
function main(amounts: Uint256[], minimum: Uint256): Uint256 {
  let total = 0n;
  for (let i = 0; i < amounts.length; i = i + 1) {
    total = total + amounts[i];
  }
  require(total >= minimum);
  return total;
}
`;

const entry = new TextEncoder().encode(source);
const artifact = compile({
  target: "evm",
  entry: "main.ts",
  resolve: (path) => (path === "main.ts" ? entry : undefined),
});

const args = hexToBytes(
  encodeAbiParameters([{ type: "uint256[]" }, { type: "uint256" }], [[25n, 75n], 100n]),
);
const payload = new Uint8Array(artifact.bytecode.length + args.length);
payload.set(artifact.bytecode);
payload.set(args, artifact.bytecode.length);

// Given a compatible engine address:
// const data = v12Pot.encodeCook(engine, bytesToHex(payload));
// Send `data` from the Pot owner with value = executionFee + programNativeValue.
```

Simulate the complete call and estimate gas with your destination RPC before submitting. The Kitchen fee is separate from network gas.

Deploy your own Pot once with `Kitchen.deployPot(owner, salt)`; `cook` is owner-only. Standalone programs use compiler-native syntax, so contract calls need an ABI import such as `ERC20.at(token).approve(...)` rather than the SDK's `USDC` globals. See [Compiler API](/sdk-reference/sauce-compiler/api) for options such as `compactArgs`.

## Solana

Solana programs compile with `target: "svm"` and return an account manifest that names the accounts to attach. Execution goes through the SDK's Kitchen client with the released program ids. The client derives a Pot for its payer and resolves accounts against the manifest. Use `simulate` before sending; `maxExecutionFee` caps the lamports accepted for the Kitchen fee and defaults to zero, so set it from the Kitchen's fee configuration.

```typescript theme={null}
import { v12SvmEngineProgramId, v12SvmKitchenProgramId } from "@eco-incorp/sauce/deployments";
import { createSauceSvmClient, type AccountResolution } from "@eco-incorp/sauce/svm";
import type { CompileResult } from "@eco-incorp/sauce/compiler";
import { address, type TransactionSigner } from "@solana/kit";

export async function simulateProgram(
  rpcUrl: string,
  payer: TransactionSigner,
  fee: bigint,
  compiled: CompileResult,
  accounts: AccountResolution,
) {
  if (!compiled.manifest) throw new Error("Compile with target: svm");
  try {
    const client = await createSauceSvmClient({
      rpcUrl,
      payer,
      kitchen: address(v12SvmKitchenProgramId()),
      programId: address(v12SvmEngineProgramId()),
      maxExecutionFee: fee, // Read from the Kitchen's ExecutionFeeConfig account.
    });
    return await client.simulate(compiled.bytecode, compiled.manifest, accounts);
  } catch (error) {
    throw new Error("Could not simulate the Sauce program", { cause: error });
  }
}
```

The SDK's SVM route helpers (`Solana(body, ...)`, `buildSauceSvmCall`) emit a legacy instruction that the released engine does not accept, so do not distribute them as executable intents. The SDK's [Solana guide](https://unpkg.com/@eco-incorp/sauce@0.99.4/docs/guides/solana.md) covers account resolution, fee reads and staging larger programs.

## Troubleshooting

| Symptom | Check |
| - | - |
| A token or protocol name does not resolve | Check its destination-chain registry entry. For application values, supply `defines` rather than relying on a callback closure. |
| `NotOwner()` | The Pot owner must be the account calling `cook`. For a direct intent route, use the destination Portal's Executor. |
| Execution fails after a fee change | Read the Kitchen fee again and rebuild with enough value for the fee and the program. |
| No code at an engine or Kitchen address | Confirm the destination network and deployment before proceeding. |
| An RPC read or simulation fails | Check the RPC endpoint and inspect the returned error. Do not submit until the required reads and simulation succeed. |
| A Solana fee or account check fails | Check `maxExecutionFee`, the manifest's account order and roles, and the selected program IDs. |

## Next steps

> **[Programs as intents](/programmable-transactions/sauce/programs-as-intents)**
>
> Intent options, token and protocol globals, builders, nested intents.

> **[SauceScript reference](/programmable-transactions/sauce/saucescript)**
>
> The language: types, control flow, contract calls, builtins.

> **[Cookbook](/cookbook/overview)**
>
> Complete programs shipped in the packages.


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