The pmv2 monolith (polymarket_fetch) is being retired and split into independent services/repos with NATS JetStream as the spine and a shared pmv2-contracts crate as the seam. This is Andras’s direction (2026-06-22, via a Miro board). This note captures the multi-service design and the trade wire-contract v2 that producers (incl. weather) emit. Weather-specific current state lives in weather-bet-live-status; project overview in weather-bet.

For Agents

This is a cross-service design note (touches all pmv2-* repos), kept in the weather vault because weather is one of the producers and signed off on the contract. The weather-relevant slice — the limit arm of the contract, weather sizing, the dry-test gate saga — is the authoritative source; the cross-service framing (cohort/crypto/notify) is context. If you only care about weather, read the “Trade wire-contract v2 — weather’s limit arm” section + weather-bet-live-status.

Companion note (AS-lane view)

pmv2-reorg-and-weather-as-2026-06-22 covers the same reorg from the Asian-weather (weather_as) producer side, with the as_weather_algo module map + the validated-but-never-live AS edge. Consistent with this note. One update since it was written: pmv2-contracts was described there as “being designed — not yet built”; it is now PUBLISHED (github.com/wowjeeez/pmv2-contracts), which is what unblocks both lanes’ publisher rewrites.

The split (producers → executor → notify)

The monolith’s responsibilities are being decomposed into independent repos under /Users/levander/coding/pmv2/, with unidirectional dependencies (producers → executor → notify):

RoleService / repoOwnerResponsibility
Producerpmv2-weather-us-algo (THIS, source=weather)weatherUS lock signals (NYC/Chicago/Miami/Atlanta)
Producerpmv2-weather-as-algo (source=weather_as)weather-AS (separate session)Asian-market lock signals (~$10–30 notional)
Producerpmv2-cohort-algo (source=first_mover_core)cohortcohort first-mover signals
Producerpmv2-crypto-algo (source=crypto_shortterm_latency_test)cryptocrypto short-term latency signals
Executorpmv2-position-manager@deploy / @positionmanagerthe trade executor (consume + gate + sign + place + settle)
Notifypmv2-telegram-connector / dedicated notification serviceTBDcentralizes the ~25–30 scattered Telegram send-sites
Seampmv2-contracts (PUBLISHED)sharedthe wire types + subject builders both producers + PM vendor

New @dionysus / @shipping producer lanes were also mentioned as future additions.

Position Manager = the executor

pmv2-position-manager (handle @positionmanager, owned by @deploy) is the executor — polymarket_fetch’s consume + execution responsibilities move here. It:

  • Consumes signals DIRECTLY from NATS (no separate runner process in between).
  • Gates: 3-layer mode (off / dry / live) + position caps + a breaker.
  • Signs EIP-712 orders and POSTs them to clob.polymarket.com.
  • Records + settles positions.
  • Has no GammaClient — it does NOT do strategy/eligibility gating. Producers own ALL of that and emit only orders they have already decided to take. PM re-resolves market params (neg_risk/tick/min) authoritatively at placement and applies its global caps/breaker as the hard backstop.

Mode is currently OFF

Position Manager is not live-armed (Mode::Off). It acks-and-skips. Going live needs the operator to flip the source’s mode (and the weather dry-rehearsal must clear first — see weather-bet-live-status).

Notification service

A dedicated service centralizing the ~25–30 Telegram send-sites that are scattered across the monolith today, sitting behind a NATS notify subject. Owner TBD.

pmv2-contracts — the shared seam (PUBLISHED)

github.com/wowjeeez/pmv2-contracts — a small, published Rust crate that producers and Position Manager both vendor so the wire can’t drift.

  • Dependencies: serde + rust_decimal only (deliberately minimal — it is a pure types crate).
  • Holds: the envelope types, a Pricing discriminator enum (limit | derived), a Side enum, SCHEMA_VERSION (= 2), and the subject builders.
  • source is a FREE String, NOT an enum. Adding a new producer lane = publish the new source string + insert an operator pmv2_autotrade_sources DB row (which sets that source’s mode). It does not require a contracts release. This is deliberate — it keeps lane-onboarding a config change, not a code+publish cycle.

source is a load-bearing exact-match key

