> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ratiofx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Full Example

> Runnable Node.js script: quote, execute on-chain and verify a swap end to end

This script runs the complete partner flow against the Sandbox: it signs every API request with HMAC-SHA256, requests an indicative and a firm quote, approves the input token, submits `commitFirmQuote` from your wallet, and confirms settlement with `GET /v1/executions`. It also defines the `signRequest` helper used in the other code samples.

<Warning>
  The script sends real on-chain transactions from your wallet. Run it against the Sandbox (Kaia Kairos testnet) with test tokens.
</Warning>

## What it does

<Steps>
  <Step title="Discover corridors">
    `GET /v1/corridors` — finds the corridor for `SWAP_PAIR` and resolves `corridor_id`, `from_token` and `to_token`.
  </Step>

  <Step title="Check rates">
    `GET /v1/rates` — prints the current mid-market rate for every corridor.
  </Step>

  <Step title="Indicative quote">
    `POST /v1/quote` — prints the output amount and fee breakdown.
  </Step>

  <Step title="Firm quote">
    `POST /v1/firm-quote` — returns the signed quote and `execution` calldata.
  </Step>

  <Step title="Approve">
    Checks the input token's allowance for the FxEngine contract and approves it if needed.
  </Step>

  <Step title="Execute">
    Sends the transaction to `execution.contract` with `execution.calldata`.
  </Step>

  <Step title="Verify">
    Polls `GET /v1/executions?quote=…` until the swap is indexed as `SETTLED`.
  </Step>
</Steps>

<Note>
  Approval is one-time per token. The script approves after receiving the firm quote to stay self-contained; in production, approve the FxEngine contract ahead of time so a firm quote never waits on an approval (firm quotes expire after 60 seconds).
</Note>

## Requirements

* Node.js 18 or later and ethers v6: `npm install ethers@6`
* Sandbox API credentials (API key and secret)
* A wallet registered to your credential with swap permission, on a corridor where `accessible` is `true`
* Enough of the input token in that wallet, and KAIA for gas

## Configuration

Set these environment variables:

| Variable                             | Required | Description                                                                                                                                                                |
| ------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RATIO_API_KEY` / `RATIO_API_SECRET` | Yes      | Your API credentials                                                                                                                                                       |
| `PRIVATE_KEY`                        | Yes      | Private key of your registered wallet                                                                                                                                      |
| `RATIO_API_BASE`                     | No       | Defaults to Sandbox (`https://sbx.ratiofx.com/api/router`). For Production use `https://app.ratiofx.com/api/router`                                                        |
| `RPC_URL`                            | No       | Kaia RPC for the same chain as the API. Defaults to Kairos (`https://public-en-kairos.node.kaia.io`); Production uses Kaia mainnet (e.g. `https://public-en.node.kaia.io`) |
| `SWAP_PAIR`                          | No       | Pair as shown in `GET /v1/corridors`. Default `USDT-JPYC`                                                                                                                  |
| `FROM_TOKEN` / `TO_TOKEN`            | No       | Token addresses. If either is empty, both are resolved as quote token → base token (e.g. JPYC → USDT)                                                                      |
| `SWAP_AMOUNT`                        | No       | Input amount for the quotes. Default `2000`                                                                                                                                |

Save the script as `ratio-swap.mjs` and run:

```bash theme={null}
RATIO_API_KEY=rk_... RATIO_API_SECRET=... PRIVATE_KEY=0x... node ratio-swap.mjs
```

## Script

