Polytraders Dev Guide
internal
v3 spine Phase 1 · Shared contracts 9 demo-wired · 0 shadow-ready · 0 production-live · 100 pending · 109 total 15/33 infra tasks the plan status board
HomeBy LayerRisk1.7 BlacklistKeeper

1.7 BlacklistKeeper

Risk Guardrail RejectReshape BETA Limited live capital · Direct P4 · Core risk pending stub

BlacklistKeeper maintains a dual-registry of banned market condition IDs and banned counterparty wallet addresses. On every OrderIntent it checks both registries and hard-rejects any intent whose target market or counterparty appears on either list. The bot also monitors for ambiguity signals (undefined resolution sources, prior dispute history, time-to-resolution below threshold) and rejects structurally hostile markets before execution. It is fail-closed: if the registry cannot be read, the intent is rejected.

v3 readiness

Docs27/27
donehow scored
Impl0/15
pendinghow scored
Backtest0/4
pendinghow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

1. Bot Identity

LayerRisk  Risk
Bot classGuardrail
AuthorityRejectReshape
StatusBETA
ReadinessLimited live
Runs beforeExecutionPlan emit
Runs afterStrategy OrderIntent
Applies toEvery OrderIntent — screens the target market and counterparty wallet against operator-maintained banned-market and banned-counterparty lists
Default modelimited_live
User-visiblesummary-only
Developer ownerPolytraders core — Risk pod

Operational profile

Modes supportedquarantine

2. Purpose

BlacklistKeeper maintains a dual-registry of banned market condition IDs and banned counterparty wallet addresses. On every OrderIntent it checks both registries and hard-rejects any intent whose target market or counterparty appears on either list. The bot also monitors for ambiguity signals (undefined resolution sources, prior dispute history, time-to-resolution below threshold) and rejects structurally hostile markets before execution. It is fail-closed: if the registry cannot be read, the intent is rejected.

3. Why This Bot Matters

  • Intent routed to a banned market

    Trading a market that has been operator-banned (due to ambiguous resolution rules, prior disputes, or platform policy) exposes the system to unresolvable settlement risk and potential fund loss.

  • Order matched against a banned counterparty wallet

    Engaging with a blacklisted counterparty (e.g. a wallet flagged for manipulation, wash-trading, or prior sanctions escalation) introduces regulatory exposure and may contaminate position records.

  • Market with ambiguous resolution rules passes through

    Markets using vague trigger words (“substantial”, “primary”) or undefined resolution sources frequently go to UMA dispute. Each dispute costs a $750 pUSD bond and 24–48 h of DVM voting delay, eroding capital efficiency.

  • Single-source resolution market accepted

    A market that resolves on a single oracle feed is vulnerable to feed manipulation or temporary outage; the bot blocks these when block_single_source is enabled.

  • Fail-open on registry outage

    If BlacklistKeeper approves orders when the registry is unavailable, banned markets and counterparties can trade undetected. The bot must never approve when its data source is unreachable.

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

InputSourceRequired?Use
Gamma API market metadata — resolution rules text, single-source flag, time-to-resolution, negRisk flag, prior dispute historygammaYesEvaluate market for ambiguity keywords, single-source resolution, time-to-resolution threshold, and structural hostility signals.

5. Required Internal Inputs

InputSourceRequired?Use
Banned-market registry — operator-curated list of banned condition_idsinternalYesHard-reject any intent whose market_id appears in the banned-market set.
Banned-counterparty registry — operator-curated list of banned wallet addressesinternalYesHard-reject any intent whose counterparty wallet appears in the banned-counterparty set.
Community / shared dispute history feedinternalNoSupplement the operator-curated list with cross-platform dispute signals to identify markets with a track record of resolution failure.
KillSwitch active flagKillSwitchYesIf KillSwitch is active, reject all orders immediately without consulting the registry.
ObservationReport streaminternalNoConsume ObservationReports from intel/disc bots to trigger registry additions when new market anomalies are detected.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
blacklisted_condition_ids[]NoneTrueExplicit list of Polymarket condition IDs (32-byte hex) that are banned from trading. Any OrderIntent targeting one of these markets is hard-rejected immediately.
blacklisted_counterparties[]NoneTrueExplicit list of wallet addresses banned as counterparties. Any OrderIntent whose counterparty field matches an entry is hard-rejected.
min_hours_to_resolution242Minimum hours remaining before market resolution. Markets resolving within this window are treated as structurally hostile and rejected. Warning threshold fires at < 4h to give time to manage positions.
block_single_sourceTrueNoneTrueWhen true, markets that resolve on a single data source (no multi-oracle fallback) are rejected to protect against feed manipulation and outage risk.
ambiguity_keywords['substantial', 'primary', 'significant', 'material', 'reasonable']['substantial', 'primary']['substantial', 'primary', 'significant', 'material', 'reasonable']List of keywords whose presence in market resolution rules text triggers an ambiguity check. If any keyword is found, the market is flagged as structurally ambiguous. Warning fires for the minimal 2-keyword set; hard block fires for any match.

