# Deluthium DarkPool :: Agent Integration (v2.0.1)

Deluthium DarkPool is an RFQ dark-pool DEX with an agent-native API. One HTTP call returns signed calldata you submit on-chain — no CLOB, no AMM slippage surface, MEV-resistant by construction. Requests and responses are plain JSON; the bytes you put on-chain are base64-decoded from the response. No SDK, no vendor runtime, no websocket required for Mode 1.

Every fill is also a training label for a Financial World Model. Agent integrators are first-class: requests and outcomes feed a live forecast of liquidity, spreads, and execution quality. Reading this doc and shipping an integration is the full contract — there is no gated onboarding call, no staged rollout. If your agent can make an HTTP request and sign an EVM transaction, it can settle on DarkPool today.

---

## 0 · TL;DR

```
1. POST /v1/agg-swap/firmQuote  →  { calldata, router_address }
2. base64-decode calldata       →  raw bytes
3. sign + broadcast tx { to: router_address, data: <raw bytes>, value: 0|amount_in }
4. done. swap_id is your receipt.
```

Decision flowchart — which mode do you want?

```
Do you pick your own Market Maker?
 ├─ No  → Mode 1 (Aggregation API, §2). Single HTTP call. Use this.
 └─ Yes → Mode 2 (Aggregator, §3). WebSocket depth + per-MM firmQuote + protobuf decode + Settlement.settle().
```

Mode 1 — "9 of 10 agents want this." The platform handles MM selection, multi-MM split, signature orchestration, and hands you a single `calldata` blob. One HTTP call in, one transaction out.

Mode 2 — "you're an aggregator routing your own MM selection." You stream per-MM depth over WebSocket, pick the MM yourself, request a single-MM firm quote, decode the protobuf payload, and call `Settlement.settle()` directly with the full `RFQQuote` struct.

---

## 1 · Setup

Declare these environment variables once; every code block below reuses them.

```bash
export DELUTHIUM_API=https://rfq-api.deluthium.ai
export DELUTHIUM_API_KEY=sk_live_REPLACE_ME        # per-partner business key
export BSC_RPC=https://bsc-dataseed.bnbchain.org
export BASE_RPC=https://mainnet.base.org
```

Supported chains:

| Chain ID | Name         |
| :------- | :----------- |
| 56       | BSC Mainnet  |
| 8453     | Base Mainnet |

Contract addresses (production mainnet — verify against the Taker doc before trusting):

| Chain    | Settlement                                   | Router                                       | Wrapped native                               |
| :------- | :------------------------------------------- | :------------------------------------------- | :------------------------------------------- |
| BSC (56) | `0xc4D38F584e998055Aef2c09bBe816465e6369993` | `0xD9d12f5192633C9DD12619B6cac90Df2f60a0F7A` | `0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c` |
| Base (8453) | `0xffA1F5d6528a8B2C06b491c07954979f6B0A95aF` | `0x5A55FD7b136b95Be784b64c06dEAC519B6a56e65` | `0x4200000000000000000000000000000000000006` |

Authentication — every HTTP request carries the header:

```
Authorization: Bearer $DELUTHIUM_API_KEY
```

How to obtain a key — **mailto:contact@deluthium.ai** — per-partner, not self-serve. Expect 1-business-day turnaround. The key issued will be a `business`-type key; only `business` keys pass auth on `/v1/agg-swap/*`, `/v1/quote/*`, `/v1/listing/*`, and the WebSocket endpoint. Reusing any other key type (retail, admin, internal) returns `code: 10070`.

Response envelope — every successful HTTP response is wrapped identically:

```json
{
  "code": 10000,
  "message": "success",
  "data": { ... }
}
```

`code === 10000` means success. Any other code is a business failure; parse `code` + `message` and branch on it. See §8.1 for the full code table.

---

## 2 · Mode 1 — Aggregation API (recommended default)

Three endpoints, in order of use: `/v1/listing/pairs` (discovery), `/v1/agg-swap/indicativeQuote` (cheap read), `/v1/agg-swap/firmQuote` (binding quote + calldata).

### 2.1 `GET /v1/listing/pairs` — discover supported pairs

Call this first from any new agent. It tells you which `(chain_id, base_token, quote_token)` triples the platform currently routes. Pairs with `is_enabled: false` or `status: "DISABLED"` will not return a quote.

**curl**

```bash
curl -sS -X GET "$DELUTHIUM_API/v1/listing/pairs" \
  -H "Authorization: Bearer $DELUTHIUM_API_KEY"
# {
#   "code": 10000,
#   "message": "success",
#   "data": [
#     { "id": 1, "chain_id": 56, "base_token": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
#       "quote_token": "0x55d398326f99059ff775485246999027b3197955",
#       "pair_symbol": "WBNB/USDT", "fee_rate": 5, "is_enabled": true, "status": "ACTIVE",
#       "min_trade_amount": 0.01 }
#   ]
# }
```

**TypeScript**

```ts
const res = await fetch(`${process.env.DELUTHIUM_API}/v1/listing/pairs`, {
  headers: { Authorization: `Bearer ${process.env.DELUTHIUM_API_KEY}` },
});
const body = await res.json();
if (body.code !== 10000) throw new Error(`listing/pairs failed: ${body.code} ${body.message}`);
const pairs = body.data; // Array<{ id, chain_id, base_token, quote_token, pair_symbol, fee_rate, is_enabled, status, min_trade_amount }>
```

**Python**

```python
import os, requests

res = requests.get(
    f"{os.environ['DELUTHIUM_API']}/v1/listing/pairs",
    headers={"Authorization": f"Bearer {os.environ['DELUTHIUM_API_KEY']}"},
    timeout=10,
)
body = res.json()
assert body["code"] == 10000, body
pairs = body["data"]
```

### 2.2 `POST /v1/agg-swap/indicativeQuote` — non-binding quote

Cheap, fast, does not trigger an MM to commit inventory. Use this for UI previews and pre-trade checks. Do not submit the output on-chain — it has no `calldata` and no binding guarantee.

**Request schema**

| Field           | Type           | Required | Notes                                     |
| :-------------- | :------------- | :------- | :---------------------------------------- |
| `src_chain_id`  | uint64         | yes      | Source chain (56 or 8453)                 |
| `dst_chain_id`  | uint64         | yes      | Currently must equal `src_chain_id`       |
| `token_in`      | string (0x…)   | yes      | Input token address                       |
| `token_out`     | string (0x…)   | yes      | Output token address                      |
| `amount_in`     | string (wei)   | yes      | Decimal integer as a string               |

**curl**

