> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rhinestone.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Swap sessions

> Scope a session to a single swap: two tokens, a fixed recipient, a spend cap, and the venues it may route through.

A swap scope narrows a session to one swap: it may spend one token, receive one token, deliver to one recipient, and route only through the venues you name. Every other operation reverts on-chain. This is what makes an automated conversion session — a backend that turns deposits into a stablecoin, say — grantable without handing the session key authority over the account.

<Note>Requires `@rhinestone/sdk` 2.6.0 or later (1.14.0 on the v1 line).</Note>

## Declaring a swap scope

```ts {10-13} theme={null}
import { parseUnits } from 'viem'
import { plasma } from 'viem/chains'

const session = await rhinestone.createSession({
  chain: plasma,
  owners: {
    type: 'ecdsa',
    accounts: [sessionOwnerAccount],
  },
  swap: {
    sell: { token: usdt0Address, maxTotal: parseUnits('1000', 6) },
    buy: { token: usdcAddress },
    to: accountAddress,
  },
})
```

| Field           | Meaning                                                                                                                                                                                                                                                |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sell.token`    | The only token the session may spend.                                                                                                                                                                                                                  |
| `sell.maxTotal` | Cumulative cap on total sell-token spend across the session's life. Enforced both as a spending limit on the approve and as an accumulating bound on each swap's own amount, so a pre-existing allowance cannot be used to exceed it. Omit for no cap. |
| `buy.token`     | The only token the session may receive.                                                                                                                                                                                                                |
| `to`            | Swap output recipient, pinned on-chain. Normally the account itself.                                                                                                                                                                                   |
| `via`           | Venues the session may route through. Defaults to `[rhinestoneSwap()]`.                                                                                                                                                                                |

A swap scope is single-chain: it applies on the session's `chain`.

## The implied restriction

Declaring `swap` implies [`restrictToActions`](./restrict-a-session). The session's only operations are to approve the sell token to a listed venue's spender, and to call that venue's swap entrypoint with the sell token, buy token, and recipient pinned. Two consequences:

* The ERC-1271 [signing](./session-signing) surface defaults to `{ mode: 'disabled' }`, so a swap-scoped key cannot sign an off-chain approval. Pass `signing` explicitly to opt back in.
* A swap scope cannot be combined with `crossChainPermits` or `claimPolicies`. Those guardrails live inside the fallback the restriction drops, so the SDK rejects the combination rather than silently dropping them.

## Naming venues

Venues are named, not addressed — router addresses, selectors, and calldata offsets stay inside the SDK, so you never encode a swap yourself and the SDK can follow a router deployment without a breaking change. The builders are published exports:

```ts theme={null}
import { fynd, rhinestoneSwap, zeroEx } from '@rhinestone/sdk/smart-sessions'
```

| Builder               | Venue                                                                                                                                                      |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rhinestoneSwap()`    | The Rhinestone Swapper. The default, and the route the orchestrator emits for same-chain smart-account swaps. Aggregator-agnostic: any quoter may fill it. |
| `fynd()`              | Rhinestone's Tycho router.                                                                                                                                 |
| `zeroEx({ settler })` | 0x, through its AllowanceHolder flow. Takes either a pinned `settler` or `anySettler: true` with a `maxSpend`.                                             |

Each builder also accepts an optional `maxSpend` — a venue-level cumulative cap that overrides `sell.maxTotal` for that venue.

Naming an aggregator authorizes **every call shape its swaps can arrive in**: both a direct router call by the account and the Swapper-wrapped one. That cannot be narrowed, because which shape the orchestrator emits depends on the swap's direction and the winning quoter — both decided after the session is signed. Authorizing a single shape is how a session ends up rejecting the swap it was created for.

Venue availability is per chain, and the SDK rejects a venue that isn't available on the session's chain.

## Pinning 0x's Settler

0x redeploys its Settler every few weeks and moves the registry token, so the SDK bundles no default address. Resolve it and pin the result:

