# Changelog

All notable changes to `@ostium/builder-sdk` will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.0] - 2026-09-16

### Added

- `previewOpenTrade(params)` — pre-trade preview for order tickets, returning everything the panel renders before a trade is signed: execution price after dynamic spread, the fee breakdown (maker/taker split, oracle fee, and the configured builder fee), collateral left backing the position, position size, resulting liquidation price, effective max leverage, and the exposure-limit check. Values derive from `@ostium/formulae`, the same math the contracts and ostium.io run, so an integrator's ticket agrees with the official frontend.
  - `warnings[]` covers selected preflight conditions — collateral bounds, leverage bounds, minimum position size, fees exceeding collateral, exposure limits, and market or day-trading closure — and `isValid` is simply `warnings.length === 0`. It is not exhaustive: chain state moves between preview and submission, so keep normal error handling around `openTrade`.
  - Note the parameter shapes differ from submission: the preview takes numbers and `isLong`, while `openTrade` takes decimal strings and `buy`.
  - One deliberate divergence from the frontend: liquidation price is computed from the post-impact execution price, which is what the contract records as the trade's open price, while the frontend computes it from the pre-impact bid/ask. At typical spreads the two differ by a few dollars on a five-figure price; this SDK reports the one the chain will use.
- `getVaultBalance()` — protocol vault balance in USDC, cached for 60 seconds. Required by the exposure-limit check and previously unreadable through the SDK.
- `getOnboardingStatus(params?)` — what still stands between a trader and their first trade (USDC approval, delegate registration, gasless setup), each outstanding step carrying the transaction that clears it, plus `needsFunding` for the case no transaction can fix. Replaces hand-rolling the sequence across three separate tx-builders.
- `liquidationPrice()` and `pnl()` — standalone trading math as pure functions over plain numbers, for bots and backtesters that need the numbers without constructing a client.
- `OstiumSubmissionPendingError` and `OstiumErrorCode.SUBMISSION_PENDING` — raised when an operation was submitted but its outcome is unknown. Carries `userOpHash`.
- `llms.txt` (published with the package) and `.claude/skills/ostium-builder-sdk/SKILL.md` — a flat, prescriptive API reference written for AI coding agents.
- `examples/` — three runnable programs: an order ticket, a live positions view, and a complete trade loop. The first two need no credentials.

### Changed

- `Fill.action` / `OrderAction` now include `'TopUpCollateral'`; `Fill.type` / fill `OrderType` now include `'TOP_UP_COLLATERAL'`.
- Gasless submission now goes through `sendUserOperation()` + `waitForUserOperationReceipt()` rather than `sendTransaction()`, and is gated on the UserOperation receipt's `success` flag. See the corresponding entry under Fixed.
- Gasless submission now picks one of two flows, so bringing your own bundler — or your own sponsorship policy — keeps working:
  - **Sponsored** — when `pimlicoUrl` is an Ostium sponsorship endpoint *and* no `sponsorshipPolicyId` is set. No paymaster client is attached, fees are zero, and gas limits are supplied rather than estimated, because a bundler will not estimate at a zero `maxFeePerGas`.
  - **Classic** — everything else, including your own Pimlico URL. A Pimlico paymaster client carrying the policy, Pimlico's fee quote, and bundler-estimated gas limits. This is what 0.7.x ran for every gasless submission. Nothing sponsors third-party operations for free, so the zero-fee shortcut would be rejected there outright.
  - The endpoint is matched on host and path rather than compared to the default constants, so a staging or self-hosted deployment of the same service still resolves to the sponsored flow, and no extra config flag is needed.
