@pulsepairs/sdk 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,10 @@ It gives you three things:
19
19
  Alchemy smart-contract account (the SAME wallet rain.trade uses), plus
20
20
  gasless onboarding / approve / withdraw.
21
21
 
22
+ > This README is the quickstart. For the exhaustive reference — every export,
23
+ > its units, its failure modes, the WS protocol, the full type surface and a
24
+ > troubleshooting table — see **[`DOCUMENTATION.md`](./DOCUMENTATION.md)**.
25
+
22
26
  > **Live integration environment:** a public demo of the full stack (markets
23
27
  > cycling 24/7 on Arbitrum One, mock oracle, mintable test USDT) runs at
24
28
  > **https://demo-pulsepairs.rainwins.com** with the matcher API/WS at
package/dist/eip712.d.ts CHANGED
@@ -218,20 +218,28 @@ export declare function parseCompositeMarketKey(market: string): ParsedComposite
218
218
  export declare function centsToBps(cents: number): number;
219
219
  /** Bps → cents (5500 → 55). Inverse of `centsToBps`. */
220
220
  export declare function bpsToCents(bps: number): number;
221
- /** Atomic-USDT denomination of the documented $5–$500 stake window. */
222
- export declare const MIN_STAKE_ATOMIC = 5000000n;
221
+ /**
222
+ * Atomic denomination of the documented stake window, in SHARE FACE VALUE
223
+ * — `Order.amount` is a share count, not the cash spent (the buyer pays
224
+ * `amount × price / 10000`). A $1 floor is $1 of face: ~$0.51 of cash at
225
+ * 51¢. Lowered from $5 → $1 on 2026-07-23 (Polymarket parity).
226
+ */
227
+ export declare const MIN_STAKE_ATOMIC = 1000000n;
223
228
  export declare const MAX_STAKE_ATOMIC = 500000000n;
224
229
  /** Parse a USD string (e.g. "5", "5.50", "12.345") into atomic USDT.
225
230
  * Rejects negatives + non-finite. */
226
231
  export declare function parseStake(usd: string | number): bigint;
227
232
  /**
228
- * Defense-in-depth stake clamp. Throws if `amountAtomic` is outside the
229
- * documented `$5 ≤ stake ≤ $500` window. Frontend gates these too; backend
230
- * enforces only the upper bound today (BUG-S2.1 — see PULSEPAIRS_BACKLOG).
231
- * SDK callers should validate before signing so a bad stake never produces
232
- * a signed payload at all.
233
+ * Defense-in-depth stake clamp. Throws if `amountAtomic` (share face value)
234
+ * is outside the documented `$1 ≤ stake ≤ $500` window; the backend applies
235
+ * the same window at the API boundary. SDK callers should validate before
236
+ * signing so a bad stake never produces a signed payload at all.
237
+ *
238
+ * Pass `side: "SELL"` for exits: the minimum is ENTRY-ONLY, so a holder left
239
+ * with a sub-$1 position (routine after a partial fill) can always sell it.
240
+ * The default stays "BUY" so existing callers keep the strict branch.
233
241
  */
234
- export declare function assertStakeBounds(amountAtomic: bigint): void;
242
+ export declare function assertStakeBounds(amountAtomic: bigint, side?: "BUY" | "SELL"): void;
235
243
  /**
236
244
  * Probability-weighted fee in atomic USDT. Mirrors the backend / contract
237
245
  * formula. Returns the fee for `notionalAtomic` filled at `priceBps`.
package/dist/eip712.js CHANGED
@@ -220,8 +220,13 @@ export function bpsToCents(bps) {
220
220
  }
221
221
  return bps / 100;
222
222
  }
223
- /** Atomic-USDT denomination of the documented $5–$500 stake window. */
224
- export const MIN_STAKE_ATOMIC = 5000000n;
223
+ /**
224
+ * Atomic denomination of the documented stake window, in SHARE FACE VALUE
225
+ * — `Order.amount` is a share count, not the cash spent (the buyer pays
226
+ * `amount × price / 10000`). A $1 floor is $1 of face: ~$0.51 of cash at
227
+ * 51¢. Lowered from $5 → $1 on 2026-07-23 (Polymarket parity).
228
+ */
229
+ export const MIN_STAKE_ATOMIC = 1000000n;
225
230
  export const MAX_STAKE_ATOMIC = 500000000n;
