npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pulsepairs/sdk

v0.6.0

Published

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.

Readme

@pulsepairs/sdk

Standalone SDK for the UpDown (PulsePairs) up/down prediction markets — BTC-USD / ETH-USD, UP or DOWN, on 5-minute / 15-minute / 1-hour cycles.

This is the package rain.trade integrates to surface UpDown markets alongside Rain's own markets ("satellite" integration, Option A). It is deliberately segregated from rain-sdk-v2: bump this package's version independently, no release coupling.

It gives you three things:

  1. Matcher client — typed REST + WebSocket client for the off-chain order book (UpDownHttpClient, UpDownWsClient).
  2. EIP-712 + trade-mathbuildOrderTypedData / buildCancelTypedData / buildWsAuthTypedData, plus fee/stake/price helpers that mirror the backend and contract exactly.
  3. Account Kit signingUpDownAccountKitSigner: sign orders with an Alchemy smart-contract account (the SAME wallet rain.trade uses), plus gasless onboarding / approve / withdraw.

This README is the quickstart. For the exhaustive reference — every export, its units, its failure modes, the WS protocol, the full type surface and a troubleshooting table — see DOCUMENTATION.md.

Live integration environment: a public demo of the full stack (markets cycling 24/7 on Arbitrum One, mock oracle, mintable test USDT) runs at https://demo-pulsepairs.rainwins.com with the matcher API/WS at https://api.demo-pulsepairs.rainwins.com (wss://…/stream). Every example below targets it by default. Test funds: POST /test/devmint (10k cap, 1 mint per address per 5 min — mint to the smart-account address, not the owner EOA; it also seeds a little gas ETH).


Install

npm install @pulsepairs/sdk viem
# For Account Kit (smart-account) signing, also install the peer deps rain.trade
# already ships (skip if you only use the EOA / private-key path):
npm install @account-kit/wallet-client@^4.88 @account-kit/infra@^4.88 \
            @account-kit/smart-contracts@^4.88 @aa-sdk/core@^4.88