- The default sponsorship endpoint is now `/v1/sponsor` instead of the legacy `/v1/pimlico/sponsor` alias. `DEFAULT_PIMLICO_URL` / `DEFAULT_PIMLICO_URL_TESTNET` still resolve, now as deprecated aliases of `DEFAULT_SPONSOR_URL` / `DEFAULT_SPONSOR_URL_TESTNET`.
- Waiting for a gasless receipt now times out after 120 seconds instead of waiting indefinitely, and polls every 500ms rather than 100ms — the SDK can have many submissions in flight against a shared, rate-limited endpoint.
- Pair queries now fetch `makerFeeP`, `makerMaxLeverage`, `fee { oracleFee, minLevPos }`, and `group { minLeverage, longCollateral, shortCollateral, maxCollateralP }`. These are the inputs to the opening-fee and exposure-limit formulas and were not previously requested.

### Fixed

- Gasless submissions no longer report a reverted trade as a success. A UserOperation whose inner call reverts still lands on-chain as a *successful* transaction — the EntryPoint catches the revert and emits `UserOperationRevertReason` — so `sendTransaction()`, which resolves on the transaction receipt, returned a transaction hash for a trade that never opened. The submitter now inspects the UserOperation receipt's `success` flag and throws `OstiumError` with code `CONTRACT_ERROR`, decoding the receipt's `reason` against the trading ABI to name the actual contract error.
- A gasless receipt timeout no longer looks like a failed submission. The bundler has already accepted the operation and it can still land, so reporting a generic failure invited a retry that opened the position twice. Timeouts now throw `OstiumSubmissionPendingError` (code `SUBMISSION_PENDING`) carrying `userOpHash`, so callers can reconcile instead of resubmitting.
- ERC-4337 failure codes are no longer matched as a bare `aa` substring. `a` is a hex digit, so revert data, addresses and transaction hashes contain `aa` constantly, and a network error carrying a hex payload was classified as a contract revert — sending callers to debug a revert that never happened. Matched as a whole token now.
- `sponsorshipPolicyId` is no longer silently dropped. It only ever travelled through `paymasterContext`, which requires a paymaster client, so a policy-scoped caller on the sponsored flow would have lost attribution, limits and billing with nothing pointing at the cause. Setting it now selects the classic paymaster-attached flow — both where the policy can actually be applied, and exactly what 0.7.x did.

### Upgrading

No changes are required. Every 0.7.1 export still resolves, all five constructors accept their existing arguments, and a gasless config that sets `sponsorshipPolicyId` keeps the 0.7.x submission path unchanged. Three things are worth knowing:

- A gasless config with **no** `sponsorshipPolicyId` moves to the sponsored flow against Ostium's endpoint. Submission is now gated on the UserOperation receipt, so a trade that reverts on-chain raises `OstiumError` instead of returning a transaction hash. Code that treated a returned hash as "opened" was already wrong in that case and will now see the error instead.
- A gasless receipt wait can now end in `OstiumSubmissionPendingError` after 120 seconds rather than hanging. Do not resubmit on it — the operation can still land; reconcile against `getOpenPositions()` or `getOrders()`.
- If you override the subgraph endpoint (`subgraphUrl`), it must serve the pair fields added in this release — the pair query requests them unconditionally, so an older deployment fails every pair read, not just the preview.

## [0.7.1] - 2026-08-12

### Added

- `extractOrderIdFromReceipt(receipt, trader?, action?)` gains two optional filters for receipts that bundle several operations (e.g. an ERC-4337 `handleOps`). `trader` restricts matching to initiated-event logs whose indexed trader topic equals that address, avoiding wrong correlations when a receipt contains multiple users' operations; only topics carrying an indexed trader can match under the filter — the pre-V2 `PriceRequested(uint256,bytes32,uint256)` carries none, so parsing those receipts requires omitting the argument. `action` (`'any'` (default) | `'open'` | `'close'`) scopes the match to an open or close order id, so a receipt carrying the trader's close ordered before their open no longer hands the close's id to a caller attaching the open's. The scan also continues past matching logs whose `topic1` is absent instead of returning `undefined` early, which masked a valid order id on a later log.
- `streamAccountUpdates()` snapshots now include `closeExecutions` per trader. Close executions surface `MarketCloseExecutedV2` economics (`px`, `percentProfit`, `usdcSentToTrader`, `percentageClosed`, `isFullClose`) from contract logs when the current stream state can attribute the trade, with subgraph close-order records as the fallback source. Executions dedupe on `orderId` plus a `source:orderId:tradeId` composite — never on the bare `tradeId`, which every close of a trade shares — so multiple partial closes of the same trade each surface as their own entry while the same close arriving from both the contract-log overlay and the subgraph still collapses to one.
- `OpenOrder` now includes `ntl`, `collateralUsed`, and `leverage`, and derives `szi` from notional and limit price when the subgraph returns `tradeNotional: "0"` for resting limits.
- SDK GraphQL, REST, and price WebSocket requests now send `User-Agent: ostium-builder-sdk/0.7.1` for traffic attribution.

