---
title: "Build a signable/sendable plan for a swap"
method: POST
path: "/swap"
---

# Build a signable/sendable plan for a swap

`POST /swap`

## Request

- Base URL: `https://swap.vurto.cc/v1`
- URL: `https://swap.vurto.cc/v1/swap`
- Auth: none required

No credential is required; wallet is mandatory and declares the EVM address that will sign. Two modes: pass `quoteId` from a prior POST /quote to build exactly that route; omit it and pass the intent fields (chainId, tokenIn, tokenOut, amount|amountRaw) to quote and build the best route in one round trip. When it picks the route itself, a route with GAIN comes first; if the first round has none, it re-quotes up to 3 rounds total (1.5 s apart) before falling back to the best route without gain, the same rule as Invoice. When the first round has no gain this costs up to two extra quote rounds plus 3 s of pauses, about 7-8 s more measured on Base on 02/10/2026; with gain in the first round it costs nothing. Always re-quotes fresh before building — if the market moved, `quote.amountOut` reflects the fresh price, not the one from POST /quote.

## Request body

- object
  - `wallet` string, required: EVM address that will sign the plan.
  - `quoteId` string
  - `chainId` integer
  - `tokenIn` string
  - `tokenOut` string
  - `amount` string
  - `amountRaw` string
  - `slippageBps` integer
  - `maxSlippageBps` integer: Optional ceiling, 0-10000, for every future POST /swap/{buildId}/refresh of this build. Once set it never loosens: a refresh asking for a laxer value is clamped back down to this.
  - `receiver` string: Optional address to deliver the output to, if different from the signing wallet. Ignored when `quoteId` is set — the receiver from that quote is used. Only honored by providers with `supportsReceiver: true`.
  - `calldataSemSimulacao` boolean: Opt-in: return the swap calldata even when the final simulation has not run, for DIRECT-POOL routes only. For an integrator that simulates on its own before listing the route, which is how aggregator adapters work. The default stays off and the general rule holds: a simulation that did not pass comes with no transaction. In a direct route the calldata is built by us from the pool we read, so what is missing is the proof of effect, not the provenance; relaying an aggregator calldata unsimulated stays refused. simulation.status is returned untouched, so the response never lies about what was checked.

## Response `200`

OK