7. Detailed Parameter Instructions

blacklisted_condition_ids

What it means

Explicit list of Polymarket condition IDs (32-byte hex) that are banned from trading. Any OrderIntent targeting one of these markets is hard-rejected immediately.

Default

{ "blacklisted_condition_ids": [] }

Why this default matters

Defaults to empty; operator must populate. The registry is the primary enforcement mechanism and must be kept current by the risk team.

Threshold logic

ConditionAction
intent.market_id in blacklisted_condition_idsREJECT — BLACKLIST_KEEPER_MARKET_BANNED
intent.market_id not in blacklisted_condition_idsAPPROVE (this check)

Developer check

if (blacklisted_condition_ids.includes(intent.market_id)) return reject('BLACKLIST_KEEPER_MARKET_BANNED');

User-facing English

This market is not available for trading on this platform.

blacklisted_counterparties

What it means

Explicit list of wallet addresses banned as counterparties. Any OrderIntent whose counterparty field matches an entry is hard-rejected.

Default

{ "blacklisted_counterparties": [] }

Why this default matters

Defaults to empty; populated by the risk team when manipulation, wash-trading, or compliance flags are identified on specific wallets.

Threshold logic

ConditionAction
intent.counterparty in blacklisted_counterpartiesREJECT — BLACKLIST_KEEPER_COUNTERPARTY_BANNED
intent.counterparty not in blacklisted_counterpartiesAPPROVE (this check)

Developer check

if (blacklisted_counterparties.includes(intent.counterparty)) return reject('BLACKLIST_KEEPER_COUNTERPARTY_BANNED');

User-facing English

This transaction cannot be completed due to a platform restriction.

min_hours_to_resolution

What it means

Minimum hours remaining before market resolution. Markets resolving within this window are treated as structurally hostile and rejected. Warning threshold fires at < 4h to give time to manage positions.

Default

{ "min_hours_to_resolution": 2 }

Why this default matters

Markets resolving in < 2 h give insufficient time for the UMA 2-hour challenge window, meaning a disputed resolution cannot be challenged before settlement. Hard floor at 2 h mirrors the UMA optimistic oracle challenge period.

Threshold logic

ConditionAction
hours_to_resolution >= 4APPROVE (this check)
hours_to_resolution >= 2 and < 4WARN — BLACKLIST_KEEPER_NEAR_RESOLUTION
hours_to_resolution < 2REJECT — BLACKLIST_KEEPER_NEAR_RESOLUTION (hard)

Developer check

const htr = (market.end_date_iso - now()) / 3600000; if (htr < params.min_hours_to_resolution) return reject('BLACKLIST_KEEPER_NEAR_RESOLUTION');

User-facing English

This market is too close to resolution to accept new orders.

block_single_source

What it means

When true, markets that resolve on a single data source (no multi-oracle fallback) are rejected to protect against feed manipulation and outage risk.

Default

{ "block_single_source": true }

Why this default matters

Single-source markets are inherently manipulable. Defaulting to block is the conservative posture; operators may disable for specific market categories with documented rationale.

Threshold logic

ConditionAction
block_single_source = true AND market.single_source = trueREJECT — BLACKLIST_KEEPER_SINGLE_SOURCE
block_single_source = false OR market.single_source = falseAPPROVE (this check)

Developer check

if (params.block_single_source && market.single_source) return reject('BLACKLIST_KEEPER_SINGLE_SOURCE');

User-facing English

This market cannot be traded due to its resolution source configuration.

ambiguity_keywords

What it means

List of keywords whose presence in market resolution rules text triggers an ambiguity check. If any keyword is found, the market is flagged as structurally ambiguous. Warning fires for the minimal 2-keyword set; hard block fires for any match.

Default

{ "ambiguity_keywords": ["substantial", "primary", "significant", "material", "reasonable"] }

Why this default matters

Markets with vague resolution language have historically high dispute rates on Polymarket. Blocking them at intent time avoids the $750 pUSD UMA bond cost and 24–48 h DVM delay.

Threshold logic

ConditionAction
no keyword found in resolution_rulesAPPROVE (this check)
keyword found AND configured list < 5 keywordsWARN — BLACKLIST_KEEPER_AMBIGUOUS_RULES
keyword found AND configured list >= 5 keywordsREJECT — BLACKLIST_KEEPER_AMBIGUOUS_RULES (hard)

