feat(mcp-server): migrate prediction orders REST endpoints to SDK (PREDICT-8817) - #74
Conversation
…EDICT-8817) - Move placeOrder, cancelOrder, placeOrderBatch, cancelOrderBatch, getActiveOrders, and getOrderHistory from the legacy HTTP client to @gemini-markets/sdk. - Map timeInForce maker-or-cancel onto the SDK's makerOrCancel flag. - Stringify bigint orderIds in every response; convert the cancel orderId with BigInt(). - Add withAccountScope() so every authenticated prediction call sends GEMINI_ACCOUNT, restoring sub-account scoping for positions and createCombo.
Nostradamus Risk Rating — MediumThe PR migrates 6 prediction order endpoints in |
svc-grace
left a comment
There was a problem hiding this comment.
Agentic Review
Nice migration overall, and the order-path coverage is thorough. One correctness issue remains: the query-only prediction positions request drops the configured account scope, so callers using a master key with GEMINI_ACCOUNT can still read the primary account instead of the selected sub-account. That’s the blocker to fix; the rest of the SDK migration reads clean.
Review process
Grace version: v0.0.218
Files reviewed (12): annotations.test.ts, combos.test.ts, combos.ts, market-data.ts, orders.test.ts, orders.ts, positions.test.ts, positions.ts, sdk.ts, server.ts
Guidelines: none discovered
Verification: 1 of 3 findings verified
Findings: 0 critical, 1 important, 0 suggestions
Linked tickets: PREDICT-8817
LLM usage: 193 calls — gpt-5.6-luna: 149 calls, 8594710 tokens, us.anthropic.claude-opus-4-6-v1: 16 calls, 651739 tokens, us.anthropic.claude-sonnet-4-6: 28 calls, 2437198 tokens
…alls (PREDICT-8817) - Revert withAccountScope() on getPositions/getSettledPositions: both SDK operations are query-only, so the SDK drops the added account key before signing. - Remove the positions account-scope tests that only checked the mocked SDK input. - Document on withAccountScope() that it only scopes operations with a request body.
Change Control Evidence CheckAuthorization — ✅ PassEvidence:
Testing — ✅ PassEvidence:
Approval — ✅ PassEvidence:
Segregation of Duties — ✅ PassValidated:
Last checked: 2026-09-28 22:01 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
The order endpoints are now migrated to the SDK, with the maker-or-cancel mapping, exact order-ID handling, confirmation gates, and account scoping for body-based calls all covered by tests and production checks. The query-only positions scope gap is still open: the SDK drops the account field, so those requests continue to read the primary account and need an SDK-level change. That persisted important issue is what drives the request for changes; the order migration itself looks well exercised.
Persisted from prior passes (1 open)
Review process
Grace version: v0.0.218
Files reviewed (1): sdk.ts
Files skipped: 18
Guidelines: none discovered
Findings: 0 critical, 0 important, 0 suggestions
Linked tickets: PREDICT-8817
LLM usage: 31 calls — gpt-5.6-luna: 25 calls, 1560501 tokens, us.anthropic.claude-sonnet-4-6: 6 calls, 322691 tokens
Whats included
placeOrder,cancelOrder,placeOrderBatch,cancelOrderBatch,getActiveOrders,getOrderHistory) off the legacy hand-rolledGeminiHttpClientand onto the@gemini-markets/sdkclient (sdkClient).createPredictionOrderToolsnow takessdkClient. This is the last prediction tool group on the legacy client — no prediction datasource or tool imports it after this.maker-or-cancelmapping. The tool still offerstimeInForce: "maker-or-cancel", but the SDK'sTimeInForceenum has no such value — maker-or-cancel is its ownmakerOrCancelboolean, and the SDK's request validation rejects the old value before anything is sent. The datasource now sendsmakerOrCancel: true(with notimeInForce) for that option andmakerOrCancel: falseotherwise, so the tool contract is unchanged. Confirmed live: Gemini echoes the order back astimeInForce: "maker-or-cancel"(test 7).orderIdtobigint; the datasource turns them back into exact strings (neverNumber(), which loses precision on these 17–18 digit IDs, andbigintisn't serializable byJSON.stringify). The single cancel converts the tool's string ID withBigInt(), since the SDK types that field asbigint.orderIdstringified — every field Gemini sends still reaches the tool output. Batch results stay positional, with rejected entries passed through verbatim.GEMINI_ACCOUNTto every signed request, so a master API key reads and trades on the configured sub-account. A newwithAccountScope()helper inclient/sdk.tskeeps that for every authenticated prediction call that has a request body: all 6 order calls,createCombo(which feat(mcp-server): migrate prediction combos REST endpoints to SDK (PREDICT-8819) #73 had dropped it from), andgetVolumeMetrics(which already did this inline). Public calls (listCombos,getCombo) never send it.gemini_get_prediction_positionsandgemini_get_prediction_settled_positionsread the primary account even withGEMINI_ACCOUNTset. Both SDK operations are query-only, so the SDK sends only their declared query fields and signs no body — an addedaccountkey is silently dropped (confirmed through the real SDK: the signed payload is just{request, nonce}). There's no SDK-supported account parameter, so fixing this needs an SDK-level account option and release; it's left out of this PR, and thewithAccountScope()comment documents the limitation.confirm: truegate on the four destructive tools.Linear: https://linear.app/gemini/issue/PREDICT-8817
Test plan
npm run build— cleannpm run typecheck— cleannpm test— 339/339 passing, including:maker-or-cancel→makerOrCancel: true, exactBigIntcancel ID, filters only sent when set)GEMINI_ACCOUNTadded to every order call andcreateCombowhen set, and never when unset or on public callsfetch: a maker-or-cancel order is accepted and sent asmakerOrCancel: true; the oldtimeInForce: "maker-or-cancel"value is rejected before sending (regression guard); the cancelorderIdgoes out as an exact 18-digit JSON number;accountreaches both the signed payload and the request body; an 18-digitorderIdin a raw response survives as the exact stringconfirm: truerequired on all 4 destructive order tools; mixed batch results kept in order; SDK errors surface withreason/codeGEMINI_ACCOUNTis setdatasources/predictions/ortools/predictions/importsGeminiHttpClient@gemini-markets/sdk@0.1.1, using 1-contract buys at $0.01–$0.02 onGEMI-NFL-2609290015-PHI-CHI-M-PHI(trading at ~$0.64, so nothing could fill). Every order was cancelled and the final check shows no open orders.Test 1 — place order
Prompt:
call gemini_place_prediction_order with symbol GEMI-NFL-2609290015-PHI-CHI-M-PHI, side buy, outcome yes, quantity 1, price 0.01, confirm true{ "orderId": "145828836533715331", "hashOrderId": "LWmopoQGzLP9", "clientOrderId": "9e868c1b-0812-4070-bd1c-05dab5fa6537", "globalOrderId": "ff509bfb463cc116427c2beba769d0786cb598f29b8acee71a006f99f3e308c8", "status": "open", "symbol": "GEMI-NFL-2609290015-PHI-CHI-M-PHI", "side": "buy", "outcome": "yes", "orderType": "limit", "timeInForce": "good-til-cancel", "quantity": "1", "filledQuantity": "0", "remainingQuantity": "1", "price": "0.01", "createdAt": "2026-09-28T18:21:38.345Z", "updatedAt": "2026-09-28T18:21:38.345Z" }Test 2 — active orders
Prompt:
call gemini_get_prediction_active_orders{ "orders": [ { "orderId": "145828836533715331", "hashOrderId": "LWmopoQGzLP9", "clientOrderId": "9e868c1b-0812-4070-bd1c-05dab5fa6537", "globalOrderId": "145828836533715331", "status": "open", "symbol": "GEMI-NFL-2609290015-PHI-CHI-M-PHI", "side": "buy", "outcome": "yes", "orderType": "limit", "timeInForce": "good-til-cancel", "quantity": "1", "filledQuantity": "0", "remainingQuantity": "1", "price": "0.01", "createdAt": "2026-09-28T18:21:38.338Z", "updatedAt": "2026-09-28T18:21:38.338Z", "contractMetadata": { "contractId": "658521", "contractName": "Philadelphia", "contractTicker": "PHI", "eventTicker": "NFL-2609290015-PHI-CHI-M", "eventName": "Philadelphia vs Chicago", "category": "Pro Football", "contractStatus": "Active", "eventType": "categorical", "instrumentSymbol": "GEMI-NFL-2609290015-PHI-CHI-M-PHI", "parentCategory": "Sports", "imageUrl": "https://assets.gemini.com/predictions/images/Philadelphia_EaglesDark_6dac9ad3-2c4c-4974-aa63-a2b126c2fd6f.avif", "expiryDate": "2026-09-30T12:15:00.000Z", "description": "This market resolves to Yes if Philadelphia wins the Pro Football game between Philadelphia and Chicago scheduled for September 28, 2026 at 8:15 PM ET.", "eventImageUrl": "https://assets.gemini.com/predictions/images/Football_fb67fb13-3768-40c7-a99d-cbe2644d299e.avif", "sortOrder": 1, "template": "sports-game", "color": "#004C54", "startTime": "2026-09-29T00:15:00.000Z" }, "fundsOnHold": "0.01" } ], "pagination": { "limit": 50, "offset": 0, "count": 1 } }Test 3 — cancel order
Prompt:
call gemini_cancel_prediction_order with orderId 145828836533715331, confirm true{ "result": "ok", "message": "Order 145828836533715331 cancelled successfully" }Test 4 — order history (cancelled)
Prompt:
call gemini_get_prediction_order_history with status cancelledTrimmed to the order from tests 1–3; the full response also listed two older cancelled orders on a different contract.
{ "orders": [ { "orderId": "145828836533715331", "hashOrderId": "LWmopoQGzLP9", "clientOrderId": "9e868c1b-0812-4070-bd1c-05dab5fa6537", "globalOrderId": "145828836533715331", "status": "cancelled", "symbol": "GEMI-NFL-2609290015-PHI-CHI-M-PHI", "side": "buy", "outcome": "yes", "orderType": "limit", "timeInForce": "good-til-cancel", "quantity": "1", "filledQuantity": "0", "remainingQuantity": "1", "price": "0.01", "createdAt": "2026-09-28T18:21:38.338Z", "updatedAt": "2026-09-28T18:25:55.962Z", "cancelledAt": "2026-09-28T18:25:55.962Z", "contractMetadata": { "contractId": "658521", "contractName": "Philadelphia", "contractTicker": "PHI", "eventTicker": "NFL-2609290015-PHI-CHI-M", "eventName": "Philadelphia vs Chicago", "category": "Pro Football", "contractStatus": "Active", "eventType": "categorical", "instrumentSymbol": "GEMI-NFL-2609290015-PHI-CHI-M-PHI", "parentCategory": "Sports", "expiryDate": "2026-09-30T12:15:00.000Z", "sortOrder": 1, "template": "sports-game", "color": "#004C54", "startTime": "2026-09-29T00:15:00.000Z" } } ], "pagination": { "limit": 50, "offset": 0, "count": 10 } }Test 5 — place batch
Prompt:
call gemini_place_prediction_order_batch with two orders: GEMI-NFL-2609290015-PHI-CHI-M-PHI buy yes quantity 1 price 0.01, and GEMI-NFL-2609290015-PHI-CHI-M-PHI buy yes quantity 1 price 0.02, confirm true{ "results": [ { "order": { "orderId": "145828836533852963", "hashOrderId": "gxPLwL7E6Xw5", "clientOrderId": "dff5f778-d4dc-4dee-ad65-efb904f0db41", "globalOrderId": "e8f9bd4cb57eae9c6565cc42129a72257f3990c812f9dd2d6ab39619e50010c5", "status": "open", "symbol": "GEMI-NFL-2609290015-PHI-CHI-M-PHI", "side": "buy", "outcome": "yes", "orderType": "limit", "timeInForce": "good-til-cancel", "quantity": "1", "filledQuantity": "0", "remainingQuantity": "1", "price": "0.01", "createdAt": "2026-09-28T18:31:14.529Z", "updatedAt": "2026-09-28T18:31:14.529Z" } }, { "order": { "orderId": "145828836533852965", "hashOrderId": "eKkjzjM269zY", "clientOrderId": "089bbffe-2058-41ee-b953-38545d6bc81c", "globalOrderId": "d926f4135332404ca7dfb4be6d4fbdd6f900cbeb7e373bab83ec99b5c7248bc9", "status": "open", "symbol": "GEMI-NFL-2609290015-PHI-CHI-M-PHI", "side": "buy", "outcome": "yes", "orderType": "limit", "timeInForce": "good-til-cancel", "quantity": "1", "filledQuantity": "0", "remainingQuantity": "1", "price": "0.02", "createdAt": "2026-09-28T18:31:14.555Z", "updatedAt": "2026-09-28T18:31:14.555Z" } } ] }Test 6 — cancel batch
Prompt:
call gemini_cancel_prediction_order_batch with orderIds 145828836533852963 and 145828836533852965, confirm true{ "results": [ { "orderId": "145828836533852963", "result": "ok" }, { "orderId": "145828836533852965", "result": "ok" } ] }Test 7 — maker-or-cancel order
Prompt:
call gemini_place_prediction_order with symbol GEMI-NFL-2609290015-PHI-CHI-M-PHI, side buy, outcome yes, quantity 1, price 0.01, timeInForce maker-or-cancel, confirm trueThe SDK sent
makerOrCancel: truewith notimeInForce; Gemini echoes it back as amaker-or-cancelorder, same as the legacy client's wire value produced.{ "orderId": "145828836534646592", "hashOrderId": "omyRYRMX1vOx", "clientOrderId": "80fc78f7-ab7e-4bcc-a5a2-8486613846c9", "globalOrderId": "cd49f3f4f9cfd3d7c42f312ea5915f83af7de724d60f458a322188207e18ae96", "status": "open", "symbol": "GEMI-NFL-2609290015-PHI-CHI-M-PHI", "side": "buy", "outcome": "yes", "orderType": "limit", "timeInForce": "maker-or-cancel", "quantity": "1", "filledQuantity": "0", "remainingQuantity": "1", "price": "0.01", "createdAt": "2026-09-28T19:23:25.340Z", "updatedAt": "2026-09-28T19:23:25.340Z" }Cancelled the maker-or-cancel order from test 7:
Prompt:
call gemini_cancel_prediction_order with orderId 145828836534646592, confirm true{ "result": "ok", "message": "Order 145828836534646592 cancelled successfully" }Test 8 — error detail
Prompt:
call gemini_cancel_prediction_order with orderId 1, confirm trueTest 9 — confirm gate
Prompt:
call gemini_place_prediction_order with symbol GEMI-NFL-2609290015-PHI-CHI-M-PHI, side buy, outcome yes, quantity 1, price 0.01The MCP client saw that the tool's schema requires
confirm: trueand asked for explicit confirmation instead of calling the tool, so no order was placed. The server-side rejection of a missingconfirmis covered by the unit tests for all four destructive order tools.Final check — nothing left open
Prompt:
call gemini_get_prediction_active_orders{ "orders": [], "pagination": { "limit": 50, "offset": 0, "count": 0 } }