Added Rain (rain.one) as the 15th venue — a permissionless AMM-plus-orderbook prediction market on Arbitrum One. Reads (markets, events, outcomes, orderbook, OHLCV, positions, balance) and full on-chain writes (market buys via AMM, limit buys/sells through the order book, cancels) are wired end-to-end via the official @buidlrrr/rain-sdk and viem signing. Verified live against production: markets like "Which Team will win FIFA World Cup 2026?" (16 outcomes) and Trump/Khamenei binary markets round-trip correctly through fetchMarkets, fetchEvents, and fetchOrderBook with prices, liquidity, and statuses populated.
Added
core/src/exchanges/rain/: New adapter following the existing 3-layer pattern (fetcher, normalizer, websocket) plus a small auth.ts that derives an EVM signer + Arbitrum public client from a privateKey. The SDK is loaded via ESM dynamic import() (same shape as Opinion) since @buidlrrr/rain-sdk is ESM-only. Multi-option markets (e.g. the 16-team FIFA market) are expanded into one synthetic binary UnifiedMarket per option inside a single UnifiedEvent, matching the Polymarket/Myriad grouping the matching engine expects. Orderbook is a 1-level emulated book at AMM spot (mirrors Myriad). Subgraph-backed methods (fetchOHLCV, fetchTrades, fetchMyTrades, fetchOpenOrders) return [] when no subgraphUrl is configured rather than throwing, since 'emulated' is the honest capability for an on-chain venue with no native list endpoint.
core/src/exchanges/rain/index.ts: Full trading path. buildOrder returns a populated BuiltOrder.tx = { to, data, value, chainId: 42161 } from the SDK's transaction builders — buildBuyOptionRawTx for market buys, buildLimitBuyOptionTx for limit buys, buildSellOptionTx for sells (Rain has no AMM market-sell; that path throws NotSupported with a message pointing at the limit branch). submitOrder signs with the viem WalletClient, auto-sends an ERC20 approve(MAX_UINT256) on the market contract before the first buy on that market, and waits for the approval receipt before submitting the order tx. cancelOrder parses an id of the form rain:{contract}:{side}:{option}:{price1e18}:{rainOrderId}:{txHash} and dispatches to buildCancelBuyOrdersTx / buildCancelSellOrdersTx, so cancels round-trip without needing subgraph state.
core/src/exchanges/rain/utils.ts: resolveDecimals() helper. The Rain SDK returns baseTokenDecimals as the scale factor (e.g. 1000000n for a 6-decimal token), not the decimal count — passing it straight into a 10 ** n computation produced astronomically large scales and silently zeroed every liquidity and volume reading. The helper detects this (> 36 → log10) and normalizes to a real decimal count; called from every fetcher/normalizer site that touches base-token math.
core/src/exchanges/rain/normalizer.ts: Reads from both the list-shape response (getPublicMarkets returns Mongo-style _id, question, options[].percentage as 0-100, and totalLiquidityUSD in base-token wei) and the on-chain details-shape (getMarketDetails returns id, title, options[].currentPrice as 1e18 bigint, totalLiquidity as wei bigint). The list-shape is the source of truth for the catalog and is sufficient on its own — the original implementation called getMarketDetails for every market in an N+1 enrichment loop because the published agent docs describe only the details shape; the loop is kept (bounded parallel, top 25 by default) so on-chain prices override the cached percentage when available, but the adapter still works correctly if every detail call fails.
core/src/index.ts, core/src/server/exchange-factory.ts, core/src/server/openapi.yaml: Standard 3-site registration. case "rain" reads RAIN_PRIVATE_KEY, RAIN_WALLET_ADDRESS, RAIN_SUBGRAPH_URL, RAIN_SUBGRAPH_API_KEY, RAIN_WS_RPC_URL, RAIN_ENVIRONMENT from env when no explicit credentials are passed.
core/package.json: Added @buidlrrr/rain-sdk ^2.0.0. The SDK declares optional peer deps on @account-kit/* and @alchemy/aa-* for its account-abstraction path; we intentionally do not install them in v1 since PMXT trades from an EOA via viem rather than through Rain's smart-account wrapper.
README.md: Rain logo added to the supported-venues row.
Hosted trading works from ESM apps and Opinion orders pass pre-sign validation. Two independent bugs each blocked all hosted writes for affected callers: the ESM build could not lazy-load ethers (bare require is undefined in ESM, and the failure was silently swallowed — the signer was dropped and every write died with "hosted write requires a signer" even when a privateKey was passed), and the client-side economics validator demanded message.opinion_market_id from a trading-API message schema that no longer carries it (the signed economic identity is the outcome tokenId). Both verified live against trade.pmxt.dev from an ESM consumer.
Fixed
- TS
pmxt/signers.ts: New loadEthers() helper used by EthersSigner — native require in the CJS build, process.getBuiltinModule("node:module").createRequire(...) in the ESM build (Node >= 20.16). Previously the ESM build's bare require("ethers") threw ReferenceError, which the lazy-signer bridge in the Exchange constructor caught and swallowed, silently discarding the caller's privateKey.
- TS
pmxt/hosted-typed-data.ts: signature verification now loads ethers via the same helper instead of bare require.
- TS
pmxt/hosted-typed-data.ts + Python pmxt/_hosted_typeddata.py: validateOpinionMarketId / _validate_opinion_market_id now validate message.tokenId against resolved.token_id — the field that is actually signed. The opinion_market_id equality check only applies when the message carries the field (legacy schema); requiring it unconditionally rejected every current-schema Opinion order pre-sign with economic mismatch: message.opinion_market_id missing.
- Python
tests/test_hosted_typeddata.py: opinion economics tests updated to the tokenId-based contract (mismatch rejection on resolved.token_id, params-only quirks no longer block, legacy opinion_market_id mismatch still rejected when present in the message).