viem is a required peer. The four @account-kit/*/@aa-sdk/* packages are optional peers — they are lazy-imported and only needed if you construct UpDownAccountKitSigner (smart-contracts specifically backs the popup-less order-session signing below). A pure-EOA bot never pulls any of them.


Two signing paths

The matcher and contract are signer-agnostic (on-chain verification is OZ SignatureChecker, which accepts EOAs and ERC-1271 smart accounts). Pick:

| Path | order.maker | Sign with | Use for | |---|---|---|---| | EOA (examples/simple-*) | your EOA | viem account.signTypedData | bots, scripts, testing | | Account Kit (examples/account-kit-taker.ts) | your SCA | UpDownAccountKitSigner | rain.trade / any smart-wallet UX |


Quickstart — EOA (bots)

import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrum } from "viem/chains";
import {
  UpDownHttpClient, buildOrderTypedData, ensureSettlementAllowance, freshNonce,
  parseCompositeMarketKey, parseStake, OrderType, OrderSide, Option,
} from "@pulsepairs/sdk";

// Point the bot at a matcher YOU chose. Don't hardcode ours into a process
// that holds a funded key — and assert the chain matches your RPC's chain
// before you approve anything (see "Handling a funded key" below).
const api = new UpDownHttpClient(process.env.UPND_API!);
const cfg = await api.getConfig();                    // never hardcode addresses
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const [live] = (await api.getMarkets({ pair: "BTC-USD", timeframe: 300 }))
  .filter((m) => m.status === "ACTIVE");
const { settlementAddress, marketId } = parseCompositeMarketKey(live.address)!;

const amount = parseStake("5");

// BOUNDED approve. `settlementAddress` is server-supplied (it came from the
// matcher's market list), so cap its reach at what you actually intend to
// spend. `amount` = how much to approve; `threshold` = when to top back up.
// Omitting `amount` still approves MAX_UINT256 — that default is legacy.
await ensureSettlementAllowance({
  publicClient, walletClient, usdt: cfg.usdtAddress, settlement: settlementAddress,
  amount: amount * 20n, threshold: amount,
});

const maxFee = (amount * BigInt(cfg.platformFeeBps + cfg.makerFeeBps)) / 10000n; // peak fee
const nonce = freshNonce();                           // CSPRNG; never Math.random/Date.now
const typedData = buildOrderTypedData({
  cfg, settlementAddress,
  message: { maker: account.address, market: BigInt(marketId), option: BigInt(Option.UP),
    side: OrderSide.BUY, type: OrderType.MARKET, price: 0n, amount, maxFee, nonce,
    expiry: BigInt(live.endTime) },
});
const signature = await account.signTypedData(typedData);
await api.postOrder({ maker: account.address, market: live.address, option: Option.UP,
  side: OrderSide.BUY, type: OrderType.MARKET, price: 0, amount: amount.toString(),
  maxFee: maxFee.toString(), nonce: Number(nonce), expiry: live.endTime, signature });

Full runnable scripts live in the repo under examples/simple-taker.ts (MARKET), simple-maker.ts (LIMIT + authed WS), full-dmm-bot.ts (two-sided quoting). They are not in the npm tarball (files: ["dist","README.md"]), so if you installed from npm without repo access, this README is your reference — the safe patterns are inlined here on purpose.


Handling a funded key

This SDK is normally driven by a hot key that holds real USDT. Three rules, each of which exists because the failure is silent:

1. Never default your endpoints. A bot signs orders for whatever matcher you point it at, and then grants that matcher's server-supplied settlement address a USDT allowance. A defaulted UPND_API plus a defaulted mainnet RPC means a real funded key trading against a box you never chose. Require both explicitly:

const API = process.env.UPND_API;
if (!API) throw new Error("Set UPND_API — refusing to default a funded key to a third-party endpoint");
const RPC = process.env.ARBITRUM_RPC_URL;
if (!RPC) throw new Error("Set ARBITRUM_RPC_URL");

2. Assert the matcher and the RPC agree on the chain, before the approve. This one check catches the whole class — a testnet/demo matcher paired with a mainnet key can't survive it:

const cfg = await api.getConfig();
const rpcChainId = await publicClient.getChainId();
if (cfg.chainId !== rpcChainId) {
  throw new Error(`chain mismatch: matcher says ${cfg.chainId}, RPC says ${rpcChainId}`);
}

3. Bound the allowance. Pass amount to ensureSettlementAllowance (EOA) / onboard/approve (Account Kit) / buildApproveSettlementTx (raw-tx tier). All four default to MAX_UINT256 for backwards compatibility; that default is an unbounded claim on your balance by an address the server named. A bounded allowance is consumed by fills, so re-run the helper on a timer — if it runs dry mid-session your fills revert with insufficient allowance.

Use freshNonce() for order/cancel nonces. It draws from crypto.getRandomValues and stays under 2^53 so Number(nonce) on the wire matches the value you signed. Math.random()/Date.now() nonces are guessable, which lets anyone pre-burn your next nonce against the replay store and block your order flow.


Quickstart — Account Kit (rain.trade / smart wallets)

UpDownAccountKitSigner builds its own Alchemy smart-wallet client using the same config rain.trade's RainAA uses, so the same owner EOA resolves to the same SCA address → one wallet across both products.

import { arbitrum } from "viem/chains";
import { UpDownAccountKitSigner, buildOrderTypedData, UpDownHttpClient } from "@pulsepairs/sdk";

const ak = new UpDownAccountKitSigner({
  walletClient: window.ethereum,                 // owner EOA provider (or connector.getProvider())
  alchemyApiKey: import.meta.env.VITE_ALCHEMY_API_KEY,       // SAME as rain.trade
  paymasterPolicyId: import.meta.env.VITE_PAYMASTER_POLICY_ID, // SAME as rain.trade
  chain: arbitrum,                               // SAME as rain.trade (42161)
});
const sca = await ak.connect();                  // deterministic; == rain.trade's SCA

// Deploy-before-fill: ONE UserOp deploys the SCA + approves USDT + installs
// the popup-less order-session key (see "Popup-less order signing" below).
await ak.onboard({ usdt: cfg.usdtAddress, settlement });

// Owner-key ERC-1271 order signature (6492 wrapper stripped automatically).
const signature = await ak.signTypedDataBare(buildOrderTypedData({ cfg, settlementAddress, message }));

// WS auth for orders:/balance: channels (keyed by the SCA):
ws.connectAuthed({ channels: [`orders:${sca.toLowerCase()}`], signAuth: () => ak.signWsAuth(cfg.chainId) });

See examples/account-kit-taker.ts for the end-to-end flow.

Already have rain.trade's smart-wallet client?

If the host app owns an Alchemy client (e.g. rain.trade's shared session), skip UpDownAccountKitSigner and just make its output UpDown-correct:

import { bareErc1271Signer } from "@pulsepairs/sdk";
const signOrder = bareErc1271Signer((td) => rainSmartWalletClient.signTypedData(td));
const signature = await signOrder(buildOrderTypedData({ cfg, settlementAddress, message }));

Low-level "raw tx" tier (v0.3.0) — send with your own AA session

If you run your own Account-Abstraction send path (like rain.trade's sendTxs through their root session), use the transport-free primitives instead of the signer class. They return rain's exact RawTransaction shape ({ to, data, value? }) for the on-chain setup, and sign orders locally with the popup-less order-session key — the part your send-only session structurally cannot do (a root session packs the owner entity → isValidSignature returns 0xffffffff).

The one mental-model flip: placing an order is not a transaction. It is an off-chain EIP-712 signature POSTed to the matcher; the relayer transacts the fill. So there is no buildPlaceOrderTx — you signWithOrderSession(...) and postOrder.

import {
  buildApproveSettlementTx, buildInstallOrderSessionTx, signWithOrderSession,
  buildOrderTypedData,
} from "@pulsepairs/sdk";
import { arbitrum } from "viem/chains";

// Onboard once — batch both raw txs into ONE userOp through YOUR session:
const approveTx = buildApproveSettlementTx({ usdt: cfg.usdtAddress, settlement });
const { tx: installTx, session } = await buildInstallOrderSessionTx({ sca, chain: arbitrum });
await sendTxs([approveTx, installTx]);   // your batched, gasless send
persist(session);                        // { v, privateKey, entityId, expirySec }

// Every order after that — no chain, no gas, no popup:
const typedData = buildOrderTypedData({ cfg, settlementAddress, message /* maker: sca, maxFee, … */ });
const signature = await signWithOrderSession({ session, sca, chain: arbitrum, alchemyApiKey, typedData });
await api.postOrder({ maker: sca, /* … */, signature });

