Repository navigation
feat(mcp-server): migrate prediction market-data REST endpoints + settlement alert fetcher to SDK (PREDICT-8820) - #67
Conversation
…tlement alert fetcher to SDK (PREDICT-8820) Routes the 8 public prediction market-data endpoints and the settlement-alert daemon fetcher through @gemini-markets/sdk's client.predictions.* operations, replacing the hand-rolled GeminiHttpClient for these paths. Wires the already-plumbed-but-unused sdkClient param from PREDICT-8816 into createPredictionMarketDataTools and the alerts daemon's buildFetchers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nostradamus Risk Rating — MediumThe PR migrates 8 prediction market-data endpoints from |
svc-grace
left a comment
There was a problem hiding this comment.
Agentic Review
Neat migration overall, but two correctness issues need attention before this can move forward: the prediction tools currently bypass the configured API base URL, and the SDK settlement response needs to be mapped back to the legacy event shape so settled alerts don't silently lose fields. There's also a small coverage gap around forwarding list options; worth adding, but it isn't blocking.
Review process
Grace version: v0.0.210
Files reviewed (6): annotations.test.ts, index.ts, market-data.test.ts, market-data.ts, server.ts
Guidelines: none discovered
Verification: 3 of 6 findings verified
Findings: 0 critical, 2 important, 1 suggestion
Linked tickets: PREDICT-8820
LLM usage: 119 calls — gpt-5.6-luna: 78 calls, 4398262 tokens, us.anthropic.claude-opus-4-6-v1: 34 calls, 844421 tokens, us.anthropic.claude-sonnet-4-6: 7 calls, 743186 tokens
| ...createMarginTools(client), | ||
| ...createStakingTools(client), | ||
| ...createPredictionMarketDataTools(client), | ||
| ...createPredictionMarketDataTools(sdkClient), |
There was a problem hiding this comment.
This wiring makes prediction tools ignore the documented GEMINI_API_BASE_URL setting because sdkClient selects only config.sdkEnv, while the legacy client used config.baseUrl. When an operator sets the documented sandbox URL without also setting GEMINI_SDK_ENV=sandbox, all eight prediction calls go to production instead of the requested endpoint. Configure the SDK from the existing base-URL setting or update the config contract and migration path so the two selectors cannot diverge.
There was a problem hiding this comment.
Fixed in 7a73521 — resolveSdkEnv() in config.ts now derives the default from GEMINI_API_BASE_URL (sandbox hostname → sandbox) instead of hardcoding production, so setting only the documented base-URL var routes prediction-market SDK calls to the same environment as everything else. GEMINI_SDK_ENV still works as an explicit override for the rare case someone wants them to diverge on purpose. Added config.test.ts covering the derivation.
There was a problem hiding this comment.
Confirmed fixed: config.ts now derives sdkEnv from the shared GEMINI_API_BASE_URL (sandbox hostname maps to sandbox) when no explicit override is set, and server.ts passes that sdkClient into the prediction tools.
| const response = await client.predictions.listEvents( | ||
| input as SdkInput<typeof client.predictions.listEvents> | ||
| ); | ||
| return response as unknown as EventsResponse; |
There was a problem hiding this comment.
The migrated datasource casts the SDK Event response directly to the legacy shape without mapping settlement fields. For a settled event, the SDK schema places the value at settlement.value and the timestamp at resolvedAt, so listRecentlySettled() returns settlementValue and settlementTime as undefined; the daemon then emits alerts without the settlement value or time. Normalize these fields before returning the legacy contract.
There was a problem hiding this comment.
Fixed in 7a73521 — added a normalizeEvent/normalizeEvents helper in market-data.ts that explicitly maps settlement.value/resolvedAt onto settlementValue/settlementTime (preferring an already-flat field if the API ever sends one directly), applied to every function that returns event(s) — not just listRecentlySettled, since any event can carry settlement data. Added test coverage for the nested mapping, the flat-field-preference case, and the no-settlement-data case.
There was a problem hiding this comment.
Confirmed fixed: normalizeEvent now maps nested settlement.value and resolvedAt to settlementValue and settlementTime, and listRecentlySettled passes its response through normalizeEvents before returning it.
| test('listEvents sends category/status as plain (unbracketed) input keys', async () => { | ||
| const { client, calls } = fakeClient({ data: [], pagination: { limit: 50, offset: 0 } }); | ||
|
|
||
| await predictions.listEvents(client, { status: ['active'], category: ['Sports', 'Crypto'] }); |
There was a problem hiding this comment.
📝 Suggestion: List option forwarding is not covered
The list tests cover only category/status (and category for the other list methods), leaving the migrated search, limit, and offset forwarding branches untested. Passing pagination or search options can regress to omission or the wrong numeric shape while every current assertion passes; cover the full input contract for each list wrapper.
There was a problem hiding this comment.
Addressed in 7a73521 — added coverage for search/limit/offset forwarding on listEvents, and limit/offset on listUpcoming/listNewlyListed/listRecentlySettled.
There was a problem hiding this comment.
Confirmed fixed: The test file now contains explicit forwarding tests asserting search/limit/offset for listEvents and limit/offset for listUpcoming, listNewlyListed, and listRecentlySettled.
- config.ts: derive sdkEnv from GEMINI_API_BASE_URL by default so setting only the documented sandbox base URL routes prediction-market SDK calls to sandbox too, instead of silently defaulting to production when the separate GEMINI_SDK_ENV var is left unset. GEMINI_SDK_ENV still overrides explicitly. - market-data.ts: normalize the SDK's nested settlement.value/resolvedAt fields onto the legacy flat settlementValue/settlementTime shape that PredictionEvent and the settlement-alert fetcher expect, instead of trusting a blind cast. Applies to every function that returns event(s). - market-data.test.ts: add coverage for the settlement-field normalization (nested mapping, flat-field preference, and the no-settlement-data case) and for search/limit/offset forwarding on the list endpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Change Control Evidence CheckAuthorization — ✅ PassEvidence:
Testing — ✅ PassEvidence:
Approval — ✅ PassEvidence:
Segregation of Duties — ✅ PassValidated:
Last checked: 2026-09-21 19:12 UTC ↩ Re-run Clarissa if tickets, CI, or approvals have changed. |
svc-grace
left a comment
There was a problem hiding this comment.
Agentic Review
Nice progress — the list option forwarding, legacy settlement-field mapping, and configured API-base handling are all resolved. The remaining follow-up is narrower: production SDK environment derivation isn't covered by tests, and the inline note has the details. There are no blocking findings in this pass, so this is unblocked with that test-coverage polish still worth a look.
Review process
Grace version: v0.0.210
Files reviewed (4): config.test.ts, config.ts, market-data.test.ts, market-data.ts
Files skipped: 7
Guidelines: none discovered
Verification: 1 of 4 findings verified
Findings: 0 critical, 0 important, 1 suggestion
Linked tickets: PREDICT-8820
LLM usage: 87 calls — gpt-5.6-luna: 61 calls, 2470721 tokens, us.anthropic.claude-opus-4-6-v1: 26 calls, 727825 tokens
|
|
||
| const { config } = await import('./config.js'); | ||
|
|
||
| test('sdkEnv derives from GEMINI_API_BASE_URL when GEMINI_SDK_ENV is unset, so the two selectors cannot diverge', () => { |
There was a problem hiding this comment.
📝 Suggestion: Production SDK environment derivation is untested
This suite covers only the unset-variable/sandbox-URL case; it does not exercise the new production derivation or the empty-value path. With GEMINI_API_BASE_URL=https://api.gemini.com, a regression that selects sandbox would route SDK calls to sandbox without CI detecting it; add cases for the production and empty-value paths.
There was a problem hiding this comment.
Addressed in 23d36e5 — exported resolveSdkEnv as a pure function of (baseUrl, sdkEnvOverride) instead of reading GEMINI_SDK_ENV internally, so it's directly unit-testable without env-var-before-import gymnastics. Added cases for the production-derivation path, both the unset and empty-value override forms, the explicit-override-wins-either-direction cases, the invalid-override throw, and a malformed-base-URL fallback.
There was a problem hiding this comment.
Confirmed fixed: config.test.ts now directly covers production derivation with both unset and empty overrides, plus the sandbox and explicit-override cases requested.
…de paths (PREDICT-8820) Export resolveSdkEnv as a pure function of (baseUrl, sdkEnvOverride) instead of reading GEMINI_SDK_ENV internally, so every derivation path is directly unit-testable without env-var-before-import gymnastics. Addresses svc-grace's follow-up suggestion that only the sandbox-URL/unset-override case was covered. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
svc-grace
left a comment
There was a problem hiding this comment.
Agentic Review
Good progress — the production SDK environment derivation, list-option forwarding, legacy settlement mapping, and API-base wiring concerns are all resolved. This pass found one blocking configuration issue: the alert daemon drops the SDK environment override, so alerts can target the wrong environment; the related config test also doesn’t prove explicit overrides. Please address that before merge.
Review process
Grace version: v0.0.210
Files reviewed (2): config.test.ts, config.ts
Files skipped: 12
Guidelines: none discovered
Verification: 2 of 2 findings verified
Findings: 0 critical, 1 important, 1 suggestion
Linked tickets: PREDICT-8820
LLM usage: 38 calls — gpt-5.6-luna: 27 calls, 723232 tokens, us.anthropic.claude-opus-4-6-v1: 11 calls, 277552 tokens
| wsUrl: process.env.GEMINI_WS_URL ?? 'wss://ws.gemini.com', | ||
| account: process.env.GEMINI_ACCOUNT ?? '', | ||
| sdkEnv: resolveSdkEnv(), | ||
| sdkEnv: resolveSdkEnv(baseUrl, process.env.GEMINI_SDK_ENV), |
There was a problem hiding this comment.
config.sdkEnv honors GEMINI_SDK_ENV, but the alert supervisor does not preserve that variable when it writes the daemon environment. When an operator sets GEMINI_SDK_ENV=sandbox with a production-looking GEMINI_API_BASE_URL, the MCP server uses sandbox while an installed alert daemon derives production and fetches settlements from the wrong environment. Add GEMINI_SDK_ENV to the supervisor environment whitelist and document the override.
There was a problem hiding this comment.
Fixed in 3b88bd0 — added GEMINI_SDK_ENV to ENV_WHITELIST in alerts/supervisor/types.ts, so an explicit override survives daemon install. Added a regression test (pickWhitelistedEnv preserves an explicit GEMINI_SDK_ENV override) plus an ENV_WHITELIST membership check.
There was a problem hiding this comment.
Confirmed fixed: ENV_WHITELIST now includes GEMINI_SDK_ENV in packages/mcp-server/src/alerts/supervisor/types.ts, and the supervisor regression test verifies pickWhitelistedEnv preserves an explicit sandbox override; config.ts documents and consumes the override.
| // GEMINI_API_BASE_URL/GEMINI_SDK_ENV at module load, not just that the function | ||
| // itself is correct in isolation (covered above). | ||
| assert.strictEqual(config.baseUrl, SANDBOX_URL); | ||
| assert.strictEqual(config.sdkEnv, 'sandbox'); |
There was a problem hiding this comment.
📝 Suggestion: Config integration does not prove explicit SDK environment overrides
The module-load integration test exercises only an unset override, while explicit override handling is tested only by calling resolveSdkEnv() directly. With GEMINI_API_BASE_URL set to sandbox and GEMINI_SDK_ENV=production, a regression that stops passing the environment override into config would still pass every test and route SDK calls to sandbox instead of production. Add startup coverage for the explicit override path.
There was a problem hiding this comment.
Addressed in 3b88bd0 — added config.override.test.ts, a separate test file/process setting GEMINI_API_BASE_URL to sandbox and GEMINI_SDK_ENV=production together, then importing config fresh and asserting the override wins at module-load time (not just when calling resolveSdkEnv() directly).
There was a problem hiding this comment.
Confirmed fixed: config.override.test.ts sets sandbox GEMINI_API_BASE_URL and production GEMINI_SDK_ENV before a fresh config import, then asserts config.sdkEnv is production, directly covering module-load override forwarding.
…lert daemon (PREDICT-8820) The alert supervisor's ENV_WHITELIST (baked into the daemon's persistent OS-supervised unit on install) never included GEMINI_SDK_ENV, so an operator's explicit override was silently dropped once installed — the daemon would then derive sdkEnv from GEMINI_API_BASE_URL alone and fetch prediction-market settlements from a different environment than the live MCP server process. Inert before this PR (the daemon never consumed sdkClient), now load-bearing since PREDICT-8820 wires it into the settlement-alert fetcher. Also adds a startup-integration test proving an explicit GEMINI_SDK_ENV override reaches config even against a sandbox base URL, per svc-grace's follow-up suggestion that only the unset-override path was covered. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
svc-grace
left a comment
There was a problem hiding this comment.
Agentic Review
Nice progress — all six carried-over concerns are resolved in code, including the SDK environment wiring, option forwarding, legacy settlement mapping, and prediction base-URL handling. This pass found no new issues. The remaining confidence gap is the settlement-alert path: the real settled-event shape and end-to-end alert behavior still need verification, especially for preserving settlement fields through the SDK response.
Review process
Grace version: v0.0.210
Files reviewed (3): config.override.test.ts, index.test.ts, types.ts
Files skipped: 14
Suggestions damped: 2 (pass 4 with nothing blocking — new suggestion-level findings are withheld so the review converges, TOOLS-6878)
Guidelines: none discovered
Verification: 0 of 2 findings verified
Findings: 0 critical, 0 important, 0 suggestions
Linked tickets: PREDICT-8820
LLM usage: 28 calls — gpt-5.6-luna: 24 calls, 429126 tokens, us.anthropic.claude-haiku-4-5-20251001-v1:0: 1 call, 12811 tokens, us.anthropic.claude-sonnet-4-6: 3 calls, 71626 tokens
Linear Board: https://linear.app/gemini/issue/PREDICT-8820/migrate-prediction-market-data-rest-endpoints-settlement-alert-fetcher
What's Included
Migrated all 8 market-data endpoints in
datasources/predictions/market-data.tsfrom the hand-rolledGeminiHttpClientto@gemini-markets/sdk'sclient.predictions.*operations:listEvents,getEvent,getEventStrike,listNewlyListedEvents,listRecentlySettledEvents,listUpcomingEvents,getCategories,getVolumeMetrics. Return types are unchanged (existing hand-verified-against-production shapes intypes/predictions.ts) — the SDK's OpenAPI-generated response types are a superset, so results are cast through rather than re-typed, keeping every downstream consumer's contract identical.Settlement fields normalized explicitly: the SDK's generated
Eventschema places settlement data atsettlement.value/resolvedAt, not the flatsettlementValue/settlementTimefieldsPredictionEventand the settlement-alert fetcher expect. Added anormalizeEvent/normalizeEventshelper that maps these explicitly (preferring an already-flat field if the API ever sends one directly), applied to every function that returns event(s).getVolumeMetricsaccount scoping preserved: this is the one authenticated call among the 8. The legacy client'sauthenticatedPostunconditionally injectedconfig.accountinto the body for sub-account scoping; the SDK has no first-class account field, but its body builder forwards any extra input key straight through, so the same injection is replicated explicitly in the datasource function.Wired the dead
sdkClientparam from PREDICT-8816:server.tsnow actually passessdkClientintocreatePredictionMarketDataTools(previously accepted but unused).tools/predictions/market-data.ts's factory signature updated toSdkClient.Settlement alert fetcher migrated for free, per the ticket's scope:
alerts/daemon/index.ts'sbuildFetchersnow takessdkClientalongside the legacy client, andpredictionsSettledcalls the migratedlistRecentlySettled(sdkClient).config.ts'ssdkEnvnow derives fromGEMINI_API_BASE_URLby default, instead of hardcodingproduction, so setting only the documented base-URL var routes prediction-market SDK calls to the same environment as everything else.GEMINI_SDK_ENVstill works as an explicit override, and is now included in the alert supervisor'sENV_WHITELISTso that override survives daemon install (it was silently dropped before — inert prior to this PR, now load-bearing since the daemon consumessdkClient).Testing
npm test— 311/311 passing, 0 regressions (includes new coverage added while addressing automated review feedback: settlement-field normalization, list-option forwarding,sdkEnvderivation across sandbox/production/override paths, and the supervisor env-whitelist fix)npm run typecheck— cleannpm run build— cleanmarket-data.test.ts(rewrote the mock harness to fakeSdkClient.predictionsinstead ofGeminiHttpClient):category/statuskeys) — now asserting against the SDK input object rather than raw query paramsgetEvent,getEventStrike,getVolumeMetrics(previously untested)config.accountinjection ongetVolumeMetrics, both with and without an account configuredsearch/limit/offsetforwarding on the list endpointstools/annotations.test.ts, which independently rebuildsallToolsto regression-test the destructive/write tool lists — needed a second fake client typedSdkClientfor the market-data factoryconfig.test.ts/config.override.test.tscoveringresolveSdkEnv's derivation across sandbox/production base URLs, unset/empty/explicit overrides, and the module-load wiring itselfpickWhitelistedEnv/ENV_WHITELISTcoverage for theGEMINI_SDK_ENVsupervisor fixbuildFetchersdirectly)Manual, against real production only — sandbox not yet exercised (fresh
geminiMCP session, since MCP servers don't hot-reload):gemini_get_prediction_volume_metrics— authenticated SDK call confirmed working end-to-end against a real event (CFBPLAYOFF2627, College Football Playoff Qualifiers), 50 contracts returned. Confirms the HMAC-auth path through the SDK works for the one authenticated endpoint here.gemini_list_recently_settled_prediction_events— confirmed against a real settled event (SOL05M2609211705, production): raw JSON has flat, top-levelsettlementValue/settlementTime, both correctly populated. (Interesting note: production sends these flat, not nested undersettlement.value/resolvedAtas the SDK's generated OpenAPI schema implied — the schema-drift risk didn't materialize here, butnormalizeEvent's flat-field-preference design means it's a safe no-op in this case and would have been the actual fix if it had.)gemini_list_prediction_events/gemini_get_prediction_event/gemini_get_prediction_event_strike/gemini_list_upcoming_prediction_events/gemini_list_newly_listed_prediction_events/gemini_list_prediction_categories— spot-checked viaCFBPLAYOFF2627(event details, category, upcoming/newly-listed membership) and via category+search+limit filtering (active crypto events, upcoming sports search).getEventStrikecorrectly 404s for this multi-outcome event (no single reference strike) and propagates cleanly throughwrapHandlerrather than crashing.prediction.settledrule against a real, freshly-settled event (SOL05M2609211705), ran the daemon binary directly in the foreground (not installed as a persistent service) to trigger its immediate first-poll evaluation. Confirmed viagemini_alert_history/gemini_alert_get: fired with reason"Prediction event SOL05M2609211705 settled at 117.35199999999999",lastFiredAtset, rule auto-disabled (oneShot: true). (Hit an unrelated localnode-notifier/macOS Notification Center spawn error —errno -86trying to exec its bundled helper binary — butScheduler.dispatchcatches that and still persists the fire/history regardless, so it didn't block verification. Nothing in this PR touches the notifier.)(Spot Check)

What Success Looks Like
A reviewer should be able to call any of the 8
gemini_*_prediction_*market-data tools against sandbox or production and get back the same shape of data as before this PR — same field names, same values — just served through the SDK client instead of the legacy HTTP client. A false negative here looks like: a field silently going missing orundefinedin the response (most likelysettlementValue/settlementTimeon settled events — now normalized and unit-tested, but still worth a live check per above), or an authenticated call togetVolumeMetricsfailing where it used to succeed. No regression to existing tools — orders, positions, and combos still run through the legacyGeminiHttpClientuntouched, and spot/margin/staking/fund tools are unaffected.🤖 Generated with Claude Code