# What is Multiply

Empowering onchain front-ends to natively offer their users up to 10x exposure on prediction market positions.

<figure><img src="/files/DLZeDIFxXk6bmkgDkmn2" alt=""><figcaption></figcaption></figure>

**Dimes Multiply** <mark style="color:$info;">is a middle-layer protocol that</mark> **gives trading terminals, wallets, and apps,** <mark style="color:$info;">the ability to</mark> **natively offer users leveraged exposure on external prediction markets**, <mark style="color:$info;">specifically Polymarket, without building internal leverage infrastructure.</mark>

<mark style="color:$info;">Multiply does one thing:</mark> **extend credit on top of PM positions while managing all liquidity, hedging, and risk.**

<mark style="color:$info;">More specifically,</mark> **Dimes handles:**

* <mark style="color:$info;">liquidity and exposure-aware hedging on Polymarket</mark>
* <mark style="color:$info;">inventory netting across thousands of dynamic user positions</mark>
* <mark style="color:$info;">slippage-bounded exposure sizing</mark>
* <mark style="color:$info;">jump-risk modelling</mark>
* <mark style="color:$info;">operational management of settlement flows</mark>
* <mark style="color:$info;">credit provisioning across thousands of concurrent positions</mark>

**This allows front-ends to focus on what they do best**<mark style="color:$info;">: create fast, intuitive, and complete high-retention trading experiences.</mark>

<mark style="color:$info;">The Multiply architecture is intentionally</mark> **narrow and specialized**<mark style="color:$info;">:</mark>

* <mark style="color:$info;">We do not create our own markets</mark>
* <mark style="color:$info;">We do not host an orderbook</mark>
* <mark style="color:$info;">We do not compete with PM venues and front-ends</mark>
* <mark style="color:$info;">We do not attempt to own user acquisition</mark>

<mark style="color:$info;">We sit in the middle: front-ends on one side, PM venues on the other, and supply the</mark> **liquidity** **+ risk engine that makes safely offering leveraged PM exposure accessible to onchain trading apps.**

<mark style="color:$info;">From a margining standpoint, Multiply uses a dedicated</mark> **Underwriting Facility, an institutionally-sourced guaranteed pool of capital capable of underwriting $50m+ in monthly volume,** <mark style="color:$info;">to finance delta-neutral hedges while handling jump-to-settlement risk by construction.</mark> <mark style="color:$info;">For front-ends, this means guaranteed liquidity with no dependency on pool-based TVL and the availability uncertainty that comes with it.</mark>

<figure><img src="/files/Ak3K4UGenczWz4LV5AtO" alt=""><figcaption></figcaption></figure>


# Levered Exposure Demand

Why leverage matters for prediction market growth.

<mark style="color:$info;">Prediction markets express</mark> **probabilities**, <mark style="color:$info;">which means they naturally have</mark> **capped upside**. <mark style="color:$info;">A YES share priced at $0.34 can at most go to $1.00. Large belief shifts translate into</mark> **modest raw returns.**

<mark style="color:$info;">This is both the strength and limitation of prediction markets:</mark>

* **Strength**: <mark style="color:$info;">prices reflect conviction</mark>
* **Limitation**: <mark style="color:$info;">conviction cannot be</mark> **magnified** <mark style="color:$info;">without leverage</mark>

<mark style="color:$info;">Every major tradable asset class saw volume and participation</mark> **inflect once credit entered the system.** <mark style="color:$info;">Prediction markets sit on the same curve today.</mark>

**"Let me express a strong view without needing 10× the capital"** <mark style="color:$info;">is a universal trader expectation, proven across asset classes.</mark> **Multiply delivers it — safely and deterministically — while removing any burden on terminals to source additional liquidity or carry risk.**


# Supported Markets

Markets are supported based on event type, liquidity microstructure, and safe margining.

<mark style="color:$info;">Prediction markets fall into three categories:</mark>

* <mark style="color:$info;">Markets that can be hedged continuously with</mark> **no jump-to-settlement risk**,
* <mark style="color:$info;">Markets that trade normally but have a short,</mark> **clearly defined jump-to-settlement window**, <mark style="color:$info;">and</mark>
* <mark style="color:$info;">Markets that</mark> **can resolve at any time**, <mark style="color:$info;">with no reliable hedge at any point in their life.</mark>

<mark style="color:$info;">A</mark> **jump-to-settlement** <mark style="color:$info;">is a discrete move from a tradable probability to a final 0 or 1 outcome without an opportunity to unwind at intermediate prices.</mark>

<mark style="color:$info;">Multiply supports the first two categories, which account for the majority of trading volume on Polymarket.</mark>

**Examples:**

1. **No jump to resolution: Fully covered by the Facility throughout lifecycle.**\ <mark style="color:$info;">"Will the S\&P 500 close above 6,500 on Friday?"</mark>
2. **Clear, short jump-to-resolution window**\ <mark style="color:$info;">"Will the Federal Reserve raise interest rates at the next meeting?"</mark>\ <mark style="color:$info;">Long pre-meeting hedgeable phase, then a very tight FOMC announcement window where jump is introduced.</mark>
3. **Full jump-to-settlement over the whole life: Not supported by Dimes**\ <mark style="color:$info;">"Will a new Supreme Court justice be confirmed in 2026?"</mark>\ <mark style="color:$info;">Can settle unpredictably.</mark>

<mark style="color:$info;">Beyond market structure, Multiply applies a clear framework for structuring its exposure. We evaluate the market's liquidity and reliability, eligibility criteria for safe margining, and the sizing caps required to keep execution efficient. All market greenlighting and sizing is fully autonomous.</mark>

<details>

<summary><strong>Venues</strong></summary>

<mark style="color:$info;">Multiply currently provides leveraged exposure on</mark> **Polymarket** <mark style="color:$info;">and handles all underwriting, collateralization, and settlement.</mark> <mark style="color:$info;">Future integrations may include Kalshi, Opinion, Hyperliquid, and Limitless.</mark>

<mark style="color:$info;">Additional venues will be added if they meet the same requirements of:</mark>

* <mark style="color:$info;">reliable price feeds</mark>
* <mark style="color:$info;">consistent orderbook activity</mark>
* <mark style="color:$info;">predictable, objective settlement</mark>
* <mark style="color:$info;">safe hedging pathways</mark>

</details>

<details>

<summary><strong>Market Types</strong></summary>

<mark style="color:$info;">Multiply supports binary and simple categorical markets. These markets are enabled only when the underlying venue provides:</mark>

1. **Sufficient tradable liquidity:** <mark style="color:$info;">minimum thresholds for usable depth, effective spread, order book stability, fill rate, update frequency, volume, and holder distribution;</mark>
2. **Predictable resolution with continuous price discovery:** <mark style="color:$info;">markets must exhibit stable pricing dynamics over a sufficient horizon, maintain prices within eligible ranges, and have current exposure within per-market and per-side caps.</mark>

</details>

<details>

<summary><strong>Per-Market Caps and Exposure Limits</strong></summary>

<mark style="color:$info;">To operate safe leverage on capped-payoff instruments, Multiply uses strict exposure control:</mark>

* **Market-Level Exposure Cap:** <mark style="color:$info;">Total outstanding notional across all users is capped at a fraction of hedgeable depth across both sides, ensuring unwindability even during stress.</mark>
* **Side-Level Caps (YES vs NO):** <mark style="color:$info;">YES and NO exposures have independent caps to avoid unbalanced books, preventing skewed risk profiles, degraded hedgeability, and situations where one leg becomes too thin to support exit liquidity.</mark>

</details>


# Position Management

Real-time controls to maintain solvency and enforce the trader's intended exposure.

#### Margining and Collateral Lock

<mark style="color:$info;">When a position is opened:</mark>

* <mark style="color:$info;">Multiply locks the initial margin required to support the leveraged exposure.</mark>
* <mark style="color:$info;">Additional buffer may be required depending on liquidity conditions, probability region, and modeled unwind cost.</mark>
* <mark style="color:$info;">Locked collateral remains onchain and cannot be withdrawn while the position is open.</mark>

<mark style="color:$info;">Multiply continuously recomputes required margin based on:</mark>

* <mark style="color:$info;">current venue prices</mark>
* <mark style="color:$info;">exposure size and leverage</mark>
* <mark style="color:$info;">leverage decay schedule</mark>
* <mark style="color:$info;">modeled execution cost to unwind at current depth and spread</mark>

**Marking to market and PnL updates**

<mark style="color:$info;">Multiply updates PnL and margin state in real time as the underlying prediction market price changes.</mark>

* <mark style="color:$info;">When the price moves in the user's favor, their collateral buffer increases.</mark>
* <mark style="color:$info;">When the price moves against them, their buffer shrinks.</mark>

<mark style="color:$info;">The front-end receives a continuous feed of:</mark>

* <mark style="color:$info;">current state of the leveraged position</mark>
* <mark style="color:$info;">unrealized PnL</mark>
* <mark style="color:$info;">remaining collateral buffer</mark>
* <mark style="color:$info;">liquidation threshold</mark>

<mark style="color:$info;">The front-end displays this information to users. Multiply maintains the hedge automatically; users do not see or interact with hedging activity. Hedges are static once executed — they are not rebalanced in response to price movement, only modified when exposure changes through user action, leverage decay, or liquidation.</mark>

**Failure and fallback scenarios**

<mark style="color:$info;">If the underlying venue experiences an outage, feed interruption, or delayed resolution:</mark>

* <mark style="color:$info;">Multiply may activate circuit breakers, pausing new entries and adjustments (close-only mode).</mark>
* <mark style="color:$info;">Existing positions remain marked to the last known valid price.</mark>
* <mark style="color:$info;">If unwind execution exceeds slippage bounds, protected unwind mode is engaged — execution proceeds incrementally, paced against available depth.</mark>
* <mark style="color:$info;">If resolution is delayed, positions remain open until the venue publishes an authoritative result.</mark>
* <mark style="color:$info;">If the venue provides a corrected settlement result, Multiply adjusts PnL accordingly.</mark>

<mark style="color:$info;">If a hedge cannot be executed or unwound, Multiply refuses new entries and may engage protected unwind mode or escalate per its incident response framework.</mark>


# Position Lifecycle

How positions are opened, monitored, hedged, and settled.

<mark style="color:$info;">This section describes how a leveraged prediction market position moves from creation to settlement. All user actions occur through the originating front-end. Dimes executes and maintains the underlying hedge through Multiply.</mark>

{% stepper %}
{% step %}
**Opening a Position**

<mark style="color:$info;">The user selects:</mark>

* <mark style="color:$info;">a supported prediction market</mark>
* <mark style="color:$info;">a direction (YES or NO)</mark>
* <mark style="color:$info;">a leverage level</mark>
  {% endstep %}

{% step %}
**The terminal requests a quote from Multiply, passing information such as:**

* <mark style="color:$info;">market and trader</mark>
* <mark style="color:$info;">direction</mark>
* <mark style="color:$info;">requested leverage</mark>
* <mark style="color:$info;">notional demand and collateral value</mark>
  {% endstep %}

{% step %}
**Multiply evaluates:**

* <mark style="color:$info;">current venue price and market microstructure</mark>
* <mark style="color:$info;">hedgeability of requested size within slippage bounds</mark>
* <mark style="color:$info;">required collateral and margin</mark>
* <mark style="color:$info;">per-market, per-side, and per-user cap headroom</mark>
  {% endstep %}

{% step %}
**Multiply returns a firm executable quote that includes:**

* <mark style="color:$info;">entry price and execution cost</mark>
* <mark style="color:$info;">required collateral</mark>
* <mark style="color:$info;">maximum allowed size (if clipped by caps or depth)</mark>
* <mark style="color:$info;">implied liquidation price</mark>
* <mark style="color:$info;">an expiry timestamp — the offer is only valid for a limited window. The</mark> <mark style="color:$info;">`expires_at`</mark> <mark style="color:$info;">field in the API response indicates the deadline. If the user does not execute before this time, the offer becomes invalid and a new quote must be requested.</mark>
  {% endstep %}

{% step %}
**Hedge-first execution**

<mark style="color:$info;">If the user accepts, the front-end submits an execute call. Multiply executes the hedge on the external venue first.</mark>

* <mark style="color:$info;">Only if the hedge fills within slippage bounds is the position created, user collateral is locked, and the leveraged exposure is established.</mark>
* <mark style="color:$info;">If the hedge fails or slippage exceeds bounds, the order is rejected and no position is created.</mark>

<mark style="color:$info;">After the hedge fills, the position payload exposes both the indicative entry price quoted on the offer (</mark>`entry.price_*`<mark style="color:$info;">) and the actual fill price reported by the venue (</mark> `entry.effective_entry_price_*`<mark style="color:$info;">). The difference is reported as</mark> `entry.effective_slippage_bps` <mark style="color:$info;">— signed basis points, positive when the fill came in worse than the quote and negative when better. These three fields are</mark> `null` <mark style="color:$info;">until the onchain fill is recorded.</mark>
{% endstep %}

{% step %}
**Active monitoring**

<mark style="color:$info;">The position becomes active and appears in the front-end's portfolio view. Once active:</mark>

* <mark style="color:$info;">Multiply monitors margin, venue health, and eligibility continuously</mark>
* <mark style="color:$info;">Limits and maximum leverage evolve dynamically based on market conditions</mark>
* <mark style="color:$info;">The market may enter close-only if any eligibility threshold is breached</mark>
  {% endstep %}

{% step %}
[**Leverage decay (J-factor)**](/leverage-and-risk/leverage-decay-j-factor)

* <mark style="color:$info;">J-factor is Multiply's dynamic risk engine. It continuously monitors depth profile, odds dynamics, open interest microstructure, and resolution conditions, defining when leverage should be reduced.</mark>
* <mark style="color:$info;">Leverage reduction is triggered only when microstructure conditions warrant it. On the winning side, this means leverage often remains intact through resolution. On the losing side, deteriorating microstructure typically triggers deleverage earlier, protecting both the user and the facility.</mark>
* <mark style="color:$info;">Each unwind in a position's history (via</mark> <mark style="color:orange;">`expand=unwinds`</mark><mark style="color:$info;">) carries a</mark> <mark style="color:orange;">`reason`</mark> <mark style="color:$info;">— the specific microstructure signal that drove that deleverage, such as</mark> <mark style="color:orange;">`spread_blowout`</mark><mark style="color:$info;">,</mark> <mark style="color:orange;">`depth_decay`</mark><mark style="color:$info;">, or</mark> <mark style="color:orange;">`price_drop_severe`</mark><mark style="color:$info;">. It is</mark> <mark style="color:orange;">`null`</mark> <mark style="color:$info;">for unwinds not driven by the risk engine (e.g. manual admin deleveraging). See the API reference for the full list of reasons.</mark>
  {% endstep %}

{% step %}
**Waiting on the venue to resolve**

* <mark style="color:$info;">A market stops trading before its result exists on chain. A sporting event's market closes when the game ends, but the venue's oracle only publishes the outcome some time later — often hours. In that window the position stays open, no new positions are accepted, and nothing can be redeemed yet.</mark>
* <mark style="color:$info;">The position reports</mark> <mark style="color:orange;">`timing.settlement_state`</mark> <mark style="color:$info;">of</mark> <mark style="color:orange;">`awaiting_resolution`</mark><mark style="color:$info;">, and the market reports the same value. This is normal, not a stuck position — surface it to users as "waiting to settle".</mark>
* <mark style="color:$info;">Once the venue publishes the result the state becomes</mark> <mark style="color:orange;">`settling`</mark> <mark style="color:$info;">and</mark> <mark style="color:orange;">`timing.is_settlement_pending`</mark> <mark style="color:$info;">turns</mark> <mark style="color:orange;">`true`</mark><mark style="color:$info;">. Note that the boolean stays</mark> <mark style="color:orange;">`false`</mark> <mark style="color:$info;">during</mark> <mark style="color:orange;">`awaiting_resolution`</mark><mark style="color:$info;">, so use the state when you need to tell the two apart.</mark>
  {% endstep %}

{% step %}
**Liquidation** **(if triggered)**

* <mark style="color:$info;">Multiply is designed to keep your position alive. The J-factor risk engine works continuously to ease your leverage down as a position deteriorates, giving your collateral room to recover before hard limits are reached. As such, forced liquidations are rare. But the system can't guard against every market move. If your remaining collateral falls to maintenance margin at any point during the position lifecycle, Multiply liquidates to protect what's left: the hedge is unwound on the originating venue and all execution costs are deducted from your collateral.</mark>
  {% endstep %}

{% step %}
**Voided markets**

* <mark style="color:$info;">Occasionally a market is voided — cancelled, forfeited, or closed with no winner (e.g. rescheduled sporting events). When this happens, both YES and NO tokens pay out at $0.50 each via the Conditional Token Framework. The position's mark price updates to $0.50 immediately. The</mark> <mark style="color:orange;">`timing.is_voided`</mark> <mark style="color:$info;">field on the position becomes</mark> <mark style="color:orange;">`true`</mark><mark style="color:$info;">, and</mark> <mark style="color:orange;">`timing.is_settlement_pending`</mark> <mark style="color:$info;">indicates that settlement is incoming. PnL is calculated against the trading venue refund.</mark>
  {% endstep %}
  {% endstepper %}

## Entry vs exit notional

<mark style="color:$info;">A closed position reports two notional figures so you can show the round trip.</mark> <mark style="color:orange;">`entry.notional_usd`</mark> <mark style="color:$info;">is the levered exposure the position opened with (entry shares × entry price).</mark> <mark style="color:orange;">`result.exit_notional_usd`</mark> <mark style="color:$info;">is the volume-weighted notional realized as the position wound down — the sum of shares sold × fill price across every force-unwind plus the final close. Because positions are partially deleveraged over their lifetime at different prices, this is not a single exit price; it is the aggregate of all exit fills. It is distinct from</mark> <mark style="color:orange;">`result.proceeds_usd`</mark><mark style="color:$info;">, which is the de-levered cash equity returned to the wallet after repaying the loan and fees. Exit notional is</mark> <mark style="color:orange;">`null`</mark> <mark style="color:$info;">for</mark> <mark style="color:orange;">`reverted`</mark> <mark style="color:$info;">and</mark> <mark style="color:orange;">`cancelled`</mark> <mark style="color:$info;">positions, where no shares were ever sold.</mark>

## On-chain transaction visibility