226
231
  /** Parse a USD string (e.g. "5", "5.50", "12.345") into atomic USDT.
227
232
  * Rejects negatives + non-finite. */
@@ -233,14 +238,17 @@ export function parseStake(usd) {
233
238
  return BigInt(Math.round(n * 1e6));
234
239
  }
235
240
  /**
236
- * Defense-in-depth stake clamp. Throws if `amountAtomic` is outside the
237
- * documented `$5 ≤ stake ≤ $500` window. Frontend gates these too; backend
238
- * enforces only the upper bound today (BUG-S2.1 — see PULSEPAIRS_BACKLOG).
239
- * SDK callers should validate before signing so a bad stake never produces
240
- * a signed payload at all.
241
+ * Defense-in-depth stake clamp. Throws if `amountAtomic` (share face value)
242
+ * is outside the documented `$1 ≤ stake ≤ $500` window; the backend applies
243
+ * the same window at the API boundary. SDK callers should validate before
244
+ * signing so a bad stake never produces a signed payload at all.
245
+ *
246
+ * Pass `side: "SELL"` for exits: the minimum is ENTRY-ONLY, so a holder left
247
+ * with a sub-$1 position (routine after a partial fill) can always sell it.
248
+ * The default stays "BUY" so existing callers keep the strict branch.
241
249
  */