Developer check

const hit = params.ambiguity_keywords.find(kw => market.resolution_rules.includes(kw)); if (hit) return reject('BLACKLIST_KEEPER_AMBIGUOUS_RULES');

User-facing English

This market has ambiguous resolution rules and is not available for trading.

8. Default Configuration

{
  "bot_id": "risk.blacklist_keeper",
  "version": "2.0.0",
  "mode": "hard_guard",
  "defaults": {
    "blacklisted_condition_ids": [],
    "blacklisted_counterparties": [],
    "min_hours_to_resolution": 2,
    "block_single_source": true,
    "ambiguity_keywords": [
      "substantial",
      "primary",
      "significant",
      "material",
      "reasonable"
    ]
  },
  "locked": {
    "min_hours_to_resolution": {
      "min": 2
    },
    "ambiguity_keywords": {
      "min_length": 2
    }
  }
}

9. Implementation Flow

  1. Receive OrderIntent from Strategy layer including market_id, counterparty wallet, and intent metadata.
  2. Check KillSwitch active flag; if active, return REJECT with KILL_SWITCH_ACTIVE immediately.
  3. Look up intent.market_id in the banned-market registry (Redis SET). On any match, return HARD_REJECT with BLACKLIST_KEEPER_MARKET_BANNED. If registry is unavailable, return HARD_REJECT with BLACKLIST_KEEPER_DATA_UNAVAILABLE (fail-closed).
  4. Look up intent.counterparty in the banned-counterparty registry (Redis SET). On any match, return HARD_REJECT with BLACKLIST_KEEPER_COUNTERPARTY_BANNED.
  5. Fetch target market metadata from Gamma API: resolution_rules text, single_source flag, end_date_iso, and prior dispute history.
  6. If Gamma fetch fails or returns stale data (age > 300s), return HARD_REJECT with BLACKLIST_KEEPER_DATA_UNAVAILABLE.
  7. Compute hours_to_resolution = (market.end_date_iso - now_ms()) / 3600000. If hours_to_resolution < params.min_hours_to_resolution, return HARD_REJECT with BLACKLIST_KEEPER_NEAR_RESOLUTION.
  8. If params.block_single_source = true and market.single_source = true, return HARD_REJECT with BLACKLIST_KEEPER_SINGLE_SOURCE.
  9. Scan market.resolution_rules text for any term in params.ambiguity_keywords. On any match, return HARD_REJECT with BLACKLIST_KEEPER_AMBIGUOUS_RULES.
  10. Check market dispute history: if prior_disputes > 0, return HARD_REJECT with BLACKLIST_KEEPER_PRIOR_DISPUTE.
  11. All checks passed — return APPROVE with inputs_used list and checked_at timestamp (ms).

10. Reference Implementation

Runs five sequential guardrail checks — KillSwitch, banned-market registry lookup, banned-counterparty registry lookup, structural market checks (time-to-resolution, single-source, ambiguity keywords, dispute history) — before emitting a RiskVote. All checks are fail-closed: any registry or data-source error produces HARD_REJECT.

Pseudocode is language-agnostic. FETCH = read input or call service. EMIT = produce output. Translate to TS/Python/Go/Rust.