```javascript ratio-swap.mjs theme={null}
import crypto from "crypto";

let ethers;
async function loadEthers() {
  if (!ethers) {
    try {
      ethers = (await import("ethers")).ethers;
    } catch {
      throw new Error("ethers is required. Run: npm install ethers");
    }
  }
  return ethers;
}

async function getSignerAddress() {
  const { computeAddress } = await loadEthers();
  return computeAddress(CONFIG.PRIVATE_KEY);
}

// ─── Configuration ───────────────────────────────────────────────
const CONFIG = {
  // Ratio API base URL (Sandbox by default). The "/api/router" prefix is not part
  // of the HMAC string-to-sign — requests are signed over "/v1/..." paths.
  API_BASE: process.env.RATIO_API_BASE || "https://sbx.ratiofx.com/api/router",
  API_KEY: process.env.RATIO_API_KEY || "",
  API_SECRET: process.env.RATIO_API_SECRET || "",

  // Blockchain
  RPC_URL: process.env.RPC_URL || "https://public-en-kairos.node.kaia.io",
  PRIVATE_KEY: process.env.PRIVATE_KEY || "",

  // Swap parameters (override via env or change here)
  // PAIR is used to find the corridor; FROM_TOKEN/TO_TOKEN determine swap direction.
  PAIR: process.env.SWAP_PAIR || "USDT-JPYC",
  // Set FROM_TOKEN and TO_TOKEN to the ERC-20 contract addresses.
  // These are resolved automatically from the corridor if left empty.
  FROM_TOKEN: process.env.FROM_TOKEN || "",
  TO_TOKEN: process.env.TO_TOKEN || "",
  AMOUNT: process.env.SWAP_AMOUNT || "2000",
};

// ─── HMAC Authentication ─────────────────────────────────────────
function signRequest(method, path, body = null) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyStr = body ? JSON.stringify(body) : "";

  // POST requests require x-ratio-request-id for replay protection.
  // It is included in the HMAC payload between timestamp and method.
  const isMutating = method !== "GET";
  const requestId = isMutating ? `req-${timestamp}-${crypto.randomUUID()}` : null;

  const payload = requestId
    ? timestamp + requestId + method + path + bodyStr
    : timestamp + method + path + bodyStr;
  const signature = crypto
    .createHmac("sha256", CONFIG.API_SECRET)
    .update(payload)
    .digest("hex");

  const headers = {
    "x-ratio-api-key": CONFIG.API_KEY,
    "x-ratio-timestamp": timestamp,
    "x-ratio-signature": signature,
    "Content-Type": "application/json",
  };
  if (requestId) headers["x-ratio-request-id"] = requestId;

  return { headers, bodyStr };
}

async function ratioRequest(method, path, body = null) {
  const { headers, bodyStr } = signRequest(method, path, body);
  const url = CONFIG.API_BASE + path;
  const opts = { method, headers };
  if (method === "POST") opts.body = bodyStr;

  const res = await fetch(url, opts);
  const text = await res.text();

  let json;
  try {
    json = JSON.parse(text);
  } catch {
    throw new Error(`${method} ${path} → ${res.status} (non-JSON): ${text.slice(0, 300)}`);
  }
  if (!json.success) {
    throw new Error(`${method} ${path} → ${res.status}: ${json.error?.code} — ${json.error?.message}`);
  }
  return json;
}

async function ratioGet(path) { return ratioRequest("GET", path); }
async function ratioPost(path, body) { return ratioRequest("POST", path, body); }

// ─── Step 1: Check available corridors & resolve swap tokens ─────
async function checkCorridors() {
  console.log("\n=== Step 1: Checking available corridors ===");
  const { data } = await ratioGet("/v1/corridors");

  let target = null;
  for (const c of data.corridors) {
    console.log(`  ${c.pair}  state=${c.state ?? "N/A"}  status=${c.state_status}  directions=${JSON.stringify(c.allowed_directions)}`);
    console.log(`    limits: ${c.limits.min_amount_usd}–${c.limits.max_amount_usd} USD`);
    console.log(`    spread: base=${c.spread_config.base_spread_bps}bps  liquidity=${c.spread_config.liquidity_bps}bps`);
    if (c.pair === CONFIG.PAIR) target = c;
  }

  if (!target) throw new Error(`Corridor ${CONFIG.PAIR} not found`);

  if (!CONFIG.FROM_TOKEN || !CONFIG.TO_TOKEN) {
    CONFIG.FROM_TOKEN = target.tokens.quote.asset;
    CONFIG.TO_TOKEN = target.tokens.base.asset;
    console.log(`\n  Auto-resolved tokens for ${CONFIG.PAIR}:`);
    console.log(`    from_token (${target.tokens.quote.symbol}): ${CONFIG.FROM_TOKEN}`);
    console.log(`    to_token  (${target.tokens.base.symbol}): ${CONFIG.TO_TOKEN}`);
  }
  CONFIG.CORRIDOR_ID = target.corridor_id;
  console.log(`    corridor_id: ${CONFIG.CORRIDOR_ID}`);
  return data.corridors;
}

// ─── Step 2: Check live rates ────────────────────────────────────
async function checkRates() {
  console.log("\n=== Step 2: Checking live rates ===");
  const { data } = await ratioGet("/v1/rates");

  for (const r of data.rates) {
    console.log(`  ${r.pair}  mid=${r.mid}  age=${r.oracle_age_ms}ms  updated=${r.updated_at}`);
  }
  return data.rates;
}

// ─── Step 3: Get indicative quote ────────────────────────────────
async function getIndicativeQuote(signerAddr) {
  console.log("\n=== Step 3: Getting indicative quote ===");
  const body = {
    corridor_id: CONFIG.CORRIDOR_ID,
    from_token: CONFIG.FROM_TOKEN,
    to_token: CONFIG.TO_TOKEN,
    amount: CONFIG.AMOUNT,
    signer: signerAddr,
  };
  const { data } = await ratioPost("/v1/quote", body);

  console.log(`  ${data.amount.in.display} ${data.amount.in.token} → ${data.amount.out.display} ${data.amount.out.token}`);
  console.log(`  Fee tier: ${data.fee_breakdown.tier}`);
  console.log(`  Fixed fee: ${data.fee_breakdown.fixed.charged.display} ${data.fee_breakdown.fixed.charged.token}`);
  console.log(`  Variable fee: ${data.fee_breakdown.variable.charged.display} ${data.fee_breakdown.variable.charged.token} (${data.fee_breakdown.variable.bps}bps)`);
  console.log(`  Spread: ${data.fee_breakdown.spread.total_bps}bps`);
  console.log(`  Risk state: ${data.state_flag}  expires: ${new Date(data.expires_at * 1000).toISOString()}`);
  return data;
}

// ─── Step 4: Get firm quote ──────────────────────────────────────
async function getFirmQuote(signerAddr) {
  console.log("\n=== Step 4: Requesting firm quote ===");
  const clientRef = `TEST-${Date.now()}`;
  const body = {
    corridor_id: CONFIG.CORRIDOR_ID,
    from_token: CONFIG.FROM_TOKEN,
    to_token: CONFIG.TO_TOKEN,
    amount: CONFIG.AMOUNT,
    signer: signerAddr,
    quote_type: "FIRM",
    client_ref: clientRef,
  };
  const { data } = await ratioPost("/v1/firm-quote", body);

  console.log(`  Quote ID: ${data.quote.quote_id}`);
  console.log(`  ${data.amount.in.display} ${data.amount.in.token} → ${data.amount.out.display} ${data.amount.out.token}`);
  console.log(`  Expires: ${new Date(data.expires_at * 1000).toISOString()}`);
  console.log(`  Contract: ${data.execution.contract}`);
  console.log(`  Chain ID: ${data.execution.chain_id}`);
  console.log(`  Client ref: ${clientRef}`);
  return data;
}

// ─── Step 5: Approve token spend ─────────────────────────────────
async function ensureAllowance(firmQuote) {
  console.log("\n=== Step 5: Ensuring ERC-20 allowance ===");
  const { JsonRpcProvider, Wallet, Contract, MaxUint256 } = await loadEthers();
  const provider = new JsonRpcProvider(CONFIG.RPC_URL);
  const wallet = new Wallet(CONFIG.PRIVATE_KEY, provider);

  const fromToken = CONFIG.FROM_TOKEN;
  const spender = firmQuote.execution.contract;
  const needed = BigInt(firmQuote.execution.source_amount_raw || firmQuote.amount.in.raw);

  const erc20 = new Contract(fromToken, [
    "function allowance(address,address) view returns (uint256)",
    "function approve(address,uint256) returns (bool)",
  ], wallet);

  const current = await erc20.allowance(wallet.address, spender);
  console.log(`  Token: ${fromToken}`);
  console.log(`  Spender (FxEngine): ${spender}`);
  console.log(`  Current allowance: ${current}`);
  console.log(`  Needed: ${needed}`);

  if (current >= needed) {
    console.log("  Allowance sufficient — skipping approve.");
    return;
  }

  console.log("  Approving max allowance...");
  const tx = await erc20.approve(spender, MaxUint256);
  console.log(`  Approve tx: ${tx.hash}`);
  await tx.wait();
  console.log("  Approved.");
}

// ─── Step 6: Execute on-chain ────────────────────────────────────
async function executeOnChain(firmQuote) {
  console.log("\n=== Step 6: Executing on-chain ===");
  const { JsonRpcProvider, Wallet } = await loadEthers();
  const provider = new JsonRpcProvider(CONFIG.RPC_URL);
  const wallet = new Wallet(CONFIG.PRIVATE_KEY, provider);
  const { execution } = firmQuote;

  console.log(`  Sending tx to ${execution.contract}...`);
  const tx = await wallet.sendTransaction({
    to: execution.contract,
    data: execution.calldata,
    value: execution.value,
    chainId: execution.chain_id,
  });

  console.log(`  Submitted: ${tx.hash}`);
  console.log("  Waiting for confirmation...");
  const receipt = await tx.wait();
  console.log(`  Confirmed in block ${receipt.blockNumber} (status=${receipt.status})`);
  return tx.hash;
}

// ─── Step 7: Verify execution ────────────────────────────────────
async function verifyExecution(quoteId, txHash) {
  console.log("\n=== Step 7: Verifying execution ===");

  // Wait for indexer to catch up
  for (let attempt = 1; attempt <= 10; attempt++) {
    try {
      const { data } = await ratioGet(`/v1/executions?quote=${quoteId}`);
      console.log(`  Status: ${data.status}`);
      console.log(`  Filled rate: ${data.filled_rate}`);
      console.log(`  Source: ${data.source_amount}  Destination: ${data.destination_amount}`);
      console.log(`  Total fee (USD): ${data.total_fee_usd}`);
      console.log(`  Tx: ${data.tx_hash}`);
      console.log(`  Settled at: ${data.settled_at}`);
      return data;
    } catch (e) {
      if (attempt < 10) {
        console.log(`  Not indexed yet (attempt ${attempt}/10), waiting 3s...`);
        await new Promise((r) => setTimeout(r, 3000));
      } else {
        console.log(`  Could not verify after ${attempt} attempts. Check tx: ${txHash}`);
      }
    }
  }
}

// ─── Main ────────────────────────────────────────────────────────
async function main() {
  console.log("Ratio FX — Partner Integration Test");
  console.log(`  Pair: ${CONFIG.PAIR}  Amount: ${CONFIG.AMOUNT}`);

  // Validate config
  if (!CONFIG.API_KEY || !CONFIG.API_SECRET || !CONFIG.PRIVATE_KEY) {
    console.error("\nMissing required env vars. Set:");
    console.error("  RATIO_API_KEY, RATIO_API_SECRET, PRIVATE_KEY");
    console.error("\nOptional:");
    console.error("  RATIO_API_BASE, RPC_URL, SWAP_PAIR, FROM_TOKEN, TO_TOKEN, SWAP_AMOUNT");
    process.exit(1);
  }

  const signerAddr = await getSignerAddress();
  console.log(`  Signer wallet: ${signerAddr}`);

  await checkCorridors();
  await checkRates();
  const indicative = await getIndicativeQuote(signerAddr);

  // Execution steps (requires funded wallet)
  const firmQuote = await getFirmQuote(signerAddr);
  await ensureAllowance(firmQuote);
  const txHash = await executeOnChain(firmQuote);
  await verifyExecution(firmQuote.quote.quote_id, txHash);

  console.log("\n=== Done ===");
}

main().catch((e) => {
  console.error("\nFatal:", e.message);
  process.exit(1);
});
```

## Troubleshooting

| Symptom                                                         | Cause                                                                                        | Fix                                                                     |
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `401` on every request                                          | Wrong key or secret, clock more than 30 s off, or signing a path that includes `/api/router` | Check credentials and system time; sign `/v1/...` paths only            |
| `403` on quote                                                  | Wallet not registered with swap permission, or corridor not enabled for your credential      | Check `accessible` in `GET /v1/corridors`; contact your account manager |
| `could not decode result data (value="0x")` at the approve step | The RPC is on a different chain than the API environment, or the RPC node is lagging         | Check `RPC_URL`; retry or switch RPC                                    |
| Transaction reverts                                             | Quote expired, insufficient balance or allowance, or the wallet is not `quote.partner`       | Request a new firm quote and check balance and allowance                |