<mark style="color:$info;">Every on-chain step above — opening, finalizing, closing, liquidating, settling, force-unwinding, and redeeming — is recorded as a transaction the position owner can inspect. The</mark> [<mark style="color:$info;">position transactions endpoint</mark>](/for-developers/api-and-events/api-reference#position-transactions-sub-resource) <mark style="color:$info;">returns these transactions grouped by operation type, including the on-chain hashes of the nested exchange (CLOB/DEX) fills that hedge the position. Only transactions that landed and finalized on-chain are returned — pending, failed, and reverted attempts are excluded — so the result reflects the transfers that actually moved funds.</mark>

## Reverted opens

<mark style="color:$info;">If an open fails after the on-chain position is created — most often because the hedging exchange order could not be filled — the protocol reverts the open and returns the collateral and origination fee in full. No position is held. The closed-position response carries</mark> <mark style="color:orange;">`close_reason: "reverted"`</mark> <mark style="color:$info;">and a</mark> <mark style="color:orange;">`revert_reason`</mark> <mark style="color:$info;">enum explaining why:</mark>

* <mark style="color:orange;">`exchange_unavailable`</mark> <mark style="color:$info;">— the prediction-market venue was temporarily unavailable (e.g. its matching engine was restarting). This is transient; submitting the same offer again shortly after will usually succeed.</mark>
* <mark style="color:orange;">`slippage_exceeded`</mark> <mark style="color:$info;">— the price moved beyond the offer's slippage tolerance before the order could fill. Re-quote and retry.</mark>
* <mark style="color:orange;">`unknown`</mark> <mark style="color:$info;">— the revert could not be attributed to a specific cause.</mark>

<mark style="color:$info;">`revert_reason`</mark> <mark style="color:$info;"></mark><mark style="color:$info;">is null for any non-reverted close.</mark>

## Deferred closes

<mark style="color:$info;">If you request a close but the market resolves before the remaining shares can be sold on the order book, the close cannot complete as a normal sale. Rather than fail the request, the protocol defers it: the remaining tokens are redeemed when the market settles instead of being sold. While this is pending, the position stays</mark> <mark style="color:orange;">`open`</mark> <mark style="color:$info;">and carries a</mark> <mark style="color:orange;">`close_attempt`</mark> <mark style="color:$info;">object describing the deferral:</mark>

* <mark style="color:orange;">`outcome: "deferred"`</mark> <mark style="color:$info;">— the close was postponed rather than completed.</mark>
* <mark style="color:orange;">`reason: "awaiting_settlement"`</mark> <mark style="color:$info;">— the market resolved before the position could be sold, so the remaining tokens will be redeemed at settlement.</mark>
* <mark style="color:orange;">`deferred_at`</mark> <mark style="color:$info;">— ISO-8601 timestamp of when the close was requested.</mark>

<mark style="color:$info;">`close_attempt`</mark> <mark style="color:$info;"></mark><mark style="color:$info;">is</mark> <mark style="color:orange;">`null`</mark> <mark style="color:$info;">for any open position without a deferred close. When the market settles, the position transitions to a closed/settled state in the usual way and the redeemed proceeds are reported under</mark> <mark style="color:orange;">`result`</mark><mark style="color:$info;">.</mark>


# Fees

How funding is priced for each leveraged position.

<mark style="color:$info;">Multiply uses a simple, predictable fee model that aligns incentives across front-ends, credit partners, and Dimes. A leveraged position incurs two distinct categories of fees:</mark>

1. **Protocol fees**: <mark style="color:$info;">paid to Dimes, covering credit, hedging, and infrastructure.</mark>
2. **External fees:** <mark style="color:$info;">paid to the underlying prediction market (Polymarket) and front-end, independent of Multiply.</mark>

<mark style="color:$info;">Both are deducted from the position's collateral and netted against PnL at close. The sections below break down each in turn.</mark>

***

### 1. Protocol Fees

<mark style="color:$info;">Protocol fees compensate Dimes and credit partners for providing leverage, running hedges, and operating settlement infrastructure. They consist of an entry fee, a continuous time-based fee, and (only on forced closes) a liquidation fee.</mark>

#### Entry Fee

<mark style="color:$info;">A fixed percentage applied to</mark> <mark style="color:orange;">`Collateral × Leverage`</mark> <mark style="color:$info;">at position creation:</mark>

> <mark style="color:orange;">`EntryFee = Collateral × Leverage × f_entry`</mark>

<mark style="color:$info;">where</mark> **f\_entry** <mark style="color:$info;">is between 2.0% and 2.5%, scaled with leverage.</mark>

#### Time-Based Fee

<mark style="color:$info;">A funding-style fee applied continuously on the</mark> \*\*protocol-provided capital \*\* <mark style="color:$info;">(the borrowed portion of notional, i.e.</mark> <mark style="color:orange;">`Notional − Collateral`</mark><mark style="color:$info;">):</mark>

> <mark style="color:orange;">`TimeFee = BorrowedCapital × f_time × TimeElapsed`</mark>
>
> <mark style="color:orange;">`BorrowedCapital = Collateral × (Leverage − 1)`</mark>

<mark style="color:$info;">Where</mark> **f\_time** <mark style="color:$info;">is equivalent to</mark> **0.05% daily rate**<mark style="color:$info;">, charged continuously.</mark>

#### Liquidation Fee

<mark style="color:$info;">A 10% fee assessed on the</mark> **protocol-provided capital** ( <mark style="color:orange;">`Notional − Collateral`</mark>) <mark style="color:$info;">when a position is force-closed. Retained entirely by Dimes to offset hedge slippage, execution impact, and emergency unwind costs.</mark>

#### Total Protocol Fee

> <mark style="color:orange;">`TotalProtocolFee = EntryFee + TimeFee (+ LiquidationFee, if applicable)`</mark>

***

### 2. External Fees (Polymarket and Front-ends)

<mark style="color:$info;">In addition to protocol fees, every position routed through Polymarket pays Polymarket's own</mark> **taker fee** <mark style="color:$info;">on the underlying CLOB trades. These fees are charged by Polymarket directly, are not collected or shared by Dimes, and apply to all market participants, leveraged or not. In addition, front-ends statutory execution fees also apply.</mark>

<mark style="color:$info;">Multiply surfaces the venue fee separately on every quote and position via the</mark> <mark style="color:orange;">`polymarket_trading_fee_bps`</mark> <mark style="color:$info;">field so that end users see the true cost footprint of opening and closing a leveraged trade.</mark>

#### How Polymarket Prices Fees

<mark style="color:$info;">Polymarket charges a per-share fee that varies with the share price. The formula is:</mark>

> <mark style="color:orange;">`fee_per_share = price × feeRate × (price × (1 − price))^exponent`</mark>

<mark style="color:$info;">Three properties of this curve matter:</mark>

* **Fees peak at $0.50 and decrease toward both extremes ($0.01 and $0.99).** <mark style="color:$info;">The</mark> <mark style="color:orange;">`price × (1 − price)`</mark> <mark style="color:$info;">term is the variance of a Bernoulli outcome: Polymarket charges takers in proportion to the adverse-selection risk that market makers bear at each price level.</mark>
* **Fees are charged on both entry and exit.** <mark style="color:$info;">Every position pays the fee twice: once when shares are bought and again when they are sold. The peak effective rates below describe one leg only: round-trip cost can approach double the headline rate, and traders should size leveraged positions with the full round-trip in mind.</mark>
* **Rates differ by market category.** <mark style="color:$info;">Under Polymarket's fee schedule effective March 30, 2026,</mark> **peak effective rates** <mark style="color:$info;">(the rate paid at $0.50, per leg) range from 0% to 1.80%:</mark>

| Category                           | Peak Effective Rate (per leg) | Approx. Round-Trip at 50¢ |
| ---------------------------------- | ----------------------------- | ------------------------- |
| Geopolitics                        | 0%                            | 0%                        |
| Sports                             | 0.75%                         | \~1.50%                   |
| Finance, Politics, Tech            | 1.00%                         | \~2.00%                   |
| Culture, Economics, Weather, Other | 1.25%                         | \~2.50%                   |
| Mentions                           | 1.56%                         | \~3.12%                   |
| Crypto                             | 1.80%                         | \~3.60%                   |

<mark style="color:$info;">Refer to</mark> [Polymarket's fee documentation](https://docs.polymarket.com/trading/fees) <mark style="color:$info;">for the canonical, up-to-date schedule and the exact formula coefficients (</mark> <mark style="color:orange;">`feeRate`</mark><mark style="color:orange;">,</mark> <mark style="color:orange;">`exponent`</mark><mark style="color:$info;">) per category.</mark>

#### How Front-ends Earn

<mark style="color:$info;">Front-ends earn through Polymarket's builder-code program rather than an additional fee charged by Multiply. A partner registers their own Polymarket builder code, Dimes attaches it to the CLOB orders routed through that partner, and Polymarket gathers and pays out builder rewards through its usual builder-fee path. See</mark> [<mark style="color:$info;">Fee Flows</mark>](#id-3.-fee-flows) <mark style="color:$info;">below.</mark>

#### How External Fees Scale With Leverage

**Exeternal fees are charged on the full leveraged notional, not on user collateral.**

<mark style="color:$info;">When a user opens a 10× position with $1,000 of collateral, $10,000 of shares are purchased on Polymarket, and Polymarket assesses fees on that $10,000, not on the $1,000 the user contributed. Fees are paid both at entry (buying shares) and at exit (selling shares), so the round-trip cost is paid twice.</mark>

<mark style="color:$info;">The practical consequence is that</mark> **the cost footprint of opening and closing a position is significantly larger, in absolute terms, than the unleveraged equivalent**, <mark style="color:$info;">even when no odds movement occurs. A position opened and closed at the same price still pays the full round-trip venue fee on the leveraged notional.</mark>

<mark style="color:$info;">Some practical implications:</mark>

* **Holding to resolution avoids the exit fee.** <mark style="color:$info;">Polymarket fees are taker fees on fills; resolution is not a fill. Users with high-conviction views who hold through resolution pay only the entry leg.</mark>
* **Trades that cross the 50¢ midpoint are most expensive.** <mark style="color:$info;">Fees are highest at $0.50 and fall symmetrically toward the extremes. A move from $0.05 to $0.15 incurs a lower round-trip fee than a move from $0.30 to $0.70 of the same absolute size.</mark>

***

### 3. Fee Flows

<mark style="color:$info;">Protocol fees flow entirely to Dimes, and are used to:</mark>

* <mark style="color:$info;">fund hedging operations and routing</mark>
* <mark style="color:$info;">pay interest to credit partners</mark>
* <mark style="color:$info;">support risk modeling, monitoring, and settlement infrastructure</mark>
* <mark style="color:$info;">maintain buffers and safety margins</mark>

<mark style="color:$info;">Front-end partners earn through Polymarket's builder-code program. When a partner registers their own builder code, Dimes attaches it to every CLOB order routed through that partner, and Polymarket gathers and pays out builder rewards on those trades through its usual builder-fee path. These rewards accrue directly to the partner's Polymarket account and never flow through Dimes' books. See</mark> [<mark style="color:$info;">Partner-Configurable Options</mark>](#id-4.-partner-configurable-options) <mark style="color:$info;">below for the full set of partner-level controls.</mark>

<mark style="color:$info;">Venue fees (Polymarket) are paid directly to Polymarket by the position and never flow through Dimes' books.</mark>

***

### 4. Partner-Configurable Options

<mark style="color:$info;">Front-end partners can opt into several configuration options that customize how their integration earns and reports revenue. These are not exposed as self-serve settings. Dimes provisions them on the partner record during onboarding, so the API surface stays unchanged for end users.</mark>

* **Venue fee reporting**: <mark style="color:$info;">the protocol's origination fee is surfaced through the standard</mark> <mark style="color:orange;">`origination_fee_bps`</mark> <mark style="color:$info;">field on every quote, offer, and position. The venue trading fee paid to the underlying prediction market on every executed leg ( open, close, force-unwind, liquidate, settle) is surfaced separately on positions as</mark> <mark style="color:orange;">`accrued_venue_fee_*`</mark> <mark style="color:$info;">(open) and</mark> <mark style="color:orange;">`total_venue_fee_*`</mark> <mark style="color:$info;">(closed), and is folded into</mark> <mark style="color:orange;">`net_unrealized_pnl_*`</mark> <mark style="color:$info;">and</mark> <mark style="color:orange;">`net_realized_pnl_*`</mark><mark style="color:$info;">.</mark>
* **Dedicated fee receiver wallet:** <mark style="color:$info;">partners designate a wallet that receives their share of fees. Settlement flows directly to that address, keeping accounting clean and removing any need for off-platform reconciliation.</mark>
* **Polymarket builder code:** <mark style="color:$info;">partners using the Polymarket route can register their own builder profile with Polymarket</mark> ([polymarket.com/settings?tab=builder](https://polymarket.com/settings?tab=builder)) <mark style="color:$info;">and share the returned</mark> <mark style="color:orange;">`bytes32`</mark> <mark style="color:$info;">builder code. Dimes attaches it to every CLOB order routed through that partner. Fees are gathered and paid out by Polymarket through its standard builder-fee path — not by Dimes — so Polymarket builder rewards accrue directly to the partner's Polymarket account. Partners that don't provide a code fall back to the Dimes operator builder code.</mark>


# Integration with Front-Ends

Drop-in margin infrastructure for prediction market terminals.

<mark style="color:$info;">Front-ends integrate Multiply by</mark> **adding a small set of UI components and interacting with Dimes' API surface**. **Front-ends never underwrite risk, and never participate in hedging**<mark style="color:$info;">; they route user intent and render Multiply's state updates.</mark>

<mark style="color:$info;">As the system evolves, new features, APIs, and simplified integration paths may be introduced. Front-ends will always interact with Multiply through a narrow, stable interface, but specific parameters: leverage availability, liquidation bands, supported markets, and state feeds, may change as we expand to additional venues and improve hedging infrastructure.</mark>

<mark style="color:$info;">All integration updates will be backward-compatible or accompanied by clear migration guidelines.</mark>

#### Front-End Requirements

<mark style="color:$info;">Front-ends display basic leveraged-position functionality:</mark>

* <mark style="color:$info;">market selection</mark>
* <mark style="color:$info;">direction (YES/NO)</mark>
* <mark style="color:$info;">leverage selection</mark>
* <mark style="color:$info;">position size input</mark>
* <mark style="color:$info;">required collateral</mark>
* <mark style="color:$info;">liquidation price and buffer</mark>
* <mark style="color:$info;">fees</mark>
* <mark style="color:$info;">live PnL and risk indicators</mark>
* <mark style="color:$info;">ability to close a position</mark>

#### **What Multiply Provides**

<mark style="color:$info;">Multiply provides:</mark>

* <mark style="color:$info;">quotes for opening/closing positions</mark>
* <mark style="color:$info;">execution and settlement of positions on underlying venues</mark>
* <mark style="color:$info;">real-time PnL and margin updates</mark>
* <mark style="color:$info;">leverage decay, liquidation triggers and events</mark>
* <mark style="color:$info;">clear error codes for unhedgeable or invalid requests</mark>


# CFD Abstraction

Multiply's leveraged prediction market design.

<mark style="color:$info;">Multiply implements a Contract for Difference abstraction to deliver leveraged directional exposure on prediction markets without requiring users to manage collateral, custody, or cross-venue settlement themselves. Positions are represented onchain as synthetic instruments whose value references external market prices, while margining, PnL, and settlement remain fully native to Solana.</mark>

<mark style="color:$info;">Each position follows a bounded synthetic payoff:</mark>

> <mark style="color:yellow;">`CFD PnL = (Pnow − Pentry) × Leverage × Size`</mark><mark style="color:$info;">, bounded by the 0 → 1 settlement range of the underlying prediction market.</mark>

<mark style="color:$info;">where prices reference external market quotes and outcomes are constrained by the 0 to 1 settlement range of binary contracts. This bounded structure allows leverage to be offered in a controlled way, with deterministic maximum loss and well-defined liquidation behavior.</mark>

<mark style="color:$info;">While positions are synthetic, Multiply maintains a continuous mapping between onchain exposure and equivalent market participation. For every trade, the protocol tracks effective notional, implied share exposure, and venue attribution.</mark> **This data can be surfaced to terminals for leaderboards, activity feeds, and incentive programs, and preserved for downstream reconciliation.**

<mark style="color:$info;">From the perspective of the ecosystem,</mark> **Multiply acts as an aggregation and risk management layer** <mark style="color:$info;">rather than a replacement trading surface. Synthetic exposure is derived from, and continuously priced off, live venue markets, and</mark> **hedging activity flows back to underlying venues** <mark style="color:$info;">during hedgeable periods.</mark> **Front-ends retain full control of the user relationship and experience**<mark style="color:$info;">, while Multiply handles leverage, margining, and risk internally. Users obtain the economic effect of leveraged exposure, with all collateral, PnL, and settlement managed onchain in a single environment.</mark>


# Introduction

How leverage and liquidation safety are determined across the platform.

<mark style="color:$info;">The Multiply risk model determines:</mark>

* <mark style="color:$info;">how much leverage can be offered,</mark>
* <mark style="color:$info;">how much notional a user can take,</mark>
* <mark style="color:$info;">how much total exposure a market can support,</mark>
* <mark style="color:$info;">when margin requirements change, and</mark>
* <mark style="color:$info;">when liquidation becomes necessary.</mark>

<mark style="color:$info;">The model is built around one invariant:</mark>

**At every moment, the system must be able to unwind the net hedge at a bounded and fully collateralized cost.**

<mark style="color:$info;">All computations flow from this requirement.</mark>


# Data Pipeline

Key parameters that inform margining, hedging, and allowable exposure.

<mark style="color:$info;">The engine continuously evaluates the underlying prediction market to determine safe leverage and notional. Inputs fall into five categories:</mark> **liquidity, pricing, market structure, venue health, and current exposure.**


# Maximum Leverage

Exposure calibrated to depth, slippage, and concentration.

<mark style="color:$info;">Multiply sets maximum leverage by measuring how much directional exposure the underlying market can safely absorb. The ceiling is determined by three factors:</mark>

* <mark style="color:$info;">Available liquidity on the venue</mark>
* <mark style="color:$info;">Expected slippage when executing the hedge, and</mark>
* <mark style="color:$info;">Dimes' allowable weight in that individual market.</mark>

<mark style="color:$info;">These inputs define the effective capacity of the market and produce a leverage limit that reflects real depth rather than arbitrary risk rules.</mark>

<mark style="color:$info;">As markets vary in depth and shape, so do their leverage limits.</mark>

* <mark style="color:$info;">Deep markets with stable liquidity gradients typically support</mark> **8× to 10×** <mark style="color:$info;">leverage. These are markets where the top of book provides at least</mark> **2 to 3% of notional depth**, <mark style="color:$info;">and where simulated hedge execution stays under</mark> **30 to 50 bps** <mark style="color:$info;">of expected slippage for the size Multiply must hedge.</mark>
* <mark style="color:$info;">Mid-depth markets, where aggregated depth at the active probability bucket is closer to</mark> **1 to 2% of notional** <mark style="color:$info;">and slippage stays below</mark> **75 bps**<mark style="color:$info;">, generally support</mark> **4× to 6×** <mark style="color:$info;">leverage.</mark>
* <mark style="color:$info;">Thinner or more volatile markets with less than</mark> **1% depth** <mark style="color:$info;">at the active bucket, or with slippage simulations exceeding</mark> **1%**<mark style="color:$info;">, receive</mark> **2× to 3×** <mark style="color:$info;">leverage to maintain smooth hedge execution and avoid excessive price impact.</mark>

<mark style="color:$info;">Multiply adjusts these limits dynamically as venue liquidity changes. Every venue update recalculates depth, slippage, and concentration metrics, ensuring that leverage stays aligned with real market capacity and that the hedging facility can execute efficiently under all conditions.</mark>


# Global Limits and Circuit Breakers

Protocol-wide controls beyond user-level and market-level limits.

#### **Global notional cap**

<mark style="color:$info;">Maximum aggregate exposure the system will accept across all prediction markets.</mark>

#### **Venue health detection**

<mark style="color:$info;">If the underlying venue stops updating or becomes erratic, Multiply restricts new positions.</mark>

#### **Spread / depth shock limits**

<mark style="color:$info;">If spreads or depth move outside safe ranges, all new entry requests are rejected until conditions stabilize.</mark>


# Levered Exposure

How collateral and hedging combine to form each position.

<mark style="color:$info;">When a user opens a leveraged directional position:</mark>

1. <mark style="color:$info;">The user's collateral is locked and, together with Underwriting Facility capital, funds the position construction on the underlying venue (i.e. Polymarket).</mark>
2. <mark style="color:$info;">Dimes constructs and maintains the hedge for the duration of the position.</mark>
3. <mark style="color:$info;">User PnL becomes a function of probability movement multiplied by leverage.</mark>
4. <mark style="color:$info;">Hedge PnL offsets user PnL so that Dimes remains neutral within defined liquidation constraints.</mark>

#### Hedge Mechanics

<mark style="color:$info;">For a long synthetic YES exposure, Multiply expresses the user position by acquiring the equivalent YES exposure on the underlying prediction market venue using facility capital.</mark> **The position produces gains or losses that mirror the user's PnL, keeping Dimes neutral to price direction.**

<mark style="color:$info;">If the position evolves favorably for the user:</mark>

* <mark style="color:$info;">the hedge pays out more than its acquisition cost,</mark>
* <mark style="color:$info;">the excess is transferred to the user as profit,</mark>
* <mark style="color:$info;">and the hedging facility recovers its original principal.</mark>

<mark style="color:$info;">If the position moves against the user:</mark>

* <mark style="color:$info;">the hedge pays out less than its acquisition cost,</mark>
* <mark style="color:$info;">the user's margin absorbs the loss through liquidation,</mark>
* <mark style="color:$info;">and the transferred loss restores the hedging facility's principal.</mark>


# Contract-for-Difference

Why CFDs are the right instrument for leveraged prediction market exposure.

<mark style="color:$info;">Multiply provides leveraged directional exposure to prediction markets through a</mark> **Contract-for-Difference ("CFD") layer**<mark style="color:$info;">. Users trade via integrated front-ends, while Dimes constructs and manages the corresponding positions on the underlying venue.</mark>

<mark style="color:$info;">CFDs are the natural instrument for this use case. Alternative approaches: perps and other funding-rate-driven derivatives, collateral looping, and options-like event contracts,</mark> **each carry fundamental limitations in the context of prediction markets**<mark style="color:$info;">.</mark>

<mark style="color:$info;">Perpetual futures rely on funding rates to keep contracts' price tethered to spot. The mechanism assumes a balanced fight between longs and shorts. Prediction markets violate this structurally: as an outcome becomes probable, the price converges toward $0 or $1 and</mark> **one side of the book collapses**<mark style="color:$info;">. The losing side has no incentive to hold; the winning side has no counterparty to pay them. Funding breaks down exactly as the contract approaches its most important moment.</mark>

<mark style="color:$info;">Pool-based collateral looping: borrowing against tokenized outcome positions to buy more can technically produce leverage on fully collateralized prediction market contracts. But lending protocols following this approach are</mark> **structurally blind to the asymmetric risk surface of prediction markets**<mark style="color:$info;">. They have no awareness of interim jump risk, no mechanism to intelligently decay leverage, and no ability to manage the liquidity microstructure that makes losing-side depth vanish as outcomes become clearer. When a market jumps to settlement, looped collateral goes to zero and the lender absorbs the loss. The result is capital loss for liquidity providers and higher borrowing costs for traders.</mark>

<mark style="color:$info;">Options and similar event-derivative contracts can deliver leveraged exposure to binary outcomes, but they require</mark> **bootstrapping independent liquidity for every market, at every strike**<mark style="color:$info;">. None of this benefits from the depth that already exists on the underlying prediction market venue such as Polymarket. The result is fragmented, thin books that widen spreads and limit scale.</mark>

<mark style="color:$info;">CFDs avoid these issues. The user benefits from levered exposure; Dimes holds and manages the corresponding hedge. Because opposing user exposures can be netted internally, the total hedge the desk carries on-venue is smaller than the sum of individual positions, reducing execution costs that are ultimately passed through to traders.</mark>

**CFDs are a well-established structure in TradFi, supporting hundreds of billions in annual notional** <mark style="color:$info;">by delivering economic exposure without transferring custody of the underlying instrument. Multiply applies the same principle onchain. Synthetic positions mirror the payoff of prediction market contracts, while collateral management, PnL accounting, margining, and settlement are handled natively by Multiply's infrastructure.</mark>


# Leverage Decay (J-factor)

Automated exposure reduction driven by market microstructure.

<mark style="color:$info;">J-factor is Multiply's dynamic risk engine. It continuously monitors depth profile, odds dynamics, open interest microstructure, and resolution conditions, defining when leverage should be reduced.</mark>

<mark style="color:$info;">For users, J-factor is what quietly works in the background to keep your position safe, proactively reducing your leverage down as conditions worsen.</mark>

#### **What J-factor governs**

<mark style="color:$info;">J-factor is a set of parameters that jointly define:</mark>

* <mark style="color:$info;">The</mark> **maximum leverage** <mark style="color:$info;">permitted at any point in the position's lifecycle</mark>
* <mark style="color:$info;">The</mark> **conditions** <mark style="color:$info;">that trigger reduction: depth thresholds, spread widening, order book imbalance, fill rate degradation, etc.</mark>
* <mark style="color:$info;">The</mark> **execution path** <mark style="color:$info;">for unwinding the corresponding hedge when a threshold is breached</mark>

#### **Dynamic behavior**

<mark style="color:$info;">Because reduction is conditional on microstructure rather than scheduled against time, J-factor produces fundamentally different outcomes on the various sides of a market.</mark>

<mark style="color:$info;">On the winning side, depth typically holds or improves as resolution approaches. Participants want exposure to the likely outcome, thresholds are rarely breached, and positions frequently</mark> **carry their full initial leverage through resolution**<mark style="color:$info;">.</mark>

<mark style="color:$info;">On the losing side, counterparty depth drains, spreads widen, and fill rates deteriorate. These are exactly the signals J-factor monitors. Thresholds are breached earlier, triggering progressive reduction. Each step unwinds a proportional share of the hedge on the venue, returning UF capital and shrinking the position's footprint against a thinning book.</mark> **This also protects the user**<mark style="color:$info;">: each reduction pushes the liquidation price further away, letting them stay in the market longer than they would under a static leverage framework.</mark>

<mark style="color:$info;">This microstructure-driven approach has two additional properties.</mark>

* <mark style="color:$info;">First, because there is no fixed exit schedule, there is no predictable pattern that external participants can front-run or position against, resulting in better execution for both the trader and the Facility.</mark>
* <mark style="color:$info;">Second, because the engine does not rely on time-to-resolution to govern leverage, the same architecture that manages a multi-week election market can underwrite short-duration markets of 5 or 15 minutes without modification. What changes is the speed at which microstructure evolves, not the logic that governs the response.</mark>


# Margin Requirement

Enforcing safe leverage by tying position size to posted collateral.

<mark style="color:$info;">Liquidation occurs when a position's</mark> **remaining collateral is no longer sufficient to support its leveraged exposure at current market prices**. <mark style="color:$info;">At that point, the system closes the position and unwinds the associated hedge.</mark>

<mark style="color:$info;">A position is liquidated when</mark> **unrealized losses reduce the collateral buffer** <mark style="color:$info;">below the minimum required to maintain the position.</mark>

<mark style="color:$info;">Once this threshold is reached, Multiply executes a full close-out to ensure the exposure can still be</mark> **unwound at a bounded cost, given available market liquidity and fees**<mark style="color:$info;">.</mark>

<mark style="color:$info;">Liquidation is therefore triggered by the relationship between collateral, leverage, fees, and current market prices. The system closes the position as soon as it can no longer be maintained within its defined safety parameters.</mark>

<mark style="color:$info;">The liquidation price is computed from:</mark>

* <mark style="color:$info;">entry price and current leverage</mark>
* <mark style="color:$info;">modeled cost to unwind the hedge under conservative execution assumptions</mark>
* <mark style="color:$info;">liquidation buffer sized to absorb tail slippage beyond modeled cost</mark>
* <mark style="color:$info;">venue fees and gas costs where applicable</mark>
* <mark style="color:$info;">accrued time-based fees owed to Multiply</mark>


# Liquidation Proofs

Onchain proofs that each liquidation followed the engine's rules.

<mark style="color:$info;">To ensure trust-minimization, Multiply publishes</mark> **liquidation proofs onchain**<mark style="color:$info;">.</mark>

<mark style="color:$info;">Each liquidation proof contains:</mark>

* <mark style="color:$info;">position ID</mark>
* <mark style="color:$info;">timestamp</mark>
* <mark style="color:$info;">liquidation price</mark>
* <mark style="color:$info;">venue price data used at time of liquidation</mark>
* <mark style="color:$info;">signature verifying internal hedge unwind completion</mark>

<mark style="color:$info;">This allows:</mark>

* <mark style="color:$info;">terminals to verify that a liquidation event was valid and deterministic</mark>
* <mark style="color:$info;">users to audit liquidation conditions independently</mark>
* <mark style="color:$info;">third-party risk monitors to track solvency performance in real time</mark>

<mark style="color:$info;">The oracle stream acts as a</mark> **public audit trail** <mark style="color:$info;">for all enforced liquidations.</mark>


# Liquidation Trigger

When collateral, leverage, and market conditions no longer support the open position.

<mark style="color:$info;">Liquidation occurs when:</mark>

> <mark style="color:yellow;">`Underlying price ≥ liquidation price (YES)`</mark>
>
> <mark style="color:yellow;">`Underlying price ≤ liquidation price (NO)`</mark>

<mark style="color:$info;">Once triggered:</mark>

1. <mark style="color:$info;">The position is closed.</mark>
2. <mark style="color:$info;">The hedge is unwound.</mark>
3. <mark style="color:$info;">Remaining collateral is credited to the user.</mark>
4. <mark style="color:$info;">The terminal updates the user's account immediately.</mark>

<mark style="color:$info;">There are</mark> **no hidden conditions or discretionary rules**.

<mark style="color:$info;">Front-ends receive a continuous feed of:</mark>

* <mark style="color:$info;">current liquidation price,</mark>
* <mark style="color:$info;">current buffer (distance to liquidation),</mark>
* <mark style="color:$info;">recommended collateral top-ups.</mark>

<mark style="color:$info;">When the buffer drops below thresholds:</mark>

* **Warning 1: Margin Call**

  <mark style="color:$info;">The front-end prompts the user to either add collateral or reduce exposure.</mark>
* **Warning 2: Imminent Liquidation**

  <mark style="color:$info;">Displayed when price is within a narrow band of the liquidation price.</mark>

<mark style="color:$info;">Once liquidation is triggered:</mark>

1. <mark style="color:$info;">Multiply executes the hedge unwind within configured slippage bounds.</mark>
2. <mark style="color:$info;">Synthetic position is marked closed at the liquidation price.</mark>
3. <mark style="color:$info;">Any remaining collateral is returned.</mark>
4. <mark style="color:$info;">The front-end reflects final PnL.</mark>

<mark style="color:$info;">If liquidity is insufficient for immediate unwind, Multiply enters a protected unwind mode and pauses new exposure until safe execution is possible.</mark>


# Getting Started

Onboarding path for integrating Dimes Multiply

Everything you need to stand up a Multiply integration: install the SDK, wire up React hooks, pick an environment, and test against the sandbox before going live.

* [Quickstart](/for-developers/getting-started/quickstart)
* [SDK Installation](/for-developers/getting-started/sdk-installation)
* [React Hooks](/for-developers/getting-started/react-hooks)
* [Environments](/for-developers/getting-started/environments)
* [Sandbox](/for-developers/getting-started/sandbox)


# Quickstart

The six-step integration path. Authenticate, list markets, quote, open, monitor, close.

Start in [sandbox](/for-developers/getting-started/sandbox). Same API, same contract, test pUSD. Swap `api.dimes.fi` for `api-sandbox.dimes.fi` and use a `dm_sbx_skey_...` key — every example below works as-is. Working reference: [`dimes-demo-ui`](https://github.com/dimes-fi/dimes-demo-ui).

> **Using the SDK?** Install it: `npm install @dimes-dot-fi/sdk`. See [SDK Installation](/for-developers/getting-started/sdk-installation) for setup and peer dependencies. Every SDK snippet below has a runnable, type-checked counterpart in [`examples/`](https://github.com/dimes-fi/dimes-sdk/tree/main/examples) — start with [`01-quickstart.ts`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/01-quickstart.ts).

***

### 1. Authenticate

{% tabs %}
{% tab title="REST API" %}

```bash
curl -X POST https://api.dimes.fi/v1/prediction-markets/tokens \
  -H "Authorization: Api-Key dm_live_skey_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "wallet_address": "0x1234...abcd" }'
```

```json
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": "2026-04-16T11:00:00.000Z"
}
```

JWTs expire in 1 hour. Full details: [Authentication](/for-developers/api-and-events/authentication).
{% endtab %}

{% tab title="SDK" %}

```typescript
import { DimesClient, ApiKeyAuth } from "@dimes-dot-fi/sdk";

const client = new DimesClient({
  auth: new ApiKeyAuth({
    apiKey: process.env.DIMES_API_KEY,
    walletAddress: "0x1234...abcd",
  }),
});
```

`ApiKeyAuth` obtains and refreshes JWTs automatically. For client-side code with a JWT already in hand, use `JwtAuth` — see [SDK Installation](/for-developers/getting-started/sdk-installation).
{% endtab %}

{% tab title="React" %}

```tsx
import { DimesClient, JwtAuth } from "@dimes-dot-fi/sdk";
import { DimesProvider } from "@dimes-dot-fi/sdk/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();
const client = new DimesClient({
  auth: new JwtAuth({ tokenUrl: "https://your-backend.com/api/dimes-token" }),
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <DimesProvider client={client}>
        <YourApp />
      </DimesProvider>
    </QueryClientProvider>
  );
}
```

Hooks automatically use whatever `QueryClientProvider` is in your tree.
{% endtab %}
{% endtabs %}

***

### 2. List markets

{% tabs %}
{% tab title="REST API" %}

```bash
curl https://api.dimes.fi/v1/prediction-markets/markets
```

Each market includes `leverage.min_bps`, `step_bps`, and per-side `max_yes_bps` / `max_no_bps` — clamp input to those. Full shape: [API Reference — Markets](/for-developers/api-and-events/api-reference#1-markets).
{% endtab %}

{% tab title="SDK" %}

```typescript
const { data: markets, hasMore } = await client.getMarkets();

for (const market of markets) {
  console.log(market.ticker, market.leverage.maxYesBps, market.leverage.maxNoBps);
}
```

{% endtab %}

{% tab title="React" %}

```tsx
import { useMarkets } from "@dimes-dot-fi/sdk/react";

function MarketList() {
  const { data, isLoading } = useMarkets();

  if (isLoading) return <div>Loading...</div>;

  return (
    <ul>
      {data?.data.map((market) => (
        <li key={market.ticker}>{market.title}</li>
      ))}
    </ul>
  );
}
```

{% endtab %}
{% endtabs %}

***

### 3. Request a quote

{% tabs %}
{% tab title="REST API" %}
Draft first — pricing to show the user, no expiry, no capacity held:

```bash
curl -X POST https://api.dimes.fi/v1/prediction-markets/draft-quotes \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "market_ticker": "will-btc-hit-100k-2026",
    "effective_side": "yes",
    "leverage_bps": 50000,
    "notional_amount_usd_pips": "250000000",
    "slippage_bps": 300
  }'
```

Then promote when the user confirms — this returns the signed, executable quote. Submit it on-chain right away — check `expires_at` for its validity window:

```bash
curl -X POST https://api.dimes.fi/v1/prediction-markets/promoted-quotes/dm_dro_123 \
  -H "Authorization: Bearer <jwt>"
```

The promoted response carries pricing, fees, liquidation parameters, and a `contract_signature` for the on-chain call. Full shape and signature verification: [API Reference — Quotes](/for-developers/api-and-events/api-reference#3-quotes).

> `POST /quotes` does both in one call. It works, but the signature starts expiring while the user is still deciding — use it only for flows with no review step. See [API Reference — Create quote](/for-developers/api-and-events/api-reference#create-quote).
> {% endtab %}

{% tab title="SDK" %}

```typescript
import { executeQuote } from "@dimes-dot-fi/sdk";

const result = await executeQuote(client, {
  marketTicker: "will-btc-hit-100k-2026",
  side: "yes",
  collateralUsd: 25,
  leverageBps: 50000,
  slippageBps: 300,
});

console.log(result.quote.entryPriceUsd, result.quote.leverageBps);
```

`executeQuote` handles the draft → promote flow, retries on market-moved errors, and auto-corrects leverage/collateral/slippage when the API suggests adjustments.

Hook into each stage:

```typescript
const result = await executeQuote(client, params, {
  onDraftReady: (draft) => console.log("Draft:", draft.id),
  onMarketMoved: (event) => console.log("Retrying...", event.retryCount),
  onCorrection: (adj) => console.log("Adjusted:", adj.field, adj.toLabel),
});
```

{% endtab %}

{% tab title="React" %}

```tsx
import { useQuote } from "@dimes-dot-fi/sdk/react";

function QuoteButton({ marketTicker }: { marketTicker: string }) {
  const { state, execute, reset } = useQuote();

  const handleQuote = async () => {
    await execute({
      marketTicker,
      side: "yes",
      collateralUsd: 25,
      leverageBps: 50000,
      slippageBps: 300,
    });
  };

  return (
    <div>
      <button onClick={handleQuote} disabled={state.phase === "promoting"}>
        {state.phase === "idle" ? "Get Quote" : state.phase}
      </button>
      {state.quote && <div>Entry: {state.quote.entryPriceUsd}</div>}
    </div>
  );
}
```

`useQuote` manages the full state machine: `idle` → `loading-draft` → `draft-ready` → `promoting` → `promoted`.
{% endtab %}
{% endtabs %}

***

### 4. Open the position

Two on-chain calls: `pUSD.approve(vault, total_user_amount)` then `vault.createPosition(...)` with the quote parameters. After the tx confirms, Multiply acquires tokens, finalizes on-chain, and emits `position.opened`.

```
pending → open → closing → closed
           ↓
       liquidated
```

{% tabs %}
{% tab title="ethers.js" %}

```javascript
import { ethers } from "ethers";

const usdc = new ethers.Contract(USDC_ADDRESS, ["function approve(address,uint256)"], signer);
const approvalAmount = BigInt(quote.total_user_amount_usd_pips) * 100n;
await usdc.approve(quote.polygon_vault_contract_address, approvalAmount);

const VAULT_ABI = [
  "function createPosition(bytes16,bytes32,uint256,uint256,uint32,uint256,uint16,uint16,uint16,uint256,bytes,uint256) external",
];
const vault = new ethers.Contract(quote.polygon_vault_contract_address, VAULT_ABI, signer);

const tx = await vault.createPosition(
  quote.position_seed_hex,
  quote.polymarket_market_id,
  BigInt(quote.polymarket_token_id),
  BigInt(quote.collateral_usdc_units),
  quote.leverage_bps,
  BigInt(quote.notional_usdc_units),
  quote.origination_fee_bps,
  quote.lifetime_fee_apr_bps,
  quote.liquidation_fee_bps,
  BigInt(quote.expected_open_trading_fee_usdc_units),
  quote.contract_signature,
  BigInt(quote.signature_expiry),
);
await tx.wait();
```

Code examples for Polymarket Safe, Proxy, and Deposit Wallet patterns: [On-Chain Integration](/for-developers/wallet-integration/on-chain-integration).
{% endtab %}

{% tab title="SDK (viem)" %}

```typescript
import { buildCreatePositionTx, buildApproveTx, verifyQuoteSignature } from "@dimes-dot-fi/sdk/contract";

// Verify the quote signature against on-chain contract info
await verifyQuoteSignature(client, result.quote, userAddress);

// Build approve + createPosition transactions (viem-compatible)
const approveTx = buildApproveTx(usdcAddress, vaultAddress, approvalAmount);
const createTx = buildCreatePositionTx(result.quote);

// Submit with viem's walletClient
await walletClient.writeContract(approveTx);
await walletClient.writeContract(createTx);
```

`buildCreatePositionTx` returns a viem-compatible object (`address`, `abi`, `functionName`, `args`) — pass it directly to `walletClient.writeContract()`.
{% endtab %}
{% endtabs %}

***

### 5. Monitor positions

{% tabs %}
{% tab title="REST API" %}

```bash
curl "https://api.dimes.fi/v1/prediction-markets/positions?status=open" \
  -H "Authorization: Bearer <jwt>"
```

Each position carries P\&L, mark price, margin health, liquidation price, and accrued fees. Display rules: [UI Guidelines](/for-developers/design-and-branding/ui-guidelines).
{% endtab %}

{% tab title="SDK" %}

```typescript
const positions = await client.getPositions({ status: "open" });

for (const position of positions) {
  console.log(position.marketTicker, position.current.unrealizedPnlUsd);
}
```

{% endtab %}

{% tab title="React" %}

```tsx
import { usePositions } from "@dimes-dot-fi/sdk/react";

function OpenPositions() {
  const { data: positions, isLoading } = usePositions({ status: "open" });

  if (isLoading) return <div>Loading...</div>;

  return (
    <ul>
      {positions?.map((p) => (
        <li key={p.id}>
          {p.marketTicker} — {p.current.unrealizedPnlUsd}
        </li>
      ))}
    </ul>
  );
}
```

{% endtab %}
{% endtabs %}

***

### 6. Close

The owner calls `vault.requestClose(position_key)`. Multiply sells tokens, distributes proceeds, emits `position.closed`. If the market resolves first, settlement is automatic.

{% tabs %}
{% tab title="ethers.js" %}

```javascript
const VAULT_ABI = ["function requestClose(bytes32) external"];
const vault = new ethers.Contract(vaultAddress, VAULT_ABI, signer);

const tx = await vault.requestClose(position.on_chain_position_key);
await tx.wait();
```

Code: [On-Chain Integration — Closing](/for-developers/wallet-integration/on-chain-integration#closing-a-position).
{% endtab %}

{% tab title="SDK (viem)" %}

```typescript
import { buildRequestCloseTx } from "@dimes-dot-fi/sdk/contract";

const closeTx = buildRequestCloseTx(vaultAddress, positionKey);
await walletClient.writeContract(closeTx);
```

{% endtab %}
{% endtabs %}

***

### What's next

* [SDK Installation](/for-developers/getting-started/sdk-installation) — setup, entry points, and peer dependencies
* [Sandbox](/for-developers/getting-started/sandbox) — how to get a sandbox key and test pUSD
* [Environments](/for-developers/getting-started/environments) — sandbox vs production
* [Contract Addresses](/for-developers/wallet-integration/contract-addresses) — verify what you're approving against
* [Authentication](/for-developers/api-and-events/authentication) — key management and rotation
* [On-Chain Integration](/for-developers/wallet-integration/on-chain-integration) — contract calls, wallet patterns, ABI
* [API Reference](/for-developers/api-and-events/api-reference) — full endpoint documentation
* [WebSocket Events](/for-developers/api-and-events/websocket) — real-time position state changes
* [Error Handling](/for-developers/api-and-events/error-handling) — error codes and retry logic
* [UI Guidelines](/for-developers/design-and-branding/ui-guidelines) — leverage sliders and position cards

Need help? Telegram link at [dimes.fi](https://dimes.fi).


# SDK Installation

Install the TypeScript SDK and set up the client for server-side, browser, or React applications.

### Install

```bash
npm install @dimes-dot-fi/sdk
```

### Entry points

The SDK ships four entry points. Import only what you need — peer dependencies are optional.

| Entry point                  | What it provides                                        | Peer dependencies                         |
| ---------------------------- | ------------------------------------------------------- | ----------------------------------------- |
| `@dimes-dot-fi/sdk`          | HTTP client, quote engine, error types                  | None                                      |
| `@dimes-dot-fi/sdk/react`    | React hooks and context provider                        | `react >=19`, `@tanstack/react-query >=5` |
| `@dimes-dot-fi/sdk/contract` | On-chain transaction builders, signature verification   | `viem >=2`                                |
| `@dimes-dot-fi/sdk/ws`       | WebSocket clients for the user position/market gateways | None (`socket.io-client` is bundled)      |

Install peer dependencies for the entry points you use:

```bash
# React hooks
npm install react @tanstack/react-query

# On-chain integration
npm install viem
```

***

### Client setup

{% tabs %}
{% tab title="API Key (server-side)" %}
Use `ApiKeyAuth` when your backend holds the API key and generates JWTs for users.

```typescript
import { DimesClient, ApiKeyAuth } from "@dimes-dot-fi/sdk";

const client = new DimesClient({
  auth: new ApiKeyAuth({
    apiKey: process.env.DIMES_API_KEY,
    walletAddress: "0x1234...abcd",
  }),
});

const markets = await client.getMarkets();
```

`ApiKeyAuth` automatically obtains and refreshes JWTs.
{% endtab %}

{% tab title="JWT (client-side)" %}
Your backend holds the Dimes API key and exposes an endpoint that generates JWTs for your users. Point `JwtAuth` at that endpoint — it fetches, caches, and auto-refreshes tokens:

```typescript
import { DimesClient, JwtAuth } from "@dimes-dot-fi/sdk";

const client = new DimesClient({
  auth: new JwtAuth({ tokenUrl: "https://your-backend.com/api/dimes-token" }),
});

const markets = await client.getMarkets();
```

Your backend endpoint should call `POST /v1/prediction-markets/tokens` with your API key and return the `{ token, expires_at }` response. See [Authentication](/for-developers/api-and-events/authentication).
{% endtab %}

{% tab title="React" %}
Wrap your app in `DimesProvider` to use hooks.

```tsx
import { DimesClient, JwtAuth } from "@dimes-dot-fi/sdk";
import { DimesProvider } from "@dimes-dot-fi/sdk/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

const client = new DimesClient({
  auth: new JwtAuth({ tokenUrl: "https://your-backend.com/api/dimes-token" }),
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <DimesProvider client={client}>
        <YourApp />
      </DimesProvider>
    </QueryClientProvider>
  );
}
```

Hooks use whatever `QueryClientProvider` is in your React tree. See [React Hooks](/for-developers/getting-started/react-hooks) for the full set — data hooks, the `useQuote` / `useQuoteMachine` quote flows, and the live `usePositionStream` / `useMarketStream` WebSocket hooks.
{% endtab %}
{% endtabs %}

***

### Sandbox

Point the client at sandbox by passing `baseUrl`:

```typescript
const client = new DimesClient({
  baseUrl: "https://api-sandbox.dimes.fi",
  auth: new ApiKeyAuth({
    apiKey: "dm_sbx_skey_...",
    walletAddress: "0x...",
  }),
});
```

See [Sandbox](/for-developers/getting-started/sandbox) for how to get a sandbox key and test pUSD.

***

### Custom fetch

Pass a custom `fetch` implementation for environments without a global `fetch` (older Node.js, test mocks, etc.):

```typescript
import { DimesClient, JwtAuth } from "@dimes-dot-fi/sdk";
import fetch from "node-fetch";

const client = new DimesClient({
  auth: new JwtAuth({ tokenUrl: "https://your-backend.com/api/dimes-token" }),
  fetch: fetch as unknown as typeof globalThis.fetch,
});
```

***

### Helpers

The core entry point also exports framework-agnostic helpers for the math behind market and position UIs, so you don't reimplement it from raw fields:

| Helper                                                                     | Use                                                         |
| -------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `getSidedEligibility(market)`                                              | Per-side (yes/no) tradeability, caps, and rejection reasons |
| `defaultSide(eligibility)`                                                 | The side to preselect in a trade panel                      |
| `leverageMaxBps(leverage, side)`                                           | Max leverage for a side                                     |
| `maxLeverageBpsAtNotional(...)` / `maxViableLeverageBpsForCollateral(...)` | Notional/collateral-aware leverage ceilings                 |
| `estimateLiquidationPrice(...)`                                            | Liquidation price for an open position                      |
| `computeMaxGain(input)`                                                    | Gross/net max gain for a quote                              |
| `getOriginationFeeBreakdown(...)`                                          | Protocol/partner origination fee split                      |
| `computePolymarketTradingFee(...)`                                         | Venue trading fee                                           |
| `isOpenPosition(p)` / `isClosedPosition(p)`                                | Narrow a `Position` union                                   |
| `rejectionReasonText(...)` / `rejectionReasonShort(...)`                   | Human-readable ineligibility messages                       |

These power the same displays in the reference UI — prefer them over hand-rolling fee/leverage/liquidation math.

***

### What's next

* [Quickstart](/for-developers/getting-started/quickstart) — end-to-end integration in 6 steps
* [React Hooks](/for-developers/getting-started/react-hooks) — data, quote, and live WebSocket hooks
* [Authentication](/for-developers/api-and-events/authentication) — API keys, JWTs, and key rotation
* [API Reference](/for-developers/api-and-events/api-reference) — full endpoint documentation


# React Hooks

React hooks for the Dimes SDK — data fetching, the interactive quote flow, and live WebSocket updates, all on top of TanStack Query.

The `@dimes-dot-fi/sdk/react` entry point ships hooks for every read endpoint, two quote flows, and live WebSocket streams. They are thin wrappers over [TanStack Query](https://tanstack.com/query) — every data hook returns a standard `UseQueryResult`, so `data`, `isLoading`, `error`, `refetch`, etc. all work as usual.

Peer dependencies: `react >=19`, `@tanstack/react-query >=5`.

> **Runnable examples:** [`react/trade-panel.tsx`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/react/trade-panel.tsx) (provider + `useMarkets` + `useQuote`) and [`react/streams.tsx`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/react/streams.tsx) (`usePositions` with live WebSocket reconciliation). Both are type-checked against the SDK in CI.

## Provider setup

Wrap your app in both a `QueryClientProvider` and `DimesProvider`. The hooks read the `DimesClient` from `DimesProvider` and the query cache from whatever `QueryClientProvider` is in the tree.

```tsx
import { DimesClient, JwtAuth } from "@dimes-dot-fi/sdk";
import { DimesProvider } from "@dimes-dot-fi/sdk/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();
const client = new DimesClient({
  auth: new JwtAuth({ tokenUrl: "https://your-backend.com/api/dimes-token" }),
});

function Root() {
  return (
    <QueryClientProvider client={queryClient}>
      <DimesProvider client={client}>
        <App />
      </DimesProvider>
    </QueryClientProvider>
  );
}
```

***

## Data hooks

| Hook                                   | Returns             | Endpoint                                |
| -------------------------------------- | ------------------- | --------------------------------------- |
| `useMarkets(params?, qo?)`             | `Paginated<Market>` | `GET /markets` (pass `query` to search) |
| `useMarket(ticker, qo?)`               | `Market`            | `GET /markets/:ticker`                  |
| `usePositions(params?, qo?, options?)` | `Position[]`        | `GET /positions`                        |
| `useContractInfo(qo?)`                 | `ContractInfo`      | `GET /contract-info`                    |
| `useLimits(qo?)`                       | `CustomerLimit`     | `GET /limits`                           |

`qo` is an optional partial `UseQueryOptions` passed straight through (override `enabled`, `staleTime`, etc.).

```tsx
import { useMarkets, usePositions } from "@dimes-dot-fi/sdk/react";

function Markets() {
  const { data, isLoading } = useMarkets({ status: "active", sort: "depth_desc" });
  if (isLoading) return <Spinner />;
  return <>{data!.data.map((m) => <MarketCard key={m.id} market={m} />)}</>;
}
```

### `usePositions` options

The third argument tunes polling and cache behaviour — all opt-in, all off by default:

```tsx
usePositions(
  { state: "active" },
  { enabled: !!wallet },               // standard query options
  {
    scope: wallet?.toLowerCase(),       // append to the cache key — avoids serving a previous wallet's positions
    adaptivePolling: true,              // poll fast (5s) while any position is transitioning, slow (15s) otherwise
    reconcile: true,                    // defer to fresher WebSocket data (pair with usePositionStream reconcile)
  },
);
```

`adaptivePolling` also accepts `{ fastMs, slowMs }`; `reconcile` also accepts a window in milliseconds.

***

## Quote hooks

Two flows sit on the same draft → promote pipeline. Pick by how much control your UI needs over the steps.

### `useQuote` — one-shot

Runs draft → promote (with auto-retry on market-moved and auto-correction of leverage/collateral/slippage) in a single call. Best when you just want a signed quote back and surface progress from `state.phase`.

```tsx
import { useQuote } from "@dimes-dot-fi/sdk/react";

const { state, execute } = useQuote();

const result = await execute(
  { marketTicker, side, collateralUsd, leverageBps, slippageBps },
  {
    onMarketMoved: () => true,   // return false to abort the retry
    onCorrection: () => true,    // return false to reject the adjustment
  },
);
// result.quote is the signed quote; state.phase walks idle → loading-draft → … → promoted
```

### `useQuoteMachine` — interactive

The granular, user-in-the-loop flow: fetch a draft, let the user review it, then promote — pausing across renders at each step. On a market-moved promote failure it re-drafts and surfaces a `market-moved` state so the user can review and **accept** the new draft before it's promoted.

```tsx
import { useQuoteMachine } from "@dimes-dot-fi/sdk/react";

const { state, getDraft, promote, acceptChanges, correctAndPromote, reset } = useQuoteMachine();

// 1. fetch + show a draft
await getDraft({ marketTicker, side, collateralUsd, leverageBps, slippageBps });

// 2. user confirms → promote the reviewed draft
if (state.phase === "draft-ready") await promote(state.draft);

// 3a. market moved → show state.newDraft, then on user confirm:
if (state.phase === "market-moved") await acceptChanges(state.newDraft, state.retryCount);

// 3b. a correctable error → apply a hint and promote adjusted params directly
//     (see quoteErrorHint / hintAdjustment in the core SDK)
await correctAndPromote(state.draft, adjustedParams);
```

`state.phase` is one of `idle | loading-draft | draft-ready | promoting | promoted | market-moved | error`. In the `promoted` phase, `state.promotedQuote` is the signed quote.

> Building a fully custom flow instead? The same primitives are public: `client.createDraftQuote()` / `promoteDraftQuote()` / `createQuote()`, `buildQuoteParams()`, and `isMarketMovedError()`.

### Partial fill

Both flows accept partial-fill params. Pass `allowPartialFill: true` (and optionally `minFillBps`) in the quote params to let an order fill below the requested notional rather than rejecting atomically. The correction logic will also auto-raise `minFillBps` to the floor the API returns. See [Partial-open (FAK)](/for-developers/api-and-events/api-reference#partial-open-fak).

***

## Stream hooks

`usePositionStream` and `useMarketStream` subscribe to the user WebSocket gateways. Both default to **reconcile** mode — merging events straight into the `usePositions` / `useMarkets` caches so the UI updates live — but you can switch to **raw** mode and handle events yourself. App-specific side effects (toasts, your own stores) go in the callbacks; the SDK never reaches into your app.

```tsx
import { usePositionStream, useMarketStream } from "@dimes-dot-fi/sdk/react";

function LiveUpdates() {
  usePositionStream({
    mode: "reconcile",                       // default — merges into ["dimes","positions"]
    onEvent: (e) => toastPositionEvent(e),   // always called, in both modes
    onNotification: (n) => toast(n.data.message),
  });

  useMarketStream({
    onEvent: (e) => {
      if (e.type === "market.discovered") addToLiveStrip(e.data); // discoveries are never auto-injected
    },
  });

  return null;
}
```

Shared options: `enabled`, `mode` (`"reconcile"` | `"raw"`), `pauseOnHidden` (disconnect while the tab is hidden, default true), `invalidateOnReconnect` (refetch on reconnect to catch missed events, default true), and the `onEvent` / `onConnect` / `onDisconnect` / `onError` callbacks (`usePositionStream` adds `onNotification`). Each returns `{ socket, connected }`.

In reconcile mode, market eligibility / max-leverage **deltas** are deep-merged into cached markets (so the constant `minBps` / `stepBps` survive a leverage change); `market.discovered` events are deliberately left for you to route, since injecting them would pollute server-filtered list views.

For the raw protocol, gateways, event types, and payloads, see [WebSocket Events](/for-developers/api-and-events/websocket).

***

### What's next

* [API Reference](/for-developers/api-and-events/api-reference) — every endpoint and its SDK method
* [WebSocket Events](/for-developers/api-and-events/websocket) — the underlying socket protocol
* [On-Chain Integration](/for-developers/wallet-integration/on-chain-integration) — turning a signed quote into a position


# Environments

Dimes Multiply runs two fully isolated environments: production and sandbox. Pick one per integration and point all your traffic (REST, WebSocket, API keys) at the same environment.

Multiply exposes two independent environments. Each has its own base URL, API keys, contract addresses, and data — they share no state.

## Comparison

| Aspect             | Production                                              | Sandbox                                                           |
| ------------------ | ------------------------------------------------------- | ----------------------------------------------------------------- |
| **Purpose**        | Live positions with real pUSD                           | Integration testing with test pUSD and fake outcome tokens        |
| **REST base URL**  | `https://api.dimes.fi/v1`                               | `https://api-sandbox.dimes.fi/v1`                                 |
| **WebSocket URL**  | `wss://api.dimes.fi/v1/ws/prediction-markets/positions` | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/positions`   |
| **Swagger UI**     | `https://api.dimes.fi/v1/customer-docs`                 | `https://api-sandbox.dimes.fi/v1/customer-docs`                   |
| **API key prefix** | `dm_live_skey_...`                                      | `dm_sbx_skey_...`                                                 |
| **Resource IDs**   | `dm_pos_...`, `dm_off_...`, `dm_mkt_...`                | `dm_pos_sdx_...`, `dm_off_sdx_...`, `dm_mkt_sdx_...`              |
| **Chain**          | Polygon mainnet                                         | Polygon mainnet                                                   |
| **pUSD**           | Real pUSD                                               | Test pUSD (minted on request to your wallet by the Dimes team)    |
| **Outcome tokens** | Real Polymarket CTF                                     | Fake CTF                                                          |
| **Vault contract** | Production `LeveragedPredictionVaultV1`                 | Separate sandbox `LeveragedPredictionVaultV1` (different address) |
| **Market data**    | Live Polymarket markets                                 | Live Polymarket markets (read-only)                               |
| **Funds at risk**  | Yes                                                     | No                                                                |

## Picking an environment

* **Building a new integration?** Start in [sandbox](/for-developers/getting-started/sandbox). Request a sandbox API key via the Telegram link on [dimes.fi](https://dimes.fi), then follow the [Quickstart](/for-developers/getting-started/quickstart) against `https://api-sandbox.dimes.fi/v1`.
* **Running an existing integration in production?** Keep using `https://api.dimes.fi/v1` with your `dm_live_skey_...` key.

Keys are environment-scoped and not interchangeable. A `dm_live_skey_...` key is rejected by `api-sandbox.dimes.fi`, and a `dm_sbx_skey_...` key is rejected by `api.dimes.fi`.

{% tabs %}
{% tab title="REST API" %}

```bash
# Production
curl https://api.dimes.fi/v1/prediction-markets/markets \
  -H "Authorization: Bearer <jwt>"

# Sandbox
curl https://api-sandbox.dimes.fi/v1/prediction-markets/markets \
  -H "Authorization: Bearer <jwt>"
```

{% endtab %}

{% tab title="SDK" %}

```typescript
// Production (default)
const client = new DimesClient({auth});

// Sandbox
const client = new DimesClient({
  baseUrl: "https://api-sandbox.dimes.fi",
  auth,
});
```

{% endtab %}
{% endtabs %}

## Parity

Sandbox mirrors production on every REST endpoint, WebSocket event, response shape, error code, resolver/settlement behavior, rate limit, and auth rule. The only intentional differences:

* **Separate vault contract** at a different address.
* **Test pUSD and fake Polymarket CTF** instead of the real tokens. Test pUSD is minted to your wallet on request by the Dimes team.
* **No SLA.** Sandbox may be reset or paused for maintenance on short notice.

## Contract addresses

See [Contract Addresses](/for-developers/wallet-integration/contract-addresses) for the canonical list of production and sandbox vault, pUSD, and CTF addresses with Polygonscan links.


# Sandbox

Sandbox is a fully deployed, isolated Multiply environment running on Polygon mainnet against test pUSD and fake outcome tokens. Build and test your integration end-to-end with no funds at risk.

Sandbox is a **separate deployed environment** with its own base URL, API keys, database, and vault contract pulling against test pUSD. It runs on Polygon mainnet, so every transaction behaves exactly like production — signatures, gas, confirmations, events — but the tokens it settles are worthless outside of Multiply. Use it to drive the full position lifecycle before touching real money.

For the point-by-point comparison with production, see [Environments](/for-developers/getting-started/environments). For the canonical contract addresses, see [Contract Addresses](/for-developers/wallet-integration/contract-addresses).

***

## Getting access

Sandbox API keys are issued by the Dimes team. To request one:

1. Go to [dimes.fi](https://dimes.fi) and use the Telegram link in the site footer to contact us.
2. Share the wallet address(es) you intend to test with, plus the partner name you want the key issued under.
3. We will reply with:

* A `dm_sbx_skey_...` API key scoped to sandbox.
* A starting balance of test pUSD minted to the wallet addresses you gave us.

Sandbox contract addresses are published on [Contract Addresses](/for-developers/wallet-integration/contract-addresses) — wire them into your allowlist ahead of time.

Top up anytime: the test pUSD token is openly mintable, so the [demo UI](https://github.com/dimes-fi/dimes-demo-ui) exposes a faucet button — or ping us and we'll mint to your wallet.

***

## Pointing your integration at sandbox

Swap `api.dimes.fi` for `api-sandbox.dimes.fi` everywhere — REST calls, WebSocket connections, any pinned base URL. Everything else is identical to production.

| Property           | Value                                                                     |
| ------------------ | ------------------------------------------------------------------------- |
| **REST base URL**  | `https://api-sandbox.dimes.fi/v1`                                         |
| **WebSocket URL**  | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/positions`           |
| **Swagger UI**     | `https://api-sandbox.dimes.fi/v1/customer-docs`                           |
| **API key prefix** | `dm_sbx_skey_...`                                                         |
| **Resource IDs**   | contain `sdx` (e.g. `dm_pos_sdx_...`) so sandbox data is obvious on sight |

{% tabs %}
{% tab title="cURL" %}

```bash
curl -X POST https://api-sandbox.dimes.fi/v1/prediction-markets/tokens \
  -H "Authorization: Api-Key dm_sbx_skey_your_sandbox_key" \
  -H "Content-Type: application/json" \
  -d '{ "wallet_address": "0x1234...abcd" }'
```

{% endtab %}

{% tab title="SDK" %}

```typescript
const client = new DimesClient({
  baseUrl: "https://api-sandbox.dimes.fi",
  auth: new ApiKeyAuth({
    apiKey: "dm_sbx_skey_...",
    walletAddress: "0x...",
  }),
});
```

{% endtab %}
{% endtabs %}

From there, follow the [Quickstart](/for-developers/getting-started/quickstart) — every step works in sandbox by substituting the base URL and key.

> **Working reference frontend:** [`dimes-fi/dimes-demo-ui`](https://github.com/dimes-fi/dimes-demo-ui) is a React + Vite app that exercises the entire integration path against sandbox — auth, markets, quotes, pUSD approval, `createPosition`, signature verification, and position monitoring. Fork it, run `npm run dev`, and trace the calls.

***

## Sandbox vs production

| Aspect             | Production              | Sandbox                                                     |
| ------------------ | ----------------------- | ----------------------------------------------------------- |
| **pUSD**           | Real pUSD on Polygon    | Test pUSD — self-mint via the demo UI faucet, or on request |
| **Outcome tokens** | Real Polymarket CTF     | Fake CTF deployed by Dimes                                  |
| **Vault**          | Production deployment   | Separate deployment (different address)                     |
| **Market data**    | Live Polymarket markets | Live Polymarket markets (read-only)                         |
| **Funds at risk**  | Yes                     | No                                                          |
| **SLA**            | Production-grade        | Best-effort — may reset or pause for maintenance            |

Sandbox runs the same `LeveragedPredictionVaultV1` contract as production. Every REST endpoint, WebSocket event, response shape, and error code is identical. The only things that change between environments are the contract addresses (listed in [Contract Addresses](/for-developers/wallet-integration/contract-addresses)) and the API key.

For the full matrix, see [Environments](/for-developers/getting-started/environments).

***

## What sandbox is not

* **Not a staging mirror of production data.** Sandbox has its own database. Production positions, partners, and API keys are not visible in sandbox and vice versa. The wrong key against the wrong base URL is rejected.
* **Not production.** No SLA. Do not build customer-facing demos or production workloads on sandbox; we may reset or redeploy on short notice.
* **Not real money.** Test pUSD has no off-ramp and cannot be converted to real pUSD.
* **Not shared.** Your sandbox API key only sees positions opened with itself. Other customers' sandbox data is invisible to you.

***

## Need help?

Find us on the Telegram link at [dimes.fi](https://dimes.fi). Include your partner name and the wallet addresses you need funded.


# Integration Best Practices

Recommendations that make a Multiply integration faster and smoother for your end users — drawn from the patterns we see in the best-performing terminals.

Everything here is optional — the API works without any of it. These are the choices the smoothest, fastest integrations tend to make, with the reasoning and the payoff for your end users spelled out, so you can decide what's worth adopting for your platform.

***

### Draft on page load, promote on commit

**What a best-in-class integration does:** renders the trading screen from a **draft quote** (`POST /draft-quotes`), and only creates a real quote (`POST /promoted-quotes/{draft_quote_id}`) when the user actually commits.

**Why it matters:** creating a real quote reserves capacity against your account's notional limit and holds it until the quote expires — that reservation is what guarantees the user can open at the quoted terms. If your screen quotes on load, every end user who is just *browsing* leverage and fees holds a slice of your limit. At scale, a room full of browsers can reserve your entire limit, and the users who genuinely want to trade get turned away with `quote_partner_position_limit_exceeded` even though nothing is open. (The limit counts open positions **plus** in-flight quotes — see `in_flight_notional_usd_pips` in the [error params](/for-developers/api-and-events/error-handling#errors-with-structured-hints).)

**The payoff for your users:** a draft runs the identical pricing pipeline — same entry price, fees, liquidation price, max leverage — but reserves nothing, so your display is always available and your real capacity stays free for people ready to trade. When they commit, promotion succeeds instantly at the drafted terms if the market hasn't moved; if it has, you re-quote — exactly what a stale `/quotes` result would have forced anyway.

`POST /quotes` in one shot is a great fit when you already know the quote will be used — server-side flows, or a frontend firing at the moment of confirmation. Details: [Draft quotes](/for-developers/api-and-events/api-reference#draft-quotes) · [Promote draft quote](/for-developers/api-and-events/api-reference#promote-draft-quote).

***

### Turn a rejection into a one-tap correction

**What a best-in-class integration does:** reads the `params` on a rejected quote and uses it to move the user straight back into a valid state, rather than showing a dead-end error.

**Why it matters:** most quote rejections carry the value that *would* work — the maximum leverage the model allows, the largest collateral the book supports, the minimum notional, the collateral floor, the slippage ceiling. They're there precisely so your interface can act on them instead of leaving the user to guess.

**The payoff for your users:** the difference between "Request failed, try again" and a slider that snaps to the allowed maximum with a clear note — "Leverage adjusted 8× → 5×" — and a one-tap path to sign. Concretely:

1. Clamp the offending input to the value in `params` (rounding to the market's `leverage.step_bps` where relevant).
2. Re-quote and show plainly what changed, so the user stays in control.
3. On accept, go straight to promote and open the wallet for signature.

Dispatch on `code`, then read the keys you expect — never regex-parse `message`. Full table of which codes carry which params: [Errors with structured hints](/for-developers/api-and-events/error-handling#errors-with-structured-hints). One worth handling explicitly: `quote_leverage_exceeds_collateral_floor` can't be resolved by lowering leverage — raise collateral to `min_collateral_usd_pips` instead.

***

### Let the WebSocket drive your market list

**What a best-in-class integration does:** treats the WebSocket as the primary source for market state — `market.discovered`, `market.eligibility_changed`, `market.max_leverage_changed` — and uses polling only as a periodic safety net.

**Why it matters:** max leverage and eligibility can change by the second during a live game or a volatile crypto move. A market list refreshed on a timer will quote against values that have already shifted, and those quotes come back rejected.

**The payoff for your users:** a leverage slider and market list that are correct at the moment they act, so quotes land the first time and trading feels immediate. Keep polling `GET /markets` every 5–10 minutes as reconciliation — it catches drift and covers a dropped connection — but let the socket be how you learn a cap moved. Market events are the ones that most affect trading feel; position events over the socket are a nice extra for per-user UX.

Details: [WebSocket — Market event types](/for-developers/api-and-events/websocket#market-event-types).

***

### Render pre-trade numbers from the fee report

**What a best-in-class integration does:** when a screen only needs to *display* cost and potential gain, it calls [`POST /fee-reports`](/for-developers/api-and-events/api-reference#fee-report) rather than a quote endpoint.

**Why it matters:** a quote takes a few seconds — it pulls live data from several internal and external sources and runs the risk model over it. The fee report computes the same cost and max-gain breakdown from notional and leverage using the market's fee rates, and returns fast enough to paint on load. It creates nothing and signs nothing.

**The payoff for your users:** fees and potential gain appear instantly as they size a position, instead of a spinner while a full quote resolves. One note to carry into your UI: the fee report's liquidation price is a deterministic at-entry estimate — the binding quote uses a TWAP/inference-based price that may differ — so label it as an estimate wherever you show it.

***

### What's next

* [Quickstart](/for-developers/getting-started/quickstart) — the draft → promote flow end to end
* [Error Handling](/for-developers/api-and-events/error-handling) — every code, and the params each one carries
* [WebSocket Events](/for-developers/api-and-events/websocket) — market and position channels
* [UI Guidelines](/for-developers/design-and-branding/ui-guidelines) — leverage sliders and position cards


# Wallet Integration

Connect and abstract wallets for on-chain Multiply positions

How to connect user wallets and settle Multiply positions on-chain, including contract references and smart-wallet abstractions (Privy, Turnkey).

* [Contract Addresses](/for-developers/wallet-integration/contract-addresses)
* [On-Chain Integration](/for-developers/wallet-integration/on-chain-integration)
* [Smart Wallet Integration](/for-developers/wallet-integration/smart-wallet-integration)
* [Privy Integration](/for-developers/wallet-integration/privy-integration)
* [Turnkey Integration](/for-developers/wallet-integration/turnkey-integration)


# Contract Addresses

Canonical list of Dimes Multiply contract and token addresses on Polygon mainnet. Use these to verify on Polygonscan before approving any spend.

Multiply's on-chain surface is a single vault contract per environment. All user funds flow through the vault — pUSD in, position tokens in, pUSD out. Verify these on [Polygonscan](https://polygonscan.com) before allowlisting them. The vault address is also on every quote response (`polygon_vault_contract_address`) and from `GET /prediction-markets/contract-info`.

***

## Production

Real pUSD. Real Polymarket CTF (Conditional Token Framework). Live positions.

| Contract           | Address                                                                                                                    | Purpose                                |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| **Vault**          | [`0xECF933ccDf7ebc6a0658c77E070cFe51ebe5328A`](https://polygonscan.com/address/0xECF933ccDf7ebc6a0658c77E070cFe51ebe5328A) | `LeveragedPredictionVaultV1`           |
| **pUSD**           | [`0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB`](https://polygonscan.com/address/0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB) | Polymarket USD (pUSD)                  |
| **Polymarket CTF** | [`0x4D97DCd97eC945f40cF65F87097ACe5EA0476045`](https://polygonscan.com/address/0x4D97DCd97eC945f40cF65F87097ACe5EA0476045) | Polymarket Conditional Token Framework |

**Chain:** Polygon mainnet (EVM chain ID `137`).

The vault is the **only** contract you approve pUSD against. It never holds user funds for longer than it takes to execute an open/close against Polymarket, and it cannot move your pUSD for any purpose other than the position you authorize via signed quote parameters.

***

## Sandbox

Test pUSD. Fake Polymarket CTF. No real funds at risk. See [Sandbox](/for-developers/getting-started/sandbox) for how to get access.

| Contract      | Address                                                                                                                    | Purpose                          |
| ------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| **Vault**     | [`0xefF55c64B8E971eEbBbbc2037593588e0F271dE2`](https://polygonscan.com/address/0xefF55c64B8E971eEbBbbc2037593588e0F271dE2) | `LeveragedPredictionVaultV1`     |
| **Test pUSD** | [`0xD477EDbe627E94639d7E92119Ca62a461c6ce555`](https://polygonscan.com/address/0xD477EDbe627E94639d7E92119Ca62a461c6ce555) | Minted to your wallet on request |
| **Fake CTF**  | [`0x8B859B4459A75b50e07D345F6b1998F27540F758`](https://polygonscan.com/address/0x8B859B4459A75b50e07D345F6b1998F27540F758) | Fake Conditional Token Framework |

**Chain:** Polygon mainnet (EVM chain ID `137`).

The sandbox vault runs the same `LeveragedPredictionVaultV1` contract as production — same ABI, same signature scheme, same position lifecycle. The only differences are the addresses and that the tokens it pulls and settles against are worthless outside the sandbox.

***

## Verifying a quote signature

Every quote returned by `POST /prediction-markets/quotes` is signed by a server-side authority key. Before you submit the quote on-chain, recover the signer from the signature and compare it to the authority address in `GET /prediction-markets/contract-info`. Any quote where the recovered signer does not match must be rejected — it has been tampered with in transit.

See [API Reference — Verifying a quote signature](/for-developers/api-and-events/api-reference#verifying-a-quote-signature) for the exact message hash layout and a working ethers.js example.

***

### Dynamic lookup

{% tabs %}
{% tab title="REST API" %}

```bash
curl https://api.dimes.fi/v1/prediction-markets/contract-info \
  -H "Authorization: Bearer <jwt>"
```

{% endtab %}

{% tab title="SDK" %}

```typescript
const info = await client.getContractInfo();
console.log(info.polygonVaultContractAddress, info.polygonUsdcTokenAddress);
```

{% endtab %}

{% tab title="React" %}

```tsx
import {useContractInfo} from "@dimes-dot-fi/sdk/react";

function ContractInfo() {
  const {data: info} = useContractInfo();
  // info.polygonVaultContractAddress, info.polygonUsdcTokenAddress
}
```

{% endtab %}
{% endtabs %}

***

## Getting help

Find us on the Telegram link at [dimes.fi](https://dimes.fi) if you spot an address discrepancy, need the current sandbox contracts, or want us to sign off on your contract allowlist before you ship.


# On-Chain Integration

How to submit on-chain transactions to open and close leveraged positions, and what the backend handles automatically after each step.

Covers the on-chain transactions your integration must submit. For quote parameters and field mappings, see the [API Reference](/for-developers/api-and-events/api-reference). For the full flow end-to-end, see the [Quickstart](/for-developers/getting-started/quickstart).

> **Runnable examples:** [`03-open-position-onchain.ts`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/03-open-position-onchain.ts) (approve → verify signature → `createPosition` with viem) and [`04-positions-and-close.ts`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/04-positions-and-close.ts) (list positions, request close). Both are type-checked against the SDK in CI.

***

## Position lifecycle

Two user-initiated on-chain transactions. Everything else is handled by the Multiply backend.

```
User action                          Backend (automatic)
───────────                          ───────────────────
1. Approve pUSD
2. Call createPosition() ──────────► Detect event
                                     Place exchange order
                                     Finalize on-chain
                                     ──► position.opened WebSocket event

3. Call requestClose() ────────────► Detect event
                                     Sell tokens on exchange
                                     Distribute proceeds on-chain
                                     ──► position.closed WebSocket event

Other exits (no user action needed):
  Market resolves ─────────────────► Settle all positions
  Price breaches threshold ────────► Liquidate position
```

See [WebSocket Events](/for-developers/api-and-events/websocket) for event payloads.

***

## Wallet types

The vault doesn't care whether `msg.sender` is an EOA or smart-contract wallet:

* **Direct EOA** — covered below.
* **Polymarket Safe**, **Polymarket Proxy**, **Polymarket Deposit Wallet** — smart-contract wallets provisioned by Polymarket. Deposit wallets are the default for new API users and use a different opening flow. See [Smart Wallet Integration](/for-developers/wallet-integration/smart-wallet-integration).

**Critical:** The `wallet_address` you pass to `POST /prediction-markets/quotes` must be the address that will be `msg.sender` on-chain. For smart-contract wallets that's the **contract address**, not the owner EOA. The wrong address fails the on-chain signature check.

***

## Opening a position (Direct EOA)

### 1. Approve pUSD

The vault pulls pUSD from the caller via `transferFrom`. Approve the vault to spend the required amount first.

{% tabs %}
{% tab title="ethers.js" %}

```javascript
import {ethers} from "ethers";

const usdc = new ethers.Contract(USDC_ADDRESS, ["function approve(address,uint256)"], signer);
const approvalAmount = BigInt(quote.total_user_amount_usd_pips) * 100n;
await usdc.approve(quote.polygon_vault_contract_address, approvalAmount);
```

{% endtab %}

{% tab title="SDK (viem)" %}

```typescript
import {buildApproveTx} from "@dimes-dot-fi/sdk/contract";

const approveTx = buildApproveTx(usdcAddress, vaultAddress, approvalAmount);
await walletClient.writeContract(approveTx);
```

{% endtab %}
{% endtabs %}

### 2. Call `createPosition`

All parameters come directly from the [quote response](/for-developers/api-and-events/api-reference).

{% tabs %}
{% tab title="ethers.js" %}

```javascript
const VAULT_ABI = [
  "function createPosition(bytes16,bytes32,uint256,uint256,uint32,uint256,uint16,uint16,uint16,uint256,bytes,uint256) external",
];

const vault = new ethers.Contract(quote.polygon_vault_contract_address, VAULT_ABI, signer);

const tx = await vault.createPosition(
  quote.position_seed_hex,
  quote.polymarket_market_id,
  BigInt(quote.polymarket_token_id),
  BigInt(quote.collateral_usdc_units),
  quote.leverage_bps,
  BigInt(quote.notional_usdc_units),
  quote.origination_fee_bps,
  quote.lifetime_fee_apr_bps,
  quote.liquidation_fee_bps,
  BigInt(quote.expected_open_trading_fee_usdc_units),
  quote.contract_signature,
  BigInt(quote.signature_expiry),
);
await tx.wait();
```

{% endtab %}

{% tab title="SDK (viem)" %}

```typescript
import {buildCreatePositionTx, verifyQuoteSignature} from "@dimes-dot-fi/sdk/contract";

// Verify the quote signature against the contract-info endpoint (cached per-client)
await verifyQuoteSignature(client, quote, userAddress);

const createTx = buildCreatePositionTx(quote);
await walletClient.writeContract(createTx);
```

{% endtab %}
{% endtabs %}

### 3. What happens next (automatic)

1. Backend detects the `PositionCreated` event
2. Places an exchange order for prediction market tokens
3. Finalizes on-chain via `finalizeOpen`
4. Emits `position.opened` over [WebSocket](/for-developers/api-and-events/websocket)

If the exchange order fails, the backend reverts the position, refunds collateral + origination fee + venue fee, and emits `position.reverted`.

***

## Closing a position

The position owner calls `requestClose` on the vault. The `positionKey` is the `on_chain_position_key` field from `GET /positions`.

{% tabs %}
{% tab title="ethers.js" %}

```javascript
const VAULT_ABI = ["function requestClose(bytes32) external"];
const vault = new ethers.Contract(vaultAddress, VAULT_ABI, signer);

const tx = await vault.requestClose(position.on_chain_position_key);
await tx.wait();
```

{% endtab %}

{% tab title="SDK (viem)" %}

```typescript
import {buildRequestCloseTx} from "@dimes-dot-fi/sdk/contract";

const closeTx = buildRequestCloseTx(vaultAddress, positionKey);
await walletClient.writeContract(closeTx);
```

{% endtab %}
{% endtabs %}

Only the position owner (the wallet that called `createPosition`) can call `requestClose`. The example above is the Direct EOA path — for smart-contract wallets, wrap `requestClose` in the wallet's batch ([Smart Wallet Integration](/for-developers/wallet-integration/smart-wallet-integration)).

After the transaction confirms, the backend sells tokens, distributes proceeds, and emits `position.closed` over [WebSocket](/for-developers/api-and-events/websocket).

***

## Other exit paths

No user action required for these — the backend handles them automatically.

* **Settlement:** When a market resolves, all positions are settled. Emits `position.settled`.
* **Liquidation:** When margin health approaches zero, the position is liquidated. Emits `position.liquidated`. The liquidation price is on every position object at `risk.current_liquidation_price_usd`.

***

## Contract ABIs

### Vault

The full canonical vault ABI — every external function, event, and custom error — is published in the official UI repo:

[`canonical-leveraged-prediction-vault-v1-abi.json`](https://github.com/dimes-fi/dimes-demo-ui/blob/main/src/contract/canonical-leveraged-prediction-vault-v1-abi.json)

Use it directly so your client can decode revert reasons (e.g. `InsufficientCapital`, `SignatureExpired`, `UserCapitalExceeded`) into named errors instead of raw selectors. The two methods you'll typically call:

```json
[
  "function createPosition(bytes16 positionSeed, bytes32 marketId, uint256 tokenId, uint256 collateralUsdcUnits, uint32 leverageBps, uint256 notionalUsdcUnits, uint16 originationFeeBps, uint16 lifetimeFeeAprBps, uint16 liquidationFeeBps, uint256 venueFeeUsdcUnits, bytes signature, uint256 signatureExpiry) external",
  "function requestClose(bytes32 positionKey) external"
]
```

The vault address is returned in every quote response as `polygon_vault_contract_address`, and from `GET /prediction-markets/contract-info`.


# Smart Wallet Integration

How to open and close leveraged positions from Polymarket smart-contract wallets — Safe, Proxy, and the push-funded deposit wallet flow used by new API users.

Most users hold a Polymarket smart-contract wallet rather than a plain EOA. The vault does not care whether `msg.sender` is an EOA or a contract, but each wallet type submits transactions differently. This guide covers all three Polymarket wallet patterns.

For the core position lifecycle, the Direct EOA flow, and contract ABIs, see [On-Chain Integration](/for-developers/wallet-integration/on-chain-integration).

***

## Which wallet type?

| Pattern                       | Description                                                                 | `msg.sender` to vault       |
| ----------------------------- | --------------------------------------------------------------------------- | --------------------------- |
| **Polymarket Safe**           | Standard 1-of-1 Gnosis Safe provisioned by Polymarket for some users        | The Safe contract           |
| **Polymarket Proxy**          | EIP-1167 minimal proxy provisioned by Polymarket for some users             | The Proxy contract          |
| **Polymarket Deposit Wallet** | ERC-1967 proxy deployed via Polymarket's deposit wallet factory (new users) | The deposit wallet contract |

Polymarket provisions one of these depending on how the user registered. **Deposit wallets are the default for new API users.**

To detect a type, call `eth_getCode`: a Safe has \~250 bytes of bytecode, a Proxy \~92 bytes, a deposit wallet \~252 bytes, an EOA none. Or call `getOwners()` — Safes respond, others revert.

**Critical:** The `wallet_address` you pass to `POST /prediction-markets/quotes` must be the address that will be `msg.sender` on-chain — for all three patterns that is the **contract address**, not the owner EOA. The wrong address fails the on-chain signature check.

***

## Polymarket Safe

Encode the vault calldata normally, wrap it in a SafeTx, have the owner sign, submit `execTransaction`.

```javascript
import {ethers} from "ethers";

const SAFE_ABI = [
  "function nonce() view returns (uint256)",
  "function getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256) view returns (bytes32)",
  "function execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes) returns (bool)",
];

const safe = new ethers.Contract(safeAddress, SAFE_ABI, provider);
const vault = new ethers.Contract(vaultAddress, VAULT_ABI, provider);

// Encode the vault call
const data = vault.interface.encodeFunctionData("createPosition", [
  quote.position_seed_hex,
  quote.polymarket_market_id,
  BigInt(quote.polymarket_token_id),
  BigInt(quote.collateral_usdc_units),
  quote.leverage_bps,
  BigInt(quote.notional_usdc_units),
  quote.origination_fee_bps,
  quote.lifetime_fee_apr_bps,
  quote.liquidation_fee_bps,
  BigInt(quote.expected_open_trading_fee_usdc_units),
  quote.contract_signature,
  BigInt(quote.signature_expiry),
]);

// Build SafeTx and get the hash
const nonce = await safe.nonce();
const safeTxHash = await safe.getTransactionHash(
  vaultAddress, 0, data, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, nonce,
);

// Owner signs the hash
const signature = await ownerSigner.signMessage(ethers.getBytes(safeTxHash));

// Anyone can submit (owner, partner backend, or relayer)
const tx = await safe.connect(submitterSigner).execTransaction(
  vaultAddress, 0, data, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, signature,
);
await tx.wait();
```

pUSD approval must be a separate SafeTx before `createPosition`, or batched via Safe's MultiSend. `requestClose` follows the same pattern — encode `requestClose(positionKey)` as the inner call.

***

## Polymarket Proxy

The owner EOA calls `factory.proxy()` directly. pUSD approval can be batched in the same call.

```javascript
import {ethers} from "ethers";

const PROXY_FACTORY = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052";
const PROXY_FACTORY_ABI = [
  "function proxy(tuple(address to, uint256 typeCode, bytes data, uint256 value)[]) payable returns (bytes[])",
];

const factory = new ethers.Contract(PROXY_FACTORY, PROXY_FACTORY_ABI, ownerSigner);
const vault = new ethers.Contract(vaultAddress, VAULT_ABI, provider);
const usdc = new ethers.Contract(usdcAddress, ["function approve(address,uint256)"], provider);

const approveData = usdc.interface.encodeFunctionData("approve", [vaultAddress, approvalAmount]);
const createData = vault.interface.encodeFunctionData("createPosition", [
  quote.position_seed_hex,
  quote.polymarket_market_id,
  BigInt(quote.polymarket_token_id),
  BigInt(quote.collateral_usdc_units),
  quote.leverage_bps,
  BigInt(quote.notional_usdc_units),
  quote.origination_fee_bps,
  quote.lifetime_fee_apr_bps,
  quote.liquidation_fee_bps,
  BigInt(quote.expected_open_trading_fee_usdc_units),
  quote.contract_signature,
  BigInt(quote.signature_expiry),
]);

// Batch approve + createPosition in one transaction
const tx = await factory.proxy([
  {to: usdcAddress, typeCode: 1, data: approveData, value: 0},
  {to: vaultAddress, typeCode: 1, data: createData, value: 0},
]);
await tx.wait();
```

The owner EOA must call `factory.proxy()` directly — the proxy does not support off-chain signatures, and Polymarket's hosted relayer will not relay vault calls for it.

***

## Polymarket Deposit Wallet

Polymarket's newest wallet type and the default for new API users. They require a different opening flow than Safe and Proxy.

### Why deposit wallets need the push-funded flow

The legacy `createPosition` path pulls pUSD with `transferFrom`, which requires the caller to first `approve` the vault. **The Polymarket relayer blocks `approve` to non-whitelisted spenders**, so a deposit wallet can never grant that allowance.

Instead, the vault exposes a **push-funded** path: the deposit wallet *pushes* pUSD to the vault inside an atomic 3-call batch, and the vault verifies the exact balance delta. No approval needed.

```
Deposit wallet batch (atomic — all-or-nothing):

  1. vault.reserveDeposit(totalUserAmount)    snapshot the vault pUSD balance
  2. pUSD.transfer(vault, totalUserAmount)    push the funds directly
  3. vault.createPositionPushFunded(...)      verify the delta, create the position
```

> **⚠️ Only pUSD is accepted.** Step 2 must transfer **pUSD** — the exact token returned as `polygon_usdc_token_address` from `GET /prediction-markets/contract-info`. Pushing any other token (e.g. USDC.e, native USDC, or any bridged variant) will not register as a balance delta, and `createPositionPushFunded` will revert the whole batch. Always source the transfer target from `contract-info`; never hardcode a stablecoin address.

`reserveDeposit` records a transient snapshot of the vault's pUSD balance. `createPositionPushFunded` re-reads the balance and requires the delta to **exactly** equal the position's total amount — any over- or under-funding reverts. The three calls are signed and submitted together, so they cannot be split or reordered.

`createPositionPushFunded` takes the **same 12 arguments** as `createPosition` and accepts the **same `contract_signature`** from the quote. Only the funding mechanism differs.

### The deposit wallet batch

A deposit wallet executes calls through Polymarket's factory:

```solidity
factory.proxy(Batch[] batches, bytes[] signatures)

struct Batch {address wallet; uint256 nonce; uint256 deadline; Call[] calls;}
struct Call  {address target; uint256 value; bytes data;}
```

The wallet owner signs each `Batch` with EIP-712. The relayer submits `factory.proxy(...)` on-chain.

| Constant         | Value                                                                                              |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| Factory address  | `0x00000000000Fb5C9ADea0298D729A0CB3823Cc07`                                                       |
| EIP-712 domain   | `{ name: "DepositWallet", version: "1", chainId: 137, verifyingContract: <depositWalletAddress> }` |
| Relayer endpoint | `POST https://relayer-v2.polymarket.com/submit`                                                    |

### Opening a position

{% tabs %}
{% tab title="SDK (viem)" %}

```typescript
import {
  buildPushFundedCreateCalls,
  buildDepositWalletBatch,
  getDepositWalletBatchTypedData,
  buildRelayerSubmitPayload,
  buildRelayerAuthHeaders,
  polymarketRelayerBaseUrl,
} from "@dimes-dot-fi/sdk/contract";

// 1. Build the 3-call push-funded batch (reserveDeposit + transfer + createPositionPushFunded).
//    `pUsdAddress` is the pUSD token address from GET /prediction-markets/contract-info.
const calls = buildPushFundedCreateCalls(quote, pUsdAddress);

// 2. Read the deposit wallet nonce, assemble and sign the batch.
const nonce = await publicClient.readContract({
  address: depositWalletAddress,
  abi: [{ type: "function", name: "nonce", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] }],
  functionName: "nonce",
});
const deadline = BigInt(Math.floor(Date.now() / 1000) + 240);
const batch = buildDepositWalletBatch({ depositWalletAddress, nonce, deadline, calls });
const signature = await walletClient.signTypedData(getDepositWalletBatchTypedData(batch));

// 3. Submit to the Polymarket relayer (builder API credentials required).
const body = JSON.stringify(buildRelayerSubmitPayload({ ownerAddress, batch, signature }));
const headers = await buildRelayerAuthHeaders({ apiKey, apiSecret, apiPassphrase, body });
const res = await fetch(`${polymarketRelayerBaseUrl}/submit`, { method: "POST", headers, body });
const { transactionID } = await res.json();
```

{% endtab %}

{% tab title="ethers.js" %}

```javascript
import {ethers} from "ethers";

const FACTORY = "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07";
const vault = new ethers.Contract(vaultAddress, VAULT_ABI, provider);
const usdc = new ethers.Contract(usdcAddress, ["function transfer(address,uint256)"], provider);
const dw = new ethers.Contract(depositWalletAddress, ["function nonce() view returns (uint256)"], provider);

// reserveDeposit / createPositionPushFunded are not in the legacy VAULT_ABI — add them:
const PUSH_FUNDED_ABI = new ethers.Interface([
  "function reserveDeposit(uint256 expectedAmountUsdcUnits)",
  "function createPositionPushFunded(bytes16,bytes32,uint256,uint256,uint32,uint256,uint16,uint16,uint16,uint256,bytes,uint256)",
]);

const totalUserAmount = BigInt(quote.total_user_amount_usdc_units);

// 1. Encode the three inner calls
const reserveData = PUSH_FUNDED_ABI.encodeFunctionData("reserveDeposit", [totalUserAmount]);
const transferData = usdc.interface.encodeFunctionData("transfer", [vaultAddress, totalUserAmount]);
const createData = PUSH_FUNDED_ABI.encodeFunctionData("createPositionPushFunded", [
  quote.position_seed_hex,
  quote.polymarket_market_id,
  BigInt(quote.polymarket_token_id),
  BigInt(quote.collateral_usdc_units),
  quote.leverage_bps,
  BigInt(quote.notional_usdc_units),
  quote.origination_fee_bps,
  quote.lifetime_fee_apr_bps,
  quote.liquidation_fee_bps,
  BigInt(quote.expected_open_trading_fee_usdc_units),
  quote.contract_signature,
  BigInt(quote.signature_expiry),
]);

const calls = [
  {target: vaultAddress, value: 0, data: reserveData},
  {target: usdcAddress,  value: 0, data: transferData},
  {target: vaultAddress, value: 0, data: createData},
];

// 2. Build and sign the Batch (EIP-712)
const nonce = await dw.nonce();
const deadline = BigInt(Math.floor(Date.now() / 1000) + 240);
const batch = {wallet: depositWalletAddress, nonce, deadline, calls};

const signature = await ownerSigner.signTypedData(
  {name: "DepositWallet", version: "1", chainId: 137, verifyingContract: depositWalletAddress},
  {
    Batch: [
      {name: "wallet", type: "address"}, {name: "nonce", type: "uint256"},
      {name: "deadline", type: "uint256"}, {name: "calls", type: "Call[]"},
    ],
    Call: [
      {name: "target", type: "address"}, {name: "value", type: "uint256"},
      {name: "data", type: "bytes"},
    ],
  },
  batch,
);

// 3. Submit via the Polymarket relayer (see "Relayer submission" below)
await submitToRelayer({ownerAddress, depositWalletAddress, batch, signature});
```

{% endtab %}
{% endtabs %}

### Closing a position

Same batch mechanics with a single inner call: `requestClose(positionKey)`. The `positionKey` is the `on_chain_position_key` field from `GET /positions`.

{% tabs %}
{% tab title="SDK (viem)" %}

```typescript
import {
  buildRequestCloseCalls,
  buildDepositWalletBatch,
  getDepositWalletBatchTypedData,
  buildRelayerSubmitPayload,
  buildRelayerAuthHeaders,
  polymarketRelayerBaseUrl,
} from "@dimes-dot-fi/sdk/contract";

const calls = buildRequestCloseCalls(vaultAddress, position.on_chain_position_key);

const deadline = BigInt(Math.floor(Date.now() / 1000) + 240);
const batch = buildDepositWalletBatch({ depositWalletAddress, nonce, deadline, calls });
const signature = await walletClient.signTypedData(getDepositWalletBatchTypedData(batch));

const body = JSON.stringify(buildRelayerSubmitPayload({ ownerAddress, batch, signature }));
const headers = await buildRelayerAuthHeaders({ apiKey, apiSecret, apiPassphrase, body });
await fetch(`${polymarketRelayerBaseUrl}/submit`, { method: "POST", headers, body });
```

{% endtab %}

{% tab title="ethers.js" %}

```javascript
const closeData = vault.interface.encodeFunctionData("requestClose", [position.on_chain_position_key]);
const calls = [{target: vaultAddress, value: 0, data: closeData}];

const nonce = await dw.nonce();
const deadline = BigInt(Math.floor(Date.now() / 1000) + 240);
const batch = {wallet: depositWalletAddress, nonce, deadline, calls};

// Sign the Batch with the same EIP-712 types as the opening flow, then submit to the relayer.
const signature = await ownerSigner.signTypedData(domain, types, batch);
await submitToRelayer({ownerAddress, depositWalletAddress, batch, signature});
```

{% endtab %}
{% endtabs %}

Only the position owner — the deposit wallet that called `createPositionPushFunded` — can close the position. After confirmation the backend sells the tokens, distributes proceeds, and emits `position.closed` over [WebSocket](/for-developers/api-and-events/websocket).

### Relayer submission

The relayer authenticates every request with an HMAC over the request body. The SDK's `buildRelayerSubmitPayload` and `buildRelayerAuthHeaders` produce both; the manual shape is:

```javascript
import {createHmac} from "node:crypto";

async function submitToRelayer({ownerAddress, depositWalletAddress, batch, signature}) {
  const payload = {
    type: "WALLET",
    from: ownerAddress,
    to: "0x00000000000Fb5C9ADea0298D729A0CB3823Cc07", // factory
    nonce: batch.nonce.toString(),
    signature,
    depositWalletParams: {
      depositWallet: depositWalletAddress,
      deadline: batch.deadline.toString(),
      calls: batch.calls.map((c) => ({target: c.target, value: c.value.toString(), data: c.data})),
    },
  };
  const body = JSON.stringify(payload);

  // HMAC-SHA256 over `timestamp + "POST" + "/submit" + body`, base64url-encoded.
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const sig = createHmac("sha256", Buffer.from(builderApiSecret, "base64"))
    .update(timestamp + "POST" + "/submit" + body)
    .digest("base64")
    .replaceAll("+", "-").replaceAll("/", "_");

  const res = await fetch("https://relayer-v2.polymarket.com/submit", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      POLY_BUILDER_API_KEY: builderApiKey,
      POLY_BUILDER_TIMESTAMP: timestamp,
      POLY_BUILDER_PASSPHRASE: builderApiPassphrase,
      POLY_BUILDER_SIGNATURE: sig,
    },
    body,
  });
  const {transactionID} = await res.json();

  // Poll GET /transaction?id=<transactionID> until state is STATE_MINED / STATE_CONFIRMED.
  return transactionID;
}
```

Submission returns a `transactionID`. Poll `GET https://relayer-v2.polymarket.com/transaction?id=<transactionID>` until `state` is `STATE_MINED` or `STATE_CONFIRMED` (terminal failures: `STATE_FAILED`, `STATE_CANCELLED`, `STATE_INVALID`).

### Constraints

* **One push-funded create per transaction.** The `reserveDeposit` lock allows a single push-funded create per batch; do not bundle two.
* **Exact funding.** The pushed amount must equal `total_user_amount_usdc_units` exactly. The SDK helper sources this from the quote, so reuse it for both `reserveDeposit` and `transfer`.
* **Signature expiry.** `signature_expiry` from the quote is short-lived — sign and submit the batch promptly, and keep the batch `deadline` tight (e.g. now + 240s).
* The relayer can refuse to submit a batch but cannot split, reorder, or alter it — the EIP-712 batch signature commits to the whole `Call[]` array.

See [Polymarket's deposit wallet docs](https://docs.polymarket.com/trading/deposit-wallet-migration) for wallet deployment and builder API key setup.


# Privy Integration

Open and close leveraged positions from a Privy wallet — an embedded EOA via the wagmi connector, or an ERC-4337 smart account via batched userOps.

A Privy embedded wallet is an EOA and ships with a **wagmi connector**, so it drops into the [Direct EOA flow](/for-developers/wallet-integration/on-chain-integration) unchanged — the vault sees an ordinary `msg.sender`. Enabling Privy **smart wallets** instead makes `msg.sender` an ERC-4337 account; see [Smart wallets](#smart-wallets-account-abstraction).

Both flows are verified end-to-end in the [demo UI](https://github.com/dimes-fi/dimes-demo-ui) (`WalletProviders.tsx`, `config.privy.ts`, `ConnectControls.tsx`).

***

## 1. Wrap your app

Use Privy's `WagmiProvider` (from `@privy-io/wagmi`), not wagmi's own — it wires the connector and auto-connect.

```bash
npm install @privy-io/react-auth @privy-io/wagmi
```

```tsx
import {PrivyProvider} from "@privy-io/react-auth";
import {WagmiProvider, createConfig} from "@privy-io/wagmi";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {http} from "wagmi";
import {polygon} from "wagmi/chains";

// No `connectors` — PrivyProvider injects the wallet connector.
const wagmiConfig = createConfig({
  chains: [polygon],
  transports: {[polygon.id]: http()},
});

const queryClient = new QueryClient();

export function Providers({children}: {children: React.ReactNode}) {
  return (
    <PrivyProvider
      appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{
        defaultChain: polygon,
        supportedChains: [polygon],
        embeddedWallets: {ethereum: {createOnLogin: "users-without-wallets"}},
      }}
    >
      <QueryClientProvider client={queryClient}>
        <WagmiProvider config={wagmiConfig}>{children}</WagmiProvider>
      </QueryClientProvider>
    </PrivyProvider>
  );
}
```

`appId` is a public client identifier. Get it from the [Privy dashboard](https://dashboard.privy.io).

***

## 2. Read the wallet address

The address that signs on-chain is the `wallet_address` you must quote for. After login, read it from wagmi:

```tsx
const {ready, authenticated, login} = usePrivy();
const {address} = useAccount();
```

***

## 3. Open and close

Each call is a wagmi `writeContract`, signed by Privy — identical to the [Direct EOA flow](/for-developers/wallet-integration/on-chain-integration#opening-a-position-direct-eoa).

{% tabs %}
{% tab title="SDK (viem)" %}

```typescript
import {useWalletClient} from "wagmi";
import {
  buildApproveTx,
  buildCreatePositionTx,
  buildRequestCloseTx,
  verifyQuoteSignature,
} from "@dimes-dot-fi/sdk/contract";

const {data: walletClient} = useWalletClient();

// Approve pUSD, verify the quote signature, open.
await walletClient.writeContract(
  buildApproveTx(pUsdAddress, quote.polygonVaultContractAddress, approvalAmount),
);
await verifyQuoteSignature(client, quote, address);
await walletClient.writeContract(buildCreatePositionTx(quote));

// Close later — only the owner (this wallet) can call requestClose.
await walletClient.writeContract(
  buildRequestCloseTx(vaultAddress, position.on_chain_position_key),
);
```

{% endtab %}

{% tab title="wagmi hooks" %}

```typescript
import {useWriteContract} from "wagmi";

const {writeContractAsync} = useWriteContract();

// Args come straight from the quote response.
await writeContractAsync({
  address: quote.polygonVaultContractAddress,
  abi: VAULT_ABI,
  functionName: "createPosition",
  args: [
    quote.positionSeedHex,
    quote.polymarketMarketId,
    BigInt(quote.polymarketTokenId),
    BigInt(quote.collateralUsdcUnits),
    quote.leverageBps,
    BigInt(quote.notionalUsdcUnits),
    quote.originationFeeBps,
    quote.lifetimeFeeAprBps,
    quote.liquidationFeeBps,
    BigInt(quote.expectedOpenTradingFeeUsdcUnits),
    quote.contractSignature,
    BigInt(quote.signatureExpiry),
  ],
});
```

{% endtab %}
{% endtabs %}

The backend handles the rest — exchange order on open, proceeds on close, with `position.opened` / `position.closed` over [WebSocket](/for-developers/api-and-events/websocket).

***

## Smart wallets (Account Abstraction)

Enable *Smart wallets* in the dashboard and each user also gets an ERC-4337 account that becomes `msg.sender`. Two changes from the EOA flow:

1. **Quote for the smart account.** `wallet_address` is `client.account.address` (the contract that signs), not the owner EOA.
2. **Submit via the smart-wallet client**, batching approve + open into one userOperation.

```typescript
import {useSmartWallets} from "@privy-io/react-auth/smart-wallets";
import {encodeFunctionData} from "viem";
import {buildApproveTx, buildCreatePositionTx} from "@dimes-dot-fi/sdk/contract";

const {client} = useSmartWallets();           // undefined until a smart wallet exists

const approve = buildApproveTx(pUsdAddress, quote.polygonVaultContractAddress, approvalAmount);
const create = buildCreatePositionTx(quote);

const hash = await client.sendTransaction({
  account: client.account,                    // smart account = msg.sender
  calls: [
    {to: approve.address, data: encodeFunctionData(approve), value: 0n},
    {to: create.address,  data: encodeFunctionData(create),  value: 0n},
  ],
});
```

Closing is the same with a single `requestClose` call.

A 4337 wallet needs a **bundler** (Privy dashboard). Use a keyed endpoint — `https://api.pimlico.io/v2/137/rpc?apikey=<key>`; the keyless `public.pimlico.io` blocks browser CORS. Add a **paymaster** for gasless opens; without one the smart account pays its own gas (fund it with POL — the first userOp also deploys it).

Verified end-to-end in the demo UI (`contract/smartWalletHooks.ts`, `SmartWalletBridge` in `WalletProviders.tsx`).

***

## Notes

* **Funding.** `useFundWallet` opens Privy's on-ramp/transfer flow; or send pUSD and a little POL to the wallet directly.
* **Export.** `useExportWallet` reveals the embedded key for self-custody.
* **One switch.** In the demo UI the whole RainbowKit ↔ Privy swap is gated on `VITE_PRIVY_APP_ID`; the contract layer is untouched.


# Turnkey Integration

How to open and close leveraged positions from a Turnkey embedded wallet by bridging Turnkey's signer into wagmi with a small custom connector.

[Turnkey](https://turnkey.com) provisions embedded wallets from an email-OTP, passkey, or OAuth login via its **Auth Proxy** (no backend required). A Turnkey embedded wallet is an ordinary EOA, so once you can sign with it, the on-chain path is the standard Direct EOA flow — `approve` / `createPosition` / `requestClose` over `viem`.

The one difference from [Privy](/for-developers/wallet-integration/privy-integration): **Turnkey ships no wagmi connector**, so if your app is built on wagmi you bridge Turnkey's signer in with a small custom connector. That bridge is the only Turnkey-specific code; nothing in the contract layer changes.

For the core lifecycle, ABIs, and the Direct EOA snippets, see [On-Chain Integration](/for-developers/wallet-integration/on-chain-integration).

***

## 1. Install and wrap your app

```bash
npm install @turnkey/react-wallet-kit @turnkey/viem
```

```tsx
import {TurnkeyProvider} from "@turnkey/react-wallet-kit";
import "@turnkey/react-wallet-kit/styles.css";

export function Providers({children}: {children: React.ReactNode}) {
  return (
    <TurnkeyProvider
      config={{
        organizationId: import.meta.env.VITE_TURNKEY_ORG_ID,
        authProxyConfigId: import.meta.env.VITE_TURNKEY_AUTH_PROXY_CONFIG_ID,
      }}
    >
      {children}
    </TurnkeyProvider>
  );
}
```

Both ids come from the [Turnkey dashboard](https://dashboard.turnkey.com): your **organization ID** and the **WalletKit / Auth Proxy config ID** (enable Auth Proxy and pick your auth methods first). Both are public client identifiers.

***

## 2. Log in and derive a viem account

`useTurnkey()` exposes the login trigger, the authenticated client, and the user's wallets. Each wallet account carries its own (sub-)`organizationId` — that's what `createAccount` must sign under.

```tsx
import {useTurnkey, AuthState} from "@turnkey/react-wallet-kit";
import {createAccount} from "@turnkey/viem";

function useTurnkeyAccount() {
  const {authState, httpClient, wallets} = useTurnkey();

  async function buildAccount() {
    if (authState !== AuthState.Authenticated || !httpClient) return null;
    const eth = wallets
      .flatMap((w) => w.accounts ?? [])
      .find((a) => a.addressFormat === "ADDRESS_FORMAT_ETHEREUM");
    if (!eth) return null;

    // A viem LocalAccount that signs through Turnkey.
    return createAccount({
      client: httpClient,
      organizationId: eth.organizationId,
      signWith: eth.address,
    });
  }

  return buildAccount;
}
```

`handleLogin()` from `useTurnkey()` opens the login modal.

***

## 3a. Open and close — plain viem (no wagmi)

If you are **not** on wagmi, use the Turnkey account directly with a viem walletClient. This is the simplest path.

```typescript
import {createWalletClient, http} from "viem";
import {polygon} from "viem/chains";
import {
  buildApproveTx,
  buildCreatePositionTx,
  buildRequestCloseTx,
  verifyQuoteSignature,
} from "@dimes-dot-fi/sdk/contract";

const account = await buildAccount();           // from step 2
const walletClient = createWalletClient({account, chain: polygon, transport: http()});

// Pass account.address as `wallet_address` when requesting the quote.

await walletClient.writeContract(
  buildApproveTx(usdcAddress, quote.polygonVaultContractAddress, approvalAmount),
);
await verifyQuoteSignature(client, quote, account.address);
await walletClient.writeContract(buildCreatePositionTx(quote));

// later
await walletClient.writeContract(
  buildRequestCloseTx(vaultAddress, position.on_chain_position_key),
);
```

***

## 3b. Open and close — bridging into wagmi

If your app already uses wagmi (so `useWriteContract`, `useAccount`, etc. just work), wrap the Turnkey account in an **EIP-1193 provider** and expose it through a custom wagmi connector. Delegate signing/sending to a viem walletClient and let reads fall through to the RPC:

```typescript
import {
  createPublicClient, createWalletClient, http,
  hexToBigInt, hexToNumber, numberToHex,
  type Chain, type EIP1193Provider, type LocalAccount,
} from "viem";

export function createTurnkeyProvider(account: LocalAccount, chain: Chain): EIP1193Provider {
  const transport = http();
  const publicClient = createPublicClient({chain, transport});
  const walletClient = createWalletClient({account, chain, transport});

  const request = async ({method, params = []}: {method: string; params?: any[]}) => {
    switch (method) {
      case "eth_requestAccounts":
      case "eth_accounts":
        return [account.address];
      case "eth_chainId":
        return numberToHex(chain.id);
      case "personal_sign":
        return walletClient.signMessage({message: {raw: params[0]}});
      case "eth_signTypedData_v4":
        return account.signTypedData(
          typeof params[1] === "string" ? JSON.parse(params[1]) : params[1],
        );
      case "eth_sendTransaction": {
        const tx = params[0];
        return walletClient.sendTransaction({
          account, chain,
          to: tx.to, data: tx.data,
          value: tx.value ? hexToBigInt(tx.value) : undefined,
          gas: tx.gas ? hexToBigInt(tx.gas) : undefined,
          nonce: tx.nonce != null ? hexToNumber(tx.nonce) : undefined,
        });
      }
      default:
        return publicClient.request({method, params} as any);
    }
  };
  return {request, on: () => {}, removeListener: () => {}} as unknown as EIP1193Provider;
}
```

Wrap it in a wagmi connector with `createConnector`, reading the live account from a module-level holder you populate on login (the signer only exists after the user authenticates). Then keep wagmi in sync with the Turnkey session: on login, build the account, set the holder, and call wagmi `connect`; on logout, clear it and `disconnect`. With that in place, the existing `createPosition` / `requestClose` wagmi calls run unchanged.

> The official demo UI implements exactly this — see [`dimes-demo-ui`](https://github.com/dimes-fi/dimes-demo-ui) (`src/turnkey/provider.ts`, `src/turnkey/connector.ts`, and the `TurnkeyBridge` in `src/WalletProviders.tsx`).

***

## Funding the embedded wallet

`handleOnRamp()` from `useTurnkey()` opens the funding flow; or send pUSD and a little POL to the wallet directly. The vault pulls `total_user_amount_usdc_units` of pUSD; the wallet needs POL for gas.

***

## Notes

* **`wallet_address`.** Pass the Turnkey wallet's Ethereum address — the one that signs on-chain — as `wallet_address` to `POST /prediction-markets/quotes`.
* **Sub-organizations.** Each end user gets a Turnkey sub-org; sign under the account's own `organizationId`, not the parent org id.
* **Export.** `handleExportWallet({walletId})` lets a user self-custody.


# API & Events

REST API, authentication, realtime events, and error handling

The core programmatic surface of Multiply: authenticate, call the REST API, subscribe to realtime WebSocket events, and handle errors.

* [Authentication](/for-developers/api-and-events/authentication)
* [API Reference](/for-developers/api-and-events/api-reference)
* [WebSocket Events](/for-developers/api-and-events/websocket)
* [Error Handling](/for-developers/api-and-events/error-handling)


# Authentication

Dimes uses a two-tier authentication model: API keys for partner-level operations and short-lived JWTs for user-scoped actions.

### Overview

| Level       | Mechanism | Header                                    | Use                                        |
| ----------- | --------- | ----------------------------------------- | ------------------------------------------ |
| **Partner** | API key   | `Authorization: Api-Key dm_live_skey_...` | Token generation, partner limits, API keys |
| **User**    | JWT       | `Authorization: Bearer <jwt>`             | Positions, quotes, user limits             |
| **Public**  | None      | —                                         | Market browsing                            |

API keys are environment-scoped. Production keys start with `dm_live_skey_`; sandbox keys start with `dm_sbx_skey_` and only work against `https://api-sandbox.dimes.fi`. See [Environments](/for-developers/getting-started/environments) for the full comparison.

***

### API keys

API keys are issued by the Dimes team on request — reach out via the Telegram link on [dimes.fi](https://dimes.fi). Each key is scoped to a single partner and a single environment, and identifies all requests as coming from that integration.

{% tabs %}
{% tab title="REST API" %}

```bash
curl https://api.dimes.fi/v1/prediction-markets/partners/limits \
  -H "Authorization: Api-Key dm_live_skey_your_api_key"
```

```javascript
const headers = {
  "Authorization": `Api-Key ${process.env.DIMES_API_KEY}`,
  "Content-Type": "application/json",
};
```

{% endtab %}

{% tab title="SDK" %}

```typescript
import { DimesClient, ApiKeyAuth } from "@dimes-dot-fi/sdk";

const client = new DimesClient({
  auth: new ApiKeyAuth({
    apiKey: process.env.DIMES_API_KEY,
    walletAddress: "0x1234...abcd",
  }),
});
```

{% endtab %}
{% endtabs %}

API keys follow the format `<env_prefix>_skey_` followed by a random string:

* `dm_live_skey_...` — production keys, valid against `https://api.dimes.fi`.
* `dm_sbx_skey_...` — sandbox keys, valid against `https://api-sandbox.dimes.fi`.

A key is only valid against the base URL it was issued for. Cross-environment requests are rejected. You can have up to **two active keys** per environment at a time, enabling zero-downtime rotation.

***

### User JWTs

User-scoped endpoints require a JWT, generated via the partner API key. The JWT scopes all queries to a specific `(wallet_address, partner_id)` pair.

#### Generating a token

{% tabs %}
{% tab title="REST API" %}

```bash
curl -X POST https://api.dimes.fi/v1/prediction-markets/tokens \
  -H "Authorization: Api-Key dm_live_skey_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "wallet_address": "0x1234...abcd" }'
```

**Response: `201 Created`**

```json
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": "2025-06-01T11:00:00.000Z"
}
```

| Field        | Description                                  |
| ------------ | -------------------------------------------- |
| `token`      | JWT to use in `Authorization: Bearer` header |
| `expires_at` | Token expiry (1 hour from creation)          |
| {% endtab %} |                                              |

{% tab title="SDK" %}
`ApiKeyAuth` handles token generation and refresh automatically. When you create a client with `ApiKeyAuth`, it exchanges your API key and wallet address for a JWT behind the scenes and refreshes it before expiry.

```typescript
import { DimesClient, ApiKeyAuth } from "@dimes-dot-fi/sdk";

const client = new DimesClient({
  auth: new ApiKeyAuth({
    apiKey: process.env.DIMES_API_KEY,
    walletAddress: "0x1234...abcd",
  }),
});

// No manual token management needed — just make requests
const positions = await client.getPositions();
```

{% endtab %}

{% tab title="React" %}

```tsx
import { DimesProvider } from "@dimes-dot-fi/sdk/react";
import { ApiKeyAuth } from "@dimes-dot-fi/sdk";

function App() {
  return (
    <DimesProvider
      auth={
        new ApiKeyAuth({
          apiKey: process.env.DIMES_API_KEY,
          walletAddress: "0x1234...abcd",
        })
      }
    >
      <YourApp />
    </DimesProvider>
  );
}
```

{% endtab %}
{% endtabs %}

#### Using the token

{% tabs %}
{% tab title="REST API" %}

```bash
curl https://api.dimes.fi/v1/prediction-markets/positions \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
```

```javascript
const userHeaders = {
  "Authorization": `Bearer ${jwt}`,
  "Content-Type": "application/json",
};
```

Tokens are valid for 1 hour. Your backend should generate a new token for each user session or when the current one approaches expiry.
{% endtab %}

{% tab title="SDK" %}
Point `JwtAuth` at your backend's token endpoint. It fetches, caches, and auto-refreshes tokens before they expire:

```typescript
import { DimesClient, JwtAuth } from "@dimes-dot-fi/sdk";

const client = new DimesClient({
  auth: new JwtAuth({ tokenUrl: "https://your-backend.com/api/dimes-token" }),
});
```

Your endpoint should call `POST /v1/prediction-markets/tokens` with your API key server-side and return the `{ token, expires_at }` response.
{% endtab %}
{% endtabs %}

JWTs are safe to pass to your frontend — they scope queries to a single wallet and expire after 1 hour. API keys must never leave your backend.

***

### Key security

Your API key carries full access to your integration — including the ability to generate user tokens and view partner limits. Treat it like a password.

**Do:**

* Store keys in environment variables or a secrets manager.
* Make all Dimes API calls from your backend server.
* Rotate keys immediately if you suspect a leak.

**Don't:**

* Embed keys in client-side code (JavaScript bundles, mobile apps).
* Commit keys to version control.
* Share keys over unencrypted channels.

***

### Key rotation

Zero-downtime rotation with two active keys:

1. Request a new key from the Dimes team via the Telegram link on [dimes.fi](https://dimes.fi).
2. Deploy the new key to your servers.
3. Verify requests succeed with the new key.
4. Ask us to revoke the old key.

Both keys are valid simultaneously until the old one is revoked. There is no automatic expiry.

***

### Error responses

All authentication errors follow the standard error format:

| Status | Type                    | Code                                   | Meaning                          |
| ------ | ----------------------- | -------------------------------------- | -------------------------------- |
| `401`  | `AUTHENTICATION_ERROR`  | `unauthorized`                         | Missing or invalid API key / JWT |
| `400`  | `INVALID_REQUEST_ERROR` | `customer_auth_invalid_wallet_address` | Wallet address failed validation |

```json
{
  "error": {
    "type": "AUTHENTICATION_ERROR",
    "code": "unauthorized",
    "message": "Unauthorized"
  }
}
```

For the full error taxonomy, see [Error Handling](/for-developers/api-and-events/error-handling).


# API Reference

Complete reference for all Dimes Multiply endpoints. For a guided walkthrough, see Quickstart.

**Base URLs:**

| Environment | Base URL                          | API key prefix     |
| ----------- | --------------------------------- | ------------------ |
| Production  | `https://api.dimes.fi/v1`         | `dm_live_skey_...` |
| Sandbox     | `https://api-sandbox.dimes.fi/v1` | `dm_sbx_skey_...`  |

Pick one environment per integration. Keys and URLs are not interchangeable across environments — see [Environments](/for-developers/getting-started/environments) for the full comparison.

**OpenAPI spec:**

* Interactive Swagger UI: [`https://api.dimes.fi/v1/customer-docs`](https://api.dimes.fi/v1/customer-docs) (production) or [`https://api-sandbox.dimes.fi/v1/customer-docs`](https://api-sandbox.dimes.fi/v1/customer-docs) (sandbox)
* Raw JSON: [`https://api.dimes.fi/v1/customer-docs-json`](https://api.dimes.fi/v1/customer-docs-json) (production) or [`https://api-sandbox.dimes.fi/v1/customer-docs-json`](https://api-sandbox.dimes.fi/v1/customer-docs-json) (sandbox)

***

## Conventions

| Convention          | Choice                                             |
| ------------------- | -------------------------------------------------- |
| **Field casing**    | `snake_case`                                       |
| **URL path casing** | `kebab-case`                                       |
| **Pagination**      | Cursor-based                                       |
| **List envelope**   | `{ "data": [...], "has_more": bool }`              |
| **Error format**    | `{ "error": { "type", "code", "message" } }`       |
| **Monetary values** | Pips (string) + dollar (string)                    |
| **IDs**             | Prefixed strings (`dm_pos_`, `dm_off_`, `dm_mkt_`) |

### Monetary values

Every monetary field appears twice — as precise internal units (pips, string) and as a human-readable dollar amount (string):

```json
{
  "entry_price_usd_pips": "5000",
  "entry_price_usd": "0.50",
  "collateral_usd_pips": "500000",
  "collateral_usd": "50.00"
}
```

The `_usd_pips` fields are the source of truth (10,000 pips = $1.00). The `_usd` fields are convenience values for display.

### Pagination

All list endpoints use cursor-based pagination:

| Param            | Type    | Default | Description                                  |
| ---------------- | ------- | ------- | -------------------------------------------- |
| `limit`          | integer | 25      | Results per page (1–100)                     |
| `starting_after` | string  | —       | Cursor: return results after this object ID  |
| `ending_before`  | string  | —       | Cursor: return results before this object ID |

Response envelope:

```json
{
  "data": [
    ...
  ],
  "has_more": true
}
```

`starting_after` and `ending_before` are mutually exclusive.

### Expanding sub-resources

Some endpoints accept an `expand` query parameter to inline related sub-resources in the response, saving a follow-up request per item. The value is a comma-separated list of allowed expansion keys, documented per endpoint.

```
GET /prediction-markets/positions?expand=unwinds
GET /prediction-markets/positions?expand=unwinds,foo,bar
```

Each expanded sub-resource appears as a top-level field on the parent object using the same shape it would have if fetched standalone. When the parent has no data for the requested sub-resource, the field is omitted.

### SDK method mapping

If you're using `@dimes-dot-fi/sdk`, each endpoint maps to a client method:

| Endpoint                               | SDK method                                   |
| -------------------------------------- | -------------------------------------------- |
| `GET /markets`                         | `client.getMarkets(params?)`                 |
| `GET /markets/:ticker`                 | `client.getMarket(ticker)`                   |
| `GET /events/:event_ticker/markets`    | *not yet in the SDK — call over HTTP*        |
| `GET /series/:series_ticker/markets`   | *not yet in the SDK — call over HTTP*        |
| `GET /markets/:ticker/positions`       | *not yet in the SDK — call over HTTP*        |
| `GET /events/:event_ticker/positions`  | *not yet in the SDK — call over HTTP*        |
| `GET /series/:series_ticker/positions` | *not yet in the SDK — call over HTTP*        |
| `GET /contract-info`                   | `client.getContractInfo()`                   |
| `POST /tokens`                         | Handled by `ApiKeyAuth` automatically        |
| `POST /draft-quotes`                   | `client.createDraftQuote(params)`            |
| `POST /promoted-quotes/:id`            | `client.promoteDraftQuote(draftId)`          |
| `POST /quotes`                         | `client.createQuote(params)`                 |
| `GET /positions`                       | `client.getPositions(params?)`               |
| `GET /positions/:id/transactions`      | `client.getPositionTransactions(positionId)` |
| `GET /user-limits`                     | `client.getUserLimits()`                     |
| `GET /partner-limits`                  | `client.getPartnerLimits()`                  |
| `GET /fee-rates`                       | `client.getFeeRates(params?)`                |
| `POST /fee-reports`                    | `client.getFeeReport(params)`                |

The high-level `executeQuote()` function handles the full draft → promote flow with auto-retry and correction; `useQuote` / `useQuoteMachine` wrap it for React. See [SDK Installation](/for-developers/getting-started/sdk-installation) and [React Hooks](/for-developers/getting-started/react-hooks).

***

## 1. Markets

### List markets

Returns markets currently eligible for trading.

```
GET /prediction-markets/markets
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const markets = await client.getMarkets({limit: 25, sort: "depth_desc", expand: ["prices"]});
// markets.data: Market[]   markets.hasMore: boolean   markets.totalCount?: number
```

**Auth:** User JWT or partner API key

**Query parameters:**

| Param                     | Type    | Default      | Description                                                                                                                                   |
| ------------------------- | ------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`                   | string  | —            | Free-text search term. When set, results are filtered to markets matching the term (see Search behavior below).                               |
| `accepting_new_positions` | boolean | —            | Filter on whether **either** side is accepting new positions. `true` returns markets where YES or NO accepts.                                 |
| `limit`                   | integer | 25           | Max results (1–100)                                                                                                                           |
| `starting_after`          | string  | —            | Cursor: return results after this ticker                                                                                                      |
| `ending_before`           | string  | —            | Cursor: return results before this ticker                                                                                                     |
| `category`                | string  | —            | Filter by category                                                                                                                            |
| `provider`                | string  | —            | Filter: `polymarket`                                                                                                                          |
| `status`                  | string  | `active`     | Filter: `active`, `closed`, `determined`, `finalized`, `disputed`                                                                             |
| `sort`                    | string  | `ticker_asc` | Sort order: `ticker_asc` (alphabetical), `depth_desc` (deepest markets first), `discovered_at_desc` (newest markets first by discovery date). |
| `expand`                  | string  | —            | Comma-separated list of sub-resources to embed inline. Allowed values: `total_count`, `prices`.                                               |

**Search behavior:**

Pass `query` to search. It matches against multiple fields:

| Field        | Match type                       |
| ------------ | -------------------------------- |
| Market title | Case-insensitive substring match |
| Ticker       | Case-insensitive exact match     |
| Yes token ID | Exact match                      |
| No token ID  | Exact match                      |

```typescript
const results = await client.getMarkets({query: "bitcoin", limit: 10});
```

**Response: `200 OK`**

```json
{
  "data": [
    {
      "accepting_new_positions": true,
      "id": "dm_mkt_abc123",
      "ticker": "will-btc-hit-100k-2025",
      "polymarket": {
        "slug": "will-btc-hit-100k-2025",
        "condition_id": "0xabc123def4567890abc123def4567890abc123def4567890abc123def4567890",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563"
      },
      "title": "Will BTC hit $100K?",
      "yes_sub_title": "Yes, before September 30",
      "category": "crypto",
      "event": {
        "ticker": "btc-price-milestones-2025",
        "title": "BTC price milestones 2025",
        "series_ticker": "btc-price-milestones"
      },
      "rejection_reason_code": null,
      "sided_eligibility": {
        "yes": {
          "accepting_new_positions": true,
          "rejection_reason_code": null
        },
        "no": {
          "accepting_new_positions": false,
          "rejection_reason_code": "QUOTE_SIDE_CAPACITY_EXCEEDED"
        }
      },
      "status": "open",
      "tags": [
        "crypto",
        "bitcoin"
      ],
      "close_time": "2025-09-30T00:00:00.000Z",
      "discovered_at": "2025-09-01T08:00:00.000Z",
      "latest_enter_at": "2025-09-29T23:00:00.000Z",
      "provider": "polymarket",
      "min_collateral_usd_pips": "100000",
      "min_collateral_usd": "10.00",
      "min_notional_usd_pips": "150000",
      "min_notional_usd": "15.00",
      "leverage": {
        "min_bps": 20000,
        "max_yes_bps": 100000,
        "max_no_bps": 100000,
        "step_bps": 2500,
        "max_market_leverage_per_notional": {
          "yes": {
            "at100_usd_bps": 100000,
            "at500_usd_bps": 80000,
            "at1000_usd_bps": 60000,
            "at10000_usd_bps": 30000
          },
          "no": {
            "at100_usd_bps": 100000,
            "at500_usd_bps": 80000,
            "at1000_usd_bps": 60000,
            "at10000_usd_bps": 30000
          }
        }
      },
      "fees": {
        "origination_tiers": [
          {
            "max_leverage_bps": 40000,
            "fee_bps": 100
          },
          {
            "max_leverage_bps": 70000,
            "fee_bps": 125
          },
          {
            "max_leverage_bps": 100000,
            "fee_bps": 150
          }
        ],
        "lifetime_fee_apr_bps": 2000,
        "liquidation_fee_bps": 250
      },
      "max_notional_yes_usd_pips": "500000000",
      "max_notional_yes_usd": "50000.00",
      "max_notional_no_usd_pips": "500000000",
      "max_notional_no_usd": "50000.00",
      "capacity_max_notional_yes_usd_pips": "400000000",
      "capacity_max_notional_yes_usd": "40000.00",
      "capacity_max_notional_no_usd_pips": "400000000",
      "capacity_max_notional_no_usd": "40000.00",
      "slippage_max_notional_yes_usd_pips": "350000000",
      "slippage_max_notional_yes_usd": "35000.00",
      "slippage_max_notional_no_usd_pips": "350000000",
      "slippage_max_notional_no_usd": "35000.00",
      "prices": {
        "yes_ask_price_usd": "0.51",
        "yes_ask_price_usd_pips": "5100",
        "yes_bid_price_usd": "0.49",
        "yes_bid_price_usd_pips": "4900",
        "no_ask_price_usd": "0.51",
        "no_ask_price_usd_pips": "5100",
        "no_bid_price_usd": "0.49",
        "no_bid_price_usd_pips": "4900"
      }
    }
  ],
  "has_more": true
}
```

**Field reference:**

| Field                                                                               | Type                      | Description                                                                                                                                                                                                                                                       |
| ----------------------------------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accepting_new_positions`                                                           | boolean                   | Whether this market is currently accepting new positions on **either** side. See `sided_eligibility` for per-side breakdown.                                                                                                                                      |
| `capacity_max_notional_no_usd`                                                      | string or null            | Capacity-limited maximum notional for NO side (dollar display)                                                                                                                                                                                                    |
| `capacity_max_notional_no_usd_pips`                                                 | string or null            | Capacity-limited maximum notional for NO side (internal precision); `null` if capacity unknown                                                                                                                                                                    |
| `capacity_max_notional_yes_usd`                                                     | string or null            | Capacity-limited maximum notional for YES side (dollar display)                                                                                                                                                                                                   |
| `capacity_max_notional_yes_usd_pips`                                                | string or null            | Capacity-limited maximum notional for YES side (internal precision); `null` if capacity unknown                                                                                                                                                                   |
| `category`                                                                          | string                    | Market category                                                                                                                                                                                                                                                   |
| `close_time`                                                                        | string (ISO 8601) or null | When the market resolves                                                                                                                                                                                                                                          |
| `discovered_at`                                                                     | string (ISO 8601)         | When this market was first discovered and listed on the platform. Sort by it with `sort=discovered_at_desc` to surface the newest markets first.                                                                                                                  |
| `fees.lifetime_fee_apr_bps`                                                         | integer                   | Annual fee rate on borrowed capital, i.e. notional − collateral (2000 = 20% APR)                                                                                                                                                                                  |
| `fees.liquidation_fee_bps`                                                          | integer                   | Fee charged on liquidation, applied to borrowed capital (250 = 2.5%)                                                                                                                                                                                              |
| `fees.origination_tiers`                                                            | array                     | Fee tiers by leverage bracket                                                                                                                                                                                                                                     |
| `id`                                                                                | string                    | Market ID                                                                                                                                                                                                                                                         |
| `latest_enter_at`                                                                   | string (ISO 8601) or null | Latest time a new position can be opened                                                                                                                                                                                                                          |
| `leverage.max_market_leverage_per_notional.{yes,no}.at{100,500,1000,10000}_usd_bps` | integer                   | Maximum market leverage per side broken out by position notional ($100 / $500 / $1,000 / $10,000). Slippage grows with notional, so larger notionals have lower max leverage. A UI slider should publish the bucket that matches the user's selected notional.    |
| `leverage.max_no_bps`                                                               | integer                   | Maximum leverage on the NO side (100000 = 10x)                                                                                                                                                                                                                    |
| `leverage.max_yes_bps`                                                              | integer                   | Maximum leverage on the YES side (100000 = 10x). May differ from `max_no_bps` on skewed markets due to per-side fee-tolerance caps.                                                                                                                               |
| `leverage.min_bps`                                                                  | integer                   | Minimum leverage (20000 = 2x)                                                                                                                                                                                                                                     |
| `leverage.step_bps`                                                                 | integer                   | Leverage must be a multiple of this (2500 = 0.25x steps)                                                                                                                                                                                                          |
| `max_notional_no_usd`                                                               | string or null            | Maximum notional you can open on the NO side right now — the lesser of slippage, capacity, your partner's remaining position limit, and the per-user position limit for a wallet with no open positions (dollar display)                                          |
| `max_notional_no_usd_pips`                                                          | string or null            | Maximum notional you can open on the NO side right now — the lesser of slippage, capacity, your partner's remaining position limit, and the per-user position limit for a wallet with no open positions (internal precision)                                      |
| `max_notional_yes_usd`                                                              | string or null            | Maximum notional you can open on the YES side right now — the lesser of slippage, capacity, your partner's remaining position limit, and the per-user position limit for a wallet with no open positions (dollar display)                                         |
| `max_notional_yes_usd_pips`                                                         | string or null            | Maximum notional you can open on the YES side right now — the lesser of slippage, capacity, your partner's remaining position limit, and the per-user position limit for a wallet with no open positions (internal precision)                                     |
| `min_collateral_usd`                                                                | string                    | Minimum collateral (dollar display)                                                                                                                                                                                                                               |
| `min_collateral_usd_pips`                                                           | string                    | Minimum collateral (internal precision). A request with `notional / leverage` below this floor is rejected. The floor is partner-specific and can change over time — always read it from this endpoint per market; never hardcode the value shown in the example. |
| `min_notional_usd`                                                                  | string                    | Minimum notional (dollar display)                                                                                                                                                                                                                                 |
| `min_notional_usd_pips`                                                             | string                    | Minimum notional (internal precision). Partner-specific and subject to change — read it from this endpoint per market rather than hardcoding the example value.                                                                                                   |
| `polymarket`                                                                        | object                    | Polymarket identifiers for mapping this market onto Polymarket's own data feeds. Always present (all live markets are Polymarket-sourced).                                                                                                                        |
| `polymarket.condition_id`                                                           | string or null            | Polymarket CTF condition ID. Use it to look the market up on Polymarket's CLOB and Gamma APIs. `null` for the rare market where Polymarket has not exposed a condition ID.                                                                                        |
| `polymarket.no_token_id`                                                            | string                    | Polymarket CLOB token ID (ERC1155 position token ID) for the NO outcome.                                                                                                                                                                                          |
| `polymarket.slug`                                                                   | string                    | Polymarket market slug, matching the slug in Polymarket URLs and Gamma API responses. Same value as the legacy `ticker` field.                                                                                                                                    |
| `polymarket.yes_token_id`                                                           | string                    | Polymarket CLOB token ID (ERC1155 position token ID) for the YES outcome.                                                                                                                                                                                         |
| `prices`                                                                            | object or null            | Latest bid/ask prices. Only present when `expand=prices` is requested. Null if no recent data.                                                                                                                                                                    |
| `prices.no_ask_price_usd`                                                           | string                    | NO side ask price (dollar display)                                                                                                                                                                                                                                |
| `prices.no_ask_price_usd_pips`                                                      | string                    | NO side ask price (internal precision)                                                                                                                                                                                                                            |
| `prices.no_bid_price_usd`                                                           | string                    | NO side bid price (dollar display)                                                                                                                                                                                                                                |
| `prices.no_bid_price_usd_pips`                                                      | string                    | NO side bid price (internal precision)                                                                                                                                                                                                                            |
| `prices.yes_ask_price_usd`                                                          | string                    | YES side ask price (dollar display)                                                                                                                                                                                                                               |
| `prices.yes_ask_price_usd_pips`                                                     | string                    | YES side ask price (internal precision, 10000 pips = $1)                                                                                                                                                                                                          |
| `prices.yes_bid_price_usd`                                                          | string                    | YES side bid price (dollar display)                                                                                                                                                                                                                               |
| `prices.yes_bid_price_usd_pips`                                                     | string                    | YES side bid price (internal precision)                                                                                                                                                                                                                           |
| `provider`                                                                          | string                    | `polymarket`                                                                                                                                                                                                                                                      |
| `rejection_reason_code`                                                             | string or null            | Why this market is not accepting positions on **either** side (see table below). `null` if at least one side accepts. For per-side detail use `sided_eligibility.{yes,no}.rejection_reason_code`.                                                                 |
| `sided_eligibility.no.accepting_new_positions`                                      | boolean                   | Whether the NO side is accepting new positions.                                                                                                                                                                                                                   |
| `sided_eligibility.no.rejection_reason_code`                                        | string or null            | Why the NO side is not accepting positions; `null` when accepting.                                                                                                                                                                                                |
| `sided_eligibility.yes.accepting_new_positions`                                     | boolean                   | Whether the YES side is accepting new positions. A market with healthy YES depth but thin/saturated NO can be `true` on YES and `false` on NO.                                                                                                                    |
| `sided_eligibility.yes.rejection_reason_code`                                       | string or null            | Why the YES side is not accepting positions; `null` when accepting. Codes match the table below.                                                                                                                                                                  |
| `slippage_max_notional_no_usd`                                                      | string or null            | Slippage-limited maximum notional for NO side (dollar display)                                                                                                                                                                                                    |
| `slippage_max_notional_no_usd_pips`                                                 | string or null            | Slippage-limited maximum notional for NO side (internal precision); `null` if slippage data unavailable                                                                                                                                                           |
| `slippage_max_notional_yes_usd`                                                     | string or null            | Slippage-limited maximum notional for YES side (dollar display)                                                                                                                                                                                                   |
| `slippage_max_notional_yes_usd_pips`                                                | string or null            | Slippage-limited maximum notional for YES side (internal precision); `null` if slippage data unavailable                                                                                                                                                          |
| `status`                                                                            | string                    | `active`, `closed`, `determined`, `finalized`, or `disputed`                                                                                                                                                                                                      |
| `tags`                                                                              | string\[]                 | Descriptive tags                                                                                                                                                                                                                                                  |
| `ticker`                                                                            | string                    | **Deprecated** — use `polymarket.slug`. Unique market ticker sourced from the upstream trading venue (the Polymarket slug). Still accepted in quote requests and path params.                                                                                     |
| `title`                                                                             | string or null            | Human-readable market title                                                                                                                                                                                                                                       |
| `yes_sub_title`                                                                     | string or null            | Description of the "yes" outcome                                                                                                                                                                                                                                  |

> **Note:** `ticker` and "slug" refer to the same identifier — the terms are used interchangeably across our docs and integrations.

**Rejection reason codes:**

These are the values the `rejection_reason_code` field (market-level and per-side) can take. They are distinct from the codes a live `POST /quotes` call throws — those are in [Error Handling](/for-developers/api-and-events/error-handling#post-quotes). Both are UPPERCASE, unlike an error response's lowercase `error.code`.

| Code                                               | Meaning                                                                          |
| -------------------------------------------------- | -------------------------------------------------------------------------------- |
| `QUOTE_MARKET_NOT_ACTIVE`                          | Market status is not `active`                                                    |
| `QUOTE_MARKET_UNSUPPORTED_CATEGORY`                | Market category is not supported (only `crypto` and `sport`)                     |
| `QUOTE_MARKET_UNSUPPORTED_CRYPTO_ASSET`            | Crypto market whose underlying asset has no supported price feed                 |
| `QUOTE_MARKET_RISK_TOO_HIGH`                       | Market risk level is not `low`                                                   |
| `QUOTE_MAX_LEVERAGE_TOO_LOW`                       | Model max leverage for this market has fallen below the tradable minimum         |
| `QUOTE_TRADING_WINDOW_CLOSING`                     | Market is at or inside its hard-exit deadline — no new entries                   |
| `QUOTE_EVENT_NOT_STARTED`                          | Event has not started and pre-game entry is not allowed for this market          |
| `QUOTE_SIDE_CAPACITY_EXCEEDED`                     | This side is at capacity                                                         |
| `QUOTE_ENTRY_BOOK_ONE_SIDED`                       | Order book is missing or one-sided                                               |
| `QUOTE_ENTRY_EXCLUDED_SPORT`                       | Sport type is excluded from entry                                                |
| `QUOTE_ENTRY_EXCLUDED_MARKET_TYPE`                 | Market type (e.g. first-half moneyline) is excluded                              |
| `QUOTE_ENTRY_VOLUME_TOO_LOW`                       | 24-hour trading volume is below minimum                                          |
| `QUOTE_ENTRY_PRICE_OUT_OF_RANGE`                   | Mid price is outside the allowed range                                           |
| `NOTIONAL_SELECTOR_BELOW_MIN_NOTIONAL`             | Liquidity within slippage is too thin to fill even the minimum size on this side |
| `NOTIONAL_SELECTOR_PREGAME_INSUFFICIENT_LIQUIDITY` | Pre-game market lacks enough estimated live liquidity on this side               |
| `RISK_ENGINE_MARKET_DISABLED`                      | Market disabled by an admin kill switch                                          |

***

### Get market

Returns a single market by ticker. Same shape as a list item (not wrapped in `data` array).

```
GET /prediction-markets/markets/{ticker}
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const market = await client.getMarket("will-btc-hit-100k-2025");
```

**Auth:** User JWT or partner API key

**Response: `200 OK`** — Single market object.

**Errors:**

| Status | Code                        |
| ------ | --------------------------- |
| 404    | `customer_market_not_found` |

***

### List markets for an event

Returns the markets belonging to a single event — the real-world happening a market resolves against, such as one game or one hourly price window. An event usually holds several markets (moneyline, spread, totals, and so on).

```
GET /prediction-markets/events/{event_ticker}/markets
```

Get the `event_ticker` from the `event.ticker` field of any market.

**Auth:** User JWT or partner API key

**Query parameters:** identical to [List markets](#list-markets) — `accepting_new_positions`, `category`, `provider`, `status`, `query`, `sort`, `limit`, `starting_after`, `ending_before`, and `expand`.

**Response: `200 OK`** — Same paginated envelope and same market objects as `GET /markets`.

**Errors:**

| Status | Code                       |
| ------ | -------------------------- |
| 404    | `customer_event_not_found` |

***

### List markets for a series

Returns the markets belonging to a single series, across every event in that series. A series is the recurring template an event belongs to — for example the 5-minute BTC up/down series, which produces a new event every 5 minutes.

```
GET /prediction-markets/series/{series_ticker}/markets
```

Get the `series_ticker` from the `event.series_ticker` field of any market. It is `null` for markets whose event has no series.

**Auth:** User JWT or partner API key

**Query parameters:** identical to [List markets](#list-markets).

**Response: `200 OK`** — Same paginated envelope and same market objects as `GET /markets`. A series commonly holds more markets than fit on one page, so paginate with `starting_after`.

**Errors:**

| Status | Code                        |
| ------ | --------------------------- |
| 404    | `customer_series_not_found` |

***

### Get contract info

Returns the vault contract address on Polygon and the public address of the authorized quote signer. Use this to verify that quote responses have not been tampered with by a man-in-the-middle.

```
GET /prediction-markets/contract-info
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const info = await client.getContractInfo();
// info.polygonVaultContractAddress, info.polygonUsdcTokenAddress, info.polygonSignerAddress
```

**Auth:** Public

**Response: `200 OK`**

```json
{
  "evm_chain_id": "137",
  "polygon_signer_address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
  "polygon_usdc_token_address": "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB",
  "polygon_vault_contract_address": "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"
}
```

**Field reference:**

| Field                            | Type   | Description                                                                                                                                         |
| -------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `evm_chain_id`                   | string | EVM chain ID (`"137"` for Polygon mainnet)                                                                                                          |
| `polygon_signer_address`         | string | Checksummed address of the authorized signer on Polygon                                                                                             |
| `polygon_usdc_token_address`     | string | Checksummed address of the pUSD stablecoin token — the **only** token accepted as collateral. Use it as the transfer target when funding a position |
| `polygon_vault_contract_address` | string | Checksummed address of the vault contract on Polygon                                                                                                |

> **Tip:** These values are static. Fetch them once at startup and cache.

#### Verifying a quote signature

Before submitting an on-chain transaction, verify the quote's `contract_signature` was produced by the authorized signer. Reconstruct the message hash from the quote fields, recover the signer, and compare to `polygon_signer_address`:

```javascript
import {ethers} from "ethers";

const messageHash = ethers.solidityPackedKeccak256(
  [
    "string", "bytes16", "address", "bytes32", "uint256",
    "uint256", "uint32", "uint256", "uint16", "uint16",
    "uint16", "uint256", "uint256", "address",
  ],
  [
    "CREATE_POSITION",
    quote.position_seed_hex,
    userWalletAddress,
    quote.polymarket_market_id,
    BigInt(quote.polymarket_token_id),
    BigInt(quote.collateral_usdc_units),
    quote.leverage_bps,
    BigInt(quote.notional_usdc_units),
    quote.origination_fee_bps,
    quote.lifetime_fee_apr_bps,
    quote.liquidation_fee_bps,
    BigInt(quote.signature_expiry),
    BigInt(contractInfo.evm_chain_id),
    contractInfo.polygon_vault_contract_address,
  ]
);

const recoveredSigner = ethers.verifyMessage(
  ethers.getBytes(messageHash),
  quote.contract_signature
);

if (recoveredSigner.toLowerCase() !== contractInfo.polygon_signer_address.toLowerCase()) {
  throw new Error("Signature verification failed");
}
```

Also verify that `polygon_vault_contract_address` and `evm_chain_id` match the corresponding fields in the quote.

***

## 2. Auth

### Generate token

Generate a user-scoped JWT. The partner is responsible for verifying wallet ownership before calling.

```
POST /prediction-markets/tokens
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
// You don't call this endpoint directly — the auth providers exchange your
// credentials for a JWT and refresh it automatically:
const client = new DimesClient({auth: new ApiKeyAuth({apiKey, walletAddress})}); // server-side
// In the browser: new JwtAuth({ tokenUrl: "https://your-backend.com/api/dimes-token" })
```

**Auth:** Partner API key

**Request:**

```json
{
  "wallet_address": "0x1234...abcd"
}
```

| Field            | Type   | Required | Validation                             |
| ---------------- | ------ | -------- | -------------------------------------- |
| `wallet_address` | string | yes      | Valid EVM address or Solana public key |

**Response: `201 Created`**

```json
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": "2025-06-01T11:00:00.000Z"
}
```

**Errors:**

| Status | Code                                   |
| ------ | -------------------------------------- |
| 401    | `unauthorized`                         |
| 400    | `customer_auth_invalid_wallet_address` |

***

## 3. Quotes

### Create quote

Create a new quote (step 1 of opening a position). Returns pricing, fees, liquidation parameters, and a transaction to sign.

```
POST /prediction-markets/quotes
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
// Recommended — executeQuote runs the full draft → promote flow with auto-correction:
const {quote, corrections} = await executeQuote(client, {
  marketTicker: "will-btc-hit-100k-2025",
  side: "yes",
  collateralUsd: 25,
  leverageBps: 50_000, // 5x
  slippageBps: 300,
});
// Low-level single call (params already finalized):
const direct = await client.createQuote(buildQuoteParams({
  marketTicker: "will-btc-hit-100k-2025", side: "yes", collateralUsd: 25, leverageBps: 50_000, slippageBps: 300,
}));
```

**Auth:** User JWT

**Rate limit:** 1 request per second for each authenticated wallet within a partner integration.

**Request:**

```json
{
  "market_ticker": "will-btc-hit-100k-2025",
  "effective_side": "yes",
  "leverage_bps": 50000,
  "notional_amount_usd_pips": "250000000",
  "slippage_bps": 300,
  "provider": "polymarket"
}
```

| Field                      | Type    | Required | Validation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| -------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `market_ticker`            | string  | yes      | Market ticker                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `effective_side`           | string  | yes      | `yes` or `no`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `leverage_bps`             | integer | yes      | 20000–100000, multiple of 2500                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `notional_amount_usd_pips` | string  | yes      | Minimum `10000`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `slippage_bps`             | integer | yes      | 100–1000                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `provider`                 | string  | no       | `polymarket` (default)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `allow_partial_fill`       | boolean | no       | `false` (default) — the order is atomic (fully filled or reverted). `true` opts into partial-open: the position opens at the actually-filled size. See [Partial-Open (FAK)](/for-developers/order-types/partial-open).                                                                                                                                                                                                                                                                                                              |
| `min_fill_bps`             | integer | no       | Only valid with `allow_partial_fill: true`. Range `2000`–`5000`, multiple of `500` (5% steps). The minimum fraction of the requested notional the user will accept. Capped from below by `max(2000, ceil(MIN_COLLATERAL × 10000 / requested_collateral), ceil(MIN_NOTIONAL × 10000 / requested_notional))` so a worst-case partial fill cannot drop below the minimum collateral **or** the minimum notional — see `quote_min_fill_bps_below_floor` in [error handling](/for-developers/api-and-events/error-handling#post-quotes). |

**Response: `201 Created`**

```json
{
  "id": "dm_off_abc123",
  "market_ticker": "will-btc-hit-100k-2025",
  "authority_public_key": "0x1234...abcd",
  "effective_side": "yes",
  "provider": "polymarket",
  "leverage_bps": 50000,
  "collateral_usdc_units": "5000000000",
  "notional_amount_usd_pips": "250000000",
  "notional_amount_usd": "25000.00",
  "notional_usdc_units": "25000000000",
  "entry_price_usd_pips": "5000",
  "entry_price_usd": "0.50",
  "origination_fee_bps": 125,
  "origination_fee_usd_pips": "3125000",
  "origination_fee_usd": "312.50",
  "origination_fee_usdc_units": "312500000",
  "protocol_origination_fee_bps": 100,
  "protocol_origination_fee_usd_pips": "2500000",
  "protocol_origination_fee_usd": "250.00",
  "protocol_origination_fee_usdc_units": "250000000",
  "partner_origination_fee_bps": 25,
  "partner_origination_fee_usd_pips": "625000",
  "partner_origination_fee_usd": "62.50",
  "partner_origination_fee_usdc_units": "62500000",
  "polymarket_trading_fee_bps": 100,
  "partner_trading_fee_bps": 0,
  "partner_trading_fee_usd_pips": "0",
  "partner_trading_fee_usd": "0.00",
  "partner_trading_fee_usdc_units": "0",
  "liquidation_fee_bps": 250,
  "current_liquidation_price_usd_pips": "4100",
  "current_liquidation_price_usd": "0.41",
  "expected_open_trading_fee_usd_pips": "2500000",
  "expected_open_trading_fee_usd": "250.00",
  "expected_open_trading_fee_usdc_units": "250000000",
  "min_expected_position_token_units": "48500000000",
  "total_user_amount_usd_pips": "55625000",
  "total_user_amount_usd": "5562.50",
  "total_user_amount_usdc_units": "5562500000",
  "expires_at": "2025-06-01T10:05:00.000Z",
  "position_seed": "abc123...",
  "position_seed_hex": "0xa1b2c3d4e5f67890abcdef1234567890",
  "on_chain_position_key": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890",
  "swap_transaction": "base64-encoded-solana-tx...",
  "evm_chain_id": "137",
  "polymarket_market_id": "0xabcdef...",
  "contract_signature": "0xabc...",
  "polymarket_token_id": "52114319501245915516055106046884209969926127482827954674443846427813813222426",
  "polygon_vault_contract_address": "0x1234...5678",
  "signature_expiry": "1717236300"
}
```

**Expanding max gain (`?expand=max_gain`)**

Add `?expand=max_gain` to any quote endpoint (`/quotes`, `/draft-quotes`, `/promoted-quotes/{id}`) to include a `max_gain` object — the expected profit on a win (profit over principal). It is omitted unless requested.

```json
"max_gain": {
  "gross_max_gain_usd": "25000.00",
  "gross_max_gain_usd_pips": "250000000",
  "gross_max_gain_usdc_units": "25000000000",
  "net_max_gain_usd": "24437.50",
  "net_max_gain_usd_pips": "244375000",
  "net_max_gain_usdc_units": "24437500000"
}
```

`gross_max_gain` is the position's value on a win (tokens redeemed at $1 via settlement) minus the notional, before fees. `net_max_gain` also subtracts the open trading fee and origination fee. It assumes settlement (no exit trading fee) and excludes time-based lifetime fees, so it is an upper bound — and may be negative for high-price entries.

| Field                                                         | Description                                                                                                                                                                                                                                         |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `authority_public_key`                                        | Wallet address (from JWT)                                                                                                                                                                                                                           |
| `collateral_usdc_units`                                       | Collateral in USDC units (1,000,000 units = $1). Contract-ready value for `createPosition`                                                                                                                                                          |
| `contract_signature`                                          | EIP-191 signature for contract create position                                                                                                                                                                                                      |
| `current_liquidation_price_usd_pips` / `_usd`                 | Price triggering liquidation                                                                                                                                                                                                                        |
| `effective_side`                                              | `yes` or `no`                                                                                                                                                                                                                                       |
| `entry_price_usd_pips` / `_usd`                               | Entry price                                                                                                                                                                                                                                         |
| `evm_chain_id`                                                | Chain ID for EVM signature (EVM only)                                                                                                                                                                                                               |
| `expected_open_trading_fee_usd_pips` / `_usd` / `_usdc_units` | Expected exchange trading fee. Pass the `_usdc_units` value as the contract's `venueFeeUsdcUnits` parameter. Refunded in full on cancel or revert.                                                                                                  |
| `expires_at`                                                  | Quote expiry (ISO 8601). The validity window is short and may change without notice — always read this field from the response and do not hardcode a duration. Requests submitted after this timestamp will be rejected; fetch a new quote instead. |
| `id`                                                          | Quote ID                                                                                                                                                                                                                                            |
| `leverage_bps`                                                | Leverage in basis points                                                                                                                                                                                                                            |
| `liquidation_fee_bps`                                         | Fee charged on liquidation in basis points                                                                                                                                                                                                          |
| `market_ticker`                                               | Market ticker                                                                                                                                                                                                                                       |
| `min_expected_position_token_units`                           | Minimum tokens expected (1,000,000 units = 1 token)                                                                                                                                                                                                 |
| `notional_amount_usd_pips` / `_usd` / `_usdc_units`           | Notional exposure                                                                                                                                                                                                                                   |
| `on_chain_position_key`                                       | On-chain position key (bytes32) that will be derived on `createPosition`. Matches `on_chain_position_key` on the position response — use it to link a quote to its future position                                                                  |
| `origination_fee_bps`                                         | Combined fee rate at open (`protocol + partner`)                                                                                                                                                                                                    |
| `origination_fee_usd_pips` / `_usd` / `_usdc_units`           | Combined absolute fee at open                                                                                                                                                                                                                       |
| `partner_origination_fee_bps`                                 | Partner component of the origination fee in basis points *(deprecated — prefer builder code fees)*                                                                                                                                                  |
| `partner_origination_fee_usd_pips` / `_usd` / `_usdc_units`   | Partner component in absolute terms *(deprecated — prefer builder code fees)*                                                                                                                                                                       |
| `partner_trading_fee_bps`                                     | Your Polymarket builder taker fee rate (flat % of notional). `0` when no builder code is configured                                                                                                                                                 |
| `partner_trading_fee_usd_pips` / `_usd` / `_usdc_units`       | Your builder taker fee in absolute terms. Included in `expected_open_trading_fee_*`                                                                                                                                                                 |
| `polygon_vault_contract_address`                              | Vault contract address on Polygon                                                                                                                                                                                                                   |
| `polymarket_market_id`                                        | On-chain bytes32 Polymarket market identifier                                                                                                                                                                                                       |
| `polymarket_token_id`                                         | Polymarket ERC1155 conditional token ID for the selected side                                                                                                                                                                                       |
| `polymarket_trading_fee_bps`                                  | Polymarket venue trading fee rate (applied to the expected open trading fee). `0` for Kalshi. The breakdown is only present on the quote response; historical position entries expose only the combined `origination_fee_*` values.                 |
| `position_seed`                                               | Seed for on-chain account derivation                                                                                                                                                                                                                |
| `position_seed_hex`                                           | Position seed as 0x-prefixed bytes16 hex — contract-ready value for `createPosition`                                                                                                                                                                |
| `protocol_origination_fee_bps`                                | Protocol component of the origination fee in basis points                                                                                                                                                                                           |
| `protocol_origination_fee_usd_pips` / `_usd` / `_usdc_units`  | Protocol component in absolute terms                                                                                                                                                                                                                |
| `provider`                                                    | `polymarket`                                                                                                                                                                                                                                        |
| `signature_expiry`                                            | Unix timestamp when the EVM signature expires (EVM only). Always read this from the response — the validity window is short and may change without notice.                                                                                          |
| `swap_transaction`                                            | Base64-encoded Solana transaction (Solana only)                                                                                                                                                                                                     |
| `total_user_amount_usd_pips` / `_usd` / `_usdc_units`         | Total economic cost to open the position. Polymarket: collateral + origination fee + expected trading fee. The EVM contract pulls `collateral + origination fee + venueFee` via `safeTransferFrom` at `createPosition`.                             |
| `authority_public_key`                                        | Wallet address (from JWT)                                                                                                                                                                                                                           |

### Partial-open (FAK)

By default, quotes open atomically: the order either fills its full requested notional in one FOK match or reverts and refunds the user (no position is created). For thinner markets, set `allow_partial_fill: true` (optionally with a `min_fill_bps` floor) to open at the actually-filled size instead.

```json
{
  "market_ticker": "will-btc-hit-100k-2025",
  "effective_side": "yes",
  "leverage_bps": 20000,
  "notional_amount_usd_pips": "1000000000",
  "slippage_bps": 200,
  "allow_partial_fill": true,
  "min_fill_bps": 5000
}
```

The quote and position responses echo `allow_partial_fill` and `min_fill_bps` so the caller can confirm what the user signed. The full lifecycle (floor leg, FAK retries, `finalizeOpenPartial` shrink/refund), the `min_fill_bps` floor rules, the progress/floor-missed notifications, and the `quote_min_fill_bps_below_floor` rejection are documented on the dedicated [Partial-Open (FAK)](/for-developers/order-types/partial-open) page.

The invariant `notional_usdc_units === collateral_usdc_units × leverage_bps / 10_000` always holds on the returned quote, and matches the payload the API signed.

**Errors:** See [Error Handling — Quotes](/for-developers/api-and-events/error-handling#post-quotes).

### Draft quotes

Draft quotes compute the same pricing, fees, leverage, and liquidation numbers as a regular quote but do **not** reserve capacity or generate a chain signature. Use them for display purposes (e.g. showing estimated terms before the user commits) without consuming capacity that could block actual trades.

```
POST /prediction-markets/draft-quotes
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const draft = await client.createDraftQuote(buildQuoteParams({
  marketTicker: "will-btc-hit-100k-2025", side: "yes", collateralUsd: 25, leverageBps: 50_000, slippageBps: 300,
}));
```

**Auth:** User JWT

**Rate limit:** 1 request per second for each authenticated wallet within a partner integration.

**Request:** Identical to [Create quote](#create-quote) (same body fields and validation).

**Response: `201 Created`**

The response shape is the same as a regular quote with these differences:

| Field                   | Value in draft quote                                           |
| ----------------------- | -------------------------------------------------------------- |
| `id`                    | Draft quote ID (`dm_dro_...`)                                  |
| `contract_signature`    | Empty string — no chain signature is generated                 |
| `evm_chain_id`          | Empty string                                                   |
| `signature_expiry`      | `"0"`                                                          |
| `swap_transaction`      | Absent                                                         |
| `position_seed`         | Empty string — no position seed is allocated                   |
| `position_seed_hex`     | Empty string                                                   |
| `on_chain_position_key` | Empty string                                                   |
| `expires_at`            | Current timestamp (draft quotes do not have a validity window) |

All pricing and fee fields (`entry_price_*`, `origination_fee_*`, `liquidation_*`, `total_user_amount_*`, etc.) are computed identically to a regular quote.

A draft quote **cannot** be used to open a position directly. Instead, promote it to a real quote using the endpoint below.

**Errors:** Same as [Error Handling — Quotes](/for-developers/api-and-events/error-handling#post-quotes), except capacity and position limit errors (`QUOTE_SIDE_CAPACITY_EXCEEDED`, `USER_POSITION_LIMIT_EXCEEDED`, etc.) are not returned since draft quotes do not reserve capacity.

### Promote draft quote

Promote a draft quote to a real, executable quote. The backend loads the draft's parameters and re-runs the full quote pipeline, returning a signed quote that can be submitted on-chain.

Recommended flow:

1. Call `POST /prediction-markets/draft-quotes` to get pricing — display it to the user
2. When the user is ready, call `POST /prediction-markets/promoted-quotes/{draft_quote_id}`
3. On success, immediately submit the signed quote to the contract

If market conditions changed between the draft and promotion, the endpoint returns the same errors as a regular quote. The user can retry (step 1) to get fresh numbers.

```
POST /prediction-markets/promoted-quotes/{draft_quote_id}
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const quote = await client.promoteDraftQuote(draft.id);
```

**Auth:** User JWT (must match the wallet that created the draft)

**Rate limit:** 1 request per second for each authenticated wallet within a partner integration.

**Path parameters:**

| Parameter        | Type   | Description                        |
| ---------------- | ------ | ---------------------------------- |
| `draft_quote_id` | string | Draft quote ID (from step 1 above) |

**Request body:** Empty (all parameters are loaded from the draft).

**Response: `201 Created`**

Same shape as [Create quote](#create-quote) — a fully signed quote with `contract_signature`, `position_seed`, `signature_expiry`, and all pricing fields populated.

**Errors:**

| Status | Code                         | Description                                                       |
| ------ | ---------------------------- | ----------------------------------------------------------------- |
| `404`  | —                            | Draft not found, or wallet/partner does not match the draft owner |
| `400`  | Same as regular quote errors | Market conditions changed since the draft                         |

Promoting the same draft multiple times is allowed — each call creates a new independent quote. Only one can be used on-chain (position seeds are unique per quote).

***

## 4. Positions (User JWT)

### List positions

List positions for the authenticated user. Scoped to `(wallet_address, partner_id)` from JWT.

```
GET /prediction-markets/positions
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const positions = await client.getPositions({status: "open"});
// positions: Position[] — narrow with isOpenPosition() / isClosedPosition()
```

**Auth:** User JWT

**Query parameters:**

| Param                 | Type    | Default      | Description                                                                                                                                                                                                                                       |
| --------------------- | ------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`              | string  | —            | Filter by a single status: `pending`, `open`, `unwinding`, `closing`, `settling`, `closed`, `settled`, `liquidated`, `cancelled`. `open` includes positions currently being unwound for backwards compatibility. Mutually exclusive with `state`. |
| `state`               | string  | —            | Filter by lifecycle group: `active` (in-flight: `pending`, `open`, `unwinding`, `closing`, `settling`) or `inactive` (terminal: `closed`, `settled`, `liquidated`, `cancelled`). Mutually exclusive with `status`.                                |
| `polymarket_token_id` | string  | —            | Filter by Polymarket conditional token ID                                                                                                                                                                                                         |
| `sort_by`             | string  | `created_at` | Sort field: `created_at`, `closed_at`                                                                                                                                                                                                             |
| `sort_direction`      | string  | `desc`       | Sort order: `asc`, `desc`                                                                                                                                                                                                                         |
| `limit`               | integer | 25           | Max results (1–100)                                                                                                                                                                                                                               |
| `starting_after`      | string  | —            | Cursor: position ID                                                                                                                                                                                                                               |
| `ending_before`       | string  | —            | Cursor: position ID                                                                                                                                                                                                                               |
| `expand`              | string  | —            | Comma-separated list of related sub-resources to embed inline in each position. Allowed values: `unwinds`. See [Expanding sub-resources](#expanding-sub-resources).                                                                               |

**Response: `200 OK`** — Open position:

```json
{
  "data": [
    {
      "id": "dm_pos_abc123",
      "created_at": "2025-06-01T09:59:55.000Z",
      "on_chain_position_key": "0x1a2b3c...",
      "market_ticker": "will-btc-hit-100k-2025",
      "market_title": "Will BTC hit $100K?",
      "provider": "polymarket",
      "status": "open",
      "side": "yes",
      "wallet_address": "0x1234...abcd",
      "effective_leverage_bps": 50000,
      "failure": null,
      "close_attempt": null,
      "pending_operation": null,
      "entry": {
        "collateral_usd_pips": "50000000",
        "collateral_usd": "5000.00",
        "notional_usd_pips": "250000000",
        "notional_usd": "25000.00",
        "leverage_bps": 50000,
        "price_usd_pips": "5000",
        "price_usd": "0.50",
        "effective_entry_price_usd_pips": "5020",
        "effective_entry_price_usd": "0.50",
        "effective_slippage_bps": 40,
        "position_token_units": "49800000000",
        "initial_fill_bps": 10000,
        "origination_fee_bps": 125,
        "protocol_origination_fee_bps": 100,
        "partner_origination_fee_bps": 25,
        "origination_fee_usd_pips": "3125000",
        "origination_fee_usd": "312.50",
        "opened_at": "2025-06-01T10:00:00.000Z",
        "open_latency_ms": 12500
      },
      "current": {
        "book_leverage_bps": 50000,
        "leverage_bps": 50000,
        "mark_price_usd_pips": "7000",
        "mark_price_usd": "0.70",
        "market_leverage_bps": 23458,
        "notional_usd_pips": "250000000",
        "notional_usd": "25000.00",
        "position_token_units": "49800000000",
        "remaining_bps": 10000,
        "collateral_usd_pips": "50000000",
        "collateral_usd": "5000.00",
        "effective_collateral_usd_pips": "148600000",
        "effective_collateral_usd": "14860.00",
        "unrealized_pnl_usd_pips": "98600000",
        "unrealized_pnl_usd": "9860.00",
        "unrealized_pnl_bps": 19720,
        "net_unrealized_pnl_usd_pips": "92037500",
        "net_unrealized_pnl_usd": "9203.75",
        "net_unrealized_pnl_bps": 18407,
        "position_value_usd_pips": "348600000",
        "position_value_usd": "34860.00"
      },
      "risk": {
        "health_bps": 10000,
        "current_liquidation_price_usd_pips": "4100",
        "current_liquidation_price_usd": "0.41",
        "liquidation_buffer_bps": 4142,
        "liquidation_fee_bps": 250,
        "margin_buffer_usd_pips": "144420000",
        "margin_buffer_usd": "14442.00"
      },
      "fees": {
        "lifetime_apr_bps": 2000,
        "accrued_lifetime_fee_usd_pips": "750000",
        "accrued_lifetime_fee_usd": "75.00",
        "pending_lifetime_fee_usd_pips": "187500",
        "pending_lifetime_fee_usd": "18.75",
        "accrued_venue_fee_usd_pips": "2500000",
        "accrued_venue_fee_usd": "250.00",
        "total_fees_usd_pips": "6562500",
        "total_fees_usd": "656.25"
      },
      "timing": {
        "is_settlement_pending": false,
        "is_voided": false,
        "market_close_time": "2025-09-30T00:00:00.000Z",
        "market_status": "active",
        "settlement_state": "none",
        "time_to_close_minutes": 172800
      }
    }
  ],
  "has_more": false
}
```

**Open position field reference:**

| Field                                | Description                                                                                                                                                                                                                                                                                         |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `created_at`                         | ISO 8601 timestamp when the position record was created                                                                                                                                                                                                                                             |
| `close_attempt`                      | Present when a requested close was deferred because the market resolved before the shares could be sold: `{ outcome: "deferred", reason: "awaiting_settlement", deferred_at: <ISO 8601> }`. The remaining tokens are redeemed at settlement. `null` for any open position without a deferred close. |
| `effective_leverage_bps`             | Time-weighted average structural leverage over position lifetime                                                                                                                                                                                                                                    |
| `effective_side`                     | `yes` or `no`                                                                                                                                                                                                                                                                                       |
| `failure`                            | `{ reason: string }` if the position transaction failed, otherwise `null`                                                                                                                                                                                                                           |
| `id`                                 | Position identifier                                                                                                                                                                                                                                                                                 |
| `market_ticker`                      | Market ticker                                                                                                                                                                                                                                                                                       |
| `market_title`                       | Human-readable market title, or null                                                                                                                                                                                                                                                                |
| `on_chain_position_key`              | On-chain position key (bytes32) — pass directly to `requestClose`                                                                                                                                                                                                                                   |
| `provider`                           | `polymarket`                                                                                                                                                                                                                                                                                        |
| `status`                             | `pending`, `open`, `unwinding`, `closing`, `settling`, `closed`, `settled`, `liquidated`, `cancelled`                                                                                                                                                                                               |
| `wallet_address`                     | Wallet address that owns this position                                                                                                                                                                                                                                                              |
| **entry**                            | Snapshot at position creation                                                                                                                                                                                                                                                                       |
| `entry.collateral_*`                 | User's original collateral                                                                                                                                                                                                                                                                          |
| `entry.effective_entry_price_*`      | Actual fill price on the prediction market after execution. `null` until the on-chain fill is recorded.                                                                                                                                                                                             |
| `entry.effective_slippage_bps`       | Execution slippage between the quote's indicative price and the actual fill, in basis points. Signed: positive = adverse fill, negative = favorable. `null` until the fill is recorded.                                                                                                             |
| `entry.initial_fill_bps`             | How much of the requested size was actually filled at open, in basis points (10000 = 100%). Frozen at open. Use this for an "opened at X% of requested" indicator. `null` until the open fill is recorded.                                                                                          |
| `entry.leverage_bps`                 | Entry leverage (50000 = 5x)                                                                                                                                                                                                                                                                         |
| `entry.notional_*`                   | Original notional                                                                                                                                                                                                                                                                                   |
| `entry.open_latency_ms`              | Milliseconds from position creation to on-chain open confirmation. `null` until fully opened.                                                                                                                                                                                                       |
| `entry.opened_at`                    | Open timestamp (null if still pending)                                                                                                                                                                                                                                                              |
| `entry.origination_fee_*`            | Absolute combined origination fee at open                                                                                                                                                                                                                                                           |
| `entry.origination_fee_bps`          | Combined fee rate at open (`protocol + partner`)                                                                                                                                                                                                                                                    |
| `entry.partner_origination_fee_bps`  | Partner portion of the origination fee in basis points *(deprecated — prefer builder code fees)*                                                                                                                                                                                                    |
| `entry.price_*`                      | Indicative entry price quoted at open                                                                                                                                                                                                                                                               |
| `entry.protocol_origination_fee_bps` | Protocol portion of the origination fee in basis points                                                                                                                                                                                                                                             |
| **current**                          | Live state (affected by force unwinds)                                                                                                                                                                                                                                                              |
| `current.book_leverage_bps`          | Current book-value leverage in basis points (20000 = 2x). Only changes when capital is returned via unwind.                                                                                                                                                                                         |
| `current.collateral_*`               | Original collateral (unchanged by unwinds)                                                                                                                                                                                                                                                          |
| `current.effective_collateral_*`     | What user would net after repaying loan if closed now. Floored at 0.                                                                                                                                                                                                                                |
| `current.mark_price_*`               | Current market mid price                                                                                                                                                                                                                                                                            |
| `current.market_leverage_bps`        | Current market-value leverage in basis points, computed from the live oracle price. `null` when the position is insolvent (equity ≤ 0).                                                                                                                                                             |
| `current.net_unrealized_pnl_*`       | Unrealized PnL net of all fees (origination + pending lifetime + accrued venue)                                                                                                                                                                                                                     |
| `current.net_unrealized_pnl_bps`     | Net unrealized PnL as return on equity in basis points                                                                                                                                                                                                                                              |
| `current.notional_*`                 | Current notional (reduced by unwinds)                                                                                                                                                                                                                                                               |
| `current.position_token_units`       | Token quantity held (1,000,000 units = 1 token)                                                                                                                                                                                                                                                     |
| `current.position_value_*`           | `tokens × mark_price`                                                                                                                                                                                                                                                                               |
| `current.remaining_bps`              | Fraction of the originally opened size still held, in basis points (10000 = 100%). `null` until the open fill is recorded.                                                                                                                                                                          |
| `current.unrealized_pnl_*`           | PnL assuming close at mark price                                                                                                                                                                                                                                                                    |
| `current.unrealized_pnl_bps`         | Unrealized PnL as return on equity in basis points                                                                                                                                                                                                                                                  |
| **risk**                             | Liquidation metrics                                                                                                                                                                                                                                                                                 |
| `risk.current_liquidation_price_*`   | Price triggering liquidation                                                                                                                                                                                                                                                                        |
| `risk.health_bps`                    | Margin health 0–10000 (10000 at entry, 0 at liquidation)                                                                                                                                                                                                                                            |
| `risk.liquidation_buffer_bps`        | `(mark_price - liq_price) / mark_price × 10000`                                                                                                                                                                                                                                                     |
| `risk.liquidation_fee_bps`           | Fee on liquidation, applied to borrowed capital (250 = 2.5%)                                                                                                                                                                                                                                        |
| `risk.margin_buffer_*`               | Dollar distance to liquidation                                                                                                                                                                                                                                                                      |
| **fees**                             | Fee accruals                                                                                                                                                                                                                                                                                        |
| `fees.accrued_lifetime_fee_*`        | Fees accrued since last unwind or open                                                                                                                                                                                                                                                              |
| `fees.accrued_venue_fee_*`           | Venue (Polymarket/Kalshi) trading fees paid so far on this position (open + any force-unwind legs)                                                                                                                                                                                                  |
| `fees.lifetime_fee_apr_bps`          | Annual rate on borrowed capital, i.e. notional − collateral (2000 = 20% APR)                                                                                                                                                                                                                        |
| `fees.pending_lifetime_fee_*`        | Uncollected fees from prior unwinds                                                                                                                                                                                                                                                                 |
| `fees.total_fees_*`                  | Rollup of all fees so far (origination + accrued lifetime + pending lifetime + accrued venue)                                                                                                                                                                                                       |
| **timing**                           |                                                                                                                                                                                                                                                                                                     |
| `timing.is_settlement_pending`       | Whether settlement is pending (market resolved or voided, settlement not yet executed)                                                                                                                                                                                                              |
| `timing.is_voided`                   | Whether the market was voided (closed with no winner, 50/50 payout at $0.50 per token)                                                                                                                                                                                                              |
| `timing.market_close_time`           | When the market resolves (null if no close time)                                                                                                                                                                                                                                                    |
| `timing.market_status`               | Market status (`active`, `closed`, `determined`, `finalized`, etc.). When `determined` or `finalized`, mark price reflects the settlement outcome ($1 or $0)                                                                                                                                        |
| `timing.settlement_state`            | Where the market sits on the road to settlement: `none`, `awaiting_resolution`, `settling`, `voided`, or `unresolved_upstream`. See below                                                                                                                                                           |
| `timing.time_to_close_minutes`       | Minutes until close (null if no close time)                                                                                                                                                                                                                                                         |

#### Settlement states

A market stops trading before its result exists on chain, so a position can sit open with nothing to redeem for a while. `settlement_state` tells you which part of that road the market is on:

| Value                 | What it means                                                                                                                                                                 |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `none`                | The market is still trading. Nothing is pending.                                                                                                                              |
| `awaiting_resolution` | The market has closed and the outcome is decided, but the prediction market provider has not published the result on chain yet. Nothing can be redeemed. Positions stay open. |
| `settling`            | The result is published and we are settling open positions. This is the state where `is_settlement_pending` is `true`.                                                        |
| `voided`              | The market was voided. Every token pays out at $0.50 regardless of side.                                                                                                      |
| `unresolved_upstream` | The market disappeared from the provider before publishing a result and may never resolve.                                                                                    |

`awaiting_resolution` is normal and often lasts hours — sports markets in particular close when the game ends but only resolve once the provider's oracle reports. Show it to your users as "waiting to settle" rather than treating it as an error or a stuck position.

Note that `is_settlement_pending` is `false` during `awaiting_resolution`: it means "the result is in and we are settling", not "settlement is expected at some point". Use `settlement_state` when you need to tell those apart.

***

**Closed / settled / liquidated positions** replace `current`, `risk`, and `timing` with `close_reason` and `result`:

```json
{
  "id": "dm_pos_xyz789",
  "created_at": "2025-06-01T09:59:55.000Z",
  "market_ticker": "will-eth-merge-succeed",
  "market_title": "Will the Ethereum merge succeed?",
  "provider": "polymarket",
  "status": "settled",
  "side": "yes",
  "wallet_address": "0x1234...abcd",
  "effective_leverage_bps": 50000,
  "failure": null,
  "close_reason": "settled",
  "revert_reason": null,
  "entry": {
    "collateral_usd_pips": "50000000",
    "collateral_usd": "5000.00",
    "notional_usd_pips": "250000000",
    "notional_usd": "25000.00",
    "leverage_bps": 50000,
    "price_usd_pips": "5000",
    "price_usd": "0.50",
    "position_token_units": "49800000000",
    "origination_fee_bps": 125,
    "protocol_origination_fee_bps": 100,
    "partner_origination_fee_bps": 25,
    "origination_fee_usd_pips": "3125000",
    "origination_fee_usd": "312.50",
    "opened_at": "2025-06-01T10:00:00.000Z",
    "open_latency_ms": 12500
  },
  "result": {
    "closed_at": "2025-07-15T14:30:00.000Z",
    "realized_pnl_usd_pips": "248000000",
    "realized_pnl_usd": "24800.00",
    "net_realized_pnl_usd_pips": "240875000",
    "net_realized_pnl_usd": "24087.50",
    "net_realized_pnl_bps": 48175,
    "proceeds_usd_pips": "296500000",
    "proceeds_usd": "29650.00",
    "exit_notional_usd_pips": "498000000",
    "exit_notional_usd": "49800.00",
    "collected_lifetime_fee_usd_pips": "1500000",
    "collected_lifetime_fee_usd": "150.00",
    "collected_liquidation_fee_usd_pips": "0",
    "collected_liquidation_fee_usd": "0.00"
  },
  "fees": {
    "lifetime_apr_bps": 2000,
    "total_lifetime_fee_usd_pips": "1500000",
    "total_lifetime_fee_usd": "150.00",
    "origination_fee_bps": 125,
    "protocol_origination_fee_bps": 100,
    "partner_origination_fee_bps": 25,
    "origination_fee_usd_pips": "3125000",
    "origination_fee_usd": "312.50",
    "total_venue_fee_usd_pips": "2500000",
    "total_venue_fee_usd": "250.00",
    "total_fees_usd_pips": "7125000",
    "total_fees_usd": "712.50"
  }
}
```

> **Deprecated fields on closed `fees`:** `origination_fee_bps`, `protocol_origination_fee_bps`, `partner_origination_fee_bps`, `origination_fee_usd_pips`, `origination_fee_usd` are still emitted for backwards compatibility but duplicate the canonical values on `entry.*`. New integrations should read the origination fields from `entry` only.

| Field                                | Description                                                                                                                                                                                                                                                        |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `close_reason`                       | `closed`, `settled`, `liquidated`, `cancelled`, or `reverted`                                                                                                                                                                                                      |
| `fees.total_fees_*`                  | Total blended fees (origination + lifetime + liquidation + venue)                                                                                                                                                                                                  |
| `fees.total_lifetime_fee_*`          | Total lifetime fees collected at close                                                                                                                                                                                                                             |
| `fees.total_venue_fee_*`             | Total venue (Polymarket/Kalshi) trading fees paid across the position lifetime (open + close/liquidate/settle + force-unwinds)                                                                                                                                     |
| `result.closed_at`                   | When the position was finalized                                                                                                                                                                                                                                    |
| `result.collected_lifetime_fee_*`    | Total lifetime fees collected                                                                                                                                                                                                                                      |
| `result.collected_liquidation_fee_*` | Liquidation fee (0 unless liquidated)                                                                                                                                                                                                                              |
| `result.exit_notional_*`             | Volume-weighted notional realized across all unwinds and the final close (`Σ shares sold × fill price`). Pairs with `entry.notional_*` for an entry-vs-exit view. Null for `reverted`/`cancelled` positions (no shares were ever sold).                            |
| `result.net_realized_pnl_*`          | Realized PnL net of all fees (origination + lifetime + liquidation + venue)                                                                                                                                                                                        |
| `result.net_realized_pnl_bps`        | Net realized PnL as return on equity in basis points                                                                                                                                                                                                               |
| `result.proceeds_*`                  | pUSD returned to user                                                                                                                                                                                                                                              |
| `result.realized_pnl_*`              | Final PnL: `proceeds - collateral`                                                                                                                                                                                                                                 |
| `revert_reason`                      | Why a reverted open failed. Non-null only when `close_reason` is `reverted`: `exchange_unavailable` (venue was temporarily unavailable — safe to retry), `slippage_exceeded` (price moved beyond tolerance before the order filled), or `unknown`. Null otherwise. |

***

### Get position

Get a single position. Must belong to the JWT's `(wallet_address, partner_id)`.

```
GET /prediction-markets/positions/{position_id}
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
// No dedicated single-position method — filter from getPositions():
const position = (await client.getPositions()).find((p) => p.id === positionId);
```

**Auth:** User JWT

**Query parameters:**

| Param    | Type   | Description                                                                                                                                        |
| -------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `expand` | string | Comma-separated list of related sub-resources to embed inline. Allowed values: `unwinds`. See [Expanding sub-resources](#expanding-sub-resources). |

**Response: `200 OK`** — Single position object (not wrapped in `data` array).

**Errors:**

| Status | Code                          |
| ------ | ----------------------------- |
| 404    | `customer_position_not_found` |

***

### List your positions for a market

Returns the authenticated user's positions on a single market.

```
GET /prediction-markets/markets/{ticker}/positions
```

**Auth:** User JWT

**Query parameters:** identical to [List positions](#list-positions) — `status`, `state`, `sort_by`, `sort_direction`, `limit`, `starting_after`, `ending_before`, and `expand`.

**Response: `200 OK`** — Same paginated envelope and same position objects as `GET /positions`. Returns an empty page if you hold no positions on that market.

**Errors:**

| Status | Code                        |
| ------ | --------------------------- |
| 404    | `customer_market_not_found` |

***

### List your positions for an event

Returns the authenticated user's positions across every market on a single event — for example, every position you hold on tonight's game.

```
GET /prediction-markets/events/{event_ticker}/positions
```

Get the `event_ticker` from the `event.ticker` field of any market.

**Auth:** User JWT

**Query parameters:** identical to [List positions](#list-positions).

**Response: `200 OK`** — Same paginated envelope and same position objects as `GET /positions`.

**Errors:**

| Status | Code                       |
| ------ | -------------------------- |
| 404    | `customer_event_not_found` |

***

### List your positions for a series

Returns the authenticated user's positions across every market in a single series, spanning all of that series' events.

```
GET /prediction-markets/series/{series_ticker}/positions
```

Get the `series_ticker` from the `event.series_ticker` field of any market.

**Auth:** User JWT

**Query parameters:** identical to [List positions](#list-positions).

**Response: `200 OK`** — Same paginated envelope and same position objects as `GET /positions`.

**Errors:**

| Status | Code                        |
| ------ | --------------------------- |
| 404    | `customer_series_not_found` |

***

### Position unwinds (sub-resource)

Each position has an unwinds (deleveraging) history. Fetch it inline by passing `expand=unwinds` to [List positions](#list-positions). Only completed unwinds are returned — pending or reverted unwinds are excluded.

```json
{
  "data": [
    {
      "id": "dm_pos_abc123",
      "...": "...",
      "unwinds": {
        "current_leverage_bps": 30000,
        "origination_leverage_bps": 60000,
        "originated_at": "2025-06-01T12:00:00.000Z",
        "data": [
          {
            "executed_at": "2025-06-02T14:30:00.000Z",
            "before_leverage_bps": 60000,
            "after_leverage_bps": 30000,
            "reason": "spread_blowout",
            "reason_detail": "The bid-ask spread widened sharply beyond its recent baseline, signalling thinning liquidity."
          }
        ],
        "has_more": false
      }
    }
  ],
  "has_more": true
}
```

**Field reference (each unwind event):**

| Field                 | Type    | Description                                                                                                                                                                                                                     |
| --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `after_leverage_bps`  | integer | Leverage after unwind in basis points (20000 = 2x)                                                                                                                                                                              |
| `before_leverage_bps` | integer | Leverage before unwind in basis points (20000 = 2x)                                                                                                                                                                             |
| `executed_at`         | string  | ISO 8601 timestamp when the unwind was executed on-chain                                                                                                                                                                        |
| `reason`              | string  | Machine-readable code for the market signal that triggered the risk model to deleverage this position. `null` for unwinds not tied to a risk-model run (e.g. manual admin deleveraging). See the reason reference below.        |
| `reason_detail`       | string  | Human-readable, customer-facing sentence explaining `reason` (e.g. "The bid-ask spread widened sharply beyond its recent baseline, signalling thinning liquidity."). Display this directly; `null` whenever `reason` is `null`. |

**`reason` values:**

| Value                   | Meaning                                                                                       |
| ----------------------- | --------------------------------------------------------------------------------------------- |
| `spread_blowout`        | The bid-ask spread widened sharply beyond its recent baseline, signalling thinning liquidity. |
| `spread_spike`          | The bid-ask spread jumped abruptly to several times its baseline.                             |
| `spread_warning`        | The bid-ask spread widened past an elevated warning level.                                    |
| `depth_decay`           | Resting order-book depth fell well below its recent peak.                                     |
| `depth_drain`           | Order-book depth across the market dropped sharply from entry levels.                         |
| `depth_entry_drain`     | Order-book depth fell far below the level present when the position opened.                   |
| `price_drop_warning`    | The position's side ticked down — an early adverse-price signal.                              |
| `price_drop_moderate`   | The position's side fell moderately against the position.                                     |
| `price_drop_severe`     | The position's side fell sharply against the position.                                        |
| `price_drop_full_exit`  | The position's side dropped far enough to fully deleverage rather than step down.             |
| `activity_surge`        | A surge in trading activity signalled elevated volatility.                                    |
| `cancel_acceleration`   | A spike in order cancellations signalled market makers pulling liquidity.                     |
| `large_holder`          | A large holder entered or materially grew their position in this market.                      |
| `position_exposure`     | The position grew large relative to available exit liquidity.                                 |
| `last_trade_divergence` | The last traded price diverged materially from the current bid.                               |
| `crypto_move`           | The underlying crypto price moved beyond its risk threshold.                                  |
| `game_start`            | The underlying game went live.                                                                |
| `lead_change`           | The lead changed in the underlying game.                                                      |
| `post_hard_exit_losing` | Risk was re-evaluated after a hard-exit event on the losing side.                             |
| `stale_refresh`         | A scheduled periodic risk refresh, not triggered by a specific market event.                  |
| `unknown`               | The position was deleveraged by the risk engine; the specific signal was not attributed.      |

The first unwind's `before_leverage_bps` equals the position's entry leverage. Each subsequent unwind's `before_leverage_bps` equals the previous unwind's `after_leverage_bps`.

***

### Position transactions (sub-resource)

Returns every on-chain transaction tied to a position, grouped by operation type, so you can give users full visibility into the on-chain transfers behind their position. Each group is an object with a `transactions` array; transactions that route through an exchange (open, close, liquidation, settle, force\_unwind) also include the on-chain hashes of their nested exchange (CLOB/DEX) fills. Only transactions that landed and finalized on-chain are returned — pending, failed, and reverted transactions are excluded.

```
GET /prediction-markets/positions/{position_id}/transactions
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const transactions = await client.getPositionTransactions(positionId);
```

**Auth:** User JWT — must belong to the JWT's `(wallet_address, partner_id)`.

**Response: `200 OK`**

```json
{
  "open": {
    "transactions": [
      {
        "transaction_hash": "0xffe5e3d042e9bf100fd01ad7a8d4de5d7fd20632df62e319d50275673e42e410",
        "exchange_transaction_hashes": [
          "0xddc31a8f7d2e4c0b9a1e5f3c8b6d4a2e0f9c7b5a3d1e8f6c4b2a0d9e7f5c3b1a"
        ]
      },
      {
        "transaction_hash": "0xaa11bb22cc33dd44ee55ff6677889900aabbccddeeff00112233445566778899"
      }
    ]
  },
  "close": {
    "transactions": []
  },
  "liquidation": {
    "transactions": []
  },
  "settle": {
    "transactions": []
  },
  "force_unwind": {
    "transactions": []
  },
  "cancel_request": {
    "transactions": []
  },
  "cancel": {
    "transactions": []
  },
  "revert": {
    "transactions": []
  },
  "redemption": {
    "transactions": []
  }
}
```

Each group is an object with a `transactions` array (wrapped in an object so the response can carry per-group metadata in the future without a breaking change).

**Field reference:**

| Field            | Type   | Description                                                                                            |
| ---------------- | ------ | ------------------------------------------------------------------------------------------------------ |
| `cancel`         | object | Transactions that cancelled the position before it opened.                                             |
| `cancel_request` | object | The close request transaction the position owner submitted on-chain.                                   |
| `close`          | object | Transactions that closed the position, including the finalize-close transaction.                       |
| `force_unwind`   | object | Transactions that force-unwound (deleveraged) the position, including the finalize-unwind transaction. |
| `liquidation`    | object | Transactions that liquidated the position, including the finalize-liquidation transaction.             |
| `open`           | object | Transactions that opened the position, including the finalize-open transaction.                        |
| `redemption`     | object | Transactions that redeemed settled position tokens.                                                    |
| `revert`         | object | Transactions that reverted a failed open back to the protocol.                                         |
| `settle`         | object | Transactions that settled the position, including the finalize-settle transaction.                     |

Every group object has a single `transactions` field (`object[]`) holding the entries below.

**Each transaction object:**

| Field                         | Type      | Description                                                                                             |
| ----------------------------- | --------- | ------------------------------------------------------------------------------------------------------- |
| `exchange_transaction_hashes` | string\[] | On-chain hashes of the nested exchange (CLOB/DEX) fills. Present only for exchange-routed transactions. |
| `transaction_hash`            | string    | On-chain transaction hash (EVM transaction hash or Solana signature).                                   |

**Errors:**

| Status | Code                                       |
| ------ | ------------------------------------------ |
| 404    | `customer_position_transactions_not_found` |

***

## 5. Limits

### Partner limits

Partner's global position limit and current usage.

```
GET /prediction-markets/partner-limits
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const limits = await client.getPartnerLimits();
```

**Auth:** Partner API key

**Response: `200 OK`**

```json
{
  "limit_usd_pips": "5000000000",
  "limit_usd": "500000.00",
  "usage_usd_pips": "1200000000",
  "usage_usd": "120000.00",
  "remaining_usd_pips": "3800000000",
  "remaining_usd": "380000.00"
}
```

### User limits

Position limits for the authenticated user.

```
GET /prediction-markets/user-limits
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const limits = await client.getUserLimits();
```

**Auth:** User JWT

**Response: `200 OK`**

```json
{
  "limit_usd_pips": "100000000",
  "limit_usd": "10000.00",
  "usage_usd_pips": "25000000",
  "usage_usd": "2500.00",
  "remaining_usd_pips": "75000000",
  "remaining_usd": "7500.00"
}
```

| Field             | Description                                        |
| ----------------- | -------------------------------------------------- |
| `limit_usd_*`     | Maximum total notional across all open positions   |
| `remaining_usd_*` | `limit - usage`                                    |
| `usage_usd_*`     | Sum of origination notional for all open positions |

***

## 6. Fees

These endpoints let you reproduce a quote's economics client-side — the fee schedule, and a full pre-quote cost/profit breakdown — without creating a quote.

### Fee rates

The protocol fee schedule plus your partner's fee components. Pass `ticker` to also include that market's venue (Polymarket) fee fields.

```
GET /prediction-markets/fee-rates
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const feeRates = await client.getFeeRates({ticker: "will-btc-hit-100k-2025"});
```

**Auth:** User JWT

**Query parameters:**

| Parameter | Required | Description                                              |
| --------- | -------- | -------------------------------------------------------- |
| `ticker`  | No       | Market ticker. When set, the response includes `market`. |

**Response: `200 OK`**

```json
{
  "contract_max_origination_fee_bps": 1000,
  "lifetime_fee_apr_bps": 2000,
  "liquidation_fee_bps": 250,
  "origination_fee_tiers": [
    {
      "fee_bps": 200,
      "max_leverage_bps": 40000
    },
    {
      "fee_bps": 225,
      "max_leverage_bps": 70000
    },
    {
      "fee_bps": 250,
      "max_leverage_bps": 100000
    }
  ],
  "partner_origination_fee_bps": 0,
  "partner_trading_fee_bps": 0,
  "market": {
    "polymarket_fee_exponent": 1,
    "polymarket_trading_fee_bps": 0,
    "ticker": "TRUMP-2024-WIN"
  }
}
```

| Field                              | Description                                                                                                  |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `origination_fee_tiers`            | Leverage-tiered protocol origination fee. Pick the first tier whose `max_leverage_bps >= leverage_bps`.      |
| `contract_max_origination_fee_bps` | On-chain cap on the combined (protocol + partner) origination fee.                                           |
| `lifetime_fee_apr_bps`             | Lifetime fee APR, charged on protocol capital over time.                                                     |
| `liquidation_fee_bps`              | Liquidation fee, on protocol capital.                                                                        |
| `partner_origination_fee_bps`      | Your origination fee component, added on top of the protocol tier. *(deprecated — prefer builder code fees)* |
| `partner_trading_fee_bps`          | Your Polymarket builder taker fee (flat % of notional).                                                      |
| `market`                           | Per-market venue fee fields. Only present when `ticker` is supplied.                                         |

### Fee report

A full cost and max-gain breakdown for a hypothetical position, computed from notional + leverage using the market's fee rates. Does **not** create a quote and does not sign anything. The liquidation price is a deterministic at-entry estimate; the binding quote uses a TWAP/inference-based price that may differ.

```
POST /prediction-markets/fee-reports
```

**SDK** ([`@dimes-dot-fi/sdk`](/for-developers/getting-started/sdk-installation)):

```typescript
const report = await client.getFeeReport({ /* FeeReportParams: date range + filters */});
```

**Auth:** User JWT

**Request body:**

| Field                      | Required | Description                                                                                         |
| -------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `market_ticker`            | Yes      | Market ticker.                                                                                      |
| `effective_side`           | Yes      | `yes` or `no`.                                                                                      |
| `leverage_bps`             | Yes      | Leverage in basis points (20000 = 2x).                                                              |
| `notional_amount_usd_pips` | Yes      | Notional in USD pips.                                                                               |
| `entry_price_usd_pips`     | No       | Effective-side entry price in USD pips. When omitted, the market's current reference price is used. |

**Response: `200 OK`**

```json
{
  "collateral_usdc_units": "25000000",
  "effective_side": "yes",
  "entry_price_usd_pips": "5100",
  "estimated_liquidation_price_usd_pips": "2613",
  "expected_open_trading_fee_usdc_units": "0",
  "gross_max_gain_usdc_units": "48039215",
  "leverage_bps": 20000,
  "market_ticker": "TRUMP-2024-WIN",
  "net_max_gain_usdc_units": "47039215",
  "notional_amount_usd_pips": "500000",
  "notional_usdc_units": "50000000",
  "origination_fee_bps": 200,
  "origination_fee_usdc_units": "1000000",
  "partner_origination_fee_bps": 0,
  "partner_trading_fee_bps": 0,
  "polymarket_trading_fee_bps": 0,
  "protocol_origination_fee_bps": 200,
  "total_user_amount_usdc_units": "26000000"
}
```

| Field                                  | Description                                                                                                             |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `estimated_liquidation_price_usd_pips` | At-entry liquidation price estimate: `entry * (L-1)/L * (1 + liquidation_fee_bps/10000)`. Estimate only.                |
| `expected_open_trading_fee_usdc_units` | Venue trading fee to open (protocol venue fee + partner builder fee).                                                   |
| `gross_max_gain_usdc_units`            | Profit on a win (settlement at $1) before fees: full token value minus notional. May be negative.                       |
| `net_max_gain_usdc_units`              | `gross_max_gain` minus the open trading fee and origination fee. Excludes lifetime fees (upper bound). May be negative. |
| `origination_fee_*`                    | Combined origination fee (protocol tier + partner) at this leverage.                                                    |
| `total_user_amount_usdc_units`         | Total the user must provide to open: collateral + open trading fee + origination fee.                                   |

***

## Endpoint Summary

| Method | Path                                                          | Auth                | Description            |
| ------ | ------------------------------------------------------------- | ------------------- | ---------------------- |
| `GET`  | `/v1/prediction-markets/markets`                              | User JWT or API key | List/search markets    |
| `GET`  | `/v1/prediction-markets/markets/{ticker}`                     | User JWT or API key | Get market by ticker   |
| `GET`  | `/v1/prediction-markets/events/{event_ticker}/markets`        | User JWT or API key | Markets for an event   |
| `GET`  | `/v1/prediction-markets/series/{series_ticker}/markets`       | User JWT or API key | Markets for a series   |
| `GET`  | `/v1/prediction-markets/contract-info`                        | Public              | Contract verification  |
| `POST` | `/v1/prediction-markets/tokens`                               | Partner API key     | Generate user JWT      |
| `POST` | `/v1/prediction-markets/quotes`                               | User JWT            | Create quote           |
| `POST` | `/v1/prediction-markets/draft-quotes`                         | User JWT            | Create draft quote     |
| `POST` | `/v1/prediction-markets/promoted-quotes/{id}`                 | User JWT            | Promote draft to quote |
| `GET`  | `/v1/prediction-markets/positions`                            | User JWT            | List positions         |
| `GET`  | `/v1/prediction-markets/positions/{position_id}`              | User JWT            | Get position           |
| `GET`  | `/v1/prediction-markets/positions/{position_id}/transactions` | User JWT            | Position transactions  |
| `GET`  | `/v1/prediction-markets/markets/{ticker}/positions`           | User JWT            | Positions on a market  |
| `GET`  | `/v1/prediction-markets/events/{event_ticker}/positions`      | User JWT            | Positions on an event  |
| `GET`  | `/v1/prediction-markets/series/{series_ticker}/positions`     | User JWT            | Positions in a series  |
| `GET`  | `/v1/prediction-markets/partner-limits`                       | Partner API key     | Partner limits         |
| `GET`  | `/v1/prediction-markets/user-limits`                          | User JWT            | User limits            |
| `GET`  | `/v1/prediction-markets/fee-rates`                            | User JWT            | Fee schedule           |
| `POST` | `/v1/prediction-markets/fee-reports`                          | User JWT            | Cost & max-gain report |
| `POST` | `/v1/prediction-markets/sample-events`                        | User JWT or API key | Mock WebSocket events  |

***

## Throttling

All endpoints are rate-limited. The throttling scope and limit depend on authentication:

| Auth type       | Scope          | Limit              |
| --------------- | -------------- | ------------------ |
| API key         | Per partner    | 120 requests / min |
| JWT             | Per wallet     | 60 requests / min  |
| Unauthenticated | Per IP address | 30 requests / min  |

**Exception:** The `POST /v1/prediction-markets/tokens` endpoint (JWT generation) is throttled at **1800 requests per minute** per partner.

**Exception:** The `POST /v1/prediction-markets/sample-events` endpoint (mock WebSocket events) is throttled at **one request every 30 seconds**, per partner for API-key callers and per wallet for JWT callers. See [Testing your integration](/for-developers/api-and-events/websocket#testing-your-integration).

### Rate limit headers

Every response includes:

| Header                  | Description                                    |
| ----------------------- | ---------------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests remaining in the current window       |
| `X-RateLimit-Reset`     | Seconds until the current window resets        |

When throttled, the response also includes a `Retry-After` header with the number of seconds to wait before retrying.

**`429 Too Many Requests`**

```json
{
  "error": {
    "type": "RATE_LIMIT_ERROR",
    "code": "too_many_requests",
    "message": "Too Many Requests"
  }
}
```


# WebSocket Events

Receive real-time position updates via WebSocket. All position state changes are delivered as events over a persistent Socket.IO connection.

> **SDK support:** The `@dimes-dot-fi/sdk/ws` entry point ships `PositionSocket` and `MarketSocket` clients for the **user gateways** (JWT auth). They wrap Socket.IO, manage reconnection, and camelize every payload to match the rest of the SDK. Prefer them over wiring up Socket.IO by hand — see [Using the SDK](#using-the-sdk) below. For the **partner gateways** (API key auth) the SDK has no client yet; use Socket.IO directly as shown in the raw examples. Runnable: [`05-websocket.ts`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/05-websocket.ts) (Node) and [`react/streams.tsx`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/react/streams.tsx) (React reconciliation).

Two WebSocket gateways are available. Both deliver the same event types and payload shapes — they differ only in authentication and scope.

| Gateway | Namespace                                  | Auth     | Scope                                     |
| ------- | ------------------------------------------ | -------- | ----------------------------------------- |
| Partner | `/v1/ws/prediction-markets/positions`      | API key  | All positions for the partner             |
| User    | `/v1/ws/prediction-markets/user-positions` | User JWT | Only positions for the authenticated user |

***

### Using the SDK

For the **user gateways**, the SDK's `@dimes-dot-fi/sdk/ws` entry point gives you typed clients so you don't touch Socket.IO directly. Both clients take a `getToken` callback (so they always reconnect with a fresh JWT) and an optional `baseUrl` (defaults to `https://api.dimes.fi` — set it to `https://api-sandbox.dimes.fi` for sandbox). Payloads arrive **camelCased** (`event.createdAt`, `event.data`) to match the REST types returned elsewhere by the SDK.

#### Position events

`PositionSocket` connects to the user-positions gateway. Subscribe to a specific event type, to `"*"` for all of them, or to `onNotification` for the user-gateway notification stream. Every `on*` call returns an unsubscribe function.

```typescript
import { PositionSocket } from "@dimes-dot-fi/sdk/ws";

const socket = new PositionSocket({
  getToken: () => currentJwt, // called on every (re)connect
  // baseUrl: "https://api-sandbox.dimes.fi", // for sandbox
});

socket.onConnect(() => console.log("connected"));

const off = socket.on("position.opened", (event) => {
  // event.data is a full Position (camelCased), same shape as the REST API
  addOpenPosition(event.data);
});

socket.on("position.opening", (event) => showFinalizing(event.data.id));

// Transient, informational messages (user gateway only)
socket.onNotification((event) => showToast(event.data.message));

socket.onError((err) => console.error("ws error", err));

socket.connect();

// later: off(); socket.disconnect();
```

When the JWT rotates, call `socket.reconnectWithToken()` to drop and re-establish the connection with the new token.

#### Market events

`MarketSocket` connects to the user-markets gateway with the same API. Remember that market event `data` is **always an array** (see [Market events](#market-events) below):

```typescript
import { MarketSocket } from "@dimes-dot-fi/sdk/ws";

const markets = new MarketSocket({ getToken: () => currentJwt });

markets.on("market.discovered", (event) => {
  for (const market of event.data) upsertMarket(market); // full Market objects
});

markets.on("market.eligibility_changed", (event) => {
  for (const delta of event.data) mergeMarketFields(delta.id, delta); // deltas
});

markets.connect();
```

The raw Socket.IO examples below remain accurate for the partner gateways and non-JS clients; the SDK clients simply wrap that same protocol for the user gateways.

***

### Partner gateway

**Namespace:**

| Environment | WebSocket URL                                                   |
| ----------- | --------------------------------------------------------------- |
| Production  | `wss://api.dimes.fi/v1/ws/prediction-markets/positions`         |
| Sandbox     | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/positions` |

Sandbox emits the same event types and payload shapes as production — only the host and the key prefix differ. See [Environments](/for-developers/getting-started/environments).

**Transport:** Socket.IO

**Auth:** Partner API key, passed as either:

* **Socket.IO `auth` payload (recommended):** `auth: { apiKey: "dm_live_skey_..." }` (or `dm_sbx_skey_...` in sandbox). Transmitted in the handshake body — never appears in URLs, server logs, or browser history.
* **HTTP header:** `Authorization: Api-Key dm_live_skey_...` (or `dm_sbx_skey_...` in sandbox). For Socket.IO clients, set this via the `extraHeaders` option.

On successful auth, the connection is scoped to the partner. All events for positions created through that partner's API keys are delivered.

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/positions", {
  auth: {apiKey: process.env.DIMES_API_KEY},
});

socket.on("connect", () => {
  console.log("Connected to position events");
});

socket.on("position.opened", (event) => {
  console.log("Position opened:", event.data.id);
});

socket.on("error", (error) => {
  console.error("WebSocket error:", error);
});
```

***

### User gateway

| Environment | WebSocket URL                                                        |
| ----------- | -------------------------------------------------------------------- |
| Production  | `wss://api.dimes.fi/v1/ws/prediction-markets/user-positions`         |
| Sandbox     | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/user-positions` |

**Transport:** Socket.IO

**Auth:** User JWT (the same token from `POST /tokens`), passed as either:

* **Socket.IO `auth` payload (recommended):** `auth: { token: "<jwt>" }`.
* **HTTP header:** `Authorization: Bearer <jwt>`. For Socket.IO clients, set this via the `extraHeaders` option.

On successful auth, the connection is scoped to the authenticated user. Only events for positions belonging to that specific wallet address and partner are delivered — other users' positions are never visible.

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/user-positions", {
  auth: {token: userJwt},
});

socket.on("position.opening", (event) => {
  // Early signal — position is being finalized on-chain
  showFinalizing(event.data.id);
});

socket.on("position.opened", (event) => {
  // Position fully open — display position card
  addOpenPosition(event.data);
});
```

***

### Authentication errors

If authentication fails on either gateway, the server emits an `error` event and disconnects:

```json
{
  "error": {
    "type": "AUTHENTICATION_ERROR",
    "code": "unauthorized",
    "message": "Unauthorized"
  }
}
```

***

### Event envelope

All events follow the `resource.action` naming pattern with a consistent envelope:

```json
{
  "id": "evt_abc123",
  "type": "position.opened",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": {
    ...
  }
}
```

| Field        | Type              | Description                                                                          |
| ------------ | ----------------- | ------------------------------------------------------------------------------------ |
| `id`         | string            | Unique event identifier (prefixed `evt_`)                                            |
| `type`       | string            | Event type in `resource.action` format                                               |
| `created_at` | string (ISO 8601) | When the event was emitted                                                           |
| `data`       | object            | Event payload — always contains the full position object matching the REST API shape |

The `data` object contains the same representation you would get from `GET /positions/{id}`. This means event consumers can reuse the same types/parsers as their REST integration.

***

### Event types

| Event Type                          | Trigger                                                                                                                                                  |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `position.created`                  | On-chain position account created (user submitted tx)                                                                                                    |
| `position.opening`                  | Finalize-open tx confirmed on-chain (early signal, before indexer picks up)                                                                              |
| `position.opened`                   | Position finalized on-chain, tokens acquired (indexer confirmed)                                                                                         |
| `position.close_requested`          | User submitted close request                                                                                                                             |
| `position.closed`                   | Close finalized, proceeds distributed                                                                                                                    |
| `position.settled`                  | Market resolved, position settled                                                                                                                        |
| `position.liquidated`               | Position liquidated due to insufficient margin                                                                                                           |
| `position.force_unwound`            | Position partially unwound to reduce leverage (position remains open)                                                                                    |
| `position.reverted`                 | Position open failed, collateral refunded                                                                                                                |
| `position.cancelled`                | Position cancelled before opening (EVM only)                                                                                                             |
| `position.settlement_state_changed` | The position's market moved to a new settlement stage (e.g. the market closed and is now awaiting resolution). The position itself does not change state |

***

### Event payloads

The `data` field in every event is a full position object — the same shape returned by the REST API. See the [Positions](https://docs.dimes.fi/for-developers/api-and-events/pages/NdOdqtZSHifVN4PbuN3l#4.-positions-user-jwt) section of the API Reference for complete field definitions.

| Event                               | `status`     | Position shape                                                                        |
| ----------------------------------- | ------------ | ------------------------------------------------------------------------------------- |
| `position.created`                  | `pending`    | Open shape (`entry`, `current`, `risk`, `fees`, `timing`)                             |
| `position.opening`                  | `pending`    | Open shape (early signal — position not yet indexed)                                  |
| `position.opened`                   | `open`       | Open shape                                                                            |
| `position.close_requested`          | `closing`    | Open shape                                                                            |
| `position.closed`                   | `closed`     | Closed shape — `close_reason: "closed"`, `entry`, `result`, `fees`                    |
| `position.settled`                  | `settled`    | Closed shape — `close_reason: "settled"`, `entry`, `result`, `fees`                   |
| `position.liquidated`               | `liquidated` | Closed shape — `close_reason: "liquidated"`, `entry`, `result`, `fees`                |
| `position.force_unwound`            | `open`       | Open shape (reduced size — `current` values reflect new position)                     |
| `position.reverted`                 | `cancelled`  | Closed shape — `close_reason: "reverted"`, `revert_reason`, `entry`, `result`, `fees` |
| `position.cancelled`                | `cancelled`  | Closed shape — `close_reason: "reverted"`, `entry`, `result`, `fees`                  |
| `position.settlement_state_changed` | unchanged    | Open shape — read `timing.settlement_state` and `timing.market_status`                |

Open positions include `current`, `risk`, `fees`, and `timing` sections. Closed, settled, and liquidated positions replace those with `close_reason` and `result` (including `collected_liquidation_fee_*` for liquidations).

The `position.reverted` payload additionally carries `revert_reason`, classifying why the open failed before it landed: `exchange_unavailable` (the prediction-market venue was temporarily unavailable — safe to retry), `slippage_exceeded` (price moved beyond your slippage tolerance before the order filled), or `unknown`. It is non-null only when `close_reason` is `reverted`; collateral is always returned regardless of reason. Surface `exchange_unavailable` as a transient, retryable issue and `slippage_exceeded` as a price-moved message.

#### `position.opening` vs `position.opened`

`position.opening` fires as soon as the finalize-open transaction is confirmed on-chain — **before** the event indexer picks it up. This gives the UI an early signal (\~5-15 seconds faster) that the position is about to open.

`position.opened` fires later when the indexer delivers the confirmed `PositionOpened` event.

**Recommended UI behavior:** On `position.opening`, show a "finalizing" state. On `position.opened`, transition to the full open position view with live data. If `position.reverted` arrives instead, the open failed — show the error state.

***

### Listening to events

Subscribe to specific event types using Socket.IO's standard event listeners:

```javascript
socket.on("position.created", (event) => {
  updatePositionStatus(event.data.id, "pending");
});

socket.on("position.opening", (event) => {
  // Early signal — show "finalizing" state
  updatePositionStatus(event.data.id, "finalizing");
});

socket.on("position.opened", (event) => {
  addOpenPosition(event.data);
});

socket.on("position.close_requested", (event) => {
  updatePositionStatus(event.data.id, "closing");
});

socket.on("position.closed", (event) => {
  showSettlement(event.data);
});

socket.on("position.settled", (event) => {
  showSettlement(event.data);
});

socket.on("position.liquidated", (event) => {
  alertLiquidation(event.data);
});

socket.on("position.force_unwound", (event) => {
  // Position still open but reduced in size
  updateOpenPosition(event.data);
});

socket.on("position.reverted", (event) => {
  // Open failed — show error
  showOpenFailed(event.data);
});

socket.on("position.cancelled", (event) => {
  showCancelled(event.data);
});
```

***

### Notification events (user gateway only)

In addition to position state-transition events, the user gateway emits `notification` events. These are informational messages that provide feedback during asynchronous operations — they are not errors and do not indicate a state change.

**Envelope:**

```json
{
  "id": "evt_abc123",
  "type": "notification",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": {
    "code": "ORDER_FULFILLMENT_RETRYING",
    "message": "Order fulfillment not possible at current slippage tolerance. Retrying.",
    "params": {
      "position_id": "dm_pos_abc123",
      "slippage_bps": 150
    }
  }
}
```

| Field          | Type   | Description                                      |
| -------------- | ------ | ------------------------------------------------ |
| `data.code`    | string | Machine-readable notification code               |
| `data.message` | string | Human-readable description                       |
| `data.params`  | object | Additional context (varies by notification code) |

**Notification codes:**

| Code                         | When                                                                            | Params                                                                                     |
| ---------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `ORDER_FULFILLMENT_RETRYING` | Order could not be filled at current slippage; retrying                         | `position_id`, `slippage_bps`                                                              |
| `PARTIAL_OPEN_PROGRESS`      | Floor or retry leg of a FAK partial-open fulfilled; reports cumulative progress | `position_id`, `cumulative_filled_making_amount`, `target_making_amount`, `attempt_number` |
| `PARTIAL_OPEN_FLOOR_MISSED`  | Floor FOK of a FAK partial-open failed to fill after retries; position reverted | `position_id`                                                                              |

**Listening:**

```javascript
socket.on("notification", (event) => {
  showToast(event.data.message);
});
```

Notifications are transient — display them as a toast or inline status message. They do not require user action.

***

## Market events

In addition to position events, two WebSocket gateways stream **market** updates: new markets coming online, eligibility changes, and max-leverage changes. They mirror the position gateways exactly — same Socket.IO transport, same auth, same envelope — and differ only in scope.

| Gateway | Namespace                                | Auth     | Scope                                     |
| ------- | ---------------------------------------- | -------- | ----------------------------------------- |
| Partner | `/v1/ws/prediction-markets/markets`      | API key  | All markets visible to the partner        |
| User    | `/v1/ws/prediction-markets/user-markets` | User JWT | Markets visible to the authenticated user |

Auth is identical to the position gateways: the partner gateway takes an API key (`auth: { apiKey: "dm_live_skey_..." }` or `Authorization: Api-Key ...`), and the user gateway takes a user JWT (`auth: { token: "<jwt>" }` or `Authorization: Bearer <jwt>`). See [Partner gateway](#partner-gateway) and [User gateway](#user-gateway) above for the full handshake details — they apply unchanged.

| Environment | Partner URL                                                   | User URL                                                           |
| ----------- | ------------------------------------------------------------- | ------------------------------------------------------------------ |
| Production  | `wss://api.dimes.fi/v1/ws/prediction-markets/markets`         | `wss://api.dimes.fi/v1/ws/prediction-markets/user-markets`         |
| Sandbox     | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/markets` | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/user-markets` |

### `data` is always an array

> **Important:** Unlike position events — where `data` is a single object — market events always deliver `data` as an **array**. A single market change arrives as an array of one; when many markets change at once (e.g. a batch eligibility sweep), they are batched into a single event. **Always iterate `data` as an array.**

The envelope is otherwise identical to position events:

```json
{
  "id": "evt_mkt_abc123",
  "type": "market.discovered",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": [
    { ... },
    { ... }
  ]
}
```

### Market event types

| Event Type                    | Trigger                                                     | `data` items                                                                 |
| ----------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `market.discovered`           | A new market comes online                                   | **Full** market objects (same shape as `GET /v1/markets`)                    |
| `market.eligibility_changed`  | A market's tradeability / capacity / notional limits change | **Delta** objects — only the changed fields                                  |
| `market.max_leverage_changed` | An eligible market's max leverage changes                   | **Delta** objects — `id`, `polymarket`, `leverage` (no `min_bps`/`step_bps`) |

`market.discovered` carries the **complete** market payload — identical to an item from `GET /v1/markets`, including the optional `prices` block when available. The two `*_changed` events carry **deltas**: each item contains the market `id`, the always-present `polymarket` identifiers (so you can map the delta onto your cached market), and **only the fields that actually changed**. Merge those fields into your cached copy of the market by `id`.

#### `market.discovered`

`data` is an array of full market objects, each identical to an item from `GET /v1/markets`. See the [Markets](/for-developers/api-and-events/api-reference) section of the API Reference for complete field definitions.

```json
{
  "id": "evt_mkt_abc123",
  "type": "market.discovered",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": [
    {
      "id": "dm_mkt_abc123",
      "category": "politics",
      "provider": "polymarket",
      "status": "active",
      "ticker": "will-trump-win-the-2024-election",
      "title": "Will Trump win the 2024 election?",
      "accepting_new_positions": true,
      "min_notional_usd": "15.00",
      "min_notional_usd_pips": "150000",
      "max_notional_yes_usd": "50.00",
      "max_notional_no_usd": "50.00",
      "sided_eligibility": { "yes": { "...": "..." }, "no": { "...": "..." } },
      "leverage": { "...": "..." },
      "fees": { "...": "..." },
      "polymarket": {
        "condition_id": "0xabc123...",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "slug": "will-trump-win-the-2024-election",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455"
      },
      "prices": {
        "yes_bid_price_usd": "0.49",
        "yes_ask_price_usd": "0.51",
        "no_bid_price_usd": "0.49",
        "no_ask_price_usd": "0.51"
      }
    }
  ]
}
```

#### `market.eligibility_changed`

`data` is an array of deltas. Each delta is `{ id, polymarket, ...only the changed fields }`. Changed fields may include any of: `accepting_new_positions`, `rejection_reason_code`, `sided_eligibility`, `max_notional_yes_usd(_pips)`, `max_notional_no_usd(_pips)`, `capacity_max_notional_yes_usd(_pips)`, `capacity_max_notional_no_usd(_pips)`, `slippage_max_notional_yes_usd(_pips)`, `slippage_max_notional_no_usd(_pips)`. It **never** includes `leverage`.

```json
{
  "id": "evt_mkt_def456",
  "type": "market.eligibility_changed",
  "created_at": "2025-06-01T10:01:00.000Z",
  "data": [
    {
      "id": "dm_mkt_abc123",
      "polymarket": {
        "condition_id": "0xabc123...",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "slug": "will-trump-win-the-2024-election",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455"
      },
      "accepting_new_positions": false,
      "rejection_reason_code": "QUOTE_TRADING_WINDOW_CLOSING"
    }
  ]
}
```

#### `market.max_leverage_changed`

`data` is an array of deltas. Each delta is `{ id, polymarket, leverage }`, where `leverage` carries the same fields as the `leverage` block in a full market object **except** the constants `min_bps` and `step_bps` — those never change, so read them from the market discovery payload. Only emitted for markets that are currently allowed/eligible.

```json
{
  "id": "evt_mkt_ghi789",
  "type": "market.max_leverage_changed",
  "created_at": "2025-06-01T10:02:00.000Z",
  "data": [
    {
      "id": "dm_mkt_abc123",
      "polymarket": {
        "condition_id": "0xabc123...",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "slug": "will-trump-win-the-2024-election",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455"
      },
      "leverage": {
        "max_bps": 50000,
        "max_yes_bps": 50000,
        "max_no_bps": 30000,
        "max_market_leverage_per_notional": {
          "yes": { "at100_usd_bps": 50000, "at500_usd_bps": 40000, "at1000_usd_bps": 30000, "at10000_usd_bps": 20000 },
          "no": { "at100_usd_bps": 30000, "at500_usd_bps": 25000, "at1000_usd_bps": 20000, "at10000_usd_bps": 15000 }
        }
      }
    }
  ]
}
```

### Connecting to market events

The connection flow is identical to the position gateways — only the namespace changes. Remember to iterate `data`:

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/user-markets", {
  auth: {token: userJwt},
});

socket.on("connect", () => {
  console.log("Connected to market events");
});

socket.on("market.discovered", (event) => {
  // Full market objects — add/refresh them in your local store
  for (const market of event.data) {
    upsertMarket(market);
  }
});

socket.on("market.eligibility_changed", (event) => {
  // Deltas — merge only the changed fields into the cached market by id
  for (const delta of event.data) {
    mergeMarketFields(delta.id, delta);
  }
});

socket.on("market.max_leverage_changed", (event) => {
  for (const delta of event.data) {
    mergeMarketFields(delta.id, {leverage: delta.leverage});
  }
});

socket.on("error", (error) => {
  console.error("WebSocket error:", error);
});
```

For the partner gateway, swap the namespace to `/v1/ws/prediction-markets/markets` and authenticate with your API key (`auth: {apiKey: process.env.DIMES_API_KEY}`) exactly as shown for the [partner position gateway](#partner-gateway).

***

## Testing your integration

Waiting for real positions to move through their lifecycle is a slow way to build a UI. `POST /sample-events` emits **one of every event type** onto your own WebSocket connection, one per second, with fixed mock payloads — so you can build and debug every branch of your event handling in under a minute.

|                  |                                             |
| ---------------- | ------------------------------------------- |
| **Endpoint**     | `POST /v1/prediction-markets/sample-events` |
| **Auth**         | Partner API key **or** user JWT             |
| **Rate limit**   | One request every 30 seconds                |
| **Availability** | All environments, including production      |

Which gateway receives the events depends on how you authenticate:

* **Partner API key** → the events land on the [partner positions gateway](#partner-gateway) (`/v1/ws/prediction-markets/positions`), scoped to your partner. You get every position event type.
* **User JWT** → the events land on the [user positions gateway](#user-gateway) (`/v1/ws/prediction-markets/user-positions`), scoped to the wallet in the token. You get every position event type **plus** one `notification` event for every notification code.

Only your own connection receives these events. Nobody else's clients see it.

**Nothing is created or persisted.** The payloads are canned mock data: every one carries the position ID `dm_pos_sampleevent000000` and the ticker `dimes-sample-market`, neither of which exists. Fetching them over REST will 404 — that is expected. Filter sample events out of production analytics by checking for that position ID if you need to.

### Connect first, then call

The events are fired at whatever is connected *at that moment*. Open your WebSocket, wait for it to authenticate, and only then call the endpoint.

```bash
# Partner scope
curl -X POST https://api.dimes.fi/v1/prediction-markets/sample-events \
  -H "Authorization: Api-Key $DIMES_API_KEY"

# User scope
curl -X POST https://api.dimes.fi/v1/prediction-markets/sample-events \
  -H "Authorization: Bearer $USER_JWT"
```

The call returns `202 Accepted` immediately with the size and pacing of what is about to arrive:

```json
{
  "event_count": 19,
  "interval_ms": 1000
}
```

The position events arrive first, then the notification events. Read `event_count` to know how many messages to expect and `interval_ms` for the spacing — do not hard-code either. New event types are added to the sample set as they are added to the protocol, so a handler that copes with everything listed under [Event types](#event-types) will not be surprised later.

### A worked example

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/user-positions", {
  auth: {token: userJwt},
});

socket.onAny((type, event) => {
  console.log(type, event.data.status ?? event.data.code);
});

socket.on("connect", async () => {
  await fetch("https://api.dimes.fi/v1/prediction-markets/sample-events", {
    method: "POST",
    headers: {authorization: `Bearer ${userJwt}`},
  });
});
```

### What the sample events do not cover

* **Market events.** `market.discovered`, `market.eligibility_changed` and `market.max_leverage_changed` are global broadcasts, so they are deliberately excluded. Sandbox produces real market events at a steady rate — use it to exercise that path.
* **Ordering and timing realism.** The endpoint walks the whole catalogue in a fixed order in one pass. Real positions emit a handful of these, with gaps of seconds to days between them, and occasionally out of order.
* **Error paths.** Authentication failures, disconnects and reconnection are not simulated. Test those by dropping the connection yourself.

***

### Best practices

**Deduplicate on event `id`.** Store processed event IDs and skip duplicates. The same event may be delivered more than once.

**Handle events out of order.** A `position.closed` event could arrive before `position.close_requested` due to network timing. Use the `created_at` timestamp and the position status from the REST API as the source of truth if ordering matters.

**Never assume you know every event type.** New event types are added over time and will start arriving on your existing connection without warning. Route on the `type` field with an explicit default branch that logs and ignores anything unrecognised — never one that throws or drops the connection.

**Treat `data.status` as the truth, not the event name.** Several events leave a position `open` (`position.force_unwound`). Re-render from the payload's `status` and `current` values rather than inferring state from which event fired.

**Reconnect on disconnect.** Configure your client's reconnection settings for production reliability.

**Use REST API as fallback.** If your WebSocket connection drops, use `GET /positions` to catch up on any missed state changes. Events are a latency optimisation over polling, not a durable log — there is no replay of missed events, so a reconnect should always be followed by a REST reconciliation.

**Build against `POST /sample-events` first.** Wire up every branch of your handler against the mock events before you place a single real position — see [Testing your integration](#testing-your-integration) above.


# Error Handling

All errors follow a consistent format. Every error response includes an error type, a machine-readable code, and a human-readable message.

### Error format

```json
{
  "error": {
    "type": "INVALID_REQUEST_ERROR",
    "code": "quote_leverage_below_minimum",
    "message": "Leverage is below the minimum allowed for this market."
  }
}
```

| Field     | Type   | Description                                                                                                                                                |
| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`    | string | Error category (see below)                                                                                                                                 |
| `code`    | string | Machine-readable error code (`snake_case`). Use this for programmatic handling                                                                             |
| `message` | string | Human-readable description of the error. Suitable for displaying to end users. **Don't parse this** — values are duplicated as typed fields under `params` |
| `params`  | object | Optional. Structured hint values for codes that carry actionable data. See [Errors with hints](#errors-with-structured-hints)                              |

***

### Error types

| Type                    | HTTP Statuses | Description                                                                |
| ----------------------- | ------------- | -------------------------------------------------------------------------- |
| `INVALID_REQUEST_ERROR` | 400, 404, 409 | The request was malformed, missing parameters, or violated a business rule |
| `AUTHENTICATION_ERROR`  | 401, 403      | Missing or invalid API key / JWT                                           |
| `RATE_LIMIT_ERROR`      | 429           | Too many requests                                                          |
| `API_ERROR`             | 500+          | Something went wrong on our side                                           |

***

### Error codes by endpoint

#### `POST /tokens`

| Code                                   | Status | When                                                |
| -------------------------------------- | ------ | --------------------------------------------------- |
| `customer_auth_invalid_wallet_address` | 400    | Wallet address is not a valid EVM or Solana address |

#### `GET /markets/{ticker}`

| Code                        | Status | When                       |
| --------------------------- | ------ | -------------------------- |
| `customer_market_not_found` | 404    | No market with that ticker |

#### `GET /markets/{ticker}/positions`

| Code                        | Status | When                       |
| --------------------------- | ------ | -------------------------- |
| `customer_market_not_found` | 404    | No market with that ticker |

#### `GET /events/{event_ticker}/markets` and `GET /events/{event_ticker}/positions`

| Code                       | Status | When                            |
| -------------------------- | ------ | ------------------------------- |
| `customer_event_not_found` | 404    | No event with that event ticker |

#### `GET /series/{series_ticker}/markets` and `GET /series/{series_ticker}/positions`

| Code                        | Status | When                              |
| --------------------------- | ------ | --------------------------------- |
| `customer_series_not_found` | 404    | No series with that series ticker |

#### `GET /positions/{position_id}`

| Code                          | Status | When                                   |
| ----------------------------- | ------ | -------------------------------------- |
| `customer_position_not_found` | 404    | No position with that ID for this user |

#### `GET /positions/{position_id}/transactions`

| Code                                       | Status | When                                   |
| ------------------------------------------ | ------ | -------------------------------------- |
| `customer_position_transactions_not_found` | 404    | No position with that ID for this user |

#### `POST /quotes`

**Market eligibility:**

| Code                                           | Status | When                                                                           |
| ---------------------------------------------- | ------ | ------------------------------------------------------------------------------ |
| `quote_market_not_eligible`                    | 400    | Market exists but isn't eligible for leverage                                  |
| `quote_market_not_ready`                       | 400    | Market is not yet ready for trading                                            |
| `quote_market_not_active`                      | 400    | Market is not active                                                           |
| `quote_market_risk_too_high`                   | 400    | Market risk is too high right now — try again later                            |
| `quote_market_unsupported_category`            | 400    | This market category is not supported                                          |
| `quote_market_unsupported_crypto_asset`        | 400    | Crypto market whose underlying asset has no supported price feed               |
| `quote_event_not_started`                      | 400    | Event has not started yet — trading opens at the scheduled start time          |
| `quote_trading_window_closing`                 | 400    | Market is too close to its hard-exit deadline to open a new position           |
| `risk_engine_market_closed`                    | 400    | Market is no longer accepting new positions                                    |
| `risk_engine_market_disabled`                  | 400    | Market has been disabled (see `params.reason`)                                 |
| `risk_engine_market_settlement_detected`       | 400    | Market settlement has been detected                                            |
| `circuit_breaker_price_divergence_tripped`     | 400    | Price divergence circuit breaker has been triggered                            |
| `quote_polymarket_market_closed`               | 400    | Polymarket market is closed                                                    |
| `quote_polymarket_market_inactive`             | 400    | Polymarket market is inactive                                                  |
| `quote_polymarket_market_not_accepting_orders` | 400    | Polymarket market is not accepting orders                                      |
| `quote_polymarket_missing_token`               | 400    | Polymarket market is missing token configuration                               |
| `quote_market_missing_polymarket_condition_id` | 400    | Market is missing required Polymarket condition ID                             |
| `quote_creation_disabled`                      | 503    | Quote creation is temporarily disabled by a server kill switch — retry shortly |
| `quote_liquidation_not_viable`                 | 400    | Liquidation price would be too low to maintain a position                      |

**Leverage validation:**

| Code                                      | Status | When                                                     |
| ----------------------------------------- | ------ | -------------------------------------------------------- |
| `quote_leverage_below_minimum`            | 400    | Leverage is below the minimum (2x)                       |
| `quote_leverage_exceeds_maximum`          | 400    | Leverage exceeds the absolute maximum                    |
| `quote_leverage_exceeds_model_max`        | 400    | Leverage exceeds the model's maximum for this market     |
| `quote_leverage_exceeds_collateral_floor` | 400    | Implied collateral is below the minimum at this leverage |
| `quote_notional_below_minimum`            | 400    | Notional is below the minimum for this account           |
| `quote_leverage_too_high_for_price`       | 400    | Leverage is too high relative to the current entry price |
| `quote_price_too_low`                     | 400    | Entry price is too low for a leveraged position          |

**Market quality filters:**

| Code                                               | Status | When                                                                             |
| -------------------------------------------------- | ------ | -------------------------------------------------------------------------------- |
| `quote_entry_depth_too_low`                        | 400    | Not enough order book depth                                                      |
| `quote_entry_bid_depth_too_low`                    | 400    | Minimum bid depth below threshold                                                |
| `quote_entry_book_one_sided`                       | 400    | Order book is missing or one-sided — market isn't tradable now                   |
| `quote_entry_price_out_of_range`                   | 400    | Current market price is outside tradeable range                                  |
| `quote_entry_spread_too_wide`                      | 400    | Market spread exceeds maximum                                                    |
| `quote_entry_order_book_stale`                     | 400    | Order book data is stale                                                         |
| `quote_entry_price_stale`                          | 400    | Price data is stale                                                              |
| `quote_entry_crypto_price_stale`                   | 400    | Crypto price data is stale                                                       |
| `quote_entry_sport_data_stale`                     | 400    | Sport event data is stale                                                        |
| `quote_entry_excluded_market_type`                 | 400    | Market type is excluded from trading (see `params.market_type`)                  |
| `quote_entry_excluded_sport`                       | 400    | Sport type is excluded from trading (see `params.sport`)                         |
| `quote_entry_market_too_elapsed`                   | 400    | Market is too close to expiry                                                    |
| `quote_entry_top_holder_too_high`                  | 400    | Top holder concentration exceeds maximum                                         |
| `quote_entry_volume_too_low`                       | 400    | 24h trading volume is below minimum                                              |
| `quote_twap_data_unavailable`                      | 400    | TWAP price data is unavailable for this market                                   |
| `quote_twap_data_stale`                            | 400    | TWAP price data is stale                                                         |
| `notional_selector_insufficient_liquidity`         | 400    | The requested size exceeds the liquidity fillable within slippage; retry smaller |
| `notional_selector_below_min_notional`             | 400    | The order book is too thin to fill even the minimum position size on this side   |
| `notional_selector_empty_order_book`               | 400    | One side of the order book is currently empty                                    |
| `notional_selector_pregame_insufficient_liquidity` | 400    | A pre-game market does not yet have enough estimated live liquidity to open      |
| `quote_side_capacity_exceeded`                     | 400    | Notional exceeds available capacity on this side of the market                   |

**Position limits:**

| Code                                    | Status | When                                                    |
| --------------------------------------- | ------ | ------------------------------------------------------- |
| `quote_user_position_limit_exceeded`    | 400    | User has reached their maximum number of open positions |
| `quote_market_position_limit_exceeded`  | 400    | Market-level position limit reached                     |
| `quote_side_position_limit_exceeded`    | 400    | Too many positions on one side of this market           |
| `quote_global_position_limit_exceeded`  | 400    | Platform-wide position limit reached                    |
| `quote_partner_position_limit_exceeded` | 400    | Partner's aggregate exposure cap reached                |

**Borrowing limits:**

These cap the protocol-borrowed capital (notional minus your collateral), separately from the position-count limits above. A quote is rejected when the new position's borrow plus the existing outstanding borrow would exceed a cap.

| Code                            | Status | When                                                                    |
| ------------------------------- | ------ | ----------------------------------------------------------------------- |
| `quote_user_capital_exceeded`   | 400    | Your wallet's total borrowed capital would exceed the per-user limit    |
| `quote_market_capital_exceeded` | 400    | The market's total borrowed capital would exceed the per-market limit   |
| `quote_side_capital_exceeded`   | 400    | Borrowed capital on this side of the market would exceed the side limit |
| `quote_total_capital_exceeded`  | 400    | The protocol-wide borrowed capital limit would be exceeded              |

**Partial-open (FAK) validation:**

| Code                              | Status | When                                                                                                                                                                                                                                                                                                                                                                                                      |
| --------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quote_min_fill_bps_requires_fak` | 400    | `min_fill_bps` sent without `allow_partial_fill: true`. `min_fill_bps` is only valid when partial-open is enabled. See [Partial-Open (FAK)](/for-developers/order-types/partial-open)                                                                                                                                                                                                                     |
| `quote_min_fill_bps_out_of_range` | 400    | `min_fill_bps` outside `2000`–`5000`. Params: `min_fill_bps`, `absolute_min_bps`, `max_bps`                                                                                                                                                                                                                                                                                                               |
| `quote_min_fill_bps_step_invalid` | 400    | `min_fill_bps` not divisible by `500`. Params: `min_fill_bps`, `step_bps`                                                                                                                                                                                                                                                                                                                                 |
| `quote_min_fill_bps_below_floor`  | 400    | `min_fill_bps` would shrink the post-partial position below the minimum collateral or minimum notional for this size. Params: `min_fill_bps` (requested), `floor_min_fill_bps` (smallest accepted — raise `min_fill_bps` to at least this, or increase size), `bound_by` (`"collateral"` or `"notional"` — which minimum bound the floor), `requested_collateral_usd_pips`, `requested_notional_usd_pips` |

When partial-open is disabled server-side, quote creation is rejected with `quote_creation_disabled` (503) — the same kill switch that gates all quote creation. Omit `allow_partial_fill` to place an atomic order.

**Other:**

| Code                                      | Status | When                                       |
| ----------------------------------------- | ------ | ------------------------------------------ |
| `quote_slippage_too_high`                 | 400    | Slippage tolerance exceeds maximum allowed |
| `quote_invalid_polymarket_wallet_address` | 400    | Polymarket requires a valid EVM address    |

***

### Errors with structured hints

Some error codes return additional machine-readable fields under `error.params` so you can act on them programmatically — for example, pre-filling an input with the maximum supported size instead of asking the user to retry until something fits. **Don't regex-parse the `message` string** — the values you need are typed under `params`.

`params` is only present on the codes listed below. Dispatch on `code` first, then read the keys you expect. Most codes here are raised by the quoting (`POST /quotes`) path.

**Units & types:**

| Suffix          | Type   | Meaning                                                                   |
| --------------- | ------ | ------------------------------------------------------------------------- |
| `*_usd_pips`    | string | Decimal string, 10,000 pips = $1.00                                       |
| `*_usdc_units`  | string | Decimal string, 1,000,000 units = $1.00 (capital/borrow limits use these) |
| `*_bps`         | number | Basis points (10,000 bps = 1x leverage / 100%)                            |
| `*_pct_elapsed` | number | Fraction in `0`–`1`                                                       |
| `*_age_ms`      | number | Milliseconds                                                              |

Values are strings unless the unit table above marks them as numbers (`side`, `market_type`, `sport`, `market_id`, and `reason` are also strings). Treat capacity, depth, and price values as a snapshot — the order book shifts continuously, so a user who waits before retrying may still hit the same error with different numbers.

| `code`                                             | `params` keys                                                                                                                                                                           |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `notional_selector_below_min_notional`             | `max_supported_collateral_usd_pips`, `min_notional_usd_pips`, `slippage_max_usd_pips` ¹                                                                                                 |
| `notional_selector_insufficient_liquidity`         | `max_supported_collateral_usd_pips`, `min_notional_usd_pips`, `slippage_max_usd_pips` ¹                                                                                                 |
| `notional_selector_pregame_insufficient_liquidity` | `max_supported_collateral_usd_pips`, `min_notional_usd_pips`, `slippage_max_usd_pips` ¹                                                                                                 |
| `quote_entry_bid_depth_too_low`                    | `current_bid_depth_usd_pips`, `threshold_usd_pips`                                                                                                                                      |
| `quote_entry_crypto_price_stale`                   | `last_update_age_ms`, `market_id`                                                                                                                                                       |
| `quote_entry_depth_too_low`                        | `current_depth_usd_pips`, `min_depth_usd_pips`                                                                                                                                          |
| `quote_entry_excluded_market_type`                 | `market_type`                                                                                                                                                                           |
| `quote_entry_excluded_sport`                       | `sport`                                                                                                                                                                                 |
| `quote_entry_market_too_elapsed`                   | `max_pct_elapsed`, `pct_elapsed`                                                                                                                                                        |
| `quote_entry_order_book_stale`                     | `last_update_age_ms`, `market_id`                                                                                                                                                       |
| `quote_entry_price_out_of_range`                   | `current_price_usd_pips`, `max_price_usd_pips`, `min_price_usd_pips`                                                                                                                    |
| `quote_entry_price_stale`                          | `last_update_age_ms`, `market_id`                                                                                                                                                       |
| `quote_entry_sport_data_stale`                     | `last_update_age_ms`, `market_id`                                                                                                                                                       |
| `quote_entry_spread_too_wide`                      | `current_spread_usd_pips`, `max_spread_usd_pips`                                                                                                                                        |
| `quote_entry_volume_too_low`                       | `current_volume24h_usd_pips`, `min_volume24h_usd_pips`                                                                                                                                  |
| `quote_global_position_limit_exceeded`             | `available_capacity_usd_pips`, `current_notional_usd_pips`, `limit_usd_pips` ²                                                                                                          |
| `quote_leverage_below_minimum`                     | `current_book_leverage_bps`, `min_leverage_bps`                                                                                                                                         |
| `quote_leverage_exceeds_collateral_floor`          | `current_book_leverage_bps`, `current_collateral_usd_pips`, `max_leverage_bps`, `min_collateral_usd_pips` ³                                                                             |
| `quote_leverage_exceeds_maximum`                   | `current_book_leverage_bps`, `max_leverage_bps`                                                                                                                                         |
| `quote_leverage_exceeds_model_max`                 | `current_book_leverage_bps`, `max_leverage_bps`                                                                                                                                         |
| `quote_leverage_too_high_for_price`                | `current_book_leverage_bps`, `entry_price_usd_pips`, `max_acceptable_leverage_bps`, `minimum_buffer_usd_pips`                                                                           |
| `quote_liquidation_not_viable`                     | `entry_price_usd_pips`, `liquidation_price_usd_pips`, `min_tolerance_pct_bps`, `side`                                                                                                   |
| `quote_market_capital_exceeded`                    | `available_borrowed_usdc_units`, `available_collateral_usdc_units`, `available_notional_usdc_units`, `cap_usdc_units`, `current_borrowed_usdc_units`, `requested_borrowed_usdc_units` ⁴ |
| `quote_market_position_limit_exceeded`             | `available_capacity_usd_pips`, `current_notional_usd_pips`, `limit_usd_pips` ²                                                                                                          |
| `quote_max_leverage_too_low`                       | `max_leverage_bps`, `min_leverage_bps`                                                                                                                                                  |
| `quote_notional_below_minimum`                     | `min_notional_usd_pips`, `requested_notional_usd_pips`                                                                                                                                  |
| `quote_partner_position_limit_exceeded`            | `active_notional_usd_pips`, `available_capacity_usd_pips`, `current_notional_usd_pips`, `in_flight_notional_usd_pips`, `limit_usd_pips` ² ⁵                                             |
| `quote_side_capacity_exceeded`                     | `available_capacity_usd_pips`, `max_supported_collateral_usd_pips`, `min_notional_usd_pips` ⁶                                                                                           |
| `quote_side_capital_exceeded`                      | `available_borrowed_usdc_units`, `available_collateral_usdc_units`, `available_notional_usdc_units`, `cap_usdc_units`, `current_borrowed_usdc_units`, `requested_borrowed_usdc_units` ⁴ |
| `quote_side_position_limit_exceeded`               | `available_capacity_usd_pips`, `current_notional_usd_pips`, `limit_usd_pips` ²                                                                                                          |
| `quote_slippage_too_high`                          | `current_slippage_bps`, `max_slippage_bps`                                                                                                                                              |
| `quote_total_capital_exceeded`                     | `available_borrowed_usdc_units`, `available_collateral_usdc_units`, `available_notional_usdc_units`, `cap_usdc_units`, `current_borrowed_usdc_units`, `requested_borrowed_usdc_units` ⁴ |
| `quote_user_capital_exceeded`                      | `available_borrowed_usdc_units`, `available_collateral_usdc_units`, `available_notional_usdc_units`, `cap_usdc_units`, `current_borrowed_usdc_units`, `requested_borrowed_usdc_units` ⁴ |
| `quote_user_position_limit_exceeded`               | `available_capacity_usd_pips`, `current_notional_usd_pips`, `limit_usd_pips` ²                                                                                                          |
| `risk_engine_market_disabled`                      | `reason` ⁷                                                                                                                                                                              |

¹ The three `notional_selector_*` liquidity codes above all carry these `params`. `notional_selector_empty_order_book` is raised before any sizing is computed (one side of the book is empty) and carries no `params`.

² The `*_position_limit_exceeded` codes share this shape. `available_capacity_usd_pips` is `limit − current`, clamped at

1.

³ There is no escape via lowering leverage — raise collateral to at least `min_collateral_usd_pips`.

⁴ The four `*_capital_exceeded` codes share this shape and cap protocol-borrowed capital (`notional − collateral`). `requested_borrowed_usdc_units` is the borrow this quote would add; `available_borrowed_usdc_units` is `cap − current`, clamped at 0. `available_collateral_usdc_units` and `available_notional_usdc_units` re-express that same borrow headroom as the collateral and notional you could still deploy **at the leverage this quote requested** — surface these directly in the UI instead of converting `available_borrowed_usdc_units` yourself. Both are present only when the requested leverage is above 1× (a 1× position borrows nothing, so there is no borrow headroom to convert).

⁵ Same base shape as the other position-limit codes, plus `active_notional_usd_pips` and `in_flight_notional_usd_pips` (the cap counts open positions + in-flight quotes).

⁶ `max_supported_collateral_usd_pips` is **the value to surface in the UI** — the maximum collateral the user can submit at their currently selected leverage and still fit. It maps directly to the collateral input.

⁷ `params` is only present when a disable `reason` is set. Current reason values: `force_disabled` (admin killswitch).

Example (`quote_leverage_exceeds_model_max` — the most common rejection):

```json
{
  "error": {
    "type": "INVALID_REQUEST_ERROR",
    "code": "quote_leverage_exceeds_model_max",
    "message": "Requested leverage 8x exceeds the maximum 5x currently allowed for this market",
    "params": {
      "max_leverage_bps": 50000,
      "current_book_leverage_bps": 80000
    }
  }
}
```

| Param                       | Meaning                                                                                                                                   |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `max_leverage_bps`          | **The accepted leverage** — the highest the risk model allows on this market right now (50000 = 5×). Clamp the input to this and re-quote |
| `current_book_leverage_bps` | The leverage that was requested and rejected (80000 = 8×)                                                                                 |

Snap the leverage slider to `max_leverage_bps` (rounded down to the market's `leverage.step_bps`) rather than asking the user to guess. The cap moves with market conditions, so re-read it from every rejection instead of caching it.

Example (`quote_user_capital_exceeded`, quote requested 2× leverage):

```json
{
  "error": {
    "type": "INVALID_REQUEST_ERROR",
    "code": "quote_user_capital_exceeded",
    "message": "User borrowed capital 9000000 + requested 2000000 exceeds per-user cap 10000000 (USDC units)",
    "params": {
      "available_borrowed_usdc_units": "1000000",
      "available_collateral_usdc_units": "1000000",
      "available_notional_usdc_units": "2000000",
      "cap_usdc_units": "10000000",
      "current_borrowed_usdc_units": "9000000",
      "requested_borrowed_usdc_units": "2000000"
    }
  }
}
```

#### Rate limiting (429)

```json
{
  "error": {
    "type": "RATE_LIMIT_ERROR",
    "code": "too_many_requests",
    "message": "Too Many Requests"
  }
}
```

All endpoints are subject to rate limiting. Throttled responses include a `Retry-After` header indicating how many seconds to wait before retrying. Use this value instead of a fixed backoff when available.

#### Internal server error (500)

```json
{
  "error": {
    "type": "API_ERROR",
    "code": "internal_server_error",
    "message": "Something went wrong on our side. Please try again."
  }
}
```

A 500 response indicates an unexpected error on our side. These are always safe to retry with exponential backoff.

***

### Handling errors in practice

#### Check the error type and code

{% tabs %}
{% tab title="REST API" %}

```javascript
const response = await fetch(
  `${BASE_URL}/prediction-markets/quotes`,
  {
    method: "POST",
    headers: userHeaders,
    body: JSON.stringify(quoteRequest),
  }
);

if (!response.ok) {
  const {error} = await response.json();

  switch (error.code) {
    case "risk_engine_market_closed":
    case "risk_engine_market_disabled":
    case "quote_market_not_eligible":
    case "quote_entry_excluded_market_type":
    case "quote_entry_excluded_sport":
    case "quote_event_not_started":
      // Disable the trade button, show market unavailable
      // For excluded_market_type/excluded_sport, error.params
      // contains the specific market_type or sport
      break;
    case "quote_leverage_below_minimum":
    case "quote_leverage_exceeds_model_max":
      // Adjust leverage input to valid range
      break;
    case "quote_leverage_exceeds_collateral_floor":
      // Implied collateral below the floor. Raise collateral
      // to error.params.min_collateral_usd_pips (NOT lower leverage —
      // there is no escape via leverage adjustment).
      break;
    case "quote_user_position_limit_exceeded":
    case "quote_partner_position_limit_exceeded":
      // Show limit reached message
      break;
    case "notional_selector_insufficient_liquidity":
    case "quote_entry_depth_too_low":
      // Suggest reducing position size (retry at or below
      // error.params.max_supported_collateral_usd_pips)
      break;
    case "notional_selector_below_min_notional":
    case "notional_selector_empty_order_book":
    case "notional_selector_pregame_insufficient_liquidity":
      // Terminal: the book can't support even the minimum size on
      // this side right now. Don't auto-retry smaller — show the
      // message and let the user pick another market/side or wait.
      break;
    case "request_already_in_progress":
      // A previous request is still processing — wait and retry
      break;
    default:
      // Generic error state
      break;
  }
}
```

{% endtab %}

{% tab title="SDK" %}

```typescript
import {DimesApiError, formatErrorMessage} from "@dimes-dot-fi/sdk";
import {quoteErrorHint, hintAdjustment} from "@dimes-dot-fi/sdk";

try {
  const result = await executeQuote(client, params);
} catch (err) {
  if (err instanceof DimesApiError) {
    // Friendly message for any error code
    console.log(formatErrorMessage(err.code, err.params));

    // Auto-correction hints for leverage/collateral/slippage errors
    const hint = quoteErrorHint(err.code, err.params, {
      leverageBps: params.leverageBps,
    });
    const adj = hintAdjustment(hint, {
      collateralUsd: params.collateralUsd,
      leverageBps: params.leverageBps,
      slippageBps: params.slippageBps,
    });
    if (adj) {
      console.log(`Adjust ${adj.field} to ${adj.toLabel}`);
    }
  }
}
```

{% endtab %}
{% endtabs %}

#### Retry logic

Some errors are retryable, others aren't.

| Retryable                     | Not retryable                          |
| ----------------------------- | -------------------------------------- |
| `internal_server_error`       | `quote_market_not_eligible`            |
| `too_many_requests`           | `quote_leverage_below_minimum`         |
| `request_already_in_progress` | `quote_user_position_limit_exceeded`   |
|                               | `risk_engine_market_closed`            |
|                               | `risk_engine_market_disabled`          |
|                               | `quote_entry_excluded_market_type`     |
|                               | `quote_entry_excluded_sport`           |
|                               | `quote_event_not_started`              |
|                               | `customer_auth_invalid_wallet_address` |

For retryable errors, use exponential backoff starting at 1 second.

> **SDK note:** The SDK's `HttpClient` handles 429 retries (with `Retry-After` backoff) and 401 token refresh automatically, so you only need to handle business-logic errors in your application code.


# Order Types

Partial open order behavior

Order behaviors beyond a full open and close, including partial fills.

* [Partial-Open (FAK)](/for-developers/order-types/partial-open)


# Partial-Open (FAK)

By default, a quote opens **atomically**: the order either fills its full requested notional in a single fill-or-kill (FOK) match, or it reverts and the user is fully refunded — no position is created. Thin markets often can't absorb the full size in one match, so atomic opens on them fail more often than they need to.

**Partial-open** lets the user accept a smaller fill. Set `allow_partial_fill: true` on the quote and the position opens at the size that actually filled, down to an optional `min_fill_bps` floor.

## Enablement

Partial-open is **on by default** — there is no feature flag to turn on. It is gated only by the same server-side kill switch that gates all quote creation. If that switch is active, the request is rejected with `quote_creation_disabled` (HTTP 503); fall back to an atomic order (omit `allow_partial_fill`) — atomic quotes are blocked by the same switch, so retry shortly.

## Request fields

Both fields live on the quote and draft-quote request body:

| Field                | Type    | Required | Notes                                                                                 |
| -------------------- | ------- | -------- | ------------------------------------------------------------------------------------- |
| `allow_partial_fill` | boolean | no       | `false` (default) = atomic. `true` opts into partial-open.                            |
| `min_fill_bps`       | integer | no       | Minimum acceptable fill, in basis points. Only valid with `allow_partial_fill: true`. |

`min_fill_bps` rules (all enforced at quote creation, before any order is placed):

* Range **2000–5000** (20%–50%).
* Multiple of **500** (5% steps).
* At least the **floor cap** (see below).

Omit both fields for normal atomic behaviour — nothing changes.

## The `min_fill_bps` floor cap

A partial fill must never open a position below the protocol minimums. Because `min_fill_bps` is the *worst-case* fraction that can fill, the backend rejects any value that could breach a minimum. The floor is computed purely from the request — no market data, no fill required:

```
floor_min_fill_bps = max(
  2000,
  ceil(MIN_COLLATERAL × 10000 / requested_collateral),   // collateral bound
  ceil(MIN_NOTIONAL   × 10000 / requested_notional)       // notional bound
)
```

If `min_fill_bps < floor_min_fill_bps`, the request is rejected with `quote_min_fill_bps_below_floor` (see [Errors](#errors)). The collateral bound dominates at high leverage; the **notional bound dominates at low leverage** (below \~4x), where a small fraction of a small notional would otherwise fall under the minimum order size. When the floor exceeds the 5000 (50%) maximum, no partial fill is viable and the order must be atomic.

## Lifecycle

1. **Floor leg.** The backend posts an FOK sized at the requested floor (`floor_token_units = floor(target_token_units × min_fill_bps / 10000)`). If it never fills within the retry budget, the position is fully reverted — all collateral, origination fee, and prepaid venue fee are refunded — and the user gateway emits `PARTIAL_OPEN_FLOOR_MISSED`.
2. **Chase the remainder.** Once the floor fills, the backend posts up to 3 fill-and-kill (FAK) retries (5s apart) for the unfilled remainder. Each fulfilled retry emits `PARTIAL_OPEN_PROGRESS` with the cumulative fill amount.
3. **Finalize.** When the retry budget is exhausted (or cumulative fill reaches the full target), `finalizeOpenPartial` shrinks the on-chain position to the actually-filled size while preserving the originally signed leverage. Unused collateral, borrowed amount, and venue fee are refunded proportionally.

## WebSocket notifications

Subscribe to the user gateway `notification` event (see [WebSocket Events](/for-developers/api-and-events/websocket#notification-events-user-gateway-only)):

| Code                        | When                                                                                                                                      |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `PARTIAL_OPEN_PROGRESS`     | After each fill (floor + each retry). Payload: `cumulative_filled_making_amount`, `target_making_amount`, `filled_bps`, `attempt_number`. |
| `PARTIAL_OPEN_FLOOR_MISSED` | The floor FOK never filled; the position was reverted and fully refunded.                                                                 |

Use `PARTIAL_OPEN_PROGRESS` to drive a "filling… X%" indicator; treat `PARTIAL_OPEN_FLOOR_MISSED` as a terminal failure (order didn't fill, user refunded).

## Position display

The position shrinks to the actual fill, but the requested size is preserved separately:

* `origination_*` fields = the **full requested** size the user signed.
* `current_*` fields = the **actual filled** size the position opened at.

A 60% fill of a $60 request opens a \~$36 position. Render `current_*` as the live position; optionally show requested vs filled. Leverage is preserved across the shrink. The quote and position responses also echo `allow_partial_fill` and `min_fill_bps`.

## Errors

All partial-open validation happens at quote creation, before any order is placed:

| Code                              | Status | When                                                                                                                                                                                                         |
| --------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `quote_min_fill_bps_requires_fak` | 400    | `min_fill_bps` sent without `allow_partial_fill: true`.                                                                                                                                                      |
| `quote_min_fill_bps_out_of_range` | 400    | `min_fill_bps` outside `2000`–`5000`. Params: `min_fill_bps`, `absolute_min_bps`, `max_bps`.                                                                                                                 |
| `quote_min_fill_bps_step_invalid` | 400    | `min_fill_bps` not divisible by `500`. Params: `min_fill_bps`, `step_bps`.                                                                                                                                   |
| `quote_min_fill_bps_below_floor`  | 400    | `min_fill_bps` would shrink the position below the minimum collateral or notional. Params: `min_fill_bps`, `floor_min_fill_bps`, `bound_by`, `requested_collateral_usd_pips`, `requested_notional_usd_pips`. |
| `quote_creation_disabled`         | 503    | Quote creation (including partial-open) temporarily disabled by a server kill switch. Retry shortly.                                                                                                         |

See [Error Handling](/for-developers/api-and-events/error-handling#post-quotes) for the full quote-rejection table.


# Design & Branding

UI requirements and Dimes brand assets for partners

Frontend requirements for displaying leveraged positions and the "Leverage by Dimes" attribution, plus downloadable brand assets.

* [UI Guidelines](/for-developers/design-and-branding/ui-guidelines)
* [Media](/for-developers/design-and-branding/media)


# UI Guidelines

Reference implementations for the key interface elements in a Multiply integration. Each component below includes a visual reference and the corresponding API fields mapped.

All values are rendered from API responses. Partner front-ends never compute leverage, margin, P\&L, or liquidation values — they display what Dimes returns.

***

### 1. Leverage Slider

The leverage input for position creation. Respects the market's `leverage.max_bps` and `leverage.step_bps`, and updates the quote in real-time as the user adjusts.

> **This component is optional.** Partners may implement their own input mechanism (dropdown, stepper, buttons, etc.). The only requirement is that the selected leverage value is passed to the `/quotes` endpoint as `leverage_bps`.

#### Closed view

Just the slider rail with dot stops, "Leverage" label, and the current multiplier. Hovering the "?" shows a tooltip: *" Leverage auto-decays as resolution approaches."*

<figure><img src="/files/CHuOdn8R5YX41KnR5wqT" alt=""><figcaption></figcaption></figure>

#### Open view

When the user drags above 1x, the bottom section expands to show Entry Price, Shares, Position Value, and Liquidation Price. These values come from the quote response.

<figure><img src="/files/Yzk80YeEsPTGguWf7iJk" alt=""><figcaption></figcaption></figure>

**API fields used:** `leverage.max_bps` and `leverage.step_bps` (slider configuration), full quote response for expanded details.

#### Minimal code to integrate leverage without the slider

{% tabs %}
{% tab title="REST API" %}

```javascript
// Capture leverage from any input you prefer
const leverageBps = 50000; // 5x

// Pass to quote endpoint — this is the only requirement
const quote = await fetch(`${BASE_URL}/prediction-markets/quotes`, {
  method: "POST",
  headers: userHeaders,
  body: JSON.stringify({
    market_ticker: "will-btc-hit-100k-2025",
    effective_side: "yes",
    leverage_bps: leverageBps,
    notional_amount_usd_pips: "250000000",
    slippage_bps: 300,
  }),
}).then(res => res.json());

// Display from the quote response:
// quote.notional_amount_usd → position size
// quote.liquidation_price_usd → liquidation price
// quote.origination_fee_usd → combined origination fee
// quote.protocol_origination_fee_usd → protocol component
// quote.partner_origination_fee_usd → partner component (hide if 0 bps)
// quote.polymarket_trading_fee_bps → venue trading fee rate (0 on Kalshi)
// quote.total_user_amount_usd → total cost to user
```

{% endtab %}

{% tab title="React Hook" %}

```tsx
import {useMarket} from "@dimes-dot-fi/sdk/react";

function LeverageSlider({ticker}: { ticker: string }) {
  const {data: market} = useMarket(ticker);
  const leverage = market?.leverage;
  // Use leverage.minBps, leverage.maxBps, leverage.stepBps
  // to configure your slider
}
```

{% endtab %}
{% endtabs %}

***

### 1b. Quote Flow — Draft then Promote

Quotes (`POST /quotes`) have a short validity window — read `signature_expiry` from the response, don't hardcode a duration. It's enough time to submit a transaction but too short for a user to review numbers, decide, and open their wallet. The recommended UX uses a two-step flow:

1. **Get a draft** — call `POST /draft-quotes` with the same body as `/quotes`. Display the response (fees, entry price, liquidation price, total cost) and let the user review at their pace. Draft quotes have no expiry.
2. **Promote on click** — when the user clicks "Create Position", call `POST /promoted-quotes/{draft_quote_id}`. This returns a fully signed quote with the same parameters. Immediately open the wallet for signing — no need to re-display the numbers.
3. **Countdown during signing** — start the countdown only after the promoted quote arrives, while the wallet is open. Read the deadline from the `signature_expiry` field rather than assuming a fixed duration.

**Detecting a draft vs real quote:** draft quotes return `signature_expiry: "0"` and `contract_signature: ""`. Use this to show a static "Quote ready" state instead of a countdown.

**Error handling:** when a draft or promotion is rejected, read the `params` on the error and move the user back into a valid state rather than showing a dead end — clamp the offending input to the returned value, re-quote, and show what changed. See [Error Handling — structured hints](/for-developers/api-and-events/error-handling#errors-with-structured-hints) and [Integration Best Practices](/for-developers/integration-best-practices). If the market simply moved between draft and promotion, re-fetch the draft to refresh the numbers.

API details: [API Reference — Promote draft quote](/for-developers/api-and-events/api-reference#promote-draft-quote).

{% tabs %}
{% tab title="REST API" %}
Use the draft-then-promote endpoints described above (`POST /draft-quotes` followed by `POST /promoted-quotes/{id}`).
{% endtab %}

{% tab title="React Hook" %}

```tsx
import {useQuote} from "@dimes-dot-fi/sdk/react";

function QuoteFlow({marketTicker}: { marketTicker: string }) {
  const {state, execute, reset} = useQuote();

  // state.phase: "idle" | "loading-draft" | "draft-ready" | "promoting" | "promoted" | "market-moved" | "error"
  // state.draft: the draft quote (when phase is "draft-ready" or later)
  // state.quote: the promoted quote (when phase is "promoted")
}
```

{% endtab %}
{% endtabs %}

***

### 2. Position Card

The core component for monitoring open positions. Displays real-time position state with a margin health indicator.

#### Open position

<figure><img src="/files/XBItV0PNFWvTHNi7x2nf" alt=""><figcaption></figcaption></figure>

**Required elements:**

* Margin buffer in dollars (`risk.margin_buffer_usd`)
* Health ring driven by `risk.health_pct` — green above 60%, amber 30–60%, red below 30%
* Effective side (Yes / No), Entry Price, Liquidation Price
* P\&L with ROE percentage, color-coded green/red
* Starting leverage and effective leverage
* Time to resolution countdown
* Close Position action

{% tabs %}
{% tab title="REST API" %}
Use `GET /prediction-markets/positions?status=open` and the field mapping below.
{% endtab %}

{% tab title="React Hook" %}

```tsx
import {usePositions} from "@dimes-dot-fi/sdk/react";
import {isOpenPosition} from "@dimes-dot-fi/sdk";

function PositionCards() {
  const {data: positions} = usePositions({status: "open"});

  return positions?.map((p) => {
    if (isOpenPosition(p)) {
      // p.current.unrealizedPnlUsd, p.risk.currentLiquidationPriceUsd, etc.
    }
  });
}
```

{% endtab %}
{% endtabs %}

**API fields used:**

| UI element         | API field                                                  |
| ------------------ | ---------------------------------------------------------- |
| Margin buffer      | `risk.margin_buffer_usd`                                   |
| Liquidation buffer | `risk.liquidation_buffer_bps`                              |
| Health ring        | `risk.health_pct`                                          |
| P\&L               | `current.unrealized_pnl_usd`, `current.unrealized_pnl_bps` |
| Mark price         | `current.mark_price_usd`                                   |
| Entry leverage     | `entry.leverage_bps`                                       |
| Effective leverage | `effective_leverage_bps`                                   |
| Fees accrued       | `fees.accrued_lifetime_fee_usd`                            |
| Time to resolution | `timing.time_to_close_minutes`                             |

#### Settled position

When a position closes, the card transitions to a settled state.

**Settled-specific fields:**

| UI element    | API field                 |
| ------------- | ------------------------- |
| Close reason  | `close_reason`            |
| Realized P\&L | `result.realized_pnl_usd` |
| Proceeds      | `result.proceeds_usd`     |
| Total fees    | `fees.total_fees_usd`     |
| Closed at     | `result.closed_at`        |

***

### 3. Liquidation Chart

An overlay on the market price chart showing the user's liquidation price, entry price, and current mark price as horizontal reference lines.

<figure><img src="/files/iocijNuPKxdUBKPwyKcf" alt=""><figcaption></figcaption></figure>

**Required elements:**

* Dashed line at entry price (muted)
* Solid line at current mark price (green)
* Solid line at liquidation price (red) with a subtle red fill below
* Price labels on each line
* Summary metrics below: liquidation price, distance to liquidation, margin health

**API fields used:**

| UI element         | API field                     |
| ------------------ | ----------------------------- |
| Entry line         | `entry.price_usd`             |
| Current price line | `current.mark_price_usd`      |
| Liquidation line   | `risk.liquidation_price_usd`  |
| Buffer distance    | `risk.liquidation_buffer_bps` |
| Margin health      | `risk.health_pct`             |

### 4. Interactive View

The interactive reference below lets you explore each component in context. Drag the leverage slider to see the expanded view update in real-time, or drag the liquidation line on the chart to see implied leverage recalculate. All values shown are simulated. In production, these are populated by your API responses.

{% embed url="<https://components.dimes.fi/>" %}


# Media

Dimes media assets

Partners integrating Dimes Multiply are required to display a "Leverage by Dimes" attribution within their leveraged trading interface. The attribution lockup, the text "Leverage by" alongside the Dimes logo, must appear directly beneath the leverage selector on any screen where users can initiate a leveraged position.

For any usage that falls outside the guidelines on this page, co-marketing materials, press announcements, or custom placements, please reach out to the Dimes team for approval.

#### PNG

<div><figure><img src="/files/8Q98yi6kkvQkyUihhWEW" alt=""><figcaption></figcaption></figure> <figure><img src="/files/FGfGCua8jBsbcbDhaOPK" alt=""><figcaption></figcaption></figure> <figure><img src="/files/qvyIMTepAAFsnWhMxDjh" alt=""><figcaption></figcaption></figure></div>


# Integration Agreement

**Dimes Protocol Limited × \[Partner Name]**

This Multiply Integration Agreement (this "Agreement") is entered into as of \[Date] (the "Effective Date"), by and between **Dimes Protocol Limited** ("Dimes") and **\[Partner Name]** ("Partner", together the "Parties").

Dimes operates Multiply, an embedded credit infrastructure layer that provides leveraged trading on prediction markets. Partner operates a trading front-end (the "Front-End") and wishes to integrate the Multiply API to provide its users with access to the Multiply service. The [Dimes Platform Terms](https://dimes.fi/terms-policy) (the "Platform Terms") are incorporated by reference. In the event of conflict, this Agreement controls.

## 1. Scope

Dimes grants Partner a non-exclusive, revocable, non-transferable right to integrate the Multiply API into the Front-End for the sole purpose of providing Partner's users with access to leveraged prediction market exposure on venues supported by Multiply. Dimes retains sole control over all credit decisions, risk parameters, eligibility gating, leverage bands, margin enforcement, hedge execution, and liquidation.

All leveraged transactions executed through Multiply constitute a direct relationship between the user and Dimes. The Multiply smart contract designates Dimes as the counterparty to each transaction at the point of execution. Partner does not act as counterparty, underwriter, or custodian with respect to any Multiply transaction. This structure is designed to protect Partner from liability related to credit, risk, and liquidation operations, which are separated from Partner's role and managed exclusively by Dimes.

## 2. Responsibilities

**2.1** Dimes shall:

(a) operate the Multiply credit and risk engine, including eligibility gating, leverage bands, concentration limits, margin monitoring, liquidation, and settlement;

(b) execute and manage all hedging activity on external prediction market venues;

(c) provide and maintain the Multiply API and accompanying technical documentation; and

(d) publish real-time market state data for display by Partner.

**2.2** Partner shall:

(a) own and operate the Front-End and route user intent to Multiply via the API;

(b) display all Multiply-provided constraints, risk parameters, and fee disclosures accurately and without delay;

(c) comply with all laws and regulations applicable to Partner's operations in each jurisdiction in which Partner makes the Front-End available; and

(d) to further reinforce Partner's limited role in the provision of leveraged trading, Dimes recommends that Partner display the following or substantially similar language in the Front-End's Terms of Service: *"Leveraged trading is provided by Dimes Protocol. By submitting a leverage transaction, you agree to the* [*Dimes Platform Terms*](https://dimes.fi/terms-policy)*."*

## 3. Economics

Partner shall be entitled to charge a partner-defined origination fee, denominated in basis points and provisioned by Dimes on the Partner record, applied to the leveraged notional of positions originated through Partner's Front-End. The partner origination fee is collected by Dimes on Partner's behalf at position open and settled directly to the fee wallet designated by Partner. Dimes retains in full all protocol fees (entry, time-based, and liquidation) to fund credit, hedging, risk management, and settlement operations that Dimes is solely responsible for under this Agreement. Partner fee accruals are calculated per position and Dimes shall provide reporting sufficient to verify all fee calculations.

Operational details required for fee settlement and administration (including Partner's designated fee wallet and such other details as Dimes may reasonably require) shall be provided by Partner prior to go-live.

## 4. API Usage; Confidentiality

Partner shall integrate with Multiply in accordance with the technical documentation provided separately. Partner shall not cache, modify, or delay Multiply-provided market state data. Dimes may update the API from time to time and shall provide reasonable advance notice of material changes.

Each Party shall keep confidential all non-public information received from the other in connection with this Agreement and shall not disclose such information to third parties without the disclosing Party's prior written consent. User data collected by Partner remains Partner's responsibility; Dimes processes only data necessary to operate Multiply.

## 5. Intellectual Property

Each Party retains all right, title, and interest in and to its pre-existing intellectual property. Dimes retains all rights in and to the Multiply platform, API, risk engine, documentation, and all related technology. Partner retains all rights in and to the Front-End. Nothing in this Agreement grants either Party any ownership interest in the other Party's intellectual property. The limited API access granted hereunder is a license only and does not constitute a transfer or assignment of any intellectual property rights.

## 6. Term and Termination

This Agreement is effective from the Effective Date and continues until terminated. Either Party may terminate for convenience on fourteen (14) days' written notice. Either Party may terminate immediately upon: (a) material breach not cured within seven (7) days of written notice; (b) a regulatory or venue-access event rendering continued operation impractical; or (c) insolvency, fraud, or willful misconduct. Upon termination, open positions shall be managed through their natural lifecycle. Partner shall remove the integration within ten (10) business days and cease all use of the Multiply API and any Dimes intellectual property.

## 7. Limitation of Liability; Representations

Multiply is provided "as is." Dimes makes no warranties, express or implied, regarding performance, uptime, or suitability for any purpose. Dimes shall not be liable for user trading losses, venue outages, execution slippage, or market outcomes. Each Party's aggregate liability shall not exceed the total fees paid or payable to such Party in the twelve (12) months preceding the claim. Neither Party shall be liable for indirect, incidental, consequential, or punitive damages.

Each Party represents and warrants that: (a) it has the authority to enter into this Agreement; (b) it will comply with all laws applicable to its performance hereunder; and (c) its performance will not violate any agreement with a third party. Partner further represents that it will not misrepresent the nature, risks, or characteristics of the Multiply service to its users.

## 8. General

**Entire Agreement.** This Agreement and the Platform Terms constitute the entire agreement between the Parties.

**Amendment.** This Agreement may be amended only by written instrument signed by both Parties.

**Assignment.** Neither Party may assign this Agreement without the other's prior written consent.

**Governing Law.** This Agreement shall be governed by the laws of the British Virgin Islands.

**Counterparts.** This Agreement may be executed in counterparts, each of which shall be deemed an original.


