Signals

GET /flow/v1/signals · GET /flow/v1/signals/types · channel: signal · signals:read
Live

Named, explained observations over the live market: a cross-venue arbitrage, a price move, a whale trade, one-sided flow. Each carries a headline, a plain-English explanation, a strength, a confidence and the evidence it was computed from — so an agent or a screen can act on it without recomputing it.

What a signal is

A signal is a detector's claim about one position, with its working shown. It has a signal_type from a small catalog, a strength whose meaning the catalog states per type, a confidence of low / medium / high, an expires_at, a venues list, and an evidence block whose shape depends on the type.

One envelope for every type. Every signal, whatever its type, has the same top-level fields, and they mean the same thing on every type. In particular, venues is always [{ partner_id, partner_name }]: the venues the signal is about. A trade print names one venue, an arbitrage names both legs, and a price move names every venue that moved. Read the venue from here and never from evidence. evidence is the type's own data packet: everything specific to that kind of signal, and only that. Its fields are listed per type in the catalog's evidence_fields.
TypeStatusNeedsTypical lifeEvidence
arbitrageactivesignals:read120 sarbitrage_id, best_combined_price, best_guaranteed_profit_pct, after_fee_roi, profitable_after_fees, best_max_investment, sides
price_movebetasignals:read300 sfrom_price, to_price, move, direction, window_ms, venues_now, venues_before, total_available_usd
whale_tradebetasignals:read600 snotional_usd, size_multiple, threshold_usd, baseline_median_notional_usd, depth_ratio, contest_phase, trader_id, is_block
block_tradebetasignals:read900 snotional_usd, price, trader_id
trade_burstbeta · not publishedsignals:read300 strades, total_notional_usd, largest_print_usd, window_ms
flow_imbalancebetasignals:read180 sbuy_notional_usd, sell_notional_usd, imbalance, venues
trade_throughbeta · evaluatingsignals:read300 sprice, cached_price, through, quote_age_ms
volume_spikebeta · evaluatingsignals:read600 strades_last_5m, baseline_per_5m, multiple
off_market_priceretiredsignals:read—still described by the catalog; no longer emitted

The trade types are built from venue prints — trades that actually happened — rather than from quotes, and only on venues that publish their trades. Each carries the venue in evidence.partner_id. trader_id is filled where the venue exposes an identity (a wallet) and is null where it does not, so a whale on one venue is a large print and on another can name the wallet behind it.