Two tiers, adopt in order: MVP = approve (1 raw tx) + bareErc1271Signer owner-path (1 popup/order, zero new code) → Production = approve + install (batched) + signWithOrderSession (popup-less). Full walkthrough in examples/rain-taker.ts; rationale in UPDOWN_RAIN_RAWTX_SDK_DESIGN.md. The on-chain acceptance of these bytes on rain's exact MA-v2 impl is proven by updown-contracts/test/OrderSessionForkTest.t.sol.


Account Kit: the two rules you cannot break

Both come from the on-chain verifier (OZ SignatureChecker v5.6.1, no EIP-6492):

  1. Deploy the SCA before its first fill. A counterfactual (undeployed) account cannot be verified via ERC-1271 → the fill reverts. onboard() does this (deploy + approve in one UserOp); isDeployed(publicClient) checks it.
  2. Bare ERC-1271, not 6492. signTypedData on an undeployed account can return a 6492-wrapped blob the settlement can't parse. Every signature this SDK produces is run through stripErc6492Wrapper(...), so a wrapped blob never reaches the matcher. (Exposed standalone too, for the callback path.)

Confirmed live on Arbitrum One (2026-07-05/06). A real SCA was deployed, isValidSignature over the full Order typed-data returned the 0x1626ba7e magic on-chain, a real enterPosition fill settled with maker = SCA, and the negative control (undeployed SCA) reverted as expected. The MA-v2 / ERC-7739 question is resolved: sign the FULL typed-data through the account client (which this SDK does); a bare-bytes32 flow does not validate.


Popup-less order signing (order sessions) — ON by default

UpDownAccountKitSigner distinguishes two different session-key mechanisms — do not conflate them:

  1. Send session (grantSession(), Alchemy grantPermissions root) — authorizes gasless UserOp execution (onboard/approve/withdraw). Its signatures pack the OWNER validation entity, so it is verified incapable of ERC-1271 order signing.
  2. Order session (default ON) — a locally-generated key installed on the MA-v2 SCA as a signature-validation-ONLY entity (isSignatureValidation: true, isUserOpValidation: false). It can only answer isValidSignature for orders / cancels / WS-auth — it can never move funds or execute a UserOp. onboard() batches the install into the same single UserOp as deploy + approve (still exactly one owner signature), after which orders sign with zero wallet popups.

Behavior and knobs:

  • signTypedDataBare prefers the order-session key when one is active and falls back to the owner key automatically on any failure — enabling this feature can never break signing.
  • ensureOrderSession() installs a session for already-onboarded accounts (one owner-signed UserOp); revokeOrderSession() drops the local key; hasOrderSession inspects state.
  • Opt out entirely with orderSessions: false in the constructor — every order then prompts the owner wallet (the pre-session behavior).
  • Storage: browser localStorage (updown:oskey:<sca>) by default, an in-memory map in Node, or bring your own via orderSessionStorage.
  • Known limitations (fine for integration/demo, review before launch): expiry is client-side only (24 h — not enforced on-chain), and revokeOrderSession() does not uninstall the validation entity on-chain.

Validated in 4 live PoC stages before shipping (on-chain 0x1626ba7e + negative controls, live matcher order 201 + cancel 200, batched-install flow).


maxFee (F-2026-17731) — REQUIRED on every order

The signed Order includes maxFee (between amount and nonce). The contract's ORDER_TYPEHASH includes it, so omitting it produces a signature the on-chain SignatureChecker rejects — the fill reverts. Compute the peak fee and pass the same value into both the typed-data and the POST body:

