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

# Versioning

> How the Rhinestone API evolves, how to pin a version, and how to migrate between them.

The Rhinestone API is versioned. Pin a version via the `x-api-version` header and upgrade on your own schedule — new versions are additive and opt-in.

## Why versioning

The API evolves as Rhinestone adds chains, settlement layers, and new intent patterns. Versioning lets us ship those changes without forcing every integrator to migrate in lockstep.

We use dated versions (`YYYY-MM.name`) rather than semver-style major versions. Semver majors tend to hoard changes into rare, big-bang releases — each bump looks like a migration project, so teams avoid shipping them. Dated versions normalise small, frequent bumps, so each one carries a small diff.

The `.name` suffix gives each release a human handle. We name versions after mountains, one per letter — `alps`, `blanc`, `caucasus`, and so on. It's easier to say "switch to Caucasus" than "switch to 2026-09".

## Pinning a version

The direct HTTP guides use Caucasus. Pin it by sending `x-api-version` on every request, including submission and reads:

```ts {6} theme={null}
const res = await fetch("https://v1.orchestrator.rhinestone.dev/quotes", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": apiKey,
    "x-api-version": "2026-09.caucasus",
  },
  body: JSON.stringify(payload),
});
```

The format is `YYYY-MM.name`. Always send the header — requests without it fall back to the deprecated `2026-01.alps` shape, and unknown or malformed values return `400 VALIDATION_ERROR`.

## Compatibility

### What we can change without a new version

Within a version, we treat the response schema as open. These changes ship freely:

* New optional request fields
* New response fields
* New enum variants in responses (e.g. a new `settlementLayer`)
* New endpoints
* Widened request validation
* Changed default values

### What your client must do

To consume those changes safely:

