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

# Managed Wallets

> Cesto-managed Privy wallets, signed server-side — provision per user, fund by bridging from EVM, and open / close / rebalance without handling keys.

**Managed wallets** are the primary Cesto integration: for each of your users, Cesto
provisions a **Solana wallet backed by Privy** and signs transactions on its side. Your
backend never touches a private key, never runs a signing callback, and never builds a
browser signing flow — you call `users.create` once per user, fund the wallet, and open /
close / rebalance by user reference.

This is the path to pick when your end users only have an **EVM wallet** (e.g. on Base) and
you don't want to run Solana wallet infrastructure. It is also the **recommended** path in
every how-to on this site — the "Managed" tab comes first wherever both flows are shown.

```
1. provision   your server ─▶ Cesto     users.create({ evmWalletAddress }) → { userId, solanaAddress, ... }
2. fund        user + CCTP              bridge USDC base → solana, recipient = the provisioned solanaAddress
3. invest      your server ─▶ Cesto     open.start({ user, product, amount }) — Cesto signs server-side
4. poll        your server ─▶ Cesto     poll the execution until COMPLETED / PARTIALLY_COMPLETED / FAILED
```

## Prerequisites

* **A write-scoped API key.** Every call on this page except `users.get` is a write route —
  read-only keys get a `403` (`PermissionDeniedError`). Keys are created and scoped in the
  Cesto dashboard or by the Cesto team — see [Authentication](/sdk/authentication).
* **A provisioning quota.** Each write key has a managed-user quota (contact the Cesto team
  to set or raise it). When it's full, `users.create` fails with a `403`, code
  `PROVISIONING_QUOTA_EXCEEDED`.
* **Server-side execution.** Managed calls run from **your backend** — the API key must
  never ship to a browser or mobile client. Read it from the environment
  (`process.env.CESTO_API_KEY`), never hardcode it, and keep it out of version control.
* **USDC on Base** in the user's EVM wallet (or another supported funding route) — this is
  what gets bridged in and invested.

<Warning>
  **These examples move real mainnet funds.** Bridging and investing run on Base and Solana
  mainnet with real USDC. Test with small amounts first.
</Warning>

## Provisioning users

`users.create` idempotently provisions a Cesto-managed Privy Solana wallet for a user,
keyed to their **EVM wallet address**:

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

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