### Changed

- Pair-list and live-price reads now use a 5-second in-flight TTL memo, so concurrent `getPairs()`, `getAllPrices()`, `getOpenPositions()`, `getSimSlippage()`, and `getSimOrderbook()` calls share the same short-lived upstream fetches instead of repeatedly hitting the subgraph and `/v1/prices`.
- `streamAccountUpdates()` now applies exponential poll backoff after snapshot failures (1, 2, 4... ticks, capped at 10) and respects upstream `Retry-After` metadata when present (parsed by the new `retryAfterMsFromError()`, an iterative walk over `error.cause` that is safe on cyclic chains). Live-price seeding errors are now surfaced to the stream `onError` handlers instead of being silently swallowed. Backoff throttles the subgraph snapshot fetch only: the missed-event chain-log sweep reads execution logs straight from the chain RPC and runs on every tick regardless of subgraph health — it is the safety net for a subgraph outage overlapping a WebSocket reconnect gap — and its chain tip is tracked independently of the snapshot fetch, so a snapshot rejection no longer discards an already-fetched block number.

### Fixed

- Ghost pending market orders are no longer surfaced. The subgraph occasionally misses a market order's terminal event, leaving it `isPending` forever while on-chain it was executed or cancelled and fully unregistered — cancelling such an order reverts with `NoTradeToTimeoutFound`. `getOrders()`, `getBuilderOrders()`, and `streamAccountUpdates()` now verify pending market orders older than the on-chain timeout window (~13s, checked with a 120s margin) against `reqID_pendingMarketOrder` on TradingStorage: unregistered orders (ghosts) are dropped, while stale-but-registered orders stay visible — those consume the trader's pending-order slots (`MaxPendingMarketOrdersReached`) and must be cancelled by the user via the market-timeout call. Fresh in-flight orders and limit orders pass through with no extra RPC cost; verification failures fail open.
- `streamAccountUpdates()` no longer treats raw subgraph `closePercent` values like `"5000"` as full closes. Close percentages are 2-decimal fixed point on both the subgraph and `MarketCloseExecutedV2.percentageClosed` — a full close is `10000`, a 1% close is `100` — so they are now divided by 100 before the full-close comparison. Partial closes surface in `closeExecutions` with the correct `percentageClosed` instead of removing the position.
- Initiated stream overlay positions now keep `position.idx` at `-1` until an executed-open event provides the real on-chain trade slot. The previous initiated state copied the order id into `idx`, which looked actionable but belonged to the wrong id space for close/TP/SL/collateral calls.

## [0.7.0] - 2026-07-29

### Changed

- Gasless key-mode factories (`createSelfAndGasless`, `createDelegatedAndGasless`) accept an optional `safeAddress` — the smart-account address from a previous client's `getSmartAccountAddress()`. Supplying it skips permissionless's on-chain counterfactual-address derivation (one `eth_call`, ~500ms on a public RPC), making gasless construction network-free. The address is deterministic per key + chain.
- Client construction no longer blocks on a subgraph round trip. `OstiumSubgraphClient.create()` (and therefore every `OstiumClient` factory, e.g. `createSelfAndSelf`) previously awaited a full pair-list fetch (~2-3s) that build-only consumers never need; the pair cache now loads lazily on first use. `streamPrices(pairIds)` called before any data method now connects immediately and applies the pair filter once the pair list resolves in the background, instead of throwing `Pair not found` on the cold cache.