242
- export function assertStakeBounds(amountAtomic) {
243
- if (amountAtomic < MIN_STAKE_ATOMIC) {
250
+ export function assertStakeBounds(amountAtomic, side = "BUY") {
251
+ if (side === "BUY" && amountAtomic < MIN_STAKE_ATOMIC) {
244
252
  throw new Error(`stake below $${Number(MIN_STAKE_ATOMIC) / 1e6} minimum`);
245
253
  }
246
254
  if (amountAtomic > MAX_STAKE_ATOMIC) {
package/dist/http.d.ts CHANGED
@@ -18,6 +18,18 @@ export declare class UpDownHttpClient {
18
18
  getOrderbook(marketAddress: string): Promise<OrderBookFull>;
19
19
  getBalance(wallet: string): Promise<Balance>;
20
20
  getPositions(wallet: string): Promise<Position[]>;
21
+ /**
22
+ * Open positions PLUS the wallet-level realized-from-sells scalar (atomic
23
+ * USDT, signed). Uses `?includeRealized=1`; `getPositions` keeps the plain
24
+ * bare-array response for callers that don't need the realized term.
25
+ *
26
+ * Tolerant of a backend that predates the envelope: if the body comes back a
27
+ * bare array, the realized term is simply reported as "0" rather than failing.
28
+ */
29
+ getPositionsWithRealized(wallet: string): Promise<{
30
+ positions: Position[];
31
+ realizedFromSells: string;
32
+ }>;
21
33
  getTrades(wallet: string, opts?: {
22
34
  limit?: number;
23
35
  offset?: number;
package/dist/http.js CHANGED
@@ -69,6 +69,21 @@ export class UpDownHttpClient {
69
69
  const res = await fetch(buildUrl(this.baseUrl, `/positions/${wallet}`));
70
70
  return parseJson(res);
71
71
  }
72
+ /**
73
+ * Open positions PLUS the wallet-level realized-from-sells scalar (atomic
74
+ * USDT, signed). Uses `?includeRealized=1`; `getPositions` keeps the plain
75
+ * bare-array response for callers that don't need the realized term.
76
+ *
77
+ * Tolerant of a backend that predates the envelope: if the body comes back a
78
+ * bare array, the realized term is simply reported as "0" rather than failing.
79
+ */
80
+ async getPositionsWithRealized(wallet) {
81
+ const res = await fetch(buildUrl(this.baseUrl, `/positions/${wallet}`, { includeRealized: 1 }));
82
+ const body = await parseJson(res);
83
+ if (Array.isArray(body))
84
+ return { positions: body, realizedFromSells: "0" };
85
+ return { positions: body.positions ?? [], realizedFromSells: body.realizedFromSells ?? "0" };
86
+ }
72
87
  async getTrades(wallet, opts) {
73
88
  const res = await fetch(buildUrl(this.baseUrl, `/trades/${wallet}`, {
74
89
  limit: opts?.limit,
@@ -91,9 +106,15 @@ export class UpDownHttpClient {
91
106
  * @param opts.usdtDecimals display-float decimals (default 6).
92
107
  */
93
108
  async getPnl(wallet, opts) {
94
- const positions = await this.getPositions(wallet);
109
+ // Fetch the realized-from-sells scalar alongside the positions so the
110
+ // rolled-up PnL carries realized (from manual sells) as well as unrealized.
111
+ // Backward-tolerant: an old backend yields "0" here, not an error.
112
+ const { positions, realizedFromSells } = await this.getPositionsWithRealized(wallet);
95
113
  if (positions.length === 0) {
96
- return computePortfolioPnl([], new Map(), { usdtDecimals: opts?.usdtDecimals });
114
+ return computePortfolioPnl([], new Map(), {
115
+ usdtDecimals: opts?.usdtDecimals,
116
+ realizedFromSells,
117
+ });
97
118
  }
98
119
  const marketKeys = Array.from(new Set(positions.map((p) => p.market)));
99
120
  const details = await Promise.all(marketKeys.map((key) => this.getMarket(key).catch(() => null)));
@@ -111,6 +132,7 @@ export class UpDownHttpClient {
111
132
  return computePortfolioPnl(positions, marketsByKey, {
112
133
  fees,
113
134
  usdtDecimals: opts?.usdtDecimals,
135
+ realizedFromSells,
114
136
  });
115
137
  }
116
138
  async getOrders(wallet, opts) {
package/dist/pnl.d.ts CHANGED
@@ -82,6 +82,18 @@ export type PortfolioPnl = {
82
82
  * (see `feesPaidByWallet`); "0" otherwise. Not netted into `unrealizedPnl`
83
83
  * — cost basis is fee-exclusive, so fees stay a separate, explicit line. */
84
84
  fees: string;
85
+ /**
86
+ * Atomic USDT (signed) realized P&L booked from manual market-SELLs, across
87
+ * ALL of the wallet's positions — including ones sold to zero net shares,
88
+ * which never appear in `positions`. "0" when the caller did not supply it
89
+ * (or the backend predates the `?includeRealized=1` envelope).
90
+ *
91
+ * Report it ALONGSIDE `unrealizedPnl`, which already carries settled
92
+ * survivors at full-face/zero (`resolved: true`); the two together are the
93
+ * wallet's complete realized+unrealized P&L. It is average-cost per sell, so
94
+ * only the combined figure is order-invariant — never surface it standalone.
95
+ */
96
+ realizedFromSells: string;
85
97
  };
86
98
  /** Options controlling how one position is valued. */
87
99
  export type PositionPnlOptions = {
@@ -130,9 +142,13 @@ export declare function positionPnl(position: Position, opts?: PositionPnlOption
130
142
  * so treat it as an upper bound on the wallet's own fee drag. */
131
143
  export declare function feesPaidByWallet(wallet: string, trades: Trade[]): string;
132
144
  /** Roll individual position PnLs up into a portfolio total. Pass `fees` (from
133
- * `feesPaidByWallet`) to carry a fee line through; it is reported, not netted. */
145
+ * `feesPaidByWallet`) to carry a fee line through; it is reported, not netted.
146
+ * Pass `realizedFromSells` (atomic USDT string, from `GET
147
+ * /positions/:wallet?includeRealized=1`) to carry the manual-sell realized
148
+ * term; likewise reported, never netted into `unrealizedPnl`. */
134
149
  export declare function portfolioPnl(items: PositionPnl[], opts?: {
135
150
  fees?: string;
151
+ realizedFromSells?: string;
136
152
  usdtDecimals?: number;
137
153
  }): PortfolioPnl;
138
154
  /**
@@ -146,5 +162,6 @@ export declare function portfolioPnl(items: PositionPnl[], opts?: {
146
162
  */
147
163
  export declare function computePortfolioPnl(positions: Position[], marketsByKey: Map<string, Pick<MarketDetail, "winner" | "orderBook">>, opts?: {
148
164
  fees?: string;
165
+ realizedFromSells?: string;
149
166
  usdtDecimals?: number;
150
167
  }): PortfolioPnl;
package/dist/pnl.js CHANGED
@@ -113,7 +113,10 @@ export function feesPaidByWallet(wallet, trades) {
113
113
  return total.toString();
114
114
  }
115
115
  /** Roll individual position PnLs up into a portfolio total. Pass `fees` (from
116
- * `feesPaidByWallet`) to carry a fee line through; it is reported, not netted. */
116
+ * `feesPaidByWallet`) to carry a fee line through; it is reported, not netted.
117
+ * Pass `realizedFromSells` (atomic USDT string, from `GET
118
+ * /positions/:wallet?includeRealized=1`) to carry the manual-sell realized
119
+ * term; likewise reported, never netted into `unrealizedPnl`. */
117
120
  export function portfolioPnl(items, opts = {}) {
118
121
  const decimals = opts.usdtDecimals ?? DEFAULT_USDT_DECIMALS;
119
122
  let costBasis = 0n;
@@ -131,6 +134,7 @@ export function portfolioPnl(items, opts = {}) {
131
134
  unrealizedPnlUsdt: atomicToUsdt(unrealizedPnl, decimals),
132
135
  roiPct: pctOf(unrealizedPnl, costBasis),
133
136
  fees: opts.fees ?? "0",
137
+ realizedFromSells: opts.realizedFromSells ?? "0",
134
138
  };
135
139
  }
136
140
  /**
package/dist/types.d.ts CHANGED
@@ -81,8 +81,24 @@ export type MarketListItem = {
81
81
  duration: number;
82
82
  status: "ACTIVE" | "TRADING_ENDED" | "RESOLVED" | "CLAIMED" | string;
83
83
  winner: number | null;
84
- upPrice: string;
85
- downPrice: string;
84
+ /**
85
+ * Odds in basis points of $1 (5000 = 50%), from the mid of the live book.
86
+ * `null` when no price is defined (empty / one-sided book, or trading not
87
+ * open) — render "—", never 0%. `0` / `10000` occur only on a settled
88
+ * market, where they are the outcome. When non-null,
89
+ * `upPrice + downPrice === 10000`.
90
+ *
91
+ * BREAKING 2026-07-26: previously `string`, and previously carried the
92
+ * on-chain cumulative collateral flow rather than any price.
93
+ */
94
+ upPrice: number | null;
95
+ downPrice: number | null;
96
+ /**
97
+ * Why `upPrice`/`downPrice` are what they are, so a `null` is explained
98
+ * rather than merely absent. Widened to `string` deliberately: this is an
99
+ * OPEN set and new values may be added — do not switch exhaustively.
100
+ */
101
+ priceSource?: "settled" | "pending_resolution" | "not_trading" | "book_mid" | "none" | string;
86
102
  strikePrice?: string;
87
103
  settlementPrice?: string;
88
104
  volume: string;
@@ -159,6 +175,13 @@ export type Trade = {
159
175
  platformFee: string;
160
176
  makerFee: string;
161
177
  settlementStatus: string;
178
+ /** Hash of the on-chain settlement tx, for linking a fill to the explorer.
179
+ * Null until the tx is broadcast — and reset to null if a reconcile drops
180
+ * it — so treat absence as "pending", not "missing". Complementary
181
+ * (MINT/MERGE) fills settle in batches, so the same hash legitimately
182
+ * repeats across several trades. Optional for back-compat with a
183
+ * pre-2026-07-21 backend. */
184
+ settlementTxHash?: string | null;
162
185
  createdAt: string;
163
186
  /** Match geometry. NORMAL = same-option fill. MINT/MERGE = complementary
164
187
  * cross (opposite-option legs). Optional so a pre-2026-07-17 backend
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pulsepairs/sdk",
3
- "version": "0.5.2",
4
- "description": "Standalone SDK for the UpDown (PulsePairs) up/down prediction markets — matcher REST/WS client, EIP-712 order signing, trade-math, and Alchemy Account Kit (smart-account) order signing for rain.trade integration.",
3
+ "version": "0.7.0",
4
+ "description": "Standalone SDK for the UpDown (PulsePairs) up/down prediction markets \u2014 matcher REST/WS client, EIP-712 order signing, trade-math, and Alchemy Account Kit (smart-account) order signing for rain.trade integration.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -27,7 +27,8 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
- "README.md"
30
+ "README.md",
31
+ "DOCUMENTATION.md"
31
32
  ],
32
33
  "sideEffects": false,
33
34
  "scripts": {