const user = await cesto.users.create({
  evmWalletAddress: '0xUserEvmWallet',
  externalUserId: 'your-internal-user-id', // optional — your own reference
});
// {
//   userId,             // Cesto's user id
//   solanaAddress,      // the provisioned Solana wallet — fund and trade from here
//   evmWalletAddress,
//   externalUserId,
//   created,            // false if this user was already provisioned
// }
```

* **Idempotent** — calling `create` again for the same EVM wallet returns the same record
  with `created: false`. It's safe to call on every login.
* **One wallet per (partner, EVM wallet)** — the mapping is fully isolated per API key:
  another partner provisioning the same EVM address gets a different, unrelated user.
* **Quota** — each write key has a provisioning quota. When it's full, `create` fails with
  a `403` (code `PROVISIONING_QUOTA_EXCEEDED`) — talk to the Cesto team to raise it.
* **Rate limit** — `users.create` is limited to **10 requests/minute** per key.

To look a user up later, `users.get` resolves by EVM wallet address:

```ts theme={null}
const user = await cesto.users.get('0xUserEvmWallet');
// same shape as create — 404 if the user isn't one of your key's
```

`users.get` works with a **read-scoped** key. A `404` means this EVM wallet was never
provisioned under **your** key — even if another partner provisioned it, it isn't yours.

### Endpoint reference

| Method                                                      | Endpoint                           | Scope |
| ----------------------------------------------------------- | ---------------------------------- | ----- |
| `cesto.users.create({ evmWalletAddress, externalUserId? })` | `POST /sdk/users`                  | write |
| `cesto.users.get(evmWalletAddress)`                         | `GET /sdk/users/:evmWalletAddress` | read  |

## Funding the wallet

Users fund their managed wallet by **bridging USDC from their EVM wallet** with the
existing [bridge flow](/sdk/bridging) (Base → Solana, Circle CCTP). Two things matter:

* `recipient` is the provisioned **`solanaAddress`**.
* `sourceAddress` is the **user's EVM wallet** — the user signs the burn on Base, as usual.

<Tabs>
  <Tab title="Managed">
    Funding is the one step that looks the same in both models: the funds come from the
    **user's EVM wallet**, so the user always signs the EVM-side burn. The managed part is
    only the destination — the provisioned `solanaAddress`.

    ```ts theme={null}
    const { transferId, burn } = await cesto.bridge.initiate({
      sourceChain: 'base',
      destChain: 'solana',
      amount: '10000000', // 10 USDC, base units as a string
      recipient: user.solanaAddress,
      sourceAddress: user.evmWalletAddress, // signs the burn on Base
      mode: 'fast',
    });

    // user signs + lands the burn on Base, then:
    await cesto.bridge.submitBurn(transferId, { burnTxHash });
    const done = await cesto.bridge.waitForTransfer(transferId);
    ```

    When the transfer completes, invest what **arrived** — `done.netOut` — not the burn
    amount (`fast` mode deducts its fee at mint).
  </Tab>

  <Tab title="BYOW">
    Identical flow, but `recipient` is the user's own Solana wallet (not provisioned by
    Cesto), and the arrived USDC is invested via the client-signed
    [open flow](/sdk/open-position). The BYOW wallet needs **no SOL** for investing — Cesto
    sponsors gas — but it does need a little SOL (\~0.005) if it later **burns back out**
    (see [Bridging → Solana-source burns](/sdk/bridging#solana-source-burns)).
  </Tab>
</Tabs>

Full bridge mechanics, modes, and errors are in [Bridging (CCTP)](/sdk/bridging).

## Opening, closing, rebalancing

Managed positions are **custodial**: Cesto signs for the provisioned wallet server-side, so
no user signature, `signTransactions` callback, or split flow is needed. Each call returns
an `{ executionId }` and the execution runs in the background. All three are **write**
routes.

<Tabs>
  <Tab title="Managed">
    ```ts theme={null}
    const { executionId } = await cesto.open.start({
      user: user.solanaAddress,
      product: 'stable-genius',
      amount: 10_000_000n, // bigint, input-token base units (10 USDC @ 6 decimals)
    });

    await cesto.close.start({ user: user.solanaAddress, product: 'stable-genius' });
    await cesto.rebalance.start({ user: user.solanaAddress, product: 'stable-genius' });
    ```

    <Note>
      `start` is the single server-signed entry point, and it covers two kinds of user.
      Managed users are the case with **no `consent` field**: Cesto holds their key, so
      there is nothing for them to sign and your API key is the sole authority. A Cesto
      account holder — someone with their own wallet — must instead approve each action by
      signing a challenge, passed as `consent`. See
      [Existing Cesto users](/sdk/open-position#existing-cesto-users).
    </Note>

    The **`user`** field accepts any of your user's identifiers:

    * the **`externalUserId`** you passed at provisioning,
    * the user's **EVM wallet address**, or
    * the provisioned **Solana address**.

    As with the client-signed flow, **close** sells the user's full holding of the basket
    (no `amount`, no partial close) and **rebalance** migrates the position to the basket's
    latest version. `amount` on open is a **bigint** in input-token base units.

    Full reference: [Opening, closing, rebalancing](#opening-closing-rebalancing) on this
    page — the rest of this section applies to the managed flow.
  </Tab>

  <Tab title="BYOW">
    With a self-custody or partner-controlled Solana wallet, the wallet signs every
    transaction: `open.execute` / `open.prepare` + `positions.submit` with a
    `signTransactions` callback (or a browser wallet in the split flow). Walk through it in
    [Open a Position](/sdk/open-position), [Close a Position](/sdk/close-position), and
    [Rebalance a Position](/sdk/rebalance-position).
  </Tab>
</Tabs>

<Note>
  Managed executions are scoped per API key — one partner never sees another's users or
  executions. A `user` that doesn't resolve to one of **your** provisioned users fails with
  a `404` (code `MANAGED_USER_NOT_FOUND`).
</Note>

### Waiting for the result

The `*AndWait` convenience variants kick off the operation and then poll the existing
execution-status endpoint until a terminal status — `COMPLETED`, `PARTIALLY_COMPLETED`, or
`FAILED`:

```ts theme={null}
const result = await cesto.open.startAndWait({
  user: user.solanaAddress,
  product: 'stable-genius',
  amount: 10_000_000n,
});
result.status;       // 'COMPLETED' | 'PARTIALLY_COMPLETED' | 'FAILED'
result.transactions; // per-leg { nodeId, ok, signature?, error? }
```

`close.startAndWait` and `rebalance.startAndWait` take the same params as their `start`
counterparts. To
poll an execution you kicked off with the plain variants, use the shared
[`positions.getExecution` / `positions.waitForExecution`](/sdk/open-position#method-reference)
with the returned `executionId` — polling is identical in both models.

<Warning>
  **Partial completion is real**, exactly as in the client-signed flow: a multi-token open
  is several independent transactions with no atomicity across them. Check `result.status`
  and `result.transactions` before assuming the full amount landed.
</Warning>

## Withdrawing

Withdrawal is the mirror image of funding — and needs **no user signature at all** in the
managed model:

<Steps>
  <Step title="Close the position">
    `close.start` (or `close.startAndWait`) sells the full holding back to USDC on the
    managed wallet.
  </Step>

  <Step title="Bridge back to EVM">
    Use [`bridge`](/sdk/bridging) with `sourceChain: 'solana'`, `destChain: 'base'`, the
    managed wallet as `sourceAddress`, and the user's EVM wallet as `recipient`.
  </Step>
</Steps>

<Tabs>
  <Tab title="Managed">
    For a managed Solana source wallet, **Cesto signs and lands the burn itself** (sponsor
    pays gas and the CCTP message-account rent — managed wallets hold no SOL): `initiate`
    returns `{ status: 'BURN_SUBMITTED', burnTxHash, burn: null }`, so skip `submitBurn`
    and go straight to `waitForTransfer`.

    ```ts theme={null}
    await cesto.close.startAndWait({ user: user.solanaAddress, product: 'stable-genius' });

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

    // Managed source wallet: the burn is already signed and landed — res.burnTxHash.
    // Calling submitBurn with the same hash is safe/idempotent, but not needed.
    const done = await cesto.bridge.waitForTransfer(res.transferId);
    done.mintTxHash; // USDC minted to the user's EVM wallet on Base
    ```
  </Tab>

  <Tab title="BYOW">
    A self-custody Solana source wallet signs the burn itself — `initiate` returns
    `{ status: 'BURN_TX_BUILT', burn: { kind: 'solana', transaction, additionalSignerSecrets } }`.
    Sign the transaction with the wallet **plus** the ephemeral `additionalSignerSecrets`,
    land it, then `submitBurn`. The signing wallet must hold **\~0.005 SOL** for the
    transaction fee and the CCTP message-account rent — BYOW burns are **not**
    sponsor-paid. Details: [Bridging → Solana-source burns](/sdk/bridging#solana-source-burns).
  </Tab>
</Tabs>

## Errors

| Situation                                                 | What you get                                                                         |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Read-only key on `users.create` / `*.start`               | `403 PermissionDeniedError`                                                          |
| Invalid / missing / revoked API key                       | `401 AuthenticationError`                                                            |
| Provisioning quota full                                   | `403`, code `PROVISIONING_QUOTA_EXCEEDED` — talk to the Cesto team                   |
| Wallet provisioning fails upstream                        | `500`, code `MANAGED_USER_PROVISIONING_FAILED` — retry; contact Cesto if it persists |
| `users.create` faster than 10/min                         | `429 RateLimitError` (`retryAfter` ms) — back off and retry                          |
| `users.get` faster than 60/min                            | `429 RateLimitError`                                                                 |
| `users.get` for a wallet that isn't your key's user       | `404 NotFoundError`                                                                  |
| `*.start` (no `consent`) for a user that isn't your key's | `404`, code `MANAGED_USER_NOT_FOUND`                                                 |
| Managed wallet has insufficient USDC                      | execution leg fails → `FAILED` / `PARTIALLY_COMPLETED`                               |
| Another execution in flight for this user + product       | conflict — wait for the running execution to finish                                  |

See [Errors & Retries](/sdk/errors) for the typed error hierarchy and retry behavior.

## Rate limits

Per API key:

| Route                                                      | Limit                   |
| ---------------------------------------------------------- | ----------------------- |
| `users.create` (`POST /sdk/users`)                         | 10 requests/minute      |
| `users.get` (`GET /sdk/users/:evmWalletAddress`)           | 60 requests/minute      |
| `open.start` / `close.start` / `rebalance.start` (managed) | 10 requests/minute each |

Exceeding a limit returns `429` (`RateLimitError`) with a `retryAfter` hint. Because
`users.create` is idempotent, call it on login rather than caching a lookup table of your
own — 10/minute is ample for user-driven provisioning.

## Security notes

* **Server-side only.** Every managed call carries your API key; never expose it to a
  browser, mobile app, or logs. Load it from the environment
  (`process.env.CESTO_API_KEY`), as in every example on this page.
* **Write scope gates every mutation.** Provisioning, opening, closing, rebalancing, and
  bridging all require a write-scoped key. Keep a separate read-scoped key for status reads
  and dashboards if you want least-privilege separation.
* **No keys to leak on your side.** The managed wallet's key material lives in Cesto's
  Privy custody and never crosses your infrastructure — there is nothing in your stack to
  exfiltrate. Treat the API key itself as the sensitive secret.
* **Per-key isolation.** Users, executions, and bridge transfers are visible only to the
  API key that created them.

## End-to-end example

Provision a user, bridge USDC in from their EVM wallet, open a position, then close it and
bridge back out — no key handling anywhere on your side:

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

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

// 1. Provision (idempotent — safe to call on every login)
const user = await cesto.users.create({
  evmWalletAddress: userEvmAddress,
  externalUserId: 'user_123',
});

// 2. Bridge USDC Base → Solana, into the managed wallet
const { transferId, burn } = await cesto.bridge.initiate({
  sourceChain: 'base',
  destChain: 'solana',
  amount: '10000000', // 10 USDC
  recipient: user.solanaAddress,
  sourceAddress: userEvmAddress,
  mode: 'fast',
});

// 3. The user signs + lands the burn on Base (their wallet, e.g. via viem)
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 Bridging.
  }
  const burnTxHash = await walletClient.sendTransaction(burn.burnTx);
  await cesto.bridge.submitBurn(transferId, { burnTxHash });
}

// 4. Wait for the USDC to arrive on the managed wallet
const done = await cesto.bridge.waitForTransfer(transferId);

// 5. Open — invest what ARRIVED (netOut), not the burn amount. Cesto signs.
const opened = await cesto.open.startAndWait({
  user: user.solanaAddress,
  product: 'stable-genius',
  amount: BigInt(done.netOut!),
});
opened.status; // 'COMPLETED' | 'PARTIALLY_COMPLETED' | 'FAILED'

// 6. Later: close — sells the full holding back to USDC. Cesto signs.
await cesto.close.startAndWait({ user: user.solanaAddress, product: 'stable-genius' });

// 7. Withdraw: bridge back to the user's EVM wallet.
//    Managed source wallet → Cesto signs and lands the burn itself.
const out = await cesto.bridge.initiate({
  sourceChain: 'solana',
  destChain: 'base',
  amount: '10000000', // 10 USDC
  recipient: userEvmAddress,
  sourceAddress: user.solanaAddress,
});
const withdrawn = await cesto.bridge.waitForTransfer(out.transferId); // skip submitBurn
withdrawn.mintTxHash; // USDC minted on Base
```

<Warning>
  This flow runs against **Base and Solana mainnet** and moves real USDC. Run it with small
  amounts until you're confident in the integration.
</Warning>

## Related

* [Integration Models](/sdk/integration-models) — how managed wallets compare to BYOW setups.
* [Bridging (CCTP)](/sdk/bridging) — the funding and withdrawal rail.
* [Open a Position](/sdk/open-position) — the client-signed (BYOW) flow, and the shared
  execution-status methods.
* [Positions](/sdk/positions) — read a user's live per-basket holdings.