What the trade types publish, as calibrated against historical prints before release:

  • whale_trade — a print of at least twice the whale threshold (the greater of $500 or eight times the position's median print), so roughly $1,000 and up on most markets. contest_phase says whether it printed before the scheduled start; pre-game whales did not move prices in testing, so their confidence is capped at medium however large they are.
  • flow_imbalance — only unanimous flow: every aggressive print in the last minute one way, at least five prints and $1,000, with no single print more than half of it (a single large print is a whale, not flow).
  • trade_burst — observed for calibration but not published: bursts did not beat ordinary trading in testing. Its catalog entry stays so a stored type string still resolves.
  • trade_through and volume_spike — published at their initial thresholds while they are evaluated; expect their calibration to change.

Read the catalog rather than this table: GET /signals/types is open to any key on any plan and states each type's strength_semantics and evidence_fields. Beta types are served only when you ask for them.

A signal is filtered by what you hold. Each type names its required_entitlements; a signal whose type needs a code your key lacks is simply absent from the feed, not an error. Today every type needs only signals:read. To see what your own key will receive, read the catalog: each type carries entitled (true or false for the key that asked) and missing_entitlements (the codes you would need).
Arbitrage is a signal. Signals are the standard surface for everything we detect, arbitrage included. An arbitrage signal's evidence is its data packet: the summary numbers (best_combined_price, after_fee_roi, best_max_investment, …) and sides, which gives both legs with every venue quoting each one (partner_id, price, after_fee_price, available, position_hash). The legs' venues are in the envelope's venues. The older evidence.venues (two bare ids) is deprecated: it is still sent, but read venues instead. The standalone GET /flow/v1/arbitrages endpoint and the arbitrage stream channel are deprecated in favour of ?type=arbitrage and the signal channel. They keep working under arbitrage:read.
GET/flow/v1/signals/types

The catalog — every type OpenMarkets has ever emitted, retired ones included, with its status, family, strength semantics, evidence fields, required entitlements and typical TTL, and whether the key that asked will receive it. Ungated, so you can read it before deciding whether to buy signals:read.

Try it
GET/flow/v1/signals/types
Response.data
[
  {
    "signal_type": "arbitrage",
    "name": "Arbitrage",
    "family": "core",                    // core | premium
    "status": "active",                  // beta | active | deprecated | retired
    "schema_version": 1,
    "description": "…",
    "strength_semantics": "…",
    "evidence_fields": { "after_fee_roi": "…", "…": "…" },
    "required_entitlements": ["signals:read"],
    "typical_ttl_seconds": 120,
    "entitled": true,                    // for the key that asked
    "missing_entitlements": []           // codes this key lacks for the type
  }
]
GET/flow/v1/signals

The live feed. Filters compose as AND; contest_id and league_id together return the intersection. Beta types are excluded unless include_beta is set. Not read-cached, because the body depends on your entitlements. Requires signals:read.

NameInTypeRequiredDescription
contest_idquerystringnoComma-separated contest ids
league_idquerystringnoComma-separated league ids
market_idquerystringnoComma-separated market ids
position_hashquerystringnoComma-separated; also matches related_positions
typequerystringnoComma-separated signal types
include_betaqueryenumnoServe beta types too
min_strengthquerystringnoStrength floor, 0–100 (the same scale for every type)
min_confidencequeryenumno
sortqueryenumnostrength and detected_at descending; expires_at ascending
limitqueryintegernoMax 500. meta carries total, returned and contest_count
Try it
GET/flow/v1/signals?sort=strength&limit=100

Comma-separated contest ids

Comma-separated league ids

Comma-separated market ids

Comma-separated; also matches related_positions

Comma-separated signal types

Serve beta types too

Strength floor, 0–100 (the same scale for every type)

strength and detected_at descending; expires_at ascending

Max 500. meta carries total, returned and contest_count

Response.data
[
  {
    "signal_id": "sig_…",
    "dedupe_key": "arbitrage:ct_abc123:mk_ml:…",
    "signal_type": "arbitrage",
    "schema_version": 1,
    "status": "active",
    "position": {
      "position_hash": "ct_abc123:mk_ml:side_home:var_0:p_lal:tf_full",
      "router_contest_id": "ct_abc123",
      "router_league_id": "lg_nba",
      "router_market_id": "mk_ml",
      "market_key": "moneyline",
      "market_side_id": "side_home",
      "market_variable_id": "var_0",
      "participant_id": "p_lal",
      "timeframe_id": "tf_full",
      "title": "Lakers",
      "contest_label": "Lakers @ Rockets"
    },
    "related_positions": ["ct_abc123:mk_ml:side_away:var_0:p_hou:tf_full"],
    "headline": "1.2% after fees across Kalshi and ProphetX",
    "explanation": "…",
    "strength": 82,
    "confidence": "high",
    "calibration_version": 1,
    "detected_at": "2026-09-22T03:01:46.120Z",
    "expires_at": "2026-09-22T03:03:46.120Z",
    "evidence": {
      "arbitrage_id": "3f2a91c4-…",
      "best_combined_price": 0.968,
      "best_guaranteed_profit_pct": 3.31,
      "after_fee_roi": 1.18,
      "profitable_after_fees": true,
      "best_max_investment": 412.5,
      "venues": ["p_kalshi", "p_prophetx"]
    }
  }
]

Over the stream

channel: signal · signals:read + stream:realtime

Signals expire in seconds, so the WebSocket is the intended surface; the REST feed is for backfill and one-off queries. Subscribe on the signal channel with the same filters:

{ "action": "subscribe", "channel": "signal",
  "league_ids": ["lg_nba"],
  "types": ["arbitrage"],
  "min_confidence": "medium",
  "min_strength": 50,
  "include_beta": false }

Every filter is optional: a subscription with none receives signals for every contest, which is what a monitor wants. strength is 0–100 on every type. Filters are applied server-side per subscriber, so you only pay bandwidth for signals you would act on.

Server → client
{ "type": "signal.update",
  "contest_id": "ct_abc123",
  "league_id": "lg_nba",
  "signals": [ /* the same objects GET /flow/v1/signals returns */ ],
  "timestamp": "2026-09-22T03:01:46.180Z" }

Each event is a snapshot of that contest's current signals, not a delta: replace whatever you hold for contest_id. An empty signals array means the contest's signals cleared. Events are sent only when the set changes. When every signal in a change fails your filters you get nothing — except that a clear (an empty array) reaches every subscriber of the contest, so treat it as a no-op for a contest you hold nothing for. Still drop a signal yourself once its expires_at passes.

Everything else about the connection — the key in the query string, heartbeats, limits, the entitlement_required error — is on the Streaming page.

Acting on a signal

  • Detection is not advice. A signal reports a priced relationship and shows its evidence; venue rules, settlement-source differences and your own limits are yours to check.
  • Use the evidence, not just the strength. For arbitrage, trade off after_fee_roi and size to best_max_investment; the full legs are on GET /arbitrages by arbitrage_id.
  • Respect expires_at. A signal past it is history, whatever the feed still holds.

Next