# Unsigned transaction on any chain: Chord first, a fallback only when it fails

`POST https://api.getchord.xyz/v1/meta/swap`

Source: https://getchord.xyz/docs/api-reference/operations/metaswap/

The same answer as `/meta/quote` with a `taker` required: an UNSIGNED transaction, simulated as the taker. Chord never signs and never holds funds.

Send the pair and amount, or the `quoteId` of a `/meta/quote` answer from the last 2 minutes. The policy runs again at the current state, so `provider` can differ from the quote's: compare before you sign.

**Before signing**
- `approval` not null: send `token.approve(spender, amount)` first. `spender` is `tx.to`. With `approval.resetFirst: true` (USDT on Ethereum, a left-over allowance) send `approve(spender, 0)` before it.
- When `provider` is not `chord`, `tx.to` is that aggregator's router. Show the user which provider it is.

**Policy**
1. Chord's own router is always asked first, with its normal budget.
2. A fallback is asked only when Chord fails: the engine timed out, errored or is down; there is no route; Chord refuses the route itself (value guard, taxed token, stale bonding-curve route); the chain's router is not live; or the built transaction fails simulation. A better price elsewhere is not a reason. A user error (unknown token, bad amount) never falls back.
3. Fallbacks are tried in the chain's order (`GET /meta/providers`); the first answer that passes every check wins. Per provider 4 s, whole request 9 s, a circuit breaker per provider and chain.

**Safety, on every fallback answer**
- The transaction target and the approval spender must be on Chord's allowlist for that provider and chain, and deployed there.
- `minAmountOut` comes from your slippage; a provider minimum below it is refused.
- The value guard of `/quote` applies: an output worth 30 % less than the input at Chord's prices is refused. An unpriced token gets a security scan (`safety`); a honeypot is refused.
- With `taker`, the transaction is simulated as that wallet and must deliver at least `minAmountOut`. A fallback transaction that cannot be simulated is never returned.
- Without `taker`, a fallback price is shown only after the provider BUILT the route and it was simulated for a placeholder wallet (`PRICE_CHECKED`); a provider that prices a size but will not build it is skipped. The placeholder's transaction is never returned.
- An input the simulation cannot supply (a balance that is not a plain storage mapping, e.g. rebasing shares) while the taker does not hold it: HTTP 422 `INPUT_NOT_SIMULATABLE` — hold the input and ask again.

## Request body (JSON)

| Field | Required | Type | Description |
| --- | --- | --- | --- |
| `chain` | no | string | Chain id or key. |
| `tokenIn` | no | string | EVM address, hex, case-insensitive. |
| `tokenOut` | no | string | EVM address, hex, case-insensitive. |
| `amount` | no | string | Amount in the token's smallest unit, as a decimal string. Never a float. |
| `slippageBps` | no | integer |  |
| `taker` | yes | string | The wallet that signs and receives. |
| `quoteId` | no | string | Instead of chain / tokens / amount: the id of a `/meta/quote` answer (valid 2 minutes). |

```json
{
  "chain": 8453,
  "tokenIn": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  "tokenOut": "0x4200000000000000000000000000000000000006",
  "amount": "10000000",
  "slippageBps": 50,
  "taker": "0x00000000000000000000000000000000000c0ffe"
}
```

## Response 200

Sign `tx` with any wallet library and submit it to the chain in `tx.chainId`.

Example (captured from the live API):

```json
{
  "chain": 8453,
  "tokenIn": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  "tokenOut": "0x4200000000000000000000000000000000000006",
  "amountIn": "10000000",
  "taker": "0x00000000000000000000000000000000000c0ffe",
  "slippageBps": 50,
  "provider": "chord",
  "fallbackReason": null,
  "attempts": [
    {
      "provider": "chord",
      "status": "ok",
      "ms": 222,
      "error": null
    }
  ],
  "amountOut": "3727897844407334",
  "minAmountOut": "3709258355185298",
  "priceImpactPct": "0.0000",
  "valueLossPct": -0.02,
  "feeBps": 0,
  "feeRecipient": "0x6c66229675fd9ceda9a9b2ea2bd983b3d8255834",
  "route": {
    "venues": [
      "aerodrome_slipstream_3"
    ]
  },
  "tx": {
    "to": "0x1b1fee89a381e595fe6536b99ba91f3e16e9601a",
    "data": "0xdc72a437…",
    "value": "0",
    "gas": "1000000",
    "chainId": 8453
  },
  "approval": {
    "token": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
    "spender": "0x1b1fee89a381e595fe6536b99ba91f3e16e9601a",
    "amount": "10000000"
  },
  "simulation": {
    "ok": true,
    "amountOut": "3727897844407334",
    "gasUsed": 456590
  },
  "safety": null,
  "warnings": [
    "INSUFFICIENT_BALANCE: the taker holds less than the input amount; the transaction was simulated with the input supplied."
  ],
  "quoteId": "8f722080-92a4-493d-aa16-a06f468a581e",
  "expiresAt": 1791055797
}
```

- **400**: Invalid parameters — `error` names the check that failed (`INVALID_PARAMS`, `BAD_ADDRESS`, `BAD_LIMIT`, `BAD_HOURS`, …).
- **404**: No provider has a route for this pair and size (`NO_ROUTE`); the body carries `fallbackReason` and `attempts`.
- **410**: Unknown or expired `quoteId` (`QUOTE_EXPIRED`). Send the full params.
- **422**: The request is valid but cannot be served as asked (`INPUT_NOT_SIMULATABLE` on `/meta/*`: a route exists, but the input token cannot be supplied in a simulation and the taker does not hold it — hold the input and ask again).
- **429**: Over the per-minute limit (`RATE_LIMIT_EXCEEDED`). Read `X-RateLimit-Reset` and back off.
- **503**: The lane behind this endpoint is not ready or not enabled here (`NOT_READY`, `CHAIN_NOT_ENABLED`, `NOT_ENABLED`, `*_UNAVAILABLE`). Retry later; do not treat it as an empty answer.