* **Ignore unknown fields.** If you use a strict JSON parser (Go's `DisallowUnknownFields`, Rust `serde(deny_unknown_fields)`, Jackson's `FAIL_ON_UNKNOWN_PROPERTIES`), disable it for our responses.
* **Handle unknown enum values gracefully.** A `switch` on `settlementLayer` that throws on default will break the day we add a new one. Treat unknown values as "not supported by this client" and fall back.
* **Preserve native signing payloads.** On Caucasus, dispatch on each `signingRequests[].payload.kind` and return proofs in the same order. For EIP-712, forward the supplied `typedData` to `wallet.signTypedData()` without reconstructing its types. See [signing requests and proofs](#sign-requests-and-submit-ordered-proofs).
* **Treat quote 404s as normal.** Quoted intents are stored server-side with a short TTL. If a quote expires or the quote store restarts, you'll see 404 on submit. Re-quote — don't retry the submit.
* **Take `routes[0]` unless you have your own criteria.** The array is server-ranked by a cost/speed tradeoff. Re-sorting client-side usually degrades route quality.

### What triggers a new version

* Removing or renaming any field
* Adding a required request field
* Changing a field's type
* Restructuring request or response shape
* Narrowing request validation
* Making a required response field optional or nullable
* Removing an endpoint

### Deprecation lifecycle

Fields deprecated in version N remain present and documented as deprecated; they're removed in N+1. Versions themselves are not routinely deprecated — each version is intended to stay live indefinitely so you never have to migrate on our clock.

The one exception is `2026-01.alps`, which will be sunset after integrators migrate to `blanc`. The shape difference between the two is large enough that maintaining both long-term isn't practical. Future version transitions will follow the standard per-field deprecation rule.

## Versions

### `2026-09.caucasus`

Caucasus introduces per-VM accounts, explicit source and destination assets, native execution, and ordered signing requests and proofs.

The endpoint paths stay the same:

* `POST /quotes`
* `POST /quotes/estimate`
* `POST /intents`
* `GET /intents/{id}`
* `GET /intents`

#### Migrating from Blanc

This section covers direct HTTP integrations, not changes to the SDK’s methods or configuration.

Pin Caucasus on every request, including submission and reads:

```ts theme={null}
const headers = {
  "Content-Type": "application/json",
  "x-api-key": apiKey,
  "x-api-version": "2026-09.caucasus",
};
```

Keep API keys server-side. Your existing authentication method does not change.

Changing the header alone is not enough: update request bodies, signing, and response parsing together. Requests without a version header still default to Alps.

The examples below use application-defined `postBlanc`, `postCaucasus`, `getBlanc`, and `getCaucasus` HTTP helpers. Each sends the corresponding version header and rejects non-2xx responses.

<span id="restructure-quote-requests" />

##### Restructure quote requests

Caucasus names one funding pair in `source` and one delivery asset in `destination`. Replace source allowlists and destination token arrays with explicit chain and token fields.

```ts theme={null}
const baseUsdc = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const arbitrumUsdc = "0xaf88d065e77c8cc2239327c5edb3a432268e5831";

// Before: Blanc
const blancBody = {
  account: {
    address: accountAddress,
    accountType: "EOA",
  },
  accountAccessList: {
    chainIds: ["eip155:8453"],
    tokens: [baseUsdc],
  },
  destinationChainId: "eip155:42161",
  tokenRequests: [{ tokenAddress: arbitrumUsdc, amount: "10000000" }],
};

// After: Caucasus
const caucasusBody = {
  account: {
    evm: {
      type: "eoa",
      address: accountAddress,
    },
  },
  source: {
    vm: "evm",
    chainId: "eip155:8453",
    token: baseUsdc,
  },
  destination: {
    vm: "evm",
    chainId: "eip155:42161",
    token: arbitrumUsdc,
    amount: "10000000",
  },
};
```

This requests 10 USDC on Arbitrum, funded with Base USDC. It does not fix the input amount.

| Blanc | Caucasus |
| - | - |
| `accountAccessList` | Select one `source.chainId` and `source.token` |
| Per-token `accountAccessList.chainTokenAmounts` | `source.maxAmount` |
| `options.auxiliaryFunds[chainId][tokenAddress]` | `source.auxiliaryFunds` |
| `preClaimExecutions[chainId]` | `source.execution.calls` |
| `destinationChainId` | `destination.chainId`, alongside `destination.vm` |
| `tokenRequests[].tokenAddress` | `destination.token` |
| `tokenRequests[].amount` | `destination.amount` |
| `recipient` | `destination.recipient` |

If your Blanc integration relied on automatic or multi-source funding, choose the source chain/token pair before requesting a Caucasus quote. Do not translate an allowlist by silently taking its first entry.

Amounts, caps, auxiliary funds, and EVM call values use decimal strings in their respective base units.

For binding quotes:

* `source.maxAmount` optionally caps the observed source balance available to the planner. It is not an exact-input amount.
* `destination.amount` specifies the requested output. Omit it for max-out.
* Omit `destination.token` only when there is no token delivery.
* `source.amount` belongs to estimates, not binding quotes.

**Preserve balance-credit semantics.** For a self-send without destination execution, an existing destination balance can count toward the requested amount when it is also eligible as the funding source. The example selects Base USDC, so existing Arbitrum USDC does not reduce the requested delivery.

**Caps apply before auxiliary funds.** Available funds are the capped observed balance plus the declared auxiliary amount—not the combined balance capped afterward.

Source calls and auxiliary funds are currently EVM-only. Auxiliary funds remain an assertion that funds will be available by execution time, not a transfer performed by quoting. Source calls retain their pre-claim timing.

<span id="configure-accounts-per-vm" />

##### Configure accounts per VM

Replace the flat EVM account with `account.evm`. An address-only entry is a receiver, not a spending account: include `type` when the account must fund or execute the intent.

| Blanc | Caucasus |
| - | - |
| `account.accountType: "EOA"` | `account.evm.type: "eoa"` |
| `account.accountType: "ERC7579"` | `account.evm.type: "erc7579"` |
| `account.address` | `account.evm.address` |
| `account.setupOps` | `account.evm.initData.setupOps` |
| `options.signatureMode` | `account.evm.signatureMode` |
| `account.mockSignatures["0"]` | `account.evm.simulation.mockSignature` |
| `account.mockSignatures["8453"]` | `account.evm.simulation.mockSignaturesByChain["eip155:8453"]` |
| `account.delegations["0"]` | `account.evm.delegations.default` |
| `account.delegations["8453"]` | `account.evm.delegations.chains["eip155:8453"]` |

`signatureMode` remains a numeric enum, not a string such as `"ECDSA"`. Simulation stubs and setup operations belong to ERC-7579 accounts.

Solana spending uses `account.svm`. For a standalone, already-created Swig:

```ts theme={null}
const account = {
  svm: {
    type: "swig",
    address: swigWalletAddress,
    swigAccount: swigStateAddress,
    authorization: {
      kind: "secp256k1",
      address: authorityAddress,
    },
  },
};
```

`address` is the asset-holding wallet; `swigAccount` is the state account containing its roles. They are not interchangeable. P-256 authorities use `kind: "secp256r1"` with their compressed `publicKey`.

You can supply EVM and SVM entries together, but this does not authorize arbitrary wallet pairings. The orchestrator checks the supported pairing, account identity, and signing authority.

<span id="move-destination-execution" />

##### Move destination execution

Group execution with its destination:

| Blanc | Caucasus |
| - | - |
| `destinationExecutions` | `destination.execution.calls` |
| `destinationGasLimit` | `destination.execution.gasLimit` |
| `options.executionTokensReceived` | `destination.execution.executionTokensReceived` |
| `destinationInstructions` | `destination.execution.instructions` |
| `addressLookupTableAddresses` | `destination.execution.addressLookupTables` |

For EVM calls:

```ts theme={null}
// Before: fields on the Blanc quote body
const blancExecution = {
  destinationExecutions: calls,
  destinationGasLimit: "200000",
};

// After: fields inside the Caucasus destination
const destination = {
  ...caucasusBody.destination,
  execution: {
    calls,
    gasLimit: "200000",
  },
};
```

Each EVM call still contains `to`, `value`, and `data`. Preserve call order.

An explicit receiver is `destination.recipient: { address }`. Configured EVM recipients use the same lower-case account types and nested setup/delegation fields, but do not carry the spender’s `signatureMode`.

Solana delivery requires a recipient. Solana execution instead runs from the configured Swig: omit `recipient` and encode any payee in the instructions. Tron and Stellar destinations are delivery-only.

A request without `source` is restricted to no-delivery intents with effective sponsorship covering the applicable orchestrator costs. It cannot fall back to another account balance. Sponsorship does not fund arbitrary caller-authored EVM call value.

<span id="read-the-quote-plan-and-requirements" />

##### Read the quote plan and requirements

Blanc returns `{ routes }`. Caucasus returns `{ status: "quoted", routes }`.

```ts theme={null}
// Before
const blancQuote = await postBlanc("/quotes", blancBody);
const { signData, tokenRequirements } = blancQuote.routes[0];

// After
const caucasusQuote = await postCaucasus("/quotes", caucasusBody);
if (caucasusQuote.status !== "quoted") {
  throw new Error("Unsupported quote outcome");
}

const route = caucasusQuote.routes[0];
const { plan, requirements, signingRequests } = route;
```

Each route declares its `purpose`:

* `execution`: token delivery or execution.
* `deployment`: a separate account-creation intent.

Review `plan.source`, `plan.destination`, and `plan.deployments` before signing. Execution disclosures identify whether the account or a solver runs the caller’s calls. A plan describes intended behavior, not completed execution or every transaction-internal operation.

`requirements[]` replaces the chain-keyed `tokenRequirements` map:

* `erc20Approval` names the account, chain, token, amount, and spender.
* `wrapNative` names the account, chain, token, and amount.

The array is always present. An empty array means no preflight requirement; `status: "quoted"` does not mean nonempty requirements are already fulfilled.

Route ranking, expiry, and the existing `cost.input`, `cost.output`, and `cost.fees` structure remain. Caucasus presents same-chain routes as `SAME_CHAIN`, including smart-account routes that Blanc labelled `INTENT_EXECUTOR`. When present, `bridgeFill.destinationChainId` is a chain-reference string.

<span id="sign-requests-and-submit-ordered-proofs" />

##### Sign requests and submit ordered proofs

Replace role-specific signature slots with one ordered array:

```text theme={null}
signingRequests[i] → proofs[i]
```

Every request includes its account, authority, scope, purpose, chain references, validity constraints, and native `payload`.

Do not sort, deduplicate, or omit requests—even when two payloads are identical. Proof count, order, and kind must match the selected route’s requests.

This example assumes preflight requirements are satisfied and every signing request uses the same EOA with EIP-712 signing, without EIP-7702 delegation:

```ts theme={null}
// Before: Blanc
const beforeRoute = blancQuote.routes[0];

const origin = [];
for (const data of beforeRoute.signData.origin) {
  if (data.kind !== "eip712") {
    throw new Error("This example only handles EIP-712");
  }
  origin.push(await signer.signTypedData(data));
}

const destinationSignature = beforeRoute.signData.destination
  ? await signer.signTypedData(beforeRoute.signData.destination)
  : undefined;

await postBlanc("/intents", {
  intentId: beforeRoute.intentId,
  signatures: {
    origin,
    destination: destinationSignature,
  },
});

// After: Caucasus
const proofs = [];

for (const request of route.signingRequests) {
  const payload = request.payload;

  if (payload.kind !== "eip712" || payload.signatureFormat !== "secp256k1") {
    throw new Error("This example only handles EOA EIP-712 proofs");
  }

  proofs.push({
    kind: "eip712",
    signature: await signer.signTypedData(payload.typedData),
  });
}

await postCaucasus("/intents", {
  intentId: route.intentId,
  proofs,
});
```

For other signing paths, select the signer from the request’s authority and dispatch on `payload.kind`:

| Payload kind | Proof and encoding |
| - | - |
| `eip712` | `{ kind: "eip712", signature }`. Sign the supplied `typedData`. Account-format requests require the account’s native signature encoding, not necessarily a 65-byte EOA signature. |
| `personalSign` | `{ kind: "personalSign", signature }`. Sign `payload.message.value` as UTF-8 text, not decoded hex bytes. |
| `eip7702` | `{ kind: "eip7702", nonce, signature: { r, s, yParity } }`. Use the requested chain and delegate; supply the authorization nonce used by the signer. |
| `webauthn` | `{ kind: "webauthn", assertion }`. Decode the hex challenge to bytes before requesting the assertion. |

For WebAuthn, send `credentialId` as base64url, `authenticatorData` as hex, the original UTF-8 `clientDataJSON`, and the P-256 signature as 64-byte hex `r || s`, not DER. Do not parse and serialize `clientDataJSON` again.

For an account-verified origin request that needs two signature encodings, Blanc’s `{ preClaimSig, notarizedClaimSig }` becomes:

```ts theme={null}
const proof = {
  kind: "eip712",
  signature: {
    preClaim,
    notarizedClaim,
  },
};
```

This is one proof, not two. It is not accepted for every account-format request.

Separate `authorizations.sponsor` and `authorizations.recipient` bags disappear. Answer the corresponding EIP-7702 requests in `proofs[]`. Caucasus requests concrete-chain authorizations; a configured delegation default is not an any-chain authorization proof.

Honor every returned validity constraint as well as route expiry. A Solana slot deadline can expire before `route.expiresAt`. Never reconstruct or rewrite native signing payloads: their chain values retain native formats, including numeric EIP-712 and EIP-7702 chain IDs.

Submission still returns `{ intentId }`. That acknowledges submission, not completion. Poll the intent before starting a replacement when the submission outcome is uncertain.

<span id="update-sponsorship" />

##### Update sponsorship

Rename `options.sponsorSettings` to `options.sponsorship`:

```ts theme={null}
// Before
const blancOptions = {
  sponsorSettings: {
    gas: true,
    bridgeFees: true,
    swapFees: false,
  },
};

// After
const caucasusOptions = {
  sponsorship: {
    gas: true,
    bridgeFees: true,
    swapFees: false,
  },
};
```

The same rename applies to estimates. Fee flags retain their meanings; requesting sponsorship does not bypass authorization, policy, or available-budget checks.

If you issue intent-bound sponsorship grants, update the approval binding too. The approved input is:

```ts theme={null}
const approvalInput = {
  contractVersion: "sdk-3.0.0-caucasus",
  account: body.account,
  ...(body.source !== undefined && { source: body.source }),
  destination: body.destination,
  options: body.options ?? {},
};
```

The digest is SHA-256 of the RFC 8785 canonical JSON representation, encoded as lowercase hex. Preserve supplied wire values, explicit `false` settings, and array order. Omit an absent `source`; use `{}` for absent `options`.

Do not reuse a Blanc digest. The approval-contract identifier is distinct from the HTTP version header and must match exactly.

See [sponsorship](/transactions/sponsorship/overview) for policy configuration.

<span id="update-intent-reads" />

##### Update intent reads

Continue polling `GET /intents/{intentId}`. Add `?full=true` for extended details. Overall statuses remain `PENDING`, `COMPLETED`, and `FAILED`.

| Blanc | Caucasus |
| - | - |
| `accountAddress` | Optional `accounts[]` summaries, tagged by VM and chain |
| `operations[].chain` | `operations[].chainId` |
| `operations[].items[].txHash` | `operations[].items[].transaction` |
| Refund `{ chain, txHash }` | Refund `{ transaction }` |
| `details.id` | Top-level `intentId` |
| String `details.recipient` | Optional `{ vm, chainId, address }` |
| Source/destination leg `chain` and `txHash` | `chainId` and `transaction` |
| Flat `details.executions` and separate Solana execution | `details.executions.source[]` and `.destination` |
| Top-level `hyperCore.outcome` | Native result on an `EXECUTION` operation |

For transaction links:

```ts theme={null}
// Before
const before = await getBlanc(`/intents/${intentId}`);

const oldTransactions = before.operations.flatMap(({ chain, items }) =>
  items
    .filter((item) => item.txHash)
    .map((item) => ({ chain, txHash: item.txHash })),
);

// After
const after = await getCaucasus(`/intents/${intentId}`);

const transactions = after.operations.flatMap(({ items }) =>
  items.flatMap((item) =>
    "transaction" in item && item.transaction ? [item.transaction] : [],
  ),
);
```

Dispatch transaction references by `vm`:

| VM | Identifier |
| - | - |
| `evm` | `txHash` |
| `svm` | `signature` |
| `tvm` | `txId` |
| `stellar` | `txHash` |
| `unknown` | Original stored `id` |

Known chain references use strings such as `eip155:8453` or `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`. Blanc quote requests already used chain-reference strings; its read/list projections used numeric chains.

Historical records that cannot be mapped to a known chain can retain numeric chain IDs and `vm: "unknown"`. Do not invent an EVM chain reference for them.

Full details retain `nonce` in hex and add optional deployment summaries. Keep `intentId` as a string; it can exceed JavaScript’s safe integer range.

Treat missing historical accounts, authorities, executions, or deployment data as unavailable—not as empty data or proof that nothing happened. Execution details describe intended execution, not per-call results.

`refunds: []` means no refund has been observed, not that a failed intent can never be refunded. A transaction reference alone is not proof of finality.

Recorded cost amounts remain integer micro-USD. `details.cost.sponsored` describes recognized sponsorship coverage and can be true while billed amounts are zero or absent.

<span id="update-intent-lists" />

##### Update intent lists

`GET /intents` keeps its `{ data, pagination }` envelope.

| Blanc list item | Caucasus list item |
| - | - |
| `id` | `intentId` |
| `account` | Optional `accounts[]` |
| `fromChains` | `sourceChainIds` |
| `toChain` | Optional `destinationChainId` |
| `token`, `amount`, `symbol`, `decimals` | Optional `value` object, including `side`, `chainId`, and `tokenAddress` |

```ts theme={null}
// Before
const beforePage = await getBlanc("/intents?limit=20");

const oldSummaries = beforePage.data.map((item) => ({
  id: item.id,
  amount: item.amount,
  symbol: item.symbol,
}));

// After
const afterPage = await getCaucasus("/intents?limit=20");

const summaries = afterPage.data.map((item) => ({
  id: item.intentId,
  side: item.value?.side,
  amount: item.value?.amount,
  symbol: item.value?.symbol,
}));
```

`value.side` distinguishes planned output from retained input. The summary is not a wallet balance change, realized swap result, or proof of delivery. `value` is omitted when no single amount truthfully represents the intent, including deployment-only and historical multi-token cases.

Pass `pagination.nextCursor` back unchanged as `cursor`; it is not an intent ID. Caucasus lists can include non-EVM accounts that Blanc excludes, so identical pagination requests need not return identical records.

<span id="migrate-estimates" />

##### Migrate estimates

`POST /quotes/estimate` uses the same source/destination grouping, but its account descriptors are hypothetical. They contain account types, not wallet addresses or signing configuration.

```ts theme={null}
// Before: Blanc, exact-in
const blancEstimate = {
  direction: "exactIn",
  sourceChainId: "eip155:8453",
  sourceToken: baseUsdc,
  destinationChainId: "eip155:42161",
  destinationToken: arbitrumUsdc,
  amountIn: "10000000",
  accountType: "SMART_ACCOUNT",
  accountDeployed: true,
};

// After: Caucasus, exact-in
const caucasusEstimate = {
  account: {
    evm: { type: "erc7579" },
  },
  source: {
    vm: "evm",
    chainId: "eip155:8453",
    token: baseUsdc,
    amount: "10000000",
    assumptions: { deployed: true },
  },
  destination: {
    vm: "evm",
    chainId: "eip155:42161",
    token: arbitrumUsdc,
  },
};
```

Caucasus infers direction from exactly one amount:

| Estimate | Blanc | Caucasus |
| - | - | - |
| Exact-in | `direction: "exactIn"` and `amountIn` | `source.amount` |
| Exact-out | `direction: "exactOut"` and `amountOut` | `destination.amount` |

For exact-out, remove the source amount and set the destination amount:

```ts theme={null}
const { amount: _amount, ...source } = caucasusEstimate.source;

const exactOutEstimate = {
  ...caucasusEstimate,
  source,
  destination: {
    ...caucasusEstimate.destination,
    amount: "10000000",
  },
};
```

Do not send `direction`, `amountIn`, or `amountOut`. Supplying both amount sides—or neither—is invalid.

Estimate responses move financial fields into `cost`:

```ts theme={null}
// Before
const beforeEstimate = await postBlanc("/quotes/estimate", blancEstimate);
const oldInput = beforeEstimate.routes[0].input;
const oldFees = beforeEstimate.routes[0].fees;

// After
const afterEstimate = await postCaucasus("/quotes/estimate", caucasusEstimate);
const input = afterEstimate.routes[0].cost.input[0];
const fees = afterEstimate.routes[0].cost.fees;
const assumptions = afterEstimate.routes[0].assumptions;
```

`output` likewise becomes `cost.output[]`. The response reports the account assumptions used.

`source.assumptions.deployed` applies to the source ERC-7579 account. Omission assumes undeployed; the field is ignored for EOA and Swig accounts.

Estimates do not accept quote-only caps, auxiliary funds, recipients, or execution. They do not apply actual destination-balance credit and return no executable intent ID or signing requests. Obtain a binding quote before signing or submitting.

<span id="migrate-hypercore-actions-and-outcomes" />

##### Migrate HyperCore actions and outcomes

HyperCore is a native destination variant:

```ts theme={null}
// Before: fields on a Blanc quote body
const blancHyperCore = {
  destinationChainId: "hypercore:perp",
  options: {
    hyperCore: { actions },
  },
  destinationExecutions: settlementCalls,
  destinationGasLimit: "200000",
};

// After: the Caucasus destination
const hyperCoreDestination = {
  vm: "hypercore",
  chainId: "hypercore:perp",
  token: hyperEvmUsdc,
  amount: "10000000",
  execution: {
    actions,
    settlement: {
      calls: settlementCalls,
      gasLimit: "200000",
    },
  },
};
```

Use one ordered `actions[]` array. A former singular `options.hyperCore.action` becomes a one-element array. Keep caller-authored HyperEVM calls in `execution.settlement`, separate from Core actions.

Preserve `hypercore:spot` versus `hypercore:perp`. Neither is interchangeable with HyperEVM’s `eip155:999` settlement chain or a signing-domain chain ID. USDC delivery addressing and units remain unchanged.

On reads, replace `hyperCore.outcome` with the aggregate venue operation:

```ts theme={null}
const outcome = intent.operations
  .flatMap(({ items }) => items)
  .find((item) => item.type === "EXECUTION" && item.result.vm === "hypercore")
  ?.result.outcome;
```

HyperEVM transaction evidence remains separate. A list entry’s `destinationChainId` can name the HyperCore venue while `value.chainId` names HyperEVM.

`accepted` means the exchange accepted the action, not that an order filled completely. Reconcile `unknown` and `partial` outcomes against HyperCore before retrying; blindly repeating a batch can duplicate successful orders.

<span id="respect-capability-boundaries" />

##### Respect capability boundaries

A schema variant does not guarantee an available route. Chain/token support, account authority, settlement providers, sponsorship, and environment configuration still apply.

| Flow | Boundary |
| - | - |
| EVM → Solana | Token delivery is supported; cross-chain execution of caller instructions from the destination Swig remains deferred. |
| Solana → Solana | Supported SPL-token transfers and caller instructions require an existing authorized Swig. Do not assume arbitrary swaps or native-SOL transfers. |
| Solana → EVM | Destination calls require the supported EVM-derived Swig pairing and its ordered proofs. |
| Native SOL origin | Limited to the supported Relay Solana → EVM path; rent is excluded from spendable funds. |
| Tron and Stellar | Delivery-only destinations, not additional funding or execution environments. |

A missing Swig needed for spending returns `SOLANA_ACCOUNT_NOT_CREATED`. Quoting does not silently create it.

Separate sponsored Swig creation uses an ordinary `status: "quoted"` response with a `purpose: "deployment"` route. Rent is disclosed separately in `deploymentCosts`; `signingRequests` is empty and submission uses `proofs: []`.

This is not automatic creation-plus-spend or a `deploymentRequired`/re-quote response. Creation trusts the supplied root authority: verify it before creating or funding the wallet. Do not assume it is safe to fund an undeployed Swig.

Sponsored Swig authority changes are separate typed operations, not arbitrary caller instructions. Existing role permissions and lockout protections still apply.

<span id="migration-checklist" />

##### Migration checklist

Before switching traffic:

* Pin Caucasus on quote, estimate, submit, and read requests.
* Select one funding pair and preserve the intended balance-credit behavior.
* Review the route plan and fulfill preflight requirements.
* Handle supported signing kinds without changing payloads or proof order.
* Update intent-bound sponsorship bindings alongside request bodies.
* Parse native transaction references and tolerate unavailable historical data.
* Reconcile uncertain submissions and HyperCore outcomes before creating replacement intents.

Blanc remains a separate version. Do not reinterpret an existing quote’s signing data as a Caucasus response.

### `2026-04.blanc`

The previous contract used by Blanc integrations. Keep this header when continuing to use Blanc request and response shapes; follow the [Caucasus migration](#migrating-from-blanc) to upgrade. The endpoint reference documents Caucasus; Blanc's schema is in the published [`2026-04.blanc` OpenAPI document](https://raw.githubusercontent.com/rhinestonewtf/openapi/refs/heads/main/orchestrator/blanc.json).

**What changed:**

* **Flat `routes[]` array.** Each route carries its own `intentId`, `cost`, `signData`, and `tokenRequirements` (EOAs only — smart accounts handle approvals internally). Pre-ranked; `routes[0]` is recommended.
* **CAIP-2 chain ids.** Every chain id on the wire is now an `eip155:<chainId>` string — `1` becomes `"eip155:1"`. Affects scalar fields, arrays, and chain-keyed maps. Path parameters are unchanged.
* **EIP-712 typed data in the response.** Forward `signData.origin[]` and `signData.destination` directly to `wallet.signTypedData()`. No more client-side type reconstruction.
* **Server-stored intents.** `POST /quotes` returns an `intentId`; the full intent stays server-side. Submit via `POST /intents` with `{ intentId, signatures }` — no more round-tripping `intentOp`.
* **Resource-style endpoints.** `POST /quotes` (was `/intents/route`), `POST /intents` (was `/intent-operations`), `GET /intents/:id` (was `/intent-operation/:id`), `POST /intents/splits` (was `/intents/split`).
* **Flattened cost structure.** One `cost` object per route with `input`, `output`, and `fees: { total, breakdown }`. Replaces scattered `tokensSpent` / `tokensReceived` / `feeBreakdownUSD` / `gasCost` / `sponsorFee`.
* **Submit response collapsed.** `POST /intents` returns just `{ intentId }` (was `{ result: { id, status } }`). The synthesised `status: "FAILED"` is gone — simulation failures now surface as 4xx with the unified error envelope.
* **Field renames.** Notable: `destinationGasUnits` → `destinationGasLimit`, `sponsorSettings.{gas,bridgeFees,swapFees}Sponsored` → `{gas,bridgeFees,swapFees}`, `userAddress` → `accountAddress`. Portfolio token shape uses `symbol` (was `tokenName`), `chains` (was `tokenChainBalance`), and per-chain `address`/`decimals` (were `tokenAddress`/`tokenDecimals`). Timestamps move from `BigIntLike` strings to integer Unix seconds.
* **Unified error envelope.** All non-2xx responses share `{ code, message, traceId, details? }`. Switch from message-pattern matching to `code`-based dispatch.
* **Schema trimming.** `Account.mockSignature` is gone — use the per-chain `Account.mockSignatures` map. `AccountType.SMART_ACCOUNT` is gone — use `ERC7579`. The `accountAccessList` flat-array variant is gone — use the structured shape.
* **Compact / resource locks removed.** No blanc equivalent for `topupCompact`, `emissaryConfig`, `MultichainCompact` origin signatures, or the `/withdrawals` endpoints.

#### Migrating from `alps`

**Quote and submit**

```ts theme={null}
// Before (alps) — client round-trips the full intentOp
const { intentOp } = await post("/intents/route", body);
const signatures = await sign(intentOp);
await post("/intent-operations", {
  signedIntentOp: { ...intentOp, ...signatures },
});

// After (blanc) — server stores the intent, client submits signatures by id
const { routes } = await post("/quotes", body);
const { intentId, signData } = routes[0];
const signatures = {
  origin: await Promise.all(signData.origin.map(signTypedData)),
  destination: await signTypedData(signData.destination),
};
await post("/intents", { intentId, signatures });
```

**Signing**

```ts theme={null}
// Before (alps) — client hashes intent fields with hand-built EIP-712 types
const hash = getIntentHash(intentOp);
const sig = await signer.signTypedData(types, hash);

// After (blanc) — server provides the typed data directly
const sig = await signer.signTypedData(signData.origin[0]);
```

**Submit response**

```ts theme={null}
// Before (alps) — wrapped, with synthesised status
const { result } = await post("/intent-operations", body);
const id = result.id; // result.status was "PENDING" or synthesised "FAILED"

// After (blanc) — id only; simulation failures surface as 4xx
const { intentId } = await post("/intents", body);
```

**Status polling**

```ts theme={null}
// Before (alps)
const op = await get(`/intent-operation/${id}`);
const status = op.result.status;

// After (blanc)
const op = await get(`/intents/${intentId}`);
const status = op.status;
```

The status enum is narrower in blanc — legacy values that no longer apply are removed. `PRECONFIRMED` is gone; if your client branches on it, drop that case.

**Splits endpoint**

`POST /intents/split` → `POST /intents/splits`. Request and response shapes follow the same blanc-wide rules (CAIP-2 chain ids, unified error envelope).

**Chain ids**

CAIP-2 strings (`eip155:<chainId>`) replace numeric chain ids on every field that carries one — `chainId`, `sourceChainId`, `destinationChainId`, `chainIds`, `allChainIds`, and chain-keyed maps like `auxiliaryFunds`, `preClaimExecutions`, `chainTokens`, and `tokenRequirements`. Path parameters are unchanged.

```ts theme={null}
// Before (alps)
{ "chainId": 42161 }
{ "tokenRequirements": { "1": [...], "42161": [...] } }

// After (blanc)
{ "chainId": "eip155:42161" }
{ "tokenRequirements": { "eip155:1": [...], "eip155:42161": [...] } }
```

The portfolio filter switches from comma-separated lists to repeated query parameters, with `tokens` keyed as `chain:address`:

```
GET /accounts/0xabc/portfolio
  ?chainIds=eip155:1&chainIds=eip155:137
  &tokens=eip155:1:0xa0b...&tokens=eip155:137:0xa0b...
```

**Reading costs**

```ts theme={null}
// Before (alps)
const gasUSD = response.feeBreakdownUSD.gas;
const inputAmount = response.tokensSpent[0].amount;

// After (blanc)
const gasUSD = route.cost.fees.breakdown.gas.usd;
const inputAmount = route.cost.input[0].amount;
```

**Field renames**

| Before (alps) | After (blanc) |
| - | - |
| `userAddress` | `accountAddress` |
| `destinationGasUnits` | `destinationGasLimit` |
| `tokensSpent` | `cost.input` |
| `tokensReceived` | `cost.output` |
| `feeBreakdownUSD` | `cost.fees.breakdown` |
| `tokenName` | `symbol` |
| `tokenChainBalance` | `chains` |
| `tokenAddress` | `address` |
| `tokenDecimals` | `decimals` |

Timestamps move from `BigIntLike` strings (`"1700000000"`) to integer Unix seconds (`1700000000`).

**Sponsor settings**

```ts theme={null}
// Before (alps)
{
  sponsorSettings: {
    gasSponsored: true,
    bridgeFeesSponsored: false,
    swapFeesSponsored: false,
  }
}

// After (blanc)
{
  sponsorSettings: {
    gas: true,
    bridgeFees: false,
    swapFees: false,
  }
}
```

**Account types and mock signatures**

`AccountType.SMART_ACCOUNT` is gone — use `ERC7579`. The flat `accountAccessList` array shape is gone — use the structured shape. Mock signatures move from a single `Account.mockSignature` to a per-chain `Account.mockSignatures` map.

```ts theme={null}
// Before (alps)
{
  account: {
    accountType: "SMART_ACCOUNT",
    mockSignature: "0x...",
  }
}

// After (blanc)
{
  account: {
    accountType: "ERC7579",
    mockSignatures: {
      "eip155:1": "0x...",
      "eip155:42161": "0x...",
    },
  }
}
```

**Error handling**

```ts theme={null}
// Before (alps) — message-pattern matching
if (err.message === "Insufficient balance") { /* ... */ }
if (err.message.startsWith("Unsupported chain ")) { /* ... */ }

// After (blanc) — code-based dispatch
switch (err.code) {
  case "INSUFFICIENT_LIQUIDITY":
    // err.details: { availableIntents, unfillable }
    break;
  case "VALIDATION_ERROR":
    // err.details: [{ message, context? }]
    break;
}
```

Honour `Retry-After` on `429 TOO_MANY_REQUESTS`.

#### Error codes

| Code | HTTP |
| - | - |
| `VALIDATION_ERROR` | 400 |
| `UNAUTHORIZED` | 401 |
| `FORBIDDEN` | 403 |
| `NOT_FOUND` | 404 |
| `CONFLICT` | 409 |
| `UNPROCESSABLE_CONTENT` | 422 |
| `INSUFFICIENT_LIQUIDITY` | 422 |
| `TOO_MANY_REQUESTS` | 429 |
| `INTERNAL_ERROR` | 500 |
| `SETTLEMENT_QUOTE_ERROR` | 502 |
| `SETTLEMENT_EXECUTION_ERROR` | 502 |
| `RELAYER_MARKET_UNAVAILABLE` | 503 |
| `EXTERNAL_SERVICE_TIMEOUT` | 504 |

The list is extensible — add unknown codes to a generic fallback path.

#### Things you can't migrate

The Compact / resource-lock surface has no blanc equivalent. If your integration depends on `topupCompact`, `emissaryConfig`, the `/withdrawals` endpoints, or `MultichainCompact` origin signatures, stay on `alps` until a replacement ships.

### `2026-01.alps`

The original release. Intents are round-tripped through the client, signed via client-reconstructed EIP-712, and submitted with the full `intentOp` attached to `signedIntentOp`. The Compact / resource-lock surface (`topupCompact`, `emissaryConfig`, `/withdrawals`, `MultichainCompact` signatures) is only available on this version — stay on `alps` if you depend on it.

<Warning>
  Deprecated. Will be removed once existing integrations migrate to `blanc`. No new features will land on this version.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.