FUNCTION evaluateBlacklist(intent):
  // --- 0. KillSwitch gate ---
  ks = FETCH internal.killswitch.status
  IF ks.active:
    EMIT RiskVote(decision=HARD_REJECT, reason=KILL_SWITCH_ACTIVE)
    RETURN

  // --- 1. Banned-market registry check ---
  bannedMarkets = FETCH internal.registry.banned_markets
  IF bannedMarkets IS NULL:
    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_DATA_UNAVAILABLE,
                  detail="banned_markets registry unavailable")
    RETURN
  IF bannedMarkets.contains(intent.market_id):
    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_MARKET_BANNED,
                  market_id=intent.market_id)
    RETURN

  // --- 2. Banned-counterparty registry check ---
  bannedCounterparties = FETCH internal.registry.banned_counterparties
  IF bannedCounterparties IS NULL:
    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_DATA_UNAVAILABLE,
                  detail="banned_counterparties registry unavailable")
    RETURN
  IF bannedCounterparties.contains(intent.counterparty):
    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_COUNTERPARTY_BANNED,
                  counterparty=intent.counterparty)
    RETURN

  // --- 3. Fetch Gamma market metadata ---
  market = FETCH gamma.getMarketByConditionId(intent.market_id)
  IF market IS NULL OR market.fetched_at_ms < now_ms() - 300000:
    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_DATA_UNAVAILABLE,
                  detail="gamma market metadata unavailable or stale")
    RETURN

  // --- 4. Time-to-resolution check ---
  htr = (market.end_date_ms - now_ms()) / 3600000
  IF htr < params.min_hours_to_resolution:
    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_NEAR_RESOLUTION,
                  hours_to_resolution=htr)
    RETURN
  IF htr < 4:
    annotations.append(WARN(BLACKLIST_KEEPER_NEAR_RESOLUTION, hours_to_resolution=htr))

  // --- 5. Single-source resolution check ---
  IF params.block_single_source AND market.single_source:
    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_SINGLE_SOURCE)
    RETURN

  // --- 6. Ambiguity keyword scan ---
  FOR keyword IN params.ambiguity_keywords:
    IF keyword IN market.resolution_rules.lower():
      EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_AMBIGUOUS_RULES,
                    keyword=keyword)
      RETURN

  // --- 7. Prior dispute history check ---
  IF market.prior_disputes > 0:
    EMIT RiskVote(decision=HARD_REJECT, reason=BLACKLIST_KEEPER_PRIOR_DISPUTE,
                  prior_disputes=market.prior_disputes)
    RETURN

  // --- 8. All checks passed ---
  EMIT RiskVote(decision=APPROVE, reason=BLACKLIST_KEEPER_PASS,
                inputs_used=["internal.registry.banned_markets",
                             "internal.registry.banned_counterparties",
                             "gamma.market.resolution_rules",
                             "gamma.market.single_source",
                             "gamma.market.end_date_ms",
                             "gamma.market.prior_disputes"],
                annotations=annotations,
                checked_at=now_ms())

SDK calls used

  • gamma.getMarketByConditionId(market_id)
  • internal.registry.banned_markets.contains(condition_id)
  • internal.registry.banned_counterparties.contains(wallet)
  • internal.killswitch.status()
  • toPusdUnits(amount)

Complexity: O(1) — constant number of registry lookups and linear keyword scan over resolution_rules text (bounded by max text length ~2 KB); all backed by in-memory Redis cache

11. Wire Examples

Input — what arrives on the wire

OrderIntent from strategy — market on banned listinternal

{
  "intent_id": "int_a1b2c3d4e5f6a7b8",
  "trace_id": "trc_0011223344556677",
  "market_id": "0x3f7a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
  "side": "BUY",
  "outcome": "YES",
  "size_pusd": 250,
  "price": 0.6,
  "counterparty": "0xFa9E1234567890AbCdEf1234567890AbCdEf1234",
  "wallet": "0x1aBcDeF0987654321AbCdEf0987654321AbCdEf09",
  "generated_at_ms": 1746780000000
}

Gamma market metadata for the target marketgamma

{
  "condition_id": "0x3f7a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
  "resolution_rules": "This market resolves YES if the primary government agency publishes an official statement before end date.",
  "single_source": false,
  "end_date_ms": 1746866400000,
  "prior_disputes": 0,
  "neg_risk": false,
  "fetched_at_ms": 1746779980000
}

Output — what the bot emits

RiskVote — HARD_REJECT (market banned)

{
  "guard_id": "risk.blacklist_keeper",
  "decision": "HARD_REJECT",
  "severity": "HARD",
  "reason_code": "BLACKLIST_KEEPER_MARKET_BANNED",
  "message": "Market 0x3f7a...7f8a is on the banned-market registry. Order rejected.",
  "constraints": {},
  "inputs_used": [
    "internal.registry.banned_markets",
    "internal.registry.banned_counterparties",
    "gamma.market.resolution_rules",
    "gamma.market.single_source",
    "gamma.market.end_date_ms"
  ],
  "trace_id": "trc_0011223344556677",
  "checked_at": "2026-05-09T11:05:00Z"
}

RiskVote — APPROVE (all checks pass)

{
  "guard_id": "risk.blacklist_keeper",
  "decision": "APPROVE",
  "severity": "INFO",
  "reason_code": "BLACKLIST_KEEPER_PASS",
  "message": "All blacklist and structural checks passed for market 0xb2c3...e4f5.",
  "constraints": {},
  "inputs_used": [
    "internal.registry.banned_markets",
    "internal.registry.banned_counterparties",
    "gamma.market.resolution_rules",
    "gamma.market.single_source",
    "gamma.market.end_date_ms",
    "gamma.market.prior_disputes"
  ],
  "trace_id": "trc_aabbccddeeff0011",
  "checked_at": "2026-05-09T11:05:02Z"
}

12. Decision Logic

APPROVE