### Fixed

- `OpenOrder.idx` from `getOpenOrders()` now carries the on-chain limit slot index instead of the subgraph's global `orderId`. `cancelOrder({ type: CancelOrderType.Limit })` and `modifyOrder()` feed `idx` straight into the Trading contract's `uint8 index` argument, so the global order counter (e.g. `148843`) overflowed the 0-255 range and every limit-order cancel and price/TP/SL edit threw viem `IntegerOutOfRangeError`. The formatter now parses the slot index from the trailing segment of the subgraph limit id (`<trader>_<pairIndex>_<index>`).
- `Fill.oid` / `Order.oid` is now documented and enforced as the on-chain keeper order id (base-10 numeric string) for both fast-overlay and subgraph-indexed entries. The previous docstring incorrectly described it as a composite hex subgraph id; the formatter now canonicalizes numeric ids so the two sources always share one comparable id space.

- `extractOrderIdFromReceipt()` now decodes the order id from the `MarketOpenOrderInitiated`, `MarketCloseOrderInitiated`, and `MarketCloseOrderInitiatedV2` events the current Trading contract emits. It previously only scanned for `PriceRequested` (a pre-V2 contract event, still supported for old receipts) and therefore returned `undefined` for every receipt produced by the current contracts.
- Fast-overlay trades in `streamAccountUpdates()` (both optimistic entries and `MarketOpenExecuted`-confirmed entries) now seed their rollover baseline from the pair's rollover accumulator at the open block. They previously seeded `rollover: "0"`, charging the pair's entire rollover history against a brand-new position — inflating `cumRollover` and pulling the estimated liquidation price closer than it should be until the subgraph-indexed trade replaced the overlay entry.
- `OstiumPriceStream.subscribe()` / `.unsubscribe()` no longer drop the filter message when the socket is still connecting; it is sent on `open` instead. This surfaced with the lazy pair cache above: `streamPrices(pairIds)` on a cold cache subscribes as soon as the pair list resolves, which can beat the WebSocket handshake and leave the consumer on the unfiltered feed.
- The account-updates WebSocket now retries reconnection indefinitely (2s delay) instead of inheriting viem's default of 5 attempts, after which the stream would go permanently silent — surfacing only an `onError` while snapshots quietly stopped updating.

## [0.6.0] - 2026-07-23

### Changed

- **BREAKING**: Migrated the default builder API endpoints from `https://builder.ostium.io` to `https://builder.prod.bedrock.ostium.io` (subgraph, Pimlico sponsor, prices/OHLC, and WebSocket stream). `builder.ostium.io` is being deprecated; clients relying on the old default must upgrade. Consumers passing explicit URLs should update them accordingly.

### Fixed

- **Ghost positions in `streamAccountUpdates()`**: a trade closed before its open was ever observed indexed, whose `MarketCloseExecutedV2` event was missed (e.g. during a WebSocket reconnect), previously lingered in the emitted snapshot forever. Three complementary fixes:
  - The account snapshot query now also fetches recently executed close/liquidation orders (15-minute lookback) and tombstones matching overlay entries, so the subgraph poll can clear a ghost even with a dead event socket.
  - New missed-event backfill: each poll sweeps `MarketOpenExecuted` / `MarketCloseExecutedV2` logs over HTTP from the last swept block, so events dropped during a WebSocket flap surface within roughly one poll interval instead of being lost.
  - `executed` overlay entries are no longer exempt from the overlay TTL; an executed trade the subgraph never confirms within the TTL now expires instead of ghosting indefinitely.

## [0.5.0] - 2026-07-13

### Changed

