3.8 News Materiality Trader
News Materiality Trader positions ahead of the book's full digestion of material news events. It receives news items from the NewsIngest pipeline (Reuters, AP, Bloomberg, league wires), scores them using a NLP materiality classifier, resolves the news entity to one or more watchlisted Polymarket markets via an entity-resolution dictionary, and emits an IOC OrderIntent when materiality_score >= materiality_threshold. A cooldown period (cooldown_s) prevents re-trading the same entity-market pair within the window. Order TTL (order_ttl_s) limits exposure if the IOC is not immediately filled. This is a user-controlled execution tool for reacting to material news before book equilibration; it does not predict resolution outcomes or make performance claims.
v3 readiness
A bot is done when all four scores are. What does done mean?
1. Bot Identity
| Layer | Strategy Strategy |
|---|---|
| Bot class | Alpha Strategy |
| Authority | Trade |
| Status | BETA |
| Readiness | Limited live |
| Runs before | Risk guardrail pipeline |
| Runs after | NewsIngest materiality scorer + entity-resolution |
| Applies to | Standard binary markets where a resolved entity match triggers a NLP materiality score >= materiality_threshold on incoming Reuters, AP, Bloomberg, or league wire events |
| Default mode | limited_live |
| User-visible | Advanced details only |
| Developer owner | Polytraders core — Strategy pod |
2. Purpose
News Materiality Trader positions ahead of the book's full digestion of material news events. It receives news items from the NewsIngest pipeline (Reuters, AP, Bloomberg, league wires), scores them using a NLP materiality classifier, resolves the news entity to one or more watchlisted Polymarket markets via an entity-resolution dictionary, and emits an IOC OrderIntent when materiality_score >= materiality_threshold. A cooldown period (cooldown_s) prevents re-trading the same entity-market pair within the window. Order TTL (order_ttl_s) limits exposure if the IOC is not immediately filled. This is a user-controlled execution tool for reacting to material news before book equilibration; it does not predict resolution outcomes or make performance claims.
3. Why This Bot Matters
Low-materiality news classified as high
The bot takes a position based on a rumour or corrected story, entering before the market corrects back, resulting in a loss when the initial price move reverses.
Entity resolution maps to wrong market
A news event about 'Team X wins championship' is resolved to a market about a different competition, causing a trade on an unrelated market.
Book depth insufficient for the news event
Filling the full IOC at the entry price depletes the top-of-book, causing significant slippage that overwhelms the news edge.
cooldown_s too short — re-trading the same event
Multiple news items from the same event (e.g., breaking news + confirmation + detail) trigger multiple entries on the same market before the book has repriced.
feeRateBps present on signed order (V1 pattern)
CTFExchangeV2 rejects orders with feeRateBps. Fees are operator-set at match time. The signed order must not contain this field.
No worked examples on this bot yet. Worked examples are optional but strongly recommended — they turn an abstract failure mode into something a developer can verify in a fixture.
4. Required Polymarket Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| Watchlisted markets matched by entity | internal (entity-resolution dictionary + Gamma API) | Yes | Map the news event's entity to one or more open Polymarket markets. Only markets in the watchlist are eligible. |
| Book depth and recent volatility on the target market | clob_public + ws_market | Yes | Size the IOC order to min(available_depth, max_position_usd) and check recent volatility hasn't already absorbed the news. |
| Market open/closed/resolved status | clob_public | Yes | Skip markets that are closed, resolved, or approaching resolution (< 30 min to close). |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| KillSwitch active flag | KillSwitch | Yes | Abort all intent emission immediately if KillSwitch is active. |
| NewsIngest materiality score + entity match + news item | internal (NewsIngest pipeline) | Yes | Trigger trade when materiality_score >= materiality_threshold and entity resolves to an open watchlist market. |
| NLP materiality classifier | internal | Yes | Score each news item on a 0–1 scale for market-moving impact on the resolved entity's Polymarket market. |
| Entity-resolution dictionary | internal | Yes | Map news entities (persons, teams, countries, companies) to Polymarket condition IDs. |
| Builder code bytes32 | internal config | Yes | Injected into builder field on every signed V2 OrderIntent. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| materiality_threshold | 0.72 | 0.55 | 0.4 | Minimum NLP materiality score (0–1) required before emitting an OrderIntent on a matched market. |
| cooldown_s | 120 | 45 | 20 | Minimum seconds between trades on the same entity-market pair. Prevents re-entering on follow-up coverage of the same event. |
| order_ttl_s | 90 | 200 | 300 | Maximum seconds the IOC order's intent is considered valid. If not filled within this window, the trade is abandoned (not re-sent). |
| max_position_usd | 300 | 500 | 750 | Maximum pUSD size per news-triggered IOC order. |
7. Detailed Parameter Instructions
materiality_threshold
What it means
Minimum NLP materiality score (0–1) required before emitting an OrderIntent on a matched market.
Default
{ "materiality_threshold": 0.72 }
Why this default matters
0.72 filters out speculation and soft news, targeting only high-confidence material events. Below 0.55 the classifier confidence is marginal; below 0.40 the bot will not trade regardless of config.
Threshold logic
| Condition | Action |
|---|---|
| >= 0.72 | EMIT IOC OrderIntent |
| 0.55–0.72 | WARN NEWS_MATERIALITY_SCORE_MARGINAL; emit at 50% size |
| < 0.40 (hard floor) | SKIP — NEWS_MATERIALITY_TOO_LOW |
Developer check
if score < params.hard: return skip('NEWS_MATERIALITY_TOO_LOW')
User-facing English
The news item did not score highly enough on the materiality classifier to justify a trade.
cooldown_s
What it means
Minimum seconds between trades on the same entity-market pair. Prevents re-entering on follow-up coverage of the same event.
Default
{ "cooldown_s": 120 }
Why this default matters
120 seconds ensures the book has time to partially reprice after the first trade before a second entry. Below 45s the book may not have moved at all; below 20s the bot will not trade regardless of config.
Threshold logic
| Condition | Action |
|---|---|
| >= 120s | Normal cooldown |
| 45–120s | WARN NEWS_MATERIALITY_SHORT_COOLDOWN; may re-trade same event |
| < 20s (hard floor) | Reject config — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if params.cooldown_s < params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
User-facing English
— not yet authored —
order_ttl_s
What it means
Maximum seconds the IOC order's intent is considered valid. If not filled within this window, the trade is abandoned (not re-sent).
Default
{ "order_ttl_s": 90 }
Why this default matters
90 seconds limits news-edge exposure to the window where the event is still not fully priced in. Beyond 200s the news is likely fully digested and the edge is gone.
Threshold logic
| Condition | Action |
|---|---|
| <= 90s | Normal TTL window |
| 90–300s | WARN NEWS_MATERIALITY_LONG_TTL; news likely digested before fill |
| > 300s (hard ceiling) | Reject config — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if params.order_ttl_s > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
User-facing English
The news trade window has passed. The order was not placed as the opportunity may no longer be valid.
max_position_usd
What it means
Maximum pUSD size per news-triggered IOC order.
Default
{ "max_position_usd": 300 }
Why this default matters
300 pUSD per event limits single-news exposure. Above 500 pUSD the order may consume most of the top-of-book depth and cause significant slippage.
Threshold logic
| Condition | Action |
|---|---|
| <= 300 pUSD | Normal size |
| 300–750 pUSD | WARN; confirm depth supports order; slippage risk elevated |
| > 750 pUSD | Reject config — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if params.max_position_usd > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
User-facing English
The news trade was sized to fit within available market liquidity.
8. Default Configuration
{
"bot_id": "strat.news_materiality_trader",
"version": "2.1.0",
"mode": "limited_live",
"defaults": {
"materiality_threshold": 0.72,
"cooldown_s": 120,
"order_ttl_s": 90,
"max_position_usd": 300
},
"locked": {
"materiality_threshold": {
"min": 0.4
},
"cooldown_s": {
"min": 20
},
"order_ttl_s": {
"max": 300
},
"max_position_usd": {
"max": 750
}
}
}9. Implementation Flow
- Check KillSwitch active flag; if active, emit no OrderIntents.
- Subscribe to NewsIngest internal bus for materiality-scored news events.
- On each news event: check materiality_score >= materiality_threshold hard floor (0.40); if below, skip NEWS_MATERIALITY_TOO_LOW (sampled 1/100).
- Run entity-resolution: map news entity to Polymarket condition ID(s) via entity-resolution dictionary. If no match, skip NEWS_MATERIALITY_NO_MARKET_MATCH.
- For each matched market: confirm market is open and not approaching resolution (< 30 min to close) via clob_public.
- Check cooldown: if last trade on this entity-market pair was < cooldown_s seconds ago, skip NEWS_MATERIALITY_COOLDOWN_ACTIVE.
- If materiality_score < warning threshold (0.72): WARN NEWS_MATERIALITY_SCORE_MARGINAL; reduce size to 50% of max_position_usd.
- Fetch top-of-book depth from clob_public; set orderSize = min(depth, max_position_usd) * sizeMultiplier.
- Check recent market volatility (ws_market): if price has already moved > 50% of expected news impact, skip NEWS_MATERIALITY_ALREADY_DIGESTED.
- Emit OrderIntent: outcome=YES (or NO depending on event direction), side=buy, price=best_ask, size_pUSD=orderSize, tif=IOC, builder={code, fee_bps:25}.
- Note: fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order.
- Register cooldown state: last_trade_ms for entity-market pair.
- Emit DecisionReport with intent_emitted=true, materiality_score, entity_id, market_id, reason NEWS_MATERIALITY_TRADE_TRIGGERED.
10. Reference Implementation
Consumes news events from the NewsIngest internal bus, scores materiality via NLP classifier, resolves entities to open Polymarket markets, and emits IOC OrderIntents when materiality exceeds threshold and cooldown has elapsed.
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.
FUNCTION onNewsEvent(newsItem):
// --- 0. KillSwitch gate ---
ks = FETCH internal.killswitch.status
IF ks.active: RETURN
// --- 1. Materiality score gate ---
score = newsItem.materiality_score // from NLP classifier
IF score < params.materiality_threshold_hard: // 0.40
IF random() < 0.01:
EMIT DecisionReport(intent_emitted=false, reason='NEWS_MATERIALITY_TOO_LOW', score=score)
RETURN
// --- 2. Entity resolution ---
markets = FETCH internal.entityDict.resolve(newsItem.entity_id)
IF NOT markets:
EMIT DecisionReport(intent_emitted=false, reason='NEWS_MATERIALITY_NO_MARKET_MATCH')
RETURN
FOR market_id IN markets:
// --- 3. Market status check ---
mkt = FETCH clob_public.GET('/markets/' + market_id)
IF mkt.closed OR mkt.resolved OR mkt.minutes_to_close < 30:
CONTINUE
// --- 4. Cooldown check ---
lastTrade = FETCH state.lastTradeMs(newsItem.entity_id, market_id)
IF (now_ms() - lastTrade) < params.cooldown_s * 1000:
EMIT DecisionReport(intent_emitted=false, reason='NEWS_MATERIALITY_COOLDOWN_ACTIVE')
CONTINUE
// --- 5. Already-digested check ---
recentMove = FETCH ws_market.priceChangeSinceMs(market_id, newsItem.received_at_ms)
IF recentMove > 0.5 * expectedNewsImpact(score):
EMIT DecisionReport(intent_emitted=false, reason='NEWS_MATERIALITY_ALREADY_DIGESTED')
CONTINUE
// --- 6. Size determination ---
sizeMultiplier = 0.5 IF score < params.materiality_threshold ELSE 1.0
IF sizeMultiplier < 1.0: WARN('NEWS_MATERIALITY_SCORE_MARGINAL')
depth = FETCH clob_public.depth(market_id, side='YES')
orderSize = toPusdUnits(min(depth, params.max_position_usd) * sizeMultiplier)
// --- 7. Emit IOC OrderIntent (V2: no feeRateBps) ---
side = 'buy' IF newsItem.direction == 'positive' ELSE 'sell'
EMIT OrderIntent(
market_id = market_id, outcome = 'YES', side = side,
price = mkt.best_ask, size_pUSD = orderSize, tif = 'IOC', post_only = false,
builder = {code: internal.builder_code, fee_bps: 25}
)
// --- 8. Register cooldown ---
SET state.lastTradeMs(newsItem.entity_id, market_id, now_ms())
EMIT DecisionReport(intent_emitted=true, materiality_score=score,
entity_id=newsItem.entity_id, market_id=market_id,
reason='NEWS_MATERIALITY_TRADE_TRIGGERED')
SDK calls used
fetchClobPublic('/markets/' + market_id)ws_market.subscribe('book', [market_id])internal.entityDict.resolve(entity_id)internal.newsingest.subscribe()internal.killswitch.status()toPusdUnits(rawFloat)buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })internal.builder_code
Complexity: O(n_markets_matched) per news event; typically O(1) for single-entity events
11. Wire Examples
Input — what arrives on the wire
NewsIngest event — high materiality, entity resolved — internal (NewsIngest bus)
{
"event_id": "news_01HXNMT000EVENT",
"entity_id": "entity_candidate_A_primary",
"headline": "Candidate A wins state primary by large margin",
"source": "Reuters",
"materiality_score": 0.81,
"direction": "positive",
"matched_market_ids": [
"0xnewsmat000000000000000000000000000000000000000000000000000000001"
],
"received_at_ms": 1746790600000
}
Output — what the bot emits
OrderIntent — news IOC buy YES (builder-attributed)
{
"intent_id": "oi_01HXNMT0000001A",
"trace_id": "tr_01HXNMT000TR001",
"market_id": "0xnewsmat000000000000000000000000000000000000000000000000000000001",
"outcome": "YES",
"side": "buy",
"price": "0.438",
"size_pUSD": "300.00",
"tif": "IOC",
"post_only": false,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 25
},
"negrisk_aware": false,
"decision": {
"materiality_score": 0.81,
"entity_id": "entity_candidate_A_primary",
"news_source": "Reuters",
"reasons": [
"NEWS_MATERIALITY_TRADE_TRIGGERED"
]
},
"comment": "fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order"
}
DecisionReport — skipped (score too low), sampled 1/100
{
"report_id": "dr_01HXNMT999ZZZZ",
"bot_id": "strat.news_materiality_trader",
"entity_id": "entity_candidate_B",
"intent_emitted": false,
"materiality_score": 0.28,
"reasons": [
"NEWS_MATERIALITY_TOO_LOW"
],
"sampled": true,
"evaluated_at_ms": 1746790601000
}12. Decision Logic
APPROVE
materiality_score >= materiality_threshold, entity resolves to open market, cooldown elapsed, depth sufficient, KillSwitch inactive. Emit IOC OrderIntent.
RESHAPE_REQUIRED
Not applicable — strat bots emit OrderIntents; reshaping is handled by the downstream Risk guardrail pipeline.
REJECT
score < 0.40 hard floor; no entity match; cooldown active; market closed; KillSwitch active; stale feed. Emit DecisionReport intent_emitted=false.
WARNING_ONLY
score between 0.40 and 0.72 triggers NEWS_MATERIALITY_SCORE_MARGINAL and 50% size reduction.
13. Standard Decision Output
This bot returns a OrderIntent object. See OrderIntent schema.
{
"intent_id": "oi_01HXNMT0000001A",
"trace_id": "tr_01HXNMT000TR001",
"market_id": "0xnewsmat000000000000000000000000000000000000000000000000000000001",
"outcome": "YES",
"side": "buy",
"price": "0.438",
"size_pUSD": "300.00",
"tif": "IOC",
"post_only": false,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 25
},
"negrisk_aware": false,
"decision": {
"materiality_score": 0.81,
"entity_id": "entity_candidate_A_primary",
"news_source": "Reuters",
"reasons": [
"NEWS_MATERIALITY_TRADE_TRIGGERED"
]
},
"comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
}14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
NEWS_MATERIALITY_TRADE_TRIGGERED | INFO | materiality_score >= threshold, entity resolved, cooldown cleared, market open. IOC OrderIntent emitted. | Emit IOC OrderIntent; register cooldown. | A material news event was detected and a trade was placed to capture the expected price move. |
NEWS_MATERIALITY_TOO_LOW | INFO | materiality_score is below the 0.40 hard floor. News is not material enough to trade. | Skip; emit sampled DecisionReport. | The news item was not rated as material enough to justify a trade. |
NEWS_MATERIALITY_SCORE_MARGINAL | WARN | materiality_score between 0.40 and 0.72. Trade is marginal; size reduced to 50%. | Emit IOC at 50% size; log warning. | A moderately material news event was detected. A smaller trade was placed. |
NEWS_MATERIALITY_NO_MARKET_MATCH | INFO | News entity did not resolve to any open watchlist market in the entity-resolution dictionary. | Skip; emit DecisionReport. | |
NEWS_MATERIALITY_COOLDOWN_ACTIVE | INFO | Last trade on this entity-market pair was within cooldown_s seconds. | Skip; emit DecisionReport. | A trade was placed recently for this news entity. The cooldown period must pass before another trade. |
NEWS_MATERIALITY_ALREADY_DIGESTED | INFO | Market price has already moved significantly since the news event; likely already digested. | Skip; emit DecisionReport. | The market appears to have already reacted to this news. No trade was placed. |
NEWS_MATERIALITY_SHORT_COOLDOWN | WARN | cooldown_s config is below the 45s warning threshold. Re-trading same event risk elevated. | Allow but log warning. | |
STALE_MARKET_DATA | HARD_REJECT | clob_public or ws_market data is stale (> 5s) for target market. | Skip; no OrderIntent emitted. | Market data was too old to act on safely. |
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch is active. | Skip all news events; no OrderIntents emitted. | Trading is currently paused. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
polytraders_strat_newsmateriality_decisions_total | counter | count | verdict, reason_code | Total news events evaluated by intent_emitted and reason code. |
polytraders_strat_newsmateriality_score | histogram | score | Distribution of NLP materiality scores across all evaluated news events. | |
polytraders_strat_newsmateriality_intents_emitted_total | counter | count | news_source, entity_category | Total IOC OrderIntents emitted by news source and entity category. |
polytraders_strat_newsmateriality_cooldown_blocks_total | counter | count | entity_id | Number of trades blocked by active cooldown per entity. |
polytraders_strat_newsmateriality_eval_latency_ms | histogram | milliseconds | Wall-clock time from news event receipt to OrderIntent emit. | |
polytraders_strat_newsmateriality_digested_skips_total | counter | count | market_id | News events skipped because market had already priced the news. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|---|---|---|
NewsMaterialityHighLatency | histogram_quantile(0.99, rate(polytraders_strat_newsmateriality_eval_latency_ms_bucket[5m])) > 500 | warn | #runbook-newsmateriality-latency |
NewsMaterialityHighDigestedRate | rate(polytraders_strat_newsmateriality_digested_skips_total[10m]) / rate(polytraders_strat_newsmateriality_decisions_total[10m]) > 0.6 | warn | #runbook-newsmateriality-digested |
NewsMaterialityStaleFeed | rate(polytraders_strat_newsmateriality_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1 | warn | #runbook-newsmateriality-stale-feed |
NewsMaterialityKillSwitchBlocking | rate(polytraders_strat_newsmateriality_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0 | page | #runbook-killswitch |
Dashboards
- Grafana — Strategy / NewsMateriality score distribution and trade frequency
- Grafana — Strategy / NewsMateriality entity resolution hit rate and cooldown blocks
16. Developer Reporting
{
"bot_id": "strat.news_materiality_trader",
"market_id": "0xnewsmat000000000000000000000000000000000000000000000000000000001",
"materiality_score": 0.81,
"entity_id": "entity_candidate_A_primary",
"news_source": "Reuters",
"order_size_pusd": 300.0,
"price_at_entry": 0.438,
"cooldown_cleared": true,
"depth_pusd": 520.0,
"intent_emitted": true,
"reason": "NEWS_MATERIALITY_TRADE_TRIGGERED",
"emitted_at_ms": 1746790600000
}17. Plain-English Reporting
| Situation | User-facing explanation |
|---|---|
| News-triggered trade placed | A material news event was detected and matched to this market. An order was placed to position ahead of the expected book repricing. |
| News scored too low — no trade | The news item did not rate highly enough on the materiality classifier to justify a trade. No order was placed. |
| Cooldown active — no trade | A trade was placed recently on the same market for the same news entity. The cooldown period must elapse before another trade is considered. |
| News already digested — no trade | The market price had already moved significantly before the order was placed, suggesting the news is already priced in. No order was placed to avoid chasing the move. |
18. Failure-Mode Block
| main_failure_mode | False materiality classification: the NLP classifier scores a rumour or corrected story as material, triggering a trade that reverses when the story is clarified, producing a loss. |
|---|---|
| false_positive_risk | High materiality score on irrelevant news that shares entity keywords with a target market (e.g., 'Team X wins match' when the market is about a different competition). |
| false_negative_risk | materiality_threshold set too high misses genuine market-moving events, especially fast-breaking sports results where the classifier has less training data. |
| safe_fallback | If NewsIngest feed is unavailable or clob_public market data is stale (> 5s), emit STALE_MARKET_DATA DecisionReport and skip without emitting any OrderIntent. Fail closed on feed unavailability. |
| required_dependencies | NewsIngest internal feed (Reuters, AP, Bloomberg, league wires + NLP classifier), Entity-resolution dictionary, clob_public market endpoint (status + depth), ws_market (recent price volatility check), KillSwitch active flag, internal builder code |
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
LOW_MATERIALITY_NEWS | Send mock news event with materiality_score=0.25 (< 0.40 hard floor) | Automatic; next qualifying news event can fire. | |
COOLDOWN_ACTIVE | Set state.lastTradeMs = now_ms() - 30s (< cooldown_s=120) | Automatic when cooldown_s elapses. | |
NEWS_ALREADY_DIGESTED | Set mock price change since news to > 50% of expected impact | Automatic; subsequent news events on same market can trade. | |
NLP_CLASSIFIER_UNAVAILABLE | Take NLP classifier offline | Automatic when classifier restores. | |
KILL_SWITCH_ON | Set killswitch.active=true | Automatic on manual KillSwitch reset. |
20. State & Persistence
Cold-start recovery
On cold start, cooldown state is cleared (conservative: may re-trade within cooldown on restart). Price state rebuilt from first ws_market tick.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|---|
| Execution model | single-threaded event loop |
| Max in-flight | 20 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 300 |
| Backpressure strategy | drop oldest pending news event per entity when queue depth > 3 |
| Locking / mutual exclusion | per entity_id+market_id mutex for cooldown state read/write |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|---|---|
| risk.kill_switch | Checked first; blocks all intent emission when active. |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|---|---|
| risk.portfolio_guard | ||
| gov.builder_attribution |
External services
| Service | Endpoint | SLA assumed | On failure |
|---|---|---|---|
| Polymarket CLOB public API | 99.9% (Polymarket-published) | ||
| Polymarket CLOB WebSocket (ws_market) | best-effort | ||
| NewsIngest internal feed (Reuters, AP, Bloomberg, league wires) | internal SLA | ||
| NLP materiality classifier (internal) | internal SLA |
23. Security Surfaces
On-chain contract calls
| Contract | Method | Network | Effect |
|---|---|---|---|
CTFExchangeV2 | | polygon |
Abuse vectors considered
- Injecting synthetic news into the NewsIngest feed to trigger trades on specific markets
- Adversary front-runs the bot by monitoring entity-resolution patterns to predict which markets will be traded on material news
- Cooldown manipulation: flooding the system with low-score news to exhaust entity-market pair cooldown windows
Mitigations
- NewsIngest feed is authenticated and sourced only from verified news providers; synthetic injection requires compromising the internal feed
- IOC orders do not rest on the book; adversary cannot exploit known bid/ask placement
- Hard materiality floor (0.40) prevents cooldown exhaustion via noise events
- V2 order timestamp(ms) invalidates replayed signed orders
24. Polymarket V2 Compatibility
| Aspect | Value |
|---|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | no |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | Executes taker IOC orders on standard binary markets when a material news event is scored above materiality_threshold by the NLP classifier. feeRateBps is not present on any signed order. Builder code injected on every intent. |
API surfaces declared
Networks supported
25. Versioning & Migration
| Field | Value |
|---|---|
| spec | 2.0.0 |
| implementation | 2.1.0 |
| schema | 2 |
| released | 2026-04-28 |
Migration history
| Date | From | To | Reason | Action taken |
|---|---|---|---|---|
| 2026-04-28 | v1 (USDC.e, feeRateBps on signed order) | v2 (pUSD, fees operator-set at match time) | CLOB V2 cutover | Switched to py-clob-client-v2. Removed feeRateBps from all signed order construction. Updated collateral to pUSD. Injected builder field (bytes32) on every OrderIntent. EIP-712 Exchange domain version updated from '1' to '2'. NewsIngest pipeline updated to V2 internal bus schema. Entity-resolution dictionary updated for 2026 market coverage. |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|---|---|
| Emit IOC when materiality_score=0.81, entity resolves, cooldown cleared | score=0.81, threshold=0.72, cooldown elapsed, market open, depth=520 | IOC OrderIntent emitted; DecisionReport intent_emitted=true, reason=NEWS_MATERIALITY_TRADE_TRIGGERED |
| Skip when score < hard floor (0.40) | materiality_score=0.35 | No OrderIntent; sampled DecisionReport reason=NEWS_MATERIALITY_TOO_LOW |
| Skip when cooldown active | last_trade_ms = now_ms() - 30s (cooldown_s=120) | No OrderIntent; reason=NEWS_MATERIALITY_COOLDOWN_ACTIVE |
| Reduce size 50% when score marginal (0.60) | score=0.60, threshold=0.72, max_position_usd=300 | OrderIntent emitted with size=150; WARN NEWS_MATERIALITY_SCORE_MARGINAL |
| Skip when no entity match in dictionary | news entity not in entity-resolution dictionary | No OrderIntent; reason=NEWS_MATERIALITY_NO_MARKET_MATCH |
| Skip when KillSwitch active | killswitch.active=true | No OrderIntents emitted |
Integration Tests
| Test | Expected result |
|---|---|
| Full cycle: news event arrives → NLP scores → entity resolved → IOC OrderIntent submitted | Order has builder.code (bytes32), no feeRateBps, tif=IOC, EIP-712 domain version '2' |
| Cooldown correctly prevents re-trading same entity-market pair within cooldown_s | Second intent blocked for cooldown_s; emitted after cooldown elapsed |
Property Tests
| Property | Required behaviour |
|---|---|
| Bot never trades a market without a confirmed entity match in the resolution dictionary | Always true |
| feeRateBps never present on any signed OrderIntent | Always true — V2 fees are operator-set at match time |
| Cooldown always enforced per entity-market pair; no two intents within cooldown_s on same pair | Always true |
27. Operational Runbook
News Materiality Trader incidents are typically high already-digested rates (slow news pipeline latency), NLP classifier outages (blocking all trades), or false-materiality classifications causing unexpected positions. Classifier outages fail-closed and resolve automatically.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|---|---|---|---|
NewsMaterialityHighLatency | ||||
NewsMaterialityHighDigestedRate | ||||
NewsMaterialityStaleFeed | ||||
NewsMaterialityKillSwitchBlocking |
Manual overrides
——
Healthcheck
GET /internal/health/news-materiality-trader -> 200 if NewsIngest feed live, NLP classifier reachable, clob_public reachable, KillSwitch inactive, at least one news event evaluated in last 10 min.28. Promotion Gates
A bot does not advance to the next readiness state until every gate below is green. Gates are observable from production data — no subjective sign-off.
Promote to Shadow
| Gate | How measured | Threshold |
|---|---|---|
| All unit tests pass including cooldown invariant and entity-resolution gate | CI test run | 100% pass |
| feeRateBps absence verified; tif=IOC verified in integration test | Integration test asserting V2 order schema | Pass |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| p99 eval latency < 300ms over 24h (from news event receipt to OrderIntent emit) | polytraders_strat_newsmateriality_eval_latency_ms histogram | p99 < 300ms |
| Already-digested rate < 40% of qualifying events over 48h shadow run | polytraders_strat_newsmateriality_digested_skips_total / decisions_total | < 40% |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| E2E: news event arrives → NLP scores → entity resolved → IOC OrderIntent submitted on Polygon testnet | E2E test | Pass |
| Cooldown enforcement verified: same entity-market pair not re-traded within cooldown_s in integration test | Integration test | Pass |
29. Developer Checklist
Ready-to-ship score: 27/27 sections complete · 100%
| Requirement | Status |
|---|---|
| Purpose defined | ✓ done |
| Required inputs listed | ✓ done |
| Parameters defined | ✓ done |
| Defaults defined | ✓ done |
| Warning thresholds defined | ✓ done |
| Hard thresholds defined | ✓ done |
| Safe fallback defined | ✓ done |
| Structured output defined | ✓ done |
| Developer log defined | ✓ done |
| Plain-English explanation | ✓ done |
| Unit tests defined | ✓ done |
| Integration tests defined | ✓ done |
| Property tests defined | ✓ done |
| Failure-mode block complete | ✓ done |
| Reference implementation pseudocode | ✓ done |
| Wire examples (input + output) | ✓ done |
| Reason codes listed | ✓ done |
| Metrics & logs defined | ✓ done |
| State & persistence defined | ✓ done |
| Concurrency & idempotency defined | ✓ done |
| Dependencies declared | ✓ done |
| Security surfaces declared | ✓ done |
| Polymarket V2 compatibility declared | ✓ done |
| Version & migration history declared | ✓ done |
| Operational runbook defined | ✓ done |
| Promotion gates defined | ✓ done |
| Failure-injection recipes defined | ✓ done |