```bash
curl -sS -X POST "$DELUTHIUM_API/v1/agg-swap/indicativeQuote" \
  -H "Authorization: Bearer $DELUTHIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "src_chain_id": 56,
    "dst_chain_id": 56,
    "token_in":  "0x55d398326f99059ff775485246999027b3197955",
    "token_out": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
    "amount_in": "1000000000000000000"
  }'
# data: { src_chain_id, token_in, token_out, amount_in, amount_out, fee_rate, fee_amount }
```

**TypeScript**

```ts
const res = await fetch(`${process.env.DELUTHIUM_API}/v1/agg-swap/indicativeQuote`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DELUTHIUM_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    src_chain_id: 56,
    dst_chain_id: 56,
    token_in: "0x55d398326f99059ff775485246999027b3197955",
    token_out: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
    amount_in: "1000000000000000000",
  }),
});
const body = await res.json();
if (body.code !== 10000) throw new Error(`indicativeQuote failed: ${body.code} ${body.message}`);
const { amount_out, fee_rate, fee_amount } = body.data;
```

**Python**

```python
import os, requests

res = requests.post(
    f"{os.environ['DELUTHIUM_API']}/v1/agg-swap/indicativeQuote",
    headers={
        "Authorization": f"Bearer {os.environ['DELUTHIUM_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "src_chain_id": 56,
        "dst_chain_id": 56,
        "token_in":  "0x55d398326f99059ff775485246999027b3197955",
        "token_out": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
        "amount_in": "1000000000000000000",
    },
    timeout=10,
)
body = res.json()
assert body["code"] == 10000, body
amount_out = body["data"]["amount_out"]
```

### 2.3 `POST /v1/agg-swap/firmQuote` — binding quote with calldata

Returns base64-encoded `Router.swap(...)` calldata plus the `router_address` to send it to. The platform has already selected MMs, collected their signatures, and serialized the tx — you are the broadcaster.

**Request schema**

| Field            | Type         | Required | Notes                                                          |
| :--------------- | :----------- | :------- | :------------------------------------------------------------- |
| `src_chain_id`   | uint64       | yes      | Source chain                                                   |
| `dst_chain_id`   | uint64       | yes      | Currently equals `src_chain_id`                                |
| `from_address`   | string (0x…) | yes      | Payer (the EOA that sends the tx)                              |
| `to_address`     | string (0x…) | yes      | Recipient of `token_out`                                       |
| `token_in`       | string (0x…) | yes      | Input token                                                    |
| `token_out`      | string (0x…) | yes      | Output token                                                   |
| `amount_in`      | string (wei) | yes      | Decimal integer string                                         |
| `amount_out_min` | string (wei) | yes      | Slippage floor. See §7 on time-decay before setting this to 0. |
| `deadline`       | uint64       | yes      | Unix **seconds** (not ms). Broadcast before this.              |

**Response `data`**

| Field             | Type         | Notes                                                       |
| :---------------- | :----------- | :---------------------------------------------------------- |
| `swap_id`         | string       | Your receipt / correlation id                               |
| `src_chain_id`    | uint64       |                                                             |
| `calldata`        | string       | **base64**-encoded `Router.swap()` calldata bytes           |
| `router_address`  | string (0x…) | Target contract — send tx `to` this                         |
| `from_address`    | string (0x…) |                                                             |
| `to_address`      | string (0x…) |                                                             |
| `token_in`        | string       |                                                             |
| `token_out`       | string       |                                                             |
| `amount_in`       | string (wei) |                                                             |
| `amount_out`      | string (wei) | Expected output (before time-decay)                         |
| `amount_out_min`  | string (wei) | Echoed back — what the contract enforces                    |
| `fee_rate`        | uint64       | Basis points, 1 = 0.01%                                     |
| `fee_amount`      | string (wei) |                                                             |
| `deadline`        | uint64       | Unix seconds                                                |

**curl**

```bash
curl -sS -X POST "$DELUTHIUM_API/v1/agg-swap/firmQuote" \
  -H "Authorization: Bearer $DELUTHIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"src_chain_id\": 56,
    \"dst_chain_id\": 56,
    \"from_address\": \"$ME\",
    \"to_address\":   \"$ME\",
    \"token_in\":  \"0x55d398326f99059ff775485246999027b3197955\",
    \"token_out\": \"0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c\",
    \"amount_in\":      \"1000000000000000000\",
    \"amount_out_min\": \"0\",
    \"deadline\": $(($(date +%s) + 60))
  }"
# data: { swap_id, calldata, router_address, amount_out, amount_out_min, deadline, ... }
```

**TypeScript** — request + broadcast via viem:

```ts
import { createWalletClient, http, hexToBytes } from "viem";
import { bsc } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.PK as `0x${string}`);
const wallet = createWalletClient({ account, chain: bsc, transport: http(process.env.BSC_RPC) });

const quoteRes = await fetch(`${process.env.DELUTHIUM_API}/v1/agg-swap/firmQuote`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DELUTHIUM_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    src_chain_id: 56,
    dst_chain_id: 56,
    from_address: account.address,
    to_address:   account.address,
    token_in:  "0x55d398326f99059ff775485246999027b3197955",
    token_out: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
    amount_in:      "1000000000000000000",
    amount_out_min: "0",
    deadline: Math.floor(Date.now() / 1000) + 60,
  }),
});
const body = await quoteRes.json();
if (body.code !== 10000) throw new Error(`firmQuote failed: ${body.code} ${body.message}`);
const { calldata, router_address, swap_id } = body.data;

// base64 -> 0x-hex
const raw = Buffer.from(calldata, "base64");
const dataHex = `0x${raw.toString("hex")}` as `0x${string}`;

const txHash = await wallet.sendTransaction({
  to: router_address as `0x${string}`,
  data: dataHex,
  value: 0n,     // token_in is ERC20; see §2.4 for native
});
console.log({ swap_id, txHash });
```

**Python** — request + broadcast via web3.py:

```python
import base64, os, time
import requests
from web3 import Web3
from eth_account import Account

w3 = Web3(Web3.HTTPProvider(os.environ["BSC_RPC"]))
acct = Account.from_key(os.environ["PK"])

quote = requests.post(
    f"{os.environ['DELUTHIUM_API']}/v1/agg-swap/firmQuote",
    headers={
        "Authorization": f"Bearer {os.environ['DELUTHIUM_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "src_chain_id": 56,
        "dst_chain_id": 56,
        "from_address": acct.address,
        "to_address":   acct.address,
        "token_in":  "0x55d398326f99059ff775485246999027b3197955",
        "token_out": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
        "amount_in":      "1000000000000000000",
        "amount_out_min": "0",
        "deadline": int(time.time()) + 60,
    },
    timeout=10,
).json()
assert quote["code"] == 10000, quote
d = quote["data"]

raw = base64.b64decode(d["calldata"])
tx = {
    "to":    Web3.to_checksum_address(d["router_address"]),
    "data":  "0x" + raw.hex(),
    "value": 0,
    "gas":   500_000,
    "gasPrice": w3.eth.gas_price,
    "nonce": w3.eth.get_transaction_count(acct.address),
    "chainId": 56,
}
signed = acct.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
print(d["swap_id"], tx_hash.hex())
```

