Skip to content

Swaps ​

v1 is closed to new integrations

Do not build on v1. The current API is v2 RFQ. v1 stays served for integrations already running on it, takes no new features, and gets no new endpoints.

An instant swap sells collateral into the resting filler book in one on-chain transaction. The API builds the transactions; you sign and broadcast them. Nothing custodial happens on our side.

Build a swap ​

POST /v1/swaps

Requires trades:write.

json
{
  "chainId": 8453,
  "sellToken": "0xCNGN",
  "buyToken": "0xUSDT",
  "sellAmount": "1000000000",
  "minRate": "600000000000000000000000000",
  "taker": "0xYourWallet",
  "preferredLiquidityWallets": ["0xYourStitchWallet"]
}

taker is the wallet that will sign and broadcast. It's msg.sender, so it receives the proceeds directly.

preferredLiquidityWallets and restrictedLiquidityWallets are optional alternates. Don't send both. Preferred wallets are matched before the rest of the funded book, with remaining volume falling back to normal liquidity. Restricted wallets refuse that fallback and match only the listed operators. minRate still protects the execution price either way. Use the same field on /quote when previewing the swap.

If there's liquidity, you get back a swap record and two unsigned transactions to run in order:

json
{
  "data": {
    "id": "swp_…",
    "status": "QUOTED",
    "fillable": true,
    "fillableAmount": "1000000000",
    "proceeds": "612000000",
    "requiredAllowance": "1000300000",
    "transactions": {
      "approval": {
        "to": "0xCNGN",
        "data": "0x095ea7b3…",
        "value": "0",
        "chainId": 8453
      },
      "swap": {
        "to": "0xReactor",
        "data": "0x…",
        "value": "0",
        "chainId": 8453
      }
    }
  }
}
  1. Approval. Approve requiredAllowance of the collateral to the reactor. requiredAllowance is the fillable amount plus the reactor's native fee. You can skip this if the reactor already has enough allowance from a prior swap.
  2. Swap. Send transactions.swap. This is the reactor's executeBatch call over the matched resting orders. value is always "0": the fee is taken in the collateral token, not native.

Sign both with your own wallet and broadcast them. The quote is only good while the matched orders rest. Treat the returned transactions as short-lived and rebuild if you wait.

Sign and broadcast (TypeScript) ​

The whole flow with viem. Build, approve, swap, report the hash. It's server-to-server, so the signer is a server-side key, never shipped to a browser. The API hands you { to, data, value }; you sign and send them, we never touch your key.

ts
import { createWalletClient, createPublicClient, http, type Hex } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { base } from 'viem/chains'

const account = privateKeyToAccount(process.env.TAKER_PRIVATE_KEY as Hex)
const wallet = createWalletClient({ account, chain: base, transport: http() })
const pub = createPublicClient({ chain: base, transport: http() })

const API = 'https://api.textilecredit.com/v1'
const headers = {
  Authorization: `Bearer ${process.env.TEXTILE_API_KEY}`,
  'Content-Type': 'application/json',
}

// 1. Build the swap. The API returns two unsigned transactions.
const built = await fetch(`${API}/swaps`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    chainId: 8453,
    sellToken: '0xCNGN',
    buyToken: '0xUSDT',
    sellAmount: '1000000000',
    minRate: '600000000000000000000000000',
    taker: account.address,
  }),
}).then((r) => r.json())

// No resting liquidity at your price. Retry later, don't treat it as an error.
if (!built.data.fillable) throw new Error(built.data.reason)

const { approval, swap } = built.data.transactions

// 2. Approve the collateral to the reactor, then wait for it to land. Skip this
//    block if the reactor already has enough allowance from a prior swap.
const approvalHash = await wallet.sendTransaction({
  to: approval.to as Hex,
  data: approval.data as Hex,
  value: BigInt(approval.value),
})
await pub.waitForTransactionReceipt({ hash: approvalHash })

// 3. Send the swap. The reactor's executeBatch over the matched orders.
const txHash = await wallet.sendTransaction({
  to: swap.to as Hex,
  data: swap.data as Hex,
  value: BigInt(swap.value),
})
await pub.waitForTransactionReceipt({ hash: txHash })

// 4. Report the hash so we reconcile the fill and fire your webhook.
await fetch(`${API}/swaps/${built.data.id}/submit`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ txHash }),
})

