1.7 BlacklistKeeper
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
A bot is done when all four scores are. What does done mean?
1. Bot Identity
| Layer | Risk Risk |
|---|---|
| Bot class | Guardrail |
| Authority | RejectReshape |
| Status | BETA |
| Readiness | Limited live |
| Runs before | ExecutionPlan emit |
| Runs after | Strategy OrderIntent |
| Applies to | Every OrderIntent — screens the target market and counterparty wallet against operator-maintained banned-market and banned-counterparty lists |
| Default mode | limited_live |
| User-visible | summary-only |
| Developer owner | Polytraders core — Risk pod |
Operational profile
| Modes supported | quarantine |
|---|
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
| Input | Source | Required? | Use |
|---|---|---|---|
| Gamma API market metadata — resolution rules text, single-source flag, time-to-resolution, negRisk flag, prior dispute history | gamma | Yes | Evaluate market for ambiguity keywords, single-source resolution, time-to-resolution threshold, and structural hostility signals. |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| Banned-market registry — operator-curated list of banned condition_ids | internal | Yes | Hard-reject any intent whose market_id appears in the banned-market set. |
| Banned-counterparty registry — operator-curated list of banned wallet addresses | internal | Yes | Hard-reject any intent whose counterparty wallet appears in the banned-counterparty set. |
| Community / shared dispute history feed | internal | No | Supplement the operator-curated list with cross-platform dispute signals to identify markets with a track record of resolution failure. |
| KillSwitch active flag | KillSwitch | Yes | If KillSwitch is active, reject all orders immediately without consulting the registry. |
| ObservationReport stream | internal | No | Consume ObservationReports from intel/disc bots to trigger registry additions when new market anomalies are detected. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| blacklisted_condition_ids | [] | None | True | 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. |
| blacklisted_counterparties | [] | None | True | Explicit list of wallet addresses banned as counterparties. Any OrderIntent whose counterparty field matches an entry is hard-rejected. |
| min_hours_to_resolution | 2 | 4 | 2 | 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. |
| block_single_source | True | None | True | When 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
| Condition | Action |
|---|---|
| intent.market_id in blacklisted_condition_ids | REJECT — BLACKLIST_KEEPER_MARKET_BANNED |
| intent.market_id not in blacklisted_condition_ids | APPROVE (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
| Condition | Action |
|---|---|
| intent.counterparty in blacklisted_counterparties | REJECT — BLACKLIST_KEEPER_COUNTERPARTY_BANNED |
| intent.counterparty not in blacklisted_counterparties | APPROVE (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
| Condition | Action |
|---|---|
| hours_to_resolution >= 4 | APPROVE (this check) |
| hours_to_resolution >= 2 and < 4 | WARN — BLACKLIST_KEEPER_NEAR_RESOLUTION |
| hours_to_resolution < 2 | REJECT — 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
| Condition | Action |
|---|---|
| block_single_source = true AND market.single_source = true | REJECT — BLACKLIST_KEEPER_SINGLE_SOURCE |
| block_single_source = false OR market.single_source = false | APPROVE (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
| Condition | Action |
|---|---|
| no keyword found in resolution_rules | APPROVE (this check) |
| keyword found AND configured list < 5 keywords | WARN — BLACKLIST_KEEPER_AMBIGUOUS_RULES |
| keyword found AND configured list >= 5 keywords | REJECT — 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
- Receive OrderIntent from Strategy layer including market_id, counterparty wallet, and intent metadata.
- Check KillSwitch active flag; if active, return REJECT with KILL_SWITCH_ACTIVE immediately.
- 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).
- Look up intent.counterparty in the banned-counterparty registry (Redis SET). On any match, return HARD_REJECT with BLACKLIST_KEEPER_COUNTERPARTY_BANNED.
- Fetch target market metadata from Gamma API: resolution_rules text, single_source flag, end_date_iso, and prior dispute history.
- If Gamma fetch fails or returns stale data (age > 300s), return HARD_REJECT with BLACKLIST_KEEPER_DATA_UNAVAILABLE.
- 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.
- If params.block_single_source = true and market.single_source = true, return HARD_REJECT with BLACKLIST_KEEPER_SINGLE_SOURCE.
- Scan market.resolution_rules text for any term in params.ambiguity_keywords. On any match, return HARD_REJECT with BLACKLIST_KEEPER_AMBIGUOUS_RULES.
- Check market dispute history: if prior_disputes > 0, return HARD_REJECT with BLACKLIST_KEEPER_PRIOR_DISPUTE.
- 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 list — internal
{
"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 market — gamma
{
"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
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
KILL_SWITCH_ACTIVE | HARD_REJECT | Global 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_BANNED | HARD_REJECT | The 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_BANNED | HARD_REJECT | The 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_RESOLUTION | HARD_REJECT | The 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_SOURCE | HARD_REJECT | The 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_RULES | HARD_REJECT | The 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_DISPUTE | HARD_REJECT | The 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_UNAVAILABLE | HARD_REJECT | One 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_PASS | INFO | All registry and structural checks passed for this market and counterparty. | Emit APPROVE and continue to next guardrail. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
polytraders_risk_blacklistkeeper_decisions_total | counter | count | decision, reason_code | Total 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_total | counter | count | registry, action | Number 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_size | gauge | count | registry | Current 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_ms | histogram | milliseconds | Wall-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_total | counter | count | source | Number 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_seconds | gauge | seconds | registry | Age of the in-memory registry cache for banned_markets and banned_counterparties. Alerts if stale beyond configured refresh interval (default 30s). |
Alerts
| Alert | Condition | Severity | Runbook |
|---|---|---|---|
BlacklistKeeperDataSourceError | rate(polytraders_risk_blacklistkeeper_data_source_errors_total[5m]) > 0 | page | #runbook-blacklistkeeper-data-source-error |
BlacklistKeeperRegistryCacheStale | polytraders_risk_blacklistkeeper_registry_cache_age_seconds > 60 | page | #runbook-blacklistkeeper-registry-cache-stale |
BlacklistKeeperHighRejectRate | rate(polytraders_risk_blacklistkeeper_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_blacklistkeeper_decisions_total[5m]) > 0.15 | warn | #runbook-blacklistkeeper-reject-rate |
BlacklistKeeperRegistryShrinkage | delta(polytraders_risk_blacklistkeeper_registry_size[1h]) < -5 | warn | #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
| Situation | User-facing explanation |
|---|---|
| Order blocked — market is banned | This market is not available for trading on this platform. |
| Order blocked — counterparty is banned | This transaction cannot be completed due to a platform restriction on the counterparty. |
| Order blocked — market resolves too soon | This market closes too soon to accept new orders. Please look for a market with more time remaining. |
| Order blocked — ambiguous resolution rules | This market has resolution rules that may lead to a disputed outcome and is not available for trading. |
| Order blocked — single-source resolution | This market resolves on a single data source and is not available for trading due to reliability requirements. |
| Warning — market near resolution | This market resolves soon. Consider whether your position size is appropriate given the limited time remaining. |
18. Failure-Mode Block
| main_failure_mode | Failing 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_risk | Incorrectly 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_risk | Approving 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_fallback | If 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_dependencies | Banned-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
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
REGISTRY_UNAVAILABLE | Block outbound TCP to Redis; wait for in-memory snapshot TTL (30s) to expire | Returns to normal within one snapshot-refresh cycle after Redis connectivity is restored. | |
BANNED_MARKET_HIT | Add a test condition_id to the banned_markets Redis SET and submit an OrderIntent targeting that market_id | Returns to APPROVE immediately after the condition_id is removed from the registry and cache refreshes. | |
AMBIGUOUS_RESOLUTION_RULES | Override Gamma mock to return resolution_rules containing 'substantial' for a target market | Returns to APPROVE once Gamma returns clean resolution_rules and metadata cache refreshes. | |
NEAR_RESOLUTION_HARD | Set 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_DOWN | Return 503 from Gamma API mock for the getMarketByConditionId endpoint | Returns to APPROVE once Gamma API is reachable and market metadata cache is refreshed. | |
BANNED_COUNTERPARTY_HIT | Add a test wallet address to the banned_counterparties Redis SET and submit an OrderIntent with that counterparty | Returns 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
| Aspect | Specification |
|---|---|
| Execution model | single-threaded event loop |
| Max in-flight | 500 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 30 |
| Backpressure strategy | drop newest |
| Locking / mutual exclusion | registry 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)
| Bot | Why | Contract |
|---|---|---|
| risk.kill_switch | Global 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)
| Bot | Why | Contract |
|---|---|---|
| exec.smart_router | Approved RiskVote passes to SmartRouter for ExecutionPlan construction. | APPROVE passes through; HARD_REJECT causes SmartRouter to discard the intent without retry. |
Sibling bots (same OrderIntent)
| Bot | Why | Contract |
|---|---|---|
| risk.compliance_gate | Sibling guardrail; both must APPROVE before SmartRouter runs. ComplianceGate handles user/wallet policy; BlacklistKeeper handles market and counterparty registry. | |
| risk.liquidity_guard | Sibling guardrail; runs in parallel with BlacklistKeeper on the same OrderIntent. | |
| risk.portfolio_guard | Sibling guardrail; all risk guardrails must APPROVE or RESHAPE before execution proceeds. |
External services
| Service | Endpoint | SLA assumed | On failure |
|---|---|---|---|
| Gamma API (market metadata) | https://gamma-api.polymarket.com | 99.9% / 300ms p99 | |
| Redis (registry store) | internal Redis cluster | 99.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
| Aspect | Value |
|---|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | no |
| Aware of negative-risk markets | yes |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | BlacklistKeeper 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
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 | v2 | CLOB V2 cutover | Switched 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
| Test | Setup | Expected result |
|---|---|---|
| Approve when all checks pass | market not banned, counterparty not banned, hours_to_resolution=48, single_source=false, no ambiguity keywords, no prior disputes | APPROVE with no constraints |
| Reject when market is on banned list | intent.market_id in blacklisted_condition_ids | HARD_REJECT with reason_code=BLACKLIST_KEEPER_MARKET_BANNED |
| Reject when counterparty is on banned list | intent.counterparty in blacklisted_counterparties | HARD_REJECT with reason_code=BLACKLIST_KEEPER_COUNTERPARTY_BANNED |
| Reject when hours_to_resolution < min_hours_to_resolution | market.end_date_iso = now + 1h, min_hours_to_resolution=2 | HARD_REJECT with reason_code=BLACKLIST_KEEPER_NEAR_RESOLUTION |
| Warn when hours_to_resolution between warning and hard threshold | market.end_date_iso = now + 3h, min_hours_to_resolution=2, warning threshold=4h | WARN annotation with reason_code=BLACKLIST_KEEPER_NEAR_RESOLUTION, verdict=APPROVE |
| Reject when single_source=true and block_single_source=true | market.single_source=true, block_single_source=true | HARD_REJECT with reason_code=BLACKLIST_KEEPER_SINGLE_SOURCE |
| Reject when ambiguity keyword found in resolution rules | market.resolution_rules contains 'substantial' | HARD_REJECT with reason_code=BLACKLIST_KEEPER_AMBIGUOUS_RULES |
| Reject when prior disputes > 0 | market.prior_disputes=1 | HARD_REJECT with reason_code=BLACKLIST_KEEPER_PRIOR_DISPUTE |
| Reject when registry unavailable (fail-closed) | Redis returns connection error for banned-market registry | HARD_REJECT with reason_code=BLACKLIST_KEEPER_DATA_UNAVAILABLE |
Integration Tests
| Test | Expected result |
|---|---|
| Registry update propagates within TTL window | HARD_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-end | HARD_REJECT(BLACKLIST_KEEPER_AMBIGUOUS_RULES) when Gamma returns resolution_rules text containing a banned keyword for a live market |
| KillSwitch active bypasses all registry checks | HARD_REJECT(KILL_SWITCH_ACTIVE) without consulting banned-market registry or Gamma API |
Property Tests
| Property | Required behaviour |
|---|---|
| Banned markets are never approved regardless of other check outcomes | Always true — registry hit is unconditional HARD_REJECT before any structural checks run |
| Registry unavailability never results in APPROVE | Always true — any registry read error produces HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE) |
| KillSwitch active always produces HARD_REJECT before registry is consulted | Always 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
| Alert | First step | Diagnosis | Mitigation | Escalate 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
| Gate | How measured | Threshold |
|---|---|---|
| All unit tests pass including fail-closed and registry scenarios | CI test run | 100% pass |
| Registry integration test: ban a test market, confirm rejection within 30s | Integration test suite | Pass |
| Fail-closed injection test: Redis unavailable → HARD_REJECT(BLACKLIST_KEEPER_DATA_UNAVAILABLE) | Failure injection test suite | Pass |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| Shadow reject rate matches expected baseline within 5% over 48h | Grafana shadow vs live comparison dashboard | < 5% divergence |
| p99 evaluation latency < 30ms (cache-backed) | polytraders_risk_blacklistkeeper_eval_latency_ms histogram | p99 < 30ms |
| Zero false-positive bans confirmed over 48h shadow run | Manual review of BLACKLIST_KEEPER_MARKET_BANNED rejections by risk team | 0 confirmed false positives |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| Zero BLACKLIST_KEEPER_DATA_UNAVAILABLE rejections during normal operating hours over 7 days | BlacklistKeeperDataSourceError alert history | 0 firings |
| Registry shrinkage alert has never fired in staging or limited-live | BlacklistKeeperRegistryShrinkage alert history | 0 firings |
| Risk team sign-off on ambiguity keyword list completeness | Written approval from risk pod lead | Approved |
| Counterparty ban registry integration test confirmed in staging | Integration test suite — banned counterparty 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 |