**Foundry `cast`** — if you have `calldata` already base64-decoded to hex in `$DATA_HEX`:

```bash
cast send "$ROUTER_ADDRESS" "$DATA_HEX" \
  --rpc-url "$BSC_RPC" \
  --private-key "$PK" \
  --value 0
```

### 2.4 Native-token inputs (BNB / ETH)

When `token_in` is the native coin (BNB on BSC, ETH on Base — represented as `0x0000000000000000000000000000000000000000` or the chain's native-placeholder convention returned by `/v1/listing/tokens`), set `value = amount_in` on the transaction instead of calling `approve()`. When `token_in` is an ERC-20, `approve()` the **Router** (not Settlement, for Mode 1) for at least `amount_in` before broadcasting the swap tx. Infinite approval (`2**256 - 1`) is a common agent pattern; per-tx approval (`amount_in`) is safer and mandatory on chains where the token blocks re-approval without zero-reset.

---

## 3 · Mode 2 — Aggregator (advanced, direct MM selection)

Skip this section unless you're re-selling Deluthium liquidity inside a larger aggregator. Mode 1 covers every agent-swap use case and gives strictly better pricing than naive single-MM picks, because the platform splits across makers for you.

### 3.1 WebSocket depth subscribe

Connection URL — the WebSocket domain will be supplied alongside your API key; it is distinct from the HTTP gateway. Auth via query parameter:

```
wss://<ws_domain>/v1/quote/orderbook?api_key=$DELUTHIUM_API_KEY
```

Or via header `X-API-Key: $DELUTHIUM_API_KEY`. Note: `Authorization: Bearer` is **not** accepted on the WS endpoint — only the two forms above.

Frame format is binary protobuf; every message is a `quote.v1.QEMessage` wrapper (see §5 and `/integrate/proto/quote.v1.proto`). On successful connect you receive a `QE_MESSAGE_TYPE_CONNECTION_ACK`; respond by sending a `QE_MESSAGE_TYPE_SUBSCRIBE` with the pairs you want. The server then streams per-MM `QE_MESSAGE_TYPE_DEPTH_UPDATE` messages, one snapshot per (MM, pair).

**TypeScript** — open the WS, subscribe to BSC WBNB/USDT, log depth:

```ts
import WebSocket from "ws";
import protobuf from "protobufjs";

const root = await protobuf.load("./quote.v1.proto");
const QEMessage = root.lookupType("quote.v1.QEMessage");

const ws = new WebSocket(
  `wss://<ws_domain>/v1/quote/orderbook?api_key=${process.env.DELUTHIUM_API_KEY}`
);

ws.on("open", () => {
  const subMsg = QEMessage.create({
    type: 1, // QE_MESSAGE_TYPE_SUBSCRIBE
    timestamp: Date.now(),
    subscribe: {
      pairs: [{
        chain_id: 56,
        base_token:  "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", // WBNB
        quote_token: "0x55d398326f99059ff775485246999027b3197955", // USDT
      }],
    },
  });
  ws.send(QEMessage.encode(subMsg).finish());
});

ws.on("message", (buf: Buffer) => {
  const msg = QEMessage.decode(buf) as any;
  if (msg.type === 5 /* DEPTH_UPDATE */) {
    const d = msg.depthUpdate;
    console.log(`mm=${d.mmId} bids[0]=${d.bids[0]?.price} asks[0]=${d.asks[0]?.price}`);
  } else if (msg.type === 6 /* HEARTBEAT */) {
    if (msg.heartbeat.ping) {
      ws.send(QEMessage.encode(QEMessage.create({
        type: 6, timestamp: Date.now(), heartbeat: { pong: true },
      })).finish());
    }
  }
});
```

`SubscribeAck` comes back with per-pair `subscribed: bool` — check it before assuming you're receiving updates. Heartbeat every 30s; if you see no messages for 60s, reconnect (§9).

### 3.2 `POST /v1/quote/firmQuote` — single-MM firm quote

Once you've picked an MM from the depth stream, request a binding quote from just that MM.

**Request schema**

| Field              | Type         | Required | Notes                                               |
| :----------------- | :----------- | :------- | :-------------------------------------------------- |
| `chain_id`         | uint64       | yes      |                                                     |
| `mm_id`            | string       | yes      | From the `mm_id` field of a `DEPTH_UPDATE`          |
| `token_in`         | string (0x…) | yes      |                                                     |
| `token_out`        | string (0x…) | yes      |                                                     |
| `amount_in`        | string (wei) | yes      | Decimal integer string                              |
| `deadline`         | int64        | yes      | Unix **seconds**                                    |
| `from`             | string (0x…) | no       | Payer (defaults to key-owner)                       |
| `recipient`        | string (0x…) | no       | Recipient                                           |
| `protocol_version` | string       | no       | Defaults to `"v1"`                                  |

**Response `data`**

| Field                | Type    | Notes                                                             |
| :------------------- | :------ | :---------------------------------------------------------------- |
| `chain_id`           | uint64  |                                                                   |
| `mm_id`              | string  |                                                                   |
| `error_code`         | int     | `0` = success; see §8.3 QuoteErrorCode enum for nonzero values    |
| `error_message`      | string  |                                                                   |
| `protocol_version`   | string  |                                                                   |
| `rfq_quote_data`     | bytes   | **base64**-encoded `RFQQuoteV1` protobuf                          |
| `settlement_address` | string  | Target contract for `Settlement.settle(...)`                      |

```bash
curl -sS -X POST "$DELUTHIUM_API/v1/quote/firmQuote" \
  -H "Authorization: Bearer $DELUTHIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"chain_id\": 56,
    \"mm_id\":    \"$MM_ID\",
    \"token_in\":  \"0x55d398326f99059ff775485246999027b3197955\",
    \"token_out\": \"0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c\",
    \"amount_in\": \"1000000000000000000\",
    \"deadline\":  $(($(date +%s) + 60)),
    \"protocol_version\": \"v1\"
  }"
```

### 3.3 Decode protobuf RFQQuoteV1

Three options — pick whichever fits your stack.

**(a) Fetch the canonical proto file at build time:**

```bash
curl -sL https://agent.deluthium.ai/integrate/proto/quote.v1.proto -o quote.v1.proto
```

**(b) TypeScript — 15-line decoder with `protobufjs/light`:**

```ts
import protobuf from "protobufjs/light";
import quoteProtoJson from "./quote.v1.proto.json"; // converted via pbjs -t json

const root = protobuf.Root.fromJSON(quoteProtoJson);
const RFQQuoteV1 = root.lookupType("quote.v1.RFQQuoteV1");

export function decodeRfqQuote(rfqQuoteDataB64: string) {
  const buf = Buffer.from(rfqQuoteDataB64, "base64");
  const obj = RFQQuoteV1.decode(buf) as any;
  return {
    mmQuote:      obj.mmQuote,       // MMQuoteV1
    fee:          obj.fee,           // FeeV1
    rfqSignature: obj.rfqSignature,  // Uint8Array (65 bytes)
  };
}
```

**(c) Python — equivalent with `google.protobuf`:**

```python
import base64
# After: protoc --python_out=. quote.v1.proto
import quote_v1_pb2 as pb

def decode_rfq_quote(rfq_quote_data_b64: str):
    raw = base64.b64decode(rfq_quote_data_b64)
    msg = pb.RFQQuoteV1()
    msg.ParseFromString(raw)
    return msg  # .mm_quote, .fee, .rfq_signature
```

### 3.4 Call `Settlement.settle(RFQQuote, amountIn, to)` on chain

After decoding, map protobuf fields to the Solidity struct (see §6) and call `Settlement.settle(...)`. Approve `token_in` to the **Settlement** address (not Router — this is Mode 2) for at least `amountIn` before calling.

**Settlement ABI fragment**

```solidity
function settle(
    RFQQuote calldata rfqQuote,
    uint256 amountIn,
    address to
) external returns (uint256 amountOut, uint256 feeAmount);
```

**cast invocation** — once you've ABI-encoded the `RFQQuote` tuple into `$RFQ_TUPLE`:

```bash
cast send "$SETTLEMENT_ADDRESS" \
  "settle((((bytes32,address,address,address,address,address,uint256,uint256,uint256,uint256,(uint256,uint256,uint256,uint256),bytes,bytes),(address,uint256),bytes)),uint256,address)" \
  "$RFQ_TUPLE" "$AMOUNT_IN" "$TO" \
  --rpc-url "$BSC_RPC" \
  --private-key "$PK"
```

> **[PENDING]** `Permit2Data` layout for `Router.swap()` is not yet published. Use the plain ERC-20 `approve` path (approve the Router or Settlement, skip the `permit2Data` field by passing an empty struct) until Theta publishes the Permit2 typehash and signing domain.

---

## 4 · Solidity interfaces

All structs live in the `RFQQuote` family and are consumed by `Settlement.settle(...)` and `Router.swap(...)`.

```solidity
struct RFQQuote {
    MMQuote mmQuote;
    Fee fee;
    bytes rfqSignature;     // platform EIP-712 signature (65 bytes)
}

struct MMQuote {
    bytes32 quoteId;
    address maker;
    address vault;
    address executor;       // Settlement contract address
    address inputToken;
    address outputToken;
    uint256 amountIn;
    uint256 amountOut;
    uint256 deadline;
    uint256 nonce;
    ConfidenceExtractedValue confidenceExtractedValue;
    bytes extraData;
    bytes mmSignature;      // MM EIP-712 signature (65 bytes)
}

struct Fee {
    address feeTo;
    uint256 feeRate;        // basis points
}

struct ConfidenceExtractedValue {
    uint256 confidenceExtractedValueT;
    uint256 confidenceExtractedValueN;
    uint256 confidenceExtractedValueM;
    uint256 confidenceExtractedValueE;
}
```

**Settlement.settle** — single RFQQuote, supports partial fill:

```solidity
function settle(
    RFQQuote calldata rfqQuote,
    uint256 amountIn,        // actual input; must be <= mmQuote.amountIn
    address to               // recipient
) external nonReentrant whenNotPaused
  returns (uint256 amountOut, uint256 feeAmount);
```

**Router.swap** — batch over multiple MMs:

```solidity
function swap(
    address from,
    address to,
    RFQQuote[] calldata rfqQuotes,
    uint256[] calldata amountsIn,    // same length as rfqQuotes
    uint256 amountOutMin,            // aggregate slippage floor
    Permit2Data calldata permit2Data
) external payable;
```

> **[PENDING]** The `rfq_signature` EIP-712 domain + typehash are not yet published. The field is an opaque 65-byte signature that takers **must pass through unmodified**; takers do not verify it. When the domain is published this doc will be updated so agents can sanity-check on the client side.

### 4.1 · On-chain events

Both Settlement and Router emit one event each. The signatures are byte-identical across BSC (56) and Base (8453) — `topic0` depends only on the canonical signature string, so one constant covers both chains.

> **⚠ Address note for indexers.** The Settlement address this doc publishes in §2.1 (`0xc4D3…9993` on BSC, `0xffA1…95aF` on Base) is the **taker-API entry point** — what `/v1/agg-swap/firmQuote` returns and what direct `settle()` calls target. However, the MMHub aggregator routes ~99% of real fills through a second Settlement deployment at **`0x5F86475d57e9B488500d3CdA6F6Cb3938B192077`** (same address on both chains, via CREATE2). That's where `QuoteSettled` actually emits at scale. If you want the full fill stream, subscribe on `0x5F86…2077`; if you care only about your own direct `settle()` calls, subscribe on the §2.1 address. Both share the identical ABI + topic0 below.

**`Settlement.QuoteSettled`** — the authoritative per-fill event. Subscribe to this for a real-time RFQ stream.

```solidity
event QuoteSettled(
    address indexed maker,       // MM that signed the quote
    address indexed payer,       // taker / swap initiator
    address indexed inputToken,
    bytes32 quoteId,
    address to,                  // recipient of outputToken
    address vault,
    address outputToken,
    uint256 grossAmountIn,       // before feeAmount is taken
    uint256 feeAmount,
    address feeTo,
    uint256 amountOut,           // realized output
    uint256 nonce
);
```

`topic0` = `0xf39f2205bfffe8d0f1a7e0060fcb84974534b5f0c54fa4d88e53f2501b26008e`

**`Router.SwapExecuted`** — co-emits with one-or-more `QuoteSettled` on aggregator-routed swaps. `refundAmount > 0` signals a partial fill (router refunded the leftover input).

```solidity
event SwapExecuted(
    address indexed from,
    address indexed inputToken,
    address indexed outputToken,
    address to,
    uint256 totalAmountIn,
    uint256 totalFeeAmount,
    uint256 totalAmountOut,
    uint256 amountOutMin,
    uint256 quoteCount,
    uint256 refundAmount         // > 0 = partial fill
);
```

`topic0` = `0x0f1ca616ed09030cc30d1c572824e0ffeae3eb6dd287b8b288649fe143af23b7`

**Indexing tips:**

- Subscribe to both contracts. `QuoteSettled` carries per-quote granularity (quoteId, maker, nonce); `SwapExecuted` gives the aggregate refund signal. De-dupe by `(txHash, logIndex)` — they are not mutually exclusive.
- Reverts do **not** emit an event — they surface as custom errors (`DeadlinePassed`, `NonceAlreadyUsed`, `AmountOutMinNotReceived`, `AmountOutMinTooHigh`, `ConfidenceExtractedValueMTooHigh`, etc.). For a revert ledger, scan failed txs with `tx.status == 0x0` then decode the error selector from the revert data against the Settlement ABI.
- `QuoteSettled.grossAmountIn` is the `amountIn` *before* the fee; downstream `amountOut` is post-fee. To reconstruct the quoted `amountOut` from chain data you need the MMQuote — decode it from `settle()` calldata.

**RPC caveat — BSC public endpoints refuse log queries on this contract.** The canonical BSC public RPCs (`bsc-dataseed.bnbchain.org`, `bsc-dataseed1.binance.org`, `bsc-dataseed-public.bnbchain.org`) return `"Request exceeds defined limit"` for `eth_getLogs` with an address filter on the Settlement address, even with a 100-block range. Use a premium RPC — Alchemy, Ankr paid, QuickNode, NodeReal, or a self-hosted BSC archive node. Base public RPC (`mainnet.base.org`) accepts full-range log queries but is slow on historical ranges; `base-rpc.publicnode.com` is a usable alternative. The agent.deluthium.ai dashboard honours `DELUTHIUM_BSC_RPC_URL` and `DELUTHIUM_BASE_RPC_URL` **server-side** env vars and proxies browser JSON-RPC through `/api/rpc/[chainId]` so upstream API keys never reach the client bundle. Legacy `NEXT_PUBLIC_*_RPC` names still work as a fallback during migration.

### 4.2 · CEV decay fields live in calldata, not events

The `ConfidenceExtractedValue {T, N, M, E}` tuple signed by the MM is carried inside `MMQuote` and is **not** re-emitted in `QuoteSettled`. To reconstruct CEV residuals for a realized fill, fetch the tx via `eth_getTransactionByHash` and decode the `settle()` input against the ABI above.

---

## 5 · Protobuf reference

The full definition is hosted at **https://agent.deluthium.ai/integrate/proto/quote.v1.proto** — fetch at build time or vendor the file into your repo.

**RFQQuoteV1**

| Field            | Proto type | Notes                                     |
| :--------------- | :--------- | :---------------------------------------- |
| `mm_quote`       | MMQuoteV1  | Field tag 2                               |
| `fee`            | FeeV1      | Field tag 3                               |
| `rfq_signature`  | bytes      | Field tag 4 — 65-byte platform signature  |

**MMQuoteV1**

| Field                         | Proto type                | Notes                                             |
| :---------------------------- | :------------------------ | :------------------------------------------------ |
| `maker`                       | string                    | MM signer address (0x…)                           |
| `vault`                       | string                    | Vault contract                                    |
| `executor`                    | string                    | Settlement contract                               |
| `token_in`                    | string                    |                                                   |
| `token_out`                   | string                    |                                                   |
| `amount_in`                   | string                    | uint256 decimal string                            |
| `amount_out`                  | string                    | uint256 decimal string                            |
| `deadline`                    | string                    | uint256 decimal string, Unix seconds              |
| `nonce`                       | string                    | uint256 decimal string                            |
| `confidence_extracted_value`  | ConfidenceExtractedValue  | See below                                         |
| `extra_data`                  | bytes                     | ABI-encoded vault extras                          |
| `mm_signature`                | bytes                     | 65-byte MM EIP-712 signature                      |
| `quote_id`                    | string                    | bytes32 hex                                       |

**FeeV1**

| Field        | Proto type | Notes                                    |
| :----------- | :--------- | :--------------------------------------- |
| `fee_to`     | string     | 0x…                                      |
| `fee_rate`   | string     | basis points, uint256 decimal string     |
| `fee_amount` | string     | wei, uint256 decimal string              |

**ConfidenceExtractedValue**

| Field                            | Proto type | Notes                                                        |
| :------------------------------- | :--------- | :----------------------------------------------------------- |
| `confidence_extracted_value_t`   | string     | decay window (seconds)                                       |
| `confidence_extracted_value_n`   | string     | decay start (Unix seconds, typically quote creation time)    |
| `confidence_extracted_value_m`   | string     | max decay basis points, 1 = 0.01%, capped at 500 (= 5%)      |
| `confidence_extracted_value_e`   | string     | decay exponent, 1e18 = linear                                |

**TypeScript decoder** (20 lines):

```ts
import protobuf from "protobufjs/light";
import quoteJson from "./quote.v1.proto.json";

const root = protobuf.Root.fromJSON(quoteJson);
const RFQQuoteV1 = root.lookupType("quote.v1.RFQQuoteV1");

export function decodeQuote(b64: string) {
  const msg = RFQQuoteV1.decode(Buffer.from(b64, "base64")) as any;
  return {
    maker:     msg.mmQuote.maker,
    amountIn:  BigInt(msg.mmQuote.amountIn),
    amountOut: BigInt(msg.mmQuote.amountOut),
    deadline:  BigInt(msg.mmQuote.deadline),
    nonce:     BigInt(msg.mmQuote.nonce),
    cev: {
      t: BigInt(msg.mmQuote.confidenceExtractedValue.confidenceExtractedValueT),
      n: BigInt(msg.mmQuote.confidenceExtractedValue.confidenceExtractedValueN),
      m: BigInt(msg.mmQuote.confidenceExtractedValue.confidenceExtractedValueM),
      e: BigInt(msg.mmQuote.confidenceExtractedValue.confidenceExtractedValueE),
    },
    feeRate:   BigInt(msg.fee.feeRate),
    rfqSig:    msg.rfqSignature as Uint8Array,
    mmSig:     msg.mmQuote.mmSignature as Uint8Array,
  };
}
```

**Python decoder** (equivalent, with compiled `quote_v1_pb2`):

```python
import base64
import quote_v1_pb2 as pb

def decode_quote(b64: str):
    msg = pb.RFQQuoteV1()
    msg.ParseFromString(base64.b64decode(b64))
    return {
        "maker":      msg.mm_quote.maker,
        "amount_in":  int(msg.mm_quote.amount_in),
        "amount_out": int(msg.mm_quote.amount_out),
        "deadline":   int(msg.mm_quote.deadline),
        "nonce":      int(msg.mm_quote.nonce),
        "cev": {
            "t": int(msg.mm_quote.confidence_extracted_value.confidence_extracted_value_t),
            "n": int(msg.mm_quote.confidence_extracted_value.confidence_extracted_value_n),
            "m": int(msg.mm_quote.confidence_extracted_value.confidence_extracted_value_m),
            "e": int(msg.mm_quote.confidence_extracted_value.confidence_extracted_value_e),
        },
        "fee_rate": int(msg.fee.fee_rate),
        "rfq_sig":  msg.rfq_signature,
        "mm_sig":   msg.mm_quote.mm_signature,
    }
```

---

## 6 · Protobuf → Solidity field mapping

Direct mapping from `RFQQuoteV1` (wire) to the `RFQQuote` struct (chain):

| Protobuf (RFQQuoteV1)                      | Solidity (RFQQuote)                     | Conversion                       |
| :----------------------------------------- | :-------------------------------------- | :------------------------------- |
| `mm_quote.quote_id`                        | `mmQuote.quoteId`                       | hex string → bytes32             |
| `mm_quote.maker`                           | `mmQuote.maker`                         | address string → address         |
| `mm_quote.vault`                           | `mmQuote.vault`                         | address string → address         |
| `mm_quote.executor`                        | `mmQuote.executor`                      | address string → address         |
| `mm_quote.token_in`                        | `mmQuote.inputToken`                    | address string → address         |
| `mm_quote.token_out`                       | `mmQuote.outputToken`                   | address string → address         |
| `mm_quote.amount_in`                       | `mmQuote.amountIn`                      | decimal string → uint256         |
| `mm_quote.amount_out`                      | `mmQuote.amountOut`                     | decimal string → uint256         |
| `mm_quote.deadline`                        | `mmQuote.deadline`                      | decimal string → uint256         |
| `mm_quote.nonce`                           | `mmQuote.nonce`                         | decimal string → uint256         |
| `mm_quote.confidence_extracted_value.*`    | `mmQuote.confidenceExtractedValue.*`    | decimal string → uint256         |
| `mm_quote.extra_data`                      | `mmQuote.extraData`                     | bytes pass-through               |
| `mm_quote.mm_signature`                    | `mmQuote.mmSignature`                   | bytes pass-through               |
| `fee.fee_to`                               | `fee.feeTo`                             | address string → address         |
| `fee.fee_rate`                             | `fee.feeRate`                           | decimal string → uint256         |
| `rfq_signature`                            | `rfqSignature`                          | bytes pass-through               |

The `fee_amount` proto field has no chain-struct counterpart; the contract recomputes it from `feeRate × amountOut / 10000`.

---

## 7 · Time-decay (CEV) math

The MM's committed `amountOut` decays linearly-to-exponentially between timestamps `N` and `N + T`. This lets MMs hedge against staleness without widening spreads. Compute the decay client-side **before** submitting so your `amount_out_min` is realistic; the chain reverts `InsufficientOutput` if the realized output falls below `amount_out_min`.

**Formula** (from Taker doc §10.3):

```
effectiveAmountOut = amountOut - maxDropAmt × (elapsed / T)^E
  where
    maxDropAmt = amountOut × M / 10000
    elapsed    = min(block.timestamp - N, T)
```

**Parameters**

| Param | Meaning                                                                               |
| :---- | :------------------------------------------------------------------------------------ |
| `T`   | Decay time window (seconds). After `T` seconds past `N`, decay is fully applied.      |
| `N`   | Decay-start timestamp (Unix seconds). Usually the quote creation time.                |
| `M`   | Max decay in basis points (1 = 0.01%). Capped at 500 = 5%.                            |
| `E`   | Decay exponent. `1e18` = linear. Higher = back-loaded decay (slower early, fast late). |

**Special cases**

- `M === 0` or `T === 0` → no decay, return raw `amountOut`.
- `block.timestamp <= N` → decay not started, return raw `amountOut`.
- `elapsed >= T` → full decay applied, return `amountOut - maxDropAmt`.

**TypeScript reference impl** (matches on-chain math; use BigInt for precision):

```ts
export function effectiveAmountOut(
  amountOut: bigint,
  nowSec: bigint,
  cev: { t: bigint; n: bigint; m: bigint; e: bigint }
): bigint {
  if (cev.m === 0n || cev.t === 0n || nowSec <= cev.n) return amountOut;
  const elapsed = nowSec - cev.n > cev.t ? cev.t : nowSec - cev.n;
  const maxDrop = (amountOut * cev.m) / 10_000n;
  // Linear case (e === 1e18): decay = maxDrop × elapsed / T
  const ONE = 10n ** 18n;
  if (cev.e === ONE) return amountOut - (maxDrop * elapsed) / cev.t;
  // Exponent case — approximate with BigInt fixed-point; for E != 1e18 prefer on-chain confirmation.
  const ratio = Number(elapsed) / Number(cev.t);
  const drop  = BigInt(Math.floor(Number(maxDrop) * Math.pow(ratio, Number(cev.e) / 1e18)));
  return amountOut - drop;
}
```

---

## 8 · Error codes + troubleshooting

### 8.1 HTTP business codes

| `code` | Meaning                                    |
| :----- | :----------------------------------------- |
| 10000  | Success                                    |
| 10030  | Insufficient liquidity                     |
| 10070  | Invalid token (API key invalid / wrong type) |
| 10084  | Signature error                            |
| 10090  | Chain not supported                        |
| 10091  | Insufficient balance                       |
| 10095  | Parameter error                            |
| 20001  | Service error                              |
| 20002  | Service unavailable                        |
| 20003  | Internal service error                     |
| 20004  | Not found                                  |

Auth failure example (HTTP 401):

```json
{ "code": 401, "message": "missing api key" }
```

Common causes: missing `Authorization` header, non-`business` key, expired/disabled key.

### 8.2 Chain reverts

| Revert reason        | Explanation                         | What to check                                                              |
| :------------------- | :---------------------------------- | :------------------------------------------------------------------------- |
| `QuoteExpired`       | Quote past its deadline             | Broadcast before `mmQuote.deadline` / `deadline` from firmQuote response    |
| `InvalidSignature`   | Signature check failed              | Did you modify any field of `rfq_quote_data`? Don't. Pass it through raw.  |
| `NonceUsed`          | nonce already consumed              | Don't resubmit the same firmQuote; request a fresh one                     |
| `AmountTooLarge`     | `amountIn > mmQuote.amountIn`       | Clamp to the quote's `amountIn`                                            |
| `InsufficientOutput` | Realized output < `amountOutMin`    | Time-decay hit harder than expected; set realistic `amount_out_min` (§7)   |

### 8.3 WebSocket QEErrorCode

| Value | Meaning                                   |
| :---- | :---------------------------------------- |
| 1     | Invalid message                           |
| 2     | Unauthorized                              |
| 3     | Rate limited                              |
| 4     | Pair not supported                        |
| 5     | Internal error                            |
| 6     | Subscription count limit exceeded         |

QuoteErrorCode (on `/v1/quote/firmQuote` response `error_code` field):

| Value | Meaning                |
| :---- | :--------------------- |
| 0     | Success                |
| 1     | Insufficient liquidity |
| 2     | MM timeout             |
| 3     | MM rejected quote      |
| 4     | Pair not supported     |
| 5     | Amount too small       |
| 6     | Internal error         |
| 7     | Protocol version mismatch |

### 8.4 Top 5 first-hour tripwires

1. **HTTP 401 / `code: 10070`** — wrong key type. Confirm you were issued a `business` key (not a retail or internal one); only `business` keys authenticate on `/v1/agg-swap/*`, `/v1/quote/*`, `/v1/listing/*`.
2. **`code: 10095` (parameter error)** — usually `deadline` sent in milliseconds instead of Unix **seconds**, or `amount_in` sent as a number instead of a decimal integer string. Every wei field must be a string.
3. **`QuoteExpired` revert** — you submitted on-chain after `deadline`. Mempool latency + block time can eat 10-20s on busy chains; request quotes with at least a 60s deadline and broadcast within 30s.
4. **`InsufficientOutput` revert** — time-decay (§7) brought the realized output below your `amount_out_min`. Either compute `effectiveAmountOut` client-side and size `amount_out_min` against that, or set `amount_out_min: "0"` during development and re-tighten once you've measured realistic decay.
5. **`QE_ERROR_CODE_SUBSCRIPTION_LIMIT`** — unbounded `SUBSCRIBE` loop. Track your open subscriptions; always `UNSUBSCRIBE` before re-subscribing; never re-issue a full pair list on every reconnect without first unsubscribing.

---

## 9 · Rate limits, auth, WS reconnect

**Rate limits** — layered, all three apply:

| Tier     | Limit      | Notes                                  |
| :------- | :--------- | :------------------------------------- |
| IP       | 100 QPS    | Per source IP, burst ceiling 120       |
| Instance | 1000 QPS   | Per service instance                   |
| Global   | 5000 QPS   | All instances combined                 |

Over-limit returns HTTP 429:

```json
{ "code": 429, "message": "Rate limit exceeded", "data": {} }
```

**Auth paths**

| Path prefix        | API key required |
| :----------------- | :--------------- |
| `/v1/agg-swap/*`   | yes              |
| `/v1/quote/*`      | yes              |
| `/v1/listing/*`    | yes              |
| WebSocket          | yes              |

**WebSocket reconnect** — exponential backoff, capped at 160s:

| Attempt | Wait |
| :------ | :--- |
| 1       | 5s   |
| 2       | 10s  |
| 3       | 20s  |
| 4       | 40s  |
| 5       | 80s  |
| 6+      | 160s |

After reconnect: wait for `CONNECTION_ACK`, confirm `success === true`, and re-send your `SUBSCRIBE` frames — the server does **not** retain pre-disconnect subscriptions.

**Heartbeat** — bidirectional ping/pong at 30s interval; read-timeout 60s. On timeout, close and reconnect.

---

## 10 · Worked end-to-end example — BSC USDT → BNB, 1 USDT, Mode 1

Copy-paste shell script. Placeholders: `$DELUTHIUM_API_KEY`, `$ME`, `$PK`. Requires `jq`, `curl`, Foundry's `cast`.

```bash
#!/usr/bin/env bash
set -euo pipefail

# --- 0. Env ---
export DELUTHIUM_API=https://rfq-api.deluthium.ai
: "${DELUTHIUM_API_KEY:?set your sk_live_ business key}"
: "${ME:?set the taker EOA address (0x…)}"
: "${PK:?set the taker private key}"
export BSC_RPC=https://bsc-dataseed.bnbchain.org

USDT=0x55d398326f99059ff775485246999027b3197955
WBNB=0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
AMOUNT_IN=1000000000000000000   # 1 USDT (18 decimals on BSC)

# --- 1. Discover pairs on BSC ---
curl -sS -X GET "$DELUTHIUM_API/v1/listing/pairs" \
  -H "Authorization: Bearer $DELUTHIUM_API_KEY" \
  | jq '.data[] | select(.chain_id == 56)'

# --- 2. Indicative quote (non-binding) ---
curl -sS -X POST "$DELUTHIUM_API/v1/agg-swap/indicativeQuote" \
  -H "Authorization: Bearer $DELUTHIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"src_chain_id\":56,\"dst_chain_id\":56,\"token_in\":\"$USDT\",\"token_out\":\"$WBNB\",\"amount_in\":\"$AMOUNT_IN\"}" \
  | jq .

# --- 3. Firm quote (binding, returns calldata) ---
QUOTE=$(curl -sS -X POST "$DELUTHIUM_API/v1/agg-swap/firmQuote" \
  -H "Authorization: Bearer $DELUTHIUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"src_chain_id\": 56,
    \"dst_chain_id\": 56,
    \"from_address\": \"$ME\",
    \"to_address\":   \"$ME\",
    \"token_in\":  \"$USDT\",
    \"token_out\": \"$WBNB\",
    \"amount_in\":      \"$AMOUNT_IN\",
    \"amount_out_min\": \"0\",
    \"deadline\": $(($(date +%s) + 60))
  }")

test "$(echo "$QUOTE" | jq -r .code)" = "10000" || { echo "firmQuote failed: $QUOTE"; exit 1; }

SWAP_ID=$(echo "$QUOTE" | jq -r .data.swap_id)
ROUTER=$(echo "$QUOTE" | jq -r .data.router_address)
CALLDATA_B64=$(echo "$QUOTE" | jq -r .data.calldata)
DATA_HEX=0x$(echo "$CALLDATA_B64" | base64 -d | xxd -p | tr -d '\n')

echo "swap_id=$SWAP_ID router=$ROUTER"

# --- 4. Approve USDT to Router (one-time or per-tx) ---
cast send "$USDT" \
  "approve(address,uint256)" "$ROUTER" "$AMOUNT_IN" \
  --rpc-url "$BSC_RPC" --private-key "$PK"

# --- 5. Broadcast the swap ---
TX_HASH=$(cast send "$ROUTER" "$DATA_HEX" \
  --rpc-url "$BSC_RPC" --private-key "$PK" \
  --value 0 --json | jq -r .transactionHash)

echo "tx_hash=$TX_HASH"
echo "explorer: https://bscscan.com/tx/$TX_HASH"
echo "receipt: swap_id=$SWAP_ID"
```

Verify the fill by matching the tx's `Transfer(address,address,uint256)` events on BSCScan against the `amount_out` from the firmQuote response. The `swap_id` can be used with your account manager as a correlation id if you need to investigate a specific fill.

---

## 11 · Changelog

### v2.0.0 — Breaking changes

- Two integration modes now split explicitly: Aggregation API (Mode 1) and Aggregator (Mode 2).
- Contract architecture moved to three-contract (Router → Settlement → Vault) topology. Router address is new; Settlement address is unchanged.
- WebSocket protocol formalized under `quote.v1` protobuf; full proto published in appendix.
- Removed legacy endpoints `/v1/agg-swap/autoSign`, `/v1/matchers/*`, `/v1/market/*`, `/v1/stock/*`, `/api/v1/cmc/*`.
- `/v1/listing/*` now requires API key (previously open).
- Auth unified on `Authorization: Bearer` header for HTTP. WebSocket uses `api_key` query param or `X-API-Key` header (not `Authorization: Bearer`).
- `firmQuote` responses gained `settlement_address` field; `rfq_quote_data` is now specifically `RFQQuoteV1` protobuf.

### v2.0.1 — `min_order_size` semantics clarified

No HTTP path, auth, or on-chain settlement changes. The `DEPTH_UPDATE` and `DepthSnapshot` messages document field 8, `min_order_size`, as a `base_token` wei decimal string; `"0"`, empty string, or missing means "no constraint." Aligns the doc with the on-wire `quote/v1/quoteWs.proto`.

---

## Appendix A · Full quote.v1.proto

Also hosted verbatim at **https://agent.deluthium.ai/integrate/proto/quote.v1.proto**. Paste directly into `protoc` — proto3 compliant.

```proto
syntax = "proto3";

package quote.v1;

option go_package = "proto/quote/v1;quotev1";

// ============================================================================
// QE WebSocket Message Wrapper
// ============================================================================

message DepthSnapshot {
  uint64 chain_id = 1;
  string mm_id = 2;
  string pair_id = 3;
  string token_a = 4;
  string token_b = 5;
  repeated PriceLevel bids = 6;
  repeated PriceLevel asks = 7;
  string min_order_size = 8;
}

message PriceLevel {
  string price = 1;
  string amount = 2;
}

message DepthSnapshotBatch {
  repeated DepthSnapshot snapshots = 1;
}

message QEMessage {
  QEMessageType type = 1;
  int64 timestamp = 2;
  oneof payload {
    SubscribeRequest subscribe = 3;
    UnsubscribeRequest unsubscribe = 4;
    SubscribeAck subscribe_ack = 5;
    UnsubscribeAck unsubscribe_ack = 6;
    DepthUpdate depth_update = 7;
    QEHeartbeat heartbeat = 8;
    QEError error = 9;
    QEConnectionAck connection_ack = 10;
    DepthSnapshotBatch depth_forward = 11;
  }
}

enum QEMessageType {
  QE_MESSAGE_TYPE_UNSPECIFIED = 0;
  QE_MESSAGE_TYPE_SUBSCRIBE = 1;
  QE_MESSAGE_TYPE_UNSUBSCRIBE = 2;
  QE_MESSAGE_TYPE_SUBSCRIBE_ACK = 3;
  QE_MESSAGE_TYPE_UNSUBSCRIBE_ACK = 4;
  QE_MESSAGE_TYPE_DEPTH_UPDATE = 5;
  QE_MESSAGE_TYPE_HEARTBEAT = 6;
  QE_MESSAGE_TYPE_ERROR = 7;
  QE_MESSAGE_TYPE_CONNECTION_ACK = 8;
  QE_MESSAGE_TYPE_DEPTH_FORWARD = 9;
}

message QEConnectionAck {
  bool success = 1;
  string session_id = 2;
  int64 server_time = 3;
  string error_message = 4;
}

message SubscribeRequest {
  repeated PairSubscription pairs = 1;
}

message PairSubscription {
  uint64 chain_id = 1;
  string base_token = 2;
  string quote_token = 3;
}

message SubscribeAck {
  bool success = 1;
  repeated PairSubscriptionStatus statuses = 2;
  string error_message = 3;
}

message PairSubscriptionStatus {
  uint64 chain_id = 1;
  string base_token = 2;
  string quote_token = 3;
  bool subscribed = 4;
  string reason = 5;
}

message UnsubscribeRequest {
  repeated PairSubscription pairs = 1;
}

message UnsubscribeAck {
  bool success = 1;
  string error_message = 2;
}

message DepthUpdate {
  uint64 chain_id = 1;
  string mm_id = 2;
  string base_token = 3;
  string quote_token = 4;
  repeated PriceLevelInfo bids = 5;
  repeated PriceLevelInfo asks = 6;
  int64 update_time = 7;
  string min_order_size = 8;
}

message PriceLevelInfo {
  string price = 1;
  string amount = 2;
}

message QEHeartbeat {
  bool ping = 1;
  bool pong = 2;
}

message QEError {
  QEErrorCode code = 1;
  string message = 2;
}

enum QEErrorCode {
  QE_ERROR_CODE_UNSPECIFIED = 0;
  QE_ERROR_CODE_INVALID_MESSAGE = 1;
  QE_ERROR_CODE_UNAUTHORIZED = 2;
  QE_ERROR_CODE_RATE_LIMITED = 3;
  QE_ERROR_CODE_PAIR_NOT_SUPPORTED = 4;
  QE_ERROR_CODE_INTERNAL = 5;
  QE_ERROR_CODE_SUBSCRIPTION_LIMIT = 6;
}

// ============================================================================
// RFQ Quote (firmQuote response payload)
// ============================================================================

message RFQQuoteV1 {
  MMQuoteV1 mm_quote = 2;
  FeeV1 fee = 3;
  bytes rfq_signature = 4;
}

message MMQuoteV1 {
  string maker = 1;
  string vault = 2;
  string executor = 3;
  string token_in = 4;
  string token_out = 5;
  string amount_in = 6;
  string amount_out = 7;
  string deadline = 8;
  string nonce = 9;
  ConfidenceExtractedValue confidence_extracted_value = 10;
  bytes extra_data = 11;
  bytes mm_signature = 12;
  string quote_id = 13;
}

message FeeV1 {
  string fee_to = 1;
  string fee_rate = 2;
  string fee_amount = 3;
}

message ConfidenceExtractedValue {
  string confidence_extracted_value_t = 1;
  string confidence_extracted_value_n = 2;
  string confidence_extracted_value_m = 3;
  string confidence_extracted_value_e = 4;
}
```