- SwapPlan
  - `buildId` string, uuid: Identity of this specific build. Use in POST /swap/{buildId}/refresh.
  - `buildHash` string: sha256 canonical hash of chainId + wallet + destination + calldata + value. Recompute and compare before signing if you don't trust the transport.
  - `wallet` string
  - `chainId` integer
  - `validUntil` integer: Unix ms. The build expires 10 minutes after creation; call refresh after this.
  - `quote` NormalizedQuote
    - `id` string: Opaque quote id. Pass as `quoteId` to POST /swap or POST /orders.
    - `provider` 'velora' | 'kyberswap' | 'uniswap' | 'aerodrome' | 'sushi' | 'pancake' | 'quickswap' | 'composto' | '1inch' | 'cowswap': Full provider list varies by chain; see GET /chains and provider health.
    - `venues` object[]: The exchanges the route crosses, in order, present only when there is more than one. A `composto` quote carries two entries; a single-venue route omits the field entirely.
      - `provider` string
      - `tokenIn` string
      - `tokenOut` string
    - `providerLabel` string
    - `chainId` integer
    - `tokenIn` string
    - `tokenOut` string
    - `amountIn` string: Raw integer string, in the input token's smallest unit.
    - `amountOut` string: Raw integer string, the provider's expected output.
    - `minimumAmountOut` string: Raw integer string, the worst acceptable output after slippage. This is what the on-chain call enforces.
    - `amountOutUsd` number, nullable
    - `gasUsd` number, nullable
    - `netValueUsd` number, nullable: Output value minus estimated gas minus platform fee. This is the ranking key, not amountOut.
    - `priceImpact` number, nullable
    - `platformFeeBps` integer
    - `platformFeeAmount` string
    - `confidence` integer: 0-100, lower when the provider's own health has degraded recently.
    - `latencyMs` integer
    - `quotedAt` integer: Unix ms.
    - `expiresAt` integer: Unix ms. The provider's own quote TTL, informational — POST /swap always re-quotes fresh before building.
    - `quoteBlock` integer, nullable
    - `mevProtection` 'full' | 'partial' | 'public' | 'unknown'
    - `supportsReceiver` boolean: Whether this provider can deliver the output to a `receiver` different from the wallet that signs.
    - `receiver` string: Present only when the request set a `receiver` different from the signing wallet.
    - `approvalTarget` string, nullable: The spender the approve step of the build will grant the allowance to, so pre-approving this address is safe. It is the same address as `steps[].spender` and `approval.spender` for the same provider. Never absent when an allowance is needed. `null` when there is nothing to approve: native input token, or a CoW order selling the native asset. The Vurto router appears here only when it sits in the call path (a direct pool route where the Vurto fee is charged); on a direct pool route with no fee this is the DEX router itself. If the fee flips between quote and build the build refuses with `approval_target_changed` instead of approving another contract.
    - `route` RouteLeg[]
      - `exchange` string
      - `pool` string
      - `tokenIn` string
      - `tokenOut` string
      - `shareBps` integer: Share of the input routed through this leg, in basis points, for split routes.
    - `badges` string[]: Highlight codes, e.g. best_price, lowest_gas.
    - `warnings` QuoteNotice[]
      - `code` string: Stable code; translate client-side, don't display raw.
      - `vars` object
    - `executionKind` 'transaction' | 'order' | 'onchain_order': Only present on quotes returned from POST /quote. 'transaction': send `steps[].tx`. 'order': sign EIP-712 off-chain, no transaction. 'onchain_order': CoW ETH-flow, a real transaction that only registers the order.
    - `estimatedGas` integer: Gas in UNITS for this route, from our own model, present only when the USD figure also came from it. An integrator ranks in units against the gasPrice it read itself; without this it treats the route as if it cost no gas, which is an advantage by omission.
  - `simulation` object
    - `status` 'passed' | 'approval_required' | 'incomplete'
    - `gasEstimate` string
    - `reason` string
    - `outputCheck` 'passed' | 'skipped'
    - `outputDelta` string
  - `steps` PlanStep[]
    - `index` integer
    - `type` 'approve' | 'transaction' | 'signature'
    - `humanSummary` string
    - `token` string
    - `spender` string
    - `amount` string
    - `resetFirst` boolean: True when this approve step must zero the allowance before the real approve step runs (some tokens, e.g. USDT, reject a non-zero-to-non-zero approve).
    - `tx` object
      - `to` string
      - `data` string
      - `value` string
    - `gasEstimate` string
    - `scheme` 'eip712'
    - `typedData` object
      - `domain` object
      - `types` object
      - `primaryType` string
      - `message` object
    - `submit` object: Where and how to POST the signature produced from `typedData`.
      - `url` string, uri
      - `method` 'POST'
      - `bodyTemplate` object: Known fields already filled in (chainId as a number, quoteId as a string); fill in `signature` and POST as-is.
  - `slippageBps` integer: Only present on refresh responses: how many bps the output dropped versus the original build. 0 when the price didn't get worse.
  - `refresh` object
    - `url` string, uri
    - `method` 'POST'
    - `body` object
      - `wallet` string, required: The same signing wallet the plan was built for. The handler requires it: a refresh without it is rejected, and omitting it because the spec did not list it was the exact gap this field closes.
      - `slippageBps` integer, nullable, required
  - `routeSwitch` RouteSwitch: Present only when the route that was built is NOT the route that was chosen. The build falls back to the next credible route when the chosen provider refuses with a request limit, because that refusal is about who is calling, not about the pair or the wallet, and no amount of waiting fixes it. Any other failure still aborts. Switching the route is acceptable, switching it silently is not: a client that shows the route MUST surface this before asking for a signature.
    - `from` string, required: Provider the caller chose.
    - `to` string, required: Provider that actually built the transaction.
    - `reason` string, required: Error code that knocked the chosen route out, today always provider_rate_limited.

## Other responses

- `400`: invalid_slippage_bps | invalid_chain_id | token_required | same_token | amount_required | invalid_amount
- `401`: missing or invalid credential
- `403`: scope_required — credential lacks the 'build' scope
- `409`: quote_expired | route_changed | insufficient_balance
- `429`: budget_exhausted
- `502`: no_routes

---

[API](https://skmtc.dev/vurto/apis/vurto-swap-public-api.md) · [All operations](https://skmtc.dev/vurto/apis/vurto-swap-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/vurto/apis/vurto-swap-public-api/revisions/423be3a193b5?raw)
