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

# Distributed swap over Solana pools with a price limit

> A Solana cook that buys SPCX_US with up to a USDC budget across Raydium CLMM, Byreal and PancakeSwap CLMM pools found by key, never paying more than a price limit per share, fees included.

The program offers each pool in turn what is left of the budget, in the program's order and not in order of price, and each pool trades until its own fee-adjusted price reaches `priceLimit`. The cook reverts when the budget is not in the Pot, fewer than `minTokens` SPCX\_US are bought, or the average price is above `maxAvgPrice`; the SPCX\_US bought and the unspent USDC go to `recipient`.

**Target chains:** Solana. Use `--chain solana`. The example uses the `SPCX_US` Token-2022 mint.

## Prerequisites

You need the `sauce` command (`npm install -g @eco-incorp/sauce`), a Solana key to send from (a keypair file, or `sauce wallet new <name> --solana`), and the budget in the Pot's USDC token account. `--fund` is EVM only: transfer the USDC to that token account before running the cook, not to the Pot's address. `sauce pot --chain solana --owner <your-pubkey>` prints the Pot's address; use it as the token account's owner. See [Installation](https://docs.eco.com/sauce-sdk/installation.md).

## Program

`pot` is the Pot the program runs on, available without declaring it; `pot.USDC` reads its USDC token account ([Names](https://docs.eco.com/sauce/names.md#balances)).

```ts solana-distributed-swap.sauce.ts theme={null}
// Buys SPCX_US (Backpack's SpaceX share, a Token-2022 mint) with up to `budget` of the Pot's USDC
// as a limit order over the pools four keys find: no SPCX_US costs more than `priceLimit`, fees
// included. The keys are Raydium CLMM at fee 0.25% and 0.18%, Byreal at 0.1% and PancakeSwap CLMM
// at 0.08%; a key with no pool, an empty pool, a pool past its limit and a pool whose fee is not
// one rate are skipped.
// The cook reverts, and nothing moves, when the budget is not in the Pot, fewer than `minTokens`
// are bought, or the average is above `maxAvgPrice`. What was bought and the unspent USDC go to
// `recipient`.
//
//   sauce simulate solana-distributed-swap.sauce.ts --chain solana --owner <pubkey> \
//     --assume-balance USDC:<budget>n \
//     --args <budget> <priceLimit> <minTokens> <maxAvgPrice> <recipient>
//   sauce run solana-distributed-swap.sauce.ts --chain solana --keypair <keypair> \
//     --args <budget> <priceLimit> <minTokens> <maxAvgPrice> <recipient>
//
//   budget: Uint256       the most USDC to spend, in USDC's base units (6 decimals): 1000000000 is
//                         1,000 USDC; it must be in the Pot's USDC account
//   priceLimit: Uint256   the most one SPCX_US may cost, fees included: USDC base units per
//                         SPCX_US base unit, times 10^18; both have 6 decimals, so 150 USDC a
//                         share is 150000000000000000000
//   minTokens: Uint256    the least SPCX_US to buy, in its base units (6 decimals); less reverts
//   maxAvgPrice: Uint256  the most the average may cost, in priceLimit's units; 0 turns it off
//   recipient: Address    receives the SPCX_US bought and the USDC left unspent: a wallet's key
//
// Buy at least 6.666667 SPCX_US with up to 1,000 USDC, at most 150 USDC a share:
//
//   sauce simulate solana-distributed-swap.sauce.ts --chain solana --owner <pubkey> \
//     --assume-balance USDC:1000 --args 1000000000 150000000000000000000 6666667 0 <recipient>

// Each pool is found by its key: `Raydium.CLMM.pool(USDC, SPCX_US, 2500)` is the pool Raydium's
// program keeps for the pair at fee 0.25%, and a loop over keys finds one pool for each. Each swap
// is `@sauce/router`'s at that venue: it returns 0 having moved nothing for a key with no pool, a
// spent budget, an empty or past-limit pool and one whose fee is not one rate, else offers the pool
// the amount it is given up to its own fee-adjusted limit, and returns what arrived in the Pot.
function main(
  budget: Uint256,
  priceLimit: Uint256,
  minTokens: Uint256,
  maxAvgPrice: Uint256,
  recipient: Address,
): { spent: Uint256; bought: Uint256; unspent: Uint256 } {
  require(pot.USDC >= budget, "budget is not in the Pot");
  const kept = pot.USDC - budget; // the Pot's other USDC, never spent

  // Each pool in turn, offered what is left of the budget.
  let bought = 0;
  for (const fee of [2500, 1800]) {
    bought += Raydium.CLMM.swap(
      Raydium.CLMM.pool(USDC, SPCX_US, fee),
      USDC,
      SPCX_US,
      pot.USDC - kept,
      priceLimit,
      0,
    );
  }
  bought += Byreal.CLMM.swap(
    Byreal.CLMM.pool(USDC, SPCX_US, 1000),
    USDC,
    SPCX_US,
    pot.USDC - kept,
    priceLimit,
    0,
  );
  bought += Pancakeswap.CLMM.swap(
    Pancakeswap.CLMM.pool(USDC, SPCX_US, 800),
    USDC,
    SPCX_US,
    pot.USDC - kept,
    priceLimit,
    0,
  );

  const unspent = pot.USDC - kept;
  const spent = budget - unspent;
  require(bought >= minTokens, "bought below minTokens");
  require(maxAvgPrice == 0 || spent * 10 ** 18 <= maxAvgPrice * bought, "average too high");
  // The SPCX_US bought and what is left of the budget both go to the recipient.
  if (bought > 0) SPCX_US.transfer(recipient, bought);
  if (unspent > 0) USDC.transfer(recipient, unspent);
  return { spent, bought, unspent };
}
```

## How it works

* Each `<Venue>.swap(pool, tokenIn, tokenOut, amountIn, priceLimit, minOut)` is `@sauce/router`'s swap at that venue. It returns 0 having moved nothing for a key with no pool, a spent budget, an empty or past-limit pool, and a pool whose fee is not one rate; otherwise it trades up to the pool's own limit, `priceLimit` moved by the pool's fee, and returns what arrived in the Pot.
* `kept` is the Pot's USDC beyond `budget`, so each pool is offered only `pot.USDC - kept`. The totals are checked once, after every pool: `bought >= minTokens`, and `spent * 10 ** 18 <= maxAvgPrice * bought` unless `maxAvgPrice` is 0.
* The pools fill in the program's order, not in order of price: each swap trades up to its pool's own limit before the next pool is offered what is left. `sauce pools` listed Raydium's 0.25% pool at 147.93 USDC a share with its fee, Byreal's at 147.79 and PancakeSwap's at 147.77. With `priceLimit` at 150, Raydium's pool, first in the program, took the whole 1,000 USDC for 6.75963 SPCX\_US, an average of 147.94, and the two cheaper pools were offered nothing (the command's local LiteSVM copy of Solana at slot 460565379). At 147.90 (`147900000000000000000`), below Raydium's price with its fee, Byreal's and PancakeSwap's pools filled 446.356017 USDC for 3.019138 SPCX\_US, an average of 147.84, and 553.643983 USDC came back unspent (the same local copy at slot 460565433). A `priceLimit` close to the prices `sauce pools` lists keeps every pool near them.
* The keys are written in, and the command reads each key's nine accounts from its pool when it builds the cook. On the slots simulated, the Raydium pool at fee 0.18% could not be read, and the command refused the cook before simulating (exit 2). The commands below pass that key's nine accounts as the System Program, the form a key with no pool takes, so its swap returns 0 and the program runs unchanged.
* Each swap walks only the tick arrays its slot gives, and past them a venue's program fails the whole cook, so each swap at a price stops a tick inside them. On Raydium CLMM, Byreal and PancakeSwap CLMM, that window ends where the furthest of those arrays begins; where a pool's limit takes it past there, the command reads that array's last initialized tick and the swap walks on to a tick inside it. It also holds a pool short of its limit, or skips it, where the cook's compute units would not cover its walk. With a budget of 100,000 USDC at slot 460512539, the PancakeSwap pool walked on (`deepTick` 50132 in `computeStops`). [Swaps at a price and compute units](https://docs.eco.com/sauce-sdk/cli/solana.md#swaps-at-a-price-and-compute-units) explains the stops.
* `sauce compile --json` measures the program at 5,329 bytes and the cook at 7,382 bytes against the 1,232-byte limit. `sauce run` stages the program in a code buffer and passes the cook's accounts through an address lookup table it creates.

## Run the example

**1. Read the pools**

```bash theme={null}
sauce pools --chain solana USDC SPCX_US
```

The listing gives each pool's key (`found by key: Raydium.CLMM.pool(USDC, SPCX_US, 2500)`), fee, liquidity and price paying USDC, with the fee and as a `priceLimit`, and lists the pools a distributed swap skips, with why. Here it listed pool `HnBT4gtWyTNKz1VuCrg5DfC57FZSnoSLGrQ2Vjpyfkts`, the one the program's key `1800` finds, among them: `its fee is more than one rate (a pool rate, decay, imbalance or dynamic fee)`.

**2. Compile**

```bash theme={null}
sauce compile solana-distributed-swap.sauce.ts --chain solana
```

**3. Simulate**

```bash theme={null}
sauce simulate solana-distributed-swap.sauce.ts --chain solana --owner <your-pubkey> \
  --assume-balance USDC:1000 \
  --args 1000000000 150000000000000000000 6666667 0 <recipient> \
  --account raydiumClmm1800Program=system --account raydiumClmm1800Account=system \
  --account raydiumClmm1800Config=system --account raydiumClmm1800VaultA=system \
  --account raydiumClmm1800VaultB=system --account raydiumClmm1800Extra=system \
  --account raydiumClmm1800Ticks0=system --account raydiumClmm1800Ticks1=system \
  --account raydiumClmm1800Ticks2=system
```

`--assume-balance USDC:1000` gives the Pot's USDC account the budget in a local LiteSVM copy of the cook's accounts, read from the cluster. Without the nine `--account` flags the command stopped with:

```text theme={null}
sauce simulate: the account raydiumClmm1800Program (slot:Raydium.CLMM(USDC,SPCX_US,1800):USDC.program): pool HnBT4gtWyTNKz1VuCrg5DfC57FZSnoSLGrQ2Vjpyfkts's slot paying USDC could not be read: it has 2 tick arrays near its price, and its swap takes 3
```

**4. Send**

```bash theme={null}
sauce run solana-distributed-swap.sauce.ts --chain solana --keypair <keypair> \
  --args 1000000000 150000000000000000000 6666667 149072241471951730972 <recipient> \
  --account raydiumClmm1800Program=system --account raydiumClmm1800Account=system \
  --account raydiumClmm1800Config=system --account raydiumClmm1800VaultA=system \
  --account raydiumClmm1800VaultB=system --account raydiumClmm1800Extra=system \
  --account raydiumClmm1800Ticks0=system --account raydiumClmm1800Ticks1=system \
  --account raydiumClmm1800Ticks2=system
```

`maxAvgPrice` here is halfway from the simulated average to `priceLimit`, rounded up; a simulation with it at slot 460261350 succeeded. The nine `--account` flags are needed only while the command cannot read the 0.18% pool's accounts: run the Simulate step without them first, and add them if it stops with the message above. `sauce run` creates the Pot and the missing token accounts the cook names (asking first, or with `--yes`), stages the program, creates the lookup table, sends the cook and closes the buffer. `sauce tx` with `--owner <your-pubkey>` and the Simulate step's `--args` and nine `--account` flags printed a setup step of 14 unsigned transactions for another signer.

> [!WARNING]
>
> `recipient` is a wallet's public key. A token account given there is taken as a wallet, and the cook pays an associated token account it holds, which no one can spend from.

## Simulation result

Simulated on Solana at slot 460261190 with the Simulate command above, in a local LiteSVM copy. Nothing was signed or sent; values change with the slot. Lines that name the owner are left out.

```text theme={null}
...
main returns { spent: Uint256, bought: Uint256, unspent: Uint256 }: { spent: 1000000000, bought: 6750167, unspent: 0 }
Simulated in a local copy (LiteSVM) of the 36 accounts the cook names, read from solana at slot 460261190, with the released engine and Kitchen: the cook does not fit one transaction, and no cluster can stage a program without sending. Nothing was signed or sent:
  Kitchen create_pot: Pot BjFSkVpfmbX19SuADFxTsfzWdTErjdELMyd4Nd4iXo9T is not created on solana yet, so the copy creates it first
  the 5489-byte program staged into a code buffer in 9 transactions, as `sauce run` stages it
...
Token balance changes:
  6Cz6ZMnQAE4D9UFUyxpsdWYHtkJ5h315eBAFvq8gRSGs (potUSDC, the Pot's USDC account), owned by BjFSkVpfmbX19SuADFxTsfzWdTErjdELMyd4Nd4iXo9T (the Pot): -1000 USDC
  2iM4Qur7sY2cPWRvfDjQbw2u7WZuHVgWicGGaMoYjJn8 (raydiumClmm2500VaultA, pool FjuBy7jjf9DXj9d3R7cHpvcnoFW2iQxf7F7P3vqx4Jza's vaultA, paying USDC), owned by FjuBy7jjf9DXj9d3R7cHpvcnoFW2iQxf7F7P3vqx4Jza (raydiumClmm2500Account, pool FjuBy7jjf9DXj9d3R7cHpvcnoFW2iQxf7F7P3vqx4Jza's pool, paying USDC): -6.750167 SPCX_US
  FqRgdh1hE1YggkMRDvyf3HRpceUqoBdBTwmRHieiz2Kt (raydiumClmm2500VaultB, pool FjuBy7jjf9DXj9d3R7cHpvcnoFW2iQxf7F7P3vqx4Jza's vaultB, paying USDC), owned by FjuBy7jjf9DXj9d3R7cHpvcnoFW2iQxf7F7P3vqx4Jza (raydiumClmm2500Account, pool FjuBy7jjf9DXj9d3R7cHpvcnoFW2iQxf7F7P3vqx4Jza's pool, paying USDC): +1000 USDC
  48eo3Cjr4VJwA9LyT8izfjLy1TaPcHTcKN2nSjQFBjxh (recipientSPCX_US, main's argument recipient's SPCX_US account), owned by H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS: +6.750167 SPCX_US
...
```

Raydium's 0.25% pool took the whole budget: 1,000 USDC for 6.750167 SPCX\_US, an average of 148.14 USDC per share (`spent × 10^18 ÷ bought` is 148144482943903461943), inside the 150 limit. Byreal's and PancakeSwap's pools show no balance change: the budget was spent before their turn.

With `minTokens` at 6.8 SPCX\_US (`6800000`) and `--expect-revert "bought below minTokens"`:

```text theme={null}
As expected, the cook reverts: the program threw "bought below minTokens"
The cook reverts: the program threw "bought below minTokens"
Simulated in a local copy (LiteSVM) of the 36 accounts the cook names, read from solana at slot 460261312, with the released engine and Kitchen: the cook does not fit one transaction, and no cluster can stage a program without sending. Nothing was signed or sent:
...
```

## Next steps

* [Distributed swap](https://docs.eco.com/quickstarts/sauce/distributed-swap.md): the same limit order over Uniswap pools on Base
* [Solana swap](https://docs.eco.com/quickstarts/sauce/solana-swap.md): one swap at one Solana pool
* [`sauce pools`](https://docs.eco.com/sauce-sdk/cli/pools), [`sauce simulate`](https://docs.eco.com/sauce-sdk/cli/simulate), [`sauce run`](https://docs.eco.com/sauce-sdk/cli/run)


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