source is the EXACT pmv2_autotrade_sources DB key. A wrong/typo’d value silently routes to Mode::Off (the order is acked-and-skipped, no error). Weather’s value is exactly "weather".

Transport (NATS subjects + consumer binding)

  • Stream PMV2_ORDERS over subjects pmv2.order.<source>.<entry|exit>.
  • Position Manager binds ONE durable pull consumer on pmv2.order.> (wildcard, all sources, both entry/exit).
  • Explicit ack, Nats-Msg-Id = id for dedup, needs a duplicate_window configured on the stream.
producers (weather/weather_as/cohort/crypto)
   → PMV2_ORDERS / pmv2.order.<source>.<entry|exit>
   → PM single durable pull consumer on pmv2.order.>  (explicit ack, Nats-Msg-Id dedup)
   → gate (off/dry/live + caps + breaker) → sign EIP-712 → POST clob.polymarket.com → record/settle

Supersedes the old single-stream design

This replaces the old WEATHER_SIGNALS / weather.signals single-stream, strategy-agnostic-envelope design (where cohort rode the same stream on cohort.signals). The new model is one PMV2_ORDERS stream, source-routed subjects, and a shared contracts crate instead of a hand-tracked docs/signal-schema.md.

Trade wire-contract v2 — weather’s limit arm

Signed off by weather (2026-06-22)

This is the contract weather emits. It supersedes the old docs/signal-schema.md “Directive”/v1 envelope (action/max_price/flat market fields, schema_version=1, subject weather.signals). The old schema file is still v1 in the repo — the v2 producer code change is PENDING, see weather-bet-live-status.

Weather emits a READY BUY entry-order to subject pmv2.order.weather.entry.

Common fields (every order, every source)

fieldnotes
id`uuid5(“{source}
source"weather" — EXACT pmv2_autotrade_sources DB key (wrong value → silently Mode::Off)
schema_version2
produced_atemit time
valid_until_tsunix seconds; past → Term (PM drops it as stale)
market{condition_id, outcome_index, token_id, title, event_slug, slug, + neg_risk/tick/min as HINTS}
origin_refproducer-defined opaque trace ref = our Supabase weather_signals row id
side"BUY"

Pricing arm — pricing = "limit" (weather)

fieldnotes
limit_price≤ 0.95, hard cap (weather never overpays)
size_sharesweather-sized (see Sizing below)

PM placement semantics (the limit arm)

  • PM places a FAK marketable-limit order at floor_to_tick(min(limit_price, best_ask)).
  • Fills ≤ our cap. A near-certain bucket whose ask is > 0.95 simply won’t fill — and that is correct, we never overpay.
  • PM re-resolves neg_risk/tick/min authoritatively (the values on the wire are HINTS only).
  • Sized-out (shares < min) → Skip.

Entry-only (weather has no exit)

Weather is hold-to-settlement → no exit-signal (the exit is resolution → $1). The exit-signal path (pmv2.order.<source>.exit) is cohort/crypto only. Weather emits to ...entry exclusively.

The derived arm (cohort/crypto, for contrast)

The Pricing enum’s other discriminator is derived — used by cohort/crypto, where PM derives the order price rather than being handed a fixed limit_price. Weather does not use it. (Detail owned by those producers.)

Sizing moved to producers (NEW — weather now sizes)

Sizing moved out of the executor into producers. Weather sizes its own orders:

size_shares = clamp(floor(notional / limit_price), min_size = 5, cap)
  • Default notional = $50 (configurable).
  • min_size = 5 shares (the CLOB minimum_order_size on weather markets).
  • PM’s global caps + breaker remain the hard backstop (producer sizing is advisory to PM’s limits).

What stays OFF the execution wire

Weather contextcity, bucket_label, observed_high_f, lock_ts, reason — stays off the execution wire. It lives in our Supabase weather_signals row, reachable via origin_ref. PM ignores unknown fields (fail-open on extras), so this is safe, but the convention is: execution wire carries only what PM needs to place + dedup + expire the order.

Weather-AS lane (sibling, separate)

US (source=weather, this repo) and AS/Asian (source=weather_as, pmv2-weather-as-algo, a separate repo/session, ~$10–30 notional) are deliberately separate repos/lanes — NOT a shared engine. They share the contract (pmv2-contracts) and the executor (PM), nothing else.