Market condition ID is not banned, counterparty is not banned, market resolves >= min_hours_to_resolution from now, single-source flag is clear (or block disabled), resolution rules contain no ambiguity keywords, and no prior dispute history.

RESHAPE_REQUIRED

Not used in current implementation — all blacklist violations are hard structural issues that cannot be partially accommodated. Reshape authority is reserved for future partial-position sizing logic if operator policy allows.

REJECT

Market or counterparty is on the banned list, market resolves within the hard time-to-resolution threshold, single-source resolution is detected with block_single_source=true, ambiguity keywords are found in resolution rules, prior disputes are recorded, KillSwitch is active, or any registry/data source is unavailable (fail-closed).

WARNING_ONLY

BLACKLIST_KEEPER_NEAR_RESOLUTION emits a WARN annotation when hours_to_resolution < 4h but >= 2h, allowing the upstream strategy to decide whether to reduce position size before the hard cutoff.

13. Standard Decision Output

This bot returns a RiskVote object. See RiskVote schema.

{
  "guard_id": "risk.blacklist_keeper",
  "decision": "HARD_REJECT",
  "severity": "HARD",
  "reason_code": "BLACKLIST_KEEPER_MARKET_BANNED",
  "message": "Market 0x3f7a...d9c2 is on the banned-market registry. Order rejected.",
  "constraints": {},
  "inputs_used": [
    "internal.registry.banned_markets",
    "internal.registry.banned_counterparties",
    "gamma.market.resolution_rules",
    "gamma.market.single_source",
    "gamma.market.end_date_iso"
  ],
  "checked_at": "2026-05-09T11:05:00Z"
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch is active; no orders may proceed.Immediately return HARD_REJECT without consulting the registry or Gamma API.Trading is currently paused. Please try again later.
BLACKLIST_KEEPER_MARKET_BANNEDHARD_REJECTThe target market condition ID appears in the operator-maintained banned-market registry.Return HARD_REJECT; log market_id and registry entry for audit trail.This market is not available for trading on this platform.
BLACKLIST_KEEPER_COUNTERPARTY_BANNEDHARD_REJECTThe counterparty wallet address appears in the operator-maintained banned-counterparty registry.Return HARD_REJECT; log counterparty address (redacted in user-facing messages) for audit trail.This transaction cannot be completed due to a platform restriction on the counterparty.
BLACKLIST_KEEPER_NEAR_RESOLUTIONHARD_REJECTThe market resolves within the configured min_hours_to_resolution window, which is below the UMA 2-hour challenge period. Emitted as WARN when hours_to_resolution is between 2 and 4h.Return HARD_REJECT when below hard threshold; emit WARN annotation and APPROVE when between warning and hard thresholds.This market is too close to resolution to accept new orders.
BLACKLIST_KEEPER_SINGLE_SOURCEHARD_REJECTThe market resolves on a single data source and block_single_source is enabled, indicating unacceptable feed manipulation and outage risk.Return HARD_REJECT; log market_id and single_source flag.This market cannot be traded due to its resolution source configuration.
BLACKLIST_KEEPER_AMBIGUOUS_RULESHARD_REJECTThe market resolution rules text contains one or more ambiguity keywords (e.g. 'substantial', 'primary') that indicate high dispute risk.Return HARD_REJECT; log the matched keyword and market_id for the risk team to review registry addition.This market has ambiguous resolution rules and is not available for trading.
BLACKLIST_KEEPER_PRIOR_DISPUTEHARD_REJECTThe market has a recorded prior dispute in its history, indicating structural resolution instability.Return HARD_REJECT; log market_id and prior_disputes count. Risk team should review whether to add to banned-market registry.This market has a history of resolution disputes and is not available for trading.
BLACKLIST_KEEPER_DATA_UNAVAILABLEHARD_REJECTOne or more required data sources (banned-market registry, banned-counterparty registry, or Gamma market metadata) are unavailable or returned stale data beyond TTL.Return HARD_REJECT (fail-closed). Log which source failed. Alert on-call if sustained > 60s.We could not verify this market at this time. Please try again shortly.
BLACKLIST_KEEPER_PASSINFOAll registry and structural checks passed for this market and counterparty.Emit APPROVE and continue to next guardrail.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_risk_blacklistkeeper_decisions_totalcountercountdecision, reason_codeTotal RiskVote decisions emitted by BlacklistKeeper, broken down by decision type and reason code. Primary signal for ban-list hit rate and structural rejection trends.
polytraders_risk_blacklistkeeper_registry_hits_totalcountercountregistry, actionNumber of banned-market and banned-counterparty registry hits, labeled by registry type ('market' or 'counterparty') and action ('reject'). Used to track how often the lists are triggering.
polytraders_risk_blacklistkeeper_registry_sizegaugecountregistryCurrent number of entries in each registry (banned_markets, banned_counterparties). Tracks list growth over time and alerts on unexpected shrinkage.
polytraders_risk_blacklistkeeper_eval_latency_mshistogrammillisecondsWall-clock time from OrderIntent receipt to RiskVote emit. P99 target < 30ms (registry lookups are in-memory Redis; Gamma metadata is cached).
polytraders_risk_blacklistkeeper_data_source_errors_totalcountercountsourceNumber of fail-closed rejections caused by unavailable data sources (banned_markets, banned_counterparties, gamma), by source name. Non-zero triggers immediate alert.
polytraders_risk_blacklistkeeper_registry_cache_age_secondsgaugesecondsregistryAge of the in-memory registry cache for banned_markets and banned_counterparties. Alerts if stale beyond configured refresh interval (default 30s).

Alerts

AlertConditionSeverityRunbook
BlacklistKeeperDataSourceErrorrate(polytraders_risk_blacklistkeeper_data_source_errors_total[5m]) > 0page#runbook-blacklistkeeper-data-source-error
BlacklistKeeperRegistryCacheStalepolytraders_risk_blacklistkeeper_registry_cache_age_seconds > 60page#runbook-blacklistkeeper-registry-cache-stale
BlacklistKeeperHighRejectRaterate(polytraders_risk_blacklistkeeper_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_blacklistkeeper_decisions_total[5m]) > 0.15warn#runbook-blacklistkeeper-reject-rate
BlacklistKeeperRegistryShrinkagedelta(polytraders_risk_blacklistkeeper_registry_size[1h]) < -5warn#runbook-blacklistkeeper-registry-shrinkage

Dashboards

  • Grafana — Risk overview / BlacklistKeeper
  • Grafana — Risk compliance / banned-market and banned-counterparty hit rates

16. Developer Reporting

{
  "bot_id": "risk.blacklist_keeper",
  "decision": "HARD_REJECT",
  "reason_code": "BLACKLIST_KEEPER_MARKET_BANNED",
  "inputs_used": [
    "internal.registry.banned_markets",
    "gamma.market.resolution_rules"
  ],
  "metrics": {
    "market_id": "0x3f7a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
    "counterparty": "0xAbCd...redacted",
    "registry_hit": true,
    "hours_to_resolution": 18.3,
    "single_source": false,
    "ambiguity_keyword_found": null,
    "prior_disputes": 0
  },
  "checked_at": "2026-05-09T11:05:00Z"
}

17. Plain-English Reporting

SituationUser-facing explanation
Order blocked — market is bannedThis market is not available for trading on this platform.
Order blocked — counterparty is bannedThis transaction cannot be completed due to a platform restriction on the counterparty.
Order blocked — market resolves too soonThis market closes too soon to accept new orders. Please look for a market with more time remaining.
Order blocked — ambiguous resolution rulesThis market has resolution rules that may lead to a disputed outcome and is not available for trading.
Order blocked — single-source resolutionThis market resolves on a single data source and is not available for trading due to reliability requirements.
Warning — market near resolutionThis market resolves soon. Consider whether your position size is appropriate given the limited time remaining.

18. Failure-Mode Block

main_failure_modeFailing open during a registry outage, allowing orders to reach banned markets or banned counterparties. BlacklistKeeper must never approve when the registry is unavailable.
false_positive_riskIncorrectly blocking a legitimate market due to a stale registry entry that has since been removed, or an ambiguity keyword match in boilerplate resolution-rules text that is not actually structurally ambiguous.
false_negative_riskApproving an order for a market whose condition_id was recently added to the banned list but the in-memory cache has not yet refreshed, creating a window during which banned markets can be traded.
safe_fallbackIf the banned-market registry, banned-counterparty registry, or Gamma market metadata is unavailable or returns stale data beyond TTL, BlacklistKeeper hard-rejects the intent with BLACKLIST_KEEPER_DATA_UNAVAILABLE. It never approves on missing data.
required_dependenciesBanned-market registry (Redis SET, operator-populated), Banned-counterparty registry (Redis SET, operator-populated), Gamma API — market resolution rules, single_source flag, end_date_iso, dispute history, KillSwitch active flag

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
REGISTRY_UNAVAILABLEBlock outbound TCP to Redis; wait for in-memory snapshot TTL (30s) to expireReturns to normal within one snapshot-refresh cycle after Redis connectivity is restored.
BANNED_MARKET_HITAdd a test condition_id to the banned_markets Redis SET and submit an OrderIntent targeting that market_idReturns to APPROVE immediately after the condition_id is removed from the registry and cache refreshes.
AMBIGUOUS_RESOLUTION_RULESOverride Gamma mock to return resolution_rules containing 'substantial' for a target marketReturns to APPROVE once Gamma returns clean resolution_rules and metadata cache refreshes.
NEAR_RESOLUTION_HARDSet Gamma mock market.end_date_ms = now_ms() + 3600000 (1 h from now)Returns to APPROVE once end_date_ms is > min_hours_to_resolution in the future or market is replaced with a new one.
GAMMA_API_DOWNReturn 503 from Gamma API mock for the getMarketByConditionId endpointReturns to APPROVE once Gamma API is reachable and market metadata cache is refreshed.
BANNED_COUNTERPARTY_HITAdd a test wallet address to the banned_counterparties Redis SET and submit an OrderIntent with that counterpartyReturns to APPROVE immediately after the wallet is removed from the registry and cache refreshes.

20. State & Persistence

Cold-start recovery

On cold start, registry snapshot is loaded from Redis synchronously before the first evaluation. If Redis is unavailable, all evaluations return HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE) until connectivity is restored.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight500
Idempotency keyintent_id
Per-call timeout (ms)30
Backpressure strategydrop newest
Locking / mutual exclusionregistry SETs are read-only at evaluation time; writes (admin additions/removals) use optimistic locking via Redis WATCH/MULTI/EXEC; no per-intent locking required

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchGlobal brake — checked first before any registry is consulted.RiskVote.HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all registry and structural evaluation.

