Skip to content

feat(mcp-server): migrate prediction orders REST endpoints to SDK (PREDICT-8817) - #74

Merged
karanach319 merged 2 commits into
mainfrom
predict-8817-migrate-prediction-orders-rest-endpoints-to-sdk
Sep 28, 2026
Merged

karanach319 merged 2 commits into
mainfrom
predict-8817-migrate-prediction-orders-rest-endpoints-to-sdk

Conversation

@karanach319

@karanach319 karanach319 commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Whats included

  • Migrates the 6 prediction "orders" REST endpoints (placeOrder, cancelOrder, placeOrderBatch, cancelOrderBatch, getActiveOrders, getOrderHistory) off the legacy hand-rolled GeminiHttpClient and onto the @gemini-markets/sdk client (sdkClient). createPredictionOrderTools now takes sdkClient. This is the last prediction tool group on the legacy client — no prediction datasource or tool imports it after this.
  • maker-or-cancel mapping. The tool still offers timeInForce: "maker-or-cancel", but the SDK's TimeInForce enum has no such value — maker-or-cancel is its own makerOrCancel boolean, and the SDK's request validation rejects the old value before anything is sent. The datasource now sends makerOrCancel: true (with no timeInForce) for that option and makerOrCancel: false otherwise, so the tool contract is unchanged. Confirmed live: Gemini echoes the order back as timeInForce: "maker-or-cancel" (test 7).
  • Order ID precision. The SDK decodes every orderId to bigint; the datasource turns them back into exact strings (never Number(), which loses precision on these 17–18 digit IDs, and bigint isn't serializable by JSON.stringify). The single cancel converts the tool's string ID with BigInt(), since the SDK types that field as bigint.
  • Output unchanged. The legacy client passed order responses through unmapped, so each response is spread through as-is with only orderId stringified — every field Gemini sends still reaches the tool output. Batch results stay positional, with rejected entries passed through verbatim.
  • Sub-account scope for orders and combos. The legacy client added GEMINI_ACCOUNT to every signed request, so a master API key reads and trades on the configured sub-account. A new withAccountScope() helper in client/sdk.ts keeps 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), and getVolumeMetrics (which already did this inline). Public calls (listCombos, getCombo) never send it.
  • Known gap, not addressed here: positions sub-account scope. Since feat(mcp-server): migrate prediction positions REST endpoints to SDK (PREDICT-8818) #68, gemini_get_prediction_positions and gemini_get_prediction_settled_positions read the primary account even with GEMINI_ACCOUNT set. Both SDK operations are query-only, so the SDK sends only their declared query fields and signs no body — an added account key 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 the withAccountScope() comment documents the limitation.
  • No changes to tool names, schemas, descriptions, or the confirm: true gate on the four destructive tools.

Linear: https://linear.app/gemini/issue/PREDICT-8817

Test plan

  • npm run build — clean
  • npm run typecheck — clean
  • npm test — 339/339 passing, including:
    • the exact SDK input for all 6 order functions (maker-or-cancel → makerOrCancel: true, exact BigInt cancel ID, filters only sent when set)
    • GEMINI_ACCOUNT added to every order call and createCombo when set, and never when unset or on public calls
    • through the real SDK with a fake fetch: a maker-or-cancel order is accepted and sent as makerOrCancel: true; the old timeInForce: "maker-or-cancel" value is rejected before sending (regression guard); the cancel orderId goes out as an exact 18-digit JSON number; account reaches both the signed payload and the request body; an 18-digit orderId in a raw response survives as the exact string
    • confirm: true required on all 4 destructive order tools; mixed batch results kept in order; SDK errors surface with reason/code
    • existing combos tests no longer break when a developer's own GEMINI_ACCOUNT is set
  • Confirmed no file under datasources/predictions/ or tools/predictions/ imports GeminiHttpClient
  • Tested every order tool against production through the MCP server running @gemini-markets/sdk@0.1.1, using 1-contract buys at $0.01–$0.02 on GEMI-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 cancelled

Trimmed 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 true

The SDK sent makerOrCancel: true with no timeInForce; Gemini echoes it back as a maker-or-cancel order, 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 true

Error: HTTP 404 (reason=OrderNotFound, code=order_not_found, category=not_found)

Test 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.01

The MCP client saw that the tool's schema requires confirm: true and asked for explicit confirmation instead of calling the tool, so no order was placed. The server-side rejection of a missing confirm is covered by the unit tests for all four destructive order tools.

This is an order-placement action. Please explicitly confirm by saying "confirm true" before I place:

* Symbol: `GEMI-NFL-2609290015-PHI-CHI-M-PHI`
* Side/outcome: Buy Yes
* Quantity: 1
* Limit price: `0.01`

Final check — nothing left open

Prompt: call gemini_get_prediction_active_orders

{
  "orders": [],
  "pagination": {
    "limit": 50,
    "offset": 0,
    "count": 0
  }
}

…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.
@linear-code

linear-code Bot commented Sep 28, 2026

Copy link
Copy Markdown

PREDICT-8817

@nostradamus-bot

Copy link
Copy Markdown

Nostradamus Risk Rating — Medium

The PR migrates 6 prediction order endpoints in datasources/predictions/orders.ts from a legacy HTTP client to the official @gemini-markets/sdk, touching High-sensitivity financial transaction paths; however, no new endpoints, auth primitives, or attack surface are introduced, the confirm: true gates on destructive tools are preserved, and the withAccountScope() addition in client/sdk.ts fixes a sub-account scoping regression rather than introducing new risk.

@svc-grace svc-grace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/mcp-server/src/datasources/predictions/positions.ts Outdated
…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.
@sdlc-pr-bot

sdlc-pr-bot Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Change Control Evidence Check

Authorization — ✅ Pass

Evidence:

  • Linear ticket: PREDICT-8817
  • Change owner: Karan Acharya
  • Type of change: Normal
Testing — ✅ Pass

Evidence:

  • Tests from CI checks: ➖ No tests ran
  • Security scans: ✅ Passed
  • Testing summary: ✅ Found in PR description
    • Summary: Build compilation, type checking, and all 339 unit tests passed, including verification of correct SDK input parameters for order functions, proper account header handling, and maker-or-cancel order processing through the SDK.
  • Evidence link: View run
Approval — ✅ Pass

Evidence:

  • Approver: Kevin Nguy
  • Approval source: GitHub PR Review
  • Approval timestamp: 2026-09-28 21:52 UTC
Segregation of Duties — ✅ Pass

Validated:

  • PR author: karanach319
  • Commit author(s): karanach319
  • Linear assignee: Karan Acharya
  • Approver: Kevin Nguy
  • Result: Implementer and approver are different people ✅

Last checked: 2026-09-28 22:01 UTC

↩ Re-run Clarissa if tickets, CI, or approvals have changed.

@svc-grace svc-grace left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@karanach319
karanach319 merged commit fdf5a5d into main Sep 28, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants