{
  "info": {
    "name": "Kronos X MTF Sandbox",
    "description": "Kronos X member API demo collection for the sandbox environment.\n\nSetup (2 minutes):\n1. Import this file into Postman.\n2. Open the collection, tab Variables, and fill in apiKey and apiSecret with your sandbox credentials.\n3. Run any request. Authentication (HMAC signing) is fully automatic via the collection pre-request script. The script signs every send afresh, as the venue requires since member API revision 3.85 (a signed write is accepted once, the same signature again answers 1002); the WebSocket login, which signs the connection's challenge nonce, is in KronosX-Postman-Guide.md.\n\nEvery request in folders 1, 2, 4, 5 and 6 is meant to answer successfully against the live sandbox listing. Requests that need conditions the sandbox does not currently provide (a listed SPOT market, a perpetual running in RFQ mode, a second member for the quoting side of an RFQ) say so in their own name and description, and their tests accept the documented reject code instead of pretending to pass.\n\nPrices move. Rather than ship fixed prices that the mark walks away from (reject 2003, price outside band), the pre-request script reads GET /v1/instruments and GET /v1/ticker and fills the price variables (pxBuy, pxStopTrig, koBarrier, spotBid and friends) from the live mark, aligned to each instrument's own tickSize and qtyStep. That is exactly what your own client must do, so the collection demonstrates it rather than hard-coding around it.\n\nAll prices, quantities and amounts are exact decimal strings. Quantities are decimal BASE-ASSET UNITS (qty 0.005 on BTC-USD-PERP is 0.005 BTC); align them to the per-market qtyStep from GET /v1/instruments and keep price x qty at or above the market's minNotional (5 USD on current listings, reject code 2028 below it). clOrdId values are generated fresh on every send, so requests can be re-sent freely.\n\nIf your member has registered Authorised Users, every order must also carry reg.traderId (reject 2021 without it). Add it to the order bodies below; until your first Authorised User is registered the field is optional.\n\nFolder 2 leaves your account flat: the market order opens a small position and the flatten request closes it. Cancel all removes anything still resting.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    { "key": "baseUrl", "value": "https://sandbox.mtf.perpetuals.net" },
    { "key": "apiKey", "value": "" },
    { "key": "apiSecret", "value": "" },
    { "key": "clOrdId", "value": "" },
    { "key": "lastOrderId", "value": "" },
    { "key": "batchOrderIdA", "value": "" },
    { "key": "batchOrderIdB", "value": "" },
    { "key": "batchClOrdIdC", "value": "" },
    { "key": "lastRfqId", "value": "" },
    { "key": "lastQuoteId", "value": "" },
    { "key": "lastContractId", "value": "" },
    { "key": "segVersion", "value": "0" },
    { "key": "peerMember", "value": "SANDBOX-MM" },
    { "key": "nextCursor", "value": "000000000000000000000000000000" },
    { "key": "pxBuy", "value": "" },
    { "key": "pxBuyNear", "value": "" },
    { "key": "pxBuy2", "value": "" },
    { "key": "pxBuy3", "value": "" },
    { "key": "pxStopTrig", "value": "" },
    { "key": "pxStopLim", "value": "" },
    { "key": "koBarrier", "value": "" },
    { "key": "koQuote", "value": "" },
    { "key": "rfqSymbol", "value": "" },
    { "key": "rfqQty", "value": "" },
    { "key": "rfqIsLive", "value": "no" },
    { "key": "spotSymbol", "value": "BTC-USDC" },
    { "key": "spotListed", "value": "no" },
    { "key": "spotBid", "value": "" },
    { "key": "spotAsk", "value": "" },
    { "key": "spotQty", "value": "" },
    { "key": "flatSide", "value": "SELL" },
    { "key": "flatQty", "value": "" },
    { "key": "flatMaxQty", "value": "0.001" }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Kronos X collection pre-request script. Runs before EVERY request and does",
          "// three things, in this order:",
          "//   1. mint a fresh clOrdId, so any request can be re-sent without a 2009",
          "//   2. refresh the live-market variables this request uses ({{px...}}, {{ko...}},",
          "//      {{rfq...}}, {{spot...}}, {{flat...}}) from public market data, so no example",
          "//      carries a price the mark has walked away from",
          "//   3. HMAC-sign exactly the bytes that will be transmitted",
          "// Step 3 must stay LAST: it hashes the body and path AFTER variable substitution.",
          "//   signature = hex(HMAC_SHA256(secret, X-TIMESTAMP + METHOD + pathWithQuery + body))",
          "",
          "pm.collectionVariables.set('clOrdId', 'pm-' + Date.now());",
          "",
          "const base = pm.collectionVariables.get('baseUrl');",
          "const rawBody = (pm.request.body && pm.request.body.mode === 'raw' && pm.request.body.raw) || '';",
          "const text = rawBody + ' ' + pm.request.url.toString();",
          "const needPerp = text.indexOf('{{px') >= 0;",
          "const needKo   = text.indexOf('{{ko') >= 0;",
          "const needRfq  = text.indexOf('{{rfq') >= 0;",
          "const needSpot = text.indexOf('{{spot') >= 0;",
          "const needFlat = text.indexOf('{{flat') >= 0;",
          "",
          "function authHeaders(method, pathWithQuery, body) {",
          "    const ts = Date.now().toString();",
          "    const secret = pm.collectionVariables.get('apiSecret');",
          "    const sig = CryptoJS.HmacSHA256(ts + method + pathWithQuery + (body || ''), secret)",
          "        .toString(CryptoJS.enc.Hex);",
          "    return {",
          "        'X-API-KEY': pm.collectionVariables.get('apiKey'),",
          "        'X-TIMESTAMP': ts,",
          "        'X-SIGNATURE': sig",
          "    };",
          "}",
          "",
          "function signOutgoing() {",
          "    if (!pm.collectionVariables.get('apiSecret')) return; // public endpoints need no credentials",
          "    // getPathWithQuery() is the path and query Postman puts on the wire. Every query",
          "    // value in this collection uses unreserved characters and commas only, and Postman",
          "    // transmits those literally, so the signed string and the transmitted string are",
          "    // byte-identical. If you add a parameter that needs percent-encoding, percent-encode",
          "    // it IN THE URL yourself: signing '?a=x y' while sending '?a=x%20y' is the classic",
          "    // 1001. The venue signs the raw query it received, not a normalised form of it.",
          "    const method = pm.request.method.toUpperCase();",
          "    const path = pm.variables.replaceIn(pm.request.url.getPathWithQuery());",
          "    let body = '';",
          "    if (pm.request.body && pm.request.body.mode === 'raw' && pm.request.body.raw) {",
          "        // One raw body covers every JSON payload here, single orders and the",
          "        // POST /v1/orders/batch envelope alike: a batch is one signed message, so it",
          "        // signs exactly like any other body, entries and all.",
          "        body = pm.variables.replaceIn(pm.request.body.raw);",
          "    }",
          "    const h = authHeaders(method, path, body);",
          "    Object.keys(h).forEach(function (k) { pm.request.headers.upsert({ key: k, value: h[k] }); });",
          "}",
          "",
          "// Align a value to an instrument's tickSize / qtyStep grid. Integer arithmetic on",
          "// the scaled value: the venue never rounds for you, and a float artifact is a 2002.",
          "function onGrid(value, step, dir) {",
          "    const s = String(step);",
          "    const dot = s.indexOf('.');",
          "    const dec = dot < 0 ? 0 : s.length - dot - 1;",
          "    const p = Math.pow(10, dec);",
          "    const st = Math.round(step * p);",
          "    const v = Math.round(value * p);",
          "    const n = dir === 'up' ? Math.ceil(v / st) : Math.floor(v / st);",
          "    return ((n * st) / p).toFixed(dec);",
          "}",
          "",
          "function getJson(pathWithQuery, signed, cb) {",
          "    const req = { url: base + pathWithQuery, method: 'GET' };",
          "    if (signed) req.header = authHeaders('GET', pathWithQuery, '');",
          "    pm.sendRequest(req, function (err, res) {",
          "        // Decode first, hand over second: cb() must be called exactly once, so a",
          "        // throw inside cb can never be mistaken for a decode failure and re-run it.",
          "        let data = null;",
          "        if (!err && res) {",
          "            try { const b = res.json(); if (b && b.code === 0) data = b.data; }",
          "            catch (e) { data = null; }",
          "        }",
          "        cb(data);",
          "    });",
          "}",
          "",
          "function markOf(t) {",
          "    return parseFloat(t.markPrice || t.indexPrice || t.lastPrice || t.bestBid || '0');",
          "}",
          "",
          "// Size a quantity so price x qty clears minNotional with room to spare, on grid.",
          "function sizeFor(inst, mark) {",
          "    const floor = parseFloat(inst.minNotional || '5');",
          "    let q = onGrid((floor * 4) / mark, inst.qtyStep, 'up');",
          "    if (parseFloat(q) < parseFloat(inst.minQty)) q = inst.minQty;",
          "    return q;",
          "}",
          "",
          "if (!needPerp && !needKo && !needRfq && !needSpot && !needFlat) {",
          "    signOutgoing();",
          "} else {",
          "    getJson('/v1/instruments', false, function (instruments) {",
          "        const list = instruments || [];",
          "        const find = function (s) { return list.filter(function (i) { return i.symbol === s; })[0]; };",
          "        const jobs = [];",
          "",
          "        if (needPerp) jobs.push(['BTC-USD-PERP', function (m, i) {",
          "            const t = (i && i.tickSize) || '0.5';",
          "            // Passive prices 1 to 4 percent under the mark: comfortably inside the",
          "            // 10 percent price band (priceBandBps 1000) and behind the touch, so a",
          "            // resting order really rests instead of trading on arrival.",
          "            pm.collectionVariables.set('pxBuy',      onGrid(m * 0.98,  t, 'down'));",
          "            pm.collectionVariables.set('pxBuyNear',  onGrid(m * 0.99,  t, 'down'));",
          "            pm.collectionVariables.set('pxBuy2',     onGrid(m * 0.97,  t, 'down'));",
          "            pm.collectionVariables.set('pxBuy3',     onGrid(m * 0.96,  t, 'down'));",
          "            // Stop 5 percent above the mark: it stays PENDING_TRIGGER during the demo",
          "            // instead of triggering on the next price tick.",
          "            pm.collectionVariables.set('pxStopTrig', onGrid(m * 1.05,  t, 'up'));",
          "            pm.collectionVariables.set('pxStopLim',  onGrid(m * 1.055, t, 'up'));",
          "        }]);",
          "",
          "        if (needKo) jobs.push(['BTC-USD-KO', function (m, i) {",
          "            const t = (i && i.tickSize) || '0.5';",
          "            // The barrier market carries its OWN mark, independent of BTC-USD-PERP.",
          "            // A long holder needs the level strictly below both mark and entry (2027).",
          "            pm.collectionVariables.set('koBarrier', onGrid(m * 0.95, t, 'down'));",
          "            // The optional take profit sits on the PROFIT side: strictly above mark and entry for a long holder.",
          "            pm.collectionVariables.set('koTakeProfit', onGrid(m * 1.05, t, 'up'));",
          "            // Strict take profit (revision 3.45) needs a contract expiry at most 24 h out: one hour ahead.",
          "            pm.collectionVariables.set('koStrictExpiry', String(Date.now() + 3600 * 1000));",
          "            // Maximum contract life (revision 3.70): a market that publishes maxContractLifeHours refuses an",
          "            // omitted or later expiryTs with 2027 (the sandbox KO markets carry 8784 h): thirty days ahead.",
          "            pm.collectionVariables.set('koExpiryTs', String(Date.now() + 30 * 86400 * 1000));",
          "            pm.collectionVariables.set('koQuote',   onGrid(m, t, 'down'));",
          "        }]);",
          "",
          "        if (needRfq) {",
          "            const rfqPerp = list.filter(function (i) { return i.mode === 'RFQ' && i.kind !== 'BARRIER'; })[0];",
          "            const fallback = list.filter(function (i) { return i.kind === 'PERP'; })[0];",
          "            const chosen = rfqPerp || fallback;",
          "            pm.collectionVariables.set('rfqIsLive', rfqPerp ? 'yes' : 'no');",
          "            if (chosen) {",
          "                pm.collectionVariables.set('rfqSymbol', chosen.symbol);",
          "                jobs.push([chosen.symbol, function (m, i) {",
          "                    if (i) pm.collectionVariables.set('rfqQty', sizeFor(i, m));",
          "                }]);",
          "            }",
          "        }",
          "",
          "        if (needSpot) {",
          "            const spot = list.filter(function (i) { return i.kind === 'SPOT'; })[0];",
          "            pm.collectionVariables.set('spotListed', spot ? 'yes' : 'no');",
          "            pm.collectionVariables.set('spotSymbol', spot ? spot.symbol : 'BTC-USDC');",
          "            if (spot) jobs.push([spot.symbol, function (m, i) {",
          "                pm.collectionVariables.set('spotBid', onGrid(m * 0.98, i.tickSize, 'down'));",
          "                pm.collectionVariables.set('spotAsk', onGrid(m * 1.02, i.tickSize, 'up'));",
          "                pm.collectionVariables.set('spotQty', sizeFor(i, m));",
          "            }]);",
          "        }",
          "",
          "        const flatten = function () {",
          "            if (!needFlat || !pm.collectionVariables.get('apiSecret')) { signOutgoing(); return; }",
          "            // Close exactly what is open: reduceOnly larger than the reducible position",
          "            // is a 2008, so the close is sized from the live position, never guessed.",
          "            // It is then CAPPED at flatMaxQty, which defaults to the 0.001 that step 6",
          "            // opens. Without that cap this step market-closes EVERY BTC-USD-PERP unit",
          "            // the member holds, including a position that existed before the",
          "            // walkthrough started, which is not what 'step 7 closes it' promises.",
          "            // Raise flatMaxQty deliberately if you do want a full close.",
          "            getJson('/v1/positions', true, function (positions) {",
          "                const p = (positions || []).filter(function (x) { return x.symbol === 'BTC-USD-PERP'; })[0];",
          "                const inst = find('BTC-USD-PERP');",
          "                const step = (inst && inst.qtyStep) || '0.0001';",
          "                const net = p ? p.netQty : '0';",
          "                const open = Math.abs(parseFloat(net));",
          "                if (!p || open === 0) {",
          "                    pm.collectionVariables.set('flatSide', 'SELL');",
          "                    pm.collectionVariables.set('flatQty', (inst && inst.minQty) || '0.0001');",
          "                } else {",
          "                    const capRaw = parseFloat(pm.collectionVariables.get('flatMaxQty'));",
          "                    const cap = (isFinite(capRaw) && capRaw > 0) ? capRaw : open;",
          "                    let q = onGrid(Math.min(open, cap), step, 'down');",
          "                    if (!(parseFloat(q) > 0)) q = (inst && inst.minQty) || step;",
          "                    pm.collectionVariables.set('flatSide', parseFloat(net) > 0 ? 'SELL' : 'BUY');",
          "                    pm.collectionVariables.set('flatQty', q);",
          "                }",
          "                signOutgoing();",
          "            });",
          "        };",
          "",
          "        const run = function (k) {",
          "            if (k >= jobs.length) { flatten(); return; }",
          "            const job = jobs[k];",
          "            getJson('/v1/ticker?symbol=' + job[0], false, function (t) {",
          "                // No usable mark (market data down, or a market with no tick yet):",
          "                // keep the last known values rather than deriving prices from zero.",
          "                const m = t ? markOf(t) : 0;",
          "                if (m > 0) { try { job[1](m, find(job[0])); } catch (e) { /* keep last known */ } }",
          "                run(k + 1);",
          "            });",
          "        };",
          "        run(0);",
          "    });",
          "}"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "1. Public market data (no auth needed)",
      "item": [
        { "name": "Server time", "request": { "method": "GET", "url": "{{baseUrl}}/v1/time" } },
        { "name": "Venue + instrument status", "request": { "method": "GET", "url": "{{baseUrl}}/v1/status" } },
        {
          "name": "Instruments (kind: PERP | SPOT | BARRIER, mode: ORDER_BOOK | RFQ)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const d = pm.response.json().data;",
                  "pm.test('listing returned', () => pm.expect(d).to.be.an('array').that.is.not.empty);",
                  "// Everything else in this collection derives its prices and quantities from",
                  "// these three fields. Never hard-code them.",
                  "pm.test('every listing publishes tickSize, qtyStep and minNotional', function () {",
                  "    d.forEach(function (i) {",
                  "        pm.expect(i, i.symbol).to.include.keys('tickSize', 'qtyStep', 'minNotional');",
                  "    });",
                  "});"
                ]
              }
            }
          ],
          "request": { "method": "GET", "url": "{{baseUrl}}/v1/instruments" }
        },
        { "name": "Order book (perp)", "request": { "method": "GET", "url": "{{baseUrl}}/v1/orderbook?symbol=BTC-USD-PERP&depth=5" } },
        {
          "name": "Order book (spot; resolves to the listed spot market, or answers 2001 while none is listed)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (pm.collectionVariables.get('spotListed') === 'yes') {",
                  "    pm.test('spot book returned for ' + pm.collectionVariables.get('spotSymbol'),",
                  "        () => pm.expect(b.code).to.eql(0));",
                  "} else {",
                  "    pm.test('no SPOT market listed today, so 2001 is the correct answer',",
                  "        () => pm.expect(b.code).to.eql(2001));",
                  "}"
                ]
              }
            }
          ],
          "request": { "method": "GET", "url": "{{baseUrl}}/v1/orderbook?symbol={{spotSymbol}}&depth=5" }
        },
        { "name": "Ticker", "request": { "method": "GET", "url": "{{baseUrl}}/v1/ticker?symbol=BTC-USD-PERP" } },
        { "name": "Recent trades", "request": { "method": "GET", "url": "{{baseUrl}}/v1/trades?symbol=BTC-USD-PERP&limit=20" } },
        { "name": "Funding (perps only)", "request": { "method": "GET", "url": "{{baseUrl}}/v1/funding/current?symbol=BTC-USD-PERP" } },
        { "name": "Insurance fund", "request": { "method": "GET", "url": "{{baseUrl}}/v1/insurance-fund" } }
      ]
    },
    {
      "name": "2. Trading walkthrough (perp)",
      "description": "Run top to bottom. Every request here is expected to answer successfully against the live sandbox. Prices come from the live mark via the collection pre-request script, so nothing here goes stale.\n\nThe walkthrough ends flat: step 6 opens a small position by crossing the book, step 7 closes it, and step 8 cancels anything still resting (including the pending stop).",
      "item": [
        {
          "name": "Step 1: Place resting limit BUY",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('envelope ok', () => pm.expect(b.code).to.eql(0));",
                  "const d = b.data || {};",
                  "if (d.orderId) pm.collectionVariables.set('lastOrderId', d.orderId);",
                  "pm.test('order accepted, not rejected', function () {",
                  "    pm.expect(d.status, 'rejectCode ' + d.rejectCode).to.be.oneOf(['NEW', 'PARTIALLY_FILLED', 'FILLED']);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "description": "A passive BUY roughly 2 percent under the mark: inside the instrument's price band (priceBandBps 1000, so 10 percent) and behind the touch, so it rests. price comes from {{pxBuy}}, aligned to the instrument's tickSize by the pre-request script. qty 0.005 is 0.005 BTC, on the 0.0001 qtyStep grid, and price x qty is far above the 5 USD minNotional.\n\nThe reg block carries the RTS 24/22 fields the venue records verbatim. If your member has registered Authorised Users, add reg.traderId as well or the order rejects 2021.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/orders",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"BTC-USD-PERP\",\"side\":\"BUY\",\"type\":\"LIMIT\",\"tif\":\"GTC\",\"price\":\"{{pxBuy}}\",\"qty\":\"0.005\",\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-A\",\"reg\":{\"tradingCapacity\":\"MTCH\",\"clientIdType\":\"LEI\",\"clientId\":\"529900T8BM49AURSDO55\"}}"
            }
          }
        },
        { "name": "Step 2: Open orders", "request": { "method": "GET", "url": "{{baseUrl}}/v1/orders/open?symbol=BTC-USD-PERP" } },
        {
          "name": "Step 3: Order state by id",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('lastOrderId')) {",
                  "    throw new Error('lastOrderId is empty: run \"Step 1: Place resting limit BUY\" first');",
                  "}"
                ]
              }
            }
          ],
          "request": { "method": "GET", "url": "{{baseUrl}}/v1/orders/{{lastOrderId}}" }
        },
        {
          "name": "Step 4: Amend price/qty",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('lastOrderId')) {",
                  "    throw new Error('lastOrderId is empty: run \"Step 1: Place resting limit BUY\" first');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('amend accepted', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "pm.test('new price and qty echoed', function () {",
                  "    pm.expect(b.data.qty).to.eql('0.01');",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "description": "Amends to roughly 1 percent under the mark and doubles the size. An amend re-checks the FULL amended quantity against the price band and the minimum notional, so the price must be a live one, not the price you first sent hours ago.",
            "method": "PUT",
            "url": "{{baseUrl}}/v1/orders/{{lastOrderId}}",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\"price\":\"{{pxBuyNear}}\",\"qty\":\"0.01\"}" }
          }
        },
        {
          "name": "Step 5: Cancel the order",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Guard: without a stored orderId this path would become /v1/orders,",
                  "// which is CANCEL ALL. Run Step 1 first.",
                  "if (!pm.collectionVariables.get('lastOrderId')) {",
                  "    throw new Error('lastOrderId is empty: run \"Step 1: Place resting limit BUY\" first');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('canceled', () => pm.expect(b.data.status).to.eql('CANCELED'));"
                ]
              }
            }
          ],
          "request": { "method": "DELETE", "url": "{{baseUrl}}/v1/orders/{{lastOrderId}}" }
        },
        {
          "name": "Step 6: Market order (IOC, opens a small position)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('envelope ok', () => pm.expect(b.code).to.eql(0));",
                  "const d = b.data || {};",
                  "if (d.rejectCode === 2007) {",
                  "    pm.test('no eligible liquidity right now (2007): the sandbox book was empty',",
                  "        () => pm.expect(d.rejectCode).to.eql(2007));",
                  "} else {",
                  "    pm.test('filled against the book', () => pm.expect(d.status).to.eql('FILLED'));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "description": "Takes liquidity inside the market-order protection collar (marketMaxSlippageBps). This leaves you long 0.001 BTC; step 7 closes it. If the sandbox book happens to be empty the venue answers 2007 rather than filling at any price.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/orders",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"BTC-USD-PERP\",\"side\":\"BUY\",\"type\":\"MARKET\",\"tif\":\"IOC\",\"qty\":\"0.001\",\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-A\"}"
            }
          }
        },
        {
          "name": "Step 7: Close what step 6 opened (reduce-only market, capped at flatMaxQty)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('envelope ok', () => pm.expect(b.code).to.eql(0));",
                  "const d = b.data || {};",
                  "if (d.rejectCode === 2008) {",
                  "    pm.test('nothing to close (2008): the account is already flat',",
                  "        () => pm.expect(d.rejectCode).to.eql(2008));",
                  "} else {",
                  "    pm.test('position closed', () => pm.expect(d.status).to.eql('FILLED'));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "description": "Closes the 0.001 BTC that step 6 opened. The pre-request script reads GET /v1/positions, takes your live BTC-USD-PERP net position, and sends the SMALLER of that position and the collection variable flatMaxQty (default 0.001, the size step 6 uses), on the instrument's qtyStep grid, as {{flatSide}} / {{flatQty}}.\n\nThe live read matters: a reduceOnly order LARGER than the reducible position rejects 2008, so a close is sized from the position, never guessed. The cap matters just as much in the other direction. This is a MARKET order and it trades immediately, so without a cap it would close every BTC-USD-PERP unit on the credential, including a position that was already there before you opened Postman, placed from FIX, another session, or your own strategy. Raise flatMaxQty yourself when you actually want a full close, and check GET /v1/positions first.\n\nReduce-only orders within your reducible position are exempt from the minimum notional, which is how a sub-floor residual stays closable. If you are already flat this answers 2008, which is the honest response to \"close nothing\".",
            "method": "POST",
            "url": "{{baseUrl}}/v1/orders",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"BTC-USD-PERP\",\"side\":\"{{flatSide}}\",\"type\":\"MARKET\",\"tif\":\"IOC\",\"qty\":\"{{flatQty}}\",\"reduceOnly\":true,\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-A\"}"
            }
          }
        },
        {
          "name": "Stop-limit (conditional) order",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('envelope ok', () => pm.expect(b.code).to.eql(0));",
                  "pm.test('resting as a conditional', function () {",
                  "    pm.expect(b.data.status, 'rejectCode ' + b.data.rejectCode).to.eql('PENDING_TRIGGER');",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "description": "Trigger 5 percent ABOVE the mark, so it stays PENDING_TRIGGER for the demo instead of firing on the next price tick. A trigger placed on the wrong side of the mark fires immediately and the resulting limit order crosses the book, which is a filled position, not a demonstration of a conditional. The limit leg sits just above the trigger and inside the price band. Cancel all (below) removes it.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/orders",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"BTC-USD-PERP\",\"side\":\"BUY\",\"type\":\"STOP_LIMIT\",\"tif\":\"GTC\",\"qty\":\"0.001\",\"trigger\":{\"source\":\"LAST\",\"price\":\"{{pxStopTrig}}\",\"direction\":\"AT_OR_ABOVE\",\"limitPrice\":\"{{pxStopLim}}\"},\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-A\"}"
            }
          }
        },
        {
          "name": "Batch: three client orders in one message",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('envelope code 0 even for mixed results', () => pm.expect(b.code).to.eql(0));",
                  "pm.test('one result per entry, array order', () => pm.expect(b.data.results.length).to.eql(3));",
                  "pm.test('every entry accepted', function () {",
                  "    b.data.results.forEach(function (r) {",
                  "        pm.expect(r.status, r.clOrdId + ' rejectCode ' + r.rejectCode).to.eql('NEW');",
                  "    });",
                  "});",
                  "pm.test('clientUid preserved per entry', function () {",
                  "    pm.expect(b.data.results.map(r => r.clientUid)).to.eql(['CLIENT-A', 'CLIENT-B', 'CLIENT-C']);",
                  "});",
                  "// The two batch requests below (revision 3.74) name these orders: A and B by orderId, C by clOrdId.",
                  "if (b.data && b.data.results && b.data.results.length === 3) {",
                  "    pm.collectionVariables.set('batchOrderIdA', b.data.results[0].orderId);",
                  "    pm.collectionVariables.set('batchOrderIdB', b.data.results[1].orderId);",
                  "    pm.collectionVariables.set('batchClOrdIdC', b.data.results[2].clOrdId);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "description": "POST /v1/orders/batch: 1 to 50 complete, independent orders in one signed message, each entry with its own clOrdId and its own clientUid. Here it is the everyday case, three end clients' orders bundled into one round trip, at three passive prices so all three rest and all three results come back NEW.\n\nThe batch is non-atomic: each entry validates and executes independently, and data.results returns per-entry outcomes in array order. A mixed batch behaves the same way. Send one entry with reduceOnly true and no position for that side and the envelope still returns code 0 while that single entry carries status REJECTED and rejectCode 2008; its neighbours stand. That is the mass-close pattern too: one message, N reduceOnly closes, one per end client.\n\nThe batch consumes one order-budget unit per entry, charged up front, all or nothing: a throttled batch consumes nothing. Clean up with Cancel all below.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/orders/batch",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"orders\":[{\"symbol\":\"BTC-USD-PERP\",\"side\":\"BUY\",\"type\":\"LIMIT\",\"tif\":\"GTC\",\"price\":\"{{pxBuy}}\",\"qty\":\"0.005\",\"clOrdId\":\"{{clOrdId}}-a\",\"clientUid\":\"CLIENT-A\"},{\"symbol\":\"BTC-USD-PERP\",\"side\":\"BUY\",\"type\":\"LIMIT\",\"tif\":\"GTC\",\"price\":\"{{pxBuy2}}\",\"qty\":\"0.003\",\"clOrdId\":\"{{clOrdId}}-b\",\"clientUid\":\"CLIENT-B\"},{\"symbol\":\"BTC-USD-PERP\",\"side\":\"BUY\",\"type\":\"LIMIT\",\"tif\":\"GTC\",\"price\":\"{{pxBuy3}}\",\"qty\":\"0.002\",\"clOrdId\":\"{{clOrdId}}-c\",\"clientUid\":\"CLIENT-C\"}]}"
            }
          }
        },
        {
          "name": "Batch amend: resize one batch order, reprice another (revision 3.74)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('batchOrderIdA') || !pm.collectionVariables.get('batchOrderIdB')) {",
                  "    throw new Error('batchOrderIdA or batchOrderIdB is empty: run \"Batch: three client orders in one message\" first');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('envelope code 0, one row per entry in array order', function () {",
                  "    pm.expect(b.code, b.msg).to.eql(0);",
                  "    pm.expect(b.data.results.length).to.eql(2);",
                  "});",
                  "pm.test('no failure row (a row carrying code is a failure row)', function () {",
                  "    b.data.results.forEach(function (r) { pm.expect(r.code, r.msg).to.be.undefined; });",
                  "});",
                  "pm.test('both orders still stand', function () {",
                  "    b.data.results.forEach(function (r) { pm.expect(r.status, 'rejectCode ' + r.rejectCode).to.eql('NEW'); });",
                  "});",
                  "pm.test('the size-down kept its size change', () => pm.expect(b.data.results[0].qty).to.eql('0.004'));",
                  "pm.test('the response carries asOfSeq', () => pm.expect(b.asOfSeq).to.be.above(0));"
                ]
              }
            }
          ],
          "request": {
            "description": "POST /v1/orders/batch/amend (Member API revision 3.74): 1 to 50 amendments of your own orders in one signed message, each entry { orderId, price?, qty? } processed by the matching engine exactly as PUT /v1/orders/{orderId}. The orderIds come from the batch request above. The first entry sizes order A DOWN at its price, which keeps its queue priority; the second reprices order B to a passive price, which is cancel and new.\n\nThe batch is non-atomic: entries run in array order and a refused entry never stops a later one. data.results returns one row per entry in array order: a row carrying code is a failure row (an orderId that is not one of your open orders answers 2014), any other row is the order as of asOfSeq. A success row can read REJECTED (a post-only order repriced to cross, 2005): that level is gone. Every entry costs one order-budget unit, charged up front after the engine-busy check.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/orders/batch/amend",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"amends\":[{\"orderId\":{{batchOrderIdA}},\"qty\":\"0.004\"},{\"orderId\":{{batchOrderIdB}},\"price\":\"{{pxBuy3}}\"}]}"
            }
          }
        },
        {
          "name": "Batch cancel: cancel the batch orders by orderId and by clOrdId (revision 3.74)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('batchOrderIdA') || !pm.collectionVariables.get('batchClOrdIdC')) {",
                  "    throw new Error('the batch order variables are empty: run \"Batch: three client orders in one message\" first');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('envelope code 0, one row per entry in array order', function () {",
                  "    pm.expect(b.code, b.msg).to.eql(0);",
                  "    pm.expect(b.data.results.length).to.eql(3);",
                  "});",
                  "pm.test('every entry cancelled', function () {",
                  "    b.data.results.forEach(function (r) { pm.expect(r.status, r.code + ' ' + r.msg).to.eql('CANCELED'); });",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "description": "POST /v1/orders/batch/cancel (Member API revision 3.74): 1 to 50 cancellations of your own orders in one signed message, each entry naming its order by orderId OR by clOrdId, exactly one. The venue looks every entry up first: an entry naming no open order of yours answers a 2014 row and nothing is submitted for it; every other entry is processed exactly as DELETE /v1/orders/{orderId}. Free: no order-budget unit, never throttled. Here it cancels the three orders the batch placed, A and B by orderId and C by its clOrdId. Run it twice and every row answers 2014.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/orders/batch/cancel",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"cancels\":[{\"orderId\":{{batchOrderIdA}}},{\"orderId\":{{batchOrderIdB}}},{\"clOrdId\":\"{{batchClOrdIdC}}\"}]}"
            }
          }
        },
        {
          "name": "Cancel all (this symbol)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('cancel all accepted', () => pm.expect(b.code).to.eql(0));",
                  "// Cancels resting limit orders AND pending conditionals for this symbol.",
                  "pm.test('canceled count reported', () => pm.expect(b.data).to.have.property('canceled'));"
                ]
              }
            }
          ],
          "request": { "method": "DELETE", "url": "{{baseUrl}}/v1/orders?symbol=BTC-USD-PERP" }
        },
        { "name": "My executions", "request": { "method": "GET", "url": "{{baseUrl}}/v1/executions?symbol=BTC-USD-PERP&limit=20" } }
      ]
    },
    {
      "name": "3. Spot trading",
      "description": "Prefunded spot: a BUY reserves quote cash, a SELL delivers the base asset out of your assets rows, and there are no positions, no margin and no funding.\n\nThe symbol is resolved at send time: the pre-request script reads GET /v1/instruments, takes the first market with kind SPOT, and fills {{spotSymbol}}, {{spotBid}}, {{spotAsk}} and {{spotQty}} from that market's own tickSize, qtyStep, minQty, minNotional and mark. The moment venue operations list a spot market this folder runs for real, with no edits.\n\nWhile no SPOT market is listed (the case on today's sandbox) {{spotSymbol}} falls back to the placeholder BTC-USDC and these requests answer 2001, unknown symbol. The tests below assert that documented answer rather than reporting a false pass, so the folder is honest either way.\n\nOne thing the venue cannot arrange for you: the SELL needs a base-asset balance. Without one it rejects on insufficient assets even on a listed market.",
      "item": [
        {
          "name": "Spot limit SELL (delivers base asset)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (pm.collectionVariables.get('spotListed') !== 'yes') {",
                  "    pm.test('no SPOT market listed today: 2001 is the correct answer',",
                  "        () => pm.expect(b.code).to.eql(2001));",
                  "} else {",
                  "    pm.test('envelope ok', () => pm.expect(b.code).to.eql(0));",
                  "    const d = b.data || {};",
                  "    pm.test('accepted, or rejected only for want of base asset', function () {",
                  "        pm.expect(d.status, 'rejectCode ' + d.rejectCode).to.be.oneOf(['NEW', 'PARTIALLY_FILLED', 'FILLED', 'REJECTED']);",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "url": "{{baseUrl}}/v1/orders",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"{{spotSymbol}}\",\"side\":\"SELL\",\"type\":\"LIMIT\",\"tif\":\"GTC\",\"price\":\"{{spotAsk}}\",\"qty\":\"{{spotQty}}\",\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-A\"}"
            }
          }
        },
        {
          "name": "Spot limit BUY (reserves quote cash)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (pm.collectionVariables.get('spotListed') !== 'yes') {",
                  "    pm.test('no SPOT market listed today: 2001 is the correct answer',",
                  "        () => pm.expect(b.code).to.eql(2001));",
                  "} else {",
                  "    pm.test('envelope ok', () => pm.expect(b.code).to.eql(0));",
                  "    pm.test('resting', function () {",
                  "        pm.expect(b.data.status, 'rejectCode ' + b.data.rejectCode).to.be.oneOf(['NEW', 'PARTIALLY_FILLED', 'FILLED']);",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "url": "{{baseUrl}}/v1/orders",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"{{spotSymbol}}\",\"side\":\"BUY\",\"type\":\"LIMIT\",\"tif\":\"GTC\",\"price\":\"{{spotBid}}\",\"qty\":\"{{spotQty}}\",\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-B\"}"
            }
          }
        },
        { "name": "Account (see assets rows + spotReserved)", "request": { "method": "GET", "url": "{{baseUrl}}/v1/account" } },
        { "name": "Cancel all spot orders", "request": { "method": "DELETE", "url": "{{baseUrl}}/v1/orders?symbol={{spotSymbol}}" } }
      ]
    },
    {
      "name": "4. Account, positions, margin",
      "item": [
        { "name": "Account + margin + assets", "request": { "method": "GET", "url": "{{baseUrl}}/v1/account" } },
        { "name": "Member positions", "request": { "method": "GET", "url": "{{baseUrl}}/v1/positions" } },
        { "name": "End-client sub-positions", "request": { "method": "GET", "url": "{{baseUrl}}/v1/positions/end-clients" } },
        { "name": "End-client sub-positions (one client)", "request": { "method": "GET", "url": "{{baseUrl}}/v1/positions/end-clients?clientUid=CLIENT-A" } },
        {
          "name": "Margin preview (what-if)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('preview returned', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "pm.test('indicative, and it tells you whether it would be accepted', function () {",
                  "    pm.expect(b.data).to.include.keys('indicative', 'accepted', 'im', 'mm', 'available');",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "description": "A read, not an order: no clOrdId uniqueness, no order budget, and never gated on reg.traderId. Sized at 0.25 BTC against a live passive price so the initial margin it reports is a real number for the current mark and your account's imMultiplier.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/margin/preview",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"orders\":[{\"symbol\":\"BTC-USD-PERP\",\"side\":\"BUY\",\"type\":\"LIMIT\",\"tif\":\"GTC\",\"price\":\"{{pxBuy}}\",\"qty\":\"0.25\",\"clOrdId\":\"preview\",\"clientUid\":\"CLIENT-A\"}]}"
            }
          }
        },
        { "name": "ADL indicator", "request": { "method": "GET", "url": "{{baseUrl}}/v1/adl/indicator?symbol=BTC-USD-PERP" } },
        { "name": "Unwind episodes affecting me", "request": { "method": "GET", "url": "{{baseUrl}}/v1/unwinds" } }
      ]
    },
    {
      "name": "5. Market Segments (counterparty acceptance)",
      "description": "Run in order. version is optimistic concurrency and it INCREMENTS on every successful write, so a hard-coded version works exactly once and then answers 4001 forever. Here every write sends {{segVersion}}, which My config seeds and each write refreshes from its own response, so the folder is re-runnable.\n\nThe allow list uses {{peerMember}}, seeded from Matchable members, because a member code that does not exist on the venue is rejected outright.",
      "item": [
        {
          "name": "My config",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('config returned', () => pm.expect(b.code).to.eql(0));",
                  "pm.collectionVariables.set('segVersion', b.data.version);",
                  "pm.test('version captured for the writes below', () => pm.expect(b.data.version).to.be.a('number'));"
                ]
              }
            }
          ],
          "request": { "method": "GET", "url": "{{baseUrl}}/v1/market-segments" }
        },
        {
          "name": "Matchable members",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('list returned', () => pm.expect(b.code).to.eql(0));",
                  "if (b.data && b.data.length) pm.collectionVariables.set('peerMember', b.data[0]);"
                ]
              }
            }
          ],
          "request": { "method": "GET", "url": "{{baseUrl}}/v1/market-segments/matchable-members" }
        },
        { "name": "My matchable book", "request": { "method": "GET", "url": "{{baseUrl}}/v1/orderbook/matchable?symbol=BTC-USD-PERP&depth=5" } },
        {
          "name": "Restrict to specific counterparties",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('restriction applied', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "pm.collectionVariables.set('segVersion', b.data.version);"
                ]
              }
            }
          ],
          "request": {
            "description": "Restricts your matchable set to one counterparty. Run My config and Matchable members first: this sends the version you last read and the member code the venue actually knows.",
            "method": "PUT",
            "url": "{{baseUrl}}/v1/market-segments",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\"restricted\":true,\"allow\":[\"{{peerMember}}\"],\"version\":{{segVersion}}}" }
          }
        },
        {
          "name": "Re-open to all",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('re-opened', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "pm.collectionVariables.set('segVersion', b.data.version);"
                ]
              }
            }
          ],
          "request": {
            "description": "Leaves you unrestricted again, which is how the sandbox expects you to leave it. Sends the version returned by the previous write.",
            "method": "PUT",
            "url": "{{baseUrl}}/v1/market-segments",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\"restricted\":false,\"allow\":[],\"version\":{{segVersion}}}" }
          }
        }
      ]
    },
    {
      "name": "6. Event stream recovery (backfill)",
      "item": [
        { "name": "My stream cursor + retention", "request": { "method": "GET", "url": "{{baseUrl}}/v1/stream/cursor" } },
        {
          "name": "Backfill events from seq 1",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('page returned', () => pm.expect(b.code).to.eql(0));",
                  "if (b.data && b.data.nextCursor) pm.collectionVariables.set('nextCursor', b.data.nextCursor);",
                  "pm.test('nextCursor stored for the paging request below',",
                  "    () => pm.expect(pm.collectionVariables.get('nextCursor')).to.have.lengthOf(30));"
                ]
              }
            }
          ],
          "request": { "method": "GET", "url": "{{baseUrl}}/v1/stream/events?fromSeq=1&limit=50" }
        },
        {
          "name": "Backfill orders + executions only",
          "request": {
            "description": "The channels filter is a comma-separated list. Note what the pre-request script signs: the path and query exactly as Postman transmits it, comma included. Sign a normalised or re-encoded form of the query and the venue answers 1001, because it verifies against the raw query it received. This is the single most common signing bug in client libraries that re-encode a URL after signing it.",
            "method": "GET",
            "url": "{{baseUrl}}/v1/stream/events?fromSeq=1&channels=order,execution&limit=50"
          }
        },
        {
          "name": "Next page (uses nextCursor from the previous request)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('page returned', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "if (b.data && b.data.nextCursor) pm.collectionVariables.set('nextCursor', b.data.nextCursor);"
                ]
              }
            }
          ],
          "request": {
            "description": "Run Backfill events from seq 1 first: it stores nextCursor. Re-send this request repeatedly to walk the stream, each send advancing the cursor. The seeded value (thirty zeros) is the start-of-stream cursor, so it also works standalone.",
            "method": "GET",
            "url": "{{baseUrl}}/v1/stream/events?afterCursor={{nextCursor}}&limit=50"
          }
        },
        { "name": "Margin-call history", "request": { "method": "GET", "url": "{{baseUrl}}/v1/margin/calls?limit=20" } }
      ]
    },
    {
      "name": "7. RFQ + barrier contracts",
      "description": "RFQ entry (create, quote, accept, cancel) is REST only, with lifecycle pushes on the private WebSocket rfq channel. There is no FIX surface for any of it, so this folder has no FIX equivalent: a FIX-only integration cannot trade RFQ markets or barrier contracts at all.\n\nThe barrier flow below is live on the current sandbox listing (BTC-USD-KO, kind BARRIER, mode RFQ). Two of the requests need a SECOND member, because both legs of an RFQ trade must be different members: quoting your own request rejects 2026, per member, not per API key or clientUid. Import the collection a second time, or switch apiKey and apiSecret, to run the quoting side.\n\nBarrier terms move with the barrier market's own mark, which is independent of BTC-USD-PERP. The pre-request script reads it and places the knock-out level 5 percent below it, so the level is strictly beyond the knock-out side of the mark as code 2027 requires.",
      "item": [
        {
          "name": "Create RFQ on a perp (shape only while no perp runs mode RFQ)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (pm.collectionVariables.get('rfqIsLive') === 'yes') {",
                  "    pm.test('rfq created on ' + pm.collectionVariables.get('rfqSymbol'),",
                  "        () => pm.expect(b.code).to.eql(0));",
                  "    if (b.data && b.data.rfqId) pm.collectionVariables.set('lastRfqId', b.data.rfqId);",
                  "} else {",
                  "    pm.test('no perp runs mode RFQ today, so 2023 is the correct answer',",
                  "        () => pm.expect(b.code).to.eql(2023));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "description": "Documents the perp-RFQ request shape: same body as the barrier RFQ minus the barrier object, which must be absent on a non-barrier market.\n\nThe symbol is resolved at send time. If any market is listed with kind PERP and mode RFQ, this request uses it and creates a real RFQ, sized from that instrument's own qtyStep and minNotional. On today's listing every perpetual runs mode ORDER_BOOK, so it falls back to a listed perp and the venue answers 2023, not an RFQ market: trade that symbol on its order book instead. The test asserts that answer rather than claiming a pass.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/rfq",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"{{rfqSymbol}}\",\"side\":\"BUY\",\"qty\":\"{{rfqQty}}\",\"expiresInMs\":120000,\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-A\"}"
            }
          }
        },
        {
          "name": "Create barrier RFQ (BUY = long holder, knock-out below)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('rfq created', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "const d = b.data || {};",
                  "if (d.rfqId) pm.collectionVariables.set('lastRfqId', d.rfqId);",
                  "pm.test('rfqId stored', () => pm.expect(d.rfqId).to.be.a('number'));",
                  "pm.test('barrier is below the market mark', function () {",
                  "    pm.expect(parseFloat(d.barrier)).to.be.above(0);",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "description": "Live on the current listing. BUY is a long holder, so the knock-out level must be strictly BELOW both the current mark and the eventual entry price, otherwise 2027. {{koBarrier}} is the barrier market's mark less 5 percent, aligned to its tickSize. expiryTs is sent as {{koExpiryTs}} (thirty days ahead, set by the pre-request script): since revision 3.70 a market that publishes maxContractLifeHours on GET /v1/instruments refuses an omitted or later expiryTs with 2027, and every sandbox KO market publishes 8784 h (366 days) from the date venue operations announce; on a market that publishes no cap the field stays optional and an omitted expiryTs opens an open-ended contract. barrier.takeProfit (revision 3.43) is optional too and omitted here: see the next request for a contract with a take profit.\n\nRemember to cancel the RFQ (below) if nobody quotes it, or just let the quoting window lapse.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/rfq",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"BTC-USD-KO\",\"side\":\"BUY\",\"qty\":\"1\",\"expiresInMs\":120000,\"barrier\":{\"level\":\"{{koBarrier}}\",\"expiryTs\":{{koExpiryTs}}},\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-A\"}"
            }
          }
        },
        {
          "name": "Create barrier RFQ with a take profit (BUY: knock-out below, take profit above)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('rfq created', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "const d = b.data || {};",
                  "if (d.rfqId) pm.collectionVariables.set('lastRfqId', d.rfqId);",
                  "pm.test('take profit echoed above the barrier', function () {",
                  "    pm.expect(parseFloat(d.takeProfit)).to.be.above(parseFloat(d.barrier));",
                  "});"
                ]
              }
            }
          ],
          "request": {
            "description": "Revision 3.43: the same request with the OPTIONAL second exit. barrier.takeProfit must sit strictly on the PROFIT side of the entry (for BUY: above the current mark now, and above the entry the writer quotes), otherwise 2027; {{koTakeProfit}} is the barrier market's mark plus 5 percent, on the tick grid. When the mark reaches it the contract terminates AT the take-profit price, fee-free like a knock-out, and the holder banks |takeProfit - entry| x qty. The create response, GET /v1/rfq/mine, the accept response and GET /v1/barriers all echo takeProfit; a close on the WS barriers channel arrives as type takeProfit with terminationReason TAKE_PROFIT. expiryTs is sent as {{koExpiryTs}} for the same reason as on the plain request (maxContractLifeHours, revision 3.70).",
            "method": "POST",
            "url": "{{baseUrl}}/v1/rfq",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"BTC-USD-KO\",\"side\":\"BUY\",\"qty\":\"1\",\"expiresInMs\":120000,\"barrier\":{\"level\":\"{{koBarrier}}\",\"takeProfit\":\"{{koTakeProfit}}\",\"expiryTs\":{{koExpiryTs}}},\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-A\"}"
            }
          }
        },
        {
          "name": "Create a STRICT take-profit barrier RFQ (revision 3.45: cheaper, pays only on a touch, expiry within 24 h)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('rfq created', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "const d = b.data || {};",
                  "if (d.rfqId) pm.collectionVariables.set('lastRfqId', d.rfqId);",
                  "pm.test('strict take profit echoed', () => pm.expect(d.strictTakeProfit).to.eql(true));",
                  "pm.test('premium share is the strict share (bps of the distance)', function () {",
                  "    pm.expect(d.premiumBps).to.be.a('number');",
                  "    pm.expect(d.premiumBps).to.be.above(0);",
                  "});",
                  "pm.test('indicative premium at the mark is a money string', () => pm.expect(d.premium).to.be.a('string'));"
                ]
              }
            }
          ],
          "request": {
            "description": "Revision 3.45: barrier.strictTakeProfit true asks for a contract that pays ONLY if the take profit is touched before the contract expiry; at the expiry without a touch the holder loses the whole premium whatever the mark. It needs BOTH a take profit and an expiryTs at most 24 hours ahead ({{koStrictExpiry}} is set one hour ahead by the collection's pre-request script), on a market whose strictTakeProfitPremiumBps is nonzero (GET /v1/instruments; every barrier market on the sandbox: 5000 = the premium is 50 percent of the entry-to-barrier distance x qty, against 90 percent for a normal contract after the 10 percent knock-out rebate), otherwise 2027. The create response, GET /v1/rfq/mine, the WS rfq created frame, the accept response, GET /v1/barriers rows and the WS barriers frames carry strictTakeProfit, premium (the holder's maximum loss: indicative at the current mark until a quote is accepted, then frozen at the quoted entry) and premiumBps.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/rfq",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"BTC-USD-KO\",\"side\":\"BUY\",\"qty\":\"1\",\"expiresInMs\":120000,\"barrier\":{\"level\":\"{{koBarrier}}\",\"takeProfit\":\"{{koTakeProfit}}\",\"expiryTs\":{{koStrictExpiry}},\"strictTakeProfit\":true},\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-A\"}"
            }
          }
        },
        {
          "name": "Open board (RFQs you can quote)",
          "request": {
            "description": "As the requester this is empty of your own requests by design: you cannot quote them. Run it with the SECOND member's credentials to see the request you just created.",
            "method": "GET",
            "url": "{{baseUrl}}/v1/rfq/open"
          }
        },
        { "name": "My RFQs (live quotes included)", "request": { "method": "GET", "url": "{{baseUrl}}/v1/rfq/mine" } },
        {
          "name": "Quote an RFQ (needs the SECOND member's credentials)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('lastRfqId')) {",
                  "    throw new Error('lastRfqId is empty: run \"Create barrier RFQ\" first');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.code === 2026) {",
                  "    pm.test('2026: this credential belongs to the member that raised the RFQ. Switch to the second member to quote it.',",
                  "        () => pm.expect(b.code).to.eql(2026));",
                  "} else {",
                  "    pm.test('quote placed', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "    if (b.data && b.data.quoteId) pm.collectionVariables.set('lastQuoteId', b.data.quoteId);",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "description": "Both legs of an RFQ trade must be different members, so this rejects 2026 while you are still signed in as the requester. That is a per-member check: a second API key or a different clientUid on the same member does not satisfy it.\n\nThe quoted entry price must sit strictly ABOVE the knock-out level for a long holder, and inside the market's price band. {{koQuote}} is the barrier market's current mark, which satisfies both.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/rfq/{{lastRfqId}}/quotes",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"price\":\"{{koQuote}}\",\"expiresInMs\":60000,\"clOrdId\":\"{{clOrdId}}\",\"clientUid\":\"CLIENT-B\"}"
            }
          }
        },
        { "name": "My quotes (across all open RFQs)", "request": { "method": "GET", "url": "{{baseUrl}}/v1/rfq/quotes/mine" } },
        {
          "name": "Accept quote (as the requester)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Guard: an empty quoteId would send {\"quoteId\":} , which is not JSON.",
                  "if (!pm.collectionVariables.get('lastRfqId') || !pm.collectionVariables.get('lastQuoteId')) {",
                  "    throw new Error('lastRfqId/lastQuoteId empty: create the RFQ, then quote it as the second member, before accepting');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "pm.test('accepted', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "const d = b.data || {};",
                  "// A barrier open carries the opened contractId (kept for the close request",
                  "// below); an early close carries the CLOSED contract's id, settlePrice and",
                  "// realizedPnl instead, so the variable is only refreshed on an open.",
                  "if (d.contractId && !d.closeContractId) pm.collectionVariables.set('lastContractId', d.contractId);",
                  "if (d.closeContractId) {",
                  "    pm.test('early close settled at the quoted price', function () {",
                  "        pm.expect(d.contractId).to.eql(d.closeContractId);",
                  "        pm.expect(d.settlePrice).to.eql(d.price);",
                  "        pm.expect(d.realizedPnl).to.be.a('string');",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "description": "Accept as the requester, with the requester's credentials. The venue re-checks the quote against the current mark at accept time, so a quote the mark has drifted away from answers 2003: take a fresh one.\n\nOn a barrier open the response carries contractId and the contract's terms; the collection keeps that contractId in lastContractId for the close request below. On a CLOSE REQUEST (revision 3.44) the same accept CLOSES the contract at the quoted price and the response carries contractId (the closed contract), closeContractId, settlePrice and your realizedPnl.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/rfq/{{lastRfqId}}/accept",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": { "mode": "raw", "raw": "{\"quoteId\":{{lastQuoteId}}}" }
          }
        },
        {
          "name": "Close an open barrier contract early (as the holder, revision 3.44)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('lastContractId')) {",
                  "    throw new Error('lastContractId is empty: open a contract first (create the barrier RFQ, quote it as the second member, accept it as the requester), or paste a contractId you HOLD from GET /v1/barriers');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.code === 2035) {",
                  "    pm.test('2035: lastContractId is not an OPEN contract this member HOLDS on BTC-USD-KO (already settled, written rather than held, or a close request is already open for it). Re-read GET /v1/barriers.',",
                  "        () => pm.expect(b.code).to.eql(2035));",
                  "} else {",
                  "    pm.test('close request created', () => pm.expect(b.code, b.msg).to.eql(0));",
                  "    const d = b.data || {};",
                  "    if (d.rfqId) pm.collectionVariables.set('lastRfqId', d.rfqId);",
                  "    pm.test('the venue derived the closing side, the full quantity and the terms', function () {",
                  "        pm.expect(String(d.closeContractId)).to.eql(String(pm.collectionVariables.get('lastContractId')));",
                  "        pm.expect(d.side).to.eql('SELL');   // a long (BUY) holder closes by SELLing",
                  "        pm.expect(d.qty).to.be.a('string');",
                  "        pm.expect(d.barrier).to.be.a('string');",
                  "    });",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "description": "Revision 3.44: the HOLDER asks the WRITER to close the contract early. The body names the market and the contract (closeContractId, an OPEN contract you HOLD there) and NOTHING else about the trade: no side, qty or barrier (any of them present rejects 2013). The venue derives the closing side (this long holder SELLs), the full contract quantity and the contract's terms, and echoes them with closeContractId. clientUid is omitted so it defaults to the contract's holder-side client.\n\nWho sees it: only the writer of that contract (its board GET /v1/rfq/open and its WS rfq created frame); nobody else may quote it (2024). Next steps: run 'Quote an RFQ' with the SECOND member's credentials (the writer): {{koQuote}} is the current mark, which lies strictly inside the terms as required (2027 otherwise); then 'Accept quote' as the requester closes the contract AT that price with terminationReason EARLY_CLOSE, maker/taker fees like the entry, and 'My barrier contracts (closed)' shows the row. At most one close request per contract at a time; 'Cancel RFQ' withdraws it for free.",
            "method": "POST",
            "url": "{{baseUrl}}/v1/rfq",
            "header": [{ "key": "Content-Type", "value": "application/json" }],
            "body": {
              "mode": "raw",
              "raw": "{\"symbol\":\"BTC-USD-KO\",\"closeContractId\":{{lastContractId}},\"expiresInMs\":120000,\"clOrdId\":\"{{clOrdId}}\"}"
            }
          }
        },
        {
          "name": "Cancel RFQ (as the requester)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('lastRfqId')) {",
                  "    throw new Error('lastRfqId is empty: run \"Create barrier RFQ\" first');",
                  "}"
                ]
              }
            }
          ],
          "request": { "method": "POST", "url": "{{baseUrl}}/v1/rfq/{{lastRfqId}}/cancel" }
        },
        {
          "name": "Withdraw quote (as the maker)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('lastRfqId') || !pm.collectionVariables.get('lastQuoteId')) {",
                  "    throw new Error('lastRfqId/lastQuoteId empty: quote an RFQ as the second member first');",
                  "}"
                ]
              }
            }
          ],
          "request": { "method": "POST", "url": "{{baseUrl}}/v1/rfq/{{lastRfqId}}/quotes/{{lastQuoteId}}/cancel" }
        },
        { "name": "My barrier contracts", "request": { "method": "GET", "url": "{{baseUrl}}/v1/barriers" } },
        {
          "name": "My barrier contracts (closed: settlePrice, realizedPnl, terminationReason)",
          "request": {
            "description": "Recently settled contracts, both roles, newest first (kept since the last venue start): settlePrice, role-signed realizedPnl, terminationReason (KNOCK_OUT | TAKE_PROFIT | EXPIRY | TEAR_UP | FINAL_SETTLEMENT | EARLY_CLOSE) and closedTs in place of the mark-derived figures. An early close (revision 3.44) shows terminationReason EARLY_CLOSE with settlePrice = the accepted quote.",
            "method": "GET",
            "url": "{{baseUrl}}/v1/barriers?status=CLOSED"
          }
        }
      ]
    }
  ]
}
