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

# Bridging (CCTP)

> Bridge USDC between Base and Solana with Circle CCTP — the user signs once on the source chain, Cesto attests and mints on the destination.

The SDK bridges **native USDC** between Base and Solana using
[Circle CCTP V2](https://developers.circle.com/cctp). Burn-and-mint means no wrapped token,
no pool, no slippage: USDC is burned on the source chain and freshly minted on the
destination.

Your integration is five calls; the user's part is **one signature on the source chain**.
Everything after that — attestation, the destination mint, destination gas — is Cesto's
background job.

```
1. quote       your server ─▶ Cesto     fees, ETA, net-out (nothing is created)
2. initiate    your server ─▶ Cesto     → { transferId, unsigned burn tx }
3. sign        user's wallet            signs + lands the burn on the SOURCE chain
4. submitBurn  your server ─▶ Cesto     Cesto verifies the burn against Circle
5. poll        your server ─▶ Cesto     attestation → relayer mint → COMPLETED
```

<Warning>
  Bridging needs a **write-scoped** API key. It delivers USDC only — investing is a separate
  step and still needs a Solana wallet (see [Bridge, then invest](#bridge-then-invest) and
  [Integration Models](/sdk/integration-models)). Bridging runs on **mainnet only** — every
  example on this page moves **real USDC**; test with small amounts first.
</Warning>

## Modes

|         | `standard` (default)                     | `fast`                    |
| ------- | ---------------------------------------- | ------------------------- |
| Fee     | **0 — lossless**                         | \~1 bps, deducted at mint |
| Latency | \~20s from Solana · \~15–20 min from EVM | \~8–20 seconds            |

With `fast`, invest what **arrived** (`netOut`), not what you burned.

## Networks

Bridging is currently **mainnet-only**, between two chain keys:

| Chain key | Chain          | Attestation            |
| --------- | -------------- | ---------------------- |
| `base`    | Base mainnet   | Circle production Iris |
| `solana`  | Solana mainnet | Circle production Iris |

Additional chains will be added as they launch — `quote` / `initiate` reject
unsupported chain keys with a `400`.

## Quickstart (EVM → Solana)

All amounts are **USDC base units (6 decimals) as strings** — `"1000000"` is 1 USDC.

```ts theme={null}
import { Cesto } from '@cesto/sdk';

const cesto = new Cesto({ apiKey: process.env.CESTO_API_KEY }); // write-scoped key

// 1. Quote — fees, ETA, and whether the recipient's USDC ATA exists
const quote = await cesto.bridge.quote({
  sourceChain: 'base',
  destChain: 'solana',
  amount: '10000000', // 10 USDC
  recipient: userSolanaWallet,
  mode: 'fast',
});

// 2. Initiate — creates the transfer, returns the unsigned burn
const { transferId, burn } = await cesto.bridge.initiate({
  sourceChain: 'base',
  destChain: 'solana',
  amount: '10000000',
  recipient: userSolanaWallet,
  mode: 'fast',
  sourceAddress: userEvmAddress, // signs the burn
});

// 3. The user signs on the source chain (viem example)
if (burn && burn.kind === 'evm') {
  if (burn.approvalTx) {
    const hash = await walletClient.sendTransaction(burn.approvalTx);
    await publicClient.waitForTransactionReceipt({ hash });
    // Wait until the allowance is readable before burning — see the note below.
  }
  const burnTxHash = await walletClient.sendTransaction(burn.burnTx);

  // 4. Report the landed burn — Cesto verifies it against Circle
  await cesto.bridge.submitBurn(transferId, { burnTxHash });

  // 5. Wait for the destination mint
  const done = await cesto.bridge.waitForTransfer(transferId);
  done.netOut;      // USDC that arrived
  done.mintTxHash;  // destination mint transaction
}
```

<Note>
  **Confirm the approval before burning.** Wait for the `approvalTx` receipt *and* for the
  allowance to be readable on-chain before sending `burnTx` — load-balanced RPCs lag a few
  seconds, and an early burn reverts with `ERC20: transfer amount exceeds allowance`.
</Note>

### Solana-source burns

How a Solana-side burn gets signed depends on whose wallet is burning:

<Tabs>
  <Tab title="Managed wallet">
    When `sourceAddress` is a Cesto-managed Solana wallet (provisioned via `users.create`
    under your API key), Cesto **signs and lands the burn server-side** — the sponsor pays
    the transaction fee and the CCTP message-account rent, so managed wallets need **no
    SOL**. `initiate` returns `{ status: 'BURN_SUBMITTED', burnTxHash, burn: null }`
    instead of an unsigned burn, so skip `submitBurn` (re-submitting the same hash is
    safe/idempotent) and go straight to `waitForTransfer`:

    ```ts theme={null}
    const res = await cesto.bridge.initiate({
      sourceChain: 'solana',
      destChain: 'base',
      amount: '10000000', // 10 USDC
      recipient: userEvmAddress,
      sourceAddress: user.solanaAddress, // managed wallet → Cesto signs
    });

    // res.status === 'BURN_SUBMITTED', res.burn === null, res.burnTxHash is landed
    const done = await cesto.bridge.waitForTransfer(res.transferId);
    ```

    See [Managed wallets — Withdrawing](/sdk/managed-wallets#withdrawing) for the full
    withdrawal flow.
  </Tab>

  <Tab title="BYOW">
    A self-custody or partner-controlled Solana wallet signs the burn itself. `initiate`
    returns `burn.kind === 'solana'`: a base64 transaction plus
    **`additionalSignerSecrets`** — ephemeral, single-use keypairs (CCTP's message account)
    that must co-sign alongside the user wallet. They hold no funds:

    ```ts theme={null}
    import { Keypair, Transaction } from '@solana/web3.js';
    import bs58 from 'bs58';

    const tx = Transaction.from(Buffer.from(burn.transaction, 'base64'));
    const extra = burn.additionalSignerSecrets.map((s) => Keypair.fromSecretKey(bs58.decode(s)));
    tx.sign(userKeypair, ...extra); // userKeypair loaded server-side from process.env
    const burnTxHash = await connection.sendRawTransaction(tx.serialize());
    await connection.confirmTransaction(burnTxHash, 'confirmed');
    await cesto.bridge.submitBurn(transferId, { burnTxHash });
    ```

    <Warning>
      **The signing wallet pays the burn's costs.** Unlike managed burns, BYOW Solana burns
      are **not sponsor-paid**: the signing wallet is the fee payer and the CCTP
      message-account rent payer, so it must hold **\~0.005 SOL** in addition to the USDC
      being burned. Without it the burn fails to land.
    </Warning>

    <Note>
      Treat `additionalSignerSecrets` like any key material: use them **server-side**,
      immediately, for this burn only — never log, persist, or ship them to a browser.
    </Note>
  </Tab>
</Tabs>

## The recipient address

`recipient` is always a **wallet** — hex account on EVM, owner wallet on Solana. CCTP mints
into a token account, so for Solana destinations Cesto derives the USDC ATA itself and, if
it's the user's first transfer, **creates it at mint time** (relayer pays the rent — no SOL
needed). `quote`'s `ataExists` tells you which case you're in.

## Bridge, then invest

Once a transfer completes, open the position **with the arrived amount** (`netOut`, not the
burn amount — `fast` mode deducts its fee at mint):

<Tabs>
  <Tab title="Managed">
    ```ts theme={null}
    const done = await cesto.bridge.waitForTransfer(transferId);

    const result = await cesto.open.startAndWait({
      user: user.solanaAddress, // the managed wallet that received the funds
      product: 'stable-genius',
      amount: BigInt(done.netOut!),
    });
    ```

    No signature needed — Cesto signs for the managed wallet. See
    [Managed Wallets](/sdk/managed-wallets).
  </Tab>

  <Tab title="BYOW">
    ```ts theme={null}
    const done = await cesto.bridge.waitForTransfer(transferId);

    const result = await cesto.open.execute({
      wallet: userSolanaWallet,
      product: 'stable-genius',
      amount: BigInt(done.netOut!), // not the burn amount — fast mode deducts its fee
      signTransactions: signWithKeypair(keypair), // keypair loaded server-side from process.env
    });
    ```

    See [Open a Position](/sdk/open-position) for the full client-signed flow.
  </Tab>
</Tabs>

Withdrawing is the mirror image: [close the position](/sdk/close-position), then bridge
`solana → base` back to the user's EVM wallet (see
[Solana-source burns](#solana-source-burns) for who signs that burn).

## Method reference

### `bridge.quote(params)` → `BridgeQuote`

<ParamField path="sourceChain" type="BridgeChain" required>
  `base` or `solana` (mainnet).
</ParamField>

<ParamField path="destChain" type="BridgeChain" required>
  Chain the USDC arrives on.
</ParamField>

<ParamField path="amount" type="string" required>
  USDC base units (6 decimals) as a decimal string.
</ParamField>

<ParamField path="recipient" type="string">
  Destination wallet — enables `ataExists` for Solana destinations.
</ParamField>

<ParamField path="mode" type="'standard' | 'fast'" default="'standard'">
  Speed / fee trade-off.
</ParamField>

Returns `{ mode, amount, maxFee, estimatedFee, netOut, etaSeconds, minFinalityThreshold, ataExists? }`.
Creates nothing.

### `bridge.initiate(params)` → `{ transferId, status, burn, burnTxHash? }`

All `quote` fields (recipient is **required** here), plus:

<ParamField path="sourceAddress" type="string" required>
  Source-chain wallet that will sign the burn — or a Cesto-managed Solana wallet, in which
  case Cesto signs for it.
</ParamField>

<ParamField path="externalUserId" type="string">
  Your user reference, stored on the transfer for reconciliation.
</ParamField>

Two response shapes, distinguished by `status`:

* **`status: 'BURN_TX_BUILT'`** (default) — `burn` is
  `{ kind: 'evm', chainId, approvalTx?, burnTx }` or
  `{ kind: 'solana', transaction, additionalSignerSecrets }`. `approvalTx` is present only
  when the current USDC allowance is insufficient — confirm it before `burnTx`.
* **`status: 'BURN_SUBMITTED'`** (managed Solana source wallet) — Cesto signed and landed
  the burn itself, sponsor paying gas: `burn` is `null` and `burnTxHash` holds the landed
  burn signature. Skip `submitBurn` and poll `waitForTransfer`.

### `bridge.submitBurn(transferId, { burnTxHash })`

Reports the landed burn. Cesto verifies it against Circle's attestation (amount, chains,
recipient) and drives the rest in the background.

<Note>
  Circle can take a few seconds to index a fresh burn — short lags are absorbed internally;
  on a `404`, retry after a few seconds. Re-submitting the same hash is safe (it echoes the
  status), and `submitBurn` is never auto-retried — poll `getTransfer` instead of re-sending
  after a client-side timeout.
</Note>

### `bridge.getTransfer(transferId)` / `bridge.waitForTransfer(transferId, options?)`

Current status, tx hashes, and — once attested — `feeExecuted` / `netOut` decoded from the
CCTP message. `waitForTransfer` resolves on `COMPLETED` and rejects with
`Cesto.BridgeTransferFailedError` on `FAILED`.

<ParamField path="pollIntervalMs" type="number" default="5000">
  Delay between status polls.
</ParamField>

<ParamField path="timeoutMs" type="number" default="1800000">
  Max total wait (30 min — beyond standard-mode worst case). A timeout doesn't stop the
  transfer; keep polling `getTransfer`.
</ParamField>

## Transfer lifecycle

```
CREATED → BURN_TX_BUILT → BURN_SUBMITTED → ATTESTING → ATTESTED → MINTING → MINTED → COMPLETED
                                                                                     ↘ FAILED
```

Most of these pass in seconds on `fast`. The two you'll surface to users: **ATTESTING**
("waiting for Circle", up to \~20 min on standard) and **COMPLETED** (funds arrived —
`netOut`, `feeExecuted`, `mintTxHash` are final). **FAILED** is terminal with the reason in
`error` — a burned amount is never lost (attestations don't expire); contact Cesto support
with the `transferId`.

## Common errors

| Situation                              | What you get                                              |
| -------------------------------------- | --------------------------------------------------------- |
| Read-only key                          | `403 PermissionDeniedError`                               |
| Burn not indexed by Circle yet         | `404` — retry `submitBurn` shortly                        |
| Burn doesn't match the transfer        | `409 ConflictError`, or `FAILED` if caught at attestation |
| Same burn attached to another transfer | `409 ConflictError` — nonces are single-use               |
| Unsupported chain key or route         | `400` at `quote` / `initiate`                             |
| Attestation deadline exceeded          | transfer `FAILED`, reason in `error`                      |

## Security model

* **Non-custodial burn** — Cesto builds the burn unsigned; only the user's wallet signs.
  (Exception: Cesto-managed Solana wallets, which Cesto signs for by design — the key never
  leaves Cesto's custody.)
* **Gas-only relayer** — the destination mint is permissionless by protocol design; the
  relayer pays gas but can only deliver to the `mintRecipient` baked into the message.
* **Verified before driven** — every burn is matched against Circle's attested message
  before a mint is attempted; nonces make double-mints impossible.
* **Key-scoped** — a transfer is visible only to the API key that created it.

## Constraints

* **USDC only**, base units as strings everywhere.
* **Relayer-dependent** — mints need a configured relayer on the destination chain; without
  one, `initiate` works but the mint fails fast with a configuration error.
* **Funds only** — positions are opened separately via [Open a Position](/sdk/open-position).