```ts {6,18} theme={null}
import { createPublicClient, http } from 'viem'
import { plasma } from 'viem/chains'
import { resolveZeroExSettler, zeroEx } from '@rhinestone/sdk/smart-sessions'

const client = createPublicClient({ chain: plasma, transport: http() })
const settler = await resolveZeroExSettler(client)

const session = await rhinestone.createSession({
  chain: plasma,
  owners: {
    type: 'ecdsa',
    accounts: [sessionOwnerAccount],
  },
  swap: {
    sell: { token: usdt0Address, maxTotal: parseUnits('1000', 6) },
    buy: { token: usdcAddress },
    to: accountAddress,
    via: [zeroEx({ settler })],
  },
})
```

Resolve once, at session-enable time — do not re-resolve per swap. A session that looked the address up at use time would follow 0x's registry wherever it points, so a compromise of 0x's upgrade key could redirect every live session's approved funds. Pinned, the worst case of a rotation is that the session stops working until it is re-enabled.

<Warning>`zeroEx({ anySettler: true, maxSpend })` trades the pin for longevity, and it is an explicit security downgrade. With the target unpinned, a compromised session key can redirect the AllowanceHolder pull to its own contract, and `maxSpend` becomes the only bound on what it takes — which is why the SDK makes it mandatory in that form.</Warning>

## What the scope binds

| Bound                                                                                | Not bound                                                                                                                                                                                                                                                          |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| The operations the key can run: the approve, plus the named venues' swap entrypoints | **Swap quality.** The minimum output is not pinned, so a scope with no venue named can spend up to the cap and receive little in return. Naming a venue largely closes this; the Rhinestone Swapper additionally enforces its own minimum output to the recipient. |
| The sell token and the buy token                                                     |                                                                                                                                                                                                                                                                    |
| The recipient — a swap delivering elsewhere is rejected                              |                                                                                                                                                                                                                                                                    |
| Cumulative sell-side spend, when `maxTotal` or `maxSpend` is set                     |                                                                                                                                                                                                                                                                    |

## Using the session

A same-chain swap intent, signed by the session key:

```ts {5-9} theme={null}
const prepared = await rhinestoneAccount.prepareTransaction({
  chain: plasma,
  tokenRequests: [{ address: usdcAddress, amount: parseUnits('10', 6) }],
  sourceAssets: [usdt0Address],
  sponsored: true,
  signers: {
    type: 'session',
    session,
  },
})
const signed = await rhinestoneAccount.signTransaction(prepared)
const result = await rhinestoneAccount.submitTransaction(signed)
```

<Warning>Sponsor the intent. An unsponsored intent adds the orchestrator's fee and gas-refund operations, which are authorized by the wildcard intent-execution fallback that a swap scope drops. Without `sponsored: true` the intent is rejected on-chain, and the error surfaces as a signature failure rather than an authorization one.</Warning>

<Note>You don't restate the venue at transaction time. When the session names specific aggregators, the SDK derives the matching `quoters` pin from `swap.via` and sends it with the quote request; across a per-chain session set the pin is the intersection of every session's venues. `rhinestoneSwap()` pins nothing, since any quoter may legitimately fill it.</Note>

Venue pinning constrains the route rather than selecting it. If no permitted venue can serve the swap, the request fails instead of falling back to a venue the session does not allow — an availability consideration, not a security one.

## Pinning venues without a session

`quoters` is also available on any transaction, as an include or exclude filter over the orchestrator's swap venues:

```ts {4} theme={null}
const prepared = await rhinestoneAccount.prepareTransaction({
  chain: plasma,
  tokenRequests: [{ address: usdcAddress, amount: parseUnits('10', 6) }],
  quoters: { include: ['0x'] },
})
```

An explicit filter can only narrow a session-derived pin, never widen it. And an empty filter is not the same as omitting the field: `{ include: [] }` means no venue may serve the swap, so the request fails at quote time rather than falling back to an unconstrained route.