Emits to (downstream consumers)

BotWhyContract
exec.smart_routerApproved RiskVote passes to SmartRouter for ExecutionPlan construction.APPROVE passes through; HARD_REJECT causes SmartRouter to discard the intent without retry.

Sibling bots (same OrderIntent)

BotWhyContract
risk.compliance_gateSibling guardrail; both must APPROVE before SmartRouter runs. ComplianceGate handles user/wallet policy; BlacklistKeeper handles market and counterparty registry.
risk.liquidity_guardSibling guardrail; runs in parallel with BlacklistKeeper on the same OrderIntent.
risk.portfolio_guardSibling guardrail; all risk guardrails must APPROVE or RESHAPE before execution proceeds.

External services

ServiceEndpointSLA assumedOn failure
Gamma API (market metadata)https://gamma-api.polymarket.com99.9% / 300ms p99
Redis (registry store)internal Redis cluster99.99% (in-cluster)

23. Security Surfaces

Abuse vectors considered

  • Operator registry poisoning: unauthorized modification of the banned-market or banned-counterparty SET in Redis, either to add legitimate markets (denial-of-service) or to remove banned entries (allowing hostile markets through)
  • Cache timing window: submitting an order for a newly banned market within the 30s registry refresh window before the in-memory snapshot is updated
  • Resolution-rules text injection: crafting a market with resolution_rules text that avoids ambiguity keywords but is semantically ambiguous, evading keyword-based detection