The approval must confirm before the swap. The reactor pulls the collateral inside executeBatch, so broadcasting both into the same block risks the swap landing first and reverting. Wait for the approval receipt, as above.

No liquidity ​

If nothing fills at your price, you get 200 with no transactions. Retry later rather than treating it as an error:

json
{ "data": { "fillable": false, "reason": "no_liquidity", "liveOrders": 0 } }

Fill-or-kill ​

The book composes whole resting orders, so fillableAmount can be less than your sellAmount: the remainder stays in your wallet. If you'd rather not execute at all than execute part of it, set requireFullFill: true. When the full amount can't be composed you get 200 with no transactions:

json
{
  "data": {
    "fillable": false,
    "reason": "partial_fill",
    "fillableAmount": "8000",
    "remainingAmount": "997",
    "nextRequiredAmount": "12006"
  }
}

nextRequiredAmount is the fee-inclusive sell amount that would clear one more whole order. For guaranteed exact amounts, use v2 RFQ with a buyAmount.

Attribution tag ​

Attribution is opt-in. Set includeAttribution: true to make transactions.swap.data end with an ERC-8021 attribution tag: a few bytes after the ABI-encoded call that the contract ignores and indexers read. It carries textile_api and a code for your account, so swaps you route through the API are counted as yours. The approval is plain approve calldata, no tag.

Broadcast the data exactly as returned. If you have your own builder code (a Base Builder Code, say), pass it in builderCodes and we put it in the same tag:

json
{ "includeAttribution": true, "builderCodes": ["bc_yourcode"] }

builderCodes alone does not enable attribution. Up to 3 codes, each 1-32 characters of a-z, 0-9 and _. Don't append a second suffix yourself: indexers read only the last tag, so one of the two would be lost. Codes starting with textile_ or tp_ are ours and get a 400.

When opted in, data.attributionSuffix returns the same hex suffix separately. For a direct EOA transaction, it is already in transactions.swap.data; do not append it again. Aggregator keys receive null and untagged swap calldata.

Safe integrations must append this suffix to the fully encoded execTransaction calldata at the relayer/broadcast layer, after the signatures argument. Adding it only to the inner swap leaves it invisible at the outer transaction's tail. For ERC-4337, append it to the final UserOperation.callData after encoding the account's execute/batch call and before gas estimation, hashing, and signing. This requires an indexer that reads UserOperations; tagging an inner call does not tag the bundler's handleOps transaction. Use your wallet SDK's supported suffix hook and confirm that the account accepts trailing bytes. A hosted wallet that doesn't expose this hook can still execute the swap, but outer-payload attribution is not guaranteed.

Omitting includeAttribution or setting it to false preserves the existing transaction bytes and response fields: attributionSuffix is absent. Existing integrators need no changes. Before opting in, check any custody policies that match exact swap calldata. Approval calldata is unchanged in both modes. After a delayed approval, request fresh executable orders if the original orders expired.

Retries with the same Idempotency-Key replay the original response. To change includeAttribution or builderCodes, use a new key; reusing it with a changed body returns 409.

Idempotency ​

Send an Idempotency-Key header on POST /v1/swaps. A retry with the same key replays the original response and never builds a second swap. Recommended for any client that retries on network errors.

Report the broadcast ​

POST /v1/swaps/{id}/submit
{ "txHash": "0x…" }

After you broadcast the swap, tell us the hash. We move the swap to SUBMITTED and reconcile it to FILLED once it settles on-chain (which is also when the webhook fires). Requires trades:write. Idempotent on the hash.

Track a swap ​

GET /v1/swaps/{id}
json
{
  "data": {
    "id": "swp_…",
    "status": "FILLED",
    "chainId": 8453,
    "txHash": "0x…",
    "fill": {
      "soldAmount": "1000000000",
      "proceeds": "612000000",
      "effectiveRateRay": "612000000000000000000000000",
      "ordersFilled": 2,
      "filledAt": "2026-07-07T12:00:05.000Z"
    }
  }
}

Status flows QUOTED → SUBMITTED → FILLED. The fill block appears once we've reconciled the on-chain settlement. Requires trades:read.

List swaps ​

GET /v1/swaps?status=FILLED&limit=50&cursor=<id>

Your swap history, newest first, cursor-paginated. nextCursor is null on the last page. Requires trades:read.