> For the complete documentation index, see [llms.txt](https://docs.osero.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.osero.com/osero-sdk/core-concepts/actions.md).

# Actions

## Actions

Actions are the core functions of `@osero/client/actions`. Each action takes an `OseroClient` and a `Request` object, determines the correct routing for the target chain, and returns a `ResultAsync<ExecutionPlan, ActionError>` that you pipe into `sendWith` to execute.

Every action has a matching **preview helper** that returns the expected output amount as a `bigint` without building or sending any transactions.

The hosted Osero API exposes a sibling method — `getSwapQuote` — that returns the same execution-plan shape for **cross-chain** USDC ↔ sUSDS routes. It is covered in Hosted swap quotes below.

***

### Request shape

All actions share the same request type:

```typescript
type Request = {
  chainId: number;          // One of the supported chain IDs
  amount: bigint;           // Input amount in the input token's native decimals
  sender: `0x${string}`;    // The wallet that pays the input token (see note below)
  receiver?: `0x${string}`; // Recipient of the output token. Defaults to sender
  slippageBps?: number;     // Slippage tolerance in basis points. Defaults to client config (5 bps)
  referralCode?: bigint;    // See "Referral code" below
};
```

#### `sender` on Ethereum Mainnet

On L2s, `sender` is simply the account paying the input token. On Ethereum Mainnet, two-phase actions (`mintSUsds`, `redeemSUsds`) route through an intermediate USDS balance, and `sender` is also the wallet that temporarily holds that USDS between phases — **even if `receiver` is a different address**. Any USDS dust left over after the second phase stays in `sender`'s USDS balance.

#### Referral code

`referralCode` is forwarded to the underlying PSM/vault event for off-chain attribution. How it flows depends on the action and chain:

| Action        | L2 (PSM3)                        | Ethereum Mainnet                                         |
| ------------- | -------------------------------- | -------------------------------------------------------- |
| `mintUsds`    | Emitted on the PSM3 `Swap` event | Ignored (Sky Lite PSM has no referral parameter)         |
| `mintSUsds`   | Emitted on the PSM3 `Swap` event | Forwarded to the sUSDS vault `deposit` referral overload |
| `redeemUsds`  | Emitted on the PSM3 `Swap` event | Ignored                                                  |
| `redeemSUsds` | Emitted on the PSM3 `Swap` event | Ignored                                                  |

Defaults to `0n` if omitted.

***

### mintUsds

**Direction:** USDC → USDS

Converts USDC to USDS via the Sky PSM. On Ethereum Mainnet this routes through Sky's `UsdsPsmWrapper`; on L2s it routes through the Spark PSM3.

```typescript
import { mintUsds } from '@osero/client/actions';
import { parseUnits } from 'viem';

const result = await mintUsds(client, {
  chainId: 8453,           // Base
  amount: parseUnits('500', 6), // 500 USDC
  sender: '0xYourAddress',
}).andThen(sendWith(wallet));
```

| Chain                             | Transaction shape                      |
| --------------------------------- | -------------------------------------- |
| Ethereum Mainnet                  | `approve` + `sellGem` (UsdsPsmWrapper) |
| L2 (Base, Arbitrum, OP, Unichain) | `approve` + PSM3 `swap`                |

***

### mintSUsds

**Direction:** USDC → sUSDS

Converts USDC to sUSDS. On L2s, a single PSM3 swap delivers sUSDS directly. On Ethereum Mainnet, this is a two-phase `MultiStepExecution`: USDC → USDS via the `UsdsPsmWrapper`, then USDS → sUSDS via the ERC-4626 sUSDS vault.

```typescript
import { mintSUsds } from '@osero/client/actions';
import { parseUnits } from 'viem';

const result = await mintSUsds(client, {
  chainId: 8453,
  amount: parseUnits('100', 6),
  sender: '0xYourAddress',
}).andThen(sendWith(wallet));
```

| Chain                             | Transaction shape                      |
| --------------------------------- | -------------------------------------- |
| Ethereum Mainnet                  | Four-tx two-phase `MultiStepExecution` |
| L2 (Base, Arbitrum, OP, Unichain) | `approve` + PSM3 `swap` (two txs)      |

***

### redeemUsds

**Direction:** USDS → USDC

Redeems USDS for USDC via the PSM. The reverse of `mintUsds`.

```typescript
import { redeemUsds } from '@osero/client/actions';
import { parseUnits } from 'viem';

const result = await redeemUsds(client, {
  chainId: 42161,          // Arbitrum One
  amount: parseUnits('500', 18), // USDS has 18 decimals
  sender: '0xYourAddress',
}).andThen(sendWith(wallet));
```

| Chain            | Transaction shape                     |
| ---------------- | ------------------------------------- |
| Ethereum Mainnet | `approve` + `buyGem` (UsdsPsmWrapper) |
| L2               | `approve` + PSM3 `swap`               |

***

### redeemSUsds

**Direction:** sUSDS → USDC

Redeems sUSDS for USDC. The reverse of `mintSUsds`. On Ethereum Mainnet this is a three-transaction two-phase flow (sUSDS → USDS → USDC); on L2s it is a two-transaction flow.

```typescript
import { redeemSUsds } from '@osero/client/actions';

const result = await redeemSUsds(client, {
  chainId: 8453,
  amount: susdsBalance, // raw bigint from getSUsdsBalance
  sender: '0xYourAddress',
}).andThen(sendWith(wallet));
```

| Chain            | Transaction shape                       |
| ---------------- | --------------------------------------- |
| Ethereum Mainnet | Three-tx two-phase `MultiStepExecution` |
| L2               | `approve` + PSM3 `swap`                 |

***

### Preview helpers

Each action has a matching preview helper that returns the expected output amount as a raw `bigint` wrapped in a `ResultAsync`. Preview helpers do not require a `sender` address and do not build or broadcast any transactions.

| Preview helper       | Quotes       | Input token decimals |
| -------------------- | ------------ | -------------------- |
| `previewMintUsds`    | USDC → USDS  | 6                    |
| `previewMintSUsds`   | USDC → sUSDS | 6                    |
| `previewRedeemUsds`  | USDS → USDC  | 18                   |
| `previewRedeemSUsds` | sUSDS → USDC | 18                   |

```typescript
import { previewMintSUsds } from '@osero/client/actions';
import { parseUnits } from 'viem';

const quote = await previewMintSUsds(client, {
  chainId: 8453,
  amount: parseUnits('100', 6), // 100 USDC
});

if (quote.isOk()) {
  // Display expected output to user before asking them to sign
  console.log('Expected sUSDS:', quote.value);
}
```

#### Common pattern: quote then execute

A common pattern in production integrations is to preview first, display the expected output to the user, and then execute the action only after the user confirms:

```typescript
import { previewMintSUsds, mintSUsds } from '@osero/client/actions';
import { parseUnits, formatUnits } from 'viem';

const amount = parseUnits('100', 6);

// 1. Get a quote
const quote = await previewMintSUsds(client, { chainId: 8453, amount });
if (quote.isErr()) return; // handle error

// 2. Show the user (format from 18-decimal sUSDS)
console.log(`You will receive approximately ${formatUnits(quote.value, 18)} sUSDS`);

// 3. On user confirmation, execute
const result = await mintSUsds(client, {
  chainId: 8453,
  amount,
  sender: wallet.account.address,
}).andThen(sendWith(wallet));
```

***

### Directing output to a different address

All actions accept an optional `receiver` field. When set, the output token is delivered to `receiver` rather than `sender`:

```typescript
const result = await mintSUsds(client, {
  chainId: 8453,
  amount: parseUnits('100', 6),
  sender: userWallet.address,
  receiver: custodyAddress,  // sUSDS lands here
}).andThen(sendWith(wallet));
```

***

### Hosted swap quotes

The hosted Osero API builds priced quotes between a counter asset (USDC on a supported chain) and the sUSDS vault on Ethereum Mainnet — including **cross-chain** routes that the local actions cannot build. The method is `getSwapQuote`, and the response carries an `executionPlan` you broadcast with the same `sendWith` adapter.

```typescript
import { OseroApiClient } from '@osero/client/api';
import { sendWith } from '@osero/client/viem';
import { parseUnits } from 'viem';

const api = OseroApiClient.create({ apiKey: process.env.OSERO_API_KEY! });

const result = await api
  .getSwapQuote({
    fromAddress: wallet.account.address,
    fromAssetId: 'base:usdc',
    toAssetId: 'ethereum:susds',
    amount: parseUnits('100', 6), // 100 USDC
    slippage: '0.5',              // percent
  })
  .map((quote) => quote.executionPlan)
  .andThen(sendWith(wallet));
```

#### Request shape

`getSwapQuote` takes one object. The fields differ slightly from the local actions:

| Field          | Type               | Required | Notes                                                              |
| -------------- | ------------------ | -------- | ------------------------------------------------------------------ |
| `fromAddress`  | `0x${string}`      | yes      | Wallet paying the input token                                      |
| `fromAssetId`  | asset ID           | yes      | e.g. `base:usdc`, `ethereum:susds`                                 |
| `toAssetId`    | asset ID           | yes      | e.g. `ethereum:susds`, `base:usdc`                                 |
| `amount`       | `bigint \| string` | yes      | Raw input units. String must be a positive base-10 integer         |
| `slippage`     | `string`           | no       | Percent, up to 2 decimals, max `'5'`. Different from `slippageBps` |
| `referralCode` | `number`           | no       | Integer `3000`–`3999`. Different from local actions (`bigint`)     |

Every quote pairs a counter asset with `ethereum:susds` — in either direction. Counter-asset-to-counter-asset routes are not supported.

Validation runs locally before any network call. Invalid input resolves to a `ValidationError` and never reaches the API.

#### Listing supported assets

`getSupportedAssets()` returns the asset IDs the API quotes against — useful at runtime rather than hard-coding asset metadata. See Configuration for the full response shape.

#### Reading the response

A successful `getSwapQuote` resolves to an `OseroApiSwapQuote`:

| Field           | What it holds                                                                       |
| --------------- | ----------------------------------------------------------------------------------- |
| `pair`          | The resolved `direction` (`to-susds` / `from-susds`) and the `from` / `to` assets   |
| `quote`         | Pricing — `amountIn`, `amountOut`, `slippage`, `gas`, `priceImpactBps`, `createdAt` |
| `approval`      | The ERC-20 approval — `token`, `spender`, `amount`, and the approval `transaction`  |
| `execution`     | The main swap — `kind`, `sourceChainId`, `destinationChainId`, `transaction`        |
| `bridge`        | Whether a cross-chain bridge is involved — see Cross-chain quotes below             |
| `executionPlan` | An `Erc20ApprovalRequired` plan                                                     |

`quote.amountOut` may be `null` when the API cannot price the output — `quote.previewUnavailable` is `true` in that case and the transactions are still signable.

#### Executing the plan

`quote.executionPlan` is the same model the local actions return — one ERC-20 approval gating one execution transaction. Every adapter broadcasts it unchanged:

```typescript
const sent = await sendWith(wallet, quote.value.executionPlan);
if (sent.isErr()) throw sent.error;
console.log('Source-chain tx:', sent.value.txHash);
```

The execution operation is `MINT_SUSDS` for `to-susds` quotes and `REDEEM_SUSDS_FOR_USDC` for `from-susds` quotes. Both transactions run on the quote's `sourceChainId`.

***

### Cross-chain quotes and bridge status

sUSDS lives only on Ethereum Mainnet. When the counter asset is on an L2, the route **bridges** between that L2 and Ethereum — and the bridge leg settles **after** the source-chain transactions confirm.

Two fields on the quote tell you which kind of route you got:

```typescript
quote.value.execution.kind;  // 'same-chain' | 'cross-chain'
quote.value.bridge.required; // false | true
```

For a cross-chain quote, `quote.bridge` carries the bridge protocol (`stargate`, `ccip`, `layerzero`, or `relay`) and a `statusRequest` you keep around to poll bridge status. For a same-chain quote, `bridge.required` is `false` and the other bridge fields are `null`.

#### `sendWith` confirms the source-chain leg only

For a cross-chain quote, `result.value.txHash` is the **source-chain** transaction hash. The destination asset is not in the wallet yet when `sendWith` resolves — the bridge still has to deliver it.

#### Polling the bridge

`getSwapStatusForQuote` reads the bridge coordinates straight off the quote:

```typescript
const status = await api.getSwapStatusForQuote(quote.value, sent.value.txHash);

if (status.isOk()) {
  const { state, destinationTxHash, error } = status.value.bridge;
  // state is one of 'pending' | 'completed' | 'failed' | 'unknown'
}
```

`getSwapStatus` is the lower-level form — it takes `txHash`, `sourceChainId`, and `bridgeProtocol` directly, and is what to use if you persist only the tx hash.

Calling `getSwapStatusForQuote` with a **same-chain** quote resolves to a `ValidationError` — guard with `bridge.required` if a quote could be either. Bridges settle asynchronously, so poll until `state` is terminal:

```typescript
async function waitForBridge(api, quote, txHash) {
  while (true) {
    const status = await api.getSwapStatusForQuote(quote, txHash);
    if (status.isErr()) return status;

    const { state } = status.value.bridge;
    if (state === 'completed' || state === 'failed') return status;

    await new Promise((resolve) => setTimeout(resolve, 15_000));
  }
}
```

For production use, add a per-attempt timeout and a cap on attempts. Every API method also accepts an `AbortSignal` — see Configuration.

#### End-to-end: quote, execute, track

```typescript
import { OseroApiClient } from '@osero/client/api';
import { sendWith } from '@osero/client/viem';
import { parseUnits } from 'viem';

const api = OseroApiClient.create({ apiKey: process.env.OSERO_API_KEY! });

// 1. Quote: Base USDC → Ethereum sUSDS (cross-chain)
const quote = await api.getSwapQuote({
  fromAddress: wallet.account.address,
  fromAssetId: 'base:usdc',
  toAssetId: 'ethereum:susds',
  amount: parseUnits('100', 6),
  slippage: '0.5',
});
if (quote.isErr()) throw quote.error;

// 2. Broadcast the source-chain transactions
const sent = await sendWith(wallet, quote.value.executionPlan);
if (sent.isErr()) throw sent.error;

// 3. Track the bridge to the destination chain
if (quote.value.bridge.required) {
  const status = await api.getSwapStatusForQuote(quote.value, sent.value.txHash);
  if (status.isOk()) {
    console.log('Bridge state:', status.value.bridge.state);
  }
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.osero.com/osero-sdk/core-concepts/actions.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