Mitigations

  • Redis registry SETs are write-protected by role-based access control; only the risk-admin service account may modify entries
  • Registry refresh is triggered both on TTL expiry and on write events (Redis keyspace notifications) to minimize the window between a ban and its enforcement
  • Fail-closed policy: Redis unavailability → HARD_REJECT, never APPROVE
  • All registry modifications are audit-logged with timestamp, operator ID, and before/after state

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldno
Aware of negative-risk marketsyes
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesBlacklistKeeper inspects Gamma V2 market metadata including negRisk and enableNegRisk flags to classify multi-outcome markets; NegRisk markets with ambiguous resolution rules or prior disputes are subject to the same registry and structural checks as standard binary markets. The bot does not sign orders or call CTFExchangeV2 directly.

API surfaces declared

gammainternal

Networks supported

polygon

25. Versioning & Migration

FieldValue
spec2.0.0
implementation2.1.0
schema2
released2026-04-28

Migration history

DateFromToReasonAction taken
2026-04-28v1v2CLOB V2 cutoverSwitched to py-clob-client-v2; all notional amounts now denominated in pUSD (collateral field updated from USDC.e). Removed feeRateBps and nonce references from intent inspection payloads. Updated Gamma market-metadata fetch to use V2 negRisk and enableNegRisk flags. EIP-712 domain version updated to '2' in downstream intent inspection. Registry wire format updated to include V2 condition_id format (32-byte hex).

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Approve when all checks passmarket not banned, counterparty not banned, hours_to_resolution=48, single_source=false, no ambiguity keywords, no prior disputesAPPROVE with no constraints
Reject when market is on banned listintent.market_id in blacklisted_condition_idsHARD_REJECT with reason_code=BLACKLIST_KEEPER_MARKET_BANNED
Reject when counterparty is on banned listintent.counterparty in blacklisted_counterpartiesHARD_REJECT with reason_code=BLACKLIST_KEEPER_COUNTERPARTY_BANNED
Reject when hours_to_resolution < min_hours_to_resolutionmarket.end_date_iso = now + 1h, min_hours_to_resolution=2HARD_REJECT with reason_code=BLACKLIST_KEEPER_NEAR_RESOLUTION
Warn when hours_to_resolution between warning and hard thresholdmarket.end_date_iso = now + 3h, min_hours_to_resolution=2, warning threshold=4hWARN annotation with reason_code=BLACKLIST_KEEPER_NEAR_RESOLUTION, verdict=APPROVE
Reject when single_source=true and block_single_source=truemarket.single_source=true, block_single_source=trueHARD_REJECT with reason_code=BLACKLIST_KEEPER_SINGLE_SOURCE
Reject when ambiguity keyword found in resolution rulesmarket.resolution_rules contains 'substantial'HARD_REJECT with reason_code=BLACKLIST_KEEPER_AMBIGUOUS_RULES
Reject when prior disputes > 0market.prior_disputes=1HARD_REJECT with reason_code=BLACKLIST_KEEPER_PRIOR_DISPUTE
Reject when registry unavailable (fail-closed)Redis returns connection error for banned-market registryHARD_REJECT with reason_code=BLACKLIST_KEEPER_DATA_UNAVAILABLE

