{
 "ok": true,
 "data": {
  "api": "MarginPad Bot API",
  "current_version": "2.9.1",
  "entries": [
   {
    "date": "2026-09-16",
    "version": "2.9.1",
    "title": "Official clients on npm and PyPI",
    "changes": [
     {
      "type": "added",
      "breaking": false,
      "text": "npm install marginpad and pip install marginpad. Both clients are 2.9.1, MIT, zero dependencies, and wrap the whole API including the new realism switches: market data, paper trading, limit and stop orders, trailing stops, books, replay, the trading report, webhooks with signature verification and the WebSocket stream. Keyless market data works with no account: new MarginPad().price(\"BTC\")."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "MCP tool paper_realism, so an AI agent can read and set fill realism without leaving the MCP server. 28 tools."
     },
     {
      "type": "fixed",
      "breaking": false,
      "text": "The clients reported their own version as 2.6.0 while shipping as 2.8.0, so marginpad.__version__ was wrong and the User-Agent named a version two releases old. Both are correct now and the publish refuses to run while the code and the package disagree."
     }
    ]
   },
   {
    "date": "2026-09-16",
    "version": "2.9.0",
    "title": "Fill realism: slippage and risk-limit tiers, off by default and yours to switch on",
    "changes": [
     {
      "type": "added",
      "breaking": false,
      "text": "GET|POST /v1/realism - two deliberate simplifications in the engine, made optional. `slippage` fills a market order against you the way walking a real book does (0.01% crypto majors, 0.05% thinner crypto, 0.005% forex, 0.02% stocks/metals/indices) instead of at the live price. `margin_tiers` raises maintenance margin with notional size the way an exchange risk-limit table does, instead of a flat 0.5%. `margin_venue` picks whose published maintenance-margin rate to build on. All three OFF by default: nothing you have already built changes."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "Per-call overrides on /v1/open: slippage, margin_tiers, mmr_pct. One strategy can be tested both ways without touching the account setting."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /api/arena now carries open_positions, open_margin_usd, open_unrealized_usd, pnl_incl_open_usd and realism.label per row. win_rate_pct only ever counted CLOSED trades, so a bot that takes its winners and holds its losers reads 100%: the open book now sits beside it on the board and in the JSON."
     },
     {
      "type": "changed",
      "breaking": false,
      "text": "A position records the realism it was filled with and keeps its own maintenance margin for life - switching either setting never rewrites a trade you already hold."
     }
    ]
   },
   {
    "date": "2026-09-15",
    "version": "2.8.0",
    "title": "A fourth plan, and the prices the plans actually ship at",
    "changes": [
     {
      "type": "changed",
      "breaking": false,
      "text": "Prices, superseding the ones announced in 2.7.0 earlier the same day: API Pro is $29/month (600 requests/minute, 10 keys, 200 open positions, 5 books, 3 webhooks, 50 AI reads a day) and API Max is $79/month (2000/minute, 30 keys, 500 open positions, 20 books, 15 webhooks, 200 AI reads). Nobody had bought at the earlier numbers."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "API Business, $159/month: 5000 requests/minute per key, 100 keys, 1000 open positions, 50 books, 50 webhooks, 500 AI market reads a day. Same engine and same endpoints as every other plan - it is headroom for a desk running many strategies at once, not a different product."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "Free is untouched: 120 requests/minute, 3 keys, 50 open positions, the whole trading engine, replay, books, the WebSocket stream and all keyless market data. Market data stays keyless and free on every plan."
     }
    ]
   },
   {
    "date": "2026-09-15",
    "version": "2.7.0",
    "title": "The API has its own plans. Premium no longer changes a single API limit.",
    "changes": [
     {
      "type": "changed",
      "breaking": true,
      "text": "API limits are no longer derived from a MarginPad Premium subscription. There are now three API plans: Free ($0 - 120 requests/minute per key, 3 keys, 50 open positions), Pro ($19/month - 600/minute, 10 keys, 200 open positions, 3 webhooks, 50 AI reads a day, full trading report) and Max ($49/month - 2000/minute, 30 keys, 500 open positions, 15 webhooks, 200 AI reads a day). GET /v1/usage reports your plan, when it ends and what every plan costs; GET /api/apiplan is the catalogue."
     },
     {
      "type": "changed",
      "breaking": true,
      "text": "Webhooks and /v1/ai now answer 402 {\"error\":\"plan_required\",\"plan_needed\":\"pro\"} instead of \"premium_required\" when the key is on Free. The old code is still described in the error dictionary so a 2.3 bot recognises it. /v1/report gained \"plan\" and \"breakdowns\"; its \"premium\" boolean is kept as a deprecated alias of \"breakdowns\" and still means exactly the same thing."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "Nothing about Free got smaller: 120 requests/minute, the whole paper-trading engine, every fee venue, limit and stop entries, trailing stops, replay, books, the WebSocket stream and all keyless market data are exactly what they were. Market data stays keyless and free - that is not for sale."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "Accounts that were already building on the API before this split keep Pro free until 30 September 2026. Every response from GET /v1/usage carries free_month.days_left, so nobody has to discover the change from a 429."
     }
    ]
   },
   {
    "date": "2026-09-12",
    "version": "2.6.0",
    "title": "Replay: run your bot through a past day on our own candles",
    "changes": [
     {
      "type": "added",
      "breaking": false,
      "text": "POST /v1/replay {symbol, day:\"YYYY-MM-DD\", speed} starts a replay of one past UTC day on this key’s book, on MarginPad’s 1-minute candles. speed = market seconds per real second (60 = a day in 24 minutes, max 600). From then on /v1/open, /positions, /close, /close_all, /account, /balance and /sltp made with that key act on a separate replay journal, priced at the candle under the cursor; stops, targets and liquidations are checked on every candle between two calls (high and low, never only the close), with the same fee and funding math as live."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /v1/replay = cursor, price, progress and the candles up to the cursor (?interval=1|5|15|60|240&bars=). POST {act:\"stop\"} closes everything at the cursor and returns the summary (closes, win rate, P&L, return, liquidations, last 50 trades); the replay journal is then emptied, the summary is the record."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "Replay rows carry src:\"replay\": the live sweep, the season boards, the Bot arena, the report and the equity curve never see them. Limit and stop entries are not available in replay yet; keyless /v1/price and /v1/klines keep answering live data - read the replay price and candles from GET /v1/replay."
     }
    ]
   },
   {
    "date": "2026-09-12",
    "version": "2.5.0",
    "title": "Books (sub-accounts), reset, equity curve, Bot arena, usage, named errors",
    "changes": [
     {
      "type": "added",
      "breaking": false,
      "text": "Books: mint a key with {\"act\":\"create\",\"book\":\"strat-a\"} on POST /api/bot/key and every call made with that key trades a SEPARATE journal, balance, report and equity curve (<account>:<book>). One strategy = one key = one clean result. Free accounts get one book besides the main account, Premium five. GET /v1/accounts lists them; /v1/account carries `account`. Webhooks fire on the owner’s hooks with `account` in the payload."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "POST /v1/reset {\"confirm\":true} restarts the book from $10,000: closed trades are archived (never deleted), resting orders cancelled, the report, ledger and equity curve start from the reset. Refused with open_positions_exist while anything is open."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /v1/equity?days=30&step_min=60 - starting balance + realized P&L per bucket from the ledger, the live unrealized point at the end, max_drawdown_pct."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /api/arena - public Bot arena: bot-opened closes in the current 14-day season per account or book (5 closes to enter), ranked by realized P&L. Page: /arena/. A scoreboard, no prizes."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /v1/usage carries usage_30d: this key’s calls per day by endpoint and the calls refused with 429. Refused calls are counted from now on."
     },
     {
      "type": "changed",
      "breaking": false,
      "text": "POST /v1/open refuses out-of-range input with named errors (leverage_max with max and requested, leverage_min, margin_usd_min_1, margin_usd_max_100000) instead of clamping. A bot that asked for 5000x used to get 1000x with ok:true."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "Reference site /api-docs/, public status /status/ (+ GET /api/status), changelog feed /api/changelog.xml and .json, SDK packages `marginpad` for npm and PyPI. The stream accepts ?api_key= (aliases key=, token=) and explains a missing key."
     }
    ]
   },
   {
    "date": "2026-09-11",
    "version": "2.4.0",
    "title": "Fees as on the exchange you will actually use",
    "changes": [
     {
      "type": "added",
      "breaking": false,
      "text": "GET|POST /v1/fees - pick whose fee schedule your paper fills are charged at: Bybit, Binance, OKX, Bitget, MEXC, Gate, KuCoin, Kraken or Hyperliquid (every one has a trading API), at the venue’s published taker rate LESS the referral discount a MarginPad sign-up gets there (Bybit, Binance, Bitget, Gate 20%; Hyperliquid 4% with code MARGINPAD). GET lists the table with the effective rate, code and link; POST {venue} sets the account default, {venue:null} restores the MarginPad class rate. The same default drives the \"Fees as on\" selector in the Paper Trade terminal, so a bot and its owner’s manual trades pay identical fees."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "fee_venue on POST /v1/open (market, limit and stop) overrides the default for that one position. Positions carry fee_rate_pct and fee_venue; dry_run:true reports fee_venue and fee_vs_marginpad_default_usd, the round-trip difference in dollars."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "Nothing changes unless you opt in. The PnL formula is untouched - a venue only changes the per-side rate a position is stamped with at open; both legs pay that taker rate (the engine fills at market; a maker rebate on a resting entry is not modelled). Crypto perps only; stocks, forex, metals and indices keep the MarginPad class rates. Above ~181x the rate is still lowered so a round trip never exceeds 20% of margin. Open positions keep the rate they were filled with. MCP: paper_fees (23 tools)."
     }
    ]
   },
   {
    "date": "2026-09-11",
    "version": "2.3.0",
    "title": "Webhooks, trailing stops, stop entries, modify order, dry run, report, AI, keyed data tier",
    "changes": [
     {
      "type": "added",
      "breaking": false,
      "text": "Webhooks (Premium). GET|POST /v1/webhooks registers up to 3 https URLs per account; we POST position.opened / position.updated / position.closed / position.liquidated / order.filled / order.expired / order.cancelled to them, seconds after the event, whether or not your bot is connected. Every delivery is signed (X-MP-Signature: sha256=HMAC_SHA256(secret, raw body)) with the hook’s own secret; failures retry 5 times with backoff; a hook is paused after 25 consecutive failures and the list says why. POST {act:\"test\"} sends a ping and returns the HTTP status your server answered. Measured before: 89.5% of all Bot API traffic was polling /positions and /account for events that this now pushes."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "Trailing stops, server-side. trail_pct on POST /v1/open (market and resting orders) and on POST /v1/sltp sets a stop that follows the best price seen since entry at that percent distance. It ratchets on the minute sweep and on every /positions read, from 1-minute candle extremes when candles are available, and never loosens. Positions carry trail_pct and trail_hwm."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "Stop entries. POST /v1/open with type:\"stop\" rests a breakout entry: a long above the market or a short below it, filled AT the level from 1m candles like a limit order. The wrong side is refused (stop_wrong_side) so a stop and a limit can never be confused. GET /v1/orders now returns type: \"limit\"|\"stop\" on every order."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "POST /v1/modify_order changes a resting order in place: limit_price, sl, tp, margin_usd, leverage, trail_pct. The direction is re-derived from the market now and the candle watermark restarts, so a moved level can never fill on a bar printed before the change."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "dry_run:true on POST /v1/open validates and prices the request and writes nothing: entry, quantity, notional, liquidation price and distance, the open fee and the full round trip, the taker rate and the leverage cap. For a resting order it also says whether the level would fill immediately."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /v1/report?days=30 - the trading report the site shows at /trading-report/, for the account behind the key: totals and the skill score on every plan, breakdowns by coin / leverage band / side / hour / day and the written findings on Premium. Every finding carries the n it rests on."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "POST /v1/ai {symbol, interval, question} (Premium) - the chart panel’s AI read from the API: the same model, the same prompt and the same 50-a-day quota as Ask-AI on the site. The brief it reasons over (price, move, swing high/low, moving averages, RSI, ATR, Bollinger, recent closes) is returned with the answer, and a ```plan block, when the model gives one, comes back parsed as plan."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "Keyed data tier. Send X-API-Key on any /api/v1/* market-data call and it is metered against that key (120/minute free, 600/minute Premium) instead of the 60/minute per-IP limit - no new endpoint, and a bot behind a shared address stops sharing its budget with strangers. Keyless calls are unchanged. X-RateLimit-Scope says which limit applied."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /v1/positions?status=open|closed. GET /v1/usage now lists the features the key is entitled to (webhooks, report breakdowns, AI) and the data-API budget. Official single-file clients: /assets/sdk/marginpad.py and /assets/sdk/marginpad.js (REST + WebSocket, zero dependencies)."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "MCP: paper_modify_order and paper_report; paper_open takes trail_pct and dry_run, paper_sltp takes trail_pct, paper_limit_order takes type \"limit\"|\"stop\". 22 tools."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "/api/bot/v1/* response shapes stay frozen. Every field above is additive; nothing was renamed or removed."
     }
    ]
   },
   {
    "date": "2026-09-09",
    "version": "2.2.1",
    "title": "Margin is what you committed",
    "changes": [
     {
      "type": "fixed",
      "breaking": false,
      "text": "A fill used to store margin MINUS the entry-leg taker fee while realized pnl at close charges both legs, so the entry leg was counted twice by everything that reads position.margin - the ROE denominator, and any equity built from margins. Measured on a 100 USD position at 163x closed at a flat price: margin 91.03 with pnl -17.93, i.e. 26.90 accounted for a round trip that really costs 17.93. position.margin is now the amount you sent; position.feeOpen still reports the entry leg for a cost breakdown, and the fee is charged exactly once, inside realized pnl. Positions opened before this keep the margin they were filled with."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "The fee itself did not change: taker rate per side on NOTIONAL (crypto 0.055%), so leverage raises the fee only by raising the position - the rate is flat, and above ~181x it is lowered so a round trip can never exceed 20% of margin. Realized pnl is still qty*(exit-entry)*dir - qty*(entry+exit)*rate - funding."
     }
    ]
   },
   {
    "date": "2026-09-07",
    "version": "2.2.0",
    "title": "Regional quotes",
    "changes": [
     {
      "type": "added",
      "breaking": false,
      "text": "GET /api/latam/ar - dólar cripto for Argentina: the peso price of USDT on every exchange operating there, fees included, sorted by buy price, with the median, the best venue to buy and to sell, the reference dollars (oficial, blue, MEP, CCL, tarjeta) and the gap in percent, plus an hourly history MarginPad collects itself. Human page: /dolar-cripto/."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /api/latam/br - Bitcoin and USDT in reais on every exchange operating in Brazil, fees included, with each venue's ágio over BTC/USD × the commercial dollar (Banco Central PTAX as fallback). Human page: /bitcoin-hoje/."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "Both are plain JSON rather than the {ok,data,ts} envelope, keyless, cached 60 s at the edge, and carry stale:true when the upstream is down and the last known quote is served. Nothing on /api/v1/* or /api/bot/* changed."
     }
    ]
   },
   {
    "date": "2026-09-05",
    "version": "2.1.0",
    "title": "Limit orders",
    "changes": [
     {
      "type": "added",
      "breaking": false,
      "text": "POST /v1/open accepts type:\"limit\" with limit_price. The order rests server-side and fills at ITS OWN price - not at the price the engine happened to notice the cross - so a fill detected late is still priced correctly. Fills are found from the 1m high/low, which catches a wick that retraced between two checks; the position is stamped with the minute the market actually reached the level. The level may sit on EITHER side of the market: below it the order behaves as a classic limit, above it as a breakout entry (what other venues call a trigger order). Either way it waits for the market to reach the level and fills at the level - it is never turned into a market order behind your back."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /v1/orders lists resting orders plus the last 20 that filled, expired or were cancelled (with the position id a fill created). POST /v1/cancel_order {order_id} cancels one. Orders are good until cancelled with a 30-day expiry, 20 resting per account."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "client_order_id works on limit placement too: a retried POST returns the order the first call created (idempotent: true) instead of resting a second one."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "sl and tp on a limit order are side-checked against the LIMIT price, not the live price - that is the entry the order will actually get. One-way mode and the open-position caps are re-checked at fill time, and an order blocked by them is closed with a readable note rather than resting forever."
     }
    ]
   },
   {
    "date": "2026-08-19",
    "version": "2.0.0",
    "title": "Bot API v2: correct fees, per-key metering, idempotency, MCP",
    "changes": [
     {
      "type": "changed",
      "breaking": false,
      "text": "Fills are at the live price (no simulated slippage), so a new position opens at 0.00 unrealized. The entry-leg taker fee is deducted from margin at fill and reported as position.feeOpen; unrealized_pnl_usd is gross minus funding, and the exit leg settles at close. Realized pnl at close is unchanged: qty*(exit-entry)*dir - qty*(entry+exit)*rate - funding."
     },
     {
      "type": "fixed",
      "breaking": false,
      "text": "POST /v1/close_all charged no taker fee and no funding, while /v1/close charged both. Closing through close_all was free. Both paths now settle identically: pnl = qty*(exit-entry)*dir - qty*(entry+exit)*feeRate - funding, floored at -margin."
     },
     {
      "type": "fixed",
      "breaking": false,
      "text": "Fees ignored the asset class on server-side fills: every market was charged the crypto rate of 0.055% per side. A EURUSD position paid 6.9x its real cost. Stocks, forex, metals and indices now use their own rate, and the rate is a server-held field a client sync can no longer overwrite."
     },
     {
      "type": "fixed",
      "breaking": false,
      "text": "unrealized_pnl_usd on open positions was GROSS - it ignored the round trip you would pay to exit. Every open position looked better than it could possibly settle, and a strategy tuned on it was tuned on a number that does not exist. It is now net of fees and funding, and the new fees_usd / funding_usd fields show the deduction."
     },
     {
      "type": "fixed",
      "breaking": false,
      "text": "/v1/account reported realized_pnl_usd, closed_trades, wins, losses and win_rate_pct from the position journal, which is capped at 100 trades - so anyone past their 100th trade saw a truncated number labelled as lifetime. These now come from the trim-proof lifetime accumulator."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "client_order_id on POST /v1/open. Retrying after a network timeout returns the position the first call created (idempotent: true) instead of opening a second one. Strongly recommended for any unattended bot."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "POST /v1/sltp - move the stop-loss or take-profit on an open position. Previously the only way to trail a stop through the API was to close and reopen, which changed the entry and paid a full round trip."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /v1/markets - every tradable symbol with asset_class, max_leverage and taker_fee_pct. GET /v1/balance - balance, equity, free margin. GET /v1/trades - the full closed-trade ledger with paging, beyond the 100-trade journal cap. GET /v1/time - server clock and drift, keyless."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "GET /v1/positions now supports ETag / If-None-Match and ?since=<unix ms>. The ETag covers structural state (ids, status, qty, sl, tp, exit) and deliberately not mark_price, so a poll that changed nothing costs a 304 instead of a full body. Take live prices from the keyless /v1/price, which costs no rate budget."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset on every response, including the 429, which also carries Retry-After."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "API keys: many per account instead of one. Each key has a name, its own usage counter and its own rate-limit budget, and can be revoked on its own (usage history is kept). Manage them with POST /api/bot/key {act: list|create|rename|revoke}."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "/api/bot/v2/* - the same API wrapped in the standard {ok, data, ts} envelope, with {ok:false, error:{code, message}, ts} on failure. Same paths, same parameters, same key."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "Remote MCP server at /mcp - 17 tools covering market data, calculators and paper trading, so Claude, ChatGPT and Cursor can attach natively instead of being told how to call REST."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "WebSocket push at wss://marginpad.io/api/bot/v2/stream?api_key=… - position opened/updated/closed events and mark prices, so a bot learns its stop fired in about two seconds instead of on its next poll. While you are connected the stream also drives the server-side SL/TP/liquidation sweep on the same cadence. Up to 3 sockets per account."
     },
     {
      "type": "fixed",
      "breaking": false,
      "text": "Closing a position took three sequential round trips to the trading store and priced up to twelve symbols, so a close on a liquid pair could wait on an illiquid one. Send symbol in the close body and it is now one round trip and one price. Measured: a round trip is ~20ms from Europe but ~198ms median and ~368ms at p95 from Singapore, and a cold price feed is ~400ms."
     },
     {
      "type": "fixed",
      "breaking": false,
      "text": "The read endpoints (positions, account, balance) fetched fresh upstream prices on every call, paying a cold-fetch penalty on the 70% of traffic that is polling. They now use the same 5s price cache the website uses. Fills - open, close, close_all - still demand a fresh price, because a stale fill is a wrong fill."
     },
     {
      "type": "fixed",
      "breaking": false,
      "text": "The website offered leverage the server refused: up to 1000x on stocks, 100x on metals and indices, 200x on forex, while the server clamps those to 50x, 20x and 50x. The ticket therefore displayed a liquidation price for leverage the fill never used. The interface now mirrors the server exactly across all 157 symbols. This affects the site, not the API - /v1/markets always reported the true server cap."
     },
     {
      "type": "fixed",
      "breaking": false,
      "text": "The OpenAPI spec described the /v1/open body as {margin, leverage}; the API has always read margin_usd. Code generated from the old spec could not open a position. The spec now carries real request and response schemas for every paper-trading path instead of an empty \"ok\"."
     },
     {
      "type": "added",
      "breaking": false,
      "text": "Plans. Free stays the full product: all market data keyless, full paper trading, and the WebSocket stream - 120 requests/minute per key, 3 keys, 50 open positions. Premium raises those to 600/minute, 10 keys and 200 open positions. Streaming is free on both tiers deliberately, because gating it would push everyone back onto polling, which costs us more than the stream does. GET /v1/usage reports your plan and what is left in the current window."
     },
     {
      "type": "unchanged",
      "breaking": false,
      "text": "/api/bot/v1/* response shapes are FROZEN. Existing bots keep working: nothing was renamed or removed, and the fixes above only change values that were wrong. New fields are additive."
     }
    ]
   }
  ]
 },
 "ts": 1789502479649
}