- `streamAccountUpdates()` now fetches the account snapshot for all subscribed traders in **one batched subgraph query** per poll instead of one query per trader. If the shared 1000-row window saturates (any entity set returns a full page), the poll transparently falls back to per-user queries so a busy account still cannot starve the others.
- Default `pollIntervalMs` raised from `700` to `3000`. Sub-second open/close confirmations come from the Alchemy event watchers (which also trigger an immediate poll), so the timer poll only paces reconciliation of changes with no watcher (limit fills, partial closes, TP/SL edits, liquidations). Pass `pollIntervalMs` to restore a faster cadence.
- The per-poll Alchemy `eth_blockNumber` call is now served from a 30-second cache. Combined with the changes above, a 20-user stream drops from ~1,700 subgraph requests/minute to ~20, and steady-state Alchemy HTTP calls drop ~15×.

## [0.4.1] - 2026-06-14

### Added

- `streamAccountUpdates()` now accepts `user` as an address array, allowing one or more trader addresses on a single stream (one WebSocket, one poll loop, one price feed). Emitted snapshots are keyed by normalized trader address with `{ positions, orders, limits }` per trader.
- `OstiumAccountUpdatesStream.users` getter returning the subscribed trader addresses.
- Optional `user` argument to `OstiumAccountUpdatesStream.addOptimisticOpen(params, submission?, user?)` to attribute an optimistic open to a specific subscribed address. Required when streaming multiple addresses; defaults to the sole subscribed address otherwise.

### Fixed

- Multi-user account streams now scope optimistic reconciliation and pair/index deduplication by trader, preventing same-pair opens or slots from crossing accounts.
- Multi-user account stream snapshots now fetch each subscribed trader separately before merging, so one busy account cannot consume the shared 1000-row subgraph window.

## [0.4.0] - 2026-06-11

### Added

- Added this changelog.
- Added `sets` pagination support to `getCandles()`.
- Added `getOrders()` filters for global orders, builder address, status, pair ids, and execution time (`start` / `end` as Unix seconds UTC, inclusive bounds on `executedAt`).
- Added `getBuilderOrders(builder, params?)` — fetches builder-tagged open orders plus sibling close/TP/SL orders on the same positions. `limit` caps phase-1 results only; phase-2 siblings are appended without a cap.
- Added `builder` to returned `Fill` and `Order` objects.
- Added `ntl` (USD notional) to returned `Fill` and `Order` objects.
- Added `trader` to returned `Fill`, `Order`, `Position`, and `OpenOrder` objects.
- Added `timestamp` (execution time, Unix seconds UTC — subgraph `executedAt`) to returned `Fill` and `Order` objects.
- Added `MIN_OPEN_SIZE_USD` for the fixed $5 minimum open size.
- Added per-trade optional `builder.address` / `builder.feeBps` overrides on `openTrade()`; omitted fields fall back to client config.
- Added `openFee` and `closeFee` (bps) to `Pair` — `openFee` is `takerFeeP / 10_000` plus the configured builder fee; `closeFee` is always `0` as there's no closing fees on Ostium currently.
- Added `streamAccountUpdates()` for low-latency account confirmations using subgraph polling, Alchemy contract-log overlays, and live price repricing for open-trade PnL.
- Added optimistic market-open overlays to `streamAccountUpdates()`, receipt `orderId` extraction via `extractOrderIdFromReceipt()`, and `attachOrderId()` reconciliation for lower-latency confirmations.
- Added `alchemyApiKey` as a client option for account confirmation streams.
- Added account update snapshots with SDK-formatted `Order`, `OpenOrder`, and `PairPosition` values.
- Added `schedule` (market hours — `timezone`, `openingHours`, `alwaysOpen`) to `Pair` returned by `getPairs()`, and to `PriceData` / `PriceTick` from the live price feed.
- Added background SDK usage attribution: submissions that target the Trading contract report their transaction hash to the builder API (`POST /v1/trade`) as a fire-and-forget request that never blocks or affects trading calls.

### Changed

- Updated the default mainnet subgraph URL to `https://builder.ostium.io/v1/subgraph/gn`.

### Fixed

- Removed the SDK-side `openTrade()` maximum leverage cap so contract-side validation is authoritative.
- Removed the internal minimum-open-size config override path.

## [0.3.1]

- Current published package version when this changelog was introduced.