Integration Tests

TestExpected result
Registry update propagates within TTL windowHARD_REJECT(BLACKLIST_KEEPER_MARKET_BANNED) within 30s of a condition_id being added to the banned-market registry and cache refresh completing
Gamma API market metadata fetch triggers ambiguity check end-to-endHARD_REJECT(BLACKLIST_KEEPER_AMBIGUOUS_RULES) when Gamma returns resolution_rules text containing a banned keyword for a live market
KillSwitch active bypasses all registry checksHARD_REJECT(KILL_SWITCH_ACTIVE) without consulting banned-market registry or Gamma API

Property Tests

PropertyRequired behaviour
Banned markets are never approved regardless of other check outcomesAlways true — registry hit is unconditional HARD_REJECT before any structural checks run
Registry unavailability never results in APPROVEAlways true — any registry read error produces HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE)
KillSwitch active always produces HARD_REJECT before registry is consultedAlways true — KillSwitch check is gate zero in the evaluation chain

27. Operational Runbook

BlacklistKeeper incidents typically involve a Redis registry outage causing fail-closed mass rejections, a stale registry cache preventing a newly banned market from being blocked, or an unexpected spike in ambiguity-rule rejections indicating a new market category has been listed without review.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
BlacklistKeeperDataSourceError
BlacklistKeeperRegistryCacheStale
BlacklistKeeperHighRejectRate
BlacklistKeeperRegistryShrinkage

Manual overrides

Healthcheck

GET /internal/health/blacklistkeeper → 200 if Redis registry SETs are reachable, snapshot age < 30s, and Gamma metadata cache is populated for all recently seen market_ids; red if Redis is unreachable, registry snapshot age > 60s, Gamma metadata fetch failing for > 60s, or p99 evaluation latency > 100ms.

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

GateHow measuredThreshold
All unit tests pass including fail-closed and registry scenariosCI test run100% pass
Registry integration test: ban a test market, confirm rejection within 30sIntegration test suitePass
Fail-closed injection test: Redis unavailable → HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE)Failure injection test suitePass

Promote to Limited live

GateHow measuredThreshold
Shadow reject rate matches expected baseline within 5% over 48hGrafana shadow vs live comparison dashboard< 5% divergence
p99 evaluation latency < 30ms (cache-backed)polytraders_risk_blacklistkeeper_eval_latency_ms histogramp99 < 30ms
Zero false-positive bans confirmed over 48h shadow runManual review of BLACKLIST_KEEPER_MARKET_BANNED rejections by risk team0 confirmed false positives

Promote to General live

GateHow measuredThreshold
Zero BLACKLIST_KEEPER_DATA_UNAVAILABLE rejections during normal operating hours over 7 daysBlacklistKeeperDataSourceError alert history0 firings
Registry shrinkage alert has never fired in staging or limited-liveBlacklistKeeperRegistryShrinkage alert history0 firings
Risk team sign-off on ambiguity keyword list completenessWritten approval from risk pod leadApproved
Counterparty ban registry integration test confirmed in stagingIntegration test suite — banned counterparty testPass

29. Developer Checklist

Ready-to-ship score: 27/27 sections complete · 100%

RequirementStatus
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