const maxFee = (amount * BigInt(cfg.platformFeeBps + cfg.makerFeeBps)) / 10000n;

Signing the peak means legitimate (probability-weighted) fills never revert with FeeExceedsTakerCap; the contract enforces the actual cumulative fee under this cap across partial fills.


PnL — positions marked to market (v0.5.0)

The matcher hands you the ingredients for PnL (shares / avgPrice / costBasis on positions, winner + order book on markets) but never a PnL number. getPnl assembles one, marking each position:

  • resolved market → won shares at full face (10000 bps), lost shares at 0;
  • live market → the order-book mid for that option;
  • no book signal → the position's own avgPrice (an honest 0-PnL mark, flagged markSource: "cost" so you can tell it apart).
const client = new UpDownHttpClient("https://api.demo-pulsepairs.rainwins.com");

const pnl = await client.getPnl(wallet, { includeFees: true });
console.log(pnl.unrealizedPnlUsdt, pnl.roiPct); // e.g. -3, -30
for (const p of pnl.positions) {
  console.log(p.optionLabel, p.markSource, p.unrealizedPnlUsdt);
}

All money math is BigInt-exact on atomic units and returned as decimal strings (costBasis, markValue, unrealizedPnl — signed); the *Usdt / roiPct floats are display-only. Cost basis is fee-exclusive (matching the backend's fold), so fees are a separate fees line, reported not netted.

Already holding the data? The pure helpers are transport-free and unit-tested:

import { positionPnl, computePortfolioPnl, midBps, feesPaidByWallet } from "@pulsepairs/sdk";

positionPnl(position, { markBps: 7000 });        // one position, explicit mark
positionPnl(position, { winner: 1 });            // settled: full-face / zero
computePortfolioPnl(positions, marketsByKey);    // roll-up from market details

Units: shares atomic (= USDT face at settlement), price/avgPrice/markBps in bps (10000 = full face), fees atomic. Option is 1 = UP, 2 = DOWN.


API surface

Clients          UpDownHttpClient, UpDownWsClient, wsUrlFromHttpBase
EIP-712          ORDER_TYPES, CANCEL_TYPES, WS_AUTH_TYPES,
                 buildOrderTypedData, buildCancelTypedData, buildWsAuthTypedData,
                 freshSessionId, freshNonce, domainForSettlement,
                 findPairBySettlement, parseCompositeMarketKey
Trade-math       centsToBps, bpsToCents, parseStake, assertStakeBounds, feeAtomic,
                 MIN_STAKE_ATOMIC, MAX_STAKE_ATOMIC
PnL (v0.5.0)     UpDownHttpClient.getPnl, positionPnl, portfolioPnl,
                 computePortfolioPnl, markValueAtomic, midBps, markForOption,
                 feesPaidByWallet, atomicToUsdt, isResolvedWinner
Approve (EOA)    ensureSettlementAllowance, MAX_UINT256
Account Kit      UpDownAccountKitSigner (connect, onboard, approve, withdraw,
                 signTypedDataBare, signWsAuth, isDeployed, grantSession,
                 ensureOrderSession, revokeOrderSession, hasOrderSession),
                 bareErc1271Signer, stripErc6492Wrapper, isErc6492Signature
Raw-tx tier      buildApproveSettlementTx, buildInstallOrderSessionTx,
(v0.3.0)         signWithOrderSession, RawTransaction, OrderSessionRecord
L2 HMAC auth     CLOB_AUTH_TYPES, buildClobAuthTypedData, buildHmacSignature, HMAC_HEADERS
Enums/types      OrderType, OrderSide, Option, ApiConfig, PairConfig, PostOrderBody, …

Field-name gotchas (live with them)

  • OrderRow.nonce / OrderRow.expiry are JSON strings (uint256 precision).
  • WS order_update uses orderType; REST OrderRow uses type. Same value.
  • Option is 1 = UP, 2 = DOWN — not 0/1.
  • Market addresses are composite <settlementAddress>-<marketId>. The signed Order.market is the bare uint256 marketId; the composite goes in the POST body's market field for routing only.
  • EIP-712 domain name is "UpDown Exchange", verifyingContract = the settlement contract, price in bps (not 1e18). WS-auth uses a distinct domain ("PulsePairs WebSocket Auth", verifyingContract = zero) so an order signature can never replay as a WS-auth signature.

Reference

  • Wire shapes / REST / WS: docs/api.md in the updown-backend repo (branch audit/hacken-remediation-v2) — auth model, WS handshake, all endpoints, error strings, rate limits.
  • On-chain spec: src/UpDownSettlement.sol in the updown-contracts repo (same branch) — ORDER_TYPEHASH, enterPosition, fee cap enforcement.
  • Schema drift guard: npm test runs an EIP-712 golden-vector test whose expected digest/signature were derived independently from the backend and contract definitions.