Skip to main content
How you open depends on who controls the Solana wallet. The managed flow is the recommended default: Cesto signs server-side and you make one call. The BYOW flow is for self-custody or partner-controlled wallets: Cesto builds the exact unsigned transactions, the wallet signs them, and Cesto verifies and lands them — Cesto never holds the user’s key.
Examples on this page move real mainnet funds. Open positions run on Solana mainnet with real USDC — test with small amounts first.

Existing Cesto users

There is a third case, between the two tabs above: the wallet already belongs to a Cesto account holder. Cesto can sign for them — but they own the key, so a write-scoped API key alone must never move their funds. Instead the user approves the action once, by signing a message, and Cesto opens the position with their Cesto wallet. No per-transaction signing, no round trip to the browser for each leg. It is the same open.start as the managed flow, with a consent field added — that field is the whole difference between the two. A managed user cannot produce a consent signature (Cesto holds the key); an account holder must. Check first — this path only works for wallets Cesto holds a key for:
An external wallet (Phantom, Solflare) that has never used Cesto returns false; those users take the BYOW flow above. See Users.
1

Create the approval challenge

The SDK derives the canonical params itself and resolves any slug to the exact product id start will carry, so the approval always matches the action.
2

The user signs the message

An Ed25519 signature over challenge.message, base58-encoded, from challenge.expectedPubkey. It’s a message — not a transaction — so there is nothing to land on chain.
3

Start the open

4

Track the result

start resolves as soon as the job is queued, not when it has settled. Read the position back to see the outcome:
What the user approves is bound to the action and its parameters — this wallet, this basket, this amount. The signature cannot be replayed against a different amount or basket. The challenge is short-lived and single-use, so create it when the user is ready to approve rather than ahead of time.
positions.getExecution / waitForExecution cannot see these executions. They are recorded against the user rather than the API key that started them, and those methods are scoped to the issuing key — so polling one returns 404 indefinitely rather than ever reaching a terminal status. Use getHoldings as above. For the same reason open.startAndWait rejects when consent is present, instead of polling forever.This applies only to the approval flow. The managed and client-signed flows poll normally.
start is never retried automatically, whatever maxRetries says. The approval is single-use, so a request that landed server-side but whose response was lost would be rejected on retry as a replayed nonce — reporting failure for an open that is actually running. On an ambiguous failure, read positions.getHoldings to see whether the open landed, rather than re-sending.

Execution results

Terminal statuses are COMPLETED, PARTIALLY_COMPLETED, and FAILED — identical in both models.
Partial completion is real. A multi-token open is several independent transactions with no atomicity across them. If a leg fails, the execution ends PARTIALLY_COMPLETED and result.transactions shows exactly which legs landed (with signatures) and which failed. What to do next is your call: retry the open for the remainder, or close what landed.
To read the resulting position, use positions.getHoldings — SDK positions are self-custody and do not appear in positions.list.

Common errors

Constraints

  • Swap-only baskets (one or more tokens). No prediction markets, lending, or perps.
  • Open during close is gated — opening while a close is in flight for the same wallet + product is rejected with 403, code FORBIDDEN_OPERATION. Concurrent opens are otherwise not limited.
  • Gas sponsored — Cesto pays network gas + ATA rent; the wallet needs only the input token (USDC), no SOL (unless the deployment runs without a sponsor).

Closing & rebalancing

Closing is symmetric — see Close a Position. To migrate a position to the basket’s latest version, see Rebalance a Position.