From 716fc28e7fa5220a8fa85651b804fd7b2e5f9be2 Mon Sep 17 00:00:00 2001 From: Kevin Nguy Date: Wed, 16 Sep 2026 16:50:15 -0700 Subject: [PATCH 1/6] feat: add checked-in API contract conformance --- .gitattributes | 2 + .github/CODEOWNERS | 4 + .github/workflows/validate-go-sdk.yml | 2 + .github/workflows/validate-typescript-sdk.yml | 2 + conformance/README.md | 189 + .../http/errors/insufficient-funds-406.json | 13 + .../http/errors/invalid-nonce-400.json | 13 + .../http/errors/market-closed-400.json | 13 + .../missing-role-403-error-envelope.json | 13 + .../missing-role-403-result-envelope.json | 13 + .../http/errors/not-found-404-empty-body.json | 12 + .../http/errors/order-not-found-404.json | 13 + .../errors/rate-limited-429-retry-after.json | 17 + .../errors/server-error-500-unstructured.json | 12 + ...ms-required-400-accept-terms-required.json | 13 + .../terms-required-400-must-accept.json | 13 + .../private-post-empty-body.json | 38 + .../private-post-wide-integer.json | 39 + .../hmac-requests/private-post-with-body.json | 51 + .../websocket-upgrade-nonce.json | 26 + .../market-data-order-book-limits.json | 19 + .../unsigned-requests/market-data-ticker.json | 14 + ...rediction-markets-list-events-filters.json | 16 + conformance/json/decimal-number-exact.json | 10 + conformance/json/decimal-string-exact.json | 10 + conformance/json/wide-integer-safe.json | 10 + conformance/json/wide-integer-unsafe.json | 13 + conformance/manifest.json | 86 + .../websocket/events/balance-update.json | 13 + .../events/depth-unsafe-last-update-id.json | 16 + .../websocket/events/order-update.json | 17 + conformance/websocket/events/trade.json | 21 + .../private-balances-interval.json | 12 + .../subscriptions/private-orders-session.json | 12 + .../subscriptions/public-book-ticker.json | 10 + .../subscriptions/public-contract-status.json | 10 + .../public-depth-diff-interval.json | 13 + .../subscriptions/public-depth-diff.json | 10 + .../public-partial-depth-20.json | 13 + .../public-trades-untrimmed-symbol.json | 10 + .../subscriptions/public-trades.json | 13 + packages/sdk-go/conformance/errors_test.go | 137 + packages/sdk-go/conformance/fixtures_test.go | 196 + .../sdk-go/conformance/hmac_requests_test.go | 246 + .../sdk-go/conformance/json_decoding_test.go | 66 + packages/sdk-go/conformance/manifest_test.go | 154 + .../conformance/unsigned_requests_test.go | 149 + packages/sdk-go/conformance/ws_events_test.go | 185 + .../conformance/ws_subscriptions_test.go | 280 + .../sdk-go/generated/predictions/types.gen.go | 3 +- .../sdk-go/generated/trading/types.gen.go | 3 + packages/sdk-go/scripts/contract_test.go | 92 +- packages/sdk-go/scripts/generate.go | 650 +- packages/sdk-go/scripts/release_smoke.sh | 3 + .../sdk-go/scripts/service_coverage_test.go | 19 +- .../sdk-go/scripts/websocket_coverage_test.go | 6 +- packages/sdk-go/transport/errors.go | 52 +- .../scripts/api-surface.snapshot.json | 54 +- .../scripts/generate-market-data.mjs | 83 +- .../scripts/generate-prediction-markets.mjs | 95 +- .../scripts/generate-rest-modules.mjs | 82 +- .../scripts/generate-rest-ownership.mjs | 21 +- .../scripts/generate-ws-types.mjs | 68 +- .../scripts/generated-drift.test.mjs | 54 + .../scripts/numeric-overlay.test.mjs | 83 + .../scripts/openapi-rest-generator.mjs | 29 +- .../sdk-typescript/scripts/spec-sources.mjs | 66 +- .../scripts/websocket-compatibility.mjs | 39 +- .../scripts/websocket-types.test.mjs | 22 +- packages/sdk-typescript/src/errors.ts | 4 + .../src/generated/market-data/models.ts | 50 +- .../sdk-typescript/src/generated/models.ts | 2 +- .../src/generated/websocket/index.ts | 52 +- .../src/tests/conformance/errors.test.ts | 129 + .../tests/conformance/hmac-requests.test.ts | 143 + .../tests/conformance/json-decoding.test.ts | 50 + .../src/tests/conformance/manifest.test.ts | 160 + .../src/tests/conformance/support/fixtures.ts | 75 + .../conformance/unsigned-requests.test.ts | 95 + .../src/tests/conformance/ws-events.test.ts | 118 + .../conformance/ws-subscriptions.test.ts | 120 + .../sdk-typescript/src/websocket/public.ts | 5 +- specs/SOURCES.json | 26 + specs/asyncapi/websocket.yaml | 1618 +++ specs/openapi/prediction-markets.yaml | 5188 ++++++++ specs/openapi/rest.yaml | 10475 ++++++++++++++++ specs/overlays/gemini-wire-exceptions.yaml | 37 + specs/overlays/numeric-types.yaml | 78 + specs/refresh.mjs | 26 + 89 files changed, 21693 insertions(+), 541 deletions(-) create mode 100644 conformance/README.md create mode 100644 conformance/http/errors/insufficient-funds-406.json create mode 100644 conformance/http/errors/invalid-nonce-400.json create mode 100644 conformance/http/errors/market-closed-400.json create mode 100644 conformance/http/errors/missing-role-403-error-envelope.json create mode 100644 conformance/http/errors/missing-role-403-result-envelope.json create mode 100644 conformance/http/errors/not-found-404-empty-body.json create mode 100644 conformance/http/errors/order-not-found-404.json create mode 100644 conformance/http/errors/rate-limited-429-retry-after.json create mode 100644 conformance/http/errors/server-error-500-unstructured.json create mode 100644 conformance/http/errors/terms-required-400-accept-terms-required.json create mode 100644 conformance/http/errors/terms-required-400-must-accept.json create mode 100644 conformance/http/hmac-requests/private-post-empty-body.json create mode 100644 conformance/http/hmac-requests/private-post-wide-integer.json create mode 100644 conformance/http/hmac-requests/private-post-with-body.json create mode 100644 conformance/http/hmac-requests/websocket-upgrade-nonce.json create mode 100644 conformance/http/unsigned-requests/market-data-order-book-limits.json create mode 100644 conformance/http/unsigned-requests/market-data-ticker.json create mode 100644 conformance/http/unsigned-requests/prediction-markets-list-events-filters.json create mode 100644 conformance/json/decimal-number-exact.json create mode 100644 conformance/json/decimal-string-exact.json create mode 100644 conformance/json/wide-integer-safe.json create mode 100644 conformance/json/wide-integer-unsafe.json create mode 100644 conformance/manifest.json create mode 100644 conformance/websocket/events/balance-update.json create mode 100644 conformance/websocket/events/depth-unsafe-last-update-id.json create mode 100644 conformance/websocket/events/order-update.json create mode 100644 conformance/websocket/events/trade.json create mode 100644 conformance/websocket/subscriptions/private-balances-interval.json create mode 100644 conformance/websocket/subscriptions/private-orders-session.json create mode 100644 conformance/websocket/subscriptions/public-book-ticker.json create mode 100644 conformance/websocket/subscriptions/public-contract-status.json create mode 100644 conformance/websocket/subscriptions/public-depth-diff-interval.json create mode 100644 conformance/websocket/subscriptions/public-depth-diff.json create mode 100644 conformance/websocket/subscriptions/public-partial-depth-20.json create mode 100644 conformance/websocket/subscriptions/public-trades-untrimmed-symbol.json create mode 100644 conformance/websocket/subscriptions/public-trades.json create mode 100644 packages/sdk-go/conformance/errors_test.go create mode 100644 packages/sdk-go/conformance/fixtures_test.go create mode 100644 packages/sdk-go/conformance/hmac_requests_test.go create mode 100644 packages/sdk-go/conformance/json_decoding_test.go create mode 100644 packages/sdk-go/conformance/manifest_test.go create mode 100644 packages/sdk-go/conformance/unsigned_requests_test.go create mode 100644 packages/sdk-go/conformance/ws_events_test.go create mode 100644 packages/sdk-go/conformance/ws_subscriptions_test.go create mode 100644 packages/sdk-typescript/scripts/generated-drift.test.mjs create mode 100644 packages/sdk-typescript/scripts/numeric-overlay.test.mjs create mode 100644 packages/sdk-typescript/src/tests/conformance/errors.test.ts create mode 100644 packages/sdk-typescript/src/tests/conformance/hmac-requests.test.ts create mode 100644 packages/sdk-typescript/src/tests/conformance/json-decoding.test.ts create mode 100644 packages/sdk-typescript/src/tests/conformance/manifest.test.ts create mode 100644 packages/sdk-typescript/src/tests/conformance/support/fixtures.ts create mode 100644 packages/sdk-typescript/src/tests/conformance/unsigned-requests.test.ts create mode 100644 packages/sdk-typescript/src/tests/conformance/ws-events.test.ts create mode 100644 packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts create mode 100644 specs/SOURCES.json create mode 100644 specs/asyncapi/websocket.yaml create mode 100644 specs/openapi/prediction-markets.yaml create mode 100644 specs/openapi/rest.yaml create mode 100644 specs/overlays/gemini-wire-exceptions.yaml create mode 100644 specs/overlays/numeric-types.yaml create mode 100644 specs/refresh.mjs diff --git a/.gitattributes b/.gitattributes index d207b180..e244b07f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ *.go text eol=lf +specs/** -text +conformance/** text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 90c06fc2..62efce59 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,3 +13,7 @@ # Published Go SDK source, module metadata, and release workflow. /packages/sdk-go/** @fuller @ximt + +# Cross-SDK API contract and conformance fixtures. +/specs/** @fuller @ximt +/conformance/** @fuller @ximt diff --git a/.github/workflows/validate-go-sdk.yml b/.github/workflows/validate-go-sdk.yml index 60493791..6546a4f3 100644 --- a/.github/workflows/validate-go-sdk.yml +++ b/.github/workflows/validate-go-sdk.yml @@ -4,6 +4,8 @@ on: pull_request: paths: - "packages/sdk-go/**" + - "specs/**" + - "conformance/**" - ".github/workflows/validate-go-sdk.yml" - ".github/workflows/release-go-sdk.yml" diff --git a/.github/workflows/validate-typescript-sdk.yml b/.github/workflows/validate-typescript-sdk.yml index bff8ebd4..53ae7763 100644 --- a/.github/workflows/validate-typescript-sdk.yml +++ b/.github/workflows/validate-typescript-sdk.yml @@ -4,6 +4,8 @@ on: pull_request: paths: - "packages/sdk-typescript/**" + - "specs/**" + - "conformance/**" - ".github/workflows/validate-typescript-sdk.yml" permissions: diff --git a/conformance/README.md b/conformance/README.md new file mode 100644 index 00000000..35bf2306 --- /dev/null +++ b/conformance/README.md @@ -0,0 +1,189 @@ +# API contract conformance fixtures + +This directory is the language-neutral wire contract for the Gemini SDKs. The +fixtures are deterministic, contain no credentials that grant access, and do +not call the network. A runner loads `manifest.json`, executes every case in +its listed suite, and asserts the observable request, response, decoded value, +or WebSocket frame described by that case. + +## Manifest and layout + +`manifest.json` is the single case enumeration. Each suite has an `id`, a +`kind`, and a list of case file names. A case is stored at +`conformance//.json`; for example, +`http/errors/not-found-404-empty-body.json`. The top-level `exceptions` array +is the authoritative list of reviewed cross-SDK tolerances. It must contain +the same IDs as `specs/overlays/gemini-wire-exceptions.yaml`. + +Runners enforce the contract in both directions: every manifest entry must +have exactly one JSON file, and every fixture JSON file (other than the +manifest itself) must be listed. They also reject a manifest that silently +shrinks coverage. Case-level `exceptions` references must be declared in the +wire-exceptions overlay, and overlay IDs must be represented in the manifest +(the overlay may intentionally declare an ID with no cases). + +All files are UTF-8 JSON with LF line endings. Values that are significant on +the wire are represented as strings or raw JSON numbers in the fixture; a +runner must preserve number text rather than passing unsafe integers through a +platform-sized floating-point value. + +## Case schemas + +The five `kind` values are described below. Fields not marked optional are +required. `id` is always the full path `/` and `kind` must +match its manifest suite. + +### `hmacRequest` + +```json +{ + "id": "http/hmac-requests/example", + "kind": "hmacRequest", + "credentials": { "apiKey": "...", "apiSecret": "..." }, + "nonce": { "mode": "monotonic", "value": "1700000000000" }, + "request": { "method": "POST", "path": "/v1/order/new", "body": {} }, + "expect": { + "headers": ["X-GEMINI-APIKEY", "X-GEMINI-PAYLOAD", "X-GEMINI-SIGNATURE"], + "apiKeyHeader": "...", + "payload": { "request": "/v1/order/new", "nonce": "...", "fields": {} }, + "signature": { "algorithm": "HMAC-SHA384", "over": "payloadBase64", "encoding": "hex-lower" } + }, + "exceptions": ["rest-nonce-json-type"] +} +``` + +`nonce.mode` is `monotonic` (the exact decimal `value` is supplied) or +`websocket` (no exact value is supplied). For a monotonic request, the runner +sends the request through the SDK's HTTP signing seam. Every expected header +must be present and non-empty; the API-key header must equal `apiKeyHeader`. +Base64-decode `X-GEMINI-PAYLOAD`, parse it losslessly, and assert `request`, +nonce decimal text (allowing optional JSON string quotes), and every declared +field's raw JSON value. The payload may contain only `request`, `nonce`, and +the declared fields. Recompute HMAC-SHA384 over the base64 payload text and +compare the lowercase hexadecimal signature. For `websocket` nonce cases, +assert that `X-GEMINI-PAYLOAD` decodes to the `X-GEMINI-NONCE` value, that the +nonce is ten decimal epoch-second digits, and that the signature verifies. + +### `unsignedRequest` + +```json +{ + "id": "http/unsigned-requests/example", + "kind": "unsignedRequest", + "operation": "marketData.getTicker", + "input": { "symbol": "BTCUSD" }, + "expect": { + "method": "GET", + "path": "/v1/pubticker/BTCUSD", + "query": {}, + "authHeaders": [] + } +} +``` + +`operation` identifies the SDK service wrapper. The runner invokes it with +`input` through a capturing HTTP transport and asserts method, path, query +values, and absence of authentication headers. Array query values are +repeated plain keys (not comma-joined); query object ordering is not +significant. + +### `errorMapping` + +```json +{ + "id": "http/errors/example", + "kind": "errorMapping", + "response": { + "status": 403, + "headers": { "content-type": "application/json" }, + "body": "{\"result\":\"error\",\"reason\":\"MissingRole\"}" + }, + "expect": { "kind": "missing_role", "reason": "MissingRole" } +} +``` + +`response.body` is verbatim response text. Runners feed the status, headers, +and body through their SDK transport and map the canonical expected kinds: +`invalid_nonce`, `missing_nonce`, `invalid_signature`, `missing_role`, +`terms_required`, `insufficient_funds`, `rate_limited`, `order_not_found`, +`market_closed`, `not_found`, `service_error`, and `invalid_request`. +Observed status must match. If `reason` is present, it must be preserved on +the resulting error. The rate-limit case also has +`retryAfterSeconds` and requires the SDK's retry-after metadata to equal that +number (three seconds in the checked-in fixture). + +### `jsonDecoding` + +```json +{ + "id": "json/example", + "kind": "jsonDecoding", + "raw": "{\"v\":9007199254740993}", + "field": "v", + "expect": { "valueKind": "integer", "text": "9007199254740993" } +} +``` + +`raw` is the exact JSON input and `field` identifies the decoded value. +`valueKind` is `integer` or `decimal`. The runner decodes with its lossless +JSON path, or with the language's exact integer/decimal type, and renders the +value as the expected decimal text. Unsafe integers must never be rounded; +decimal strings and decimal JSON numbers must retain their exact value. + +### `wsSubscription` + +```json +{ + "id": "websocket/subscriptions/example", + "kind": "wsSubscription", + "stream": "trades", + "symbol": "BTCUSD", + "options": {}, + "expect": { "method": "SUBSCRIBE", "params": ["btcusd@trade"] }, + "exceptions": ["ws-subscription-id-scope"] +} +``` + +`stream` is one of `trades`, `bookTicker`, `depthUpdates`, `partialDepth`, +`contractStatus`, `orders`, `balances`, or `positions`. `symbol` is required +for symbol streams and omitted for global/private streams. `options` may +contain `intervalMs` (100 or 1000 as applicable), `levels` (5, 10, or 20), or +private `scope` (`account` or `session`). The runner maps the stream to the +SDK's typed call, captures the single wire frame, and asserts `method`, exact +`params`, and a positive integer `id`. It must acknowledge the request locally; +no network or wall-clock value is needed for this assertion. + +### `wsEvent` + +```json +{ + "id": "websocket/events/example", + "kind": "wsEvent", + "stream": "trades", + "symbol": "BTCUSD", + "frame": "{\"e\":\"trade\",\"s\":\"btcusd\"}", + "expect": { + "fields": { + "s": { "text": "BTCUSD", "compare": "caseInsensitive" } + } + }, + "exceptions": ["ws-inbound-symbol-case"] +} +``` + +`frame` is verbatim inbound WebSocket JSON. `stream` selects the typed SDK +subscription and `expect.fields` names decoded wire fields. Each field has +expected rendered `text` and `compare` of `exact` or `caseInsensitive`. +Runners assert that events are delivered to the selected subscription; the +case-insensitive comparison is used only for the reviewed symbol-case +exception. Wide update IDs are compared by decimal text. + +## Implementing another runner + +A new SDK runner should iterate suites from `manifest.json` rather than +hard-code case names, load each case from its suite directory, and implement +all five schemas above. It should use an in-memory HTTP/WebSocket double, +perform independent signature verification, preserve raw numeric text, and +fail on missing, extra, or unlisted fixtures. It must also compare the +manifest exception IDs and every case exception reference with the reviewed +wire-exceptions overlay before executing behavioral cases. diff --git a/conformance/http/errors/insufficient-funds-406.json b/conformance/http/errors/insufficient-funds-406.json new file mode 100644 index 00000000..f1410227 --- /dev/null +++ b/conformance/http/errors/insufficient-funds-406.json @@ -0,0 +1,13 @@ +{ + "id": "http/errors/insufficient-funds-406", + "kind": "errorMapping", + "response": { + "status": 406, + "headers": { "content-type": "application/json" }, + "body": "{\"result\":\"error\",\"reason\":\"InsufficientFunds\",\"message\":\"Insufficient funds\"}" + }, + "expect": { + "kind": "insufficient_funds", + "reason": "InsufficientFunds" + } +} diff --git a/conformance/http/errors/invalid-nonce-400.json b/conformance/http/errors/invalid-nonce-400.json new file mode 100644 index 00000000..f7862e6b --- /dev/null +++ b/conformance/http/errors/invalid-nonce-400.json @@ -0,0 +1,13 @@ +{ + "id": "http/errors/invalid-nonce-400", + "kind": "errorMapping", + "response": { + "status": 400, + "headers": { "content-type": "application/json" }, + "body": "{\"result\":\"error\",\"reason\":\"InvalidNonce\",\"message\":\"Nonce must be greater than the previous nonce\"}" + }, + "expect": { + "kind": "invalid_nonce", + "reason": "InvalidNonce" + } +} diff --git a/conformance/http/errors/market-closed-400.json b/conformance/http/errors/market-closed-400.json new file mode 100644 index 00000000..3cea6e70 --- /dev/null +++ b/conformance/http/errors/market-closed-400.json @@ -0,0 +1,13 @@ +{ + "id": "http/errors/market-closed-400", + "kind": "errorMapping", + "response": { + "status": 400, + "headers": { "content-type": "application/json" }, + "body": "{\"result\":\"error\",\"reason\":\"MarketClosed\",\"message\":\"Market is closed\"}" + }, + "expect": { + "kind": "market_closed", + "reason": "MarketClosed" + } +} diff --git a/conformance/http/errors/missing-role-403-error-envelope.json b/conformance/http/errors/missing-role-403-error-envelope.json new file mode 100644 index 00000000..c953f4fe --- /dev/null +++ b/conformance/http/errors/missing-role-403-error-envelope.json @@ -0,0 +1,13 @@ +{ + "id": "http/errors/missing-role-403-error-envelope", + "kind": "errorMapping", + "response": { + "status": 403, + "headers": { "content-type": "application/json" }, + "body": "{\"error\":\"MissingRole\",\"message\":\"OrderStatus required\"}" + }, + "expect": { + "kind": "missing_role", + "reason": "MissingRole" + } +} diff --git a/conformance/http/errors/missing-role-403-result-envelope.json b/conformance/http/errors/missing-role-403-result-envelope.json new file mode 100644 index 00000000..ba48ce22 --- /dev/null +++ b/conformance/http/errors/missing-role-403-result-envelope.json @@ -0,0 +1,13 @@ +{ + "id": "http/errors/missing-role-403-result-envelope", + "kind": "errorMapping", + "response": { + "status": 403, + "headers": { "content-type": "application/json" }, + "body": "{\"result\":\"error\",\"reason\":\"MissingRole\",\"message\":\"OrderStatus required\"}" + }, + "expect": { + "kind": "missing_role", + "reason": "MissingRole" + } +} diff --git a/conformance/http/errors/not-found-404-empty-body.json b/conformance/http/errors/not-found-404-empty-body.json new file mode 100644 index 00000000..db305347 --- /dev/null +++ b/conformance/http/errors/not-found-404-empty-body.json @@ -0,0 +1,12 @@ +{ + "id": "http/errors/not-found-404-empty-body", + "kind": "errorMapping", + "response": { + "status": 404, + "headers": {}, + "body": "" + }, + "expect": { + "kind": "not_found" + } +} diff --git a/conformance/http/errors/order-not-found-404.json b/conformance/http/errors/order-not-found-404.json new file mode 100644 index 00000000..40c0b121 --- /dev/null +++ b/conformance/http/errors/order-not-found-404.json @@ -0,0 +1,13 @@ +{ + "id": "http/errors/order-not-found-404", + "kind": "errorMapping", + "response": { + "status": 404, + "headers": { "content-type": "application/json" }, + "body": "{\"result\":\"error\",\"reason\":\"OrderNotFound\",\"message\":\"Order not found\"}" + }, + "expect": { + "kind": "order_not_found", + "reason": "OrderNotFound" + } +} diff --git a/conformance/http/errors/rate-limited-429-retry-after.json b/conformance/http/errors/rate-limited-429-retry-after.json new file mode 100644 index 00000000..be6c950b --- /dev/null +++ b/conformance/http/errors/rate-limited-429-retry-after.json @@ -0,0 +1,17 @@ +{ + "id": "http/errors/rate-limited-429-retry-after", + "kind": "errorMapping", + "response": { + "status": 429, + "headers": { + "content-type": "application/json", + "retry-after": "3" + }, + "body": "{\"result\":\"error\",\"reason\":\"RateLimit\",\"message\":\"Rate limit exceeded\"}" + }, + "expect": { + "kind": "rate_limited", + "reason": "RateLimit", + "retryAfterSeconds": 3 + } +} diff --git a/conformance/http/errors/server-error-500-unstructured.json b/conformance/http/errors/server-error-500-unstructured.json new file mode 100644 index 00000000..641785f5 --- /dev/null +++ b/conformance/http/errors/server-error-500-unstructured.json @@ -0,0 +1,12 @@ +{ + "id": "http/errors/server-error-500-unstructured", + "kind": "errorMapping", + "response": { + "status": 500, + "headers": { "content-type": "text/plain" }, + "body": "upstream unavailable" + }, + "expect": { + "kind": "service_error" + } +} diff --git a/conformance/http/errors/terms-required-400-accept-terms-required.json b/conformance/http/errors/terms-required-400-accept-terms-required.json new file mode 100644 index 00000000..f150a728 --- /dev/null +++ b/conformance/http/errors/terms-required-400-accept-terms-required.json @@ -0,0 +1,13 @@ +{ + "id": "http/errors/terms-required-400-accept-terms-required", + "kind": "errorMapping", + "response": { + "status": 400, + "headers": { "content-type": "application/json" }, + "body": "{\"result\":\"error\",\"reason\":\"AcceptTermsRequired\",\"message\":\"Terms must be accepted before placing orders\"}" + }, + "expect": { + "kind": "terms_required", + "reason": "AcceptTermsRequired" + } +} diff --git a/conformance/http/errors/terms-required-400-must-accept.json b/conformance/http/errors/terms-required-400-must-accept.json new file mode 100644 index 00000000..1e2a4b41 --- /dev/null +++ b/conformance/http/errors/terms-required-400-must-accept.json @@ -0,0 +1,13 @@ +{ + "id": "http/errors/terms-required-400-must-accept", + "kind": "errorMapping", + "response": { + "status": 400, + "headers": { "content-type": "application/json" }, + "body": "{\"result\":\"error\",\"reason\":\"MustAccepTerms\",\"message\":\"Terms must be accepted before placing orders\"}" + }, + "expect": { + "kind": "terms_required", + "reason": "MustAccepTerms" + } +} diff --git a/conformance/http/hmac-requests/private-post-empty-body.json b/conformance/http/hmac-requests/private-post-empty-body.json new file mode 100644 index 00000000..764e3cb5 --- /dev/null +++ b/conformance/http/hmac-requests/private-post-empty-body.json @@ -0,0 +1,38 @@ +{ + "id": "http/hmac-requests/private-post-empty-body", + "kind": "hmacRequest", + "credentials": { + "apiKey": "conformance-api-key", + "apiSecret": "conformance-api-secret" + }, + "nonce": { + "mode": "monotonic", + "value": "1700000000001" + }, + "request": { + "method": "POST", + "path": "/v1/order/status", + "body": {} + }, + "expect": { + "headers": [ + "X-GEMINI-APIKEY", + "X-GEMINI-PAYLOAD", + "X-GEMINI-SIGNATURE" + ], + "apiKeyHeader": "conformance-api-key", + "payload": { + "request": "/v1/order/status", + "nonce": "1700000000001", + "fields": {} + }, + "signature": { + "algorithm": "HMAC-SHA384", + "over": "payloadBase64", + "encoding": "hex-lower" + } + }, + "exceptions": [ + "rest-nonce-json-type" + ] +} diff --git a/conformance/http/hmac-requests/private-post-wide-integer.json b/conformance/http/hmac-requests/private-post-wide-integer.json new file mode 100644 index 00000000..f485fd22 --- /dev/null +++ b/conformance/http/hmac-requests/private-post-wide-integer.json @@ -0,0 +1,39 @@ +{ + "id": "http/hmac-requests/private-post-wide-integer", + "kind": "hmacRequest", + "credentials": { + "apiKey": "conformance-api-key", + "apiSecret": "conformance-api-secret" + }, + "nonce": { + "mode": "monotonic", + "value": "1700000000002" + }, + "request": { + "method": "POST", + "path": "/v1/order/status", + "body": { + "order_id": 9007199254740993 + } + }, + "expect": { + "headers": [ + "X-GEMINI-APIKEY", + "X-GEMINI-PAYLOAD", + "X-GEMINI-SIGNATURE" + ], + "apiKeyHeader": "conformance-api-key", + "payload": { + "request": "/v1/order/status", + "nonce": "1700000000002", + "fields": { + "order_id": 9007199254740993 + } + }, + "signature": { + "algorithm": "HMAC-SHA384", + "over": "payloadBase64", + "encoding": "hex-lower" + } + } +} diff --git a/conformance/http/hmac-requests/private-post-with-body.json b/conformance/http/hmac-requests/private-post-with-body.json new file mode 100644 index 00000000..ca9d8b4d --- /dev/null +++ b/conformance/http/hmac-requests/private-post-with-body.json @@ -0,0 +1,51 @@ +{ + "id": "http/hmac-requests/private-post-with-body", + "kind": "hmacRequest", + "credentials": { + "apiKey": "conformance-api-key", + "apiSecret": "conformance-api-secret" + }, + "nonce": { + "mode": "monotonic", + "value": "1700000000000" + }, + "request": { + "method": "POST", + "path": "/v1/order/new", + "body": { + "symbol": "BTCUSD", + "amount": "0.01", + "price": "1000.00", + "side": "buy", + "type": "exchange limit" + } + }, + "expect": { + "headers": [ + "X-GEMINI-APIKEY", + "X-GEMINI-PAYLOAD", + "X-GEMINI-SIGNATURE" + ], + "apiKeyHeader": "conformance-api-key", + "payload": { + "request": "/v1/order/new", + "nonce": "1700000000000", + "fields": { + "symbol": "BTCUSD", + "amount": "0.01", + "price": "1000.00", + "side": "buy", + "type": "exchange limit" + } + }, + "signature": { + "algorithm": "HMAC-SHA384", + "over": "payloadBase64", + "encoding": "hex-lower" + } + }, + "exceptions": [ + "rest-nonce-json-type", + "rest-payload-key-order" + ] +} diff --git a/conformance/http/hmac-requests/websocket-upgrade-nonce.json b/conformance/http/hmac-requests/websocket-upgrade-nonce.json new file mode 100644 index 00000000..e5aac88a --- /dev/null +++ b/conformance/http/hmac-requests/websocket-upgrade-nonce.json @@ -0,0 +1,26 @@ +{ + "id": "http/hmac-requests/websocket-upgrade-nonce", + "kind": "hmacRequest", + "credentials": { + "apiKey": "conformance-api-key", + "apiSecret": "conformance-api-secret" + }, + "nonce": { + "mode": "websocket" + }, + "expect": { + "headers": [ + "X-GEMINI-APIKEY", + "X-GEMINI-NONCE", + "X-GEMINI-PAYLOAD", + "X-GEMINI-SIGNATURE" + ], + "apiKeyHeader": "conformance-api-key", + "payload": {}, + "signature": { + "algorithm": "HMAC-SHA384", + "over": "payloadBase64", + "encoding": "hex-lower" + } + } +} diff --git a/conformance/http/unsigned-requests/market-data-order-book-limits.json b/conformance/http/unsigned-requests/market-data-order-book-limits.json new file mode 100644 index 00000000..2a0f5728 --- /dev/null +++ b/conformance/http/unsigned-requests/market-data-order-book-limits.json @@ -0,0 +1,19 @@ +{ + "id": "http/unsigned-requests/market-data-order-book-limits", + "kind": "unsignedRequest", + "operation": "marketData.getCurrentOrderBook", + "input": { + "symbol": "BTCUSD", + "limit_bids": 1, + "limit_asks": 1 + }, + "expect": { + "method": "GET", + "path": "/v1/book/BTCUSD", + "query": { + "limit_asks": ["1"], + "limit_bids": ["1"] + }, + "authHeaders": [] + } +} diff --git a/conformance/http/unsigned-requests/market-data-ticker.json b/conformance/http/unsigned-requests/market-data-ticker.json new file mode 100644 index 00000000..92691ea3 --- /dev/null +++ b/conformance/http/unsigned-requests/market-data-ticker.json @@ -0,0 +1,14 @@ +{ + "id": "http/unsigned-requests/market-data-ticker", + "kind": "unsignedRequest", + "operation": "marketData.getTicker", + "input": { + "symbol": "BTCUSD" + }, + "expect": { + "method": "GET", + "path": "/v1/pubticker/BTCUSD", + "query": {}, + "authHeaders": [] + } +} diff --git a/conformance/http/unsigned-requests/prediction-markets-list-events-filters.json b/conformance/http/unsigned-requests/prediction-markets-list-events-filters.json new file mode 100644 index 00000000..7a78b8ae --- /dev/null +++ b/conformance/http/unsigned-requests/prediction-markets-list-events-filters.json @@ -0,0 +1,16 @@ +{ + "id": "http/unsigned-requests/prediction-markets-list-events-filters", + "kind": "unsignedRequest", + "operation": "predictions.listEvents", + "input": { + "status": ["active", "closed"] + }, + "expect": { + "method": "GET", + "path": "/v1/prediction-markets/events", + "query": { + "status": ["active", "closed"] + }, + "authHeaders": [] + } +} diff --git a/conformance/json/decimal-number-exact.json b/conformance/json/decimal-number-exact.json new file mode 100644 index 00000000..24ed5817 --- /dev/null +++ b/conformance/json/decimal-number-exact.json @@ -0,0 +1,10 @@ +{ + "id": "json/decimal-number-exact", + "kind": "jsonDecoding", + "raw": "{\"v\":0.00000001}", + "field": "v", + "expect": { + "valueKind": "decimal", + "text": "0.00000001" + } +} diff --git a/conformance/json/decimal-string-exact.json b/conformance/json/decimal-string-exact.json new file mode 100644 index 00000000..a36568a8 --- /dev/null +++ b/conformance/json/decimal-string-exact.json @@ -0,0 +1,10 @@ +{ + "id": "json/decimal-string-exact", + "kind": "jsonDecoding", + "raw": "{\"v\":\"0.00000001\"}", + "field": "v", + "expect": { + "valueKind": "decimal", + "text": "0.00000001" + } +} diff --git a/conformance/json/wide-integer-safe.json b/conformance/json/wide-integer-safe.json new file mode 100644 index 00000000..01c01238 --- /dev/null +++ b/conformance/json/wide-integer-safe.json @@ -0,0 +1,10 @@ +{ + "id": "json/wide-integer-safe", + "kind": "jsonDecoding", + "raw": "{\"v\":9007199254740991}", + "field": "v", + "expect": { + "valueKind": "integer", + "text": "9007199254740991" + } +} diff --git a/conformance/json/wide-integer-unsafe.json b/conformance/json/wide-integer-unsafe.json new file mode 100644 index 00000000..bccf8e91 --- /dev/null +++ b/conformance/json/wide-integer-unsafe.json @@ -0,0 +1,13 @@ +{ + "id": "json/wide-integer-unsafe", + "kind": "jsonDecoding", + "raw": "{\"v\":9007199254740993}", + "field": "v", + "expect": { + "valueKind": "integer", + "text": "9007199254740993" + }, + "exceptions": [ + "int64-static-typing" + ] +} diff --git a/conformance/manifest.json b/conformance/manifest.json new file mode 100644 index 00000000..1bf61af4 --- /dev/null +++ b/conformance/manifest.json @@ -0,0 +1,86 @@ +{ + "version": 1, + "suites": [ + { + "id": "http/hmac-requests", + "kind": "hmacRequest", + "cases": [ + "private-post-with-body", + "private-post-empty-body", + "private-post-wide-integer", + "websocket-upgrade-nonce" + ] + }, + { + "id": "http/unsigned-requests", + "kind": "unsignedRequest", + "cases": [ + "market-data-ticker", + "market-data-order-book-limits", + "prediction-markets-list-events-filters" + ] + }, + { + "id": "http/errors", + "kind": "errorMapping", + "cases": [ + "invalid-nonce-400", + "missing-role-403-result-envelope", + "missing-role-403-error-envelope", + "terms-required-400-must-accept", + "terms-required-400-accept-terms-required", + "insufficient-funds-406", + "rate-limited-429-retry-after", + "order-not-found-404", + "market-closed-400", + "server-error-500-unstructured", + "not-found-404-empty-body" + ] + }, + { + "id": "json", + "kind": "jsonDecoding", + "cases": [ + "wide-integer-unsafe", + "wide-integer-safe", + "decimal-string-exact", + "decimal-number-exact" + ] + }, + { + "id": "websocket/subscriptions", + "kind": "wsSubscription", + "cases": [ + "public-trades", + "public-trades-untrimmed-symbol", + "public-book-ticker", + "public-depth-diff", + "public-depth-diff-interval", + "public-partial-depth-20", + "public-contract-status", + "private-orders-session", + "private-balances-interval" + ] + }, + { + "id": "websocket/events", + "kind": "wsEvent", + "cases": [ + "trade", + "depth-unsafe-last-update-id", + "order-update", + "balance-update" + ] + } + ], + "exceptions": [ + "rest-nonce-json-type", + "rest-payload-key-order", + "ws-subscription-id-scope", + "ws-inbound-symbol-case", + "int64-static-typing", + "http-406-insufficient-funds", + "rest-query-signing", + "candle-timeframe-spelling" + ] +} diff --git a/conformance/websocket/events/balance-update.json b/conformance/websocket/events/balance-update.json new file mode 100644 index 00000000..03c67b76 --- /dev/null +++ b/conformance/websocket/events/balance-update.json @@ -0,0 +1,13 @@ +{ + "id": "websocket/events/balance-update", + "kind": "wsEvent", + "stream": "balances", + "frame": "{\"e\":\"balanceUpdate\",\"E\":1700000000000,\"u\":1700000000001,\"B\":[{\"a\":\"USD\",\"f\":\"100.00\",\"c\":\"100.00\"}]}", + "expect": { + "fields": { + "e": { "text": "balanceUpdate", "compare": "exact" }, + "E": { "text": "1700000000000", "compare": "exact" }, + "u": { "text": "1700000000001", "compare": "exact" } + } + } +} diff --git a/conformance/websocket/events/depth-unsafe-last-update-id.json b/conformance/websocket/events/depth-unsafe-last-update-id.json new file mode 100644 index 00000000..4e8ee8db --- /dev/null +++ b/conformance/websocket/events/depth-unsafe-last-update-id.json @@ -0,0 +1,16 @@ +{ + "id": "websocket/events/depth-unsafe-last-update-id", + "kind": "wsEvent", + "stream": "depthUpdates", + "symbol": "BTCUSD", + "frame": "{\"e\":\"depthUpdate\",\"E\":1700000000000,\"s\":\"BTCUSD\",\"U\":9007199254740993,\"u\":9007199254740994,\"b\":[],\"a\":[]}", + "expect": { + "fields": { + "e": { "text": "depthUpdate", "compare": "exact" }, + "E": { "text": "1700000000000", "compare": "exact" }, + "s": { "text": "BTCUSD", "compare": "caseInsensitive" }, + "U": { "text": "9007199254740993", "compare": "exact" }, + "u": { "text": "9007199254740994", "compare": "exact" } + } + } +} diff --git a/conformance/websocket/events/order-update.json b/conformance/websocket/events/order-update.json new file mode 100644 index 00000000..0d4f7f49 --- /dev/null +++ b/conformance/websocket/events/order-update.json @@ -0,0 +1,17 @@ +{ + "id": "websocket/events/order-update", + "kind": "wsEvent", + "stream": "orders", + "symbol": "BTCUSD", + "frame": "{\"e\":\"orderUpdate\",\"E\":1700000000000,\"s\":\"BTCUSD\",\"i\":12345,\"c\":\"client-order-1\",\"S\":\"BUY\",\"o\":\"LIMIT\",\"X\":\"FILLED\",\"p\":\"1000.00\",\"q\":\"0.25\",\"z\":\"0.25\",\"Z\":\"250.00\",\"T\":1700000000001}", + "expect": { + "fields": { + "e": { "text": "orderUpdate", "compare": "exact" }, + "E": { "text": "1700000000000", "compare": "exact" }, + "s": { "text": "BTCUSD", "compare": "exact" }, + "i": { "text": "12345", "compare": "exact" }, + "X": { "text": "FILLED", "compare": "exact" }, + "T": { "text": "1700000000001", "compare": "exact" } + } + } +} diff --git a/conformance/websocket/events/trade.json b/conformance/websocket/events/trade.json new file mode 100644 index 00000000..047db7ed --- /dev/null +++ b/conformance/websocket/events/trade.json @@ -0,0 +1,21 @@ +{ + "id": "websocket/events/trade", + "kind": "wsEvent", + "stream": "trades", + "symbol": "BTCUSD", + "frame": "{\"e\":\"trade\",\"E\":1700000000000,\"s\":\"btcusd\",\"t\":123456789,\"p\":\"1000.00\",\"q\":\"0.25\",\"m\":true}", + "expect": { + "fields": { + "e": { "text": "trade", "compare": "exact" }, + "E": { "text": "1700000000000", "compare": "exact" }, + "s": { "text": "BTCUSD", "compare": "caseInsensitive" }, + "t": { "text": "123456789", "compare": "exact" }, + "p": { "text": "1000.00", "compare": "exact" }, + "q": { "text": "0.25", "compare": "exact" }, + "m": { "text": "true", "compare": "exact" } + } + }, + "exceptions": [ + "ws-inbound-symbol-case" + ] +} diff --git a/conformance/websocket/subscriptions/private-balances-interval.json b/conformance/websocket/subscriptions/private-balances-interval.json new file mode 100644 index 00000000..e8c5bfaf --- /dev/null +++ b/conformance/websocket/subscriptions/private-balances-interval.json @@ -0,0 +1,12 @@ +{ + "id": "websocket/subscriptions/private-balances-interval", + "kind": "wsSubscription", + "stream": "balances", + "options": { + "intervalMs": 1000 + }, + "expect": { + "method": "SUBSCRIBE", + "params": ["balances@account@1s"] + } +} diff --git a/conformance/websocket/subscriptions/private-orders-session.json b/conformance/websocket/subscriptions/private-orders-session.json new file mode 100644 index 00000000..a67a7488 --- /dev/null +++ b/conformance/websocket/subscriptions/private-orders-session.json @@ -0,0 +1,12 @@ +{ + "id": "websocket/subscriptions/private-orders-session", + "kind": "wsSubscription", + "stream": "orders", + "options": { + "scope": "session" + }, + "expect": { + "method": "SUBSCRIBE", + "params": ["orders@session"] + } +} diff --git a/conformance/websocket/subscriptions/public-book-ticker.json b/conformance/websocket/subscriptions/public-book-ticker.json new file mode 100644 index 00000000..708b61c9 --- /dev/null +++ b/conformance/websocket/subscriptions/public-book-ticker.json @@ -0,0 +1,10 @@ +{ + "id": "websocket/subscriptions/public-book-ticker", + "kind": "wsSubscription", + "stream": "bookTicker", + "symbol": "ETHUSD", + "expect": { + "method": "SUBSCRIBE", + "params": ["ethusd@bookTicker"] + } +} diff --git a/conformance/websocket/subscriptions/public-contract-status.json b/conformance/websocket/subscriptions/public-contract-status.json new file mode 100644 index 00000000..f15ff191 --- /dev/null +++ b/conformance/websocket/subscriptions/public-contract-status.json @@ -0,0 +1,10 @@ +{ + "id": "websocket/subscriptions/public-contract-status", + "kind": "wsSubscription", + "stream": "contractStatus", + "symbol": "BTCUSD", + "expect": { + "method": "SUBSCRIBE", + "params": ["contractStatus"] + } +} diff --git a/conformance/websocket/subscriptions/public-depth-diff-interval.json b/conformance/websocket/subscriptions/public-depth-diff-interval.json new file mode 100644 index 00000000..98cdb0f7 --- /dev/null +++ b/conformance/websocket/subscriptions/public-depth-diff-interval.json @@ -0,0 +1,13 @@ +{ + "id": "websocket/subscriptions/public-depth-diff-interval", + "kind": "wsSubscription", + "stream": "depthUpdates", + "symbol": "BTCUSD", + "options": { + "intervalMs": 100 + }, + "expect": { + "method": "SUBSCRIBE", + "params": ["btcusd@depth@100ms"] + } +} diff --git a/conformance/websocket/subscriptions/public-depth-diff.json b/conformance/websocket/subscriptions/public-depth-diff.json new file mode 100644 index 00000000..28a65893 --- /dev/null +++ b/conformance/websocket/subscriptions/public-depth-diff.json @@ -0,0 +1,10 @@ +{ + "id": "websocket/subscriptions/public-depth-diff", + "kind": "wsSubscription", + "stream": "depthUpdates", + "symbol": "BTCUSD", + "expect": { + "method": "SUBSCRIBE", + "params": ["btcusd@depth"] + } +} diff --git a/conformance/websocket/subscriptions/public-partial-depth-20.json b/conformance/websocket/subscriptions/public-partial-depth-20.json new file mode 100644 index 00000000..64cc4f64 --- /dev/null +++ b/conformance/websocket/subscriptions/public-partial-depth-20.json @@ -0,0 +1,13 @@ +{ + "id": "websocket/subscriptions/public-partial-depth-20", + "kind": "wsSubscription", + "stream": "partialDepth", + "symbol": "BTCUSD", + "options": { + "levels": 20 + }, + "expect": { + "method": "SUBSCRIBE", + "params": ["btcusd@depth20"] + } +} diff --git a/conformance/websocket/subscriptions/public-trades-untrimmed-symbol.json b/conformance/websocket/subscriptions/public-trades-untrimmed-symbol.json new file mode 100644 index 00000000..d5130dcb --- /dev/null +++ b/conformance/websocket/subscriptions/public-trades-untrimmed-symbol.json @@ -0,0 +1,10 @@ +{ + "id": "websocket/subscriptions/public-trades-untrimmed-symbol", + "kind": "wsSubscription", + "stream": "trades", + "symbol": " BTCUSD ", + "expect": { + "method": "SUBSCRIBE", + "params": ["btcusd@trade"] + } +} diff --git a/conformance/websocket/subscriptions/public-trades.json b/conformance/websocket/subscriptions/public-trades.json new file mode 100644 index 00000000..dd7ed41f --- /dev/null +++ b/conformance/websocket/subscriptions/public-trades.json @@ -0,0 +1,13 @@ +{ + "id": "websocket/subscriptions/public-trades", + "kind": "wsSubscription", + "stream": "trades", + "symbol": "BTCUSD", + "expect": { + "method": "SUBSCRIBE", + "params": ["btcusd@trade"] + }, + "exceptions": [ + "ws-subscription-id-scope" + ] +} diff --git a/packages/sdk-go/conformance/errors_test.go b/packages/sdk-go/conformance/errors_test.go new file mode 100644 index 00000000..b054aabd --- /dev/null +++ b/packages/sdk-go/conformance/errors_test.go @@ -0,0 +1,137 @@ +package conformance + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gemini/developer-platform/packages/sdk-go/transport" +) + +func TestErrorMapping(t *testing.T) { + manifest := LoadManifest(t) + var suite FixtureSuite + for _, candidate := range manifest.Suites { + if candidate.ID == "http/errors" { + suite = candidate + break + } + } + if suite.ID == "" { + t.Fatal("manifest does not contain http/errors suite") + } + for _, caseID := range suite.Cases { + t.Run(caseID, func(t *testing.T) { + var fixture FixtureError + if err := json.Unmarshal(LoadCase(t, suite.ID, caseID), &fixture); err != nil { + t.Fatalf("decoding fixture: %v", err) + } + if fixture.ID != suite.ID+"/"+caseID || fixture.Kind != suite.Kind { + t.Fatalf("fixture identity = (%q, %q), want (%q, %q)", fixture.ID, fixture.Kind, suite.ID+"/"+caseID, suite.Kind) + } + testErrorCase(t, fixture) + }) + } +} + +func testErrorCase(t *testing.T, fixture FixtureError) { + t.Helper() + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + t.Errorf("handler method = %s, want GET", req.Method) + } + for name, value := range fixture.Response.Headers { + w.Header().Set(name, value) + } + w.WriteHeader(fixture.Response.Status) + if fixture.Response.Body != "" { + _, _ = io.WriteString(w, fixture.Response.Body) + } + })) + defer server.Close() + + client := transport.NewClient( + transport.WithHTTPClient(server.Client()), + transport.WithRetryPolicy(transport.RetryPolicy{MaxRetries: 0}), + ) + _, _, err := client.RequestRaw(context.Background(), http.MethodGet, server.URL+"/v1/conformance", nil) + if err == nil { + t.Fatal("expected fixture response to return an error") + } + wantSentinel := errorForCanonicalKind(fixture.Expect.Kind) + if wantSentinel == nil { + t.Fatalf("unsupported canonical error kind %q", fixture.Expect.Kind) + } + if !errors.Is(err, wantSentinel) { + t.Errorf("error %v does not match %v for canonical kind %q", err, wantSentinel, fixture.Expect.Kind) + } + + var apiErr *transport.APIError + if !errors.As(err, &apiErr) { + t.Fatalf("error %T does not expose *transport.APIError", err) + } + if apiErr.StatusCode != fixture.Response.Status { + t.Errorf("error status = %d, want %d", apiErr.StatusCode, fixture.Response.Status) + } + if string(apiErr.RawBody) != fixture.Response.Body { + t.Errorf("error raw body = %q, want exact %q", apiErr.RawBody, fixture.Response.Body) + } + if fixture.Expect.Reason != "" { + reason := apiErr.Reason + if reason == "" { + reason = apiErr.ErrorMessage + } + if reason != fixture.Expect.Reason { + t.Errorf("error reason = %q, want %q", reason, fixture.Expect.Reason) + } + } + if fixture.Expect.RetryAfterSeconds != nil { + var rateErr *transport.RateLimitError + if !errors.As(err, &rateErr) { + t.Fatalf("error %T does not expose *transport.RateLimitError", err) + } + want := time.Duration(*fixture.Expect.RetryAfterSeconds) * time.Second + if rateErr.RetryAfter != want { + t.Errorf("retry-after = %v, want %v", rateErr.RetryAfter, want) + } + } + if fixture.Response.Body == "" && len(apiErr.RawBody) != 0 { + t.Errorf("empty response body produced raw bytes %q", apiErr.RawBody) + } +} + +func errorForCanonicalKind(kind string) error { + switch kind { + case "invalid_nonce": + return transport.ErrInvalidNonce + case "missing_nonce": + return transport.ErrMissingNonce + case "invalid_signature": + return transport.ErrInvalidSignature + case "missing_role": + return transport.ErrMissingRole + case "terms_required": + return transport.ErrAcceptTermsRequired + case "insufficient_funds": + return transport.ErrInsufficientFunds + case "rate_limited": + return transport.ErrRateLimited + case "order_not_found": + return transport.ErrOrderNotFound + case "market_closed": + return transport.ErrMarketClosed + case "not_found": + return transport.ErrNotFound + case "service_error": + return transport.ErrInternalServer + case "invalid_request": + return transport.ErrBadRequest + default: + return nil + } +} diff --git a/packages/sdk-go/conformance/fixtures_test.go b/packages/sdk-go/conformance/fixtures_test.go new file mode 100644 index 00000000..94be195c --- /dev/null +++ b/packages/sdk-go/conformance/fixtures_test.go @@ -0,0 +1,196 @@ +package conformance + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +// FixtureManifest is the language-neutral conformance manifest. +type FixtureManifest struct { + Version int `json:"version"` + Suites []FixtureSuite `json:"suites"` + Exceptions []string `json:"exceptions"` +} + +// FixtureSuite enumerates the fixture files for one runner kind. +type FixtureSuite struct { + ID string `json:"id"` + Kind string `json:"kind"` + Cases []string `json:"cases"` +} + +type FixtureCredentials struct { + APIKey string `json:"apiKey"` + APISecret string `json:"apiSecret"` +} + +type FixtureNonce struct { + Mode string `json:"mode"` + Value string `json:"value,omitempty"` +} + +type FixtureHMACRequest struct { + Method string `json:"method"` + Path string `json:"path"` + Body json.RawMessage `json:"body"` +} + +type FixtureHMACPayload struct { + Request string `json:"request,omitempty"` + Nonce string `json:"nonce,omitempty"` + Fields map[string]json.RawMessage `json:"fields,omitempty"` +} + +type FixtureSignature struct { + Algorithm string `json:"algorithm"` + Over string `json:"over"` + Encoding string `json:"encoding"` +} + +type FixtureHMACExpect struct { + Headers []string `json:"headers"` + APIKeyHeader string `json:"apiKeyHeader"` + Payload FixtureHMACPayload `json:"payload"` + Signature FixtureSignature `json:"signature"` +} + +type FixtureHMAC struct { + ID string `json:"id"` + Kind string `json:"kind"` + Credentials FixtureCredentials `json:"credentials"` + Nonce FixtureNonce `json:"nonce"` + Request FixtureHMACRequest `json:"request"` + Expect FixtureHMACExpect `json:"expect"` + Exceptions []string `json:"exceptions,omitempty"` +} + +type FixtureUnsignedExpect struct { + Method string `json:"method"` + Path string `json:"path"` + Query map[string][]string `json:"query"` + AuthHeaders []string `json:"authHeaders"` +} + +type FixtureUnsigned struct { + ID string `json:"id"` + Kind string `json:"kind"` + Operation string `json:"operation"` + Input map[string]json.RawMessage `json:"input"` + Expect FixtureUnsignedExpect `json:"expect"` +} + +type FixtureErrorResponse struct { + Status int `json:"status"` + Headers map[string]string `json:"headers"` + Body string `json:"body"` +} + +type FixtureErrorExpect struct { + Kind string `json:"kind"` + Reason string `json:"reason,omitempty"` + RetryAfterSeconds *int `json:"retryAfterSeconds,omitempty"` +} + +type FixtureError struct { + ID string `json:"id"` + Kind string `json:"kind"` + Response FixtureErrorResponse `json:"response"` + Expect FixtureErrorExpect `json:"expect"` +} + +type FixtureJSONExpect struct { + ValueKind string `json:"valueKind"` + Text string `json:"text"` +} + +type FixtureJSON struct { + ID string `json:"id"` + Kind string `json:"kind"` + Raw string `json:"raw"` + Field string `json:"field"` + Expect FixtureJSONExpect `json:"expect"` +} + +type FixtureWSSubscriptionExpect struct { + Method string `json:"method"` + Params []string `json:"params"` +} + +type FixtureWSSubscription struct { + ID string `json:"id"` + Kind string `json:"kind"` + Stream string `json:"stream"` + Symbol string `json:"symbol,omitempty"` + Options map[string]json.RawMessage `json:"options,omitempty"` + Expect FixtureWSSubscriptionExpect `json:"expect"` + Exceptions []string `json:"exceptions,omitempty"` +} + +type FixtureWSEventField struct { + Text string `json:"text"` + Compare string `json:"compare,omitempty"` +} + +type FixtureWSEventExpect struct { + Fields map[string]FixtureWSEventField `json:"fields"` +} + +type FixtureWSEvent struct { + ID string `json:"id"` + Kind string `json:"kind"` + Stream string `json:"stream"` + Symbol string `json:"symbol,omitempty"` + Frame string `json:"frame"` + Expect FixtureWSEventExpect `json:"expect"` + Exceptions []string `json:"exceptions,omitempty"` +} + +// ConformanceRoot locates the repository root by walking upward from the +// process working directory until conformance/manifest.json is present. +func ConformanceRoot(t *testing.T) string { + t.Helper() + startDir, err := os.Getwd() + if err != nil { + t.Fatalf("getting working directory: %v", err) + } + for dir := startDir; ; dir = filepath.Dir(dir) { + if _, err := os.Stat(filepath.Join(dir, "conformance", "manifest.json")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + } + t.Fatalf("conformance/manifest.json not found above %s", startDir) + return "" +} + +// LoadManifest reads and decodes the checked-in conformance manifest. +func LoadManifest(t *testing.T) FixtureManifest { + t.Helper() + root := ConformanceRoot(t) + data, err := os.ReadFile(filepath.Join(root, "conformance", "manifest.json")) + if err != nil { + t.Fatalf("reading conformance/manifest.json: %v", err) + } + var manifest FixtureManifest + if err := json.Unmarshal(data, &manifest); err != nil { + t.Fatalf("decoding conformance/manifest.json: %v", err) + } + return manifest +} + +// LoadCase reads one fixture named by its suite and case identifiers. +func LoadCase(t *testing.T, suiteID, caseID string) []byte { + t.Helper() + root := ConformanceRoot(t) + path := filepath.Join(root, "conformance", filepath.FromSlash(suiteID), caseID+".json") + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("reading conformance case %s/%s: %v", suiteID, caseID, err) + } + return data +} diff --git a/packages/sdk-go/conformance/hmac_requests_test.go b/packages/sdk-go/conformance/hmac_requests_test.go new file mode 100644 index 00000000..73ca0047 --- /dev/null +++ b/packages/sdk-go/conformance/hmac_requests_test.go @@ -0,0 +1,246 @@ +package conformance + +import ( + "context" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "testing" + + "github.com/gemini/developer-platform/packages/sdk-go/auth" + "github.com/gemini/developer-platform/packages/sdk-go/transport" +) + +type fixedNonce struct { + value string +} + +func (n fixedNonce) Next() string { return n.value } + +type fixtureRoundTripFunc func(*http.Request) (*http.Response, error) + +func (f fixtureRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestHMACRequests(t *testing.T) { + manifest := LoadManifest(t) + var suite FixtureSuite + for _, candidate := range manifest.Suites { + if candidate.ID == "http/hmac-requests" { + suite = candidate + break + } + } + if suite.ID == "" { + t.Fatal("manifest does not contain http/hmac-requests suite") + } + for _, caseID := range suite.Cases { + t.Run(caseID, func(t *testing.T) { + var fixture FixtureHMAC + if err := json.Unmarshal(LoadCase(t, suite.ID, caseID), &fixture); err != nil { + t.Fatalf("decoding fixture: %v", err) + } + if fixture.ID != suite.ID+"/"+caseID || fixture.Kind != suite.Kind { + t.Fatalf("fixture identity = (%q, %q), want (%q, %q)", fixture.ID, fixture.Kind, suite.ID+"/"+caseID, suite.Kind) + } + switch fixture.Nonce.Mode { + case "monotonic": + testHMACREST(t, fixture) + case "websocket": + testHMACWebSocket(t, fixture) + default: + t.Fatalf("unsupported nonce mode %q", fixture.Nonce.Mode) + } + }) + } +} + +func testHMACREST(t *testing.T, fixture FixtureHMAC) { + t.Helper() + if fixture.Nonce.Value == "" { + t.Fatal("monotonic fixture nonce is empty") + } + if fixture.Request.Method == "" || fixture.Request.Path == "" { + t.Fatal("fixture request method/path must be non-empty") + } + if len(fixture.Request.Body) == 0 { + fixture.Request.Body = json.RawMessage("null") + } + requestURL := "https://api.sandbox.gemini.com" + fixture.Request.Path + parsed, err := url.Parse(requestURL) + if err != nil { + t.Fatalf("parsing fixture request path: %v", err) + } + var observed *http.Request + rt := fixtureRoundTripFunc(func(req *http.Request) (*http.Response, error) { + observed = req.Clone(req.Context()) + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(`{}`)), + Request: req, + }, nil + }) + strategy := auth.NewHMAC( + auth.APIKey(fixture.Credentials.APIKey), + auth.APISecret(fixture.Credentials.APISecret), + auth.WithCustomNonceGenerator(fixedNonce{value: fixture.Nonce.Value}), + ) + client := transport.NewClient( + transport.WithHTTPClient(&http.Client{Transport: rt}), + transport.WithAuth(strategy), + transport.WithRetryPolicy(transport.RetryPolicy{MaxRetries: 0}), + ) + req, err := http.NewRequestWithContext(context.Background(), fixture.Request.Method, parsed.String(), nil) + if err != nil { + t.Fatalf("creating fixture request: %v", err) + } + if _, _, err := client.Execute(context.Background(), req, fixture.Request.Body); err != nil { + t.Fatalf("transport Execute failed: %v", err) + } + if observed == nil { + t.Fatal("fixture request did not reach HTTP handler") + } + if observed.Method != fixture.Request.Method { + t.Errorf("method = %q, want %q", observed.Method, fixture.Request.Method) + } + if observed.URL.Path != parsed.Path || observed.URL.RawQuery != parsed.RawQuery { + t.Errorf("URL path/query = %q?%q, want %q?%q", observed.URL.Path, observed.URL.RawQuery, parsed.Path, parsed.RawQuery) + } + if observed.Body != http.NoBody || observed.ContentLength != 0 { + t.Errorf("authenticated request wire body = %v, content length %d; want empty", observed.Body, observed.ContentLength) + } + assertHMACHeaders(t, observed.Header, fixture) + payloadB64 := observed.Header.Get("X-GEMINI-PAYLOAD") + decoded, err := base64.StdEncoding.DecodeString(payloadB64) + if err != nil { + t.Fatalf("decoding payload header: %v", err) + } + assertRESTPayload(t, decoded, fixture.Expect.Payload) + if fixture.Expect.Signature.Algorithm != "HMAC-SHA384" || fixture.Expect.Signature.Over != "payloadBase64" || fixture.Expect.Signature.Encoding != "hex-lower" { + t.Fatalf("unsupported signature expectation: %+v", fixture.Expect.Signature) + } + if !auth.VerifySignature(auth.APISecret(fixture.Credentials.APISecret), payloadB64, observed.Header.Get("X-GEMINI-SIGNATURE")) { + t.Fatal("signature does not verify against the exact base64 payload") + } +} + +func testHMACWebSocket(t *testing.T, fixture FixtureHMAC) { + t.Helper() + if fixture.Request.Method != "" || fixture.Request.Path != "" || len(fixture.Request.Body) != 0 { + t.Fatalf("websocket fixture unexpectedly contains REST request: %+v", fixture.Request) + } + strategy := auth.NewTimeBasedHMAC( + auth.APIKey(fixture.Credentials.APIKey), + auth.APISecret(fixture.Credentials.APISecret), + auth.WithCustomNonceGenerator(fixedNonce{value: fixture.Nonce.Value}), + ) + req, err := http.NewRequest(http.MethodGet, "wss://ws.sandbox.gemini.com/v1/marketdata", nil) + if err != nil { + t.Fatalf("creating websocket handshake request: %v", err) + } + if err := strategy.AuthenticateWebSocket(context.Background(), req); err != nil { + t.Fatalf("authenticating websocket handshake: %v", err) + } + for _, name := range fixture.Expect.Headers { + if value := req.Header.Get(name); strings.TrimSpace(value) == "" { + t.Errorf("expected non-empty %s header", name) + } + } + if got := req.Header.Get("X-GEMINI-APIKEY"); got != fixture.Expect.APIKeyHeader { + t.Errorf("API key header = %q, want %q", got, fixture.Expect.APIKeyHeader) + } + nonce := req.Header.Get("X-GEMINI-NONCE") + if !regexp.MustCompile(`^[0-9]{10}$`).MatchString(nonce) { + t.Fatalf("websocket nonce = %q, want epoch-second shape", nonce) + } + payloadB64 := req.Header.Get("X-GEMINI-PAYLOAD") + decoded, err := base64.StdEncoding.DecodeString(payloadB64) + if err != nil { + t.Fatalf("decoding websocket payload header: %v", err) + } + if string(decoded) != nonce { + t.Errorf("decoded websocket payload = %q, want nonce %q", decoded, nonce) + } + if !auth.VerifySignature(auth.APISecret(fixture.Credentials.APISecret), payloadB64, req.Header.Get("X-GEMINI-SIGNATURE")) { + t.Fatal("websocket signature does not verify") + } +} + +func assertHMACHeaders(t *testing.T, headers http.Header, fixture FixtureHMAC) { + t.Helper() + for _, name := range fixture.Expect.Headers { + if strings.TrimSpace(headers.Get(name)) == "" { + t.Errorf("expected non-empty %s header", name) + } + } + if got := headers.Get("X-GEMINI-APIKEY"); got != fixture.Expect.APIKeyHeader { + t.Errorf("API key header = %q, want %q", got, fixture.Expect.APIKeyHeader) + } +} + +func assertRESTPayload(t *testing.T, decoded []byte, expected FixtureHMACPayload) { + t.Helper() + var payload map[string]json.RawMessage + if err := json.Unmarshal(decoded, &payload); err != nil { + t.Fatalf("decoding signed payload JSON %q: %v", decoded, err) + } + if len(payload) != len(expected.Fields)+2 { + t.Fatalf("payload keys = %v, want request, nonce, and fields %v", mapKeys(payload), mapKeys(expected.Fields)) + } + var request string + if err := json.Unmarshal(payload["request"], &request); err != nil { + t.Fatalf("payload request is not a JSON string: %v", err) + } + if request != expected.Request { + t.Errorf("payload request = %q, want %q", request, expected.Request) + } + if got := decimalJSONText(payload["nonce"]); got != expected.Nonce { + t.Errorf("payload nonce text = %q, want %q", got, expected.Nonce) + } + for name, want := range expected.Fields { + got, ok := payload[name] + if !ok { + t.Errorf("payload is missing field %q", name) + continue + } + if !jsonRawEqual(got, want) { + t.Errorf("payload field %s = %s, want %s", name, got, want) + } + } + for name := range payload { + if name != "request" && name != "nonce" { + if _, ok := expected.Fields[name]; !ok { + t.Errorf("payload contains undeclared field %q", name) + } + } + } +} + +func decimalJSONText(raw json.RawMessage) string { + text := strings.TrimSpace(string(raw)) + if len(text) >= 2 && text[0] == '"' && text[len(text)-1] == '"' { + var value string + if json.Unmarshal(raw, &value) == nil { + return value + } + } + return text +} + +func jsonRawEqual(a, b json.RawMessage) bool { + return strings.TrimSpace(string(a)) == strings.TrimSpace(string(b)) +} + +func mapKeys[T any](values map[string]T) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + return keys +} diff --git a/packages/sdk-go/conformance/json_decoding_test.go b/packages/sdk-go/conformance/json_decoding_test.go new file mode 100644 index 00000000..b28e442f --- /dev/null +++ b/packages/sdk-go/conformance/json_decoding_test.go @@ -0,0 +1,66 @@ +package conformance + +import ( + "encoding/json" + "strconv" + "strings" + "testing" + + "github.com/gemini/developer-platform/packages/sdk-go/types" +) + + +func TestJSONDecodingConformance(t *testing.T) { + manifest := LoadManifest(t) + found := false + for _, suite := range manifest.Suites { + if suite.Kind != "jsonDecoding" { + continue + } + found = true + for _, caseID := range suite.Cases { + t.Run(caseID, func(t *testing.T) { + var fixture FixtureJSON + if err := json.Unmarshal(LoadCase(t, suite.ID, caseID), &fixture); err != nil { + t.Fatalf("decode fixture: %v", err) + } + if fixture.Kind != "jsonDecoding" { + t.Fatalf("fixture kind = %q, want jsonDecoding", fixture.Kind) + } + if fixture.Field != "v" { + t.Fatalf("unsupported fixture field %q", fixture.Field) + } + + decoder := json.NewDecoder(strings.NewReader(fixture.Raw)) + decoder.UseNumber() + switch fixture.Expect.ValueKind { + case "integer": + var value struct { + V int64 `json:"v"` + } + if err := decoder.Decode(&value); err != nil { + t.Fatalf("decode integer: %v", err) + } + if got := strconv.FormatInt(value.V, 10); got != fixture.Expect.Text { + t.Fatalf("decoded integer = %q, want %q", got, fixture.Expect.Text) + } + case "decimal": + var value struct { + V types.Decimal `json:"v"` + } + if err := decoder.Decode(&value); err != nil { + t.Fatalf("decode decimal: %v", err) + } + if got := value.V.String(); got != fixture.Expect.Text { + t.Fatalf("decoded decimal = %q, want %q", got, fixture.Expect.Text) + } + default: + t.Fatalf("unsupported value kind %q", fixture.Expect.ValueKind) + } + }) + } + } + if !found { + t.Fatal("manifest has no jsonDecoding suite") + } +} diff --git a/packages/sdk-go/conformance/manifest_test.go b/packages/sdk-go/conformance/manifest_test.go new file mode 100644 index 00000000..5d9a75ea --- /dev/null +++ b/packages/sdk-go/conformance/manifest_test.go @@ -0,0 +1,154 @@ +package conformance + +import ( + "encoding/json" + "io/fs" + "os" + "path/filepath" + "sort" + "testing" +) + +func TestManifest(t *testing.T) { + manifest := LoadManifest(t) + if manifest.Version != 1 { + t.Fatalf("manifest version = %d, want 1", manifest.Version) + } + + expectedSuites := []FixtureSuite{ + {ID: "http/hmac-requests", Kind: "hmacRequest", Cases: []string{"private-post-with-body", "private-post-empty-body", "private-post-wide-integer", "websocket-upgrade-nonce"}}, + {ID: "http/unsigned-requests", Kind: "unsignedRequest", Cases: []string{"market-data-ticker", "market-data-order-book-limits", "prediction-markets-list-events-filters"}}, + {ID: "http/errors", Kind: "errorMapping", Cases: []string{"invalid-nonce-400", "missing-role-403-result-envelope", "missing-role-403-error-envelope", "terms-required-400-must-accept", "terms-required-400-accept-terms-required", "insufficient-funds-406", "rate-limited-429-retry-after", "order-not-found-404", "market-closed-400", "server-error-500-unstructured", "not-found-404-empty-body"}}, + {ID: "json", Kind: "jsonDecoding", Cases: []string{"wide-integer-unsafe", "wide-integer-safe", "decimal-string-exact", "decimal-number-exact"}}, + {ID: "websocket/subscriptions", Kind: "wsSubscription", Cases: []string{"public-trades", "public-trades-untrimmed-symbol", "public-book-ticker", "public-depth-diff", "public-depth-diff-interval", "public-partial-depth-20", "public-contract-status", "private-orders-session", "private-balances-interval"}}, + {ID: "websocket/events", Kind: "wsEvent", Cases: []string{"trade", "depth-unsafe-last-update-id", "order-update", "balance-update"}}, + } + if len(manifest.Suites) != len(expectedSuites) { + t.Fatalf("manifest suite count = %d, want %d", len(manifest.Suites), len(expectedSuites)) + } + for i, want := range expectedSuites { + got := manifest.Suites[i] + if got.ID != want.ID || got.Kind != want.Kind { + t.Errorf("suite %d = (%q, %q), want (%q, %q)", i, got.ID, got.Kind, want.ID, want.Kind) + } + if !equalStrings(got.Cases, want.Cases) { + t.Errorf("suite %s cases = %v, want %v", got.ID, got.Cases, want.Cases) + } + } + + wantExceptions := []string{ + "rest-nonce-json-type", + "rest-payload-key-order", + "ws-subscription-id-scope", + "ws-inbound-symbol-case", + "int64-static-typing", + "http-406-insufficient-funds", + "rest-query-signing", + "candle-timeframe-spelling", + } + if !equalStringSet(manifest.Exceptions, wantExceptions) { + t.Fatalf("manifest exception ids = %v, want exact set %v", manifest.Exceptions, wantExceptions) + } + + root := ConformanceRoot(t) + conformanceDir := filepath.Join(root, "conformance") + listed := make(map[string]struct{}) + referencedExceptions := make(map[string]struct{}) + for _, suite := range manifest.Suites { + for _, caseID := range suite.Cases { + rel := filepath.ToSlash(filepath.Join(suite.ID, caseID+".json")) + if _, ok := listed[rel]; ok { + t.Fatalf("manifest lists duplicate case %s", rel) + } + listed[rel] = struct{}{} + path := filepath.Join(conformanceDir, filepath.FromSlash(rel)) + if _, err := os.Stat(path); err != nil { + t.Errorf("manifest case %s is missing: %v", rel, err) + continue + } + var envelope struct { + Exceptions []string `json:"exceptions"` + } + data, err := os.ReadFile(path) + if err != nil { + t.Errorf("reading listed case %s: %v", rel, err) + continue + } + if err := json.Unmarshal(data, &envelope); err != nil { + t.Errorf("decoding listed case %s: %v", rel, err) + continue + } + for _, id := range envelope.Exceptions { + referencedExceptions[id] = struct{}{} + } + } + } + + diskCases := make(map[string]struct{}) + if err := filepath.WalkDir(conformanceDir, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() { + return nil + } + rel, err := filepath.Rel(conformanceDir, path) + if err != nil { + return err + } + if filepath.ToSlash(rel) == "manifest.json" || filepath.Ext(path) != ".json" { + return nil + } + diskCases[filepath.ToSlash(rel)] = struct{}{} + return nil + }); err != nil { + t.Fatalf("walking conformance fixtures: %v", err) + } + for rel := range listed { + if _, ok := diskCases[rel]; !ok { + t.Errorf("manifest-listed case %s is not present on disk", rel) + } + } + for rel := range diskCases { + if _, ok := listed[rel]; !ok { + t.Errorf("orphaned fixture file %s is not listed in manifest", rel) + } + } + declaredExceptions := make(map[string]struct{}, len(manifest.Exceptions)) + for _, id := range manifest.Exceptions { + declaredExceptions[id] = struct{}{} + } + for id := range referencedExceptions { + if _, ok := declaredExceptions[id]; !ok { + t.Errorf("case references undeclared exception %q", id) + } + } +} + +func equalStrings(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func equalStringSet(a, b []string) bool { + if len(a) != len(b) { + return false + } + left := append([]string(nil), a...) + right := append([]string(nil), b...) + sort.Strings(left) + sort.Strings(right) + for i := range left { + if left[i] != right[i] || left[i] == "" { + return false + } + } + return true +} diff --git a/packages/sdk-go/conformance/unsigned_requests_test.go b/packages/sdk-go/conformance/unsigned_requests_test.go new file mode 100644 index 00000000..cb01e303 --- /dev/null +++ b/packages/sdk-go/conformance/unsigned_requests_test.go @@ -0,0 +1,149 @@ +package conformance + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gemini/developer-platform/packages/sdk-go" + "github.com/gemini/developer-platform/packages/sdk-go/generated/predictions" +) + +func TestUnsignedRequests(t *testing.T) { + manifest := LoadManifest(t) + var suite FixtureSuite + for _, candidate := range manifest.Suites { + if candidate.ID == "http/unsigned-requests" { + suite = candidate + break + } + } + if suite.ID == "" { + t.Fatal("manifest does not contain http/unsigned-requests suite") + } + for _, caseID := range suite.Cases { + t.Run(caseID, func(t *testing.T) { + var fixture FixtureUnsigned + if err := json.Unmarshal(LoadCase(t, suite.ID, caseID), &fixture); err != nil { + t.Fatalf("decoding fixture: %v", err) + } + if fixture.ID != suite.ID+"/"+caseID || fixture.Kind != suite.Kind { + t.Fatalf("fixture identity = (%q, %q), want (%q, %q)", fixture.ID, fixture.Kind, suite.ID+"/"+caseID, suite.Kind) + } + testUnsignedRequest(t, fixture) + }) + } +} + +func testUnsignedRequest(t *testing.T, fixture FixtureUnsigned) { + t.Helper() + var observed *http.Request + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + observed = req.Clone(req.Context()) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{}`)) + })) + defer server.Close() + + client := gemini.NewClient( + gemini.WithEnvironment(gemini.Sandbox), + gemini.WithCustomRESTURL(server.URL), + gemini.WithHTTPClient(server.Client()), + ) + defer client.Close() + + ctx := context.Background() + switch fixture.Operation { + case "marketData.getTicker": + symbol := requiredStringInput(t, fixture.Input, "symbol") + if _, err := client.MarketData.GetTicker(ctx, symbol); err != nil { + t.Fatalf("MarketData.GetTicker failed: %v", err) + } + case "marketData.getCurrentOrderBook": + symbol := requiredStringInput(t, fixture.Input, "symbol") + limitBids := requiredIntInput(t, fixture.Input, "limit_bids") + limitAsks := requiredIntInput(t, fixture.Input, "limit_asks") + if _, err := client.MarketData.GetOrderBook(ctx, symbol, limitBids, limitAsks); err != nil { + t.Fatalf("MarketData.GetOrderBook failed: %v", err) + } + case "predictions.listEvents": + var statusValues []string + raw, ok := fixture.Input["status"] + if !ok { + t.Fatal("prediction fixture input is missing status") + } + if err := json.Unmarshal(raw, &statusValues); err != nil { + t.Fatalf("decoding status input: %v", err) + } + statuses := make([]predictions.MarketStatus, len(statusValues)) + for i, value := range statusValues { + statuses[i] = predictions.MarketStatus(value) + } + params := &predictions.ListEventsParams{Status: &statuses} + if _, err := client.Predictions.GetEvents(ctx, params); err != nil { + t.Fatalf("Predictions.GetEvents failed: %v", err) + } + default: + t.Fatalf("unsupported unsigned operation %q", fixture.Operation) + } + if observed == nil { + t.Fatal("service request did not reach httptest handler") + } + if observed.Method != fixture.Expect.Method { + t.Errorf("method = %q, want %q", observed.Method, fixture.Expect.Method) + } + if observed.URL.Path != fixture.Expect.Path { + t.Errorf("path = %q, want %q", observed.URL.Path, fixture.Expect.Path) + } + if got, want := observed.URL.Query(), fixture.Expect.Query; !reflect.DeepEqual(map[string][]string(got), want) { + t.Errorf("query = %v, want %v", got, want) + } + for _, name := range fixture.Expect.AuthHeaders { + if observed.Header.Get(name) == "" { + t.Errorf("expected non-empty authentication header %q", name) + } + } + for _, name := range []string{"Authorization", "X-GEMINI-APIKEY", "X-GEMINI-NONCE", "X-GEMINI-PAYLOAD", "X-GEMINI-SIGNATURE"} { + if !contains(fixture.Expect.AuthHeaders, name) && observed.Header.Get(name) != "" { + t.Errorf("unsigned request unexpectedly included %s=%q", name, observed.Header.Get(name)) + } + } +} + +func requiredStringInput(t *testing.T, input map[string]json.RawMessage, name string) string { + t.Helper() + raw, ok := input[name] + if !ok { + t.Fatalf("input is missing %s", name) + } + var value string + if err := json.Unmarshal(raw, &value); err != nil || value == "" { + t.Fatalf("input %s is not a non-empty string: %s", name, raw) + } + return value +} + +func requiredIntInput(t *testing.T, input map[string]json.RawMessage, name string) int { + t.Helper() + raw, ok := input[name] + if !ok { + t.Fatalf("input is missing %s", name) + } + var value int + if err := json.Unmarshal(raw, &value); err != nil { + t.Fatalf("input %s is not an integer: %s (%v)", name, raw, err) + } + return value +} + +func contains(values []string, target string) bool { + for _, value := range values { + if value == target { + return true + } + } + return false +} diff --git a/packages/sdk-go/conformance/ws_events_test.go b/packages/sdk-go/conformance/ws_events_test.go new file mode 100644 index 00000000..dd947407 --- /dev/null +++ b/packages/sdk-go/conformance/ws_events_test.go @@ -0,0 +1,185 @@ +package conformance + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/gemini/developer-platform/packages/sdk-go/auth" + "github.com/gemini/developer-platform/packages/sdk-go/websocket" +) + +func TestWebSocketEventConformance(t *testing.T) { + manifest := LoadManifest(t) + found := false + for _, suite := range manifest.Suites { + if suite.Kind != "wsEvent" { + continue + } + found = true + suiteID := suite.ID + for _, caseID := range suite.Cases { + caseID := caseID + t.Run(caseID, func(t *testing.T) { + var fixture FixtureWSEvent + if err := json.Unmarshal(LoadCase(t, suiteID, caseID), &fixture); err != nil { + t.Fatalf("decode fixture: %v", err) + } + if fixture.Kind != "wsEvent" { + t.Fatalf("fixture kind = %q, want wsEvent", fixture.Kind) + } + if fixture.Frame == "" { + t.Fatal("fixture frame is empty") + } + + conn := newConformanceWSConn() + dialer := &conformanceWSDialer{conn: conn} + private := fixture.Stream == "orders" || fixture.Stream == "order" || fixture.Stream == "balances" + var client *websocket.Client + if private { + client = websocket.NewPrivateClient( + "wss://ws.gemini.com", + auth.NewTimeBasedHMAC(auth.APIKey("conformance-api-key"), auth.APISecret("conformance-api-secret")), + websocket.WithDialer(dialer), + websocket.WithAutoReconnect(false), + ) + } else { + client = websocket.NewPublicClient( + "wss://ws.gemini.com", + websocket.WithDialer(dialer), + websocket.WithAutoReconnect(false), + ) + } + defer client.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + event, err := subscribeAndReceiveEvent(client, conn, fixture, ctx) + if err != nil { + t.Fatalf("receive %q event: %v", fixture.Stream, err) + } + for wireField, expected := range fixture.Expect.Fields { + got, ok := eventFieldText(event, wireField) + if !ok { + t.Errorf("typed %T event has no wire field %q", event, wireField) + continue + } + if expected.Compare == "caseInsensitive" { + if !strings.EqualFold(got, expected.Text) { + t.Errorf("event field %s = %q, want case-insensitive match for %q", wireField, got, expected.Text) + } + } else if got != expected.Text { + t.Errorf("event field %s = %q, want %q", wireField, got, expected.Text) + } + } + }) + } + } + if !found { + t.Fatal("manifest has no wsEvent suite") + } +} + +func subscribeAndReceiveEvent(client *websocket.Client, conn *conformanceWSConn, fixture FixtureWSEvent, ctx context.Context) (any, error) { + symbol := fixture.Symbol + var event any + var err error + switch fixture.Stream { + case "trade", "trades": + var ch <-chan *websocket.TradeEvent + ch, err = client.SubscribeTrades(ctx, symbol) + if err == nil { + conn.feed([]byte(fixture.Frame)) + select { + case event = <-ch: + case <-ctx.Done(): + return nil, ctx.Err() + } + } + case "depth", "depthUpdates": + var ch <-chan *websocket.DepthUpdate + ch, err = client.SubscribeDepth(ctx, symbol) + if err == nil { + conn.feed([]byte(fixture.Frame)) + select { + case event = <-ch: + case <-ctx.Done(): + return nil, ctx.Err() + } + } + case "order", "orders": + var ch <-chan *websocket.OrderEvent + ch, err = client.SubscribeOrderEvents(ctx) + if err == nil { + conn.feed([]byte(fixture.Frame)) + select { + case event = <-ch: + case <-ctx.Done(): + return nil, ctx.Err() + } + } + case "balance", "balances": + var ch <-chan *websocket.BalanceUpdate + ch, err = client.SubscribeBalances(ctx) + if err == nil { + conn.feed([]byte(fixture.Frame)) + select { + case event = <-ch: + case <-ctx.Done(): + return nil, ctx.Err() + } + } + default: + return nil, fmt.Errorf("unsupported WebSocket event stream %q", fixture.Stream) + } + if err != nil { + return nil, err + } + if event == nil { + return nil, errors.New("typed WebSocket event was nil") + } + return event, nil +} + +func eventFieldText(event any, wireField string) (string, bool) { + encoded, err := json.Marshal(event) + if err != nil { + return "", false + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(encoded, &fields); err != nil { + return "", false + } + raw, ok := fields[wireField] + if !ok { + return "", false + } + if len(raw) > 0 && raw[0] == '"' { + var text string + if json.Unmarshal(raw, &text) != nil { + return "", false + } + return text, true + } + decoder := json.NewDecoder(strings.NewReader(string(raw))) + decoder.UseNumber() + var value any + if decoder.Decode(&value) != nil { + return "", false + } + switch typed := value.(type) { + case json.Number: + return typed.String(), true + case bool: + return strconv.FormatBool(typed), true + case nil: + return "null", true + default: + return "", false + } +} diff --git a/packages/sdk-go/conformance/ws_subscriptions_test.go b/packages/sdk-go/conformance/ws_subscriptions_test.go new file mode 100644 index 00000000..d291f6a9 --- /dev/null +++ b/packages/sdk-go/conformance/ws_subscriptions_test.go @@ -0,0 +1,280 @@ +package conformance + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "reflect" + "sync" + "testing" + "time" + + "github.com/gemini/developer-platform/packages/sdk-go/auth" + "github.com/gemini/developer-platform/packages/sdk-go/websocket" +) + +// conformanceWSConn is an in-memory WebSocket transport. It records every +// client frame and returns the protocol acknowledgement expected by the +// subscription request path, so these tests never require a network socket. +type conformanceWSConn struct { + mu sync.Mutex + closed bool + closedCh chan struct{} + readCh chan []byte + written [][]byte +} + +func newConformanceWSConn() *conformanceWSConn { + return &conformanceWSConn{ + closedCh: make(chan struct{}), + readCh: make(chan []byte, 128), + } +} + +func (c *conformanceWSConn) ReadMessage(ctx context.Context) (int, []byte, error) { + select { + case <-ctx.Done(): + return 0, nil, ctx.Err() + case <-c.closedCh: + return 0, nil, errors.New("conformance websocket connection closed") + case payload := <-c.readCh: + return websocket.TextMessage, payload, nil + } +} + +func (c *conformanceWSConn) WriteMessage(ctx context.Context, _ int, payload []byte) error { + c.mu.Lock() + if c.closed { + c.mu.Unlock() + return errors.New("conformance websocket connection closed") + } + c.written = append(c.written, append([]byte(nil), payload...)) + c.mu.Unlock() + + var request struct { + ID int64 `json:"id"` + Method string `json:"method"` + } + if err := json.Unmarshal(payload, &request); err != nil || request.ID <= 0 || request.Method == "" { + return nil + } + ack, err := json.Marshal(struct { + ID int64 `json:"id"` + Status int `json:"status"` + Result map[string]any `json:"result"` + }{ID: request.ID, Status: http.StatusOK, Result: map[string]any{}}) + if err != nil { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-c.closedCh: + return errors.New("conformance websocket connection closed") + case c.readCh <- ack: + return nil + } +} + +func (c *conformanceWSConn) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + if !c.closed { + c.closed = true + close(c.closedCh) + } + return nil +} + +func (c *conformanceWSConn) feed(payload []byte) { + select { + case <-c.closedCh: + case c.readCh <- append([]byte(nil), payload...): + } +} + +func (c *conformanceWSConn) frames() [][]byte { + c.mu.Lock() + defer c.mu.Unlock() + frames := make([][]byte, len(c.written)) + for i, frame := range c.written { + frames[i] = append([]byte(nil), frame...) + } + return frames +} + +type conformanceWSDialer struct { + conn *conformanceWSConn +} + +func (d *conformanceWSDialer) Dial(context.Context, string, http.Header) (websocket.Conn, *http.Response, error) { + return d.conn, &http.Response{StatusCode: http.StatusSwitchingProtocols}, nil +} + +func TestWebSocketSubscriptionConformance(t *testing.T) { + manifest := LoadManifest(t) + found := false + for _, suite := range manifest.Suites { + if suite.Kind != "wsSubscription" { + continue + } + found = true + suiteID := suite.ID + for _, caseID := range suite.Cases { + caseID := caseID + t.Run(caseID, func(t *testing.T) { + var fixture FixtureWSSubscription + if err := json.Unmarshal(LoadCase(t, suiteID, caseID), &fixture); err != nil { + t.Fatalf("decode fixture: %v", err) + } + if fixture.Kind != "wsSubscription" { + t.Fatalf("fixture kind = %q, want wsSubscription", fixture.Kind) + } + + conn := newConformanceWSConn() + dialer := &conformanceWSDialer{conn: conn} + private := fixture.Stream == "orders" || fixture.Stream == "balances" || fixture.Stream == "positions" + var client *websocket.Client + if private { + client = websocket.NewPrivateClient( + "wss://ws.gemini.com", + auth.NewTimeBasedHMAC(auth.APIKey("conformance-api-key"), auth.APISecret("conformance-api-secret")), + websocket.WithDialer(dialer), + websocket.WithAutoReconnect(false), + ) + } else { + client = websocket.NewPublicClient( + "wss://ws.gemini.com", + websocket.WithDialer(dialer), + websocket.WithAutoReconnect(false), + ) + } + defer client.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if err := subscribeFixture(t, client, fixture, ctx); err != nil { + t.Fatalf("subscribe %q: %v", fixture.Stream, err) + } + + frames := conn.frames() + if len(frames) != 1 { + t.Fatalf("recorded %d wire frames, want one", len(frames)) + } + var wire struct { + ID int64 `json:"id"` + Method string `json:"method"` + Params []string `json:"params"` + } + if err := json.Unmarshal(frames[0], &wire); err != nil { + t.Fatalf("decode SUBSCRIBE frame: %v", err) + } + if wire.ID <= 0 { + t.Fatalf("SUBSCRIBE id = %d, want positive integer", wire.ID) + } + if wire.Method != fixture.Expect.Method { + t.Fatalf("SUBSCRIBE method = %q, want %q", wire.Method, fixture.Expect.Method) + } + if !reflect.DeepEqual(wire.Params, fixture.Expect.Params) { + t.Fatalf("SUBSCRIBE params = %#v, want %#v", wire.Params, fixture.Expect.Params) + } + }) + } + } + if !found { + t.Fatal("manifest has no wsSubscription suite") + } +} + +func subscribeFixture(t *testing.T, client *websocket.Client, fixture FixtureWSSubscription, ctx context.Context) error { + t.Helper() + symbol := fixture.Symbol + interval, err := fixtureInterval(fixture.Options) + if err != nil { + return err + } + switch fixture.Stream { + case "trades": + _, err = client.SubscribeTrades(ctx, symbol) + case "bookTicker": + _, err = client.SubscribeBookTicker(ctx, symbol) + case "depthUpdates": + _, err = client.SubscribeDepthWithOptions(ctx, symbol, websocket.DepthSubscriptionOptions{Interval: interval}) + case "partialDepth": + levels, levelErr := fixtureOptionInt(fixture.Options, "levels") + if levelErr != nil { + return levelErr + } + var level websocket.PartialDepthLevel + switch levels { + case 0: + level = 0 + case 5: + level = websocket.DepthLevel5 + case 10: + level = websocket.DepthLevel10 + case 20: + level = websocket.DepthLevel20 + default: + return errors.New("invalid partial-depth fixture level") + } + _, err = client.SubscribePartialDepth(ctx, symbol, websocket.PartialDepthSubscriptionOptions{Levels: level, Interval: interval}) + case "contractStatus": + _, err = client.SubscribeContractStatus(ctx, symbol) + case "orders": + scope, scopeErr := fixtureOptionString(fixture.Options, "scope") + if scopeErr != nil { + return scopeErr + } + var subscriptionScope websocket.SubscriptionScope + switch scope { + case "account": + subscriptionScope = websocket.ScopeAccount + case "session": + subscriptionScope = websocket.ScopeSession + default: + return errors.New("invalid order scope fixture") + } + _, err = client.SubscribeOrderEventsWithScope(ctx, subscriptionScope) + case "balances": + _, err = client.SubscribeBalancesWithOptions(ctx, websocket.AccountStreamOptions{Interval: interval}) + case "positions": + _, err = client.SubscribePositionsWithOptions(ctx, websocket.AccountStreamOptions{Interval: interval}) + default: + return errors.New("unsupported WebSocket stream fixture: " + fixture.Stream) + } + return err +} + +func fixtureInterval(options map[string]json.RawMessage) (time.Duration, error) { + intervalMs, err := fixtureOptionInt(options, "intervalMs") + if err != nil { + return 0, err + } + return time.Duration(intervalMs) * time.Millisecond, nil +} + +func fixtureOptionInt(options map[string]json.RawMessage, key string) (int, error) { + raw, ok := options[key] + if !ok { + return 0, nil + } + var value int + if err := json.Unmarshal(raw, &value); err != nil { + return 0, err + } + return value, nil +} + +func fixtureOptionString(options map[string]json.RawMessage, key string) (string, error) { + raw, ok := options[key] + if !ok { + return "", nil + } + var value string + if err := json.Unmarshal(raw, &value); err != nil { + return "", err + } + return value, nil +} diff --git a/packages/sdk-go/generated/predictions/types.gen.go b/packages/sdk-go/generated/predictions/types.gen.go index 689cb654..cf7a1384 100644 --- a/packages/sdk-go/generated/predictions/types.gen.go +++ b/packages/sdk-go/generated/predictions/types.gen.go @@ -1523,6 +1523,8 @@ type ComboWriteError struct { // Contract Contract quantity and price validation is instrument-specific. Clients must validate order quantities and prices against the returned increment and minimum fields rather than assuming a fixed grid. type Contract struct { + // TotalShares Total shares available for the contract. + TotalShares *string `json:"totalShares,omitempty"` // AbbreviatedName Short form label (e.g., ">$90") AbbreviatedName *string `json:"abbreviatedName,omitempty"` Color *string `json:"color,omitempty"` @@ -1582,7 +1584,6 @@ type Contract struct { Strike *Strike `json:"strike,omitempty"` TermsAndConditionsUrl *string `json:"termsAndConditionsUrl,omitempty"` Ticker *string `json:"ticker,omitempty"` - TotalShares *string `json:"totalShares,omitempty"` } // ContractMarketState Trading state of the contract diff --git a/packages/sdk-go/generated/trading/types.gen.go b/packages/sdk-go/generated/trading/types.gen.go index 7b1c4eb9..cf9fbf42 100644 --- a/packages/sdk-go/generated/trading/types.gen.go +++ b/packages/sdk-go/generated/trading/types.gen.go @@ -2491,6 +2491,9 @@ type GetNotionalTradingVolumeJSONBody struct { // Request The API endpoint path Request string `json:"request"` + + // Symbol Optional. The symbol to get fee promotions or specific fee schedule rates for. + Symbol *string `json:"symbol,omitempty"` } // GetNotionalTradingVolumeParams defines parameters for GetNotionalTradingVolume. diff --git a/packages/sdk-go/scripts/contract_test.go b/packages/sdk-go/scripts/contract_test.go index d547bb17..455d67ef 100644 --- a/packages/sdk-go/scripts/contract_test.go +++ b/packages/sdk-go/scripts/contract_test.go @@ -7,7 +7,6 @@ import ( "go/parser" "go/token" "os" - "path" "path/filepath" "reflect" "strings" @@ -18,16 +17,20 @@ import ( ) func TestGeneratedDecimalFieldsPreserveWireType(t *testing.T) { + policy, err := loadNumericOverlay() + if err != nil { + t.Fatalf("loading numeric overlay: %v", err) + } for _, mod := range Modules { t.Run(mod.ID, func(t *testing.T) { - raw, err := loadPublishedSpec(mod.SpecURL) + raw, err := loadVendoredSpec(mod.SpecID) if err != nil { - t.Fatalf("reading spec %s: %v", mod.SpecURL, err) + t.Fatalf("reading spec %s: %v", mod.SpecID, err) } loader := openapi3.NewLoader() - doc, err := loader.LoadFromData(sanitizeSpecBytes(raw)) + doc, err := loader.LoadFromData(sanitizeSpecBytes(raw, policy)) if err != nil { - t.Fatalf("loading spec %s: %v", mod.SpecURL, err) + t.Fatalf("loading spec %s: %v", mod.SpecID, err) } code, err := RenderModule(mod) if err != nil { @@ -45,7 +48,7 @@ func TestGeneratedDecimalFieldsPreserveWireType(t *testing.T) { if property.Value == nil || property.Value.Type == nil || !property.Value.Type.Is("number") || property.Value.Format != "decimal" { continue } - want := "*openapi_types.DecimalNumber" + want := "*" + strings.Replace(policy.overlay.DecimalFormat.Go.NumberSchema, policy.overlay.DecimalFormat.Go.ImportAlias, "openapi_types", 1) got, ok := fields[schemaName][field] if !ok { t.Errorf("%s.%s was not found in generated code", schemaName, field) @@ -59,16 +62,20 @@ func TestGeneratedDecimalFieldsPreserveWireType(t *testing.T) { } func TestGeneratedWideIntegerFieldsAvoidPlatformSizedInts(t *testing.T) { + policy, err := loadNumericOverlay() + if err != nil { + t.Fatalf("loading numeric overlay: %v", err) + } for _, mod := range Modules { t.Run(mod.ID, func(t *testing.T) { - raw, err := loadPublishedSpec(mod.SpecURL) + raw, err := loadVendoredSpec(mod.SpecID) if err != nil { - t.Fatalf("reading spec %s: %v", mod.SpecURL, err) + t.Fatalf("reading spec %s: %v", mod.SpecID, err) } loader := openapi3.NewLoader() - doc, err := loader.LoadFromData(sanitizeSpecBytes(raw)) + doc, err := loader.LoadFromData(sanitizeSpecBytes(raw, policy)) if err != nil { - t.Fatalf("loading spec %s: %v", mod.SpecURL, err) + t.Fatalf("loading spec %s: %v", mod.SpecID, err) } code, err := RenderModule(mod) if err != nil { @@ -84,7 +91,7 @@ func TestGeneratedWideIntegerFieldsAvoidPlatformSizedInts(t *testing.T) { continue } for field, property := range schemaRef.Value.Properties { - if _, wide := wideIntegerPropertyNames[field]; !wide || property.Value == nil || property.Value.Type == nil { + if _, wide := policy.wideIntegerPropertyNames[field]; !wide || property.Value == nil || property.Value.Type == nil { continue } if len(property.Value.AllOf) > 0 || len(property.Value.AnyOf) > 0 || len(property.Value.OneOf) > 0 { @@ -98,7 +105,7 @@ func TestGeneratedWideIntegerFieldsAvoidPlatformSizedInts(t *testing.T) { t.Errorf("%s.%s was not found in generated code", schemaName, field) continue } - if field == "order_id" && (schemaName == "CancelOrderRequest" || schemaName == "OrderStatusRequest") { + if isUnsignedIntegerField(policy, schemaName, field) { if got != "uint64" { t.Errorf("%s.%s generated as %s, want uint64", schemaName, field, got) } @@ -113,6 +120,15 @@ func TestGeneratedWideIntegerFieldsAvoidPlatformSizedInts(t *testing.T) { } } +func isUnsignedIntegerField(policy *numericPolicy, schemaName, fieldName string) bool { + for _, location := range policy.overlay.UnsignedIntegers.Locations { + if location.Schema == schemaName && location.Property == fieldName { + return true + } + } + return false +} + func generatedFieldTypes(code string) (map[string]map[string]string, error) { file, err := parser.ParseFile(token.NewFileSet(), "generated.go", code, 0) if err != nil { @@ -160,7 +176,7 @@ func TestOpenAPIContractDrift(t *testing.T) { t.Run(mod.ID, func(t *testing.T) { expectedCode, err := RenderModule(mod) if err != nil { - t.Fatalf("failed to render module %s from spec %s: %v", mod.ID, mod.SpecURL, err) + t.Fatalf("failed to render module %s from spec %s: %v", mod.ID, mod.SpecID, err) } committedFile := filepath.Join("..", "generated", mod.Package, "types.gen.go") @@ -193,14 +209,18 @@ func TestOpenAPIContractDrift(t *testing.T) { } t.Fatalf("Contract drift detected in %s! The committed code in %s does not match the current OpenAPI specification %s.\n"+ "Run 'go run ./scripts/generate.go' to regenerate and commit the updated models.", - mod.ID, committedFile, mod.SpecURL) + mod.ID, committedFile, mod.SpecID) } }) } } func TestNoOrphanedEndpoints(t *testing.T) { - specs := []string{restSpecURL, predictionMarketsSpecURL} + policy, err := loadNumericOverlay() + if err != nil { + t.Fatalf("loading numeric overlay: %v", err) + } + specs := []string{restSpecID, predictionMarketsSpecID} allMappedTags := make(map[string]bool) for _, mod := range Modules { @@ -209,21 +229,22 @@ func TestNoOrphanedEndpoints(t *testing.T) { } } - for _, specURL := range specs { - t.Run(path.Base(specURL), func(t *testing.T) { - raw, err := loadPublishedSpec(specURL) + for _, specID := range specs { + specName := specBasename(specID) + t.Run(specName, func(t *testing.T) { + raw, err := loadVendoredSpec(specID) if err != nil { - t.Fatalf("reading spec %s: %v", specURL, err) + t.Fatalf("reading spec %s: %v", specID, err) } loader := openapi3.NewLoader() - doc, err := loader.LoadFromData(sanitizeSpecBytes(raw)) + doc, err := loader.LoadFromData(sanitizeSpecBytes(raw, policy)) if err != nil { - t.Fatalf("loading openapi doc %s: %v", specURL, err) + t.Fatalf("loading openapi doc %s: %v", specID, err) } if doc.Paths == nil { - t.Fatalf("spec %s has no paths", specURL) + t.Fatalf("spec %s has no paths", specID) } var orphanedOps []string @@ -250,26 +271,31 @@ func TestNoOrphanedEndpoints(t *testing.T) { if len(orphanedOps) > 0 { t.Errorf("Found %d orphaned operations in %s not covered by any SDK module tags:\n - %s", - len(orphanedOps), path.Base(specURL), strings.Join(orphanedOps, "\n - ")) + len(orphanedOps), specName, strings.Join(orphanedOps, "\n - ")) } }) } } func TestOperationIDsUnique(t *testing.T) { - specs := []string{restSpecURL, predictionMarketsSpecURL} + policy, err := loadNumericOverlay() + if err != nil { + t.Fatalf("loading numeric overlay: %v", err) + } + specs := []string{restSpecID, predictionMarketsSpecID} - for _, specURL := range specs { - t.Run(path.Base(specURL), func(t *testing.T) { - raw, err := loadPublishedSpec(specURL) + for _, specID := range specs { + specName := specBasename(specID) + t.Run(specName, func(t *testing.T) { + raw, err := loadVendoredSpec(specID) if err != nil { - t.Fatalf("reading spec %s: %v", specURL, err) + t.Fatalf("reading spec %s: %v", specID, err) } loader := openapi3.NewLoader() - doc, err := loader.LoadFromData(sanitizeSpecBytes(raw)) + doc, err := loader.LoadFromData(sanitizeSpecBytes(raw, policy)) if err != nil { - t.Fatalf("loading openapi doc %s: %v", specURL, err) + t.Fatalf("loading openapi doc %s: %v", specID, err) } seenIDs := make(map[string]string) @@ -292,10 +318,10 @@ func TestOperationIDsUnique(t *testing.T) { } func TestAsyncAPIWebSocketStreams(t *testing.T) { - specURL := websocketSpecURL - raw, err := loadPublishedSpec(specURL) + specID := websocketSpecID + raw, err := loadVendoredSpec(specID) if err != nil { - t.Fatalf("reading websocket spec %s: %v", specURL, err) + t.Fatalf("reading websocket spec %s: %v", specID, err) } var root struct { diff --git a/packages/sdk-go/scripts/generate.go b/packages/sdk-go/scripts/generate.go index 3319c662..ca5a04a4 100644 --- a/packages/sdk-go/scripts/generate.go +++ b/packages/sdk-go/scripts/generate.go @@ -3,198 +3,413 @@ package main import ( "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "go/format" - "io" - "net/http" "os" - "path" "path/filepath" "regexp" "strings" "sync" - "time" "github.com/getkin/kin-openapi/openapi3" "github.com/oapi-codegen/oapi-codegen/v2/pkg/codegen" + "gopkg.in/yaml.v3" ) type ModuleConfig struct { ID string Package string - SpecURL string + SpecID string Tags []string } const ( - restSpecURL = "https://developer.gemini.com/specs/openapi/rest.yaml" - predictionMarketsSpecURL = "https://developer.gemini.com/specs/openapi/prediction-markets.yaml" - websocketSpecURL = "https://developer.gemini.com/specs/asyncapi/websocket.yaml" + restSpecID = "rest" + predictionMarketsSpecID = "predictionMarkets" + websocketSpecID = "websocket" ) -// Update these values only in a reviewed change that also updates generated output. -var publishedSpecSHA256 = map[string]string{ - restSpecURL: "79a0dc4061f3942dca8b30a589bbd406c781d2c6c19283d87cb21177afdcab5e", - predictionMarketsSpecURL: "0c70a976f4553ae39d14d6851416cb974f081919216b94ebd851f044d108cfe7", - websocketSpecURL: "d83c624336f16542c3f1f4554101e7fa19bbc703012ae7bcd780c401667a5def", -} - -var ( - publishedSpecClient = &http.Client{ - Timeout: 30 * time.Second, - // The specification URLs are allowlisted and pinned by digest. Do not - // follow redirects because that would permit an allowlisted HTTPS URL to - // fetch from an unexpected host or downgrade to HTTP. - CheckRedirect: func(_ *http.Request, _ []*http.Request) error { - return http.ErrUseLastResponse - }, - } - publishedSpecCache = struct { - sync.Mutex - values map[string][]byte - }{values: make(map[string][]byte)} -) +type specSource struct { + ID string `json:"id"` + Path string `json:"path"` + SHA256 string `json:"sha256"` +} + +type specSourcesManifest struct { + Version int `json:"version"` + Specs []specSource `json:"specs"` +} +type numericOverlay struct { + Version int `yaml:"version"` + FormatAliases numericFormatAliases `yaml:"formatAliases"` + DecimalFormat numericDecimalFormat `yaml:"decimalFormat"` + WideIntegers numericWideIntegers `yaml:"wideIntegers"` + UnsignedIntegers numericUnsignedIntegers `yaml:"unsignedIntegers"` + SchemaOverrides numericSchemaOverrides `yaml:"schemaOverrides"` +} + +type numericFormatAliases struct { + AppliesTo []string `yaml:"appliesTo"` + Reason string `yaml:"reason"` + Rules []numericFormatAlias `yaml:"rules"` +} + +type numericFormatAlias struct { + From string `yaml:"from"` + To string `yaml:"to"` +} + +type numericDecimalFormat struct { + AppliesTo []string `yaml:"appliesTo"` + Format string `yaml:"format"` + Go numericDecimalGo `yaml:"go"` + TypeScript numericDecimalTS `yaml:"typescript"` +} + +type numericDecimalGo struct { + NumberSchema string `yaml:"numberSchema"` + StringSchema string `yaml:"stringSchema"` + ImportPath string `yaml:"importPath"` + ImportAlias string `yaml:"importAlias"` +} + +type numericDecimalTS struct { + NumberSchema string `yaml:"numberSchema"` + StringSchema string `yaml:"stringSchema"` + ExactArithmetic string `yaml:"exactArithmetic"` +} + +type numericWideIntegers struct { + AppliesTo []string `yaml:"appliesTo"` + Reason string `yaml:"reason"` + Matches []string `yaml:"matches"` + SkipComposedSchemas bool `yaml:"skipComposedSchemas"` + Properties []string `yaml:"properties"` +} + +type numericUnsignedIntegers struct { + AppliesTo []string `yaml:"appliesTo"` + Extension string `yaml:"extension"` + Locations []numericUnsignedLocation `yaml:"locations"` +} + +type numericUnsignedLocation struct { + Schema string `yaml:"schema"` + Property string `yaml:"property"` +} + +type numericSchemaOverrides struct { + AppliesTo []string `yaml:"appliesTo"` + Reason string `yaml:"reason"` + DecimalFields []numericDecimalField `yaml:"decimalFields"` + Int64Fields []numericInt64Field `yaml:"int64Fields"` + Int64OneOfVariants []numericSchemaReference `yaml:"int64OneOfVariants"` +} + +type numericDecimalField struct { + Schema string `yaml:"schema"` + Properties []string `yaml:"properties"` +} + +type numericInt64Field struct { + Schema string `yaml:"schema"` + Property string `yaml:"property"` +} + +type numericSchemaReference struct { + Schema string `yaml:"schema"` +} + +type numericFormatRegex struct { + regex *regexp.Regexp + replacement []byte +} + +type numericPolicy struct { + overlay numericOverlay + formatAliases []numericFormatRegex + integerFormats map[string]struct{} + wideIntegerPropertyNames map[string]struct{} + integerFormat string +} + +var numericOverlayCache struct { + sync.Once + value *numericPolicy + err error +} + +func loadNumericOverlay() (*numericPolicy, error) { + numericOverlayCache.Do(func() { + root, err := specsRoot() + if err != nil { + numericOverlayCache.err = fmt.Errorf("loading numeric overlay: %w", err) + return + } + raw, err := os.ReadFile(filepath.Join(root, "overlays", "numeric-types.yaml")) + if err != nil { + numericOverlayCache.err = fmt.Errorf("loading numeric overlay: %w", err) + return + } + var overlay numericOverlay + if err := yaml.Unmarshal(raw, &overlay); err != nil { + numericOverlayCache.err = fmt.Errorf("loading numeric overlay: %w", err) + return + } -const maxPublishedSpecBytes int64 = 16 << 20 + policy := &numericPolicy{ + overlay: overlay, + integerFormats: make(map[string]struct{}), + wideIntegerPropertyNames: make(map[string]struct{}, len(overlay.WideIntegers.Properties)), + } + for _, propertyName := range overlay.WideIntegers.Properties { + policy.wideIntegerPropertyNames[propertyName] = struct{}{} + } + for _, alias := range overlay.FormatAliases.Rules { + pattern := fmt.Sprintf(`(?m)^(\s*)format:\s*%s\s*$`, regexp.QuoteMeta(alias.From)) + re, err := regexp.Compile(pattern) + if err != nil { + numericOverlayCache.err = fmt.Errorf("loading numeric overlay: %w", err) + return + } + policy.formatAliases = append(policy.formatAliases, numericFormatRegex{ + regex: re, + replacement: []byte("${1}format: " + alias.To), + }) + if alias.To != "" { + policy.integerFormats[alias.To] = struct{}{} + if policy.integerFormat == "" { + policy.integerFormat = alias.To + } + } + } + numericOverlayCache.value = policy + }) + return numericOverlayCache.value, numericOverlayCache.err +} + +var publishedSpecCache = struct { + sync.Mutex + values map[string][]byte +}{values: make(map[string][]byte)} var Modules = []ModuleConfig{ { ID: "marketdata", Package: "marketdata", - SpecURL: restSpecURL, + SpecID: restSpecID, Tags: []string{"Market Data"}, }, { ID: "trading", Package: "trading", - SpecURL: restSpecURL, + SpecID: restSpecID, Tags: []string{"Orders", "Session"}, }, { ID: "margin", Package: "margin", - SpecURL: restSpecURL, + SpecID: restSpecID, Tags: []string{"Margin Trading"}, }, { ID: "perpetuals", Package: "perpetuals", - SpecURL: restSpecURL, + SpecID: restSpecID, Tags: []string{"Derivatives"}, }, { ID: "account", Package: "account", - SpecURL: restSpecURL, + SpecID: restSpecID, Tags: []string{"Account Administration", "Fund Management", "OAuth", "Staking"}, }, { ID: "clearing", Package: "clearing", - SpecURL: restSpecURL, + SpecID: restSpecID, Tags: []string{"Clearing", "Instant"}, }, { ID: "predictions", Package: "predictions", - SpecURL: predictionMarketsSpecURL, + SpecID: predictionMarketsSpecID, Tags: []string{"Combos", "Markets", "Positions", "Rewards", "Terms", "Trading", "Volume"}, }, } -var ( - longFormatRegex = regexp.MustCompile(`(?m)^(\s*)format:\s*long\s*$`) - integerFormatRegex = regexp.MustCompile(`(?m)^(\s*)format:\s*integer\s*$`) -) +func specsRoot() (string, error) { + startDir, err := os.Getwd() + if err != nil { + return "", err + } + dir := startDir + for { + manifestPath := filepath.Join(dir, "specs", "SOURCES.json") + if _, err := os.Stat(manifestPath); err == nil { + return filepath.Join(dir, "specs"), nil + } else if !os.IsNotExist(err) { + return "", err + } + + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + return "", fmt.Errorf("specs/SOURCES.json not found above %s", startDir) +} + +func loadVendoredSpec(specID string) ([]byte, error) { + if specID != restSpecID && specID != predictionMarketsSpecID && specID != websocketSpecID { + return nil, fmt.Errorf("unknown specification id: %s", specID) + } + + publishedSpecCache.Lock() + cached := publishedSpecCache.values[specID] + publishedSpecCache.Unlock() + if cached != nil { + return cached, nil + } + + root, err := specsRoot() + if err != nil { + return nil, err + } + manifestBytes, err := os.ReadFile(filepath.Join(root, "SOURCES.json")) + if err != nil { + return nil, err + } + var manifest specSourcesManifest + if err := json.Unmarshal(manifestBytes, &manifest); err != nil { + return nil, err + } + + var entry *specSource + for i := range manifest.Specs { + if manifest.Specs[i].ID == specID { + entry = &manifest.Specs[i] + break + } + } + if entry == nil { + return nil, fmt.Errorf("unknown specification id: %s", specID) + } + + raw, err := os.ReadFile(filepath.Join(root, entry.Path)) + if err != nil { + return nil, err + } + digest := sha256.Sum256(raw) + actualHash := hex.EncodeToString(digest[:]) + if actualHash != entry.SHA256 { + return nil, fmt.Errorf("vendored specification digest mismatch for %s: expected %s, got %s; run node specs/refresh.mjs", entry.Path, entry.SHA256, actualHash) + } -// These fields are identifiers or timestamps whose valid values are wider -// than the native int on 32-bit builds. Keep bounded collection sizes and -// other API limits as int because callers use them as local slice/request -// sizes, but never represent wire-level wide integers with a platform-sized -// type. -var wideIntegerPropertyNames = map[string]struct{}{ - "cancelRejects": {}, - "cancelledOrders": {}, - "eid": {}, - "last_updated_ms": {}, - "order_id": {}, - "quoteId": {}, - "since_tid": {}, - "tid": {}, - "timestamp_nanos": {}, - "timestampms": {}, - "txTime": {}, -} - -func sanitizeSpecBytes(data []byte) []byte { - out := longFormatRegex.ReplaceAll(data, []byte("${1}format: int64")) - out = integerFormatRegex.ReplaceAll(out, []byte("${1}format: int64")) + publishedSpecCache.Lock() + if existing := publishedSpecCache.values[specID]; existing != nil { + raw = existing + } else { + publishedSpecCache.values[specID] = raw + } + publishedSpecCache.Unlock() + + return raw, nil +} + +func specBasename(specID string) string { + switch specID { + case restSpecID: + return "rest.yaml" + case predictionMarketsSpecID: + return "prediction-markets.yaml" + case websocketSpecID: + return "websocket.yaml" + default: + return "" + } +} + +func sanitizeSpecBytes(data []byte, policy *numericPolicy) []byte { + out := data + for _, alias := range policy.formatAliases { + out = alias.regex.ReplaceAll(out, alias.replacement) + } return out } -func fixSchema(s *openapi3.Schema) { +func isComposedSchema(s *openapi3.Schema) bool { + return len(s.AllOf) > 0 || len(s.AnyOf) > 0 || len(s.OneOf) > 0 +} + +func setInt64Override(s *openapi3.Schema, policy *numericPolicy) { if s == nil { return } + s.Type = &openapi3.Types{"integer"} + s.Format = policy.integerFormat +} + +func fixSchema(s *openapi3.Schema, policy *numericPolicy) { + if s == nil || policy == nil { + return + } + if s.Extensions != nil { + if unsigned, ok := s.Extensions[policy.overlay.UnsignedIntegers.Extension].(bool); ok && unsigned { + setGoTypeOverride(s, "uint64") + } + } if s.Type != nil { - if s.Type.Is("number") { - if s.Format == "long" || s.Format == "integer" || s.Format == "int64" { - s.Type = &openapi3.Types{"integer"} - s.Format = "int64" - } else if s.Format == "decimal" { - setDecimalOverride(s) - } - } else if s.Type.Is("integer") { - if s.Format == "long" || s.Format == "integer" { - s.Format = "int64" - } + if _, wide := policy.integerFormats[s.Format]; wide && s.Type.Is("number") { + setInt64Override(s, policy) + } else if s.Type.Is("number") && s.Format == policy.overlay.DecimalFormat.Format { + setDecimalOverride(s, policy) } } for propertyName, prop := range s.Properties { if prop.Value != nil { - setWideIntegerOverride(propertyName, prop.Value) - fixSchema(prop.Value) + setWideIntegerOverride(propertyName, prop.Value, policy) + fixSchema(prop.Value, policy) } } for _, allOf := range s.AllOf { if allOf.Value != nil { - fixSchema(allOf.Value) + fixSchema(allOf.Value, policy) } } for _, anyOf := range s.AnyOf { if anyOf.Value != nil { - fixSchema(anyOf.Value) + fixSchema(anyOf.Value, policy) } } for _, oneOf := range s.OneOf { if oneOf.Value != nil { - fixSchema(oneOf.Value) + fixSchema(oneOf.Value, policy) } } if s.Items != nil && s.Items.Value != nil { - fixSchema(s.Items.Value) + fixSchema(s.Items.Value, policy) } if s.AdditionalProperties.Schema != nil && s.AdditionalProperties.Schema.Value != nil { - fixSchema(s.AdditionalProperties.Schema.Value) + fixSchema(s.AdditionalProperties.Schema.Value, policy) } } -func setWideIntegerOverride(propertyName string, s *openapi3.Schema) { - if _, ok := wideIntegerPropertyNames[propertyName]; !ok || s == nil || s.Type == nil { +func setWideIntegerOverride(propertyName string, s *openapi3.Schema, policy *numericPolicy) { + if _, ok := policy.wideIntegerPropertyNames[propertyName]; !ok || s == nil || s.Type == nil { return } if s.Type.Is("array") && s.Items != nil && s.Items.Value != nil { - setWideIntegerOverride(propertyName, s.Items.Value) + setWideIntegerOverride(propertyName, s.Items.Value, policy) return } - // Do not replace unions such as timestamp aliases. The generated type may - // intentionally support both numeric and string representations. - if len(s.AllOf) > 0 || len(s.AnyOf) > 0 || len(s.OneOf) > 0 { + if policy.overlay.WideIntegers.SkipComposedSchemas && isComposedSchema(s) { return } if s.Type.Is("integer") || s.Type.Is("number") { - s.Type = &openapi3.Types{"integer"} - s.Format = "int64" + setInt64Override(s, policy) } } @@ -205,93 +420,42 @@ func setGoTypeOverride(s *openapi3.Schema, goType string) { s.Extensions["x-go-type"] = goType } -func setDecimalOverride(s *openapi3.Schema) { - goType := "types.Decimal" +func setDecimalOverride(s *openapi3.Schema, policy *numericPolicy) { + goType := policy.overlay.DecimalFormat.Go.StringSchema if s.Type != nil && s.Type.Is("number") { - goType = "types.DecimalNumber" + goType = policy.overlay.DecimalFormat.Go.NumberSchema } setGoTypeOverride(s, goType) if s.Extensions == nil { s.Extensions = make(map[string]any) } s.Extensions["x-go-type-import"] = map[string]any{ - "path": "github.com/gemini/developer-platform/packages/sdk-go/types", - "name": "types", - } -} - -func loadPublishedSpec(specURL string) ([]byte, error) { - expectedHash, ok := publishedSpecSHA256[specURL] - if !ok { - return nil, fmt.Errorf("unallowlisted published specification URL: %s", specURL) - } - - publishedSpecCache.Lock() - cached := publishedSpecCache.values[specURL] - publishedSpecCache.Unlock() - if cached != nil { - return cached, nil - } - - response, err := publishedSpecClient.Get(specURL) - if err != nil { - return nil, fmt.Errorf("fetching spec %s: %w", specURL, err) - } - defer response.Body.Close() - - if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { - return nil, fmt.Errorf("fetching spec %s: HTTP %s", specURL, response.Status) - } - if response.ContentLength > maxPublishedSpecBytes { - return nil, fmt.Errorf("published specification %s exceeds %d-byte limit", specURL, maxPublishedSpecBytes) - } - - raw, err := io.ReadAll(io.LimitReader(response.Body, maxPublishedSpecBytes+1)) - if err != nil { - return nil, fmt.Errorf("reading spec %s: %w", specURL, err) + "path": policy.overlay.DecimalFormat.Go.ImportPath, + "name": policy.overlay.DecimalFormat.Go.ImportAlias, } - if int64(len(raw)) > maxPublishedSpecBytes { - return nil, fmt.Errorf("published specification %s exceeds %d-byte limit", specURL, maxPublishedSpecBytes) - } - - digest := sha256.Sum256(raw) - actualHash := hex.EncodeToString(digest[:]) - if actualHash != expectedHash { - return nil, fmt.Errorf("published specification hash mismatch for %s: expected %s, got %s", specURL, expectedHash, actualHash) - } - - publishedSpecCache.Lock() - if existing := publishedSpecCache.values[specURL]; existing != nil { - raw = existing - } else { - publishedSpecCache.values[specURL] = raw - } - publishedSpecCache.Unlock() - - return raw, nil } -func fixDoc(doc *openapi3.T) { - if doc == nil { +func fixDoc(doc *openapi3.T, policy *numericPolicy) { + if doc == nil || policy == nil { return } if doc.Components != nil { for _, schemaRef := range doc.Components.Schemas { if schemaRef != nil && schemaRef.Value != nil { - fixSchema(schemaRef.Value) + fixSchema(schemaRef.Value, policy) } } for _, paramRef := range doc.Components.Parameters { if paramRef != nil && paramRef.Value != nil && paramRef.Value.Schema != nil && paramRef.Value.Schema.Value != nil { - setWideIntegerOverride(paramRef.Value.Name, paramRef.Value.Schema.Value) - fixSchema(paramRef.Value.Schema.Value) + setWideIntegerOverride(paramRef.Value.Name, paramRef.Value.Schema.Value, policy) + fixSchema(paramRef.Value.Schema.Value, policy) } } for _, respRef := range doc.Components.Responses { if respRef != nil && respRef.Value != nil { for _, content := range respRef.Value.Content { if content != nil && content.Schema != nil && content.Schema.Value != nil { - fixSchema(content.Schema.Value) + fixSchema(content.Schema.Value, policy) } } } @@ -308,14 +472,14 @@ func fixDoc(doc *openapi3.T) { } for _, paramRef := range op.Parameters { if paramRef != nil && paramRef.Value != nil && paramRef.Value.Schema != nil && paramRef.Value.Schema.Value != nil { - setWideIntegerOverride(paramRef.Value.Name, paramRef.Value.Schema.Value) - fixSchema(paramRef.Value.Schema.Value) + setWideIntegerOverride(paramRef.Value.Name, paramRef.Value.Schema.Value, policy) + fixSchema(paramRef.Value.Schema.Value, policy) } } if op.RequestBody != nil && op.RequestBody.Value != nil { for _, content := range op.RequestBody.Value.Content { if content != nil && content.Schema != nil && content.Schema.Value != nil { - fixSchema(content.Schema.Value) + fixSchema(content.Schema.Value, policy) } } } @@ -324,7 +488,7 @@ func fixDoc(doc *openapi3.T) { if respRef != nil && respRef.Value != nil { for _, content := range respRef.Value.Content { if content != nil && content.Schema != nil && content.Schema.Value != nil { - fixSchema(content.Schema.Value) + fixSchema(content.Schema.Value, policy) } } } @@ -333,60 +497,158 @@ func fixDoc(doc *openapi3.T) { } } } - applySDKTypeOverrides(doc) + applySDKTypeOverrides(doc, policy) } // applySDKTypeOverrides contains Go-specific model decisions that are owned by // this SDK repository rather than the shared REST description. Keeping these // transformations here makes generation reproducible without changing the // API source specification. -func applySDKTypeOverrides(doc *openapi3.T) { - if doc.Components == nil { +func applySDKTypeOverrides(doc *openapi3.T, policy *numericPolicy) { + if doc == nil || doc.Components == nil || policy == nil { return } - if balance := doc.Components.Schemas["Balance"]; balance != nil && balance.Value != nil { - for _, field := range []string{"amount", "available", "availableForWithdrawal", "pendingWithdrawal", "pendingDeposit"} { - if prop := balance.Value.Properties[field]; prop != nil && prop.Value != nil { - setDecimalOverride(prop.Value) + for _, override := range policy.overlay.SchemaOverrides.DecimalFields { + schema := doc.Components.Schemas[override.Schema] + if schema == nil || schema.Value == nil { + continue + } + for _, field := range override.Properties { + if prop := schema.Value.Properties[field]; prop != nil && prop.Value != nil { + setDecimalOverride(prop.Value, policy) } } } - if order := doc.Components.Schemas["NewOrderRequest"]; order != nil && order.Value != nil { - if nonce := order.Value.Properties["nonce"]; nonce != nil && nonce.Value != nil { - nonce.Value.Type = &openapi3.Types{"integer"} - nonce.Value.Format = "int64" + for _, override := range policy.overlay.SchemaOverrides.Int64Fields { + schema := doc.Components.Schemas[override.Schema] + if schema == nil || schema.Value == nil { + continue } - } - if cancel := doc.Components.Schemas["CancelOrderRequest"]; cancel != nil && cancel.Value != nil { - if orderID := cancel.Value.Properties["order_id"]; orderID != nil && orderID.Value != nil { - // Order IDs are unsigned 64-bit wire values. Using uint64 avoids - // rejecting valid IDs above MaxInt64 in cancellation helpers. - setGoTypeOverride(orderID.Value, "uint64") + if prop := schema.Value.Properties[override.Property]; prop != nil && prop.Value != nil { + setInt64Override(prop.Value, policy) } } - if status := doc.Components.Schemas["OrderStatusRequest"]; status != nil && status.Value != nil { - if orderID := status.Value.Properties["order_id"]; orderID != nil && orderID.Value != nil { - setGoTypeOverride(orderID.Value, "uint64") + for _, override := range policy.overlay.SchemaOverrides.Int64OneOfVariants { + schema := doc.Components.Schemas[override.Schema] + if schema == nil || schema.Value == nil { + continue } - } - if nonce := doc.Components.Schemas["Nonce"]; nonce != nil && nonce.Value != nil { - for _, variant := range nonce.Value.OneOf { + for _, variant := range schema.Value.OneOf { if variant.Value != nil && variant.Value.Type != nil && variant.Value.Type.Is("integer") { - variant.Value.Type = &openapi3.Types{"integer"} - variant.Value.Format = "int64" + setInt64Override(variant.Value, policy) } } } } +// preserveContractTotalShares keeps the field that older versions of the +// prediction-markets API exposed even when the current schema omits it. +func preserveContractTotalShares(code string, predictionMarkets bool) string { + if !predictionMarkets { + return code + } + const marker = "type Contract struct {\n" + start := strings.Index(code, marker) + if start < 0 { + return code + } + bodyStart := start + len(marker) + bodyEnd := strings.Index(code[bodyStart:], "\n}") + if bodyEnd < 0 { + return code + } + bodyEnd += bodyStart + if strings.Contains(code[bodyStart:bodyEnd], "TotalShares ") { + return code + } + return code[:bodyStart] + + "\t// TotalShares Total shares available for the contract.\n" + + "\tTotalShares *string `json:\"totalShares,omitempty\"`\n" + + code[bodyStart:] +} + +// rewriteDecimalImportsAndTypes rewrites decimal references and deduplicates +// the generated import for the configured decimal package. +func rewriteDecimalImportsAndTypes(code string, decimalGo numericDecimalGo) string { + const runtimeTypesPath = "github.com/oapi-codegen/runtime/types" + importPath := decimalGo.ImportPath + if importPath == "" { + return code + } + + // oapi-codegen uses the existing runtime/types import alias for generated + targetAlias := "openapi_" + strings.TrimPrefix(decimalGo.ImportAlias, "openapi_") + if decimalGo.ImportAlias == "" { + targetAlias = "openapi_types" + } + runtimeImport := regexp.MustCompile(`(?m)^[ \t]*([[:alnum:]_]+)[ \t]+` + regexp.QuoteMeta(fmt.Sprintf("%q", runtimeTypesPath)) + `[ \t]*\r?\n`) + if match := runtimeImport.FindStringSubmatch(code); len(match) == 2 { + targetAlias = match[1] + } + + quotedImportPath := fmt.Sprintf("%q", importPath) + code = strings.ReplaceAll(code, fmt.Sprintf("%q", runtimeTypesPath), quotedImportPath) + + // Keep exactly one import for the configured package. Depending on which + // generated model first requires it, codegen may emit several aliases for + // the same path (for example, openapi_types and openapi_openapi_types). + importLine := regexp.MustCompile(`(?m)^[ \t]*(?:[[:alnum:]_]+[ \t]+)?` + regexp.QuoteMeta(quotedImportPath) + `[ \t]*\r?\n`) + seenImport := false + code = importLine.ReplaceAllStringFunc(code, func(string) string { + if seenImport { + return "" + } + seenImport = true + return "\t" + targetAlias + " " + quotedImportPath + "\n" + }) + + for _, configuredType := range []string{decimalGo.NumberSchema, decimalGo.StringSchema} { + if configuredType == "" { + continue + } + typeQualifier := decimalGo.ImportAlias + typeName := configuredType + if dot := strings.LastIndex(configuredType, "."); dot >= 0 { + typeQualifier = configuredType[:dot] + typeName = configuredType[dot+1:] + } + + // Match complete qualified identifiers. An unbounded replacement of + // "types.Decimal" also matches the suffix of "openapi_types.Decimal", + // producing the invalid "openapi_openapi_types.Decimal". + qualifiers := []string{ + typeQualifier + "." + typeName, + decimalGo.ImportAlias + "." + typeName, + targetAlias + "." + typeName, + "openapi_" + typeQualifier + "." + typeName, + "openapi_" + decimalGo.ImportAlias + "." + typeName, + "openapi_" + targetAlias + "." + typeName, + } + seenQualifiers := make(map[string]struct{}, len(qualifiers)) + for _, qualifier := range qualifiers { + if _, seen := seenQualifiers[qualifier]; seen || qualifier == "" { + continue + } + seenQualifiers[qualifier] = struct{}{} + qualifiedType := regexp.MustCompile(`(^|[^[:alnum:]_])` + regexp.QuoteMeta(qualifier) + `([^[:alnum:]_]|$)`) + code = qualifiedType.ReplaceAllString(code, `${1}`+targetAlias+"."+typeName+`${2}`) + } + } + return code +} + // RenderModule generates the Go code for a given module configuration from its OpenAPI spec. func RenderModule(mod ModuleConfig) (string, error) { - raw, err := loadPublishedSpec(mod.SpecURL) + policy, err := loadNumericOverlay() + if err != nil { + return "", err + } + raw, err := loadVendoredSpec(mod.SpecID) if err != nil { return "", err } - sanitized := sanitizeSpecBytes(raw) + sanitized := sanitizeSpecBytes(raw, policy) loader := openapi3.NewLoader() loader.IsExternalRefsAllowed = false @@ -395,7 +657,7 @@ func RenderModule(mod ModuleConfig) (string, error) { return "", fmt.Errorf("loading openapi doc: %w", err) } - fixDoc(doc) + fixDoc(doc, policy) cfg := codegen.Configuration{ PackageName: mod.Package, @@ -413,21 +675,16 @@ func RenderModule(mod ModuleConfig) (string, error) { return "", fmt.Errorf("generating code for %s: %w", mod.ID, err) } - // Rewire third-party runtime imports to stdlib-backed internal packages - code = strings.ReplaceAll(code, "\"github.com/oapi-codegen/runtime/types\"", "\"github.com/gemini/developer-platform/packages/sdk-go/types\"") + // Rewire third-party runtime imports to stdlib-backed internal packages. code = strings.ReplaceAll(code, "\"github.com/oapi-codegen/runtime\"", "\"github.com/gemini/developer-platform/packages/sdk-go/internal/runtime\"") - // The generated files already import the shared types package as - // openapi_types for dates and UUIDs. Reuse that alias for Decimal fields so - // generation does not emit two imports of the same package under different - // names. - code = strings.ReplaceAll(code, "types.Decimal", "openapi_types.Decimal") - code = strings.ReplaceAll(code, "\ttypes \"github.com/gemini/developer-platform/packages/sdk-go/types\"\n", "") - - // Normalize tool version comment line to ensure deterministic comparison across environments + code = rewriteDecimalImportsAndTypes(code, policy.overlay.DecimalFormat.Go) + code = preserveContractTotalShares(code, mod.SpecID == predictionMarketsSpecID) + + // Normalize tool version comment line to ensure deterministic comparison across environments. versionRegex := regexp.MustCompile(`(?m)^// Code generated by .* DO NOT EDIT\.\r?\n`) code = versionRegex.ReplaceAllString(code, "// Code generated by oapi-codegen. DO NOT EDIT.\n") - header := fmt.Sprintf("// Code generated from %s (%s). DO NOT EDIT.\n\n", path.Base(mod.SpecURL), strings.Join(mod.Tags, ", ")) + header := fmt.Sprintf("// Code generated from %s (%s). DO NOT EDIT.\n\n", specBasename(mod.SpecID), strings.Join(mod.Tags, ", ")) formatted, err := format.Source([]byte(header + code)) if err != nil { return "", fmt.Errorf("formatting generated code for %s: %w", mod.ID, err) @@ -441,7 +698,6 @@ func GenerateModule(mod ModuleConfig) error { if err != nil { return err } - outDir := filepath.Join("..", "generated", mod.Package) if err := os.MkdirAll(outDir, 0750); err != nil { return fmt.Errorf("creating dir %s: %w", outDir, err) diff --git a/packages/sdk-go/scripts/release_smoke.sh b/packages/sdk-go/scripts/release_smoke.sh index 62e1d585..78480763 100644 --- a/packages/sdk-go/scripts/release_smoke.sh +++ b/packages/sdk-go/scripts/release_smoke.sh @@ -11,6 +11,9 @@ staged_sdk="$staging_root/packages/sdk-go" consumer="$staging_root/consumer" mkdir -p "$(dirname "$staged_sdk")" cp -R "$sdk_root" "$staged_sdk" +repo_root="$(cd "$sdk_root/../.." && pwd)" +cp -R "$repo_root/specs" "$staging_root/specs" +cp -R "$repo_root/conformance" "$staging_root/conformance" go test -C "$staged_sdk" ./... go test -C "$staged_sdk/websocket/gorilla" ./... diff --git a/packages/sdk-go/scripts/service_coverage_test.go b/packages/sdk-go/scripts/service_coverage_test.go index 3302dcfa..f007ffe7 100644 --- a/packages/sdk-go/scripts/service_coverage_test.go +++ b/packages/sdk-go/scripts/service_coverage_test.go @@ -1,7 +1,6 @@ package main import ( - "path" "strings" "testing" @@ -135,19 +134,23 @@ var sdkRESTOperationCoverage = map[string]restOperationCoverage{ } func TestRESTOperationCoverageManifest(t *testing.T) { - specs := []string{restSpecURL, predictionMarketsSpecURL} + policy, err := loadNumericOverlay() + if err != nil { + t.Fatalf("loading numeric overlay: %v", err) + } + specs := []string{restSpecID, predictionMarketsSpecID} seen := make(map[string]struct{}) - for _, specURL := range specs { - specName := path.Base(specURL) - raw, err := loadPublishedSpec(specURL) + for _, specID := range specs { + specName := specBasename(specID) + raw, err := loadVendoredSpec(specID) if err != nil { - t.Fatalf("reading spec %s: %v", specURL, err) + t.Fatalf("reading spec %s: %v", specID, err) } loader := openapi3.NewLoader() - doc, err := loader.LoadFromData(sanitizeSpecBytes(raw)) + doc, err := loader.LoadFromData(sanitizeSpecBytes(raw, policy)) if err != nil { - t.Fatalf("loading openapi doc %s: %v", specURL, err) + t.Fatalf("loading openapi doc %s: %v", specID, err) } for path, item := range doc.Paths.Map() { diff --git a/packages/sdk-go/scripts/websocket_coverage_test.go b/packages/sdk-go/scripts/websocket_coverage_test.go index 11260350..565713de 100644 --- a/packages/sdk-go/scripts/websocket_coverage_test.go +++ b/packages/sdk-go/scripts/websocket_coverage_test.go @@ -54,10 +54,10 @@ var sdkWebSocketCoverage = map[string]websocketCoverage{ } func TestAsyncAPIWebSocketCoverageManifest(t *testing.T) { - specURL := websocketSpecURL - raw, err := loadPublishedSpec(specURL) + specID := websocketSpecID + raw, err := loadVendoredSpec(specID) if err != nil { - t.Fatalf("reading websocket spec %s: %v", specURL, err) + t.Fatalf("reading websocket spec %s: %v", specID, err) } var root struct { XGeminiCoverage struct { diff --git a/packages/sdk-go/transport/errors.go b/packages/sdk-go/transport/errors.go index 68585e29..ec04acfd 100644 --- a/packages/sdk-go/transport/errors.go +++ b/packages/sdk-go/transport/errors.go @@ -24,7 +24,10 @@ const ( ReasonOrderNotFound = "OrderNotFound" ReasonNoSuchOrder = "NoSuchOrder" ReasonSelfCrossPrevented = "SelfCrossPrevented" - ReasonMustAcceptTerms = "MustAcceptTerms" + ReasonMustAcceptTerms = "MustAcceptTerms" + ReasonMustAccepTerms = "MustAccepTerms" + ReasonAcceptTermsRequired = "AcceptTermsRequired" + ReasonTermsNotAccepted = "TermsNotAccepted" ) // ----------------------------------------------------------------------------- @@ -100,18 +103,26 @@ func (e *ResyncRequiredError) Unwrap() error { // APIError represents a structured error response returned by the Gemini REST API. type APIError struct { - StatusCode int - Result string `json:"result,omitempty"` - Reason string `json:"reason,omitempty"` - Message string `json:"message,omitempty"` - RequestID string `json:"-"` - RawBody []byte `json:"-"` - Header http.Header `json:"-"` + StatusCode int + Result string `json:"result,omitempty"` + Reason string `json:"reason,omitempty"` + ErrorMessage string `json:"error,omitempty"` + Message string `json:"message,omitempty"` + RequestID string `json:"-"` + RawBody []byte `json:"-"` + Header http.Header `json:"-"` +} + +func (e *APIError) reason() string { + if e.Reason != "" { + return e.Reason + } + return e.ErrorMessage } // IsDomain reports whether the APIError represents an exchange business logic/domain error. func (e *APIError) IsDomain() bool { - switch e.Reason { + switch e.reason() { case "InvalidNonce", "GenericNonceError", "MissingNonce", "InvalidSignature", "RateLimit", "UsageLimit", @@ -120,7 +131,7 @@ func (e *APIError) IsDomain() bool { "MarketClosed", "TradingClosed", "OrderNotFound", "NoSuchOrder", "SelfCrossPrevented", - "MustAcceptTerms": + ReasonMustAcceptTerms, ReasonMustAccepTerms, ReasonAcceptTermsRequired, ReasonTermsNotAccepted: return true default: return false @@ -128,13 +139,14 @@ func (e *APIError) IsDomain() bool { } func (e *APIError) Error() string { + reason := e.reason() msg := "" - if e.Reason != "" && e.Message != "" { - msg = fmt.Sprintf("gemini api error (status %d): %s - %s", e.StatusCode, e.Reason, e.Message) + if reason != "" && e.Message != "" { + msg = fmt.Sprintf("gemini api error (status %d): %s - %s", e.StatusCode, reason, e.Message) } else if e.Message != "" { msg = fmt.Sprintf("gemini api error (status %d): %s", e.StatusCode, e.Message) - } else if e.Reason != "" { - msg = fmt.Sprintf("gemini api error (status %d): %s", e.StatusCode, e.Reason) + } else if reason != "" { + msg = fmt.Sprintf("gemini api error (status %d): %s", e.StatusCode, reason) } else { msg = fmt.Sprintf("gemini api error (status %d): unstructured response body", e.StatusCode) } @@ -146,7 +158,7 @@ func (e *APIError) Error() string { } func (e *APIError) Unwrap() error { - switch e.Reason { + switch e.reason() { case "MissingNonce": return ErrMissingNonce case "MissingRole": @@ -165,7 +177,7 @@ func (e *APIError) Unwrap() error { return ErrOrderNotFound case "SelfCrossPrevented": return ErrSelfCrossPrevented - case "MustAcceptTerms": + case ReasonMustAcceptTerms, ReasonMustAccepTerms, ReasonAcceptTermsRequired, ReasonTermsNotAccepted: return ErrAcceptTermsRequired default: switch e.StatusCode { @@ -193,10 +205,14 @@ func (e *APIError) Unwrap() error { // Is implements error matching for APIError. func (e *APIError) Is(target error) bool { - if target == ErrRateLimited && (e.StatusCode == http.StatusTooManyRequests || e.Reason == "RateLimit" || e.Reason == "UsageLimit") { + reason := e.reason() + if target == ErrRateLimited && (e.StatusCode == http.StatusTooManyRequests || reason == "RateLimit" || reason == "UsageLimit") { + return true + } + if target == ErrAcceptTermsRequired && (reason == ReasonMustAcceptTerms || reason == ReasonMustAccepTerms || reason == ReasonAcceptTermsRequired || reason == ReasonTermsNotAccepted) { return true } - if target == ErrInvalidNonce && (e.Reason == "InvalidNonce" || e.Reason == "GenericNonceError" || e.Reason == "MissingNonce") { + if target == ErrInvalidNonce && (reason == ReasonInvalidNonce || reason == ReasonGenericNonceError || reason == ReasonMissingNonce) { return true } return false diff --git a/packages/sdk-typescript/scripts/api-surface.snapshot.json b/packages/sdk-typescript/scripts/api-surface.snapshot.json index c39bdd6f..b43fb0ae 100644 --- a/packages/sdk-typescript/scripts/api-surface.snapshot.json +++ b/packages/sdk-typescript/scripts/api-surface.snapshot.json @@ -46,6 +46,11 @@ "name": "AnonymousSchema_117", "declaration": "export declare enum AnonymousSchema_117 { YES = \"YES\", NO = \"NO\" }" }, + { + "kind": "EnumDeclaration", + "name": "AnonymousSchema_132", + "declaration": "export declare enum AnonymousSchema_132 { YES = \"YES\", NO = \"NO\", UNSPECIFIED = \"UNSPECIFIED\" }" + }, { "kind": "EnumDeclaration", "name": "AnonymousSchema_135", @@ -53,18 +58,18 @@ }, { "kind": "EnumDeclaration", - "name": "AnonymousSchema_148", - "declaration": "export declare enum AnonymousSchema_148 { YES = \"YES\", NO = \"NO\" }" + "name": "AnonymousSchema_145", + "declaration": "export declare enum AnonymousSchema_145 { YES = \"YES\", NO = \"NO\" }" }, { "kind": "EnumDeclaration", - "name": "AnonymousSchema_152", - "declaration": "export declare enum AnonymousSchema_153 { RESERVED_CLOSED = \"CLOSED\", ACCEPTED = \"ACCEPTED\", CONFIRMED = \"CONFIRMED\", DECLINED = \"DECLINED\", FINALIZED = \"FINALIZED\", FAILED = \"FAILED\" }" + "name": "AnonymousSchema_150", + "declaration": "export declare enum AnonymousSchema_150 { RESERVED_CLOSED = \"CLOSED\", ACCEPTED = \"ACCEPTED\", CONFIRMED = \"CONFIRMED\", DECLINED = \"DECLINED\", FINALIZED = \"FINALIZED\", FAILED = \"FAILED\" }" }, { "kind": "EnumDeclaration", - "name": "AnonymousSchema_153", - "declaration": "export declare enum AnonymousSchema_153 { RESERVED_CLOSED = \"CLOSED\", ACCEPTED = \"ACCEPTED\", CONFIRMED = \"CONFIRMED\", DECLINED = \"DECLINED\", FINALIZED = \"FINALIZED\", FAILED = \"FAILED\" }" + "name": "AnonymousSchema_152", + "declaration": "export declare enum AnonymousSchema_150 { RESERVED_CLOSED = \"CLOSED\", ACCEPTED = \"ACCEPTED\", CONFIRMED = \"CONFIRMED\", DECLINED = \"DECLINED\", FINALIZED = \"FINALIZED\", FAILED = \"FAILED\" }" }, { "kind": "InterfaceDeclaration", @@ -469,7 +474,7 @@ { "kind": "InterfaceDeclaration", "name": "NamedAmount", - "declaration": "export interface NamedAmount { t: string; v: string; c?: string; }" + "declaration": "export interface NamedAmount { t: string; v: string; c?: string; o?: AnonymousSchema_132; }" }, { "kind": "VariableDeclaration", @@ -639,7 +644,7 @@ { "kind": "InterfaceDeclaration", "name": "PredictionMarketsComponents", - "declaration": "export interface components { schemas: { Error: { /** * @description Error code * @example InvalidInput */ error?: string; /** * @description Human-readable error message * @example orderId is required */ message?: string; }; PredictionMarketsError: { /** * @description Prediction Markets error class * @example InvalidInput */ error: string; /** * @description Request field associated with the error, when available * @example orders */ field?: string; /** * @description Human-readable error detail, when available * @example orders must contain between 1 and 20 entries */ message?: string; }; AuthErrorResponse: { /** @enum {string} */ result: \"error\"; /** * @description Authentication or authorization error class * @example MissingNonce */ reason: string; /** * @description Human-readable authentication or authorization detail * @example Must provide unique monotonic increasing 'nonce' field in payload */ message: string; }; AccountGroupBlockedError: { /** @enum {string} */ error: \"This account is not permitted to trade prediction markets\"; /** @enum {string} */ code: \"ACCOUNT_GROUP_BLOCKED\"; }; TermsNotAcceptedError: { /** @enum {string} */ error: \"TERMS_NOT_ACCEPTED\"; /** @enum {string} */ message: \"Prediction markets terms must be accepted before placing orders\"; }; RestrictedSellOnlyError: { /** @enum {string} */ error: \"ACCOUNT_RESTRICTED_SELL_ONLY\"; /** @enum {string} */ message: \"Your account is restricted to selling existing positions; buying is not permitted.\"; }; PredictionMarketsTerms: { /** * @description Terms type identifier * @example PredictionsMarket */ termsType: string; /** * @description Latest terms version * @example 3 */ version: number; /** * @description Terms content to display before acceptance * @example These are the prediction market terms. */ content: string; /** * Format: date-time * @description UTC timestamp when the terms content was last updated * @example 2026-05-18T17:00:00Z */ updatedAt: string; }; PredictionMarketsTermsStatus: { /** * @description Whether the account group has accepted the latest configured Prediction Markets terms * @example false */ hasAcceptedLatest: boolean; /** * @description Latest terms version accepted by the account group, if any * @example 2 */ acceptedVersion?: number | null; /** * @description Latest configured Prediction Markets terms version, if available * @example 3 */ latestVersion?: number | null; }; AcceptPredictionMarketsTermsResponse: { /** @example true */ success: boolean; }; /** * @description Status of a prediction market * @enum {string} */ MarketStatus: \"approved\" | \"active\" | \"closed\" | \"under_review\" | \"settled\" | \"invalid\"; /** * @description Type of prediction market * @enum {string} */ MarketType: \"binary\" | \"categorical\"; /** * @description Sport whose rules give the market's scope and metric their sport-specific meaning. * @enum {string} */ SportsMarketSport: \"american_football\" | \"athletics\" | \"australian_rules_football\" | \"baseball\" | \"basketball\" | \"boxing\" | \"chess\" | \"cricket\" | \"cycling\" | \"darts\" | \"esports\" | \"golf\" | \"hockey\" | \"lacrosse\" | \"mixed_martial_arts\" | \"motorsports\" | \"rugby\" | \"sailing\" | \"soccer\" | \"tennis\"; /** * @description Conventional sports-market family. `subject`, `scope`, and `metric` provide detail within the family. This classification is independent of the event's structural `type` (`binary` or `categorical`). * @enum {string} */ SportsMarketType: \"moneyline\" | \"spread\" | \"total\" | \"prop\" | \"correct_score\" | \"to_advance\" | \"futures\" | \"other\"; /** * @description What the market is about. `participant` covers non-player entrants such as drivers and horses. * @enum {string} */ SportsMarketSubject: \"contest\" | \"team\" | \"player\" | \"participant\" | \"other\"; /** * @description Unit covered by the market. `full_contest` follows the market's official final-result rules; `regulation` covers scheduled regulation play only. Ordinal and range qualifiers are represented separately on `SportsMarketScope`. * @enum {string} */ SportsMarketScopeType: \"full_contest\" | \"regulation\" | \"half\" | \"quarter\" | \"period\" | \"inning\" | \"team_innings\" | \"over\" | \"powerplay\" | \"set\" | \"game\" | \"round\" | \"hole\" | \"match_day\" | \"session\" | \"super_over\" | \"race\" | \"sprint\" | \"qualifying\" | \"practice\" | \"stage\" | \"lap\" | \"series\" | \"season\" | \"tournament\" | \"competition\" | \"other\"; /** * @description Statistic measured by the market. Interpret shared metric names using `sportsMarket.sport`. * @enum {string} */ SportsMarketMetric: \"aces\" | \"assists\" | \"balls_faced\" | \"birdies\" | \"blocked_shots\" | \"blocks\" | \"bogeys\" | \"boundaries\" | \"break_points_won\" | \"cards\" | \"catches\" | \"clean_sheets\" | \"completed_passes\" | \"control_time\" | \"corners\" | \"defensive_rebounds\" | \"double_double\" | \"double_faults\" | \"doubles\" | \"eagles\" | \"earned_runs\" | \"errors\" | \"faceoff_wins\" | \"fairways_hit\" | \"fantasy_points\" | \"fastest_lap\" | \"field_goals_made\" | \"finishing_position\" | \"fouls\" | \"fours\" | \"free_throws_made\" | \"fumbles\" | \"games\" | \"goals\" | \"goals_allowed\" | \"greens_in_regulation\" | \"grid_position\" | \"hits\" | \"hits_allowed\" | \"hits_runs_rbis\" | \"holes_in_one\" | \"home_runs\" | \"innings_pitched\" | \"interceptions_thrown\" | \"kicking_points\" | \"knockdowns\" | \"laps_completed\" | \"laps_led\" | \"lap_time\" | \"longest_pass_completion\" | \"longest_reception\" | \"longest_rush\" | \"maiden_overs\" | \"offensive_rebounds\" | \"offsides\" | \"pars\" | \"passes\" | \"pass_attempts\" | \"pass_completions\" | \"passing_touchdowns\" | \"passing_yards\" | \"penalty_minutes\" | \"pitching_outs_recorded\" | \"pit_stops\" | \"points\" | \"points_assists\" | \"points_rebounds\" | \"points_rebounds_assists\" | \"positions_gained\" | \"power_play_points\" | \"putts\" | \"qualifying_position\" | \"rebounds\" | \"rebounds_assists\" | \"receiving_touchdowns\" | \"receiving_yards\" | \"receptions\" | \"red_cards\" | \"retirements\" | \"rounds\" | \"runs\" | \"runs_batted_in\" | \"runs_conceded\" | \"rush_attempts\" | \"rushing_touchdowns\" | \"rushing_yards\" | \"sacks\" | \"safety_cars\" | \"saves\" | \"sets\" | \"shots\" | \"shots_on_goal\" | \"shots_on_target\" | \"shutouts\" | \"significant_strikes\" | \"singles\" | \"sixes\" | \"steals\" | \"stolen_bases\" | \"strokes\" | \"strikeouts\" | \"submission_attempts\" | \"tackles\" | \"takedowns\" | \"three_pointers_made\" | \"tiebreaks_won\" | \"total_bases\" | \"total_points_won\" | \"total_strikes\" | \"touchdowns\" | \"triples\" | \"triple_double\" | \"turnovers\" | \"walks\" | \"wickets\" | \"wins\" | \"yellow_cards\" | \"other\"; /** @description Settlement scope. `ordinal` identifies one unit; `start` and `end` identify an inclusive range of units. */ SportsMarketScope: { type: components[\"schemas\"][\"SportsMarketScopeType\"]; /** * Format: int32 * @description Optional ordinal within the scope type, such as half `1` or quarter `4`. */ ordinal?: number; /** * Format: int32 * @description Optional inclusive start of a scope range, such as inning `1`. */ start?: number; /** * Format: int32 * @description Optional inclusive end of a scope range, such as inning `5`. */ end?: number; }; /** @description Atomic sports-market classification shared by every contract grouped under the event. Present only for sports events. All fields except `metric` are required together. */ SportsMarket: { sport: components[\"schemas\"][\"SportsMarketSport\"]; type: components[\"schemas\"][\"SportsMarketType\"]; subject: components[\"schemas\"][\"SportsMarketSubject\"]; scope: components[\"schemas\"][\"SportsMarketScope\"]; metric?: components[\"schemas\"][\"SportsMarketMetric\"]; }; /** * @description Order type. `stop-limit` orders require a `stopPrice` that triggers a limit order at `price` when the market reaches the trigger. * @enum {string} */ OrderType: \"limit\" | \"stop-limit\"; /** @enum {string} */ OrderSide: \"buy\" | \"sell\"; /** * @description The outcome being traded (Yes or No) * @enum {string} */ Outcome: \"yes\" | \"no\"; /** * @description Order execution behavior: * - `good-til-cancel` - Order remains active until filled or cancelled (default) * - `immediate-or-cancel` - Fill immediately or cancel remaining * - `fill-or-kill` - Fill entire order immediately or cancel * @default good-til-cancel * @enum {string} */ TimeInForce: \"good-til-cancel\" | \"immediate-or-cancel\" | \"fill-or-kill\"; /** @enum {string} */ OrderStatus: \"open\" | \"filled\" | \"cancelled\"; /** @enum {string} */ PositionStatus: \"active\" | \"resolved\" | \"cancelled\"; Pagination: { /** @example 50 */ limit?: number; /** @example 0 */ offset?: number; /** @example 100 */ total?: number; }; PaginationSimple: { limit?: number; offset?: number; /** @description Number of items in current response */ count?: number; }; OrderBook: { bids?: components[\"schemas\"][\"OrderBookEntry\"][]; asks?: components[\"schemas\"][\"OrderBookEntry\"][]; }; OrderBookEntry: { side?: components[\"schemas\"][\"OrderSide\"]; /** @example 0.65 */ price?: string; /** @example 1000 */ quantity?: string; }; OrderBookDepth: { bids?: components[\"schemas\"][\"OrderBookLevel\"][]; asks?: components[\"schemas\"][\"OrderBookLevel\"][]; /** Format: date-time */ lastUpdateTime?: string; }; OrderBookLevel: { price?: string; quantity?: string; orderCount?: number; }; /** @description Contract quantity and price validation is instrument-specific. Clients must validate order quantities and prices against the returned increment and minimum fields rather than assuming a fixed grid. */ Contract: { id?: string; /** @description Human-readable label for the contract's YES-space proposition (e.g., \"SOL > $90\") */ label?: string; /** @description Short form label (e.g., \">$90\") */ abbreviatedName?: string | null; /** @description Rich text description */ description?: Record; prices?: components[\"schemas\"][\"ContractPrices\"]; totalShares?: string | null; color?: string | null; status?: components[\"schemas\"][\"MarketStatus\"]; imageUrl?: string | null; priceHistory?: components[\"schemas\"][\"PricePoint\"][] | null; /** Format: date-time */ createdAt?: string; /** Format: date-time */ expiryDate?: string | null; resolutionSide?: components[\"schemas\"][\"Outcome\"]; /** Format: date-time */ resolvedAt?: string | null; termsAndConditionsUrl?: string; ticker?: string; instrumentSymbol?: string; /** @description Contract quantity grid from instrument refdata (for example, \"0.01\"). */ quantityIncrement?: string | null; /** @description Minimum contract quantity from instrument refdata (for example, \"1.00\"). */ quantityMinimum?: string | null; /** @description Contract price grid from instrument refdata (for example, \"0.0001\"). */ priceIncrement?: string | null; /** @description Decimal places supported by the instrument's quote asset. */ quoteAssetPrecision?: number | null; /** @description Minimum contract price and anchor for the instrument price grid (for example, \"0.0001\"). */ priceMinimum?: string | null; /** Format: date-time */ effectiveDate?: string | null; /** * @description Trading state of the contract * @enum {string|null} */ marketState?: \"open\" | \"closed\" | null; /** @description Display order within the event */ sortOrder?: number | null; strike?: components[\"schemas\"][\"Strike\"]; /** * @deprecated * @description Deprecated: use the event-level `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation (e.g., \"GRR-KAIKO_BTCUSD_60S\"). Present for crypto Up/Down contracts. * @example GRR-KAIKO_BTCUSD_60S */ source?: string | null; /** * @description The observed settlement price. Only present after the contract is settled. * @example 87654.32 */ settlementValue?: string | null; }; /** * @description Strike or condition inequality type for contract threshold evaluation. - `reference`: Crypto Up/Down reference strike price captured at `availableAt` time. - `above`: Higher/Lower contract threshold. - `spread`: Point, run, or goal handicap spread line. - `over`: Total or prop threshold evaluated as strict greater than (`>`). - `over_or_equal`: Total or prop threshold evaluated as greater than or equal to (`>=`). - `under`: Total or prop threshold evaluated as strict less than (`<`). - `under_or_equal`: Position, rank, or total threshold evaluated as less than or equal to (`<=`). * @example spread * @enum {string} */ StrikeType: \"reference\" | \"above\" | \"spread\" | \"over\" | \"over_or_equal\" | \"under\" | \"under_or_equal\"; /** @description Strike price or contract threshold information for Up/Down crypto contracts and sports prediction market contracts. */ Strike: { /** * @description The strike price value. Null for \"reference\" type strikes where the value is determined at availableAt time. For sports contracts, this represents the derived numeric strike value (e.g. spread margin, total line, or position/rank threshold). * @example 87500.00 */ value?: string | null; type?: components[\"schemas\"][\"StrikeType\"]; /** * Format: date-time * @description When the strike price becomes available * @example 2026-03-27T19:45:00.000Z */ availableAt?: string | null; }; /** @description Structured data source information for price observation. Replaces the deprecated flat `source` string on the event and contract. Present for crypto Up/Down events. Both fields are omitted when not available. */ SourceDetails: { /** * @description The data provider / vendor name. * @example Kaiko */ agency?: string | null; /** * @description The specific data feed identifier (the value previously carried by the flat `source` field). * @example GRR-KAIKO_BTCUSD_60S */ index?: string | null; } | null; PricePoint: { /** Format: date-time */ timestamp?: string; price?: string; }; /** @description Current bid/ask pricing for the contract */ ContractPrices: { /** @description Buy prices for each outcome */ buy?: { /** * @description Price to buy YES outcome * @example 0.42 */ yes?: string; /** * @description Price to buy NO outcome * @example 0.58 */ no?: string; }; /** @description Sell prices for each outcome */ sell?: { /** * @description Price to sell YES outcome * @example 0.42 */ yes?: string; /** * @description Price to sell NO outcome * @example 0.58 */ no?: string; }; /** * @description Highest buy offer * @example 0.49 */ bestBid?: string | null; /** * @description Lowest sell offer * @example 0.54 */ bestAsk?: string | null; /** * @description Most recent transaction price * @example 0.75 */ lastTradePrice?: string | null; } | null; /** @description A prediction market event containing one or more tradeable contracts */ Event: { id?: string; /** @example Will Bitcoin reach $100k by end of 2028? */ title?: string; /** @example bitcoin-100k-2028 */ slug?: string; description?: string | null; imageUrl?: string | null; type?: components[\"schemas\"][\"MarketType\"]; /** @example crypto */ category?: string; series?: string | null; sportsMarket?: components[\"schemas\"][\"SportsMarket\"]; /** * @description The event ticker (e.g., \"BTC100K2028\") * @example BTC100K2028 */ ticker?: string; status?: components[\"schemas\"][\"MarketStatus\"]; /** Format: date-time */ resolvedAt?: string | null; /** Format: date-time */ createdAt?: string; /** @description Tradeable contracts within this event */ contracts?: components[\"schemas\"][\"Contract\"][]; contractOrderbooks?: { [key: string]: components[\"schemas\"][\"OrderBook\"]; }; /** * @description Total trading volume in USD * @example 125000.00 */ volume?: string; /** * @description Total liquidity in USD * @example 50000.00 */ liquidity?: string; tags?: string[] | null; /** Format: date-time */ effectiveDate?: string; /** Format: date-time */ expiryDate?: string | null; subcategory?: components[\"schemas\"][\"Subcategory\"]; /** * @deprecated * @description Deprecated: use `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation. Aggregated from contracts for crypto Up/Down events. * @example GRR-KAIKO_BTCUSD_60S */ source?: string | null; sourceDetails?: components[\"schemas\"][\"SourceDetails\"]; settlement?: components[\"schemas\"][\"Settlement\"]; }; /** @description Nested category information for the event */ Subcategory: { /** * @description Category identifier * @example 35 */ id?: number; /** * @description URL-friendly category identifier * @example crypto_solana */ slug?: string; /** * @description Display name * @example Solana */ name?: string; /** * @description Category hierarchy path * @example [ * \"Crypto\", * \"Solana\" * ] */ path?: string[]; } | null; /** @description Settlement information for resolved events */ Settlement: { /** * @description The observed settlement value (e.g., the price at expiry for crypto contracts) * @example 87654.32 */ value?: string | null; }; EventsResponse: { data?: components[\"schemas\"][\"Event\"][]; pagination?: components[\"schemas\"][\"Pagination\"]; }; ContractMetadata: { contractId?: string; contractName?: string; contractTicker?: string; eventTicker?: string; eventName?: string; category?: string; contractStatus?: string; /** @description Event type (\"binary\" or \"categorical\") */ eventType?: string; /** Format: date-time */ expiryDate?: string | null; /** Format: date-time */ resolvedAt?: string | null; /** @description Winning outcome if resolved (\"yes\" or \"no\") */ resolutionSide?: string | null; /** @description Parent event ticker for sub-events */ parentEventTicker?: string | null; /** * Format: date-time * @description Start datetime (ISO 8601) */ startTime?: string | null; }; ComboLeg: { /** * Format: int64 * @description Internal ID of the parent combo contract * @example 456 */ comboId: bigint; /** * @description Zero-based position of this leg in the combo * @example 0 */ legIndex: number; /** * @description Internal ID of the underlying single contract, represented as a decimal string * @example 101 */ contractId: string; /** * @description The outcome this leg must settle for the combo to settle YES * @example Yes * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; /** * @description The outcome this leg has settled to, if resolved (`\"Yes\"` or `\"No\"`). Null while the leg is still active. * @example null */ legOutcome?: string | null; /** * Format: date-time * @description UTC timestamp when this leg resolved. Null while still active. * @example null */ resolvedAt?: string | null; /** @description Full metadata for the underlying single contract */ contract?: components[\"schemas\"][\"ContractMetadata\"] | null; }; ComboResponse: { /** @description Metadata for the combo contract itself (ticker, status, expiry, etc.) */ contract: components[\"schemas\"][\"ContractMetadata\"]; /** @description Ordered list of legs that make up this combo */ legs: components[\"schemas\"][\"ComboLeg\"][]; }; ListCombosResponse: { /** @description List of combo contracts matching the query */ combos: components[\"schemas\"][\"ComboResponse\"][]; pagination: components[\"schemas\"][\"Pagination\"]; }; /** @description A canonical combo definition. The authenticated account is derived from the signed request and is not a request field. */ CreateComboRequest: { /** @description Two to six distinct underlying contract legs. The service canonicalizes their complete set, so leg order does not create a distinct combo. */ legs: components[\"schemas\"][\"CreateComboLeg\"][]; }; CreateComboLeg: { /** * @description Underlying contract ID as a decimal string. * @example 101 */ contractId: string; /** * @description Required settlement outcome for this leg. * @example Yes * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; }; CreateComboResponse: { combo: components[\"schemas\"][\"ComboSummary\"]; /** @description `false` when this request created the canonical combo; `true` when the canonical combo already existed. */ alreadyExisted: boolean; }; ComboSummary: { /** * Format: int64 * @description Internal combo ID. * @example 456 */ id: bigint; /** * @description Canonical identity of the complete combo leg set. * @example 101:Yes|202:No */ canonicalLegKey: string; /** * Format: int32 * @description Number of legs in the combo. * @example 2 */ legCount: number; /** @description Human-readable combo name, when available. */ displayName?: string; /** @description Current combo status, when available. */ status?: string; /** * Format: int64 * @description Associated instrument ID, when available. */ instrumentId?: bigint; /** * @description Associated instrument symbol, when available. * @example GEMI-CMB-0526-A7F3B2C1D4E5 */ instrumentSymbol?: string; /** @description Whether the combo has been registered with an instrument symbol. */ instrumentRegistered: boolean; /** * Format: date-time * @description Latest expiry among the underlying legs, when available. */ latestExpiryDate?: string; /** * Format: date-time * @description Creation time, when available. */ createdAt?: string; /** * Format: date-time * @description Most recent update time, when available. */ updatedAt?: string; /** @description Canonically ordered combo legs. */ legs: components[\"schemas\"][\"ComboSummaryLeg\"][]; }; ComboSummaryLeg: { /** * Format: int64 * @description Parent combo ID. */ comboId: bigint; /** * Format: int32 * @description Zero-based leg position in canonical order. */ legIndex: number; /** @description Underlying contract ID as a decimal string. */ contractId: string; /** * @description Required settlement outcome for the leg. * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; /** * @description Settled outcome for the leg, when resolved. * @enum {string|null} */ legOutcome?: \"Yes\" | \"No\" | null; /** * Format: date-time * @description Resolution time for the leg, when resolved. */ resolvedAt?: string | null; /** @description Underlying contract metadata, when available. */ contract?: components[\"schemas\"][\"ContractMetadata\"]; }; ComboWriteError: { /** * @description Error class. * @example InvalidInput */ error: string; /** * @description Machine-readable code for validation or missing-leg errors, when available. * @example COMBO_VALIDATION_ERROR */ code?: string; /** * @description Human-readable error detail. * @example a combo needs 2-6 legs */ message: string; }; OrderRequest: { /** * @description Contract instrument symbol * @example GEMI-FEDJAN26-DN25 */ symbol: string; orderType: components[\"schemas\"][\"OrderType\"]; side: components[\"schemas\"][\"OrderSide\"]; /** * Format: decimal * @description Number of contracts * @example 100 */ quantity: string; /** * Format: decimal * @description Limit price (0-1 range) * @example 0.65 */ price: string; /** * Format: decimal * @description The price to trigger a stop-limit order (0-1 range). Only available for stop-limit orders. See [Stop-Limit Orders](#operation/placeOrder) above for `stopPrice`/`price` constraints. * @example 0.60 */ stopPrice?: string; outcome: components[\"schemas\"][\"Outcome\"]; timeInForce?: components[\"schemas\"][\"TimeInForce\"]; /** * @description Set to `true` to require maker-only behavior. If the order would immediately take liquidity, the order is cancelled instead of filling. * @default false */ makerOrCancel: boolean; }; PlaceOrderBatchRequest: { /** @description Orders to submit. Every entry is validated before any order is submitted. All orders use the account associated with the authenticated request. */ orders: components[\"schemas\"][\"OrderRequest\"][]; }; /** @description An accepted order returned for one batch entry. */ BatchOrderResponse: { /** * Format: int64 * @example 12345678 */ orderId: bigint; /** @description Hashed order ID; omitted when unavailable */ hashOrderId?: string; /** @description Client-provided order ID; omitted when unavailable */ clientOrderId?: string; /** @description Global order ID; omitted when unavailable */ globalOrderId?: string; /** @enum {string} */ status: \"open\" | \"filled\" | \"cancelled\" | \"closed\"; symbol: string; side: components[\"schemas\"][\"OrderSide\"]; outcome: components[\"schemas\"][\"Outcome\"]; orderType: components[\"schemas\"][\"OrderType\"]; /** @enum {string} */ timeInForce: \"good-til-cancel\" | \"immediate-or-cancel\" | \"fill-or-kill\" | \"maker-or-cancel\"; /** @description Original order quantity */ quantity: string; /** @description Amount filled so far */ filledQuantity: string; /** @description Amount remaining to fill */ remainingQuantity: string; /** @description Limit price */ price: string; /** @description Stop trigger price; omitted unless populated for a `stop-limit` order */ stopPrice?: string; /** @description Average price of fills; omitted when unavailable */ avgExecutionPrice?: string; /** Format: date-time */ createdAt: string; /** Format: date-time */ updatedAt: string; /** * Format: date-time * @description Cancellation time; omitted unless the order was cancelled */ cancelledAt?: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; /** @description Promotional cash reserved or applied to the order; omitted when unavailable */ promoCashApplied?: string; /** @description Cash reserved for the unfilled portion of a resting buy order; omitted when unavailable */ fundsOnHold?: string; }; PlaceOrderBatchSuccessResult: { order: components[\"schemas\"][\"BatchOrderResponse\"]; }; PlaceOrderBatchErrorResult: { /** * @description Error class for a rejected entry * @example InsufficientFunds */ error: string; /** * @description Human-readable detail for a rejected entry * @example Insufficient funds */ message: string; }; /** @description Exactly one outcome is present. Accepted entries contain `order`; rejected entries contain `error` and `message`. */ PlaceOrderBatchResult: components[\"schemas\"][\"PlaceOrderBatchSuccessResult\"] | components[\"schemas\"][\"PlaceOrderBatchErrorResult\"]; PlaceOrderBatchResponse: { /** @description One result for each submitted order, in request order. */ results: components[\"schemas\"][\"PlaceOrderBatchResult\"][]; }; CancelOrderBatchRequest: { /** @description Order IDs to cancel. Each ID may be an integer or a quoted numeric string. All IDs are validated before any cancellation is attempted. */ orderIds: (bigint | string)[]; }; CancelOrderBatchSuccessResult: { /** * Format: int64 * @description Order ID from the corresponding request entry. * @example 12345678 */ orderId: bigint; /** @enum {string} */ result: \"ok\"; }; CancelOrderBatchErrorResult: { /** * Format: int64 * @description Order ID from the corresponding request entry. * @example 12345678 */ orderId: bigint; /** * @description Error class for a rejected cancellation * @example OrderNotFound */ error: string; /** * @description Human-readable detail for a rejected cancellation * @example Order 12345678 not found */ message: string; }; /** @description Exactly one outcome is present. Successful entries contain `orderId` and `result`; rejected entries contain `orderId`, `error`, and `message`. */ CancelOrderBatchResult: components[\"schemas\"][\"CancelOrderBatchSuccessResult\"] | components[\"schemas\"][\"CancelOrderBatchErrorResult\"]; CancelOrderBatchResponse: { /** @description One result for each requested cancellation, in request order. */ results: components[\"schemas\"][\"CancelOrderBatchResult\"][]; }; OrderResponse: { /** * Format: int64 * @example 12345678 */ orderId?: bigint; hashOrderId?: string | null; clientOrderId?: string | null; globalOrderId?: string | null; status?: components[\"schemas\"][\"OrderStatus\"]; symbol?: string; side?: components[\"schemas\"][\"OrderSide\"]; outcome?: components[\"schemas\"][\"Outcome\"]; orderType?: components[\"schemas\"][\"OrderType\"]; /** @description Original order quantity */ quantity?: string; /** @description Amount filled so far */ filledQuantity?: string; /** @description Amount remaining to fill */ remainingQuantity?: string; /** @description Limit price */ price?: string; /** @description Stop trigger price (populated for `stop-limit` orders) */ stopPrice?: string | null; /** @description Average price of fills */ avgExecutionPrice?: string | null; /** Format: date-time */ createdAt?: string; /** Format: date-time */ updatedAt?: string; /** Format: date-time */ cancelledAt?: string | null; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; }; OrdersResponse: { orders?: components[\"schemas\"][\"OrderResponse\"][]; pagination?: components[\"schemas\"][\"PaginationSimple\"]; }; Position: { symbol?: string; /** Format: int64 */ instrumentId?: bigint; /** @description Total position size */ totalQuantity?: string; /** @description Quantity currently on hold from open orders */ quantityOnHold?: string; /** @description Average entry price */ avgPrice?: string; outcome?: components[\"schemas\"][\"Outcome\"]; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; prices?: components[\"schemas\"][\"PositionPrices\"]; /** @description Winning outcome (\"yes\" or \"no\") if the contract has resolved */ resolutionSide?: string | null; /** @description Whether the position is above the auto-start threshold */ isAboveAutoStartThreshold?: boolean; /** @description Whether the market is currently live/active */ isLive?: boolean; /** @description Realized profit/loss from sells */ realizedPl?: string | null; /** * @description Mark-to-market value of the position in USD at the current sell price (bestBid for YES, bestAsk for NO). **Absent** from the response when the held outcome has no live sell quote (no liquidity to sell into) — surface a no-liquidity state rather than a price the user cannot transact at. `lastTradePrice` is still returned for display. Treat as `Optional`. * @example 65.00 */ marketValue?: string; /** * @description Unrealized P&L in USD (`marketValue - costBasis`). **Absent** whenever `marketValue` is absent. Treat as `Optional`. * @example 12.50 */ unrealizedPnl?: string; /** * Format: double * @description Unrealized P&L as a percentage of cost basis. Expressed as a percent (e.g. `12.5` represents 12.5%, **not** `0.125`); rounded to 4 decimal places. **Absent** when there is no live sell quote, or when cost basis is zero. Treat as `Optional`. * @example 23.81 */ unrealizedPct?: number; }; /** @description Current bid/ask/last-trade prices for the contract */ PositionPrices: { buy: { yes?: string | null; no?: string | null; }; sell: { yes?: string | null; no?: string | null; }; bestBid?: string | null; bestAsk?: string | null; lastTradePrice?: string | null; } | null; PositionsResponse: { positions?: components[\"schemas\"][\"Position\"][]; /** @description Total number of positions (for pagination) */ total?: number | null; }; /** @description A historically settled position in a resolved prediction market contract. */ SettledPosition: { /** * Format: int64 * @description Account that held the position */ accountId?: bigint; /** * Format: int64 * @description Unique instrument identifier for the contract */ instrumentId?: bigint; /** * @description Contract instrument symbol * @example GEMI-FEDJAN26-DN25 */ instrumentSymbol?: string; /** * @description Signed position held at settlement. Positive values represent a `yes` position; negative values represent a `no` position. * @example 125 */ position?: string; /** * @description Absolute quantity held at settlement (unsigned) * @example 125 */ positionQuantity?: string; outcome?: components[\"schemas\"][\"Outcome\"]; /** * @description Payout received from settlement. `0` when the position lost. * @example 125.00 */ payout?: string; /** @description The winning outcome of the contract */ resolutionSide?: components[\"schemas\"][\"Outcome\"]; /** * Format: date-time * @description Settlement timestamp (ISO 8601) */ settledAt?: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; /** * @description Total amount spent to enter the position, net of any prior realized P&L from partial sells. Omitted when cost-basis data is not available. * @example 78.75 */ costBasis?: string | null; /** * @description Realized profit or loss recorded from sells prior to settlement. Omitted when not available. * @example 0 */ realizedPnl?: string | null; /** * @description Net profit for the position, computed as `payout - costBasis + realizedPnl`. Omitted when `costBasis` is not available. * @example 46.25 */ netProfit?: string | null; }; SettledPositionsResponse: { positions?: components[\"schemas\"][\"SettledPosition\"][]; /** @description Total number of settled positions across all pages for the current filter set. */ total?: number | null; /** @description Sum of `payout` across all settled positions in the filter set. Retained for binary back-compat with the legacy response shape; **field is absent (not `null`) on the unified backend** because computing a roll-up over the full filtered set would require a separate aggregate query (deferred until a partner asks). Play's default `OptionHandlers` omits absent `Option` fields rather than emitting `null`. */ totalPayout?: string; /** @description Sum of `costBasis` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ totalCostBasis?: string; /** @description Sum of `netProfit` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ totalNetProfit?: string; /** @description Cash-outs (early sells before contract resolution) in the same account-scoped time window as the returned page's settled positions. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. `positions[]` pagination is unaffected — `limit`/`offset` continue to scope `positions[]` only. */ cashOuts?: components[\"schemas\"][\"CashedOutPosition\"][]; /** * @description Sum of `cashOuts[].proceeds` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 120.00 */ totalCashOutProceeds?: string; /** * @description Sum of `cashOuts[].costBasis` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 100.00 */ totalCashOutCostBasis?: string; /** * @description Sum of `cashOuts[].netProfit` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 20.00 */ totalCashOutNetProfit?: string; }; /** @description A qualifying cash-out (early sell before contract resolution) with cost-basis context. Exposed only via the `withCashOuts=true` sibling array on `POST /v1/prediction-markets/positions/settled`. Distinct from `SettledPosition` — cash-outs don't have a `payout` or `resolutionSide` since the contract hadn't resolved when the user sold. */ CashedOutPosition: { /** * Format: int64 * @description Account that held the position. * @example 456 */ accountId: bigint; /** * Format: int64 * @description Contract instrument ID. * @example 16789219 */ instrumentId: bigint; /** * @description Contract instrument symbol. * @example GEMI-FEDJAN26-DN25 */ instrumentSymbol: string; /** * Format: date-time * @description Wall-clock timestamp when the cash-out order closed (ISO 8601). * @example 2026-05-15T14:30:00.000Z */ timestamp: string; /** * @description Quantity sold (cumulative filled quantity on the cash-out order). * @example 10 */ filledQuantity: string; /** * @description Always `sell` for cash-outs. * @example sell * @enum {string} */ side: \"sell\"; /** * @description Amount received from the sale in USD. For prediction sells, proceeds flow through `cash_balance` rather than `closed_orders.total_spend`, so the value is derived from position-balance snapshots before/after the fill. * @example 10.50 */ proceeds: string; /** * @description Cost basis allocated proportionally to the filled quantity (`(costBasisSpend / costBasisPositionBalance) * filledQuantity`). * @example 10.00 */ costBasis: string; /** * @description Realized P&L from this cash-out fill (`proceeds - costBasis`). Equals the ledger `realized_pl` delta on the position-balance row pair around the fill; falls back to `0` under transient market-data lag so a missing post-fill snapshot can't poison the page. * @example 0.50 */ netProfit: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; }; ContractShareVolume: { /** * @description Contract instrument symbol * @example GEMI-FED260318-CUT25 */ symbol?: string; /** * @description Total taker volume across all participants (in shares) * @example 94625 */ totalQty?: string; /** * @description The authenticated user's taker (aggressor) volume (in shares) * @example 1 */ userAggressorQty?: string | null; /** * @description The authenticated user's maker (resting) volume (in shares) * @example 0 */ userRestingQty?: string | null; }; VolumeMetricsResponse: { /** * @description The event ticker * @example FED260318 */ eventTicker?: string; contracts?: components[\"schemas\"][\"ContractShareVolume\"][]; }; PredictionMarketVolumeCategory: { /** * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. * @example [ * \"Sports\", * \"Football\", * \"Pro Football\" * ] */ categoryPath: string[]; /** @description Total volume for this category, including all descendant categories. */ volume: components[\"schemas\"][\"PredictionMarketVolumeDecimal\"]; }; PredictionMarketHourlyVolumeCategory: { /** * Format: date-time * @description Inclusive UTC start of this hourly period. * @example 2026-07-20T00:00:00Z */ periodStart: string; /** * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. * @example [ * \"Sports\", * \"Football\", * \"Pro Football\" * ] */ categoryPath: string[]; /** @description Total volume for this category in this hour, including all descendant categories. */ volume: components[\"schemas\"][\"PredictionMarketVolumeDecimal\"]; }; /** * @description Non-negative decimal string. Preserve it as a string to avoid floating-point precision loss. * @example 143567.25 */ PredictionMarketVolumeDecimal: string; MakerRebateRateRule: { /** * Format: int64 * @description Stable identifier for this rate rule. * @example 12 */ id: bigint; /** * Format: int32 * @description Portion of the maker fee that is rebated, in basis points (10000 bps = 100%). * @example 5000 */ rebate_multiplier_bps: number; /** * Format: date-time * @description ISO-8601 timestamp at which this rule becomes effective. Always present; in practice never `null`. * @example 2026-03-19T00:00:00Z */ effective_from: string | null; /** * @description Market category this rule applies to. When absent, the rule applies to all categories. * @example Crypto */ category?: string; /** * Format: date-time * @description ISO-8601 timestamp after which this rule is superseded. Omitted when the rule is still current. * @example 2026-04-19T00:00:00Z */ effective_to?: string; }; MakerRebateRatesResponse: { rate_rules: components[\"schemas\"][\"MakerRebateRateRule\"][]; }; MakerRebatePayout: { /** * Format: int64 * @description Stable payout identifier. * @example 9182 */ id: bigint; /** * @description Total qualifying maker volume contributing to this payout, in USD. * @example 12450.00 */ total_volume_usd: string; /** * @description Total rebate paid, in USD. * @example 6.23 */ total_rebate_usd: string; /** * Format: int32 * @description Number of qualifying maker fills that contributed to the payout. * @example 187 */ total_fill_count: number; /** * @description Payout status (e.g. `PENDING`, `PAID`). * @example PAID */ status: string; /** * Format: date-time * @description ISO-8601 timestamp at which the rebate was credited. Always present; `null` for payouts that have not yet been paid. * @example 2026-05-20T21:00:00Z */ paid_at: string | null; /** * Format: date-time * @description ISO-8601 timestamp at which the payout row was created. Always present. * @example 2026-05-20T20:55:12Z */ created_at: string | null; }; MakerRebatePayoutsResponse: { payouts: components[\"schemas\"][\"MakerRebatePayout\"][]; }; MakerRebateLifetimeSummary: { /** * @description Sum of `total_rebate_usd` across payouts in the window. * @example 152.40 */ total_earned_usd: string; /** * Format: int64 * @description Sum of qualifying maker fills across payouts in the window. * @example 4218 */ total_fill_count: bigint; /** * @description Sum of qualifying maker volume (USD) across payouts in the window. * @example 304800.00 */ total_volume_usd: string; /** * Format: int32 * @description Number of payouts in the window. Always present; `0` when no payouts exist in the window. * @example 27 */ payout_count: number; /** * Format: date * @description Date of the earliest payout in the window, or `null` if no payouts exist. * @example 2026-03-19 */ first_payout_date: string | null; /** * Format: date * @description Date of the most recent payout in the window, or `null` if no payouts exist. * @example 2026-05-20 */ last_payout_date: string | null; }; LiquidityRewardsConfig: { /** * Format: int32 * @description Quotes wider than this spread score zero in the scoring algorithm. Only present when `enabled` is `true`. * @example 10 */ max_spread_cents?: number; /** * @description Daily reward amounts below this threshold are suppressed (sub-threshold accounts get no row at all). Only present when `enabled` is `true`. * @example 1.00 */ min_payout_threshold_usd?: string; /** * @description True when the program is fully configured upstream. When false, the response collapses to `{ \"enabled\": false }` only. * @example true */ enabled: boolean; }; LiquidityRewardEvent: { /** * @description Event ticker (e.g. `BTC2605202100`). * @example BTC2605202100 */ event_ticker: string; /** * @description Event title. * @example BTC above $95,000? */ title: string; /** * @description Market category. * @example Crypto */ category: string; /** * @description Daily USD reward pool budgeted for this event. * @example 500.00 */ daily_pool_usd: string; /** * @description Whether the pool came from a per-event override or the category default. * @example event_override * @enum {string} */ pool_source: \"event_override\" | \"category_default\" | \"unspecified\"; /** * Format: date-time * @description ISO-8601 timestamp at which the event ends and stops scoring. `null` when the underlying event has no end timestamp set. * @example 2026-05-20T21:00:00Z */ ends_at: string | null; /** * Format: int32 * @description Number of accounts that met qualifying-maker criteria in the most recent snapshot window for this event. * @example 14 */ qualifying_maker_count: number; /** * @description Optional URL for the event icon. Omitted when not configured. * @example https://example.com/btc.png */ icon_url?: string; }; LiquidityRewardsEventsResponse: { events: components[\"schemas\"][\"LiquidityRewardEvent\"][]; pagination: components[\"schemas\"][\"Pagination\"]; /** * Format: date * @description Most recent date for which scoring has been written. `null` when no scoring has run yet. * @example 2026-05-19 */ last_score_date: string | null; }; LiquidityEventScore: { /** * Format: int64 * @description Stable event identifier. * @example 1234567890 */ event_id: bigint; /** * @description Event title. * @example BTC above $95,000? */ event_name: string; /** * @description Market category. * @example Crypto */ category_name: string; /** * @description This account's normalized score for the event on the scoring date (0-1 range as a decimal string). * @example 0.4521 */ normalized_score: string; /** * Format: int32 * @description Number of snapshots in which this account had a qualifying quote. * @example 1180 */ snapshot_count: number; /** * Format: int32 * @description Total snapshots taken for the event on the scoring date. * @example 1440 */ total_snapshots: number; /** * @description Portion of the day's total reward attributed to this event. * @example 8.20 */ event_reward_usd: string; }; LiquidityDailySummary: { /** * Format: date * @description Date the payout applies to (Eastern Time). * @example 2026-05-07 */ payout_date: string; /** * @description Total USD reward for the day across all events the account scored on. * @example 12.45 */ total_reward_usd: string; /** * @description Status of the day's payout (e.g. `PENDING`, `PAID`, `ZERO_AMOUNT`). * @example PAID */ payout_status: string; /** * Format: date-time * @description ISO-8601 timestamp the day's payout was credited. Always present; `null` if not yet paid. * @example 2026-05-08T21:00:00Z */ paid_at: string | null; /** @description Per-event score breakdown showing how the day's total was distributed. */ events: components[\"schemas\"][\"LiquidityEventScore\"][]; }; LiquidityRewardsDailySummaryResponse: { daily_summaries: components[\"schemas\"][\"LiquidityDailySummary\"][]; }; LiquidityRewardsLifetimeSummary: { /** * @description Sum of `total_reward_usd` across daily payouts in the window. * @example 84.20 */ total_earned_usd: string; /** * Format: int32 * @description Number of daily payouts in the window. Always present; `0` when no payouts exist in the window. * @example 12 */ payout_count: number; /** * Format: date * @description Date of the earliest payout in the window, or `null` if no payouts exist. * @example 2026-05-08 */ first_payout_date: string | null; /** * Format: date * @description Date of the most recent payout in the window, or `null` if no payouts exist. * @example 2026-05-20 */ last_payout_date: string | null; }; }; responses: { /** @description Invalid request parameters */ BadRequest: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Authentication required or invalid credentials */ Unauthorized: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Internal server error */ InternalError: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Prediction markets feature is temporarily unavailable */ ServiceUnavailable: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; }; parameters: { /** @description Maximum number of results to return (max 500) */ Limit: number; /** @description Number of results to skip for pagination */ Offset: number; /** @description Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. */ SportFilter: components[\"schemas\"][\"SportsMarketSport\"][]; /** @description Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketTypeFilter: components[\"schemas\"][\"SportsMarketType\"][]; /** @description Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketSubjectFilter: components[\"schemas\"][\"SportsMarketSubject\"][]; /** @description Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketScopeFilter: components[\"schemas\"][\"SportsMarketScopeType\"][]; /** @description Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketMetricFilter: components[\"schemas\"][\"SportsMarketMetric\"][]; }; requestBodies: never; headers: never; pathItems: never; }" + "declaration": "export interface components { schemas: { Error: { /** * @description Error code * @example InvalidInput */ error?: string; /** * @description Human-readable error message * @example orderId is required */ message?: string; }; PredictionMarketsError: { /** * @description Prediction Markets error class * @example InvalidInput */ error: string; /** * @description Request field associated with the error, when available * @example orders */ field?: string; /** * @description Human-readable error detail, when available * @example orders must contain between 1 and 20 entries */ message?: string; }; AuthErrorResponse: { /** @enum {string} */ result: \"error\"; /** * @description Authentication or authorization error class * @example MissingNonce */ reason: string; /** * @description Human-readable authentication or authorization detail * @example Must provide unique monotonic increasing 'nonce' field in payload */ message: string; }; AccountGroupBlockedError: { /** @enum {string} */ error: \"This account is not permitted to trade prediction markets\"; /** @enum {string} */ code: \"ACCOUNT_GROUP_BLOCKED\"; }; TermsNotAcceptedError: { /** @enum {string} */ error: \"TERMS_NOT_ACCEPTED\"; /** @enum {string} */ message: \"Prediction markets terms must be accepted before placing orders\"; }; RestrictedSellOnlyError: { /** @enum {string} */ error: \"ACCOUNT_RESTRICTED_SELL_ONLY\"; /** @enum {string} */ message: \"Your account is restricted to selling existing positions; buying is not permitted.\"; }; PredictionMarketsTerms: { /** * @description Terms type identifier * @example PredictionsMarket */ termsType: string; /** * @description Latest terms version * @example 3 */ version: number; /** * @description Terms content to display before acceptance * @example These are the prediction market terms. */ content: string; /** * Format: date-time * @description UTC timestamp when the terms content was last updated * @example 2026-05-18T17:00:00Z */ updatedAt: string; }; PredictionMarketsTermsStatus: { /** * @description Whether the account group has accepted the latest configured Prediction Markets terms * @example false */ hasAcceptedLatest: boolean; /** * @description Latest terms version accepted by the account group, if any * @example 2 */ acceptedVersion?: number | null; /** * @description Latest configured Prediction Markets terms version, if available * @example 3 */ latestVersion?: number | null; }; AcceptPredictionMarketsTermsResponse: { /** @example true */ success: boolean; }; /** * @description Status of a prediction market * @enum {string} */ MarketStatus: \"approved\" | \"active\" | \"closed\" | \"under_review\" | \"settled\" | \"invalid\"; /** * @description Type of prediction market * @enum {string} */ MarketType: \"binary\" | \"categorical\"; /** * @description Sport whose rules give the market's scope and metric their sport-specific meaning. * @enum {string} */ SportsMarketSport: \"american_football\" | \"athletics\" | \"australian_rules_football\" | \"baseball\" | \"basketball\" | \"boxing\" | \"chess\" | \"cricket\" | \"cycling\" | \"darts\" | \"esports\" | \"golf\" | \"hockey\" | \"lacrosse\" | \"mixed_martial_arts\" | \"motorsports\" | \"rugby\" | \"sailing\" | \"soccer\" | \"tennis\"; /** * @description Conventional sports-market family. `subject`, `scope`, and `metric` provide detail within the family. This classification is independent of the event's structural `type` (`binary` or `categorical`). * @enum {string} */ SportsMarketType: \"moneyline\" | \"spread\" | \"total\" | \"prop\" | \"correct_score\" | \"to_advance\" | \"futures\" | \"other\"; /** * @description What the market is about. `participant` covers non-player entrants such as drivers and horses. * @enum {string} */ SportsMarketSubject: \"contest\" | \"team\" | \"player\" | \"participant\" | \"other\"; /** * @description Unit covered by the market. `full_contest` follows the market's official final-result rules; `regulation` covers scheduled regulation play only. Ordinal and range qualifiers are represented separately on `SportsMarketScope`. * @enum {string} */ SportsMarketScopeType: \"full_contest\" | \"regulation\" | \"half\" | \"quarter\" | \"period\" | \"inning\" | \"team_innings\" | \"over\" | \"powerplay\" | \"set\" | \"game\" | \"round\" | \"hole\" | \"match_day\" | \"session\" | \"super_over\" | \"race\" | \"sprint\" | \"qualifying\" | \"practice\" | \"stage\" | \"lap\" | \"series\" | \"season\" | \"tournament\" | \"competition\" | \"other\"; /** * @description Statistic measured by the market. Interpret shared metric names using `sportsMarket.sport`. * @enum {string} */ SportsMarketMetric: \"aces\" | \"assists\" | \"balls_faced\" | \"birdies\" | \"blocked_shots\" | \"blocks\" | \"bogeys\" | \"boundaries\" | \"break_points_won\" | \"cards\" | \"catches\" | \"clean_sheets\" | \"completed_passes\" | \"control_time\" | \"corners\" | \"defensive_rebounds\" | \"double_double\" | \"double_faults\" | \"doubles\" | \"eagles\" | \"earned_runs\" | \"errors\" | \"faceoff_wins\" | \"fairways_hit\" | \"fantasy_points\" | \"fastest_lap\" | \"field_goals_made\" | \"finishing_position\" | \"fouls\" | \"fours\" | \"free_throws_made\" | \"fumbles\" | \"games\" | \"goals\" | \"goals_allowed\" | \"greens_in_regulation\" | \"grid_position\" | \"hits\" | \"hits_allowed\" | \"hits_runs_rbis\" | \"holes_in_one\" | \"home_runs\" | \"innings_pitched\" | \"interceptions_thrown\" | \"kicking_points\" | \"knockdowns\" | \"laps_completed\" | \"laps_led\" | \"lap_time\" | \"longest_pass_completion\" | \"longest_reception\" | \"longest_rush\" | \"maiden_overs\" | \"offensive_rebounds\" | \"offsides\" | \"pars\" | \"passes\" | \"pass_attempts\" | \"pass_completions\" | \"passing_touchdowns\" | \"passing_yards\" | \"penalty_minutes\" | \"pitching_outs_recorded\" | \"pit_stops\" | \"points\" | \"points_assists\" | \"points_rebounds\" | \"points_rebounds_assists\" | \"positions_gained\" | \"power_play_points\" | \"putts\" | \"qualifying_position\" | \"rebounds\" | \"rebounds_assists\" | \"receiving_touchdowns\" | \"receiving_yards\" | \"receptions\" | \"red_cards\" | \"retirements\" | \"rounds\" | \"runs\" | \"runs_batted_in\" | \"runs_conceded\" | \"rush_attempts\" | \"rushing_touchdowns\" | \"rushing_yards\" | \"sacks\" | \"safety_cars\" | \"saves\" | \"sets\" | \"shots\" | \"shots_on_goal\" | \"shots_on_target\" | \"shutouts\" | \"significant_strikes\" | \"singles\" | \"sixes\" | \"steals\" | \"stolen_bases\" | \"strokes\" | \"strikeouts\" | \"submission_attempts\" | \"tackles\" | \"takedowns\" | \"three_pointers_made\" | \"tiebreaks_won\" | \"total_bases\" | \"total_points_won\" | \"total_strikes\" | \"touchdowns\" | \"triples\" | \"triple_double\" | \"turnovers\" | \"walks\" | \"wickets\" | \"wins\" | \"yellow_cards\" | \"other\"; /** @description Settlement scope. `ordinal` identifies one unit; `start` and `end` identify an inclusive range of units. */ SportsMarketScope: { type: components[\"schemas\"][\"SportsMarketScopeType\"]; /** * Format: int32 * @description Optional ordinal within the scope type, such as half `1` or quarter `4`. */ ordinal?: number; /** * Format: int32 * @description Optional inclusive start of a scope range, such as inning `1`. */ start?: number; /** * Format: int32 * @description Optional inclusive end of a scope range, such as inning `5`. */ end?: number; }; /** @description Atomic sports-market classification shared by every contract grouped under the event. Present only for sports events. All fields except `metric` are required together. */ SportsMarket: { sport: components[\"schemas\"][\"SportsMarketSport\"]; type: components[\"schemas\"][\"SportsMarketType\"]; subject: components[\"schemas\"][\"SportsMarketSubject\"]; scope: components[\"schemas\"][\"SportsMarketScope\"]; metric?: components[\"schemas\"][\"SportsMarketMetric\"]; }; /** * @description Order type. `stop-limit` orders require a `stopPrice` that triggers a limit order at `price` when the market reaches the trigger. * @enum {string} */ OrderType: \"limit\" | \"stop-limit\"; /** @enum {string} */ OrderSide: \"buy\" | \"sell\"; /** * @description The outcome being traded (Yes or No) * @enum {string} */ Outcome: \"yes\" | \"no\"; /** * @description Order execution behavior: * - `good-til-cancel` - Order remains active until filled or cancelled (default) * - `immediate-or-cancel` - Fill immediately or cancel remaining * - `fill-or-kill` - Fill entire order immediately or cancel * @default good-til-cancel * @enum {string} */ TimeInForce: \"good-til-cancel\" | \"immediate-or-cancel\" | \"fill-or-kill\"; /** @enum {string} */ OrderStatus: \"open\" | \"filled\" | \"cancelled\"; /** @enum {string} */ PositionStatus: \"active\" | \"resolved\" | \"cancelled\"; Pagination: { /** @example 50 */ limit?: number; /** @example 0 */ offset?: number; /** @example 100 */ total?: number; }; PaginationSimple: { limit?: number; offset?: number; /** @description Number of items in current response */ count?: number; }; OrderBook: { bids?: components[\"schemas\"][\"OrderBookEntry\"][]; asks?: components[\"schemas\"][\"OrderBookEntry\"][]; }; OrderBookEntry: { side?: components[\"schemas\"][\"OrderSide\"]; /** @example 0.65 */ price?: string; /** @example 1000 */ quantity?: string; }; OrderBookDepth: { bids?: components[\"schemas\"][\"OrderBookLevel\"][]; asks?: components[\"schemas\"][\"OrderBookLevel\"][]; /** Format: date-time */ lastUpdateTime?: string; }; OrderBookLevel: { price?: string; quantity?: string; orderCount?: number; }; /** @description Contract quantity and price validation is instrument-specific. Clients must validate order quantities and prices against the returned increment and minimum fields rather than assuming a fixed grid. */ Contract: { totalShares?: string | null; id?: string; /** @description Human-readable label for the contract's YES-space proposition (e.g., \"SOL > $90\") */ label?: string; /** @description Short form label (e.g., \">$90\") */ abbreviatedName?: string | null; /** @description Rich text description */ description?: Record; prices?: components[\"schemas\"][\"ContractPrices\"]; color?: string | null; status?: components[\"schemas\"][\"MarketStatus\"]; imageUrl?: string | null; priceHistory?: components[\"schemas\"][\"PricePoint\"][] | null; /** Format: date-time */ createdAt?: string; /** Format: date-time */ expiryDate?: string | null; resolutionSide?: components[\"schemas\"][\"Outcome\"]; /** Format: date-time */ resolvedAt?: string | null; termsAndConditionsUrl?: string; ticker?: string; instrumentSymbol?: string; /** @description Contract quantity grid from instrument refdata (for example, \"0.01\"). */ quantityIncrement?: string | null; /** @description Minimum contract quantity from instrument refdata (for example, \"1.00\"). */ quantityMinimum?: string | null; /** @description Contract price grid from instrument refdata (for example, \"0.0001\"). */ priceIncrement?: string | null; /** @description Decimal places supported by the instrument's quote asset. */ quoteAssetPrecision?: number | null; /** @description Minimum contract price and anchor for the instrument price grid (for example, \"0.0001\"). */ priceMinimum?: string | null; /** Format: date-time */ effectiveDate?: string | null; /** * @description Trading state of the contract * @enum {string|null} */ marketState?: \"open\" | \"closed\" | null; /** @description Display order within the event */ sortOrder?: number | null; strike?: components[\"schemas\"][\"Strike\"]; /** * @deprecated * @description Deprecated: use the event-level `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation (e.g., \"GRR-KAIKO_BTCUSD_60S\"). Present for crypto Up/Down contracts. * @example GRR-KAIKO_BTCUSD_60S */ source?: string | null; /** * @description The observed settlement price. Only present after the contract is settled. * @example 87654.32 */ settlementValue?: string | null; }; /** * @description Strike or condition inequality type for contract threshold evaluation. - `reference`: Crypto Up/Down reference strike price captured at `availableAt` time. - `above`: Higher/Lower contract threshold. - `spread`: Point, run, or goal handicap spread line. - `over`: Total or prop threshold evaluated as strict greater than (`>`). - `over_or_equal`: Total or prop threshold evaluated as greater than or equal to (`>=`). - `under`: Total or prop threshold evaluated as strict less than (`<`). - `under_or_equal`: Position, rank, or total threshold evaluated as less than or equal to (`<=`). * @example spread * @enum {string} */ StrikeType: \"reference\" | \"above\" | \"spread\" | \"over\" | \"over_or_equal\" | \"under\" | \"under_or_equal\"; /** @description Strike price or contract threshold information for Up/Down crypto contracts and sports prediction market contracts. */ Strike: { /** * @description The strike price value. Null for \"reference\" type strikes where the value is determined at availableAt time. For sports contracts, this represents the derived numeric strike value (e.g. spread margin, total line, or position/rank threshold). * @example 87500.00 */ value?: string | null; type?: components[\"schemas\"][\"StrikeType\"]; /** * Format: date-time * @description When the strike price becomes available * @example 2026-03-27T19:45:00.000Z */ availableAt?: string | null; }; /** @description Structured data source information for price observation. Replaces the deprecated flat `source` string on the event and contract. Present for crypto Up/Down events. Both fields are omitted when not available. */ SourceDetails: { /** * @description The data provider / vendor name. * @example Kaiko */ agency?: string | null; /** * @description The specific data feed identifier (the value previously carried by the flat `source` field). * @example GRR-KAIKO_BTCUSD_60S */ index?: string | null; } | null; PricePoint: { /** Format: date-time */ timestamp?: string; price?: string; }; /** @description Current bid/ask pricing for the contract */ ContractPrices: { /** @description Buy prices for each outcome */ buy?: { /** * @description Price to buy YES outcome * @example 0.42 */ yes?: string; /** * @description Price to buy NO outcome * @example 0.58 */ no?: string; }; /** @description Sell prices for each outcome */ sell?: { /** * @description Price to sell YES outcome * @example 0.42 */ yes?: string; /** * @description Price to sell NO outcome * @example 0.58 */ no?: string; }; /** * @description Highest buy offer * @example 0.49 */ bestBid?: string | null; /** * @description Lowest sell offer * @example 0.54 */ bestAsk?: string | null; /** * @description Most recent transaction price * @example 0.75 */ lastTradePrice?: string | null; } | null; /** @description A prediction market event containing one or more tradeable contracts */ Event: { id?: string; /** @example Will Bitcoin reach $100k by end of 2028? */ title?: string; /** @example bitcoin-100k-2028 */ slug?: string; description?: string | null; imageUrl?: string | null; type?: components[\"schemas\"][\"MarketType\"]; /** @example crypto */ category?: string; series?: string | null; sportsMarket?: components[\"schemas\"][\"SportsMarket\"]; /** * @description The event ticker (e.g., \"BTC100K2028\") * @example BTC100K2028 */ ticker?: string; status?: components[\"schemas\"][\"MarketStatus\"]; /** Format: date-time */ resolvedAt?: string | null; /** Format: date-time */ createdAt?: string; /** @description Tradeable contracts within this event */ contracts?: components[\"schemas\"][\"Contract\"][]; contractOrderbooks?: { [key: string]: components[\"schemas\"][\"OrderBook\"]; }; /** * @description Total trading volume in USD * @example 125000.00 */ volume?: string; /** * @description Total liquidity in USD * @example 50000.00 */ liquidity?: string; tags?: string[] | null; /** Format: date-time */ effectiveDate?: string; /** Format: date-time */ expiryDate?: string | null; subcategory?: components[\"schemas\"][\"Subcategory\"]; /** * @deprecated * @description Deprecated: use `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation. Aggregated from contracts for crypto Up/Down events. * @example GRR-KAIKO_BTCUSD_60S */ source?: string | null; sourceDetails?: components[\"schemas\"][\"SourceDetails\"]; settlement?: components[\"schemas\"][\"Settlement\"]; }; /** @description Nested category information for the event */ Subcategory: { /** * @description Category identifier * @example 35 */ id?: number; /** * @description URL-friendly category identifier * @example crypto_solana */ slug?: string; /** * @description Display name * @example Solana */ name?: string; /** * @description Category hierarchy path * @example [ * \"Crypto\", * \"Solana\" * ] */ path?: string[]; } | null; /** @description Settlement information for resolved events */ Settlement: { /** * @description The observed settlement value (e.g., the price at expiry for crypto contracts) * @example 87654.32 */ value?: string | null; }; EventsResponse: { data?: components[\"schemas\"][\"Event\"][]; pagination?: components[\"schemas\"][\"Pagination\"]; }; ContractMetadata: { contractId?: string; contractName?: string; contractTicker?: string; eventTicker?: string; eventName?: string; category?: string; contractStatus?: string; /** @description Event type (\"binary\" or \"categorical\") */ eventType?: string; /** Format: date-time */ expiryDate?: string | null; /** Format: date-time */ resolvedAt?: string | null; /** @description Winning outcome if resolved (\"yes\" or \"no\") */ resolutionSide?: string | null; /** @description Parent event ticker for sub-events */ parentEventTicker?: string | null; /** * Format: date-time * @description Start datetime (ISO 8601) */ startTime?: string | null; }; ComboLeg: { /** * Format: int64 * @description Internal ID of the parent combo contract * @example 456 */ comboId: bigint; /** * @description Zero-based position of this leg in the combo * @example 0 */ legIndex: number; /** * @description Internal ID of the underlying single contract, represented as a decimal string * @example 101 */ contractId: string; /** * @description The outcome this leg must settle for the combo to settle YES * @example Yes * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; /** * @description The outcome this leg has settled to, if resolved (`\"Yes\"` or `\"No\"`). Null while the leg is still active. * @example null */ legOutcome?: string | null; /** * Format: date-time * @description UTC timestamp when this leg resolved. Null while still active. * @example null */ resolvedAt?: string | null; /** @description Full metadata for the underlying single contract */ contract?: components[\"schemas\"][\"ContractMetadata\"] | null; }; ComboResponse: { /** @description Metadata for the combo contract itself (ticker, status, expiry, etc.) */ contract: components[\"schemas\"][\"ContractMetadata\"]; /** @description Ordered list of legs that make up this combo */ legs: components[\"schemas\"][\"ComboLeg\"][]; }; ListCombosResponse: { /** @description List of combo contracts matching the query */ combos: components[\"schemas\"][\"ComboResponse\"][]; pagination: components[\"schemas\"][\"Pagination\"]; }; /** @description A canonical combo definition. The authenticated account is derived from the signed request and is not a request field. */ CreateComboRequest: { /** @description Two to six distinct underlying contract legs. The service canonicalizes their complete set, so leg order does not create a distinct combo. */ legs: components[\"schemas\"][\"CreateComboLeg\"][]; }; CreateComboLeg: { /** * @description Underlying contract ID as a decimal string. * @example 101 */ contractId: string; /** * @description Required settlement outcome for this leg. * @example Yes * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; }; CreateComboResponse: { combo: components[\"schemas\"][\"ComboSummary\"]; /** @description `false` when this request created the canonical combo; `true` when the canonical combo already existed. */ alreadyExisted: boolean; }; ComboSummary: { /** * Format: int64 * @description Internal combo ID. * @example 456 */ id: bigint; /** * @description Canonical identity of the complete combo leg set. * @example 101:Yes|202:No */ canonicalLegKey: string; /** * Format: int32 * @description Number of legs in the combo. * @example 2 */ legCount: number; /** @description Human-readable combo name, when available. */ displayName?: string; /** @description Current combo status, when available. */ status?: string; /** * Format: int64 * @description Associated instrument ID, when available. */ instrumentId?: bigint; /** * @description Associated instrument symbol, when available. * @example GEMI-CMB-0526-A7F3B2C1D4E5 */ instrumentSymbol?: string; /** @description Whether the combo has been registered with an instrument symbol. */ instrumentRegistered: boolean; /** * Format: date-time * @description Latest expiry among the underlying legs, when available. */ latestExpiryDate?: string; /** * Format: date-time * @description Creation time, when available. */ createdAt?: string; /** * Format: date-time * @description Most recent update time, when available. */ updatedAt?: string; /** @description Canonically ordered combo legs. */ legs: components[\"schemas\"][\"ComboSummaryLeg\"][]; }; ComboSummaryLeg: { /** * Format: int64 * @description Parent combo ID. */ comboId: bigint; /** * Format: int32 * @description Zero-based leg position in canonical order. */ legIndex: number; /** @description Underlying contract ID as a decimal string. */ contractId: string; /** * @description Required settlement outcome for the leg. * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; /** * @description Settled outcome for the leg, when resolved. * @enum {string|null} */ legOutcome?: \"Yes\" | \"No\" | null; /** * Format: date-time * @description Resolution time for the leg, when resolved. */ resolvedAt?: string | null; /** @description Underlying contract metadata, when available. */ contract?: components[\"schemas\"][\"ContractMetadata\"]; }; ComboWriteError: { /** * @description Error class. * @example InvalidInput */ error: string; /** * @description Machine-readable code for validation or missing-leg errors, when available. * @example COMBO_VALIDATION_ERROR */ code?: string; /** * @description Human-readable error detail. * @example a combo needs 2-6 legs */ message: string; }; OrderRequest: { /** * @description Contract instrument symbol * @example GEMI-FEDJAN26-DN25 */ symbol: string; orderType: components[\"schemas\"][\"OrderType\"]; side: components[\"schemas\"][\"OrderSide\"]; /** * Format: decimal * @description Number of contracts * @example 100 */ quantity: string; /** * Format: decimal * @description Limit price (0-1 range) * @example 0.65 */ price: string; /** * Format: decimal * @description The price to trigger a stop-limit order (0-1 range). Only available for stop-limit orders. See [Stop-Limit Orders](#operation/placeOrder) above for `stopPrice`/`price` constraints. * @example 0.60 */ stopPrice?: string; outcome: components[\"schemas\"][\"Outcome\"]; timeInForce?: components[\"schemas\"][\"TimeInForce\"]; /** * @description Set to `true` to require maker-only behavior. If the order would immediately take liquidity, the order is cancelled instead of filling. * @default false */ makerOrCancel: boolean; }; PlaceOrderBatchRequest: { /** @description Orders to submit. Every entry is validated before any order is submitted. All orders use the account associated with the authenticated request. */ orders: components[\"schemas\"][\"OrderRequest\"][]; }; /** @description An accepted order returned for one batch entry. */ BatchOrderResponse: { /** * Format: int64 * @example 12345678 */ orderId: bigint; /** @description Hashed order ID; omitted when unavailable */ hashOrderId?: string; /** @description Client-provided order ID; omitted when unavailable */ clientOrderId?: string; /** @description Global order ID; omitted when unavailable */ globalOrderId?: string; /** @enum {string} */ status: \"open\" | \"filled\" | \"cancelled\" | \"closed\"; symbol: string; side: components[\"schemas\"][\"OrderSide\"]; outcome: components[\"schemas\"][\"Outcome\"]; orderType: components[\"schemas\"][\"OrderType\"]; /** @enum {string} */ timeInForce: \"good-til-cancel\" | \"immediate-or-cancel\" | \"fill-or-kill\" | \"maker-or-cancel\"; /** @description Original order quantity */ quantity: string; /** @description Amount filled so far */ filledQuantity: string; /** @description Amount remaining to fill */ remainingQuantity: string; /** @description Limit price */ price: string; /** @description Stop trigger price; omitted unless populated for a `stop-limit` order */ stopPrice?: string; /** @description Average price of fills; omitted when unavailable */ avgExecutionPrice?: string; /** Format: date-time */ createdAt: string; /** Format: date-time */ updatedAt: string; /** * Format: date-time * @description Cancellation time; omitted unless the order was cancelled */ cancelledAt?: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; /** @description Promotional cash reserved or applied to the order; omitted when unavailable */ promoCashApplied?: string; /** @description Cash reserved for the unfilled portion of a resting buy order; omitted when unavailable */ fundsOnHold?: string; }; PlaceOrderBatchSuccessResult: { order: components[\"schemas\"][\"BatchOrderResponse\"]; }; PlaceOrderBatchErrorResult: { /** * @description Error class for a rejected entry * @example InsufficientFunds */ error: string; /** * @description Human-readable detail for a rejected entry * @example Insufficient funds */ message: string; }; /** @description Exactly one outcome is present. Accepted entries contain `order`; rejected entries contain `error` and `message`. */ PlaceOrderBatchResult: components[\"schemas\"][\"PlaceOrderBatchSuccessResult\"] | components[\"schemas\"][\"PlaceOrderBatchErrorResult\"]; PlaceOrderBatchResponse: { /** @description One result for each submitted order, in request order. */ results: components[\"schemas\"][\"PlaceOrderBatchResult\"][]; }; CancelOrderBatchRequest: { /** @description Order IDs to cancel. Each ID may be an integer or a quoted numeric string. All IDs are validated before any cancellation is attempted. */ orderIds: (bigint | string)[]; }; CancelOrderBatchSuccessResult: { /** * Format: int64 * @description Order ID from the corresponding request entry. * @example 12345678 */ orderId: bigint; /** @enum {string} */ result: \"ok\"; }; CancelOrderBatchErrorResult: { /** * Format: int64 * @description Order ID from the corresponding request entry. * @example 12345678 */ orderId: bigint; /** * @description Error class for a rejected cancellation * @example OrderNotFound */ error: string; /** * @description Human-readable detail for a rejected cancellation * @example Order 12345678 not found */ message: string; }; /** @description Exactly one outcome is present. Successful entries contain `orderId` and `result`; rejected entries contain `orderId`, `error`, and `message`. */ CancelOrderBatchResult: components[\"schemas\"][\"CancelOrderBatchSuccessResult\"] | components[\"schemas\"][\"CancelOrderBatchErrorResult\"]; CancelOrderBatchResponse: { /** @description One result for each requested cancellation, in request order. */ results: components[\"schemas\"][\"CancelOrderBatchResult\"][]; }; OrderResponse: { /** * Format: int64 * @example 12345678 */ orderId?: bigint; hashOrderId?: string | null; clientOrderId?: string | null; globalOrderId?: string | null; status?: components[\"schemas\"][\"OrderStatus\"]; symbol?: string; side?: components[\"schemas\"][\"OrderSide\"]; outcome?: components[\"schemas\"][\"Outcome\"]; orderType?: components[\"schemas\"][\"OrderType\"]; /** @description Original order quantity */ quantity?: string; /** @description Amount filled so far */ filledQuantity?: string; /** @description Amount remaining to fill */ remainingQuantity?: string; /** @description Limit price */ price?: string; /** @description Stop trigger price (populated for `stop-limit` orders) */ stopPrice?: string | null; /** @description Average price of fills */ avgExecutionPrice?: string | null; /** Format: date-time */ createdAt?: string; /** Format: date-time */ updatedAt?: string; /** Format: date-time */ cancelledAt?: string | null; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; }; OrdersResponse: { orders?: components[\"schemas\"][\"OrderResponse\"][]; pagination?: components[\"schemas\"][\"PaginationSimple\"]; }; Position: { symbol?: string; /** Format: int64 */ instrumentId?: bigint; /** @description Total position size */ totalQuantity?: string; /** @description Quantity currently on hold from open orders */ quantityOnHold?: string; /** @description Average entry price */ avgPrice?: string; outcome?: components[\"schemas\"][\"Outcome\"]; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; prices?: components[\"schemas\"][\"PositionPrices\"]; /** @description Winning outcome (\"yes\" or \"no\") if the contract has resolved */ resolutionSide?: string | null; /** @description Whether the position is above the auto-start threshold */ isAboveAutoStartThreshold?: boolean; /** @description Whether the market is currently live/active */ isLive?: boolean; /** @description Realized profit/loss from sells */ realizedPl?: string | null; /** * @description Mark-to-market value of the position in USD at the current sell price (bestBid for YES, bestAsk for NO). **Absent** from the response when the held outcome has no live sell quote (no liquidity to sell into) — surface a no-liquidity state rather than a price the user cannot transact at. `lastTradePrice` is still returned for display. Treat as `Optional`. * @example 65.00 */ marketValue?: string; /** * @description Unrealized P&L in USD (`marketValue - costBasis`). **Absent** whenever `marketValue` is absent. Treat as `Optional`. * @example 12.50 */ unrealizedPnl?: string; /** * Format: double * @description Unrealized P&L as a percentage of cost basis. Expressed as a percent (e.g. `12.5` represents 12.5%, **not** `0.125`); rounded to 4 decimal places. **Absent** when there is no live sell quote, or when cost basis is zero. Treat as `Optional`. * @example 23.81 */ unrealizedPct?: number; }; /** @description Current bid/ask/last-trade prices for the contract */ PositionPrices: { buy: { yes?: string | null; no?: string | null; }; sell: { yes?: string | null; no?: string | null; }; bestBid?: string | null; bestAsk?: string | null; lastTradePrice?: string | null; } | null; PositionsResponse: { positions?: components[\"schemas\"][\"Position\"][]; /** @description Total number of positions (for pagination) */ total?: number | null; }; /** @description A historically settled position in a resolved prediction market contract. */ SettledPosition: { /** * Format: int64 * @description Account that held the position */ accountId?: bigint; /** * Format: int64 * @description Unique instrument identifier for the contract */ instrumentId?: bigint; /** * @description Contract instrument symbol * @example GEMI-FEDJAN26-DN25 */ instrumentSymbol?: string; /** * @description Signed position held at settlement. Positive values represent a `yes` position; negative values represent a `no` position. * @example 125 */ position?: string; /** * @description Absolute quantity held at settlement (unsigned) * @example 125 */ positionQuantity?: string; outcome?: components[\"schemas\"][\"Outcome\"]; /** * @description Payout received from settlement. `0` when the position lost. * @example 125.00 */ payout?: string; /** @description The winning outcome of the contract */ resolutionSide?: components[\"schemas\"][\"Outcome\"]; /** * Format: date-time * @description Settlement timestamp (ISO 8601) */ settledAt?: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; /** * @description Total amount spent to enter the position, net of any prior realized P&L from partial sells. Omitted when cost-basis data is not available. * @example 78.75 */ costBasis?: string | null; /** * @description Realized profit or loss recorded from sells prior to settlement. Omitted when not available. * @example 0 */ realizedPnl?: string | null; /** * @description Net profit for the position, computed as `payout - costBasis + realizedPnl`. Omitted when `costBasis` is not available. * @example 46.25 */ netProfit?: string | null; }; SettledPositionsResponse: { positions?: components[\"schemas\"][\"SettledPosition\"][]; /** @description Total number of settled positions across all pages for the current filter set. */ total?: number | null; /** @description Sum of `payout` across all settled positions in the filter set. Retained for binary back-compat with the legacy response shape; **field is absent (not `null`) on the unified backend** because computing a roll-up over the full filtered set would require a separate aggregate query (deferred until a partner asks). Play's default `OptionHandlers` omits absent `Option` fields rather than emitting `null`. */ totalPayout?: string; /** @description Sum of `costBasis` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ totalCostBasis?: string; /** @description Sum of `netProfit` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ totalNetProfit?: string; /** @description Cash-outs (early sells before contract resolution) in the same account-scoped time window as the returned page's settled positions. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. `positions[]` pagination is unaffected — `limit`/`offset` continue to scope `positions[]` only. */ cashOuts?: components[\"schemas\"][\"CashedOutPosition\"][]; /** * @description Sum of `cashOuts[].proceeds` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 120.00 */ totalCashOutProceeds?: string; /** * @description Sum of `cashOuts[].costBasis` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 100.00 */ totalCashOutCostBasis?: string; /** * @description Sum of `cashOuts[].netProfit` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 20.00 */ totalCashOutNetProfit?: string; }; /** @description A qualifying cash-out (early sell before contract resolution) with cost-basis context. Exposed only via the `withCashOuts=true` sibling array on `POST /v1/prediction-markets/positions/settled`. Distinct from `SettledPosition` — cash-outs don't have a `payout` or `resolutionSide` since the contract hadn't resolved when the user sold. */ CashedOutPosition: { /** * Format: int64 * @description Account that held the position. * @example 456 */ accountId: bigint; /** * Format: int64 * @description Contract instrument ID. * @example 16789219 */ instrumentId: bigint; /** * @description Contract instrument symbol. * @example GEMI-FEDJAN26-DN25 */ instrumentSymbol: string; /** * Format: date-time * @description Wall-clock timestamp when the cash-out order closed (ISO 8601). * @example 2026-05-15T14:30:00.000Z */ timestamp: string; /** * @description Quantity sold (cumulative filled quantity on the cash-out order). * @example 10 */ filledQuantity: string; /** * @description Always `sell` for cash-outs. * @example sell * @enum {string} */ side: \"sell\"; /** * @description Amount received from the sale in USD. For prediction sells, proceeds flow through `cash_balance` rather than `closed_orders.total_spend`, so the value is derived from position-balance snapshots before/after the fill. * @example 10.50 */ proceeds: string; /** * @description Cost basis allocated proportionally to the filled quantity (`(costBasisSpend / costBasisPositionBalance) * filledQuantity`). * @example 10.00 */ costBasis: string; /** * @description Realized P&L from this cash-out fill (`proceeds - costBasis`). Equals the ledger `realized_pl` delta on the position-balance row pair around the fill; falls back to `0` under transient market-data lag so a missing post-fill snapshot can't poison the page. * @example 0.50 */ netProfit: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; }; ContractShareVolume: { /** * @description Contract instrument symbol * @example GEMI-FED260318-CUT25 */ symbol?: string; /** * @description Total taker volume across all participants (in shares) * @example 94625 */ totalQty?: string; /** * @description The authenticated user's taker (aggressor) volume (in shares) * @example 1 */ userAggressorQty?: string | null; /** * @description The authenticated user's maker (resting) volume (in shares) * @example 0 */ userRestingQty?: string | null; }; VolumeMetricsResponse: { /** * @description The event ticker * @example FED260318 */ eventTicker?: string; contracts?: components[\"schemas\"][\"ContractShareVolume\"][]; }; PredictionMarketVolumeCategory: { /** * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. * @example [ * \"Sports\", * \"Football\", * \"Pro Football\" * ] */ categoryPath: string[]; /** @description Total volume for this category, including all descendant categories. */ volume: components[\"schemas\"][\"PredictionMarketVolumeDecimal\"]; }; PredictionMarketHourlyVolumeCategory: { /** * Format: date-time * @description Inclusive UTC start of this hourly period. * @example 2026-07-20T00:00:00Z */ periodStart: string; /** * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. * @example [ * \"Sports\", * \"Football\", * \"Pro Football\" * ] */ categoryPath: string[]; /** @description Total volume for this category in this hour, including all descendant categories. */ volume: components[\"schemas\"][\"PredictionMarketVolumeDecimal\"]; }; /** * @description Non-negative decimal string. Preserve it as a string to avoid floating-point precision loss. * @example 143567.25 */ PredictionMarketVolumeDecimal: string; MakerRebateRateRule: { /** * Format: int64 * @description Stable identifier for this rate rule. * @example 12 */ id: bigint; /** * Format: int32 * @description Portion of the maker fee that is rebated, in basis points (10000 bps = 100%). * @example 5000 */ rebate_multiplier_bps: number; /** * Format: date-time * @description ISO-8601 timestamp at which this rule becomes effective. Always present; in practice never `null`. * @example 2026-03-19T00:00:00Z */ effective_from: string | null; /** * @description Market category this rule applies to. When absent, the rule applies to all categories. * @example Crypto */ category?: string; /** * Format: date-time * @description ISO-8601 timestamp after which this rule is superseded. Omitted when the rule is still current. * @example 2026-04-19T00:00:00Z */ effective_to?: string; }; MakerRebateRatesResponse: { rate_rules: components[\"schemas\"][\"MakerRebateRateRule\"][]; }; MakerRebatePayout: { /** * Format: int64 * @description Stable payout identifier. * @example 9182 */ id: bigint; /** * @description Total qualifying maker volume contributing to this payout, in USD. * @example 12450.00 */ total_volume_usd: string; /** * @description Total rebate paid, in USD. * @example 6.23 */ total_rebate_usd: string; /** * Format: int32 * @description Number of qualifying maker fills that contributed to the payout. * @example 187 */ total_fill_count: number; /** * @description Payout status (e.g. `PENDING`, `PAID`). * @example PAID */ status: string; /** * Format: date-time * @description ISO-8601 timestamp at which the rebate was credited. Always present; `null` for payouts that have not yet been paid. * @example 2026-05-20T21:00:00Z */ paid_at: string | null; /** * Format: date-time * @description ISO-8601 timestamp at which the payout row was created. Always present. * @example 2026-05-20T20:55:12Z */ created_at: string | null; }; MakerRebatePayoutsResponse: { payouts: components[\"schemas\"][\"MakerRebatePayout\"][]; }; MakerRebateLifetimeSummary: { /** * @description Sum of `total_rebate_usd` across payouts in the window. * @example 152.40 */ total_earned_usd: string; /** * Format: int64 * @description Sum of qualifying maker fills across payouts in the window. * @example 4218 */ total_fill_count: bigint; /** * @description Sum of qualifying maker volume (USD) across payouts in the window. * @example 304800.00 */ total_volume_usd: string; /** * Format: int32 * @description Number of payouts in the window. Always present; `0` when no payouts exist in the window. * @example 27 */ payout_count: number; /** * Format: date * @description Date of the earliest payout in the window, or `null` if no payouts exist. * @example 2026-03-19 */ first_payout_date: string | null; /** * Format: date * @description Date of the most recent payout in the window, or `null` if no payouts exist. * @example 2026-05-20 */ last_payout_date: string | null; }; LiquidityRewardsConfig: { /** * Format: int32 * @description Quotes wider than this spread score zero in the scoring algorithm. Only present when `enabled` is `true`. * @example 10 */ max_spread_cents?: number; /** * @description Daily reward amounts below this threshold are suppressed (sub-threshold accounts get no row at all). Only present when `enabled` is `true`. * @example 1.00 */ min_payout_threshold_usd?: string; /** * @description True when the program is fully configured upstream. When false, the response collapses to `{ \"enabled\": false }` only. * @example true */ enabled: boolean; }; LiquidityRewardEvent: { /** * @description Event ticker (e.g. `BTC2605202100`). * @example BTC2605202100 */ event_ticker: string; /** * @description Event title. * @example BTC above $95,000? */ title: string; /** * @description Market category. * @example Crypto */ category: string; /** * @description Daily USD reward pool budgeted for this event. * @example 500.00 */ daily_pool_usd: string; /** * @description Whether the pool came from a per-event override or the category default. * @example event_override * @enum {string} */ pool_source: \"event_override\" | \"category_default\" | \"unspecified\"; /** * Format: date-time * @description ISO-8601 timestamp at which the event ends and stops scoring. `null` when the underlying event has no end timestamp set. * @example 2026-05-20T21:00:00Z */ ends_at: string | null; /** * Format: int32 * @description Number of accounts that met qualifying-maker criteria in the most recent snapshot window for this event. * @example 14 */ qualifying_maker_count: number; /** * @description Optional URL for the event icon. Omitted when not configured. * @example https://example.com/btc.png */ icon_url?: string; }; LiquidityRewardsEventsResponse: { events: components[\"schemas\"][\"LiquidityRewardEvent\"][]; pagination: components[\"schemas\"][\"Pagination\"]; /** * Format: date * @description Most recent date for which scoring has been written. `null` when no scoring has run yet. * @example 2026-05-19 */ last_score_date: string | null; }; LiquidityEventScore: { /** * Format: int64 * @description Stable event identifier. * @example 1234567890 */ event_id: bigint; /** * @description Event title. * @example BTC above $95,000? */ event_name: string; /** * @description Market category. * @example Crypto */ category_name: string; /** * @description This account's normalized score for the event on the scoring date (0-1 range as a decimal string). * @example 0.4521 */ normalized_score: string; /** * Format: int32 * @description Number of snapshots in which this account had a qualifying quote. * @example 1180 */ snapshot_count: number; /** * Format: int32 * @description Total snapshots taken for the event on the scoring date. * @example 1440 */ total_snapshots: number; /** * @description Portion of the day's total reward attributed to this event. * @example 8.20 */ event_reward_usd: string; }; LiquidityDailySummary: { /** * Format: date * @description Date the payout applies to (Eastern Time). * @example 2026-05-07 */ payout_date: string; /** * @description Total USD reward for the day across all events the account scored on. * @example 12.45 */ total_reward_usd: string; /** * @description Status of the day's payout (e.g. `PENDING`, `PAID`, `ZERO_AMOUNT`). * @example PAID */ payout_status: string; /** * Format: date-time * @description ISO-8601 timestamp the day's payout was credited. Always present; `null` if not yet paid. * @example 2026-05-08T21:00:00Z */ paid_at: string | null; /** @description Per-event score breakdown showing how the day's total was distributed. */ events: components[\"schemas\"][\"LiquidityEventScore\"][]; }; LiquidityRewardsDailySummaryResponse: { daily_summaries: components[\"schemas\"][\"LiquidityDailySummary\"][]; }; LiquidityRewardsLifetimeSummary: { /** * @description Sum of `total_reward_usd` across daily payouts in the window. * @example 84.20 */ total_earned_usd: string; /** * Format: int32 * @description Number of daily payouts in the window. Always present; `0` when no payouts exist in the window. * @example 12 */ payout_count: number; /** * Format: date * @description Date of the earliest payout in the window, or `null` if no payouts exist. * @example 2026-05-08 */ first_payout_date: string | null; /** * Format: date * @description Date of the most recent payout in the window, or `null` if no payouts exist. * @example 2026-05-20 */ last_payout_date: string | null; }; }; responses: { /** @description Invalid request parameters */ BadRequest: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Authentication required or invalid credentials */ Unauthorized: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Internal server error */ InternalError: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Prediction markets feature is temporarily unavailable */ ServiceUnavailable: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; }; parameters: { /** @description Maximum number of results to return (max 500) */ Limit: number; /** @description Number of results to skip for pagination */ Offset: number; /** @description Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. */ SportFilter: components[\"schemas\"][\"SportsMarketSport\"][]; /** @description Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketTypeFilter: components[\"schemas\"][\"SportsMarketType\"][]; /** @description Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketSubjectFilter: components[\"schemas\"][\"SportsMarketSubject\"][]; /** @description Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketScopeFilter: components[\"schemas\"][\"SportsMarketScopeType\"][]; /** @description Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketMetricFilter: components[\"schemas\"][\"SportsMarketMetric\"][]; }; requestBodies: never; headers: never; pathItems: never; }" }, { "kind": "InterfaceDeclaration", @@ -759,7 +764,7 @@ { "kind": "InterfaceDeclaration", "name": "RfqLeg", - "declaration": "export interface RfqLeg { c: string; o: AnonymousSchema_148; s?: string; }" + "declaration": "export interface RfqLeg { c: string; o: AnonymousSchema_145; s?: string; }" }, { "kind": "EnumDeclaration", @@ -769,7 +774,7 @@ { "kind": "InterfaceDeclaration", "name": "RfqPrivateDelivery", - "declaration": "export interface RfqPrivateDelivery { e: 'requestForQuote'; i: string; E: number | bigint; r: string; x: AnonymousSchema_153; S: RfqLifecycleState; q?: string; p?: string; sz?: string; qs?: RfqQuoteStatus; vu?: number | bigint; }" + "declaration": "export interface RfqPrivateDelivery { e: 'requestForQuote'; i: string; E: number | bigint; r: string; x: AnonymousSchema_150; S: RfqLifecycleState; q?: string; p?: string; sz?: string; qs?: RfqQuoteStatus; vu?: number | bigint; }" }, { "kind": "InterfaceDeclaration", @@ -784,7 +789,7 @@ { "kind": "InterfaceDeclaration", "name": "RfqSubmitQuoteParams", - "declaration": "export interface RfqSubmitQuoteParams { rfqId: string; price: string; quantity: string; validUntil?: number | bigint; }" + "declaration": "export interface RfqSubmitQuoteParams { rfqId: string; price: string; quantity: string; validUntil?: number | bigint; clientId?: string; }" }, { "kind": "InterfaceDeclaration", @@ -1043,6 +1048,11 @@ "name": "AnonymousSchema_117", "declaration": "export declare enum AnonymousSchema_117 { YES = \"YES\", NO = \"NO\" }" }, + { + "kind": "EnumDeclaration", + "name": "AnonymousSchema_132", + "declaration": "export declare enum AnonymousSchema_132 { YES = \"YES\", NO = \"NO\", UNSPECIFIED = \"UNSPECIFIED\" }" + }, { "kind": "EnumDeclaration", "name": "AnonymousSchema_135", @@ -1050,18 +1060,18 @@ }, { "kind": "EnumDeclaration", - "name": "AnonymousSchema_148", - "declaration": "export declare enum AnonymousSchema_148 { YES = \"YES\", NO = \"NO\" }" + "name": "AnonymousSchema_145", + "declaration": "export declare enum AnonymousSchema_145 { YES = \"YES\", NO = \"NO\" }" }, { "kind": "EnumDeclaration", - "name": "AnonymousSchema_152", - "declaration": "export declare enum AnonymousSchema_153 { RESERVED_CLOSED = \"CLOSED\", ACCEPTED = \"ACCEPTED\", CONFIRMED = \"CONFIRMED\", DECLINED = \"DECLINED\", FINALIZED = \"FINALIZED\", FAILED = \"FAILED\" }" + "name": "AnonymousSchema_150", + "declaration": "export declare enum AnonymousSchema_150 { RESERVED_CLOSED = \"CLOSED\", ACCEPTED = \"ACCEPTED\", CONFIRMED = \"CONFIRMED\", DECLINED = \"DECLINED\", FINALIZED = \"FINALIZED\", FAILED = \"FAILED\" }" }, { "kind": "EnumDeclaration", - "name": "AnonymousSchema_153", - "declaration": "export declare enum AnonymousSchema_153 { RESERVED_CLOSED = \"CLOSED\", ACCEPTED = \"ACCEPTED\", CONFIRMED = \"CONFIRMED\", DECLINED = \"DECLINED\", FINALIZED = \"FINALIZED\", FAILED = \"FAILED\" }" + "name": "AnonymousSchema_152", + "declaration": "export declare enum AnonymousSchema_150 { RESERVED_CLOSED = \"CLOSED\", ACCEPTED = \"ACCEPTED\", CONFIRMED = \"CONFIRMED\", DECLINED = \"DECLINED\", FINALIZED = \"FINALIZED\", FAILED = \"FAILED\" }" }, { "kind": "InterfaceDeclaration", @@ -1491,7 +1501,7 @@ { "kind": "InterfaceDeclaration", "name": "NamedAmount", - "declaration": "export interface NamedAmount { t: string; v: string; c?: string; }" + "declaration": "export interface NamedAmount { t: string; v: string; c?: string; o?: AnonymousSchema_132; }" }, { "kind": "VariableDeclaration", @@ -1676,7 +1686,7 @@ { "kind": "InterfaceDeclaration", "name": "PredictionMarketsComponents", - "declaration": "export interface components { schemas: { Error: { /** * @description Error code * @example InvalidInput */ error?: string; /** * @description Human-readable error message * @example orderId is required */ message?: string; }; PredictionMarketsError: { /** * @description Prediction Markets error class * @example InvalidInput */ error: string; /** * @description Request field associated with the error, when available * @example orders */ field?: string; /** * @description Human-readable error detail, when available * @example orders must contain between 1 and 20 entries */ message?: string; }; AuthErrorResponse: { /** @enum {string} */ result: \"error\"; /** * @description Authentication or authorization error class * @example MissingNonce */ reason: string; /** * @description Human-readable authentication or authorization detail * @example Must provide unique monotonic increasing 'nonce' field in payload */ message: string; }; AccountGroupBlockedError: { /** @enum {string} */ error: \"This account is not permitted to trade prediction markets\"; /** @enum {string} */ code: \"ACCOUNT_GROUP_BLOCKED\"; }; TermsNotAcceptedError: { /** @enum {string} */ error: \"TERMS_NOT_ACCEPTED\"; /** @enum {string} */ message: \"Prediction markets terms must be accepted before placing orders\"; }; RestrictedSellOnlyError: { /** @enum {string} */ error: \"ACCOUNT_RESTRICTED_SELL_ONLY\"; /** @enum {string} */ message: \"Your account is restricted to selling existing positions; buying is not permitted.\"; }; PredictionMarketsTerms: { /** * @description Terms type identifier * @example PredictionsMarket */ termsType: string; /** * @description Latest terms version * @example 3 */ version: number; /** * @description Terms content to display before acceptance * @example These are the prediction market terms. */ content: string; /** * Format: date-time * @description UTC timestamp when the terms content was last updated * @example 2026-05-18T17:00:00Z */ updatedAt: string; }; PredictionMarketsTermsStatus: { /** * @description Whether the account group has accepted the latest configured Prediction Markets terms * @example false */ hasAcceptedLatest: boolean; /** * @description Latest terms version accepted by the account group, if any * @example 2 */ acceptedVersion?: number | null; /** * @description Latest configured Prediction Markets terms version, if available * @example 3 */ latestVersion?: number | null; }; AcceptPredictionMarketsTermsResponse: { /** @example true */ success: boolean; }; /** * @description Status of a prediction market * @enum {string} */ MarketStatus: \"approved\" | \"active\" | \"closed\" | \"under_review\" | \"settled\" | \"invalid\"; /** * @description Type of prediction market * @enum {string} */ MarketType: \"binary\" | \"categorical\"; /** * @description Sport whose rules give the market's scope and metric their sport-specific meaning. * @enum {string} */ SportsMarketSport: \"american_football\" | \"athletics\" | \"australian_rules_football\" | \"baseball\" | \"basketball\" | \"boxing\" | \"chess\" | \"cricket\" | \"cycling\" | \"darts\" | \"esports\" | \"golf\" | \"hockey\" | \"lacrosse\" | \"mixed_martial_arts\" | \"motorsports\" | \"rugby\" | \"sailing\" | \"soccer\" | \"tennis\"; /** * @description Conventional sports-market family. `subject`, `scope`, and `metric` provide detail within the family. This classification is independent of the event's structural `type` (`binary` or `categorical`). * @enum {string} */ SportsMarketType: \"moneyline\" | \"spread\" | \"total\" | \"prop\" | \"correct_score\" | \"to_advance\" | \"futures\" | \"other\"; /** * @description What the market is about. `participant` covers non-player entrants such as drivers and horses. * @enum {string} */ SportsMarketSubject: \"contest\" | \"team\" | \"player\" | \"participant\" | \"other\"; /** * @description Unit covered by the market. `full_contest` follows the market's official final-result rules; `regulation` covers scheduled regulation play only. Ordinal and range qualifiers are represented separately on `SportsMarketScope`. * @enum {string} */ SportsMarketScopeType: \"full_contest\" | \"regulation\" | \"half\" | \"quarter\" | \"period\" | \"inning\" | \"team_innings\" | \"over\" | \"powerplay\" | \"set\" | \"game\" | \"round\" | \"hole\" | \"match_day\" | \"session\" | \"super_over\" | \"race\" | \"sprint\" | \"qualifying\" | \"practice\" | \"stage\" | \"lap\" | \"series\" | \"season\" | \"tournament\" | \"competition\" | \"other\"; /** * @description Statistic measured by the market. Interpret shared metric names using `sportsMarket.sport`. * @enum {string} */ SportsMarketMetric: \"aces\" | \"assists\" | \"balls_faced\" | \"birdies\" | \"blocked_shots\" | \"blocks\" | \"bogeys\" | \"boundaries\" | \"break_points_won\" | \"cards\" | \"catches\" | \"clean_sheets\" | \"completed_passes\" | \"control_time\" | \"corners\" | \"defensive_rebounds\" | \"double_double\" | \"double_faults\" | \"doubles\" | \"eagles\" | \"earned_runs\" | \"errors\" | \"faceoff_wins\" | \"fairways_hit\" | \"fantasy_points\" | \"fastest_lap\" | \"field_goals_made\" | \"finishing_position\" | \"fouls\" | \"fours\" | \"free_throws_made\" | \"fumbles\" | \"games\" | \"goals\" | \"goals_allowed\" | \"greens_in_regulation\" | \"grid_position\" | \"hits\" | \"hits_allowed\" | \"hits_runs_rbis\" | \"holes_in_one\" | \"home_runs\" | \"innings_pitched\" | \"interceptions_thrown\" | \"kicking_points\" | \"knockdowns\" | \"laps_completed\" | \"laps_led\" | \"lap_time\" | \"longest_pass_completion\" | \"longest_reception\" | \"longest_rush\" | \"maiden_overs\" | \"offensive_rebounds\" | \"offsides\" | \"pars\" | \"passes\" | \"pass_attempts\" | \"pass_completions\" | \"passing_touchdowns\" | \"passing_yards\" | \"penalty_minutes\" | \"pitching_outs_recorded\" | \"pit_stops\" | \"points\" | \"points_assists\" | \"points_rebounds\" | \"points_rebounds_assists\" | \"positions_gained\" | \"power_play_points\" | \"putts\" | \"qualifying_position\" | \"rebounds\" | \"rebounds_assists\" | \"receiving_touchdowns\" | \"receiving_yards\" | \"receptions\" | \"red_cards\" | \"retirements\" | \"rounds\" | \"runs\" | \"runs_batted_in\" | \"runs_conceded\" | \"rush_attempts\" | \"rushing_touchdowns\" | \"rushing_yards\" | \"sacks\" | \"safety_cars\" | \"saves\" | \"sets\" | \"shots\" | \"shots_on_goal\" | \"shots_on_target\" | \"shutouts\" | \"significant_strikes\" | \"singles\" | \"sixes\" | \"steals\" | \"stolen_bases\" | \"strokes\" | \"strikeouts\" | \"submission_attempts\" | \"tackles\" | \"takedowns\" | \"three_pointers_made\" | \"tiebreaks_won\" | \"total_bases\" | \"total_points_won\" | \"total_strikes\" | \"touchdowns\" | \"triples\" | \"triple_double\" | \"turnovers\" | \"walks\" | \"wickets\" | \"wins\" | \"yellow_cards\" | \"other\"; /** @description Settlement scope. `ordinal` identifies one unit; `start` and `end` identify an inclusive range of units. */ SportsMarketScope: { type: components[\"schemas\"][\"SportsMarketScopeType\"]; /** * Format: int32 * @description Optional ordinal within the scope type, such as half `1` or quarter `4`. */ ordinal?: number; /** * Format: int32 * @description Optional inclusive start of a scope range, such as inning `1`. */ start?: number; /** * Format: int32 * @description Optional inclusive end of a scope range, such as inning `5`. */ end?: number; }; /** @description Atomic sports-market classification shared by every contract grouped under the event. Present only for sports events. All fields except `metric` are required together. */ SportsMarket: { sport: components[\"schemas\"][\"SportsMarketSport\"]; type: components[\"schemas\"][\"SportsMarketType\"]; subject: components[\"schemas\"][\"SportsMarketSubject\"]; scope: components[\"schemas\"][\"SportsMarketScope\"]; metric?: components[\"schemas\"][\"SportsMarketMetric\"]; }; /** * @description Order type. `stop-limit` orders require a `stopPrice` that triggers a limit order at `price` when the market reaches the trigger. * @enum {string} */ OrderType: \"limit\" | \"stop-limit\"; /** @enum {string} */ OrderSide: \"buy\" | \"sell\"; /** * @description The outcome being traded (Yes or No) * @enum {string} */ Outcome: \"yes\" | \"no\"; /** * @description Order execution behavior: * - `good-til-cancel` - Order remains active until filled or cancelled (default) * - `immediate-or-cancel` - Fill immediately or cancel remaining * - `fill-or-kill` - Fill entire order immediately or cancel * @default good-til-cancel * @enum {string} */ TimeInForce: \"good-til-cancel\" | \"immediate-or-cancel\" | \"fill-or-kill\"; /** @enum {string} */ OrderStatus: \"open\" | \"filled\" | \"cancelled\"; /** @enum {string} */ PositionStatus: \"active\" | \"resolved\" | \"cancelled\"; Pagination: { /** @example 50 */ limit?: number; /** @example 0 */ offset?: number; /** @example 100 */ total?: number; }; PaginationSimple: { limit?: number; offset?: number; /** @description Number of items in current response */ count?: number; }; OrderBook: { bids?: components[\"schemas\"][\"OrderBookEntry\"][]; asks?: components[\"schemas\"][\"OrderBookEntry\"][]; }; OrderBookEntry: { side?: components[\"schemas\"][\"OrderSide\"]; /** @example 0.65 */ price?: string; /** @example 1000 */ quantity?: string; }; OrderBookDepth: { bids?: components[\"schemas\"][\"OrderBookLevel\"][]; asks?: components[\"schemas\"][\"OrderBookLevel\"][]; /** Format: date-time */ lastUpdateTime?: string; }; OrderBookLevel: { price?: string; quantity?: string; orderCount?: number; }; /** @description Contract quantity and price validation is instrument-specific. Clients must validate order quantities and prices against the returned increment and minimum fields rather than assuming a fixed grid. */ Contract: { id?: string; /** @description Human-readable label for the contract's YES-space proposition (e.g., \"SOL > $90\") */ label?: string; /** @description Short form label (e.g., \">$90\") */ abbreviatedName?: string | null; /** @description Rich text description */ description?: Record; prices?: components[\"schemas\"][\"ContractPrices\"]; totalShares?: string | null; color?: string | null; status?: components[\"schemas\"][\"MarketStatus\"]; imageUrl?: string | null; priceHistory?: components[\"schemas\"][\"PricePoint\"][] | null; /** Format: date-time */ createdAt?: string; /** Format: date-time */ expiryDate?: string | null; resolutionSide?: components[\"schemas\"][\"Outcome\"]; /** Format: date-time */ resolvedAt?: string | null; termsAndConditionsUrl?: string; ticker?: string; instrumentSymbol?: string; /** @description Contract quantity grid from instrument refdata (for example, \"0.01\"). */ quantityIncrement?: string | null; /** @description Minimum contract quantity from instrument refdata (for example, \"1.00\"). */ quantityMinimum?: string | null; /** @description Contract price grid from instrument refdata (for example, \"0.0001\"). */ priceIncrement?: string | null; /** @description Decimal places supported by the instrument's quote asset. */ quoteAssetPrecision?: number | null; /** @description Minimum contract price and anchor for the instrument price grid (for example, \"0.0001\"). */ priceMinimum?: string | null; /** Format: date-time */ effectiveDate?: string | null; /** * @description Trading state of the contract * @enum {string|null} */ marketState?: \"open\" | \"closed\" | null; /** @description Display order within the event */ sortOrder?: number | null; strike?: components[\"schemas\"][\"Strike\"]; /** * @deprecated * @description Deprecated: use the event-level `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation (e.g., \"GRR-KAIKO_BTCUSD_60S\"). Present for crypto Up/Down contracts. * @example GRR-KAIKO_BTCUSD_60S */ source?: string | null; /** * @description The observed settlement price. Only present after the contract is settled. * @example 87654.32 */ settlementValue?: string | null; }; /** * @description Strike or condition inequality type for contract threshold evaluation. - `reference`: Crypto Up/Down reference strike price captured at `availableAt` time. - `above`: Higher/Lower contract threshold. - `spread`: Point, run, or goal handicap spread line. - `over`: Total or prop threshold evaluated as strict greater than (`>`). - `over_or_equal`: Total or prop threshold evaluated as greater than or equal to (`>=`). - `under`: Total or prop threshold evaluated as strict less than (`<`). - `under_or_equal`: Position, rank, or total threshold evaluated as less than or equal to (`<=`). * @example spread * @enum {string} */ StrikeType: \"reference\" | \"above\" | \"spread\" | \"over\" | \"over_or_equal\" | \"under\" | \"under_or_equal\"; /** @description Strike price or contract threshold information for Up/Down crypto contracts and sports prediction market contracts. */ Strike: { /** * @description The strike price value. Null for \"reference\" type strikes where the value is determined at availableAt time. For sports contracts, this represents the derived numeric strike value (e.g. spread margin, total line, or position/rank threshold). * @example 87500.00 */ value?: string | null; type?: components[\"schemas\"][\"StrikeType\"]; /** * Format: date-time * @description When the strike price becomes available * @example 2026-03-27T19:45:00.000Z */ availableAt?: string | null; }; /** @description Structured data source information for price observation. Replaces the deprecated flat `source` string on the event and contract. Present for crypto Up/Down events. Both fields are omitted when not available. */ SourceDetails: { /** * @description The data provider / vendor name. * @example Kaiko */ agency?: string | null; /** * @description The specific data feed identifier (the value previously carried by the flat `source` field). * @example GRR-KAIKO_BTCUSD_60S */ index?: string | null; } | null; PricePoint: { /** Format: date-time */ timestamp?: string; price?: string; }; /** @description Current bid/ask pricing for the contract */ ContractPrices: { /** @description Buy prices for each outcome */ buy?: { /** * @description Price to buy YES outcome * @example 0.42 */ yes?: string; /** * @description Price to buy NO outcome * @example 0.58 */ no?: string; }; /** @description Sell prices for each outcome */ sell?: { /** * @description Price to sell YES outcome * @example 0.42 */ yes?: string; /** * @description Price to sell NO outcome * @example 0.58 */ no?: string; }; /** * @description Highest buy offer * @example 0.49 */ bestBid?: string | null; /** * @description Lowest sell offer * @example 0.54 */ bestAsk?: string | null; /** * @description Most recent transaction price * @example 0.75 */ lastTradePrice?: string | null; } | null; /** @description A prediction market event containing one or more tradeable contracts */ Event: { id?: string; /** @example Will Bitcoin reach $100k by end of 2028? */ title?: string; /** @example bitcoin-100k-2028 */ slug?: string; description?: string | null; imageUrl?: string | null; type?: components[\"schemas\"][\"MarketType\"]; /** @example crypto */ category?: string; series?: string | null; sportsMarket?: components[\"schemas\"][\"SportsMarket\"]; /** * @description The event ticker (e.g., \"BTC100K2028\") * @example BTC100K2028 */ ticker?: string; status?: components[\"schemas\"][\"MarketStatus\"]; /** Format: date-time */ resolvedAt?: string | null; /** Format: date-time */ createdAt?: string; /** @description Tradeable contracts within this event */ contracts?: components[\"schemas\"][\"Contract\"][]; contractOrderbooks?: { [key: string]: components[\"schemas\"][\"OrderBook\"]; }; /** * @description Total trading volume in USD * @example 125000.00 */ volume?: string; /** * @description Total liquidity in USD * @example 50000.00 */ liquidity?: string; tags?: string[] | null; /** Format: date-time */ effectiveDate?: string; /** Format: date-time */ expiryDate?: string | null; subcategory?: components[\"schemas\"][\"Subcategory\"]; /** * @deprecated * @description Deprecated: use `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation. Aggregated from contracts for crypto Up/Down events. * @example GRR-KAIKO_BTCUSD_60S */ source?: string | null; sourceDetails?: components[\"schemas\"][\"SourceDetails\"]; settlement?: components[\"schemas\"][\"Settlement\"]; }; /** @description Nested category information for the event */ Subcategory: { /** * @description Category identifier * @example 35 */ id?: number; /** * @description URL-friendly category identifier * @example crypto_solana */ slug?: string; /** * @description Display name * @example Solana */ name?: string; /** * @description Category hierarchy path * @example [ * \"Crypto\", * \"Solana\" * ] */ path?: string[]; } | null; /** @description Settlement information for resolved events */ Settlement: { /** * @description The observed settlement value (e.g., the price at expiry for crypto contracts) * @example 87654.32 */ value?: string | null; }; EventsResponse: { data?: components[\"schemas\"][\"Event\"][]; pagination?: components[\"schemas\"][\"Pagination\"]; }; ContractMetadata: { contractId?: string; contractName?: string; contractTicker?: string; eventTicker?: string; eventName?: string; category?: string; contractStatus?: string; /** @description Event type (\"binary\" or \"categorical\") */ eventType?: string; /** Format: date-time */ expiryDate?: string | null; /** Format: date-time */ resolvedAt?: string | null; /** @description Winning outcome if resolved (\"yes\" or \"no\") */ resolutionSide?: string | null; /** @description Parent event ticker for sub-events */ parentEventTicker?: string | null; /** * Format: date-time * @description Start datetime (ISO 8601) */ startTime?: string | null; }; ComboLeg: { /** * Format: int64 * @description Internal ID of the parent combo contract * @example 456 */ comboId: bigint; /** * @description Zero-based position of this leg in the combo * @example 0 */ legIndex: number; /** * @description Internal ID of the underlying single contract, represented as a decimal string * @example 101 */ contractId: string; /** * @description The outcome this leg must settle for the combo to settle YES * @example Yes * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; /** * @description The outcome this leg has settled to, if resolved (`\"Yes\"` or `\"No\"`). Null while the leg is still active. * @example null */ legOutcome?: string | null; /** * Format: date-time * @description UTC timestamp when this leg resolved. Null while still active. * @example null */ resolvedAt?: string | null; /** @description Full metadata for the underlying single contract */ contract?: components[\"schemas\"][\"ContractMetadata\"] | null; }; ComboResponse: { /** @description Metadata for the combo contract itself (ticker, status, expiry, etc.) */ contract: components[\"schemas\"][\"ContractMetadata\"]; /** @description Ordered list of legs that make up this combo */ legs: components[\"schemas\"][\"ComboLeg\"][]; }; ListCombosResponse: { /** @description List of combo contracts matching the query */ combos: components[\"schemas\"][\"ComboResponse\"][]; pagination: components[\"schemas\"][\"Pagination\"]; }; /** @description A canonical combo definition. The authenticated account is derived from the signed request and is not a request field. */ CreateComboRequest: { /** @description Two to six distinct underlying contract legs. The service canonicalizes their complete set, so leg order does not create a distinct combo. */ legs: components[\"schemas\"][\"CreateComboLeg\"][]; }; CreateComboLeg: { /** * @description Underlying contract ID as a decimal string. * @example 101 */ contractId: string; /** * @description Required settlement outcome for this leg. * @example Yes * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; }; CreateComboResponse: { combo: components[\"schemas\"][\"ComboSummary\"]; /** @description `false` when this request created the canonical combo; `true` when the canonical combo already existed. */ alreadyExisted: boolean; }; ComboSummary: { /** * Format: int64 * @description Internal combo ID. * @example 456 */ id: bigint; /** * @description Canonical identity of the complete combo leg set. * @example 101:Yes|202:No */ canonicalLegKey: string; /** * Format: int32 * @description Number of legs in the combo. * @example 2 */ legCount: number; /** @description Human-readable combo name, when available. */ displayName?: string; /** @description Current combo status, when available. */ status?: string; /** * Format: int64 * @description Associated instrument ID, when available. */ instrumentId?: bigint; /** * @description Associated instrument symbol, when available. * @example GEMI-CMB-0526-A7F3B2C1D4E5 */ instrumentSymbol?: string; /** @description Whether the combo has been registered with an instrument symbol. */ instrumentRegistered: boolean; /** * Format: date-time * @description Latest expiry among the underlying legs, when available. */ latestExpiryDate?: string; /** * Format: date-time * @description Creation time, when available. */ createdAt?: string; /** * Format: date-time * @description Most recent update time, when available. */ updatedAt?: string; /** @description Canonically ordered combo legs. */ legs: components[\"schemas\"][\"ComboSummaryLeg\"][]; }; ComboSummaryLeg: { /** * Format: int64 * @description Parent combo ID. */ comboId: bigint; /** * Format: int32 * @description Zero-based leg position in canonical order. */ legIndex: number; /** @description Underlying contract ID as a decimal string. */ contractId: string; /** * @description Required settlement outcome for the leg. * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; /** * @description Settled outcome for the leg, when resolved. * @enum {string|null} */ legOutcome?: \"Yes\" | \"No\" | null; /** * Format: date-time * @description Resolution time for the leg, when resolved. */ resolvedAt?: string | null; /** @description Underlying contract metadata, when available. */ contract?: components[\"schemas\"][\"ContractMetadata\"]; }; ComboWriteError: { /** * @description Error class. * @example InvalidInput */ error: string; /** * @description Machine-readable code for validation or missing-leg errors, when available. * @example COMBO_VALIDATION_ERROR */ code?: string; /** * @description Human-readable error detail. * @example a combo needs 2-6 legs */ message: string; }; OrderRequest: { /** * @description Contract instrument symbol * @example GEMI-FEDJAN26-DN25 */ symbol: string; orderType: components[\"schemas\"][\"OrderType\"]; side: components[\"schemas\"][\"OrderSide\"]; /** * Format: decimal * @description Number of contracts * @example 100 */ quantity: string; /** * Format: decimal * @description Limit price (0-1 range) * @example 0.65 */ price: string; /** * Format: decimal * @description The price to trigger a stop-limit order (0-1 range). Only available for stop-limit orders. See [Stop-Limit Orders](#operation/placeOrder) above for `stopPrice`/`price` constraints. * @example 0.60 */ stopPrice?: string; outcome: components[\"schemas\"][\"Outcome\"]; timeInForce?: components[\"schemas\"][\"TimeInForce\"]; /** * @description Set to `true` to require maker-only behavior. If the order would immediately take liquidity, the order is cancelled instead of filling. * @default false */ makerOrCancel: boolean; }; PlaceOrderBatchRequest: { /** @description Orders to submit. Every entry is validated before any order is submitted. All orders use the account associated with the authenticated request. */ orders: components[\"schemas\"][\"OrderRequest\"][]; }; /** @description An accepted order returned for one batch entry. */ BatchOrderResponse: { /** * Format: int64 * @example 12345678 */ orderId: bigint; /** @description Hashed order ID; omitted when unavailable */ hashOrderId?: string; /** @description Client-provided order ID; omitted when unavailable */ clientOrderId?: string; /** @description Global order ID; omitted when unavailable */ globalOrderId?: string; /** @enum {string} */ status: \"open\" | \"filled\" | \"cancelled\" | \"closed\"; symbol: string; side: components[\"schemas\"][\"OrderSide\"]; outcome: components[\"schemas\"][\"Outcome\"]; orderType: components[\"schemas\"][\"OrderType\"]; /** @enum {string} */ timeInForce: \"good-til-cancel\" | \"immediate-or-cancel\" | \"fill-or-kill\" | \"maker-or-cancel\"; /** @description Original order quantity */ quantity: string; /** @description Amount filled so far */ filledQuantity: string; /** @description Amount remaining to fill */ remainingQuantity: string; /** @description Limit price */ price: string; /** @description Stop trigger price; omitted unless populated for a `stop-limit` order */ stopPrice?: string; /** @description Average price of fills; omitted when unavailable */ avgExecutionPrice?: string; /** Format: date-time */ createdAt: string; /** Format: date-time */ updatedAt: string; /** * Format: date-time * @description Cancellation time; omitted unless the order was cancelled */ cancelledAt?: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; /** @description Promotional cash reserved or applied to the order; omitted when unavailable */ promoCashApplied?: string; /** @description Cash reserved for the unfilled portion of a resting buy order; omitted when unavailable */ fundsOnHold?: string; }; PlaceOrderBatchSuccessResult: { order: components[\"schemas\"][\"BatchOrderResponse\"]; }; PlaceOrderBatchErrorResult: { /** * @description Error class for a rejected entry * @example InsufficientFunds */ error: string; /** * @description Human-readable detail for a rejected entry * @example Insufficient funds */ message: string; }; /** @description Exactly one outcome is present. Accepted entries contain `order`; rejected entries contain `error` and `message`. */ PlaceOrderBatchResult: components[\"schemas\"][\"PlaceOrderBatchSuccessResult\"] | components[\"schemas\"][\"PlaceOrderBatchErrorResult\"]; PlaceOrderBatchResponse: { /** @description One result for each submitted order, in request order. */ results: components[\"schemas\"][\"PlaceOrderBatchResult\"][]; }; CancelOrderBatchRequest: { /** @description Order IDs to cancel. Each ID may be an integer or a quoted numeric string. All IDs are validated before any cancellation is attempted. */ orderIds: (bigint | string)[]; }; CancelOrderBatchSuccessResult: { /** * Format: int64 * @description Order ID from the corresponding request entry. * @example 12345678 */ orderId: bigint; /** @enum {string} */ result: \"ok\"; }; CancelOrderBatchErrorResult: { /** * Format: int64 * @description Order ID from the corresponding request entry. * @example 12345678 */ orderId: bigint; /** * @description Error class for a rejected cancellation * @example OrderNotFound */ error: string; /** * @description Human-readable detail for a rejected cancellation * @example Order 12345678 not found */ message: string; }; /** @description Exactly one outcome is present. Successful entries contain `orderId` and `result`; rejected entries contain `orderId`, `error`, and `message`. */ CancelOrderBatchResult: components[\"schemas\"][\"CancelOrderBatchSuccessResult\"] | components[\"schemas\"][\"CancelOrderBatchErrorResult\"]; CancelOrderBatchResponse: { /** @description One result for each requested cancellation, in request order. */ results: components[\"schemas\"][\"CancelOrderBatchResult\"][]; }; OrderResponse: { /** * Format: int64 * @example 12345678 */ orderId?: bigint; hashOrderId?: string | null; clientOrderId?: string | null; globalOrderId?: string | null; status?: components[\"schemas\"][\"OrderStatus\"]; symbol?: string; side?: components[\"schemas\"][\"OrderSide\"]; outcome?: components[\"schemas\"][\"Outcome\"]; orderType?: components[\"schemas\"][\"OrderType\"]; /** @description Original order quantity */ quantity?: string; /** @description Amount filled so far */ filledQuantity?: string; /** @description Amount remaining to fill */ remainingQuantity?: string; /** @description Limit price */ price?: string; /** @description Stop trigger price (populated for `stop-limit` orders) */ stopPrice?: string | null; /** @description Average price of fills */ avgExecutionPrice?: string | null; /** Format: date-time */ createdAt?: string; /** Format: date-time */ updatedAt?: string; /** Format: date-time */ cancelledAt?: string | null; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; }; OrdersResponse: { orders?: components[\"schemas\"][\"OrderResponse\"][]; pagination?: components[\"schemas\"][\"PaginationSimple\"]; }; Position: { symbol?: string; /** Format: int64 */ instrumentId?: bigint; /** @description Total position size */ totalQuantity?: string; /** @description Quantity currently on hold from open orders */ quantityOnHold?: string; /** @description Average entry price */ avgPrice?: string; outcome?: components[\"schemas\"][\"Outcome\"]; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; prices?: components[\"schemas\"][\"PositionPrices\"]; /** @description Winning outcome (\"yes\" or \"no\") if the contract has resolved */ resolutionSide?: string | null; /** @description Whether the position is above the auto-start threshold */ isAboveAutoStartThreshold?: boolean; /** @description Whether the market is currently live/active */ isLive?: boolean; /** @description Realized profit/loss from sells */ realizedPl?: string | null; /** * @description Mark-to-market value of the position in USD at the current sell price (bestBid for YES, bestAsk for NO). **Absent** from the response when the held outcome has no live sell quote (no liquidity to sell into) — surface a no-liquidity state rather than a price the user cannot transact at. `lastTradePrice` is still returned for display. Treat as `Optional`. * @example 65.00 */ marketValue?: string; /** * @description Unrealized P&L in USD (`marketValue - costBasis`). **Absent** whenever `marketValue` is absent. Treat as `Optional`. * @example 12.50 */ unrealizedPnl?: string; /** * Format: double * @description Unrealized P&L as a percentage of cost basis. Expressed as a percent (e.g. `12.5` represents 12.5%, **not** `0.125`); rounded to 4 decimal places. **Absent** when there is no live sell quote, or when cost basis is zero. Treat as `Optional`. * @example 23.81 */ unrealizedPct?: number; }; /** @description Current bid/ask/last-trade prices for the contract */ PositionPrices: { buy: { yes?: string | null; no?: string | null; }; sell: { yes?: string | null; no?: string | null; }; bestBid?: string | null; bestAsk?: string | null; lastTradePrice?: string | null; } | null; PositionsResponse: { positions?: components[\"schemas\"][\"Position\"][]; /** @description Total number of positions (for pagination) */ total?: number | null; }; /** @description A historically settled position in a resolved prediction market contract. */ SettledPosition: { /** * Format: int64 * @description Account that held the position */ accountId?: bigint; /** * Format: int64 * @description Unique instrument identifier for the contract */ instrumentId?: bigint; /** * @description Contract instrument symbol * @example GEMI-FEDJAN26-DN25 */ instrumentSymbol?: string; /** * @description Signed position held at settlement. Positive values represent a `yes` position; negative values represent a `no` position. * @example 125 */ position?: string; /** * @description Absolute quantity held at settlement (unsigned) * @example 125 */ positionQuantity?: string; outcome?: components[\"schemas\"][\"Outcome\"]; /** * @description Payout received from settlement. `0` when the position lost. * @example 125.00 */ payout?: string; /** @description The winning outcome of the contract */ resolutionSide?: components[\"schemas\"][\"Outcome\"]; /** * Format: date-time * @description Settlement timestamp (ISO 8601) */ settledAt?: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; /** * @description Total amount spent to enter the position, net of any prior realized P&L from partial sells. Omitted when cost-basis data is not available. * @example 78.75 */ costBasis?: string | null; /** * @description Realized profit or loss recorded from sells prior to settlement. Omitted when not available. * @example 0 */ realizedPnl?: string | null; /** * @description Net profit for the position, computed as `payout - costBasis + realizedPnl`. Omitted when `costBasis` is not available. * @example 46.25 */ netProfit?: string | null; }; SettledPositionsResponse: { positions?: components[\"schemas\"][\"SettledPosition\"][]; /** @description Total number of settled positions across all pages for the current filter set. */ total?: number | null; /** @description Sum of `payout` across all settled positions in the filter set. Retained for binary back-compat with the legacy response shape; **field is absent (not `null`) on the unified backend** because computing a roll-up over the full filtered set would require a separate aggregate query (deferred until a partner asks). Play's default `OptionHandlers` omits absent `Option` fields rather than emitting `null`. */ totalPayout?: string; /** @description Sum of `costBasis` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ totalCostBasis?: string; /** @description Sum of `netProfit` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ totalNetProfit?: string; /** @description Cash-outs (early sells before contract resolution) in the same account-scoped time window as the returned page's settled positions. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. `positions[]` pagination is unaffected — `limit`/`offset` continue to scope `positions[]` only. */ cashOuts?: components[\"schemas\"][\"CashedOutPosition\"][]; /** * @description Sum of `cashOuts[].proceeds` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 120.00 */ totalCashOutProceeds?: string; /** * @description Sum of `cashOuts[].costBasis` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 100.00 */ totalCashOutCostBasis?: string; /** * @description Sum of `cashOuts[].netProfit` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 20.00 */ totalCashOutNetProfit?: string; }; /** @description A qualifying cash-out (early sell before contract resolution) with cost-basis context. Exposed only via the `withCashOuts=true` sibling array on `POST /v1/prediction-markets/positions/settled`. Distinct from `SettledPosition` — cash-outs don't have a `payout` or `resolutionSide` since the contract hadn't resolved when the user sold. */ CashedOutPosition: { /** * Format: int64 * @description Account that held the position. * @example 456 */ accountId: bigint; /** * Format: int64 * @description Contract instrument ID. * @example 16789219 */ instrumentId: bigint; /** * @description Contract instrument symbol. * @example GEMI-FEDJAN26-DN25 */ instrumentSymbol: string; /** * Format: date-time * @description Wall-clock timestamp when the cash-out order closed (ISO 8601). * @example 2026-05-15T14:30:00.000Z */ timestamp: string; /** * @description Quantity sold (cumulative filled quantity on the cash-out order). * @example 10 */ filledQuantity: string; /** * @description Always `sell` for cash-outs. * @example sell * @enum {string} */ side: \"sell\"; /** * @description Amount received from the sale in USD. For prediction sells, proceeds flow through `cash_balance` rather than `closed_orders.total_spend`, so the value is derived from position-balance snapshots before/after the fill. * @example 10.50 */ proceeds: string; /** * @description Cost basis allocated proportionally to the filled quantity (`(costBasisSpend / costBasisPositionBalance) * filledQuantity`). * @example 10.00 */ costBasis: string; /** * @description Realized P&L from this cash-out fill (`proceeds - costBasis`). Equals the ledger `realized_pl` delta on the position-balance row pair around the fill; falls back to `0` under transient market-data lag so a missing post-fill snapshot can't poison the page. * @example 0.50 */ netProfit: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; }; ContractShareVolume: { /** * @description Contract instrument symbol * @example GEMI-FED260318-CUT25 */ symbol?: string; /** * @description Total taker volume across all participants (in shares) * @example 94625 */ totalQty?: string; /** * @description The authenticated user's taker (aggressor) volume (in shares) * @example 1 */ userAggressorQty?: string | null; /** * @description The authenticated user's maker (resting) volume (in shares) * @example 0 */ userRestingQty?: string | null; }; VolumeMetricsResponse: { /** * @description The event ticker * @example FED260318 */ eventTicker?: string; contracts?: components[\"schemas\"][\"ContractShareVolume\"][]; }; PredictionMarketVolumeCategory: { /** * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. * @example [ * \"Sports\", * \"Football\", * \"Pro Football\" * ] */ categoryPath: string[]; /** @description Total volume for this category, including all descendant categories. */ volume: components[\"schemas\"][\"PredictionMarketVolumeDecimal\"]; }; PredictionMarketHourlyVolumeCategory: { /** * Format: date-time * @description Inclusive UTC start of this hourly period. * @example 2026-07-20T00:00:00Z */ periodStart: string; /** * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. * @example [ * \"Sports\", * \"Football\", * \"Pro Football\" * ] */ categoryPath: string[]; /** @description Total volume for this category in this hour, including all descendant categories. */ volume: components[\"schemas\"][\"PredictionMarketVolumeDecimal\"]; }; /** * @description Non-negative decimal string. Preserve it as a string to avoid floating-point precision loss. * @example 143567.25 */ PredictionMarketVolumeDecimal: string; MakerRebateRateRule: { /** * Format: int64 * @description Stable identifier for this rate rule. * @example 12 */ id: bigint; /** * Format: int32 * @description Portion of the maker fee that is rebated, in basis points (10000 bps = 100%). * @example 5000 */ rebate_multiplier_bps: number; /** * Format: date-time * @description ISO-8601 timestamp at which this rule becomes effective. Always present; in practice never `null`. * @example 2026-03-19T00:00:00Z */ effective_from: string | null; /** * @description Market category this rule applies to. When absent, the rule applies to all categories. * @example Crypto */ category?: string; /** * Format: date-time * @description ISO-8601 timestamp after which this rule is superseded. Omitted when the rule is still current. * @example 2026-04-19T00:00:00Z */ effective_to?: string; }; MakerRebateRatesResponse: { rate_rules: components[\"schemas\"][\"MakerRebateRateRule\"][]; }; MakerRebatePayout: { /** * Format: int64 * @description Stable payout identifier. * @example 9182 */ id: bigint; /** * @description Total qualifying maker volume contributing to this payout, in USD. * @example 12450.00 */ total_volume_usd: string; /** * @description Total rebate paid, in USD. * @example 6.23 */ total_rebate_usd: string; /** * Format: int32 * @description Number of qualifying maker fills that contributed to the payout. * @example 187 */ total_fill_count: number; /** * @description Payout status (e.g. `PENDING`, `PAID`). * @example PAID */ status: string; /** * Format: date-time * @description ISO-8601 timestamp at which the rebate was credited. Always present; `null` for payouts that have not yet been paid. * @example 2026-05-20T21:00:00Z */ paid_at: string | null; /** * Format: date-time * @description ISO-8601 timestamp at which the payout row was created. Always present. * @example 2026-05-20T20:55:12Z */ created_at: string | null; }; MakerRebatePayoutsResponse: { payouts: components[\"schemas\"][\"MakerRebatePayout\"][]; }; MakerRebateLifetimeSummary: { /** * @description Sum of `total_rebate_usd` across payouts in the window. * @example 152.40 */ total_earned_usd: string; /** * Format: int64 * @description Sum of qualifying maker fills across payouts in the window. * @example 4218 */ total_fill_count: bigint; /** * @description Sum of qualifying maker volume (USD) across payouts in the window. * @example 304800.00 */ total_volume_usd: string; /** * Format: int32 * @description Number of payouts in the window. Always present; `0` when no payouts exist in the window. * @example 27 */ payout_count: number; /** * Format: date * @description Date of the earliest payout in the window, or `null` if no payouts exist. * @example 2026-03-19 */ first_payout_date: string | null; /** * Format: date * @description Date of the most recent payout in the window, or `null` if no payouts exist. * @example 2026-05-20 */ last_payout_date: string | null; }; LiquidityRewardsConfig: { /** * Format: int32 * @description Quotes wider than this spread score zero in the scoring algorithm. Only present when `enabled` is `true`. * @example 10 */ max_spread_cents?: number; /** * @description Daily reward amounts below this threshold are suppressed (sub-threshold accounts get no row at all). Only present when `enabled` is `true`. * @example 1.00 */ min_payout_threshold_usd?: string; /** * @description True when the program is fully configured upstream. When false, the response collapses to `{ \"enabled\": false }` only. * @example true */ enabled: boolean; }; LiquidityRewardEvent: { /** * @description Event ticker (e.g. `BTC2605202100`). * @example BTC2605202100 */ event_ticker: string; /** * @description Event title. * @example BTC above $95,000? */ title: string; /** * @description Market category. * @example Crypto */ category: string; /** * @description Daily USD reward pool budgeted for this event. * @example 500.00 */ daily_pool_usd: string; /** * @description Whether the pool came from a per-event override or the category default. * @example event_override * @enum {string} */ pool_source: \"event_override\" | \"category_default\" | \"unspecified\"; /** * Format: date-time * @description ISO-8601 timestamp at which the event ends and stops scoring. `null` when the underlying event has no end timestamp set. * @example 2026-05-20T21:00:00Z */ ends_at: string | null; /** * Format: int32 * @description Number of accounts that met qualifying-maker criteria in the most recent snapshot window for this event. * @example 14 */ qualifying_maker_count: number; /** * @description Optional URL for the event icon. Omitted when not configured. * @example https://example.com/btc.png */ icon_url?: string; }; LiquidityRewardsEventsResponse: { events: components[\"schemas\"][\"LiquidityRewardEvent\"][]; pagination: components[\"schemas\"][\"Pagination\"]; /** * Format: date * @description Most recent date for which scoring has been written. `null` when no scoring has run yet. * @example 2026-05-19 */ last_score_date: string | null; }; LiquidityEventScore: { /** * Format: int64 * @description Stable event identifier. * @example 1234567890 */ event_id: bigint; /** * @description Event title. * @example BTC above $95,000? */ event_name: string; /** * @description Market category. * @example Crypto */ category_name: string; /** * @description This account's normalized score for the event on the scoring date (0-1 range as a decimal string). * @example 0.4521 */ normalized_score: string; /** * Format: int32 * @description Number of snapshots in which this account had a qualifying quote. * @example 1180 */ snapshot_count: number; /** * Format: int32 * @description Total snapshots taken for the event on the scoring date. * @example 1440 */ total_snapshots: number; /** * @description Portion of the day's total reward attributed to this event. * @example 8.20 */ event_reward_usd: string; }; LiquidityDailySummary: { /** * Format: date * @description Date the payout applies to (Eastern Time). * @example 2026-05-07 */ payout_date: string; /** * @description Total USD reward for the day across all events the account scored on. * @example 12.45 */ total_reward_usd: string; /** * @description Status of the day's payout (e.g. `PENDING`, `PAID`, `ZERO_AMOUNT`). * @example PAID */ payout_status: string; /** * Format: date-time * @description ISO-8601 timestamp the day's payout was credited. Always present; `null` if not yet paid. * @example 2026-05-08T21:00:00Z */ paid_at: string | null; /** @description Per-event score breakdown showing how the day's total was distributed. */ events: components[\"schemas\"][\"LiquidityEventScore\"][]; }; LiquidityRewardsDailySummaryResponse: { daily_summaries: components[\"schemas\"][\"LiquidityDailySummary\"][]; }; LiquidityRewardsLifetimeSummary: { /** * @description Sum of `total_reward_usd` across daily payouts in the window. * @example 84.20 */ total_earned_usd: string; /** * Format: int32 * @description Number of daily payouts in the window. Always present; `0` when no payouts exist in the window. * @example 12 */ payout_count: number; /** * Format: date * @description Date of the earliest payout in the window, or `null` if no payouts exist. * @example 2026-05-08 */ first_payout_date: string | null; /** * Format: date * @description Date of the most recent payout in the window, or `null` if no payouts exist. * @example 2026-05-20 */ last_payout_date: string | null; }; }; responses: { /** @description Invalid request parameters */ BadRequest: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Authentication required or invalid credentials */ Unauthorized: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Internal server error */ InternalError: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Prediction markets feature is temporarily unavailable */ ServiceUnavailable: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; }; parameters: { /** @description Maximum number of results to return (max 500) */ Limit: number; /** @description Number of results to skip for pagination */ Offset: number; /** @description Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. */ SportFilter: components[\"schemas\"][\"SportsMarketSport\"][]; /** @description Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketTypeFilter: components[\"schemas\"][\"SportsMarketType\"][]; /** @description Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketSubjectFilter: components[\"schemas\"][\"SportsMarketSubject\"][]; /** @description Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketScopeFilter: components[\"schemas\"][\"SportsMarketScopeType\"][]; /** @description Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketMetricFilter: components[\"schemas\"][\"SportsMarketMetric\"][]; }; requestBodies: never; headers: never; pathItems: never; }" + "declaration": "export interface components { schemas: { Error: { /** * @description Error code * @example InvalidInput */ error?: string; /** * @description Human-readable error message * @example orderId is required */ message?: string; }; PredictionMarketsError: { /** * @description Prediction Markets error class * @example InvalidInput */ error: string; /** * @description Request field associated with the error, when available * @example orders */ field?: string; /** * @description Human-readable error detail, when available * @example orders must contain between 1 and 20 entries */ message?: string; }; AuthErrorResponse: { /** @enum {string} */ result: \"error\"; /** * @description Authentication or authorization error class * @example MissingNonce */ reason: string; /** * @description Human-readable authentication or authorization detail * @example Must provide unique monotonic increasing 'nonce' field in payload */ message: string; }; AccountGroupBlockedError: { /** @enum {string} */ error: \"This account is not permitted to trade prediction markets\"; /** @enum {string} */ code: \"ACCOUNT_GROUP_BLOCKED\"; }; TermsNotAcceptedError: { /** @enum {string} */ error: \"TERMS_NOT_ACCEPTED\"; /** @enum {string} */ message: \"Prediction markets terms must be accepted before placing orders\"; }; RestrictedSellOnlyError: { /** @enum {string} */ error: \"ACCOUNT_RESTRICTED_SELL_ONLY\"; /** @enum {string} */ message: \"Your account is restricted to selling existing positions; buying is not permitted.\"; }; PredictionMarketsTerms: { /** * @description Terms type identifier * @example PredictionsMarket */ termsType: string; /** * @description Latest terms version * @example 3 */ version: number; /** * @description Terms content to display before acceptance * @example These are the prediction market terms. */ content: string; /** * Format: date-time * @description UTC timestamp when the terms content was last updated * @example 2026-05-18T17:00:00Z */ updatedAt: string; }; PredictionMarketsTermsStatus: { /** * @description Whether the account group has accepted the latest configured Prediction Markets terms * @example false */ hasAcceptedLatest: boolean; /** * @description Latest terms version accepted by the account group, if any * @example 2 */ acceptedVersion?: number | null; /** * @description Latest configured Prediction Markets terms version, if available * @example 3 */ latestVersion?: number | null; }; AcceptPredictionMarketsTermsResponse: { /** @example true */ success: boolean; }; /** * @description Status of a prediction market * @enum {string} */ MarketStatus: \"approved\" | \"active\" | \"closed\" | \"under_review\" | \"settled\" | \"invalid\"; /** * @description Type of prediction market * @enum {string} */ MarketType: \"binary\" | \"categorical\"; /** * @description Sport whose rules give the market's scope and metric their sport-specific meaning. * @enum {string} */ SportsMarketSport: \"american_football\" | \"athletics\" | \"australian_rules_football\" | \"baseball\" | \"basketball\" | \"boxing\" | \"chess\" | \"cricket\" | \"cycling\" | \"darts\" | \"esports\" | \"golf\" | \"hockey\" | \"lacrosse\" | \"mixed_martial_arts\" | \"motorsports\" | \"rugby\" | \"sailing\" | \"soccer\" | \"tennis\"; /** * @description Conventional sports-market family. `subject`, `scope`, and `metric` provide detail within the family. This classification is independent of the event's structural `type` (`binary` or `categorical`). * @enum {string} */ SportsMarketType: \"moneyline\" | \"spread\" | \"total\" | \"prop\" | \"correct_score\" | \"to_advance\" | \"futures\" | \"other\"; /** * @description What the market is about. `participant` covers non-player entrants such as drivers and horses. * @enum {string} */ SportsMarketSubject: \"contest\" | \"team\" | \"player\" | \"participant\" | \"other\"; /** * @description Unit covered by the market. `full_contest` follows the market's official final-result rules; `regulation` covers scheduled regulation play only. Ordinal and range qualifiers are represented separately on `SportsMarketScope`. * @enum {string} */ SportsMarketScopeType: \"full_contest\" | \"regulation\" | \"half\" | \"quarter\" | \"period\" | \"inning\" | \"team_innings\" | \"over\" | \"powerplay\" | \"set\" | \"game\" | \"round\" | \"hole\" | \"match_day\" | \"session\" | \"super_over\" | \"race\" | \"sprint\" | \"qualifying\" | \"practice\" | \"stage\" | \"lap\" | \"series\" | \"season\" | \"tournament\" | \"competition\" | \"other\"; /** * @description Statistic measured by the market. Interpret shared metric names using `sportsMarket.sport`. * @enum {string} */ SportsMarketMetric: \"aces\" | \"assists\" | \"balls_faced\" | \"birdies\" | \"blocked_shots\" | \"blocks\" | \"bogeys\" | \"boundaries\" | \"break_points_won\" | \"cards\" | \"catches\" | \"clean_sheets\" | \"completed_passes\" | \"control_time\" | \"corners\" | \"defensive_rebounds\" | \"double_double\" | \"double_faults\" | \"doubles\" | \"eagles\" | \"earned_runs\" | \"errors\" | \"faceoff_wins\" | \"fairways_hit\" | \"fantasy_points\" | \"fastest_lap\" | \"field_goals_made\" | \"finishing_position\" | \"fouls\" | \"fours\" | \"free_throws_made\" | \"fumbles\" | \"games\" | \"goals\" | \"goals_allowed\" | \"greens_in_regulation\" | \"grid_position\" | \"hits\" | \"hits_allowed\" | \"hits_runs_rbis\" | \"holes_in_one\" | \"home_runs\" | \"innings_pitched\" | \"interceptions_thrown\" | \"kicking_points\" | \"knockdowns\" | \"laps_completed\" | \"laps_led\" | \"lap_time\" | \"longest_pass_completion\" | \"longest_reception\" | \"longest_rush\" | \"maiden_overs\" | \"offensive_rebounds\" | \"offsides\" | \"pars\" | \"passes\" | \"pass_attempts\" | \"pass_completions\" | \"passing_touchdowns\" | \"passing_yards\" | \"penalty_minutes\" | \"pitching_outs_recorded\" | \"pit_stops\" | \"points\" | \"points_assists\" | \"points_rebounds\" | \"points_rebounds_assists\" | \"positions_gained\" | \"power_play_points\" | \"putts\" | \"qualifying_position\" | \"rebounds\" | \"rebounds_assists\" | \"receiving_touchdowns\" | \"receiving_yards\" | \"receptions\" | \"red_cards\" | \"retirements\" | \"rounds\" | \"runs\" | \"runs_batted_in\" | \"runs_conceded\" | \"rush_attempts\" | \"rushing_touchdowns\" | \"rushing_yards\" | \"sacks\" | \"safety_cars\" | \"saves\" | \"sets\" | \"shots\" | \"shots_on_goal\" | \"shots_on_target\" | \"shutouts\" | \"significant_strikes\" | \"singles\" | \"sixes\" | \"steals\" | \"stolen_bases\" | \"strokes\" | \"strikeouts\" | \"submission_attempts\" | \"tackles\" | \"takedowns\" | \"three_pointers_made\" | \"tiebreaks_won\" | \"total_bases\" | \"total_points_won\" | \"total_strikes\" | \"touchdowns\" | \"triples\" | \"triple_double\" | \"turnovers\" | \"walks\" | \"wickets\" | \"wins\" | \"yellow_cards\" | \"other\"; /** @description Settlement scope. `ordinal` identifies one unit; `start` and `end` identify an inclusive range of units. */ SportsMarketScope: { type: components[\"schemas\"][\"SportsMarketScopeType\"]; /** * Format: int32 * @description Optional ordinal within the scope type, such as half `1` or quarter `4`. */ ordinal?: number; /** * Format: int32 * @description Optional inclusive start of a scope range, such as inning `1`. */ start?: number; /** * Format: int32 * @description Optional inclusive end of a scope range, such as inning `5`. */ end?: number; }; /** @description Atomic sports-market classification shared by every contract grouped under the event. Present only for sports events. All fields except `metric` are required together. */ SportsMarket: { sport: components[\"schemas\"][\"SportsMarketSport\"]; type: components[\"schemas\"][\"SportsMarketType\"]; subject: components[\"schemas\"][\"SportsMarketSubject\"]; scope: components[\"schemas\"][\"SportsMarketScope\"]; metric?: components[\"schemas\"][\"SportsMarketMetric\"]; }; /** * @description Order type. `stop-limit` orders require a `stopPrice` that triggers a limit order at `price` when the market reaches the trigger. * @enum {string} */ OrderType: \"limit\" | \"stop-limit\"; /** @enum {string} */ OrderSide: \"buy\" | \"sell\"; /** * @description The outcome being traded (Yes or No) * @enum {string} */ Outcome: \"yes\" | \"no\"; /** * @description Order execution behavior: * - `good-til-cancel` - Order remains active until filled or cancelled (default) * - `immediate-or-cancel` - Fill immediately or cancel remaining * - `fill-or-kill` - Fill entire order immediately or cancel * @default good-til-cancel * @enum {string} */ TimeInForce: \"good-til-cancel\" | \"immediate-or-cancel\" | \"fill-or-kill\"; /** @enum {string} */ OrderStatus: \"open\" | \"filled\" | \"cancelled\"; /** @enum {string} */ PositionStatus: \"active\" | \"resolved\" | \"cancelled\"; Pagination: { /** @example 50 */ limit?: number; /** @example 0 */ offset?: number; /** @example 100 */ total?: number; }; PaginationSimple: { limit?: number; offset?: number; /** @description Number of items in current response */ count?: number; }; OrderBook: { bids?: components[\"schemas\"][\"OrderBookEntry\"][]; asks?: components[\"schemas\"][\"OrderBookEntry\"][]; }; OrderBookEntry: { side?: components[\"schemas\"][\"OrderSide\"]; /** @example 0.65 */ price?: string; /** @example 1000 */ quantity?: string; }; OrderBookDepth: { bids?: components[\"schemas\"][\"OrderBookLevel\"][]; asks?: components[\"schemas\"][\"OrderBookLevel\"][]; /** Format: date-time */ lastUpdateTime?: string; }; OrderBookLevel: { price?: string; quantity?: string; orderCount?: number; }; /** @description Contract quantity and price validation is instrument-specific. Clients must validate order quantities and prices against the returned increment and minimum fields rather than assuming a fixed grid. */ Contract: { totalShares?: string | null; id?: string; /** @description Human-readable label for the contract's YES-space proposition (e.g., \"SOL > $90\") */ label?: string; /** @description Short form label (e.g., \">$90\") */ abbreviatedName?: string | null; /** @description Rich text description */ description?: Record; prices?: components[\"schemas\"][\"ContractPrices\"]; color?: string | null; status?: components[\"schemas\"][\"MarketStatus\"]; imageUrl?: string | null; priceHistory?: components[\"schemas\"][\"PricePoint\"][] | null; /** Format: date-time */ createdAt?: string; /** Format: date-time */ expiryDate?: string | null; resolutionSide?: components[\"schemas\"][\"Outcome\"]; /** Format: date-time */ resolvedAt?: string | null; termsAndConditionsUrl?: string; ticker?: string; instrumentSymbol?: string; /** @description Contract quantity grid from instrument refdata (for example, \"0.01\"). */ quantityIncrement?: string | null; /** @description Minimum contract quantity from instrument refdata (for example, \"1.00\"). */ quantityMinimum?: string | null; /** @description Contract price grid from instrument refdata (for example, \"0.0001\"). */ priceIncrement?: string | null; /** @description Decimal places supported by the instrument's quote asset. */ quoteAssetPrecision?: number | null; /** @description Minimum contract price and anchor for the instrument price grid (for example, \"0.0001\"). */ priceMinimum?: string | null; /** Format: date-time */ effectiveDate?: string | null; /** * @description Trading state of the contract * @enum {string|null} */ marketState?: \"open\" | \"closed\" | null; /** @description Display order within the event */ sortOrder?: number | null; strike?: components[\"schemas\"][\"Strike\"]; /** * @deprecated * @description Deprecated: use the event-level `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation (e.g., \"GRR-KAIKO_BTCUSD_60S\"). Present for crypto Up/Down contracts. * @example GRR-KAIKO_BTCUSD_60S */ source?: string | null; /** * @description The observed settlement price. Only present after the contract is settled. * @example 87654.32 */ settlementValue?: string | null; }; /** * @description Strike or condition inequality type for contract threshold evaluation. - `reference`: Crypto Up/Down reference strike price captured at `availableAt` time. - `above`: Higher/Lower contract threshold. - `spread`: Point, run, or goal handicap spread line. - `over`: Total or prop threshold evaluated as strict greater than (`>`). - `over_or_equal`: Total or prop threshold evaluated as greater than or equal to (`>=`). - `under`: Total or prop threshold evaluated as strict less than (`<`). - `under_or_equal`: Position, rank, or total threshold evaluated as less than or equal to (`<=`). * @example spread * @enum {string} */ StrikeType: \"reference\" | \"above\" | \"spread\" | \"over\" | \"over_or_equal\" | \"under\" | \"under_or_equal\"; /** @description Strike price or contract threshold information for Up/Down crypto contracts and sports prediction market contracts. */ Strike: { /** * @description The strike price value. Null for \"reference\" type strikes where the value is determined at availableAt time. For sports contracts, this represents the derived numeric strike value (e.g. spread margin, total line, or position/rank threshold). * @example 87500.00 */ value?: string | null; type?: components[\"schemas\"][\"StrikeType\"]; /** * Format: date-time * @description When the strike price becomes available * @example 2026-03-27T19:45:00.000Z */ availableAt?: string | null; }; /** @description Structured data source information for price observation. Replaces the deprecated flat `source` string on the event and contract. Present for crypto Up/Down events. Both fields are omitted when not available. */ SourceDetails: { /** * @description The data provider / vendor name. * @example Kaiko */ agency?: string | null; /** * @description The specific data feed identifier (the value previously carried by the flat `source` field). * @example GRR-KAIKO_BTCUSD_60S */ index?: string | null; } | null; PricePoint: { /** Format: date-time */ timestamp?: string; price?: string; }; /** @description Current bid/ask pricing for the contract */ ContractPrices: { /** @description Buy prices for each outcome */ buy?: { /** * @description Price to buy YES outcome * @example 0.42 */ yes?: string; /** * @description Price to buy NO outcome * @example 0.58 */ no?: string; }; /** @description Sell prices for each outcome */ sell?: { /** * @description Price to sell YES outcome * @example 0.42 */ yes?: string; /** * @description Price to sell NO outcome * @example 0.58 */ no?: string; }; /** * @description Highest buy offer * @example 0.49 */ bestBid?: string | null; /** * @description Lowest sell offer * @example 0.54 */ bestAsk?: string | null; /** * @description Most recent transaction price * @example 0.75 */ lastTradePrice?: string | null; } | null; /** @description A prediction market event containing one or more tradeable contracts */ Event: { id?: string; /** @example Will Bitcoin reach $100k by end of 2028? */ title?: string; /** @example bitcoin-100k-2028 */ slug?: string; description?: string | null; imageUrl?: string | null; type?: components[\"schemas\"][\"MarketType\"]; /** @example crypto */ category?: string; series?: string | null; sportsMarket?: components[\"schemas\"][\"SportsMarket\"]; /** * @description The event ticker (e.g., \"BTC100K2028\") * @example BTC100K2028 */ ticker?: string; status?: components[\"schemas\"][\"MarketStatus\"]; /** Format: date-time */ resolvedAt?: string | null; /** Format: date-time */ createdAt?: string; /** @description Tradeable contracts within this event */ contracts?: components[\"schemas\"][\"Contract\"][]; contractOrderbooks?: { [key: string]: components[\"schemas\"][\"OrderBook\"]; }; /** * @description Total trading volume in USD * @example 125000.00 */ volume?: string; /** * @description Total liquidity in USD * @example 50000.00 */ liquidity?: string; tags?: string[] | null; /** Format: date-time */ effectiveDate?: string; /** Format: date-time */ expiryDate?: string | null; subcategory?: components[\"schemas\"][\"Subcategory\"]; /** * @deprecated * @description Deprecated: use `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation. Aggregated from contracts for crypto Up/Down events. * @example GRR-KAIKO_BTCUSD_60S */ source?: string | null; sourceDetails?: components[\"schemas\"][\"SourceDetails\"]; settlement?: components[\"schemas\"][\"Settlement\"]; }; /** @description Nested category information for the event */ Subcategory: { /** * @description Category identifier * @example 35 */ id?: number; /** * @description URL-friendly category identifier * @example crypto_solana */ slug?: string; /** * @description Display name * @example Solana */ name?: string; /** * @description Category hierarchy path * @example [ * \"Crypto\", * \"Solana\" * ] */ path?: string[]; } | null; /** @description Settlement information for resolved events */ Settlement: { /** * @description The observed settlement value (e.g., the price at expiry for crypto contracts) * @example 87654.32 */ value?: string | null; }; EventsResponse: { data?: components[\"schemas\"][\"Event\"][]; pagination?: components[\"schemas\"][\"Pagination\"]; }; ContractMetadata: { contractId?: string; contractName?: string; contractTicker?: string; eventTicker?: string; eventName?: string; category?: string; contractStatus?: string; /** @description Event type (\"binary\" or \"categorical\") */ eventType?: string; /** Format: date-time */ expiryDate?: string | null; /** Format: date-time */ resolvedAt?: string | null; /** @description Winning outcome if resolved (\"yes\" or \"no\") */ resolutionSide?: string | null; /** @description Parent event ticker for sub-events */ parentEventTicker?: string | null; /** * Format: date-time * @description Start datetime (ISO 8601) */ startTime?: string | null; }; ComboLeg: { /** * Format: int64 * @description Internal ID of the parent combo contract * @example 456 */ comboId: bigint; /** * @description Zero-based position of this leg in the combo * @example 0 */ legIndex: number; /** * @description Internal ID of the underlying single contract, represented as a decimal string * @example 101 */ contractId: string; /** * @description The outcome this leg must settle for the combo to settle YES * @example Yes * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; /** * @description The outcome this leg has settled to, if resolved (`\"Yes\"` or `\"No\"`). Null while the leg is still active. * @example null */ legOutcome?: string | null; /** * Format: date-time * @description UTC timestamp when this leg resolved. Null while still active. * @example null */ resolvedAt?: string | null; /** @description Full metadata for the underlying single contract */ contract?: components[\"schemas\"][\"ContractMetadata\"] | null; }; ComboResponse: { /** @description Metadata for the combo contract itself (ticker, status, expiry, etc.) */ contract: components[\"schemas\"][\"ContractMetadata\"]; /** @description Ordered list of legs that make up this combo */ legs: components[\"schemas\"][\"ComboLeg\"][]; }; ListCombosResponse: { /** @description List of combo contracts matching the query */ combos: components[\"schemas\"][\"ComboResponse\"][]; pagination: components[\"schemas\"][\"Pagination\"]; }; /** @description A canonical combo definition. The authenticated account is derived from the signed request and is not a request field. */ CreateComboRequest: { /** @description Two to six distinct underlying contract legs. The service canonicalizes their complete set, so leg order does not create a distinct combo. */ legs: components[\"schemas\"][\"CreateComboLeg\"][]; }; CreateComboLeg: { /** * @description Underlying contract ID as a decimal string. * @example 101 */ contractId: string; /** * @description Required settlement outcome for this leg. * @example Yes * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; }; CreateComboResponse: { combo: components[\"schemas\"][\"ComboSummary\"]; /** @description `false` when this request created the canonical combo; `true` when the canonical combo already existed. */ alreadyExisted: boolean; }; ComboSummary: { /** * Format: int64 * @description Internal combo ID. * @example 456 */ id: bigint; /** * @description Canonical identity of the complete combo leg set. * @example 101:Yes|202:No */ canonicalLegKey: string; /** * Format: int32 * @description Number of legs in the combo. * @example 2 */ legCount: number; /** @description Human-readable combo name, when available. */ displayName?: string; /** @description Current combo status, when available. */ status?: string; /** * Format: int64 * @description Associated instrument ID, when available. */ instrumentId?: bigint; /** * @description Associated instrument symbol, when available. * @example GEMI-CMB-0526-A7F3B2C1D4E5 */ instrumentSymbol?: string; /** @description Whether the combo has been registered with an instrument symbol. */ instrumentRegistered: boolean; /** * Format: date-time * @description Latest expiry among the underlying legs, when available. */ latestExpiryDate?: string; /** * Format: date-time * @description Creation time, when available. */ createdAt?: string; /** * Format: date-time * @description Most recent update time, when available. */ updatedAt?: string; /** @description Canonically ordered combo legs. */ legs: components[\"schemas\"][\"ComboSummaryLeg\"][]; }; ComboSummaryLeg: { /** * Format: int64 * @description Parent combo ID. */ comboId: bigint; /** * Format: int32 * @description Zero-based leg position in canonical order. */ legIndex: number; /** @description Underlying contract ID as a decimal string. */ contractId: string; /** * @description Required settlement outcome for the leg. * @enum {string} */ requiredOutcome: \"Yes\" | \"No\"; /** * @description Settled outcome for the leg, when resolved. * @enum {string|null} */ legOutcome?: \"Yes\" | \"No\" | null; /** * Format: date-time * @description Resolution time for the leg, when resolved. */ resolvedAt?: string | null; /** @description Underlying contract metadata, when available. */ contract?: components[\"schemas\"][\"ContractMetadata\"]; }; ComboWriteError: { /** * @description Error class. * @example InvalidInput */ error: string; /** * @description Machine-readable code for validation or missing-leg errors, when available. * @example COMBO_VALIDATION_ERROR */ code?: string; /** * @description Human-readable error detail. * @example a combo needs 2-6 legs */ message: string; }; OrderRequest: { /** * @description Contract instrument symbol * @example GEMI-FEDJAN26-DN25 */ symbol: string; orderType: components[\"schemas\"][\"OrderType\"]; side: components[\"schemas\"][\"OrderSide\"]; /** * Format: decimal * @description Number of contracts * @example 100 */ quantity: string; /** * Format: decimal * @description Limit price (0-1 range) * @example 0.65 */ price: string; /** * Format: decimal * @description The price to trigger a stop-limit order (0-1 range). Only available for stop-limit orders. See [Stop-Limit Orders](#operation/placeOrder) above for `stopPrice`/`price` constraints. * @example 0.60 */ stopPrice?: string; outcome: components[\"schemas\"][\"Outcome\"]; timeInForce?: components[\"schemas\"][\"TimeInForce\"]; /** * @description Set to `true` to require maker-only behavior. If the order would immediately take liquidity, the order is cancelled instead of filling. * @default false */ makerOrCancel: boolean; }; PlaceOrderBatchRequest: { /** @description Orders to submit. Every entry is validated before any order is submitted. All orders use the account associated with the authenticated request. */ orders: components[\"schemas\"][\"OrderRequest\"][]; }; /** @description An accepted order returned for one batch entry. */ BatchOrderResponse: { /** * Format: int64 * @example 12345678 */ orderId: bigint; /** @description Hashed order ID; omitted when unavailable */ hashOrderId?: string; /** @description Client-provided order ID; omitted when unavailable */ clientOrderId?: string; /** @description Global order ID; omitted when unavailable */ globalOrderId?: string; /** @enum {string} */ status: \"open\" | \"filled\" | \"cancelled\" | \"closed\"; symbol: string; side: components[\"schemas\"][\"OrderSide\"]; outcome: components[\"schemas\"][\"Outcome\"]; orderType: components[\"schemas\"][\"OrderType\"]; /** @enum {string} */ timeInForce: \"good-til-cancel\" | \"immediate-or-cancel\" | \"fill-or-kill\" | \"maker-or-cancel\"; /** @description Original order quantity */ quantity: string; /** @description Amount filled so far */ filledQuantity: string; /** @description Amount remaining to fill */ remainingQuantity: string; /** @description Limit price */ price: string; /** @description Stop trigger price; omitted unless populated for a `stop-limit` order */ stopPrice?: string; /** @description Average price of fills; omitted when unavailable */ avgExecutionPrice?: string; /** Format: date-time */ createdAt: string; /** Format: date-time */ updatedAt: string; /** * Format: date-time * @description Cancellation time; omitted unless the order was cancelled */ cancelledAt?: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; /** @description Promotional cash reserved or applied to the order; omitted when unavailable */ promoCashApplied?: string; /** @description Cash reserved for the unfilled portion of a resting buy order; omitted when unavailable */ fundsOnHold?: string; }; PlaceOrderBatchSuccessResult: { order: components[\"schemas\"][\"BatchOrderResponse\"]; }; PlaceOrderBatchErrorResult: { /** * @description Error class for a rejected entry * @example InsufficientFunds */ error: string; /** * @description Human-readable detail for a rejected entry * @example Insufficient funds */ message: string; }; /** @description Exactly one outcome is present. Accepted entries contain `order`; rejected entries contain `error` and `message`. */ PlaceOrderBatchResult: components[\"schemas\"][\"PlaceOrderBatchSuccessResult\"] | components[\"schemas\"][\"PlaceOrderBatchErrorResult\"]; PlaceOrderBatchResponse: { /** @description One result for each submitted order, in request order. */ results: components[\"schemas\"][\"PlaceOrderBatchResult\"][]; }; CancelOrderBatchRequest: { /** @description Order IDs to cancel. Each ID may be an integer or a quoted numeric string. All IDs are validated before any cancellation is attempted. */ orderIds: (bigint | string)[]; }; CancelOrderBatchSuccessResult: { /** * Format: int64 * @description Order ID from the corresponding request entry. * @example 12345678 */ orderId: bigint; /** @enum {string} */ result: \"ok\"; }; CancelOrderBatchErrorResult: { /** * Format: int64 * @description Order ID from the corresponding request entry. * @example 12345678 */ orderId: bigint; /** * @description Error class for a rejected cancellation * @example OrderNotFound */ error: string; /** * @description Human-readable detail for a rejected cancellation * @example Order 12345678 not found */ message: string; }; /** @description Exactly one outcome is present. Successful entries contain `orderId` and `result`; rejected entries contain `orderId`, `error`, and `message`. */ CancelOrderBatchResult: components[\"schemas\"][\"CancelOrderBatchSuccessResult\"] | components[\"schemas\"][\"CancelOrderBatchErrorResult\"]; CancelOrderBatchResponse: { /** @description One result for each requested cancellation, in request order. */ results: components[\"schemas\"][\"CancelOrderBatchResult\"][]; }; OrderResponse: { /** * Format: int64 * @example 12345678 */ orderId?: bigint; hashOrderId?: string | null; clientOrderId?: string | null; globalOrderId?: string | null; status?: components[\"schemas\"][\"OrderStatus\"]; symbol?: string; side?: components[\"schemas\"][\"OrderSide\"]; outcome?: components[\"schemas\"][\"Outcome\"]; orderType?: components[\"schemas\"][\"OrderType\"]; /** @description Original order quantity */ quantity?: string; /** @description Amount filled so far */ filledQuantity?: string; /** @description Amount remaining to fill */ remainingQuantity?: string; /** @description Limit price */ price?: string; /** @description Stop trigger price (populated for `stop-limit` orders) */ stopPrice?: string | null; /** @description Average price of fills */ avgExecutionPrice?: string | null; /** Format: date-time */ createdAt?: string; /** Format: date-time */ updatedAt?: string; /** Format: date-time */ cancelledAt?: string | null; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; }; OrdersResponse: { orders?: components[\"schemas\"][\"OrderResponse\"][]; pagination?: components[\"schemas\"][\"PaginationSimple\"]; }; Position: { symbol?: string; /** Format: int64 */ instrumentId?: bigint; /** @description Total position size */ totalQuantity?: string; /** @description Quantity currently on hold from open orders */ quantityOnHold?: string; /** @description Average entry price */ avgPrice?: string; outcome?: components[\"schemas\"][\"Outcome\"]; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; prices?: components[\"schemas\"][\"PositionPrices\"]; /** @description Winning outcome (\"yes\" or \"no\") if the contract has resolved */ resolutionSide?: string | null; /** @description Whether the position is above the auto-start threshold */ isAboveAutoStartThreshold?: boolean; /** @description Whether the market is currently live/active */ isLive?: boolean; /** @description Realized profit/loss from sells */ realizedPl?: string | null; /** * @description Mark-to-market value of the position in USD at the current sell price (bestBid for YES, bestAsk for NO). **Absent** from the response when the held outcome has no live sell quote (no liquidity to sell into) — surface a no-liquidity state rather than a price the user cannot transact at. `lastTradePrice` is still returned for display. Treat as `Optional`. * @example 65.00 */ marketValue?: string; /** * @description Unrealized P&L in USD (`marketValue - costBasis`). **Absent** whenever `marketValue` is absent. Treat as `Optional`. * @example 12.50 */ unrealizedPnl?: string; /** * Format: double * @description Unrealized P&L as a percentage of cost basis. Expressed as a percent (e.g. `12.5` represents 12.5%, **not** `0.125`); rounded to 4 decimal places. **Absent** when there is no live sell quote, or when cost basis is zero. Treat as `Optional`. * @example 23.81 */ unrealizedPct?: number; }; /** @description Current bid/ask/last-trade prices for the contract */ PositionPrices: { buy: { yes?: string | null; no?: string | null; }; sell: { yes?: string | null; no?: string | null; }; bestBid?: string | null; bestAsk?: string | null; lastTradePrice?: string | null; } | null; PositionsResponse: { positions?: components[\"schemas\"][\"Position\"][]; /** @description Total number of positions (for pagination) */ total?: number | null; }; /** @description A historically settled position in a resolved prediction market contract. */ SettledPosition: { /** * Format: int64 * @description Account that held the position */ accountId?: bigint; /** * Format: int64 * @description Unique instrument identifier for the contract */ instrumentId?: bigint; /** * @description Contract instrument symbol * @example GEMI-FEDJAN26-DN25 */ instrumentSymbol?: string; /** * @description Signed position held at settlement. Positive values represent a `yes` position; negative values represent a `no` position. * @example 125 */ position?: string; /** * @description Absolute quantity held at settlement (unsigned) * @example 125 */ positionQuantity?: string; outcome?: components[\"schemas\"][\"Outcome\"]; /** * @description Payout received from settlement. `0` when the position lost. * @example 125.00 */ payout?: string; /** @description The winning outcome of the contract */ resolutionSide?: components[\"schemas\"][\"Outcome\"]; /** * Format: date-time * @description Settlement timestamp (ISO 8601) */ settledAt?: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; /** * @description Total amount spent to enter the position, net of any prior realized P&L from partial sells. Omitted when cost-basis data is not available. * @example 78.75 */ costBasis?: string | null; /** * @description Realized profit or loss recorded from sells prior to settlement. Omitted when not available. * @example 0 */ realizedPnl?: string | null; /** * @description Net profit for the position, computed as `payout - costBasis + realizedPnl`. Omitted when `costBasis` is not available. * @example 46.25 */ netProfit?: string | null; }; SettledPositionsResponse: { positions?: components[\"schemas\"][\"SettledPosition\"][]; /** @description Total number of settled positions across all pages for the current filter set. */ total?: number | null; /** @description Sum of `payout` across all settled positions in the filter set. Retained for binary back-compat with the legacy response shape; **field is absent (not `null`) on the unified backend** because computing a roll-up over the full filtered set would require a separate aggregate query (deferred until a partner asks). Play's default `OptionHandlers` omits absent `Option` fields rather than emitting `null`. */ totalPayout?: string; /** @description Sum of `costBasis` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ totalCostBasis?: string; /** @description Sum of `netProfit` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). */ totalNetProfit?: string; /** @description Cash-outs (early sells before contract resolution) in the same account-scoped time window as the returned page's settled positions. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. `positions[]` pagination is unaffected — `limit`/`offset` continue to scope `positions[]` only. */ cashOuts?: components[\"schemas\"][\"CashedOutPosition\"][]; /** * @description Sum of `cashOuts[].proceeds` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 120.00 */ totalCashOutProceeds?: string; /** * @description Sum of `cashOuts[].costBasis` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 100.00 */ totalCashOutCostBasis?: string; /** * @description Sum of `cashOuts[].netProfit` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. * @example 20.00 */ totalCashOutNetProfit?: string; }; /** @description A qualifying cash-out (early sell before contract resolution) with cost-basis context. Exposed only via the `withCashOuts=true` sibling array on `POST /v1/prediction-markets/positions/settled`. Distinct from `SettledPosition` — cash-outs don't have a `payout` or `resolutionSide` since the contract hadn't resolved when the user sold. */ CashedOutPosition: { /** * Format: int64 * @description Account that held the position. * @example 456 */ accountId: bigint; /** * Format: int64 * @description Contract instrument ID. * @example 16789219 */ instrumentId: bigint; /** * @description Contract instrument symbol. * @example GEMI-FEDJAN26-DN25 */ instrumentSymbol: string; /** * Format: date-time * @description Wall-clock timestamp when the cash-out order closed (ISO 8601). * @example 2026-05-15T14:30:00.000Z */ timestamp: string; /** * @description Quantity sold (cumulative filled quantity on the cash-out order). * @example 10 */ filledQuantity: string; /** * @description Always `sell` for cash-outs. * @example sell * @enum {string} */ side: \"sell\"; /** * @description Amount received from the sale in USD. For prediction sells, proceeds flow through `cash_balance` rather than `closed_orders.total_spend`, so the value is derived from position-balance snapshots before/after the fill. * @example 10.50 */ proceeds: string; /** * @description Cost basis allocated proportionally to the filled quantity (`(costBasisSpend / costBasisPositionBalance) * filledQuantity`). * @example 10.00 */ costBasis: string; /** * @description Realized P&L from this cash-out fill (`proceeds - costBasis`). Equals the ledger `realized_pl` delta on the position-balance row pair around the fill; falls back to `0` under transient market-data lag so a missing post-fill snapshot can't poison the page. * @example 0.50 */ netProfit: string; contractMetadata?: components[\"schemas\"][\"ContractMetadata\"]; }; ContractShareVolume: { /** * @description Contract instrument symbol * @example GEMI-FED260318-CUT25 */ symbol?: string; /** * @description Total taker volume across all participants (in shares) * @example 94625 */ totalQty?: string; /** * @description The authenticated user's taker (aggressor) volume (in shares) * @example 1 */ userAggressorQty?: string | null; /** * @description The authenticated user's maker (resting) volume (in shares) * @example 0 */ userRestingQty?: string | null; }; VolumeMetricsResponse: { /** * @description The event ticker * @example FED260318 */ eventTicker?: string; contracts?: components[\"schemas\"][\"ContractShareVolume\"][]; }; PredictionMarketVolumeCategory: { /** * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. * @example [ * \"Sports\", * \"Football\", * \"Pro Football\" * ] */ categoryPath: string[]; /** @description Total volume for this category, including all descendant categories. */ volume: components[\"schemas\"][\"PredictionMarketVolumeDecimal\"]; }; PredictionMarketHourlyVolumeCategory: { /** * Format: date-time * @description Inclusive UTC start of this hourly period. * @example 2026-07-20T00:00:00Z */ periodStart: string; /** * @description Display-name path from the top-level category to this category. It replaces recursive child nodes. * @example [ * \"Sports\", * \"Football\", * \"Pro Football\" * ] */ categoryPath: string[]; /** @description Total volume for this category in this hour, including all descendant categories. */ volume: components[\"schemas\"][\"PredictionMarketVolumeDecimal\"]; }; /** * @description Non-negative decimal string. Preserve it as a string to avoid floating-point precision loss. * @example 143567.25 */ PredictionMarketVolumeDecimal: string; MakerRebateRateRule: { /** * Format: int64 * @description Stable identifier for this rate rule. * @example 12 */ id: bigint; /** * Format: int32 * @description Portion of the maker fee that is rebated, in basis points (10000 bps = 100%). * @example 5000 */ rebate_multiplier_bps: number; /** * Format: date-time * @description ISO-8601 timestamp at which this rule becomes effective. Always present; in practice never `null`. * @example 2026-03-19T00:00:00Z */ effective_from: string | null; /** * @description Market category this rule applies to. When absent, the rule applies to all categories. * @example Crypto */ category?: string; /** * Format: date-time * @description ISO-8601 timestamp after which this rule is superseded. Omitted when the rule is still current. * @example 2026-04-19T00:00:00Z */ effective_to?: string; }; MakerRebateRatesResponse: { rate_rules: components[\"schemas\"][\"MakerRebateRateRule\"][]; }; MakerRebatePayout: { /** * Format: int64 * @description Stable payout identifier. * @example 9182 */ id: bigint; /** * @description Total qualifying maker volume contributing to this payout, in USD. * @example 12450.00 */ total_volume_usd: string; /** * @description Total rebate paid, in USD. * @example 6.23 */ total_rebate_usd: string; /** * Format: int32 * @description Number of qualifying maker fills that contributed to the payout. * @example 187 */ total_fill_count: number; /** * @description Payout status (e.g. `PENDING`, `PAID`). * @example PAID */ status: string; /** * Format: date-time * @description ISO-8601 timestamp at which the rebate was credited. Always present; `null` for payouts that have not yet been paid. * @example 2026-05-20T21:00:00Z */ paid_at: string | null; /** * Format: date-time * @description ISO-8601 timestamp at which the payout row was created. Always present. * @example 2026-05-20T20:55:12Z */ created_at: string | null; }; MakerRebatePayoutsResponse: { payouts: components[\"schemas\"][\"MakerRebatePayout\"][]; }; MakerRebateLifetimeSummary: { /** * @description Sum of `total_rebate_usd` across payouts in the window. * @example 152.40 */ total_earned_usd: string; /** * Format: int64 * @description Sum of qualifying maker fills across payouts in the window. * @example 4218 */ total_fill_count: bigint; /** * @description Sum of qualifying maker volume (USD) across payouts in the window. * @example 304800.00 */ total_volume_usd: string; /** * Format: int32 * @description Number of payouts in the window. Always present; `0` when no payouts exist in the window. * @example 27 */ payout_count: number; /** * Format: date * @description Date of the earliest payout in the window, or `null` if no payouts exist. * @example 2026-03-19 */ first_payout_date: string | null; /** * Format: date * @description Date of the most recent payout in the window, or `null` if no payouts exist. * @example 2026-05-20 */ last_payout_date: string | null; }; LiquidityRewardsConfig: { /** * Format: int32 * @description Quotes wider than this spread score zero in the scoring algorithm. Only present when `enabled` is `true`. * @example 10 */ max_spread_cents?: number; /** * @description Daily reward amounts below this threshold are suppressed (sub-threshold accounts get no row at all). Only present when `enabled` is `true`. * @example 1.00 */ min_payout_threshold_usd?: string; /** * @description True when the program is fully configured upstream. When false, the response collapses to `{ \"enabled\": false }` only. * @example true */ enabled: boolean; }; LiquidityRewardEvent: { /** * @description Event ticker (e.g. `BTC2605202100`). * @example BTC2605202100 */ event_ticker: string; /** * @description Event title. * @example BTC above $95,000? */ title: string; /** * @description Market category. * @example Crypto */ category: string; /** * @description Daily USD reward pool budgeted for this event. * @example 500.00 */ daily_pool_usd: string; /** * @description Whether the pool came from a per-event override or the category default. * @example event_override * @enum {string} */ pool_source: \"event_override\" | \"category_default\" | \"unspecified\"; /** * Format: date-time * @description ISO-8601 timestamp at which the event ends and stops scoring. `null` when the underlying event has no end timestamp set. * @example 2026-05-20T21:00:00Z */ ends_at: string | null; /** * Format: int32 * @description Number of accounts that met qualifying-maker criteria in the most recent snapshot window for this event. * @example 14 */ qualifying_maker_count: number; /** * @description Optional URL for the event icon. Omitted when not configured. * @example https://example.com/btc.png */ icon_url?: string; }; LiquidityRewardsEventsResponse: { events: components[\"schemas\"][\"LiquidityRewardEvent\"][]; pagination: components[\"schemas\"][\"Pagination\"]; /** * Format: date * @description Most recent date for which scoring has been written. `null` when no scoring has run yet. * @example 2026-05-19 */ last_score_date: string | null; }; LiquidityEventScore: { /** * Format: int64 * @description Stable event identifier. * @example 1234567890 */ event_id: bigint; /** * @description Event title. * @example BTC above $95,000? */ event_name: string; /** * @description Market category. * @example Crypto */ category_name: string; /** * @description This account's normalized score for the event on the scoring date (0-1 range as a decimal string). * @example 0.4521 */ normalized_score: string; /** * Format: int32 * @description Number of snapshots in which this account had a qualifying quote. * @example 1180 */ snapshot_count: number; /** * Format: int32 * @description Total snapshots taken for the event on the scoring date. * @example 1440 */ total_snapshots: number; /** * @description Portion of the day's total reward attributed to this event. * @example 8.20 */ event_reward_usd: string; }; LiquidityDailySummary: { /** * Format: date * @description Date the payout applies to (Eastern Time). * @example 2026-05-07 */ payout_date: string; /** * @description Total USD reward for the day across all events the account scored on. * @example 12.45 */ total_reward_usd: string; /** * @description Status of the day's payout (e.g. `PENDING`, `PAID`, `ZERO_AMOUNT`). * @example PAID */ payout_status: string; /** * Format: date-time * @description ISO-8601 timestamp the day's payout was credited. Always present; `null` if not yet paid. * @example 2026-05-08T21:00:00Z */ paid_at: string | null; /** @description Per-event score breakdown showing how the day's total was distributed. */ events: components[\"schemas\"][\"LiquidityEventScore\"][]; }; LiquidityRewardsDailySummaryResponse: { daily_summaries: components[\"schemas\"][\"LiquidityDailySummary\"][]; }; LiquidityRewardsLifetimeSummary: { /** * @description Sum of `total_reward_usd` across daily payouts in the window. * @example 84.20 */ total_earned_usd: string; /** * Format: int32 * @description Number of daily payouts in the window. Always present; `0` when no payouts exist in the window. * @example 12 */ payout_count: number; /** * Format: date * @description Date of the earliest payout in the window, or `null` if no payouts exist. * @example 2026-05-08 */ first_payout_date: string | null; /** * Format: date * @description Date of the most recent payout in the window, or `null` if no payouts exist. * @example 2026-05-20 */ last_payout_date: string | null; }; }; responses: { /** @description Invalid request parameters */ BadRequest: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Authentication required or invalid credentials */ Unauthorized: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Internal server error */ InternalError: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; /** @description Prediction markets feature is temporarily unavailable */ ServiceUnavailable: { headers: { [name: string]: unknown; }; content: { \"application/json\": components[\"schemas\"][\"Error\"]; }; }; }; parameters: { /** @description Maximum number of results to return (max 500) */ Limit: number; /** @description Number of results to skip for pagination */ Offset: number; /** @description Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. */ SportFilter: components[\"schemas\"][\"SportsMarketSport\"][]; /** @description Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketTypeFilter: components[\"schemas\"][\"SportsMarketType\"][]; /** @description Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketSubjectFilter: components[\"schemas\"][\"SportsMarketSubject\"][]; /** @description Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketScopeFilter: components[\"schemas\"][\"SportsMarketScopeType\"][]; /** @description Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. */ SportsMarketMetricFilter: components[\"schemas\"][\"SportsMarketMetric\"][]; }; requestBodies: never; headers: never; pathItems: never; }" }, { "kind": "InterfaceDeclaration", @@ -1801,7 +1811,7 @@ { "kind": "InterfaceDeclaration", "name": "RfqLeg", - "declaration": "export interface RfqLeg { c: string; o: AnonymousSchema_148; s?: string; }" + "declaration": "export interface RfqLeg { c: string; o: AnonymousSchema_145; s?: string; }" }, { "kind": "EnumDeclaration", @@ -1811,7 +1821,7 @@ { "kind": "InterfaceDeclaration", "name": "RfqPrivateDelivery", - "declaration": "export interface RfqPrivateDelivery { e: 'requestForQuote'; i: string; E: number | bigint; r: string; x: AnonymousSchema_153; S: RfqLifecycleState; q?: string; p?: string; sz?: string; qs?: RfqQuoteStatus; vu?: number | bigint; }" + "declaration": "export interface RfqPrivateDelivery { e: 'requestForQuote'; i: string; E: number | bigint; r: string; x: AnonymousSchema_150; S: RfqLifecycleState; q?: string; p?: string; sz?: string; qs?: RfqQuoteStatus; vu?: number | bigint; }" }, { "kind": "InterfaceDeclaration", @@ -1826,7 +1836,7 @@ { "kind": "InterfaceDeclaration", "name": "RfqSubmitQuoteParams", - "declaration": "export interface RfqSubmitQuoteParams { rfqId: string; price: string; quantity: string; validUntil?: number | bigint; }" + "declaration": "export interface RfqSubmitQuoteParams { rfqId: string; price: string; quantity: string; validUntil?: number | bigint; clientId?: string; }" }, { "kind": "InterfaceDeclaration", diff --git a/packages/sdk-typescript/scripts/generate-market-data.mjs b/packages/sdk-typescript/scripts/generate-market-data.mjs index 5f7e2157..01b28794 100644 --- a/packages/sdk-typescript/scripts/generate-market-data.mjs +++ b/packages/sdk-typescript/scripts/generate-market-data.mjs @@ -1,6 +1,6 @@ import { writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { discoverOperationInventory, @@ -10,47 +10,52 @@ import { } from "./openapi-rest-generator.mjs"; import { ownedOperationsForModule } from "./rest-operation-ownership.mjs"; -const PUBLISHED_SPEC_URL = "https://developer.gemini.com/specs/openapi/rest.yaml"; const BANNER = "// Generated from rest.yaml#Market Data. Do not edit.\n\n"; const scriptDir = dirname(fileURLToPath(import.meta.url)); -const specPath = process.argv[2] ?? PUBLISHED_SPEC_URL; -const outputDir = resolve(process.argv[3] ?? resolve(scriptDir, "../src/generated/market-data")); -const document = await loadOpenApiDocument(specPath); -const ownedOperations = ownedOperationsForModule( - discoverOperationInventory(document, { spec: "rest" }), - { module: "marketData", spec: "rest" }, -); -const { operations } = await generateOpenApiRestTypes({ - specPath, - outputDir, - banner: BANNER, - includeOperationIds: ownedOperations.map((operation) => operation.operationId), - operationResponseModes: Object.fromEntries(ownedOperations.map((operation) => [ - operation.operationId, - operation.responseMode, - ])), - fileResponseImportPath: "../../transport/http.js", - operationsConstName: "MARKET_DATA_OPERATIONS", - operationIdTypeName: "MarketDataOperationId", - operationTypesName: "MarketDataOperationTypes", - operationNamespace: "marketData", -}); -const methodNames = new Map(ownedOperations.map((operation) => [operation.operationId, operation.methodName])); - -await writeFile(resolve(outputDir, "rest.ts"), renderRestClient( - operations.map((operation) => ({ - ...operation, - methodName: methodNames.get(operation.operationId), - })), - { +async function generateMarketData({ specPath, outputDir }) { + const document = await loadOpenApiDocument(specPath); + const ownedOperations = ownedOperationsForModule( + discoverOperationInventory(document, { spec: "rest" }), + { module: "marketData", spec: "rest" }, + ); + const { operations } = await generateOpenApiRestTypes({ + specPath, + outputDir, banner: BANNER, - className: "MarketDataRest", + includeOperationIds: ownedOperations.map((operation) => operation.operationId), + operationResponseModes: Object.fromEntries(ownedOperations.map((operation) => [ + operation.operationId, + operation.responseMode, + ])), + fileResponseImportPath: "../../transport/http.js", operationsConstName: "MARKET_DATA_OPERATIONS", + operationIdTypeName: "MarketDataOperationId", operationTypesName: "MarketDataOperationTypes", - operationsImportPath: "./operations.js", - transportImportPath: "../../transport/http.js", - executorImportPath: "../../transport/rest-operation.js", - deadlineImportPath: "../../utils/deadline.js", - }, -)); + operationNamespace: "marketData", + }); + const methodNames = new Map(ownedOperations.map((operation) => [operation.operationId, operation.methodName])); + + await writeFile(resolve(outputDir, "rest.ts"), renderRestClient( + operations.map((operation) => ({ + ...operation, + methodName: methodNames.get(operation.operationId), + })), + { + banner: BANNER, + className: "MarketDataRest", + operationsConstName: "MARKET_DATA_OPERATIONS", + operationTypesName: "MarketDataOperationTypes", + operationsImportPath: "./operations.js", + transportImportPath: "../../transport/http.js", + executorImportPath: "../../transport/rest-operation.js", + deadlineImportPath: "../../utils/deadline.js", + }, + )); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const specPath = process.argv[2] ?? "rest"; + const outputDir = resolve(process.argv[3] ?? resolve(scriptDir, "../src/generated/market-data")); + await generateMarketData({ specPath, outputDir }); +} diff --git a/packages/sdk-typescript/scripts/generate-prediction-markets.mjs b/packages/sdk-typescript/scripts/generate-prediction-markets.mjs index eae38c90..9a0e3990 100644 --- a/packages/sdk-typescript/scripts/generate-prediction-markets.mjs +++ b/packages/sdk-typescript/scripts/generate-prediction-markets.mjs @@ -1,6 +1,6 @@ import { writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { discoverOperationInventory, @@ -10,53 +10,58 @@ import { } from "./openapi-rest-generator.mjs"; import { ownedOperationsForModule } from "./rest-operation-ownership.mjs"; -const PUBLISHED_SPEC_URL = "https://developer.gemini.com/specs/openapi/prediction-markets.yaml"; const BANNER = "// Generated from prediction-markets.yaml. Do not edit.\n\n"; const scriptDir = dirname(fileURLToPath(import.meta.url)); -const specPath = process.argv[2] ?? PUBLISHED_SPEC_URL; -const outputDir = resolve(process.argv[3] ?? resolve(scriptDir, "../src/generated")); -const document = await loadOpenApiDocument(specPath); -const inventory = discoverOperationInventory(document, { spec: "predictionMarkets" }); -const ownedOperations = ownedOperationsForModule( - inventory, - { module: "predictionMarkets", spec: "predictionMarkets" }, -); -const { operations } = await generateOpenApiRestTypes({ - document, - specPath, - outputDir, - banner: BANNER, - includeOperationIds: ownedOperations.length > 0 - ? ownedOperations.map((operation) => operation.operationId) - : undefined, - operationResponseModes: Object.fromEntries(ownedOperations.map((operation) => [ - operation.operationId, - operation.responseMode, - ])), - operationsConstName: "PREDICTION_MARKET_OPERATIONS", - operationIdTypeName: "PredictionMarketOperationId", - operationTypesName: "PredictionMarketOperationTypes", - operationNamespace: "predictionMarkets", -}); -const methodNames = new Map(ownedOperations.map((operation) => [ - operation.operationId, - operation.operationId === "acceptPredictionMarketsTerms" ? "acceptTerms" : operation.methodName, -])); - -await writeFile(resolve(outputDir, "rest.ts"), renderRestClient( - operations.map((operation) => ({ - ...operation, - methodName: methodNames.get(operation.operationId), - })), - { +export async function generatePredictionMarkets({ specPath, outputDir }) { + const document = await loadOpenApiDocument(specPath); + const inventory = discoverOperationInventory(document, { spec: "predictionMarkets" }); + const ownedOperations = ownedOperationsForModule( + inventory, + { module: "predictionMarkets", spec: "predictionMarkets" }, + ); + const { operations } = await generateOpenApiRestTypes({ + document, + specPath, + outputDir, banner: BANNER, - className: "PredictionMarketsRest", + includeOperationIds: ownedOperations.length > 0 + ? ownedOperations.map((operation) => operation.operationId) + : undefined, + operationResponseModes: Object.fromEntries(ownedOperations.map((operation) => [ + operation.operationId, + operation.responseMode, + ])), operationsConstName: "PREDICTION_MARKET_OPERATIONS", + operationIdTypeName: "PredictionMarketOperationId", operationTypesName: "PredictionMarketOperationTypes", - operationsImportPath: "./operations.js", - transportImportPath: "../transport/http.js", - executorImportPath: "../transport/rest-operation.js", - deadlineImportPath: "../utils/deadline.js", - }, -)); + operationNamespace: "predictionMarkets", + }); + const methodNames = new Map(ownedOperations.map((operation) => [ + operation.operationId, + operation.operationId === "acceptPredictionMarketsTerms" ? "acceptTerms" : operation.methodName, + ])); + + await writeFile(resolve(outputDir, "rest.ts"), renderRestClient( + operations.map((operation) => ({ + ...operation, + methodName: methodNames.get(operation.operationId), + })), + { + banner: BANNER, + className: "PredictionMarketsRest", + operationsConstName: "PREDICTION_MARKET_OPERATIONS", + operationTypesName: "PredictionMarketOperationTypes", + operationsImportPath: "./operations.js", + transportImportPath: "../transport/http.js", + executorImportPath: "../transport/rest-operation.js", + deadlineImportPath: "../utils/deadline.js", + }, + )); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const specPath = process.argv[2] ?? "predictionMarkets"; + const outputDir = resolve(process.argv[3] ?? resolve(scriptDir, "../src/generated")); + await generatePredictionMarkets({ specPath, outputDir }); +} diff --git a/packages/sdk-typescript/scripts/generate-rest-modules.mjs b/packages/sdk-typescript/scripts/generate-rest-modules.mjs index 9c15468b..a3893d29 100644 --- a/packages/sdk-typescript/scripts/generate-rest-modules.mjs +++ b/packages/sdk-typescript/scripts/generate-rest-modules.mjs @@ -1,6 +1,6 @@ import { writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { discoverOperationInventory, @@ -16,45 +16,53 @@ import { const scriptDir = dirname(fileURLToPath(import.meta.url)); const publishedSpecs = { - predictionMarkets: "https://developer.gemini.com/specs/openapi/prediction-markets.yaml", - rest: "https://developer.gemini.com/specs/openapi/rest.yaml", + predictionMarkets: "predictionMarkets", + rest: "rest", }; -const restSpecOverride = process.argv[2]; -const baseOutputDir = resolve(process.argv[3] ?? resolve(scriptDir, "../src/generated")); -const specPaths = { ...publishedSpecs }; -if (restSpecOverride) specPaths.rest = restSpecOverride; -const documents = new Map(); -const inventories = new Map(); -for (const spec of new Set(REST_OPERATION_OWNERSHIP.modules.map(({ generation }) => generation?.spec))) { - if (!spec) continue; - const specPath = specPaths[spec]; - if (!specPath) throw new Error(`No REST spec configured for ${spec}`); - const document = await loadOpenApiDocument(specPath); - documents.set(spec, document); - inventories.set(spec, discoverOperationInventory(document, { spec })); -} +export async function generateRestModules({ specPaths, baseOutputDir, writeSnapshot = true }) { + const documents = new Map(); + const inventories = new Map(); + for (const spec of new Set(REST_OPERATION_OWNERSHIP.modules.map(({ generation }) => generation?.spec))) { + if (!spec) continue; + const specPath = specPaths[spec]; + if (!specPath) throw new Error(`No REST spec configured for ${spec}`); + const document = await loadOpenApiDocument(specPath); + documents.set(spec, document); + inventories.set(spec, discoverOperationInventory(document, { spec })); + } + + const allOperations = [...inventories.values()].flat(); + validateRestOperationOwnership(allOperations); -const allOperations = [...inventories.values()].flat(); -validateRestOperationOwnership(allOperations); + for (const module of REST_OPERATION_OWNERSHIP.modules) { + const generation = module.generation; + if (!generation) continue; + await generateOpenApiRestModule({ + ...generation, + document: documents.get(generation.spec), + specPath: specPaths[generation.spec], + outputDir: resolve(baseOutputDir, generation.output), + operationNamespace: module.id, + ownedOperations: ownedOperationsForModule(inventories.get(generation.spec), { + module: module.id, + spec: generation.spec, + }), + }); + } -for (const module of REST_OPERATION_OWNERSHIP.modules) { - const generation = module.generation; - if (!generation) continue; - await generateOpenApiRestModule({ - ...generation, - document: documents.get(generation.spec), - specPath: specPaths[generation.spec], - outputDir: resolve(baseOutputDir, generation.output), - operationNamespace: module.id, - ownedOperations: ownedOperationsForModule(inventories.get(generation.spec), { - module: module.id, - spec: generation.spec, - }), - }); + if (writeSnapshot) { + await writeFile( + resolve(scriptDir, "rest-operation-ownership.snapshot.json"), + `${JSON.stringify(createRestOperationOwnershipReport(allOperations), null, 2)}\n`, + ); + } } -await writeFile( - resolve(scriptDir, "rest-operation-ownership.snapshot.json"), - `${JSON.stringify(createRestOperationOwnershipReport(allOperations), null, 2)}\n`, -); +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const restSpecOverride = process.argv[2]; + const baseOutputDir = resolve(process.argv[3] ?? resolve(scriptDir, "../src/generated")); + const specPaths = { ...publishedSpecs }; + if (restSpecOverride) specPaths.rest = restSpecOverride; + await generateRestModules({ specPaths, baseOutputDir }); +} diff --git a/packages/sdk-typescript/scripts/generate-rest-ownership.mjs b/packages/sdk-typescript/scripts/generate-rest-ownership.mjs index 2844f422..8349ff72 100644 --- a/packages/sdk-typescript/scripts/generate-rest-ownership.mjs +++ b/packages/sdk-typescript/scripts/generate-rest-ownership.mjs @@ -1,16 +1,23 @@ import { writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { discoverOperationInventory, loadOpenApiDocument } from "./openapi-rest-generator.mjs"; import { createRestOperationOwnershipReport } from "./rest-operation-ownership.mjs"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const specs = [ - ["predictionMarkets", "https://developer.gemini.com/specs/openapi/prediction-markets.yaml"], - ["rest", "https://developer.gemini.com/specs/openapi/rest.yaml"], + ["predictionMarkets", "predictionMarkets"], + ["rest", "rest"], ]; -const operations = (await Promise.all(specs.map(async ([spec, specPath]) => - discoverOperationInventory(await loadOpenApiDocument(specPath), { spec })))).flat(); -const snapshot = createRestOperationOwnershipReport(operations); -await writeFile(resolve(scriptDir, "rest-operation-ownership.snapshot.json"), `${JSON.stringify(snapshot, null, 2)}\n`); + +async function generateRestOwnership() { + const operations = (await Promise.all(specs.map(async ([spec, specPath]) => + discoverOperationInventory(await loadOpenApiDocument(specPath), { spec })))).flat(); + const snapshot = createRestOperationOwnershipReport(operations); + await writeFile(resolve(scriptDir, "rest-operation-ownership.snapshot.json"), `${JSON.stringify(snapshot, null, 2)}\n`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await generateRestOwnership(); +} diff --git a/packages/sdk-typescript/scripts/generate-ws-types.mjs b/packages/sdk-typescript/scripts/generate-ws-types.mjs index 93ce4a44..91e02795 100644 --- a/packages/sdk-typescript/scripts/generate-ws-types.mjs +++ b/packages/sdk-typescript/scripts/generate-ws-types.mjs @@ -4,7 +4,7 @@ // // Usage: node scripts/generate-ws-types.mjs [outputDir] [specPath] // outputDir defaults to src/generated/websocket -// specPath defaults to fetching from https://developer.gemini.com/specs/asyncapi/websocket.yaml +// specPath defaults to the vendored websocket specification // // Why a script instead of `asyncapi generate models`: the Gemini WS protocol // uses case-distinct single-letter keys (e vs E, u vs U). Modelina's default @@ -19,29 +19,22 @@ import { } from "@asyncapi/modelina"; import { parse } from "yaml"; import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { dirname, resolve, join } from "node:path"; -import { loadPublishedSpecText } from "./spec-sources.mjs"; -import { addCompatibilityAliases } from "./websocket-compatibility.mjs"; - -const PUBLISHED_SPEC_URL = "https://developer.gemini.com/specs/asyncapi/websocket.yaml"; +import { loadVendoredSpecText } from "./spec-sources.mjs"; +import { addCompatibilityAliases, addLegacySettlementTypes } from "./websocket-compatibility.mjs"; const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, ".."); -const outDirs = [process.argv[2] ?? join(root, "src", "generated", "websocket")].map(d => resolve(root, d)); -async function loadSpec() { - const specPath = process.argv[3]; - if (specPath?.startsWith("http://") || specPath?.startsWith("https://")) { - return parse(await loadPublishedSpecText(specPath)); - } +async function loadSpec(specPath) { if (specPath) { return parse(readFileSync(resolve(specPath), "utf8")); } - console.log(`Fetching spec from ${PUBLISHED_SPEC_URL}`); - return parse(await loadPublishedSpecText(PUBLISHED_SPEC_URL)); + return parse(await loadVendoredSpecText("websocket")); } +export async function generateWebSocketTypes({ specPath, outputDir }) { // JSON Schema's default is `additionalProperties: true`. Modelina renders an // explicit `additionalProperties: true` marker as a literal nested property, // even when the object already declares its wire fields. Normalize only those @@ -169,27 +162,34 @@ function refineRfqLegSymbol(source) { // Modelina's library output doesn't prefix declarations with `export`; add it // so the barrel file exports every type. -const body = refineRfqLegSymbol( - addCompatibilityAliases(refineKnownMethodLiterals( - models - .map((m) => m.result) - .join("\n\n") - .replace(/^(interface |enum |type )/gm, "export $1"), - )), -); - -if (/\bany\b/.test(body)) { - throw new Error("generate-ws-types: generated output must not expose any."); -} -if (/\bMap\s* m.result) + .join("\n\n") + .replace(/^(interface |enum |type )/gm, "export $1"), + )), + ), + ); -for (const outDir of outDirs) { + if (/\bany\b/.test(body)) { + throw new Error("generate-ws-types: generated output must not expose any."); + } + if (/\bMap\s* { + for (const entry of readdirSync(dir)) { + const path = join(dir, entry); + if (statSync(path).isDirectory()) visit(path); + else files.push(relative(root, path)); + } + }; + visit(root); + return files.sort(); +} + +test("generated artifacts are reproducible from vendored specifications", async (t) => { + const outputDir = mkdtempSync(join(tmpdir(), "gemini-generated-drift-")); + t.after(() => rmSync(outputDir, { recursive: true, force: true })); + + await generateRestModules({ + specPaths: { rest: "rest", predictionMarkets: "predictionMarkets" }, + baseOutputDir: outputDir, + writeSnapshot: false, + }); + await generatePredictionMarkets({ specPath: "predictionMarkets", outputDir }); + await generateWebSocketTypes({ specPath: "websocket", outputDir: join(outputDir, "websocket") }); + + const produced = new Set(generatedFiles(outputDir)); + const committed = new Set(generatedFiles(generatedDir)); + const paths = [...new Set([...produced, ...committed])].sort(); + + for (const relativePath of paths) { + const producedPath = join(outputDir, relativePath); + const committedPath = join(generatedDir, relativePath); + if ( + !produced.has(relativePath) || + !committed.has(relativePath) || + !readFileSync(producedPath).equals(readFileSync(committedPath)) + ) { + assert.fail(`generated drift in ${relativePath}; run npm run regenerate`); + } + } +}); diff --git a/packages/sdk-typescript/scripts/numeric-overlay.test.mjs b/packages/sdk-typescript/scripts/numeric-overlay.test.mjs new file mode 100644 index 00000000..529a3ea3 --- /dev/null +++ b/packages/sdk-typescript/scripts/numeric-overlay.test.mjs @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +import { parse } from "yaml"; +import { loadVendoredSpecText, specsRoot } from "./spec-sources.mjs"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const modelsPath = resolve(scriptDir, "../src/generated/market-data/models.ts"); + +function extensionLocations(document) { + const locations = []; + for (const [schema, schemaDocument] of Object.entries(document.components?.schemas ?? {})) { + for (const [property, propertyDocument] of Object.entries(schemaDocument?.properties ?? {})) { + if (propertyDocument?.["x-unsigned-int64"] === true) locations.push({ schema, property }); + } + } + return locations; +} + +function generatedSchema(source, name) { + const match = source.match(new RegExp(`(?:^|\\n) ${name}: \\{(?[\\s\\S]*?)(?:\\n \\};)`)); + assert(match, `${name} schema must be present in generated market-data models`); + return match.groups.body; +} + +function locationKey({ schema, property }) { + return `${schema}.${property}`; +} + +test("numeric overlay declares the generated SDK numeric policy", async () => { + const overlay = parse(await readFile(join(specsRoot(), "overlays", "numeric-types.yaml"), "utf8")); + const [rest, predictionMarkets] = await Promise.all([ + loadVendoredSpecText("rest").then(parse), + loadVendoredSpecText("predictionMarkets").then(parse), + ]); + const models = await readFile(modelsPath, "utf8"); + + assert.equal(overlay.version, 1); + for (const [name, group] of Object.entries(overlay)) { + if (name === "version") continue; + assert.ok(group && typeof group === "object", `${name} must be a rule group`); + assert.ok(Array.isArray(group.appliesTo) && group.appliesTo.length > 0, `${name}.appliesTo must be non-empty`); + assert.equal(new Set(group.appliesTo).size, group.appliesTo.length, `${name}.appliesTo must not contain duplicates`); + assert.ok(group.appliesTo.every((language) => language === "go" || language === "typescript"), `${name}.appliesTo contains an unknown language`); + if (!group.appliesTo.includes("typescript")) { + assert.equal(typeof group.reason, "string", `${name}.reason must be a non-empty string`); + assert.ok(group.reason.trim().length > 0, `${name}.reason must be a non-empty string`); + } + } + + const unsigned = overlay.unsignedIntegers; + assert.ok(unsigned && Array.isArray(unsigned.locations), "unsignedIntegers.locations must be an array"); + for (const location of unsigned.locations) { + assert.deepEqual(Object.keys(location).sort(), ["property", "schema"], "unsigned integer locations must only declare schema and property"); + assert.equal(typeof location.schema, "string", "unsigned integer location schema must be a string"); + assert.equal(typeof location.property, "string", "unsigned integer location property must be a string"); + } + const declaredKeys = unsigned.locations.map(locationKey); + + const restLocations = extensionLocations(rest); + const predictionMarketsLocations = extensionLocations(predictionMarkets); + const specLocations = [...new Map([...restLocations, ...predictionMarketsLocations].map((location) => [locationKey(location), location])).values()]; + assert.deepEqual( + [...declaredKeys].sort(), + specLocations.map(locationKey).sort(), + "unsignedIntegers.locations must exactly match x-unsigned-int64 properties in the vendored specs", + ); + for (const location of unsigned.locations) { + assert.equal( + rest.components?.schemas?.[location.schema]?.properties?.[location.property]?.["x-unsigned-int64"], + true, + `${locationKey(location)} must be x-unsigned-int64:true in the REST spec`, + ); + } + + assert.equal(overlay.decimalFormat.typescript.numberSchema, "number"); + assert.equal(overlay.decimalFormat.typescript.stringSchema, "string"); + assert.match(generatedSchema(models, "SymbolDetails"), /\btick_size\?: number;/, "SymbolDetails.tick_size must be number"); + assert.match(generatedSchema(models, "Trade"), /\bprice\?: string;/, "Trade.price must be string"); +}); diff --git a/packages/sdk-typescript/scripts/openapi-rest-generator.mjs b/packages/sdk-typescript/scripts/openapi-rest-generator.mjs index 04bbf8a4..7fccf7d7 100644 --- a/packages/sdk-typescript/scripts/openapi-rest-generator.mjs +++ b/packages/sdk-typescript/scripts/openapi-rest-generator.mjs @@ -4,15 +4,14 @@ import { resolve } from "node:path"; import openapiTS, { astToString } from "openapi-typescript"; import ts from "typescript"; import { parse } from "yaml"; -import { loadPublishedSpecText } from "./spec-sources.mjs"; +import { loadVendoredSpecText, SPEC_IDS } from "./spec-sources.mjs"; const HTTP_METHODS = ["get", "post", "put", "patch", "delete"]; const RESERVED_HEADERS = new Set(["accept", "authorization", "content-length", "content-type", "cache-control"]); export async function loadOpenApiDocument(specPathOrUrl) { - if (specPathOrUrl.startsWith("http://") || specPathOrUrl.startsWith("https://")) { - console.log(`Fetching spec from ${specPathOrUrl}`); - return parse(await loadPublishedSpecText(specPathOrUrl)); + if (SPEC_IDS.includes(specPathOrUrl)) { + return parse(await loadVendoredSpecText(specPathOrUrl)); } return parse(await readFile(specPathOrUrl, "utf8")); } @@ -581,6 +580,26 @@ export function renderRestClient(operations, options) { `}\n`; } +function preserveContractTotalShares(source) { + const marker = /^([ \t]*)Contract: \{\r?\n/m.exec(source); + if (!marker) return source; + + const bodyStart = marker.index + marker[0].length; + const closingMarker = `\n${marker[1]}};`; + const bodyEnd = source.indexOf(closingMarker, bodyStart); + if (bodyEnd === -1) return source; + + const body = source.slice(bodyStart, bodyEnd); + if (/^[ \t]*totalShares\??\s*:/m.test(body)) return source; + + const newline = source.includes("\r\n") ? "\r\n" : "\n"; + return ( + source.slice(0, bodyStart) + + `${marker[1]} totalShares?: string | null;${newline}` + + source.slice(bodyStart) + ); +} + export async function renderModels(document, banner) { const BIGINT = ts.factory.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword); const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull()); @@ -592,7 +611,7 @@ export async function renderModels(document, banner) { } }, }); - return `${banner}${astToString(ast).trimEnd()}\n`; + return `${banner}${preserveContractTotalShares(astToString(ast).trimEnd())}\n`; } export async function generateOpenApiRestTypes(options) { diff --git a/packages/sdk-typescript/scripts/spec-sources.mjs b/packages/sdk-typescript/scripts/spec-sources.mjs index 442723fb..c447dc2f 100644 --- a/packages/sdk-typescript/scripts/spec-sources.mjs +++ b/packages/sdk-typescript/scripts/spec-sources.mjs @@ -1,31 +1,55 @@ import { createHash } from "node:crypto"; +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; -export const PUBLISHED_SPECS = Object.freeze({ - rest: "https://developer.gemini.com/specs/openapi/rest.yaml", - predictionMarkets: "https://developer.gemini.com/specs/openapi/prediction-markets.yaml", - websocket: "https://developer.gemini.com/specs/asyncapi/websocket.yaml", -}); +export const SPEC_IDS = Object.freeze(["rest", "predictionMarkets", "websocket"]); -// Update these values only in a reviewed change that also updates generated output. -const PUBLISHED_SPEC_SHA256 = Object.freeze({ - [PUBLISHED_SPECS.rest]: "79a0dc4061f3942dca8b30a589bbd406c781d2c6c19283d87cb21177afdcab5e", - [PUBLISHED_SPECS.predictionMarkets]: "0c70a976f4553ae39d14d6851416cb974f081919216b94ebd851f044d108cfe7", - [PUBLISHED_SPECS.websocket]: "d83c624336f16542c3f1f4554101e7fa19bbc703012ae7bcd780c401667a5def", +const SPEC_PATHS = Object.freeze({ + rest: "openapi/rest.yaml", + predictionMarkets: "openapi/prediction-markets.yaml", + websocket: "asyncapi/websocket.yaml", }); -const ALLOWED_PUBLISHED_SPEC_URLS = new Set(Object.values(PUBLISHED_SPECS)); +function assertSpecId(specId) { + if (!SPEC_IDS.includes(specId)) { + throw new Error(`unknown specification id: ${specId}`); + } +} -export async function loadPublishedSpecText(specUrl) { - const expectedHash = PUBLISHED_SPEC_SHA256[specUrl]; - if (!expectedHash || !ALLOWED_PUBLISHED_SPEC_URLS.has(specUrl)) { - throw new Error(`Unallowlisted published specification URL: ${specUrl}`); +export function specsRoot() { + const startDir = dirname(fileURLToPath(import.meta.url)); + let currentDir = startDir; + while (true) { + if (existsSync(join(currentDir, "specs", "SOURCES.json"))) { + return join(currentDir, "specs"); + } + const parentDir = dirname(currentDir); + if (parentDir === currentDir) break; + currentDir = parentDir; } - const response = await fetch(specUrl); - if (!response.ok) throw new Error(`Failed to fetch spec: ${response.status}`); - const bytes = Buffer.from(await response.arrayBuffer()); - const actualHash = createHash("sha256").update(bytes).digest("hex"); - if (actualHash !== expectedHash) { - throw new Error(`Published specification hash mismatch for ${specUrl}: expected ${expectedHash}, got ${actualHash}`); + throw new Error(`specs/SOURCES.json not found above ${startDir}`); +} + +export function vendoredSpecPath(specId) { + assertSpecId(specId); + return join(specsRoot(), SPEC_PATHS[specId]); +} + +export async function loadVendoredSpecText(specId) { + assertSpecId(specId); + const root = specsRoot(); + const manifest = JSON.parse(await readFile(join(root, "SOURCES.json"), "utf8")); + const entry = manifest.specs.find(({ id }) => id === specId); + if (!entry) throw new Error(`unknown specification id: ${specId}`); + + const bytes = await readFile(join(root, entry.path)); + const actual = createHash("sha256").update(bytes).digest("hex"); + if (actual !== entry.sha256) { + throw new Error( + `vendored specification digest mismatch for ${entry.path}: expected ${entry.sha256}, got ${actual}; run node specs/refresh.mjs`, + ); } return bytes.toString("utf8"); } diff --git a/packages/sdk-typescript/scripts/websocket-compatibility.mjs b/packages/sdk-typescript/scripts/websocket-compatibility.mjs index 716a18f8..016af241 100644 --- a/packages/sdk-typescript/scripts/websocket-compatibility.mjs +++ b/packages/sdk-typescript/scripts/websocket-compatibility.mjs @@ -22,6 +22,43 @@ function isRfqDeliveryEnum(body) { ); } +const SETTLEMENT_COMPATIBILITY_DECLARATIONS = [ + ["SettlementUpdate", `export interface SettlementUpdate { + type: 'settlements'; + settlements: Settlement[]; +}`], + ["Settlement", `export interface Settlement { + symbol: string; + position: string; + payout?: string; + outcome: AnonymousSchema_135; +}`], + ["AnonymousSchema_135", `export enum AnonymousSchema_135 { + YES = "yes", + NO = "no", + UNSPECIFIED = "unspecified", +}`], +]; + +function hasDeclaration(source, name) { + return new RegExp( + `^\\s*(?:export\\s+)?(?:interface|enum|type|class)\\s+${name}\\b`, + "m", + ).test(source); +} + +function appendSettlementCompatibility(source) { + const missing = SETTLEMENT_COMPATIBILITY_DECLARATIONS + .filter(([name]) => !hasDeclaration(source, name)) + .map(([, declaration]) => declaration); + if (missing.length === 0) return source; + return `${source.trimEnd()}\n\n${missing.join("\n\n")}\n`; +} + +export function addLegacySettlementTypes(source) { + return appendSettlementCompatibility(source); +} + // Preserve the public enum export emitted by the previous generated schema // numbering. Locate the RFQ delivery enum by its stable wire values because // Modelina's anonymous schema suffix can shift whenever the upstream schema @@ -38,5 +75,5 @@ export function addCompatibilityAliases(source) { const alias = `export { ${generatedName} as AnonymousSchema_152 };`; if (source.includes(alias)) return source; - return `${source}\n\n/** @deprecated Use ${generatedName}. */\n${alias}`; + return `${source.trimEnd()}\n\n/** @deprecated Use ${generatedName}. */\n${alias}`; } diff --git a/packages/sdk-typescript/scripts/websocket-types.test.mjs b/packages/sdk-typescript/scripts/websocket-types.test.mjs index a067426c..b72a7b3c 100644 --- a/packages/sdk-typescript/scripts/websocket-types.test.mjs +++ b/packages/sdk-typescript/scripts/websocket-types.test.mjs @@ -3,7 +3,7 @@ import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; -import { addCompatibilityAliases } from "./websocket-compatibility.mjs"; +import { addCompatibilityAliases, addLegacySettlementTypes } from "./websocket-compatibility.mjs"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const sdkDir = resolve(scriptDir, ".."); @@ -90,3 +90,23 @@ test("RFQ compatibility alias follows generator-derived anonymous enum names", ( const stableSource = source.replaceAll("AnonymousSchema_999", "AnonymousSchema_152"); assert.equal(addCompatibilityAliases(stableSource), stableSource); }); + +test("settlement compatibility declarations are appended separately and idempotently", () => { + const source = `export enum AnonymousSchema_999 { + FAILED = "FAILED", + FINALIZED = "FINALIZED", + DECLINED = "DECLINED", + CONFIRMED = "CONFIRMED", + ACCEPTED = "ACCEPTED", + RESERVED_CLOSED = "CLOSED", +}`; + const rfqAlias = addCompatibilityAliases(source); + + assert.doesNotMatch(rfqAlias, /\bSettlementUpdate\b/); + + const withSettlementTypes = addLegacySettlementTypes(rfqAlias); + for (const name of ["SettlementUpdate", "Settlement", "AnonymousSchema_135"]) { + assert.match(withSettlementTypes, new RegExp(`export (?:interface|enum) ${name}\\b`)); + } + assert.equal(addLegacySettlementTypes(withSettlementTypes), withSettlementTypes); +}); diff --git a/packages/sdk-typescript/src/errors.ts b/packages/sdk-typescript/src/errors.ts index 75e8efff..8baccd64 100644 --- a/packages/sdk-typescript/src/errors.ts +++ b/packages/sdk-typescript/src/errors.ts @@ -198,6 +198,8 @@ export class ApiError extends SdkError { missingnonce: MissingNonce, invalidsignature: InvalidSignature, missingrole: MissingRole, + mustacceptterms: AcceptTermsRequired, + mustaccepterms: AcceptTermsRequired, accepttermsrequired: AcceptTermsRequired, termsnotaccepted: AcceptTermsRequired, predictionmarketstermsmustbeacceptedbeforeplacingorders: AcceptTermsRequired, @@ -302,6 +304,8 @@ function reasonClassification(reason: string | undefined): Pick $90") */ label?: string; @@ -916,7 +917,6 @@ export interface components { /** @description Rich text description */ description?: Record; prices?: components["schemas"]["ContractPrices"]; - totalShares?: string | null; color?: string | null; status?: components["schemas"]["MarketStatus"]; imageUrl?: string | null; diff --git a/packages/sdk-typescript/src/generated/websocket/index.ts b/packages/sdk-typescript/src/generated/websocket/index.ts index 80dab215..23a39d07 100644 --- a/packages/sdk-typescript/src/generated/websocket/index.ts +++ b/packages/sdk-typescript/src/generated/websocket/index.ts @@ -123,6 +123,7 @@ export interface RfqSubmitQuoteParams { price: string; quantity: string; validUntil?: number | bigint; + clientId?: string; } export interface RfqWithdrawQuoteRequest { @@ -335,24 +336,13 @@ export interface NamedAmount { t: string; v: string; c?: string; + o?: AnonymousSchema_132; } -export interface SettlementUpdate { - type: 'settlements'; - settlements: Settlement[]; -} - -export interface Settlement { - symbol: string; - position: string; - payout?: string; - outcome: AnonymousSchema_135; -} - -export enum AnonymousSchema_135 { - YES = "yes", - NO = "no", - UNSPECIFIED = "unspecified", +export enum AnonymousSchema_132 { + YES = "YES", + NO = "NO", + UNSPECIFIED = "UNSPECIFIED", } export interface ContractStatus { @@ -384,11 +374,11 @@ export interface RfqPublicEvent { export interface RfqLeg { c: string; - o: AnonymousSchema_148; + o: AnonymousSchema_145; s?: string; } -export enum AnonymousSchema_148 { +export enum AnonymousSchema_145 { YES = "YES", NO = "NO", } @@ -409,7 +399,7 @@ export interface RfqPrivateDelivery { i: string; E: number | bigint; r: string; - x: AnonymousSchema_153; + x: AnonymousSchema_150; S: RfqLifecycleState; q?: string; p?: string; @@ -418,7 +408,7 @@ export interface RfqPrivateDelivery { vu?: number | bigint; } -export enum AnonymousSchema_153 { +export enum AnonymousSchema_150 { RESERVED_CLOSED = "CLOSED", ACCEPTED = "ACCEPTED", CONFIRMED = "CONFIRMED", @@ -435,5 +425,23 @@ export enum RfqQuoteStatus { LOST = "LOST", } -/** @deprecated Use AnonymousSchema_153. */ -export { AnonymousSchema_153 as AnonymousSchema_152 }; +/** @deprecated Use AnonymousSchema_150. */ +export { AnonymousSchema_150 as AnonymousSchema_152 }; + +export interface SettlementUpdate { + type: 'settlements'; + settlements: Settlement[]; +} + +export interface Settlement { + symbol: string; + position: string; + payout?: string; + outcome: AnonymousSchema_135; +} + +export enum AnonymousSchema_135 { + YES = "yes", + NO = "no", + UNSPECIFIED = "unspecified", +} diff --git a/packages/sdk-typescript/src/tests/conformance/errors.test.ts b/packages/sdk-typescript/src/tests/conformance/errors.test.ts new file mode 100644 index 00000000..2e404c5c --- /dev/null +++ b/packages/sdk-typescript/src/tests/conformance/errors.test.ts @@ -0,0 +1,129 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + AcceptTermsRequired, + ApiError, + InsufficientFunds, + InvalidNonce, + InvalidRequest, + InvalidSignature, + MissingNonce, + MissingRole, + NotFoundError, + RateLimitError, + ServiceUnavailable, + serializeError, +} from "../../errors.js"; +import { HttpTransport, parseLosslessJson } from "../../transport/http.js"; +import { streamingTextResponse } from "../support/http-fixtures.js"; +import { + loadCase, + loadManifest, + type FixtureCase, +} from "./support/fixtures.js"; + +type ErrorKind = + | "invalid_nonce" + | "missing_nonce" + | "invalid_signature" + | "missing_role" + | "terms_required" + | "insufficient_funds" + | "rate_limited" + | "order_not_found" + | "market_closed" + | "not_found" + | "service_error" + | "invalid_request"; +type ErrorFixture = FixtureCase & { + readonly response: { + readonly status: number; + readonly headers?: Record; + readonly body: string; + }; + readonly expect: { + readonly kind: ErrorKind; + readonly reason?: string; + readonly retryAfterSeconds?: number; + }; +}; + +const CLASS_BY_KIND: Record = { + invalid_nonce: InvalidNonce, + missing_nonce: MissingNonce, + invalid_signature: InvalidSignature, + missing_role: MissingRole, + terms_required: AcceptTermsRequired, + insufficient_funds: InsufficientFunds, + rate_limited: RateLimitError, + order_not_found: NotFoundError, + market_closed: ApiError, + not_found: NotFoundError, + service_error: ServiceUnavailable, + invalid_request: InvalidRequest, +}; + +function fixture(value: FixtureCase): ErrorFixture { + return value as unknown as ErrorFixture; +} + +function responseHeaders(values: Record | undefined): { get(name: string): string | null } { + const entries = Object.entries(values ?? {}); + return { + get(name) { + const entry = entries.find(([key]) => key.toLowerCase() === name.toLowerCase()); + return entry?.[1] ?? null; + }, + }; +} + +async function runCase(value: ErrorFixture): Promise { + const transport = new HttpTransport({ + env: "sandbox", + maxRetries: 0, + fetchImpl: async () => streamingTextResponse( + value.response.body, + value.response.status, + responseHeaders(value.response.headers), + ), + }); + let observed: unknown; + try { + await transport.requestPublic({ method: "GET", path: "/v1/conformance" }); + } catch (error) { + observed = error; + } + assert.ok(observed instanceof ApiError, "HTTP error must be an ApiError"); + const expectedClass = CLASS_BY_KIND[value.expect.kind]; + assert.ok(observed instanceof expectedClass, `expected ${value.expect.kind} to map to ${expectedClass.name}`); + if (value.expect.reason !== undefined) assert.equal(observed.reason, value.expect.reason); + if (value.expect.retryAfterSeconds !== undefined) { + assert.equal( + observed.metadata?.rateLimit?.retryAfter, + String(value.expect.retryAfterSeconds), + ); + } + + const serialized = serializeError(observed, { includeRawBody: true }); + if (value.response.body.length === 0) { + assert.equal(serialized.body, undefined); + } else { + let expectedBody: unknown = value.response.body; + try { + expectedBody = parseLosslessJson(value.response.body); + } catch { + // Keep unstructured response text as the retained raw body. + } + assert.deepEqual(serialized.body, expectedBody); + } +} + +const manifest = await loadManifest(); +const suite = manifest.suites.find((entry) => entry.id === "http/errors"); +if (!suite) throw new Error("conformance manifest is missing http/errors"); +for (const caseId of suite.cases) { + test(`conformance error mapping: ${caseId}`, async () => { + await runCase(fixture(await loadCase(suite.id, caseId))); + }); +} diff --git a/packages/sdk-typescript/src/tests/conformance/hmac-requests.test.ts b/packages/sdk-typescript/src/tests/conformance/hmac-requests.test.ts new file mode 100644 index 00000000..ce7d9f7d --- /dev/null +++ b/packages/sdk-typescript/src/tests/conformance/hmac-requests.test.ts @@ -0,0 +1,143 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { HmacAuth } from "../../auth/hmac.js"; +import { createServerWebSocketAuthHeaders } from "../../websocket/auth.js"; +import { HttpTransport, type FetchLike, type HttpMethod } from "../../transport/http.js"; +import { fromBase64, hmacSha384Hex } from "../../utils/encoding.js"; +import { type BoundaryRecord, type BoundaryValue } from "../../utils/boundary-value.js"; +import { parseBoundaryRecord, streamingTextResponse } from "../support/http-fixtures.js"; +import { + loadCase, + loadManifest, + type FixtureCase, +} from "./support/fixtures.js"; + +type HmacFixture = FixtureCase & { + credentials: { apiKey: string; apiSecret: string }; + nonce: { mode: "monotonic" | "websocket"; value?: string }; + request: { method: HttpMethod; path: string; body?: BoundaryRecord }; + expect: { + headers: readonly string[]; + apiKeyHeader: string; + payload?: { request?: string; nonce?: string; fields?: BoundaryRecord }; + signature: { algorithm: string; over: string; encoding: string }; + }; +}; + +function fixture(value: FixtureCase): HmacFixture { + return value as unknown as HmacFixture; +} + +function wireText(value: BoundaryValue): string { + if (typeof value === "string") return JSON.stringify(value); + if (typeof value === "bigint" || typeof value === "number" || typeof value === "boolean") return String(value); + if (value === null) return "null"; + return JSON.stringify(value) ?? String(value); +} + +function assertRawValue(actual: BoundaryValue, expected: BoundaryValue, label: string): void { + assert.equal(wireText(actual), wireText(expected), label); +} + +function assertHeaders(headers: Record, expected: readonly string[]): void { + for (const name of expected) { + assert.equal(typeof headers[name], "string", `missing ${name}`); + assert.notEqual(headers[name], "", `empty ${name}`); + } +} + +async function runHttpCase(value: HmacFixture): Promise { + const nonceValue = value.nonce.value; + assert.equal(value.nonce.mode, "monotonic"); + assert.ok(nonceValue, "monotonic fixture must provide a nonce"); + const auth = new HmacAuth({ + apiKey: value.credentials.apiKey, + apiSecret: value.credentials.apiSecret, + now: () => Number(nonceValue), + }); + let captured: { url: string; init: Parameters[1] } | undefined; + const transport = new HttpTransport({ + env: "sandbox", + auth, + maxRetries: 0, + fetchImpl: async (url, init) => { + captured = { url, init }; + return streamingTextResponse("{}", 200, { get: () => "application/json" }); + }, + }); + await transport.request({ + method: value.request.method, + path: value.request.path, + params: value.request.body, + }); + assert.ok(captured, "fixture request did not reach fetch"); + const { url, init } = captured; + assert.equal(url, `https://api.sandbox.gemini.com${value.request.path}`); + assert.equal(init.method, value.request.method); + assertHeaders(init.headers, value.expect.headers); + assert.equal(init.headers["X-GEMINI-APIKEY"], value.expect.apiKeyHeader); + assert.equal(init.body, undefined, "private REST fields belong in the signed payload"); + + const payloadBase64 = init.headers["X-GEMINI-PAYLOAD"]; + assert.ok(payloadBase64); + const payloadJson = fromBase64(payloadBase64); + const payload = parseBoundaryRecord(payloadJson); + const expectedPayload = value.expect.payload; + assert.ok(expectedPayload); + if (expectedPayload.request !== undefined) assert.equal(payload.request, expectedPayload.request); + if (expectedPayload.nonce !== undefined) { + assert.equal(String(payload.nonce).replace(/^"|"$/g, ""), expectedPayload.nonce); + } + const expectedFields = expectedPayload.fields ?? {}; + for (const [key, expected] of Object.entries(expectedFields)) { + assert.ok(Object.hasOwn(payload, key), `missing payload field ${key}`); + assertRawValue(payload[key], expected, `payload field ${key}`); + } + assert.deepEqual( + Object.keys(payload).sort(), + ["request", "nonce", ...Object.keys(expectedFields)].sort(), + "signed payload contains only the declared fields", + ); + assert.equal( + init.headers["X-GEMINI-SIGNATURE"], + await hmacSha384Hex(value.credentials.apiSecret, payloadBase64), + ); + assert.equal(value.expect.signature.algorithm, "HMAC-SHA384"); + assert.equal(value.expect.signature.over, "payloadBase64"); + assert.equal(value.expect.signature.encoding, "hex-lower"); +} + +async function runWebSocketUpgradeCase(value: HmacFixture): Promise { + assert.equal(value.nonce.mode, "websocket"); + const auth = new HmacAuth({ + apiKey: value.credentials.apiKey, + apiSecret: value.credentials.apiSecret, + now: () => 1_700_000_000_000, + }); + const headers = await createServerWebSocketAuthHeaders(auth); + assertHeaders(headers, value.expect.headers); + assert.equal(headers["X-GEMINI-APIKEY"], value.expect.apiKeyHeader); + const nonce = headers["X-GEMINI-NONCE"]; + assert.match(nonce, /^\d{10}$/); + const payloadBase64 = headers["X-GEMINI-PAYLOAD"]; + assert.equal(fromBase64(payloadBase64), nonce); + assert.equal( + headers["X-GEMINI-SIGNATURE"], + await hmacSha384Hex(value.credentials.apiSecret, payloadBase64), + ); + assert.equal(value.expect.signature.algorithm, "HMAC-SHA384"); + assert.equal(value.expect.signature.over, "payloadBase64"); + assert.equal(value.expect.signature.encoding, "hex-lower"); +} + +const manifest = await loadManifest(); +const suite = manifest.suites.find((entry) => entry.id === "http/hmac-requests"); +if (!suite) throw new Error("conformance manifest is missing http/hmac-requests"); +for (const caseId of suite.cases) { + test(`conformance hmac request: ${caseId}`, async () => { + const value = fixture(await loadCase(suite.id, caseId)); + if (value.nonce.mode === "websocket") await runWebSocketUpgradeCase(value); + else await runHttpCase(value); + }); +} diff --git a/packages/sdk-typescript/src/tests/conformance/json-decoding.test.ts b/packages/sdk-typescript/src/tests/conformance/json-decoding.test.ts new file mode 100644 index 00000000..da9cebf7 --- /dev/null +++ b/packages/sdk-typescript/src/tests/conformance/json-decoding.test.ts @@ -0,0 +1,50 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { parseLosslessJson } from "../../transport/http.js"; +import { isBoundaryObject } from "../../utils/boundary-value.js"; +import { loadCase, loadManifest } from "./support/fixtures.js"; + +test("jsonDecoding fixtures preserve wire numeric semantics", async () => { + const manifest = await loadManifest(); + const suite = manifest.suites.find((candidate) => candidate.kind === "jsonDecoding"); + assert.ok(suite, "manifest must declare a jsonDecoding suite"); + + for (const caseId of suite.cases) { + await test(caseId, async () => { + const fixture = await loadCase(suite.id, caseId); + const raw = fixture.raw; + const field = fixture.field; + const expected = fixture.expect; + assert.equal(typeof raw, "string"); + assert.equal(typeof field, "string"); + assert.ok(isBoundaryObject(expected)); + const parsed = parseLosslessJson(raw as string); + assert.ok(isBoundaryObject(parsed), "json fixture must decode to an object"); + const value = parsed[field as string]; + assert.notEqual(value, undefined, `fixture field ${String(field)} must be present`); + + switch (expected.valueKind) { + case "integer": + assert.equal(String(value), String(expected.text)); + if (BigInt(String(expected.text)) > BigInt(Number.MAX_SAFE_INTEGER) || + BigInt(String(expected.text)) < BigInt(Number.MIN_SAFE_INTEGER)) { + assert.equal(typeof value, "bigint", "unsafe integers must be promoted to bigint"); + } else { + assert.equal(typeof value, "number", "safe integers must remain numbers"); + } + break; + case "decimal": + if (typeof value === "string") { + assert.equal(value, expected.text, "decimal strings must remain exact strings"); + } else { + assert.equal(typeof value, "number", "decimal numbers must remain numbers"); + assert.equal(value, Number(expected.text), "decimal number value must be preserved"); + } + break; + default: + assert.fail(`unsupported json fixture valueKind: ${String(expected.valueKind)}`); + } + }); + } +}); diff --git a/packages/sdk-typescript/src/tests/conformance/manifest.test.ts b/packages/sdk-typescript/src/tests/conformance/manifest.test.ts new file mode 100644 index 00000000..af97d032 --- /dev/null +++ b/packages/sdk-typescript/src/tests/conformance/manifest.test.ts @@ -0,0 +1,160 @@ +import assert from "node:assert/strict"; +import { readdir } from "node:fs/promises"; +import { join, relative } from "node:path"; +import { test } from "node:test"; + +import { + conformanceRoot, + loadCase, + loadExceptions, + loadManifest, +} from "./support/fixtures.js"; + +const EXPECTED_SUITES: Readonly> = { + "http/hmac-requests": [ + "private-post-with-body", + "private-post-empty-body", + "private-post-wide-integer", + "websocket-upgrade-nonce", + ], + "http/unsigned-requests": [ + "market-data-ticker", + "market-data-order-book-limits", + "prediction-markets-list-events-filters", + ], + "http/errors": [ + "invalid-nonce-400", + "missing-role-403-result-envelope", + "missing-role-403-error-envelope", + "terms-required-400-must-accept", + "terms-required-400-accept-terms-required", + "insufficient-funds-406", + "rate-limited-429-retry-after", + "order-not-found-404", + "market-closed-400", + "server-error-500-unstructured", + "not-found-404-empty-body", + ], + json: [ + "wide-integer-unsafe", + "wide-integer-safe", + "decimal-string-exact", + "decimal-number-exact", + ], + "websocket/subscriptions": [ + "public-trades", + "public-trades-untrimmed-symbol", + "public-book-ticker", + "public-depth-diff", + "public-depth-diff-interval", + "public-partial-depth-20", + "public-contract-status", + "private-orders-session", + "private-balances-interval", + ], + "websocket/events": [ + "trade", + "depth-unsafe-last-update-id", + "order-update", + "balance-update", + ], +}; + +const EXPECTED_KINDS: Readonly> = { + "http/hmac-requests": "hmacRequest", + "http/unsigned-requests": "unsignedRequest", + "http/errors": "errorMapping", + json: "jsonDecoding", + "websocket/subscriptions": "wsSubscription", + "websocket/events": "wsEvent", +}; + +const EXPECTED_EXCEPTIONS = [ + "rest-nonce-json-type", + "rest-payload-key-order", + "ws-subscription-id-scope", + "ws-inbound-symbol-case", + "int64-static-typing", + "http-406-insufficient-funds", + "rest-query-signing", + "candle-timeframe-spelling", +] as const; + +async function fixtureJsonFiles(root: string): Promise { + const files: string[] = []; + async function visit(directory: string): Promise { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + await visit(path); + } else if (entry.isFile() && entry.name.endsWith(".json") && relative(root, path) !== "manifest.json") { + files.push(relative(root, path).replace(/\.json$/, "")); + } + } + } + await visit(root); + return files; +} + +function sorted(values: Iterable): string[] { + return [...values].sort(); +} + +test("conformance manifest covers every fixture and declared exception", async () => { + const manifest = await loadManifest(); + assert.equal(manifest.version, 1); + assert.equal(manifest.suites.length, Object.keys(EXPECTED_SUITES).length); + assert.deepEqual(sorted(manifest.suites.map((suite) => suite.id)), sorted(Object.keys(EXPECTED_SUITES))); + + const listedFiles = new Set(); + const referencedExceptions = new Map(); + for (const suite of manifest.suites) { + assert.equal(suite.kind, EXPECTED_KINDS[suite.id]); + assert.deepEqual(suite.cases, EXPECTED_SUITES[suite.id]); + for (const caseId of suite.cases) { + const relativePath = `${suite.id}/${caseId}`; + listedFiles.add(relativePath); + const value = await loadCase(suite.id, caseId); + assert.equal(value.id, relativePath); + assert.equal(value.kind, suite.kind); + for (const exception of value.exceptions ?? []) { + const ids = referencedExceptions.get(exception) ?? []; + ids.push(relativePath); + referencedExceptions.set(exception, ids); + } + } + } + + const diskFiles = new Set(await fixtureJsonFiles(conformanceRoot())); + assert.deepEqual(sorted(listedFiles), sorted(diskFiles), "manifest and fixture files must agree in both directions"); + + assert.deepEqual(manifest.exceptions, EXPECTED_EXCEPTIONS); + const exceptions = await loadExceptions(); + assert.deepEqual(sorted(exceptions.map((exception) => exception.id)), sorted(manifest.exceptions)); + const declaredExceptionIds = new Set(exceptions.map((exception) => exception.id)); + for (const exceptionId of referencedExceptions.keys()) { + assert.ok(declaredExceptionIds.has(exceptionId), `case references undeclared exception ${exceptionId}`); + } + + const declaredCases = new Map(); + for (const exception of exceptions) { + for (const caseId of exception.cases ?? []) { + const cases = declaredCases.get(caseId) ?? []; + cases.push(exception.id); + declaredCases.set(caseId, cases); + } + } + const observedCases = new Map(); + for (const [exceptionId, cases] of referencedExceptions) { + for (const caseId of cases) { + const exceptionIds = observedCases.get(caseId) ?? []; + exceptionIds.push(exceptionId); + observedCases.set(caseId, exceptionIds); + } + } + assert.deepEqual( + sorted([...declaredCases.entries()].map(([caseId, ids]) => `${caseId}:${sorted(ids).join(",")}`)), + sorted([...observedCases.entries()].map(([caseId, ids]) => `${caseId}:${sorted(ids).join(",")}`)), + "overlay exception cases must match fixture references", + ); +}); diff --git a/packages/sdk-typescript/src/tests/conformance/support/fixtures.ts b/packages/sdk-typescript/src/tests/conformance/support/fixtures.ts new file mode 100644 index 00000000..afd1f323 --- /dev/null +++ b/packages/sdk-typescript/src/tests/conformance/support/fixtures.ts @@ -0,0 +1,75 @@ +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parse as parseYaml } from "yaml"; + +import { parseLosslessJson } from "../../../transport/http.js"; +import { + isBoundaryObject, + type BoundaryRecord, +} from "../../../utils/boundary-value.js"; + +export type ManifestSuite = { + readonly id: string; + readonly kind: string; + readonly cases: readonly string[]; +}; + +export type ConformanceManifest = { + readonly version: number; + readonly suites: readonly ManifestSuite[]; + readonly exceptions: readonly string[]; +}; + +export type WireException = { + readonly id: string; + readonly cases?: readonly string[]; + readonly [key: string]: unknown; +}; + +export type FixtureCase = BoundaryRecord & { + readonly id: string; + readonly kind: string; + readonly exceptions?: readonly string[]; +}; + +/** Locate the repository's conformance directory without assuming a checkout depth. */ +export function conformanceRoot(): string { + const startDir = dirname(fileURLToPath(import.meta.url)); + let current = startDir; + for (;;) { + const root = join(current, "conformance"); + if (existsSync(join(root, "manifest.json"))) return root; + const parent = dirname(current); + if (parent === current) break; + current = parent; + } + throw new Error(`conformance/manifest.json not found above ${startDir}`); +} + +export async function loadManifest(): Promise { + const manifest = JSON.parse(await readFile(join(conformanceRoot(), "manifest.json"), "utf8")) as ConformanceManifest; + if (!manifest || typeof manifest !== "object") throw new Error("conformance manifest must be an object"); + if (manifest.version !== 1) throw new Error(`unsupported conformance manifest version: ${String(manifest.version)}`); + if (!Array.isArray(manifest.suites)) throw new Error("conformance manifest suites must be an array"); + if (!Array.isArray(manifest.exceptions)) throw new Error("conformance manifest exceptions must be an array"); + return manifest; +} + +export async function loadCase(suiteId: string, caseId: string): Promise { + const path = join(conformanceRoot(), suiteId, `${caseId}.json`); + const value = parseLosslessJson(await readFile(path, "utf8")); + if (!isBoundaryObject(value)) throw new Error(`conformance case must be an object: ${suiteId}/${caseId}`); + return value as FixtureCase; +} + +export async function loadExceptions(): Promise { + const root = dirname(conformanceRoot()); + const path = join(root, "specs", "overlays", "gemini-wire-exceptions.yaml"); + const document = parseYaml(await readFile(path, "utf8")) as { exceptions?: unknown } | null; + if (!document || !Array.isArray(document.exceptions)) { + throw new Error("wire exceptions overlay must contain an exceptions array"); + } + return document.exceptions as WireException[]; +} diff --git a/packages/sdk-typescript/src/tests/conformance/unsigned-requests.test.ts b/packages/sdk-typescript/src/tests/conformance/unsigned-requests.test.ts new file mode 100644 index 00000000..9f0ec96a --- /dev/null +++ b/packages/sdk-typescript/src/tests/conformance/unsigned-requests.test.ts @@ -0,0 +1,95 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { PredictionMarketsRest } from "../../generated/rest.js"; +import { MarketDataRest } from "../../generated/market-data/rest.js"; +import { HttpTransport, type FetchLike, type HttpMethod } from "../../transport/http.js"; +import { type BoundaryRecord } from "../../utils/boundary-value.js"; +import { streamingTextResponse } from "../support/http-fixtures.js"; +import { + loadCase, + loadManifest, + type FixtureCase, +} from "./support/fixtures.js"; + +type UnsignedFixture = FixtureCase & { + operation: "marketData.getTicker" | "marketData.getCurrentOrderBook" | "predictions.listEvents"; + input?: BoundaryRecord; + expect: { + method: HttpMethod; + path: string; + query?: Record; + authHeaders: readonly string[]; + }; +}; + +function fixture(value: FixtureCase): UnsignedFixture { + return value as unknown as UnsignedFixture; +} + +function assertQuery(url: URL, expected: Record | undefined): void { + const expectedEntries = Object.entries(expected ?? {}); + const actualKeys = [...new Set([...url.searchParams.keys()])].sort(); + assert.deepEqual(actualKeys, expectedEntries.map(([name]) => name).sort()); + for (const [name, values] of expectedEntries) { + assert.deepEqual(url.searchParams.getAll(name), [...values], `query ${name}`); + } +} + +async function runCase(value: UnsignedFixture): Promise { + let captured: { url: string; init: Parameters[1] } | undefined; + const transport = new HttpTransport({ + env: "sandbox", + maxRetries: 0, + fetchImpl: async (url, init) => { + captured = { url, init }; + return streamingTextResponse("{}", 200, { get: () => "application/json" }); + }, + }); + const marketData = new MarketDataRest(transport); + const predictions = new PredictionMarketsRest(transport); + switch (value.operation) { + case "marketData.getTicker": + await marketData.getTicker(value.input as never); + break; + case "marketData.getCurrentOrderBook": + await marketData.getCurrentOrderBook(value.input as never); + break; + case "predictions.listEvents": + await predictions.listEvents(value.input as never); + break; + default: + throw new Error(`unsupported unsigned conformance operation: ${value.operation}`); + } + assert.ok(captured, "fixture request did not reach fetch"); + const url = new URL(captured.url); + assert.equal(captured.init.method, value.expect.method); + assert.equal(url.pathname, value.expect.path); + assertQuery(url, value.expect.query); + assert.equal(captured.init.body, undefined); + const actualAuthHeaders = Object.keys(captured.init.headers) + .filter((name) => name.toLowerCase().startsWith("x-gemini-") || name.toLowerCase() === "authorization") + .map((name) => name.toLowerCase()) + .sort(); + assert.deepEqual( + actualAuthHeaders, + value.expect.authHeaders.map((name) => name.toLowerCase()).sort(), + "authentication headers must match the fixture", + ); + for (const name of value.expect.authHeaders) { + const actual = Object.entries(captured.init.headers) + .find(([header]) => header.toLowerCase() === name.toLowerCase())?.[1]; + assert.notEqual(actual, undefined, `expected auth header ${name}`); + } + for (const header of Object.values(captured.init.headers)) { + assert.notEqual(header, "", "empty header"); + } +} +const manifest = await loadManifest(); +const suite = manifest.suites.find((entry) => entry.id === "http/unsigned-requests"); +if (!suite) throw new Error("conformance manifest is missing http/unsigned-requests"); +for (const caseId of suite.cases) { + test(`conformance unsigned request: ${caseId}`, async () => { + await runCase(fixture(await loadCase(suite.id, caseId))); + }); +} diff --git a/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts b/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts new file mode 100644 index 00000000..894e3df5 --- /dev/null +++ b/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts @@ -0,0 +1,118 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { GeminiWebSocket } from "../../websocket/server.js"; +import type { WebSocketStream } from "../../websocket/public.js"; +import type { WebSocketScopeOptions } from "../../websocket/server.js"; +import type { AuthStrategy } from "../../transport/http.js"; +import { parseBoundaryRecord } from "../support/http-fixtures.js"; +import { createWebSocketHarness } from "../support/ws-harness.js"; +import { isBoundaryObject, type BoundaryRecord, type BoundaryValue } from "../../utils/boundary-value.js"; +import { loadCase, loadManifest } from "./support/fixtures.js"; + +async function flush(): Promise { + await new Promise((resolve) => setImmediate(resolve)); +} + +function auth(): AuthStrategy { + return { + nextNonce: () => "1700000000", + credentialHeaders: async (payloadBase64) => ({ + "X-GEMINI-APIKEY": "conformance-api-key", + "X-GEMINI-SIGNATURE": `conformance:${payloadBase64}`, + }), + }; +} + +function record(value: BoundaryValue | undefined): BoundaryRecord | undefined { + return isBoundaryObject(value) ? value : undefined; +} + +function eventStream(client: GeminiWebSocket, fixture: BoundaryRecord): WebSocketStream { + const stream = String(fixture.stream); + const symbol = typeof fixture.symbol === "string" ? fixture.symbol : "BTCUSD"; + switch (stream) { + case "trades": + return client.public.trades(symbol) as unknown as WebSocketStream; + case "bookTicker": + return client.public.bookTicker(symbol) as unknown as WebSocketStream; + case "depthUpdates": + case "depth": + return client.public.depthUpdates(symbol) as unknown as WebSocketStream; + case "orders": + return client.private.orders({ scope: "account" } as WebSocketScopeOptions) as unknown as WebSocketStream; + case "balances": + return client.private.balances() as unknown as WebSocketStream; + default: + assert.fail(`unsupported wsEvent stream: ${stream}`); + } +} + +function assertExpectedFields(message: BoundaryRecord, expected: BoundaryRecord): void { + const fields = record(expected.fields); + assert.ok(fields, "wsEvent fixture expect.fields must be an object"); + for (const [name, rawExpectation] of Object.entries(fields)) { + const fieldExpectation = record(rawExpectation); + assert.ok(fieldExpectation, `expected field ${name} must be an object`); + const actual = message[name]; + assert.notEqual(actual, undefined, `event field ${name} must be present`); + const text = String(fieldExpectation.text); + if (fieldExpectation.compare === "caseInsensitive") { + assert.equal(String(actual).toLowerCase(), text.toLowerCase(), `event field ${name} differs`); + } else { + assert.equal(String(actual), text, `event field ${name} differs`); + } + if (/^-?\d+$/.test(text) && + (BigInt(text) > BigInt(Number.MAX_SAFE_INTEGER) || BigInt(text) < BigInt(Number.MIN_SAFE_INTEGER))) { + assert.equal(typeof actual, "bigint", `unsafe event field ${name} must remain lossless`); + } + } +} + +test("wsEvent fixtures route and decode typed events", async () => { + const manifest = await loadManifest(); + const suite = manifest.suites.find((candidate) => candidate.kind === "wsEvent"); + assert.ok(suite, "manifest must declare a wsEvent suite"); + + for (const caseId of suite.cases) { + await test(caseId, async () => { + const fixture = await loadCase(suite.id, caseId); + assert.equal(fixture.kind, "wsEvent"); + const needsAuth = ["orders", "balances", "positions"].includes(String(fixture.stream)); + const harness = createWebSocketHarness(); + const client = new GeminiWebSocket({ + url: "wss://example.test", + auth: needsAuth ? auth() : undefined, + socketFactory: harness.socketFactory, + }); + try { + const stream = eventStream(client, fixture); + const messages: BoundaryValue[] = []; + stream.on("message", (message) => messages.push(message)); + await flush(); + assert.equal(harness.sockets.length, 1); + const socket = harness.sockets[0]!; + socket.fireOpen(); + await flush(); + const subscribeFrame = socket.sent + .map((frame) => parseBoundaryRecord(frame)) + .find((frame) => frame.method === "SUBSCRIBE"); + assert.ok(subscribeFrame); + assert.equal(subscribeFrame.id, 1); + socket.fireMessage({ data: `{"id":${String(subscribeFrame.id)},"status":200}` }); + await stream.ready; + + assert.equal(typeof fixture.frame, "string"); + socket.fireMessage({ data: fixture.frame as string }); + assert.equal(messages.length, 1, "valid fixture frame must reach its typed stream"); + const message = record(messages[0]); + assert.ok(message, "typed event must be an object"); + const expected = record(fixture.expect); + assert.ok(expected); + assertExpectedFields(message, expected); + } finally { + client.close(); + } + }); + } +}); diff --git a/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts b/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts new file mode 100644 index 00000000..8bf7e688 --- /dev/null +++ b/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts @@ -0,0 +1,120 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { GeminiWebSocket } from "../../websocket/server.js"; +import type { + DepthUpdatesOptions, + PartialDepthOptions, + WebSocketStream, +} from "../../websocket/public.js"; +import type { + WebSocketAccountIntervalOptions, + WebSocketScopeOptions, +} from "../../websocket/server.js"; +import type { AuthStrategy } from "../../transport/http.js"; +import { parseBoundaryRecord } from "../support/http-fixtures.js"; +import { createWebSocketHarness } from "../support/ws-harness.js"; +import type { BoundaryRecord, BoundaryValue } from "../../utils/boundary-value.js"; +import { isBoundaryObject } from "../../utils/boundary-value.js"; +import { loadCase, loadManifest } from "./support/fixtures.js"; + +async function flush(): Promise { + await new Promise((resolve) => setImmediate(resolve)); +} + +function auth(): AuthStrategy { + return { + nextNonce: () => "1700000000", + credentialHeaders: async (payloadBase64) => ({ + "X-GEMINI-APIKEY": "conformance-api-key", + "X-GEMINI-SIGNATURE": `conformance:${payloadBase64}`, + }), + }; +} + +function record(value: BoundaryValue | undefined): BoundaryRecord | undefined { + return isBoundaryObject(value) ? value : undefined; +} + +function subscribe(client: GeminiWebSocket, fixture: BoundaryRecord): WebSocketStream { + const stream = String(fixture.stream); + const symbol = typeof fixture.symbol === "string" ? fixture.symbol : undefined; + const options = record(fixture.options); + switch (stream) { + case "trades": + assert.equal(typeof symbol, "string"); + return client.public.trades(symbol!) as unknown as WebSocketStream; + case "bookTicker": + assert.equal(typeof symbol, "string"); + return client.public.bookTicker(symbol!) as unknown as WebSocketStream; + case "depthUpdates": + assert.equal(typeof symbol, "string"); + return client.public.depthUpdates(symbol!, options as unknown as DepthUpdatesOptions) as unknown as WebSocketStream; + case "partialDepth": + assert.equal(typeof symbol, "string"); + return client.public.depth(symbol!, options as unknown as PartialDepthOptions) as unknown as WebSocketStream; + case "contractStatus": + return client.public.contractStatus() as unknown as WebSocketStream; + case "orders": + return client.private.orders(options as unknown as WebSocketScopeOptions) as unknown as WebSocketStream; + case "balances": + return client.private.balances(options as unknown as WebSocketAccountIntervalOptions) as unknown as WebSocketStream; + case "positions": + return client.private.positions(options as unknown as WebSocketAccountIntervalOptions) as unknown as WebSocketStream; + default: + assert.fail(`unsupported wsSubscription stream: ${stream}`); + } +} + +function expectedParams(value: BoundaryValue): string[] { + assert.ok(Array.isArray(value), "subscription fixture expect.params must be an array"); + return value.map((item) => { + assert.equal(typeof item, "string"); + return item as string; + }); +} + +test("wsSubscription fixtures emit exact subscribe frames", async () => { + const manifest = await loadManifest(); + const suite = manifest.suites.find((candidate) => candidate.kind === "wsSubscription"); + assert.ok(suite, "manifest must declare a wsSubscription suite"); + + for (const caseId of suite.cases) { + await test(caseId, async () => { + const fixture = await loadCase(suite.id, caseId); + assert.equal(fixture.kind, "wsSubscription"); + const needsAuth = ["orders", "balances", "positions"].includes(String(fixture.stream)); + const harness = createWebSocketHarness(); + const client = new GeminiWebSocket({ + url: "wss://example.test", + auth: needsAuth ? auth() : undefined, + socketFactory: harness.socketFactory, + }); + try { + const stream = subscribe(client, fixture); + await flush(); + assert.equal(harness.sockets.length, 1, "one fixture subscription must create one socket"); + const socket = harness.sockets[0]!; + socket.fireOpen(); + await flush(); + + const sent = socket.sent + .map((frame) => parseBoundaryRecord(frame)) + .filter((frame) => frame.method === "SUBSCRIBE"); + assert.equal(sent.length, 1, "one fixture subscription must emit one SUBSCRIBE frame"); + const frame = sent[0]!; + const expected = record(fixture.expect); + assert.ok(expected); + assert.equal(frame.method, expected.method); + assert.deepEqual(frame.params, expectedParams(expected.params)); + assert.equal(typeof frame.id, "number"); + assert.equal(frame.id, 1, "subscription IDs are scoped to the WebSocket session"); + + socket.fireMessage({ data: `{"id":${String(frame.id)},"status":200}` }); + await stream.ready; + } finally { + client.close(); + } + }); + } +}); diff --git a/packages/sdk-typescript/src/websocket/public.ts b/packages/sdk-typescript/src/websocket/public.ts index ee1806dd..ad9c909e 100644 --- a/packages/sdk-typescript/src/websocket/public.ts +++ b/packages/sdk-typescript/src/websocket/public.ts @@ -745,10 +745,11 @@ export class PublicGeminiWebSocket { } function normalizedSymbol(symbol: string): string { - if (!isBoundaryString(symbol) || symbol.length === 0) { + const normalized = isBoundaryString(symbol) ? symbol.trim().toLowerCase() : ""; + if (normalized.length === 0) { throw new SdkError("symbol is required"); } - return symbol.toLowerCase(); + return normalized; } function streamSymbol(name: string): string | undefined { diff --git a/specs/SOURCES.json b/specs/SOURCES.json new file mode 100644 index 00000000..7024d37d --- /dev/null +++ b/specs/SOURCES.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "specs": [ + { + "id": "rest", + "path": "openapi/rest.yaml", + "url": "https://developer.gemini.com/specs/openapi/rest.yaml", + "sha256": "a07130327a2346e640fc115271dd18984cb9389793acf9fcce6b97ad918e44e4", + "fetchedAt": "2026-09-16T22:39:02.865Z" + }, + { + "id": "predictionMarkets", + "path": "openapi/prediction-markets.yaml", + "url": "https://developer.gemini.com/specs/openapi/prediction-markets.yaml", + "sha256": "1b7403cba3af9af5e64cdbf0a3f33a329876b5e48b2e13b8a9fffb347368b427", + "fetchedAt": "2026-09-16T22:39:03.027Z" + }, + { + "id": "websocket", + "path": "asyncapi/websocket.yaml", + "url": "https://developer.gemini.com/specs/asyncapi/websocket.yaml", + "sha256": "d83c624336f16542c3f1f4554101e7fa19bbc703012ae7bcd780c401667a5def", + "fetchedAt": "2026-09-16T22:39:03.368Z" + } + ] +} diff --git a/specs/asyncapi/websocket.yaml b/specs/asyncapi/websocket.yaml new file mode 100644 index 00000000..9eb49eef --- /dev/null +++ b/specs/asyncapi/websocket.yaml @@ -0,0 +1,1618 @@ +asyncapi: 3.0.0 +info: + title: Gemini WebSocket API + version: 0.10.7 + description: | + Machine-readable contract for the production Gemini WebSocket API at + `wss://ws.gemini.com`. + + This specification models the shared JSON request/response envelope, market + data streams, authenticated order/balance/position streams (including terminal + Prediction Markets settlement details), WebSocket order management methods, + and Prediction Markets combo RFQ streams/methods that + are documented in the Gemini API docs. + + The combo RFQ streams and methods are available in production for beta testing. + Quoting requires an eligible account with the required capabilities. + + Symbols may be spot/derivatives symbols such as `btcusd` or prediction + market instrument symbols such as `GEMI-BTC05M2606011000-UP`, depending on + the product surface. + contact: + name: Gemini API Support +defaultContentType: application/json +x-gemini-coverage: + legend: + machine-readable: Request, response, or event payload fields are modeled in this spec. + partial: The documented request or event is modeled, but at least one result payload is intentionally broad because the docs do not enumerate its fields. + docs-only: The surface is documented for humans but not modeled in this spec. + streams: + - name: "{symbol}@bookTicker" + status: machine-readable + auth: public + docs: /websocket/streams#book-ticker + - name: "{symbol}@depth5" + status: machine-readable + auth: public + docs: /websocket/streams#l2-partial-depth-streams + - name: "{symbol}@depth10" + status: machine-readable + auth: public + docs: /websocket/streams#l2-partial-depth-streams + - name: "{symbol}@depth20" + status: machine-readable + auth: public + docs: /websocket/streams#l2-partial-depth-streams + - name: "{symbol}@depth5@100ms" + status: machine-readable + auth: public + docs: /websocket/streams#l2-partial-depth-streams + - name: "{symbol}@depth10@100ms" + status: machine-readable + auth: public + docs: /websocket/streams#l2-partial-depth-streams + - name: "{symbol}@depth20@100ms" + status: machine-readable + auth: public + docs: /websocket/streams#l2-partial-depth-streams + - name: "{symbol}@depth" + status: machine-readable + auth: public + docs: /websocket/streams#l2-differential-depth-streams + - name: "{symbol}@depth@100ms" + status: machine-readable + auth: public + docs: /websocket/streams#l2-differential-depth-streams + - name: "{symbol}@trade" + status: machine-readable + auth: public + docs: /websocket/streams#trade-stream + - name: orders@account + status: machine-readable + auth: authenticated + docs: /websocket/streams#order-events + - name: orders@session + status: machine-readable + auth: authenticated + docs: /websocket/streams#order-events + - name: balances@account + status: machine-readable + auth: authenticated + docs: /websocket/streams#balance-updates + - name: balances@account@1s + status: machine-readable + auth: authenticated + docs: /websocket/streams#balance-updates + - name: positions@account + status: machine-readable + auth: authenticated + docs: /prediction-markets/websocket/streams#position-updates + - name: positions@account@1s + status: machine-readable + auth: authenticated + docs: /prediction-markets/websocket/streams#position-updates + - name: contractStatus + status: machine-readable + auth: public + docs: /websocket/streams#contract-status + - name: requestForQuote + status: machine-readable + auth: public + docs: /prediction-markets/combos-rfq/websocket-streams#requestforquote + notes: Combo RFQ WebSocket surface is available in production for beta testing; quoting requires an eligible account with the required capabilities. + - name: requestForQuote@account + status: machine-readable + auth: authenticated + docs: /prediction-markets/combos-rfq/websocket-streams#requestforquoteaccount + notes: Combo RFQ WebSocket surface is available in production for beta testing; quoting requires an eligible account with the required capabilities. + - name: requestForQuote@session + status: machine-readable + auth: authenticated + docs: /prediction-markets/combos-rfq/websocket-streams#requestforquotesession + notes: Combo RFQ WebSocket surface is available in production for beta testing; quoting requires an eligible account with the required capabilities. + methods: + - name: conninfo + status: partial + auth: public + docs: /websocket/playground#method-conninfo + notes: Request envelope is modeled; result fields are not enumerated in the docs. + - name: ping + status: partial + auth: public + docs: /websocket/playground#method-ping + notes: Request envelope is modeled; result fields are not enumerated in the docs. + - name: time + status: partial + auth: public + docs: /websocket/playground#method-time + notes: Request envelope is modeled; result fields are not enumerated in the docs. + - name: SUBSCRIBE + status: machine-readable + auth: conditional + docs: /prediction-markets/websocket/playground#method-subscribe + - name: subscribe + status: machine-readable + auth: conditional + docs: /trading/websocket/playground#method-subscribe + - name: UNSUBSCRIBE + status: machine-readable + auth: conditional + docs: /prediction-markets/websocket/playground#method-unsubscribe + - name: unsubscribe + status: machine-readable + auth: conditional + docs: /trading/websocket/playground#method-unsubscribe + - name: LIST_SUBSCRIPTIONS + status: machine-readable + auth: conditional + docs: /prediction-markets/websocket/playground#method-list_subscriptions + - name: list_subscriptions + status: machine-readable + auth: conditional + docs: /trading/websocket/playground#method-list_subscriptions + - name: depth + status: machine-readable + auth: public + docs: /websocket/playground#method-depth + - name: order.place + status: partial + auth: authenticated + docs: /websocket/playground#method-order.place + notes: Request payload is modeled; rely on `orderUpdate` events for authoritative lifecycle state. + - name: order.cancel + status: partial + auth: authenticated + docs: /websocket/playground#method-order.cancel + notes: Request payload is modeled; rely on `orderUpdate` events for authoritative lifecycle state. + - name: order.cancel_all + status: partial + auth: authenticated + docs: /websocket/playground#method-order.cancel_all + notes: Request payload is modeled; result fields are not enumerated in the docs. + - name: order.cancel_session + status: partial + auth: authenticated + docs: /websocket/playground#method-order.cancel_session + notes: Request payload is modeled; result fields are not enumerated in the docs. + - name: rfq.submit_quote + status: machine-readable + auth: authenticated + docs: /prediction-markets/combos-rfq/quote-methods#rfqsubmit_quote + notes: Combo RFQ WebSocket surface is available in production for beta testing; quoting requires an eligible account with the required capabilities. + - name: rfq.withdraw_quote + status: machine-readable + auth: authenticated + docs: /prediction-markets/combos-rfq/quote-methods#rfqwithdraw_quote + notes: Combo RFQ WebSocket surface is available in production for beta testing; quoting requires an eligible account with the required capabilities. + - name: rfq.confirm_quote + status: machine-readable + auth: authenticated + docs: /prediction-markets/combos-rfq/quote-methods#rfqconfirm_quote + notes: Combo RFQ WebSocket surface is available in production for beta testing; quoting requires an eligible account with the required capabilities. +x-gemini-authentication: + timing: websocket-upgrade + alternatives: + - scheme: geminiApiKeySignature + summary: HMAC-signed, account-scoped Gemini API key headers. + - scheme: geminiOAuthBearer + summary: OAuth 2.0 bearer token in the Authorization header. + notes: + - Authentication, when required, is supplied during the initial WebSocket upgrade; clients cannot authenticate or rotate credentials after the connection opens. + - Browser WebSocket clients cannot send custom upgrade headers for authenticated streams and methods; use a non-browser client or server-side proxy. + - Public streams and utility methods do not require authentication. Account/session streams, order methods, and combo RFQ quote methods require one of the listed authentication alternatives. +servers: + production: + host: ws.gemini.com + protocol: wss + pathname: / + description: Production WebSocket API. + sandbox: + host: ws.sandbox.gemini.com + protocol: wss + pathname: / + description: Sandbox environment. +channels: + connection: + address: / + title: WebSocket connection + description: | + JSON request/response control plane over a single WebSocket connection. + Authentication, when required, is supplied during the WebSocket upgrade. + messages: + conninfoRequest: + $ref: '#/components/messages/ConninfoRequest' + pingRequest: + $ref: '#/components/messages/PingRequest' + timeRequest: + $ref: '#/components/messages/TimeRequest' + subscribeRequest: + $ref: '#/components/messages/SubscribeRequest' + unsubscribeRequest: + $ref: '#/components/messages/UnsubscribeRequest' + listSubscriptionsRequest: + $ref: '#/components/messages/ListSubscriptionsRequest' + depthRequest: + $ref: '#/components/messages/DepthRequest' + orderPlaceRequest: + $ref: '#/components/messages/OrderPlaceRequest' + orderCancelRequest: + $ref: '#/components/messages/OrderCancelRequest' + orderCancelAllRequest: + $ref: '#/components/messages/OrderCancelAllRequest' + orderCancelSessionRequest: + $ref: '#/components/messages/OrderCancelSessionRequest' + rfqSubmitQuoteRequest: + $ref: '#/components/messages/RfqSubmitQuoteRequest' + rfqWithdrawQuoteRequest: + $ref: '#/components/messages/RfqWithdrawQuoteRequest' + rfqConfirmQuoteRequest: + $ref: '#/components/messages/RfqConfirmQuoteRequest' + genericSuccessResponse: + $ref: '#/components/messages/GenericSuccessResponse' + listSubscriptionsResponse: + $ref: '#/components/messages/ListSubscriptionsResponse' + depthResponse: + $ref: '#/components/messages/DepthResponse' + orderActionResponse: + $ref: '#/components/messages/OrderActionResponse' + rfqSubmitQuoteResponse: + $ref: '#/components/messages/RfqSubmitQuoteResponse' + rfqWithdrawQuoteResponse: + $ref: '#/components/messages/RfqWithdrawQuoteResponse' + rfqConfirmQuoteResponse: + $ref: '#/components/messages/RfqConfirmQuoteResponse' + errorResponse: + $ref: '#/components/messages/ErrorResponse' + bookTicker: + address: "{symbol}@bookTicker" + messages: + bookTicker: + $ref: '#/components/messages/BookTicker' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + depth5: + address: "{symbol}@depth5" + messages: + orderBookSnapshot: + $ref: '#/components/messages/OrderBookSnapshot' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + depth10: + address: "{symbol}@depth10" + messages: + orderBookSnapshot: + $ref: '#/components/messages/OrderBookSnapshot' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + depth20: + address: "{symbol}@depth20" + messages: + orderBookSnapshot: + $ref: '#/components/messages/OrderBookSnapshot' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + depth5Fast: + address: "{symbol}@depth5@100ms" + messages: + orderBookSnapshot: + $ref: '#/components/messages/OrderBookSnapshot' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + depth10Fast: + address: "{symbol}@depth10@100ms" + messages: + orderBookSnapshot: + $ref: '#/components/messages/OrderBookSnapshot' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + depth20Fast: + address: "{symbol}@depth20@100ms" + messages: + orderBookSnapshot: + $ref: '#/components/messages/OrderBookSnapshot' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + depth: + address: "{symbol}@depth" + messages: + depthUpdate: + $ref: '#/components/messages/DepthUpdate' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + depthFast: + address: "{symbol}@depth@100ms" + messages: + depthUpdate: + $ref: '#/components/messages/DepthUpdate' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + trade: + address: "{symbol}@trade" + messages: + trade: + $ref: '#/components/messages/Trade' + parameters: + symbol: + $ref: '#/components/parameters/symbol' + ordersAccount: + address: orders@account + messages: + orderUpdate: + $ref: '#/components/messages/OrderUpdate' + ordersSession: + address: orders@session + messages: + orderUpdate: + $ref: '#/components/messages/OrderUpdate' + balancesAccount: + address: balances@account + messages: + balanceUpdate: + $ref: '#/components/messages/BalanceUpdate' + balancesAccountSnapshot: + address: balances@account@1s + messages: + balanceUpdate: + $ref: '#/components/messages/BalanceUpdate' + positionsAccount: + address: positions@account + messages: + positionReport: + $ref: '#/components/messages/PositionReport' + positionsAccountSnapshot: + address: positions@account@1s + messages: + positionReport: + $ref: '#/components/messages/PositionReport' + contractStatus: + address: contractStatus + messages: + contractStatus: + $ref: '#/components/messages/ContractStatus' + requestForQuote: + address: requestForQuote + messages: + requestForQuote: + $ref: '#/components/messages/RfqPublicEvent' + requestForQuoteAccount: + address: requestForQuote@account + messages: + requestForQuote: + $ref: '#/components/messages/RfqPrivateDelivery' + requestForQuoteSession: + address: requestForQuote@session + messages: + requestForQuote: + $ref: '#/components/messages/RfqPrivateDelivery' +operations: + sendRequests: + action: send + channel: + $ref: '#/channels/connection' + x-gemini-auth: + required: conditional + timing: websocket-upgrade + notes: Public utility methods, public stream subscriptions, and the depth request work without authentication. Order management, combo RFQ quote methods, and subscriptions to authenticated stream names require an already-authenticated WebSocket connection; see each coverage entry's `auth` value. + messages: + - $ref: '#/channels/connection/messages/conninfoRequest' + - $ref: '#/channels/connection/messages/pingRequest' + - $ref: '#/channels/connection/messages/timeRequest' + - $ref: '#/channels/connection/messages/subscribeRequest' + - $ref: '#/channels/connection/messages/unsubscribeRequest' + - $ref: '#/channels/connection/messages/listSubscriptionsRequest' + - $ref: '#/channels/connection/messages/depthRequest' + - $ref: '#/channels/connection/messages/orderPlaceRequest' + - $ref: '#/channels/connection/messages/orderCancelRequest' + - $ref: '#/channels/connection/messages/orderCancelAllRequest' + - $ref: '#/channels/connection/messages/orderCancelSessionRequest' + - $ref: '#/channels/connection/messages/rfqSubmitQuoteRequest' + - $ref: '#/channels/connection/messages/rfqWithdrawQuoteRequest' + - $ref: '#/channels/connection/messages/rfqConfirmQuoteRequest' + receiveResponses: + action: receive + channel: + $ref: '#/channels/connection' + x-gemini-auth: + required: conditional + timing: websocket-upgrade + notes: The response envelope is shared by public and authenticated methods. Authentication requirements are modeled on authenticated request messages and private stream receive operations. + messages: + - $ref: '#/channels/connection/messages/genericSuccessResponse' + - $ref: '#/channels/connection/messages/listSubscriptionsResponse' + - $ref: '#/channels/connection/messages/depthResponse' + - $ref: '#/channels/connection/messages/orderActionResponse' + - $ref: '#/channels/connection/messages/rfqSubmitQuoteResponse' + - $ref: '#/channels/connection/messages/rfqWithdrawQuoteResponse' + - $ref: '#/channels/connection/messages/rfqConfirmQuoteResponse' + - $ref: '#/channels/connection/messages/errorResponse' + receiveBookTicker: + action: receive + channel: + $ref: '#/channels/bookTicker' + messages: + - $ref: '#/channels/bookTicker/messages/bookTicker' + receivePartialDepth: + action: receive + channel: + $ref: '#/channels/depth10' + messages: + - $ref: '#/channels/depth10/messages/orderBookSnapshot' + receivePartialDepth5: + action: receive + channel: + $ref: '#/channels/depth5' + messages: + - $ref: '#/channels/depth5/messages/orderBookSnapshot' + receivePartialDepth20: + action: receive + channel: + $ref: '#/channels/depth20' + messages: + - $ref: '#/channels/depth20/messages/orderBookSnapshot' + receivePartialDepth5Fast: + action: receive + channel: + $ref: '#/channels/depth5Fast' + messages: + - $ref: '#/channels/depth5Fast/messages/orderBookSnapshot' + receivePartialDepth10Fast: + action: receive + channel: + $ref: '#/channels/depth10Fast' + messages: + - $ref: '#/channels/depth10Fast/messages/orderBookSnapshot' + receivePartialDepth20Fast: + action: receive + channel: + $ref: '#/channels/depth20Fast' + messages: + - $ref: '#/channels/depth20Fast/messages/orderBookSnapshot' + receiveDifferentialDepth: + action: receive + channel: + $ref: '#/channels/depthFast' + messages: + - $ref: '#/channels/depthFast/messages/depthUpdate' + receiveDifferentialDepthStandard: + action: receive + channel: + $ref: '#/channels/depth' + messages: + - $ref: '#/channels/depth/messages/depthUpdate' + receiveTrades: + action: receive + channel: + $ref: '#/channels/trade' + messages: + - $ref: '#/channels/trade/messages/trade' + receiveOrderUpdates: + action: receive + channel: + $ref: '#/channels/ordersAccount' + security: + - $ref: '#/components/securitySchemes/geminiApiKeySignature' + - $ref: '#/components/securitySchemes/geminiOAuthBearer' + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + messages: + - $ref: '#/channels/ordersAccount/messages/orderUpdate' + receiveSessionOrderUpdates: + action: receive + channel: + $ref: '#/channels/ordersSession' + security: + - $ref: '#/components/securitySchemes/geminiApiKeySignature' + - $ref: '#/components/securitySchemes/geminiOAuthBearer' + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + messages: + - $ref: '#/channels/ordersSession/messages/orderUpdate' + receiveBalanceUpdates: + action: receive + channel: + $ref: '#/channels/balancesAccount' + security: + - $ref: '#/components/securitySchemes/geminiApiKeySignature' + - $ref: '#/components/securitySchemes/geminiOAuthBearer' + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + messages: + - $ref: '#/channels/balancesAccount/messages/balanceUpdate' + receiveBalanceSnapshots: + action: receive + channel: + $ref: '#/channels/balancesAccountSnapshot' + security: + - $ref: '#/components/securitySchemes/geminiApiKeySignature' + - $ref: '#/components/securitySchemes/geminiOAuthBearer' + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + messages: + - $ref: '#/channels/balancesAccountSnapshot/messages/balanceUpdate' + receivePositionReports: + action: receive + channel: + $ref: '#/channels/positionsAccount' + security: + - $ref: '#/components/securitySchemes/geminiApiKeySignature' + - $ref: '#/components/securitySchemes/geminiOAuthBearer' + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + messages: + - $ref: '#/channels/positionsAccount/messages/positionReport' + receivePositionReportSnapshots: + action: receive + channel: + $ref: '#/channels/positionsAccountSnapshot' + security: + - $ref: '#/components/securitySchemes/geminiApiKeySignature' + - $ref: '#/components/securitySchemes/geminiOAuthBearer' + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + messages: + - $ref: '#/channels/positionsAccountSnapshot/messages/positionReport' + receiveContractStatus: + action: receive + channel: + $ref: '#/channels/contractStatus' + messages: + - $ref: '#/channels/contractStatus/messages/contractStatus' + receiveRfqPublicEvents: + action: receive + channel: + $ref: '#/channels/requestForQuote' + messages: + - $ref: '#/channels/requestForQuote/messages/requestForQuote' + receiveRfqPrivateDeliveries: + action: receive + channel: + $ref: '#/channels/requestForQuoteAccount' + security: + - $ref: '#/components/securitySchemes/geminiApiKeySignature' + - $ref: '#/components/securitySchemes/geminiOAuthBearer' + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + capabilities: [view_orders] + messages: + - $ref: '#/channels/requestForQuoteAccount/messages/requestForQuote' + receiveRfqSessionDeliveries: + action: receive + channel: + $ref: '#/channels/requestForQuoteSession' + security: + - $ref: '#/components/securitySchemes/geminiApiKeySignature' + - $ref: '#/components/securitySchemes/geminiOAuthBearer' + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + capabilities: [view_orders] + messages: + - $ref: '#/channels/requestForQuoteSession/messages/requestForQuote' +components: + securitySchemes: + geminiApiKeySignature: + type: httpApiKey + name: X-GEMINI-APIKEY + in: header + description: | + Account-scoped Gemini API key supplied during the WebSocket upgrade. + This scheme represents the full Gemini HMAC-signed header set. A valid + handshake also includes `X-GEMINI-NONCE`, `X-GEMINI-PAYLOAD`, and + `X-GEMINI-SIGNATURE`; `X-GEMINI-PAYLOAD` is `base64(string(nonce))`, + and `X-GEMINI-SIGNATURE` is `hex(hmac_sha384(payload, api_secret))`. + Only account-scoped keys with time-based nonces are accepted; master or + group keys are rejected with HTTP 401. + x-gemini-required-headers: + - X-GEMINI-APIKEY + - X-GEMINI-NONCE + - X-GEMINI-PAYLOAD + - X-GEMINI-SIGNATURE + x-gemini-key-scope: account + x-gemini-nonce: time-based + x-gemini-signature-algorithm: HMAC-SHA384 + geminiOAuthBearer: + type: http + scheme: bearer + bearerFormat: Gemini OAuth 2.0 access token + description: | + OAuth 2.0 access token supplied as `Authorization: Bearer ` + during the WebSocket upgrade. The token must include scopes/capabilities + for the private streams or methods used on the connection. + parameters: + symbol: + description: | + Exchange symbol. For trading markets this may be a symbol like `btcusd`. + For prediction markets this is an instrument symbol such as + `GEMI-BTC05M2606011000-UP`. + messages: + ConninfoRequest: + name: ConninfoRequest + title: conninfo request + payload: + $ref: '#/components/schemas/ConninfoRequest' + PingRequest: + name: PingRequest + title: ping request + payload: + $ref: '#/components/schemas/PingRequest' + TimeRequest: + name: TimeRequest + title: time request + payload: + $ref: '#/components/schemas/TimeRequest' + SubscribeRequest: + name: SubscribeRequest + title: subscribe request + payload: + $ref: '#/components/schemas/SubscribeRequest' + UnsubscribeRequest: + name: UnsubscribeRequest + title: unsubscribe request + payload: + $ref: '#/components/schemas/UnsubscribeRequest' + ListSubscriptionsRequest: + name: ListSubscriptionsRequest + title: list subscriptions request + payload: + $ref: '#/components/schemas/ListSubscriptionsRequest' + DepthRequest: + name: DepthRequest + title: depth request + payload: + $ref: '#/components/schemas/DepthRequest' + OrderPlaceRequest: + name: OrderPlaceRequest + title: order.place request + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + roles: [Trader] + payload: + $ref: '#/components/schemas/OrderPlaceRequest' + OrderCancelRequest: + name: OrderCancelRequest + title: order.cancel request + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + roles: [Trader] + payload: + $ref: '#/components/schemas/OrderCancelRequest' + OrderCancelAllRequest: + name: OrderCancelAllRequest + title: order.cancel_all request + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + roles: [Trader] + payload: + $ref: '#/components/schemas/OrderCancelAllRequest' + OrderCancelSessionRequest: + name: OrderCancelSessionRequest + title: order.cancel_session request + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + roles: [Trader] + payload: + $ref: '#/components/schemas/OrderCancelSessionRequest' + RfqSubmitQuoteRequest: + name: RfqSubmitQuoteRequest + title: rfq.submit_quote request + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + capabilities: [place_orders] + payload: + $ref: '#/components/schemas/RfqSubmitQuoteRequest' + RfqWithdrawQuoteRequest: + name: RfqWithdrawQuoteRequest + title: rfq.withdraw_quote request + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + capabilities: [cancel_orders] + payload: + $ref: '#/components/schemas/RfqWithdrawQuoteRequest' + RfqConfirmQuoteRequest: + name: RfqConfirmQuoteRequest + title: rfq.confirm_quote request + x-gemini-auth: + required: true + timing: websocket-upgrade + alternatives: [geminiApiKeySignature, geminiOAuthBearer] + capabilities: [place_orders] + payload: + $ref: '#/components/schemas/RfqConfirmQuoteRequest' + GenericSuccessResponse: + name: GenericSuccessResponse + title: generic success response + payload: + $ref: '#/components/schemas/GenericSuccessResponse' + ListSubscriptionsResponse: + name: ListSubscriptionsResponse + title: list subscriptions response + payload: + $ref: '#/components/schemas/ListSubscriptionsResponse' + DepthResponse: + name: DepthResponse + title: depth response + payload: + $ref: '#/components/schemas/DepthResponse' + OrderActionResponse: + name: OrderActionResponse + title: order action response + payload: + $ref: '#/components/schemas/OrderActionResponse' + RfqSubmitQuoteResponse: + name: RfqSubmitQuoteResponse + title: rfq.submit_quote response + payload: + $ref: '#/components/schemas/RfqSubmitQuoteResponse' + RfqWithdrawQuoteResponse: + name: RfqWithdrawQuoteResponse + title: rfq.withdraw_quote response + payload: + $ref: '#/components/schemas/RfqWithdrawQuoteResponse' + RfqConfirmQuoteResponse: + name: RfqConfirmQuoteResponse + title: rfq.confirm_quote response + payload: + $ref: '#/components/schemas/RfqConfirmQuoteResponse' + ErrorResponse: + name: ErrorResponse + title: error response + payload: + $ref: '#/components/schemas/ErrorResponse' + BookTicker: + name: BookTicker + title: book ticker + payload: + $ref: '#/components/schemas/BookTicker' + OrderBookSnapshot: + name: OrderBookSnapshot + title: order book snapshot + payload: + $ref: '#/components/schemas/OrderBookSnapshot' + DepthUpdate: + name: DepthUpdate + title: L2 differential depth update + payload: + $ref: '#/components/schemas/DepthUpdate' + Trade: + name: Trade + title: trade + payload: + $ref: '#/components/schemas/Trade' + OrderUpdate: + name: OrderUpdate + title: order update + payload: + $ref: '#/components/schemas/OrderUpdate' + BalanceUpdate: + name: BalanceUpdate + title: balance update + payload: + $ref: '#/components/schemas/BalanceUpdate' + PositionReport: + name: PositionReport + title: position report + payload: + $ref: '#/components/schemas/PositionReport' + ContractStatus: + name: ContractStatus + title: contract status + payload: + $ref: '#/components/schemas/ContractStatus' + RfqPublicEvent: + name: RfqPublicEvent + title: public request-for-quote event + payload: + $ref: '#/components/schemas/RfqPublicEvent' + RfqPrivateDelivery: + name: RfqPrivateDelivery + title: private request-for-quote delivery + payload: + $ref: '#/components/schemas/RfqPrivateDelivery' + schemas: + CorrelationId: + description: Client-chosen id, echoed on the matching response. + oneOf: + - type: string + - type: integer + NanosecondTimestamp: + type: integer + format: int64 + description: | + Unix timestamp in nanoseconds. Values exceed JavaScript's safe integer + range; JavaScript clients that need exact values should parse + losslessly. + MillisecondTimestamp: + type: integer + format: int64 + description: Unix timestamp in milliseconds. + DecimalString: + type: string + description: Decimal value encoded as a string. + PriceLevel: + type: array + description: | + `[price, quantity]` tuple; both values are decimal strings. A quantity + of zero removes that price level in differential depth updates. + items: + type: string + minItems: 2 + maxItems: 2 + RequestBase: + type: object + required: [id, method] + properties: + id: + $ref: '#/components/schemas/CorrelationId' + method: + type: string + additionalProperties: true + ConninfoRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method] + properties: + method: + type: string + const: conninfo + params: + type: object + additionalProperties: true + PingRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method] + properties: + method: + type: string + const: ping + params: + type: object + additionalProperties: true + TimeRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method] + properties: + method: + type: string + const: time + params: + type: object + additionalProperties: true + SubscribeRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method, params] + properties: + method: + type: string + pattern: ^(SUBSCRIBE|subscribe)$ + description: | + `SUBSCRIBE` for Prediction Markets docs; lowercase `subscribe` + is also accepted by the shared Trading WebSocket docs. + params: + type: array + description: Stream names to subscribe to. + items: + type: string + UnsubscribeRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method, params] + properties: + method: + type: string + pattern: ^(UNSUBSCRIBE|unsubscribe)$ + description: | + `UNSUBSCRIBE` for Prediction Markets docs; lowercase + `unsubscribe` is also accepted by the shared Trading WebSocket + docs. + params: + type: array + description: Stream names to unsubscribe from. + items: + type: string + ListSubscriptionsRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method] + properties: + method: + type: string + pattern: ^(LIST_SUBSCRIPTIONS|list_subscriptions)$ + description: | + `LIST_SUBSCRIPTIONS` for Prediction Markets docs; lowercase + `list_subscriptions` is also accepted by the shared Trading + WebSocket docs. + params: + type: object + additionalProperties: true + DepthRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method, params] + properties: + method: + type: string + const: depth + params: + type: object + required: [symbol] + properties: + symbol: + type: string + limit: + type: integer + minimum: 1 + maximum: 5000 + default: 100 + additionalProperties: true + OrderPlaceRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method, params] + properties: + method: + type: string + const: order.place + params: + $ref: '#/components/schemas/OrderPlaceParams' + OrderCancelRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method, params] + properties: + method: + type: string + const: order.cancel + params: + $ref: '#/components/schemas/OrderCancelParams' + OrderCancelAllRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method] + properties: + method: + type: string + const: order.cancel_all + params: + type: object + additionalProperties: true + OrderCancelSessionRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method] + properties: + method: + type: string + const: order.cancel_session + params: + type: object + additionalProperties: true + RfqSubmitQuoteRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method, params] + properties: + method: + type: string + const: rfq.submit_quote + params: + $ref: '#/components/schemas/RfqSubmitQuoteParams' + RfqWithdrawQuoteRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method, params] + properties: + method: + type: string + const: rfq.withdraw_quote + params: + $ref: '#/components/schemas/RfqWithdrawQuoteParams' + RfqConfirmQuoteRequest: + allOf: + - $ref: '#/components/schemas/RequestBase' + - type: object + required: [method, params] + properties: + method: + type: string + const: rfq.confirm_quote + params: + $ref: '#/components/schemas/RfqConfirmQuoteParams' + ResponseBase: + type: object + required: [id, status] + properties: + id: + $ref: '#/components/schemas/CorrelationId' + status: + type: integer + description: HTTP-style status code. + additionalProperties: true + GenericSuccessResponse: + allOf: + - $ref: '#/components/schemas/ResponseBase' + - type: object + properties: + result: + description: Method-specific result. Some utility result fields are not enumerated in the docs. + ListSubscriptionsResponse: + allOf: + - $ref: '#/components/schemas/ResponseBase' + - type: object + properties: + result: + type: array + items: + type: string + DepthResponse: + allOf: + - $ref: '#/components/schemas/ResponseBase' + - type: object + properties: + result: + $ref: '#/components/schemas/OrderBookSnapshot' + OrderActionResponse: + allOf: + - $ref: '#/components/schemas/ResponseBase' + - type: object + properties: + result: + type: object + description: | + Method-specific order result. Order lifecycle state is carried + authoritatively on the `orderUpdate` stream. + additionalProperties: true + RfqSubmitQuoteResponse: + allOf: + - $ref: '#/components/schemas/ResponseBase' + - type: object + properties: + result: + type: object + required: [rfqId, quoteId] + properties: + rfqId: + type: string + quoteId: + type: string + additionalProperties: true + RfqWithdrawQuoteResponse: + allOf: + - $ref: '#/components/schemas/ResponseBase' + - type: object + properties: + result: + type: object + required: [rfqId, quoteId] + properties: + rfqId: + type: string + quoteId: + type: string + additionalProperties: true + RfqConfirmQuoteResponse: + allOf: + - $ref: '#/components/schemas/ResponseBase' + - type: object + properties: + result: + type: object + required: [rfqId, quoteId, confirmed] + properties: + rfqId: + type: string + quoteId: + type: string + confirmed: + type: boolean + additionalProperties: true + ErrorResponse: + allOf: + - $ref: '#/components/schemas/ResponseBase' + - type: object + required: [error] + properties: + error: + $ref: '#/components/schemas/WsError' + WsError: + type: object + required: [code, msg] + properties: + code: + type: integer + description: Internal error code, e.g. -1002. + msg: + type: string + description: Human-readable error message. + additionalProperties: true + OrderPlaceParams: + type: object + required: [symbol, side, type, timeInForce, quantity] + properties: + symbol: + type: string + side: + type: string + enum: [BUY, SELL] + type: + type: string + enum: [LIMIT, MARKET] + description: Use LIMIT with price and stopPrice for a stop-limit order. For this WebSocket contract, BUY stopPrice may be equal to or below price and SELL stopPrice may be equal to or above price. The order event reports its type as STOP_LIMIT. The legacy REST order endpoint has separate strict inequality rules. + timeInForce: + type: string + enum: [GTC, IOC, FOK, MOC] + price: + $ref: '#/components/schemas/DecimalString' + stopPrice: + $ref: '#/components/schemas/DecimalString' + quantity: + $ref: '#/components/schemas/DecimalString' + clientOrderId: + type: string + eventOutcome: + type: string + enum: [YES, NO] + description: Required for prediction market event contracts. + additionalProperties: true + OrderCancelParams: + type: object + required: [orderId] + properties: + orderId: + oneOf: + - type: string + - type: integer + additionalProperties: true + OrderBookSnapshot: + type: object + description: Snapshot payload used by partial depth streams and the `depth` method. + required: [lastUpdateId, bids, asks] + properties: + lastUpdateId: + type: integer + format: int64 + bids: + type: array + items: + $ref: '#/components/schemas/PriceLevel' + asks: + type: array + items: + $ref: '#/components/schemas/PriceLevel' + additionalProperties: true + BookTicker: + type: object + required: [u, E, s, b, B, a, A] + properties: + u: + type: integer + format: int64 + description: Update ID. + E: + $ref: '#/components/schemas/NanosecondTimestamp' + s: + type: string + b: + $ref: '#/components/schemas/DecimalString' + B: + $ref: '#/components/schemas/DecimalString' + a: + $ref: '#/components/schemas/DecimalString' + A: + $ref: '#/components/schemas/DecimalString' + c: + $ref: '#/components/schemas/DecimalString' + description: Last trade price, present once the book has traded. + C: + $ref: '#/components/schemas/DecimalString' + description: Last trade size, present once the book has traded. + additionalProperties: true + DepthUpdate: + type: object + description: > + L2 depth frame. With the `snapshot` connection parameter set — `-1` for the + full book, a positive N for the top N levels — the FIRST frame after + subscribing is the snapshot (b/a hold absolute levels, and U..u identifies + the update range covered by the frame); every frame after it is an incremental + diff. There is no separate snapshot message and no lastUpdateId field. Apply + frames in order, using u as the last applied update ID; if a frame's U skips + ahead of the last applied u, discard the book and resubscribe to resync. + required: [e, E, s, U, u, b, a] + properties: + e: + type: string + const: depthUpdate + E: + $ref: '#/components/schemas/NanosecondTimestamp' + s: + type: string + U: + type: integer + format: int64 + u: + type: integer + format: int64 + b: + type: array + items: + $ref: '#/components/schemas/PriceLevel' + a: + type: array + items: + $ref: '#/components/schemas/PriceLevel' + additionalProperties: true + Trade: + type: object + required: [E, s, t, p, q, m] + properties: + E: + $ref: '#/components/schemas/NanosecondTimestamp' + s: + type: string + t: + type: integer + format: int64 + p: + $ref: '#/components/schemas/DecimalString' + q: + $ref: '#/components/schemas/DecimalString' + m: + type: boolean + description: Whether the buyer is the maker. + additionalProperties: true + OrderUpdate: + type: object + description: | + Authenticated order lifecycle event. Fields with empty or zero values may + be omitted from the event. + required: [e, E, s, i, X, T] + properties: + e: + type: string + const: orderUpdate + E: + $ref: '#/components/schemas/NanosecondTimestamp' + s: + type: string + i: + type: integer + format: int64 + c: + type: string + description: Client order ID. For RFQ maker fills, this is the clientId supplied to rfq.submit_quote, or Gemini's deterministic RFQ client order ID when clientId is omitted. + S: + type: string + enum: [BUY, SELL] + o: + type: string + enum: [LIMIT, MARKET, STOP_LIMIT, STOP_MARKET] + X: + type: string + enum: [NEW, OPEN, FILLED, PARTIALLY_FILLED, CANCELED, REJECTED, MODIFIED] + O: + type: string + enum: [YES, NO] + description: Event outcome for prediction market contracts. + p: + $ref: '#/components/schemas/DecimalString' + P: + $ref: '#/components/schemas/DecimalString' + description: Stop price, `0` when not a stop order. + q: + $ref: '#/components/schemas/DecimalString' + z: + $ref: '#/components/schemas/DecimalString' + description: Remaining quantity. + Z: + $ref: '#/components/schemas/DecimalString' + description: | + Executed quantity. For `FILLED` / `PARTIALLY_FILLED`, this is the + last execution quantity. For `CANCELED` and other events, this is + cumulative executed quantity over the order lifetime. + L: + $ref: '#/components/schemas/DecimalString' + t: + type: integer + format: int64 + n: + $ref: '#/components/schemas/DecimalString' + description: Fee amount, only present on fill events. + m: + type: boolean + description: Maker flag on fills. + r: + type: string + description: Rejection or cancellation reason. + T: + $ref: '#/components/schemas/NanosecondTimestamp' + additionalProperties: true + BalanceUpdate: + type: object + required: [e, E, u, B] + properties: + e: + type: string + const: balanceUpdate + E: + $ref: '#/components/schemas/NanosecondTimestamp' + u: + $ref: '#/components/schemas/NanosecondTimestamp' + B: + type: array + items: + $ref: '#/components/schemas/Balance' + additionalProperties: true + Balance: + type: object + required: [a, f, c] + properties: + a: + type: string + description: Asset code. + f: + $ref: '#/components/schemas/DecimalString' + c: + $ref: '#/components/schemas/DecimalString' + additionalProperties: true + PositionReport: + type: object + required: [e, E, u, A, P] + properties: + e: + type: string + const: positionReport + E: + $ref: '#/components/schemas/NanosecondTimestamp' + u: + $ref: '#/components/schemas/NanosecondTimestamp' + A: + type: integer + format: int64 + description: Account ID. + P: + type: array + items: + $ref: '#/components/schemas/PositionRow' + additionalProperties: true + PositionRow: + type: object + required: [t, s, a] + properties: + t: + type: string + description: Product type; event contracts use `ec`. + s: + type: string + a: + type: array + items: + $ref: '#/components/schemas/NamedAmount' + additionalProperties: true + NamedAmount: + type: object + required: [t, v] + properties: + t: + type: string + description: Amount label, such as `position` or `settlement_payout`. + v: + $ref: '#/components/schemas/DecimalString' + description: Decimal amount. The `position` amount is a signed quantity; `settlement_payout` is the settlement payout. + c: + type: string + description: Optional asset code, such as `usd` for settlement payouts. + o: + type: string + enum: [YES, NO, UNSPECIFIED] + description: Settlement outcome. Present on `settlement_payout` amounts in terminal event-contract position rows. + additionalProperties: true + ContractStatus: + type: object + required: [e, E, s, k, c, i, o, n] + properties: + e: + type: string + const: contractStatus + E: + $ref: '#/components/schemas/MillisecondTimestamp' + s: + type: string + k: + type: string + description: Event ticker. + c: + type: string + description: Contract ticker. + i: + type: integer + format: int64 + p: + $ref: '#/components/schemas/DecimalString' + description: Strike price, omitted for Up/Down contracts until populated. + o: + type: string + description: Previous status. + n: + type: string + description: New status. + additionalProperties: true + RfqClientId: + type: string + description: Optional client order ID for tracking the maker fill. It is emitted as the fill event's c field. It must contain only printable ASCII characters and be at most 36 characters. Keep it unique for the maker account because the venue does not deduplicate it. + RfqSubmitQuoteParams: + type: object + required: [rfqId, price, quantity] + properties: + rfqId: + type: string + price: + $ref: '#/components/schemas/DecimalString' + quantity: + $ref: '#/components/schemas/DecimalString' + validUntil: + $ref: '#/components/schemas/MillisecondTimestamp' + clientId: + $ref: '#/components/schemas/RfqClientId' + additionalProperties: true + RfqWithdrawQuoteParams: + type: object + required: [rfqId, quoteId] + properties: + rfqId: + type: string + quoteId: + type: string + additionalProperties: true + RfqConfirmQuoteParams: + type: object + required: [rfqId, quoteId, confirm] + properties: + rfqId: + type: string + quoteId: + type: string + confirm: + type: boolean + additionalProperties: true + RfqLeg: + type: object + required: [c, o] + properties: + c: + type: string + description: CMS contract id. + o: + type: string + enum: [YES, NO] + s: + type: string + description: This leg's own instrument symbol, when available. Distinct from the combo-level `s` on the enclosing event. + example: "GEMI-XRPUSD-260828" + additionalProperties: true + RfqLifecycleState: + type: string + enum: [OPEN, PENDING_ACCEPTANCE, CONFIRMING, FINALIZING, FINALIZED, CANCELLED, EXPIRED, FAILED] + RfqQuoteStatus: + type: string + enum: [ACTIVE, WITHDRAWN, EXPIRED, WON, LOST] + RfqPublicEvent: + type: object + required: [e, E, r, l, S] + properties: + e: + type: string + const: requestForQuote + E: + $ref: '#/components/schemas/MillisecondTimestamp' + r: + type: string + description: RFQ ID. + s: + type: string + description: The combo's deterministic instrument symbol, when available. Market makers can use it to place a CLOB order on the combo, activating its book on demand. + l: + type: array + items: + $ref: '#/components/schemas/RfqLeg' + n: + $ref: '#/components/schemas/DecimalString' + description: Requested notional size, mutually exclusive with `q`. + q: + $ref: '#/components/schemas/DecimalString' + description: Requested contract quantity, mutually exclusive with `n`. The RFQ quantity grid can be whole contracts or 0.01 fractional contracts. + f: + $ref: '#/components/schemas/DecimalString' + description: Execution quantity calculated when the requester accepts. + S: + $ref: '#/components/schemas/RfqLifecycleState' + w: + $ref: '#/components/schemas/MillisecondTimestamp' + x: + $ref: '#/components/schemas/MillisecondTimestamp' + c: + $ref: '#/components/schemas/MillisecondTimestamp' + additionalProperties: true + RfqPrivateDelivery: + type: object + required: [e, i, E, r, x, S] + properties: + e: + type: string + const: requestForQuote + i: + type: string + description: Durable lifecycle-event UUID for at-least-once deduplication. + E: + $ref: '#/components/schemas/MillisecondTimestamp' + r: + type: string + description: RFQ ID. + x: + type: string + enum: [CLOSED, ACCEPTED, CONFIRMED, DECLINED, FINALIZED, FAILED] + description: Customer-visible lifecycle transition. + S: + $ref: '#/components/schemas/RfqLifecycleState' + q: + type: string + description: Quote ID on authenticated delivery streams. + p: + $ref: '#/components/schemas/DecimalString' + sz: + $ref: '#/components/schemas/DecimalString' + qs: + $ref: '#/components/schemas/RfqQuoteStatus' + vu: + $ref: '#/components/schemas/MillisecondTimestamp' + additionalProperties: true diff --git a/specs/openapi/prediction-markets.yaml b/specs/openapi/prediction-markets.yaml new file mode 100644 index 00000000..f07ca89b --- /dev/null +++ b/specs/openapi/prediction-markets.yaml @@ -0,0 +1,5188 @@ +openapi: 3.0.3 +info: + title: Gemini Prediction Markets API + description: | + API for trading prediction market contracts on Gemini. + + **Note:** Only fields documented in this specification are considered stable. Undocumented fields in API responses may change or be removed without notice. + version: 1.0.0 + contact: + name: Gemini API Support + +servers: + - url: https://api.gemini.com + description: Production + - url: https://api.sandbox.gemini.com + description: Sandbox + +tags: + - name: Markets + description: | + Public endpoints for browsing prediction markets. + + **Market Data:** Use these REST endpoints to discover active events and each contract's `instrumentSymbol` (e.g., `GEMI-FEDJAN26-DN25`). For active trading and market making, prefer [Prediction Markets WebSocket streams](/prediction-markets/websocket/streams) with that `instrumentSymbol`. + - [List Events](/rest-api/prediction-markets/events/list-events) - Discover active events and instrument symbols + - [Prediction Markets WebSocket Streams](/prediction-markets/websocket/streams) - Stream prices, depth, order events, and positions + - name: Volume + description: Public, unauthenticated prediction-market trade volume by category and UTC period. + - name: Terms + description: Read, check, and accept the latest Prediction Markets terms for API key and OAuth trading flows. + - name: Trading + description: Authenticated REST endpoints for placing and managing orders. Treat REST order placement as payload reference or a one-off server workflow; for active trading and market making, prefer WebSocket order.place. + - name: Positions + description: Authenticated endpoints for viewing positions and order history. Use REST positions for recovery or audit snapshots after reconnects, missed WebSocket messages, and settlement windows. + - name: Combos + description: Public endpoints for discovering and inspecting combo contracts, plus an authenticated endpoint to create or retrieve a canonical combo. Combos are multi-leg contracts; order entry uses the same place/cancel endpoints as single contracts. + - name: Rewards + description: | + Endpoints for the Maker Rebate and Liquidity Rewards programs. + + - **Maker Rebate** — per-fill rebates earned by resting limit orders that get filled. Rates and rebate multipliers are configured per category. Public rate schedule plus authenticated payout and summary endpoints. + - **Liquidity Rewards** — daily USD reward pools distributed across qualifying makers based on quote uptime, spread, and size. Public config + event listing, authenticated daily and lifetime summary endpoints. + + When a program is not active, its endpoints return `503 Service Unavailable`. + + **Note on field naming.** Response fields under the Rewards endpoints use `snake_case` (e.g. `event_ticker`, `daily_pool_usd`). This differs from the `camelCase` convention used by the rest of the Prediction Markets REST endpoints. + +paths: + /v1/prediction-markets/events: + get: + tags: + - Markets + summary: List prediction market events + description: Returns a paginated list of prediction market events with optional filtering by status, category, sports-market classification, and search text. Repeated values for the same filter use OR semantics; different filters combine with AND semantics. + operationId: listEvents + parameters: + - name: status + in: query + description: Filter by event status (can specify multiple) + schema: + type: array + items: + $ref: '#/components/schemas/MarketStatus' + style: form + explode: true + - name: category + in: query + description: Filter by category (can specify multiple). If omitted, returns events from all categories. + schema: + type: array + items: + type: string + style: form + explode: true + - $ref: '#/components/parameters/SportFilter' + - $ref: '#/components/parameters/SportsMarketTypeFilter' + - $ref: '#/components/parameters/SportsMarketSubjectFilter' + - $ref: '#/components/parameters/SportsMarketScopeFilter' + - $ref: '#/components/parameters/SportsMarketMetricFilter' + - name: search + in: query + description: Search text to filter events by title + schema: + type: string + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/EventsResponse' + examples: + activeEvents: + summary: List of active events + value: + data: + - id: "evt_123" + title: "Will Bitcoin reach $100k by end of 2028?" + slug: "bitcoin-100k-2028" + description: "Bitcoin must reach $100,000 USD on any major exchange by December 31, 2028" + imageUrl: "https://example.com/btc.png" + type: "binary" + category: "crypto" + series: null + ticker: "BTC100K2028" + status: "active" + resolvedAt: null + createdAt: "2026-01-01T00:00:00.000Z" + effectiveDate: "2026-01-01T00:00:00.000Z" + expiryDate: "2028-12-31T23:59:59.000Z" + liquidity: "500000.00" + tags: ["bitcoin", "cryptocurrency", "price-prediction"] + contracts: + - id: "contract_123" + label: "BTC reaches $100,000 by end of 2028" + description: null + prices: + buy: + "yes": "0.65" + "no": "0.35" + sell: + "yes": "0.65" + "no": "0.35" + bestBid: "0.64" + bestAsk: "0.66" + lastTradePrice: "0.65" + color: "#00ff00" + status: "active" + imageUrl: null + priceHistory: null + createdAt: "2026-01-01T00:00:00.000Z" + expiryDate: "2028-12-31T23:59:59.000Z" + resolutionSide: null + resolvedAt: null + termsAndConditionsUrl: "https://example.com/terms" + ticker: "BTC100K2028" + instrumentSymbol: "GEMI-BTC100K2028" + effectiveDate: "2026-01-01T00:00:00.000Z" + - id: "evt_124" + title: "Will ETH reach $5k by end of 2028?" + slug: "ethereum-5k-2028" + description: "Ethereum must reach $5,000 USD on any major exchange by December 31, 2028" + imageUrl: "https://example.com/eth.png" + type: "binary" + category: "crypto" + series: null + ticker: "ETH5K2028" + status: "active" + resolvedAt: null + createdAt: "2026-01-01T00:00:00.000Z" + effectiveDate: "2026-01-01T00:00:00.000Z" + expiryDate: "2028-12-31T23:59:59.000Z" + liquidity: "300000.00" + tags: ["ethereum", "cryptocurrency", "price-prediction"] + contracts: + - id: "contract_125" + label: "ETH reaches $5,000 by end of 2028" + description: null + prices: + buy: + "yes": "0.48" + "no": "0.52" + sell: + "yes": "0.48" + "no": "0.52" + bestBid: "0.47" + bestAsk: "0.49" + lastTradePrice: "0.48" + color: "#00ff00" + status: "active" + imageUrl: null + priceHistory: null + createdAt: "2026-01-01T00:00:00.000Z" + expiryDate: "2028-12-31T23:59:59.000Z" + resolutionSide: null + resolvedAt: null + termsAndConditionsUrl: "https://example.com/terms" + ticker: "ETH5K2028" + instrumentSymbol: "GEMI-ETH5K2028" + effectiveDate: "2026-01-01T00:00:00.000Z" + pagination: + limit: 50 + offset: 0 + total: 2 + sportsMarkets: + summary: Sports markets across leagues and periods + value: + data: + - id: "evt_mlb_first_five_spread" + title: "Yankees at Red Sox - First 5 Innings Spread" + type: "binary" + category: "Sports" + sportsMarket: + sport: "baseball" + type: "spread" + subject: "team" + scope: + type: "inning" + start: 1 + end: 5 + metric: "runs" + contracts: [] + - id: "evt_nfl_first_half_spread" + title: "Bills at Chiefs - First Half Spread" + type: "binary" + category: "Sports" + sportsMarket: + sport: "american_football" + type: "spread" + subject: "team" + scope: + type: "half" + ordinal: 1 + metric: "points" + contracts: [] + - id: "evt_ncaaf_team_total" + title: "Michigan Team Total vs Ohio State" + type: "binary" + category: "Sports" + sportsMarket: + sport: "american_football" + type: "total" + subject: "team" + scope: + type: "full_contest" + metric: "points" + contracts: [] + pagination: + limit: 50 + offset: 0 + total: 3 + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/events/{eventTicker}: + get: + tags: + - Markets + summary: Get event by ticker + description: Returns detailed information about a specific prediction market event. + operationId: getEvent + parameters: + - name: eventTicker + in: path + required: true + description: The event ticker symbol (e.g., "BTC100K") + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Event' + '404': + description: Event not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/events/{eventTicker}/strike: + get: + tags: + - Markets + summary: Get strike price for event + description: | + Returns strike price information for a specific prediction market event. + + Useful for crypto Up/Down contracts where the strike price becomes available at the start of the observation window (typically ~5 minutes before expiry for 5M contracts). + + For Up/Down contracts, the `value` field will be `null` until the strike is captured at `availableAt` time. + operationId: getEventStrike + parameters: + - name: eventTicker + in: path + required: true + description: The event ticker symbol (e.g., "BTC05M2603271950") + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/Strike' + examples: + strikeAvailable: + summary: Strike price captured + value: + value: "98000.50" + type: "reference" + availableAt: "2026-03-27T19:45:00.000Z" + strikePending: + summary: Strike pending (pre-T-0) + value: + value: null + type: "reference" + availableAt: "2026-03-27T19:45:00.000Z" + '404': + description: Strike not found (event doesn't exist or doesn't have strike data) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "Strike not found" + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/events/newly-listed: + get: + tags: + - Markets + summary: List newly listed events + description: Returns a list of prediction market events created in the last 24 hours, sorted by creation date (newest first). Repeated values for the same sports-market filter use OR semantics; different filters combine with AND semantics. + operationId: listNewlyListedEvents + parameters: + - name: category + in: query + description: Filter by category (can specify multiple). If omitted, returns events from all categories. + schema: + type: array + items: + type: string + style: form + explode: true + - $ref: '#/components/parameters/SportFilter' + - $ref: '#/components/parameters/SportsMarketTypeFilter' + - $ref: '#/components/parameters/SportsMarketSubjectFilter' + - $ref: '#/components/parameters/SportsMarketScopeFilter' + - $ref: '#/components/parameters/SportsMarketMetricFilter' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/EventsResponse' + examples: + newlyListedEvents: + summary: Newly listed events + value: + data: + - id: "4126" + title: "SOL price today at 12am EST" + slug: "sol-price-today-at-12am-est" + description: "Interval SOL price prediction event" + imageUrl: "https://images.ctfassets.net/example.png" + type: "categorical" + category: "Crypto" + series: "SOL1H" + ticker: "SOL2603050500" + status: "active" + resolvedAt: null + createdAt: "2026-03-05T02:57:31.548Z" + effectiveDate: null + expiryDate: "2026-03-05T05:00:00.000Z" + liquidity: null + tags: ["Solana"] + contracts: + - id: "4126-33859" + label: "SOL > $90" + abbreviatedName: ">$90" + description: null + prices: + buy: + "yes": "0.42" + "no": "0.58" + sell: + "yes": "0.42" + "no": "0.58" + bestBid: "0.49" + bestAsk: "0.54" + lastTradePrice: "0.75" + color: "#4CAF50" + status: "active" + imageUrl: null + priceHistory: null + createdAt: "2026-03-05T02:57:31.548Z" + expiryDate: "2026-03-05T05:00:00.000Z" + resolutionSide: null + resolvedAt: null + termsAndConditionsUrl: "https://example.com/terms" + ticker: "HI90" + instrumentSymbol: "GEMI-SOL2603050500-HI90" + effectiveDate: null + marketState: "open" + sortOrder: 1 + pagination: + limit: 50 + offset: 0 + total: 1 + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/events/recently-settled: + get: + tags: + - Markets + summary: List recently settled events + description: Returns a list of prediction market events settled in the last 24 hours, sorted by resolution date (most recently settled first). Repeated values for the same sports-market filter use OR semantics; different filters combine with AND semantics. + operationId: listRecentlySettledEvents + parameters: + - name: category + in: query + description: Filter by category (can specify multiple). If omitted, returns events from all categories. + schema: + type: array + items: + type: string + style: form + explode: true + - $ref: '#/components/parameters/SportFilter' + - $ref: '#/components/parameters/SportsMarketTypeFilter' + - $ref: '#/components/parameters/SportsMarketSubjectFilter' + - $ref: '#/components/parameters/SportsMarketScopeFilter' + - $ref: '#/components/parameters/SportsMarketMetricFilter' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/EventsResponse' + examples: + recentlySettledEvents: + summary: Recently settled events + value: + data: + - id: "4100" + title: "SOL price today at 4am EST" + slug: "sol-price-today-at-4am-est" + description: "Interval SOL price prediction event" + imageUrl: "https://images.ctfassets.net/example.png" + type: "categorical" + category: "Crypto" + series: "SOL1H" + ticker: "SOL2603050400" + status: "settled" + resolvedAt: "2026-03-05T04:00:21.400Z" + createdAt: "2026-03-05T01:57:31.548Z" + effectiveDate: null + expiryDate: "2026-03-05T04:00:00.000Z" + liquidity: null + tags: ["Solana"] + contracts: + - id: "4100-33800" + label: "SOL > $90" + abbreviatedName: ">$90" + description: null + prices: + buy: {} + sell: {} + bestBid: null + bestAsk: null + lastTradePrice: "0.80" + color: "#4CAF50" + status: "settled" + imageUrl: null + priceHistory: null + createdAt: "2026-03-05T01:57:31.548Z" + expiryDate: "2026-03-05T04:00:00.000Z" + resolutionSide: "yes" + resolvedAt: "2026-03-05T04:00:21.400Z" + termsAndConditionsUrl: "https://example.com/terms" + ticker: "HI90" + instrumentSymbol: "GEMI-SOL2603050400-HI90" + effectiveDate: null + marketState: "closed" + sortOrder: 1 + pagination: + limit: 50 + offset: 0 + total: 1 + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/events/upcoming: + get: + tags: + - Markets + summary: List upcoming events + description: Returns a list of approved prediction market events that are not yet active (pre-launch), sorted by start time (soonest first). Repeated values for the same sports-market filter use OR semantics; different filters combine with AND semantics. + operationId: listUpcomingEvents + parameters: + - name: category + in: query + description: Filter by category (can specify multiple). If omitted, returns events from all categories. + schema: + type: array + items: + type: string + style: form + explode: true + - $ref: '#/components/parameters/SportFilter' + - $ref: '#/components/parameters/SportsMarketTypeFilter' + - $ref: '#/components/parameters/SportsMarketSubjectFilter' + - $ref: '#/components/parameters/SportsMarketScopeFilter' + - $ref: '#/components/parameters/SportsMarketMetricFilter' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/EventsResponse' + examples: + upcomingEvents: + summary: Upcoming events + value: + data: + - id: "4200" + title: "Lakers vs Celtics" + slug: "lakers-vs-celtics-march-10" + description: "NBA game prediction market" + imageUrl: "https://images.ctfassets.net/example.png" + type: "binary" + category: "Sports" + series: null + sportsMarket: + sport: "basketball" + type: "moneyline" + subject: "contest" + scope: + type: "full_contest" + ticker: "NBA260310LAL-BOS" + status: "approved" + resolvedAt: null + createdAt: "2026-03-04T12:00:00.000Z" + effectiveDate: "2026-03-10T19:30:00.000Z" + expiryDate: "2026-03-11T02:00:00.000Z" + liquidity: null + tags: ["NBA", "Basketball"] + subcategory: + id: 10 + slug: "sports_nba" + name: "NBA" + path: ["Sports", "NBA"] + contracts: + - id: "4200-1" + label: "Lakers Win" + abbreviatedName: "LAL" + description: null + prices: + buy: {} + sell: {} + bestBid: null + bestAsk: null + lastTradePrice: null + color: "#552583" + status: "approved" + imageUrl: null + priceHistory: null + createdAt: "2026-03-04T12:00:00.000Z" + expiryDate: "2026-03-11T02:00:00.000Z" + resolutionSide: null + resolvedAt: null + termsAndConditionsUrl: "https://example.com/terms" + ticker: "LAL" + instrumentSymbol: "GEMI-NBA260310LAL-BOS-LAL" + effectiveDate: "2026-03-10T19:30:00.000Z" + marketState: "closed" + sortOrder: 1 + pagination: + limit: 50 + offset: 0 + total: 1 + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/categories: + get: + tags: + - Markets + summary: List event categories + description: Returns available prediction market event categories, optionally filtered by event status. + operationId: getCategories + parameters: + - name: status + in: query + description: Filter categories by event status + schema: + type: array + items: + $ref: '#/components/schemas/MarketStatus' + style: form + explode: true + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + categories: + type: array + items: + type: string + example: ["sports", "politics", "crypto", "entertainment"] + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/volume/{date}: + get: + tags: + - Volume + summary: Get daily prediction market trade volume + description: | + Returns prediction-market trade volume by category for one completed UTC day. This is a public, unauthenticated endpoint. + + `date` must use the `YYYY-MM-DD` UTC calendar-date format. Requests may select one day in the rolling one-year UTC window ending before the current UTC day; the current UTC day is not available. The exact earliest supported date is evaluated for each request. + + Prediction-market volume begins at `2025-12-15`. A pre-launch date, or a post-launch date with any missing source hour, returns `404 NOT_FOUND`. The endpoint never synthesizes zero-volume rows for time before launch. + + All volume values are non-negative decimal strings. Category rows are flat and ordered with each parent before its descendants. Each category row's `volume` includes trades assigned directly to that category and to all descendant categories. + operationId: getPredictionMarketDailyVolume + parameters: + - name: date + in: path + required: true + description: Completed UTC calendar date in `YYYY-MM-DD` format. + schema: + type: string + format: date + responses: + '200': + description: Category volume for the requested UTC day + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PredictionMarketVolumeCategory' + examples: + dailyVolume: + summary: Daily category volume + value: + - categoryPath: ["Sports"] + volume: "200097" + - categoryPath: ["Sports", "Soccer"] + volume: "3952" + '400': + description: Invalid or unsupported date. The message includes the current one-year UTC date bounds. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + invalidDate: + summary: Date must use the required UTC calendar-date format + value: + error: "BAD_REQUEST" + message: "date must use YYYY-MM-DD. Supported one-year UTC date range: <= date < " + '404': + description: No complete volume data is available for the requested date. This includes dates before prediction markets launched and post-launch dates with a missing source hour. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "NOT_FOUND" + message: "Prediction market volume data is not available for the requested date" + '503': + description: The canonical source is invalid or temporarily unavailable. The endpoint does not return a partial result. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "SERVICE_UNAVAILABLE" + message: "Prediction market volume data is temporarily unavailable" + + /v1/prediction-markets/volume/{date}/hourly: + get: + tags: + - Volume + summary: Get hourly prediction market trade volume + description: | + Returns prediction-market trade volume by category and UTC hour for one completed UTC day. This is a public, unauthenticated endpoint. + + `date` must use the `YYYY-MM-DD` UTC calendar-date format. Requests may select one day in the rolling one-year UTC window ending before the current UTC day; the current UTC day is not available. The exact earliest supported date is evaluated for each request. + + Prediction-market volume begins at `2025-12-15`. A pre-launch date, or a post-launch date with any missing source hour, returns `404 NOT_FOUND`. The endpoint never synthesizes zero-volume rows for time before launch or completed zero-volume hours. + + All volume values are non-negative decimal strings. Rows are ordered by UTC hour, then with each category parent before its descendants. Each category row's `volume` includes trades assigned directly to that category and to all descendant categories. + operationId: getPredictionMarketHourlyVolume + parameters: + - name: date + in: path + required: true + description: Completed UTC calendar date in `YYYY-MM-DD` format. + schema: + type: string + format: date + responses: + '200': + description: Hourly category volume for the requested UTC day + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PredictionMarketHourlyVolumeCategory' + examples: + hourlyVolume: + summary: Hourly category volume + value: + - periodStart: "2026-07-20T00:00:00Z" + categoryPath: ["Sports"] + volume: "200097" + - periodStart: "2026-07-20T00:00:00Z" + categoryPath: ["Sports", "Soccer"] + volume: "3952" + '400': + description: Invalid or unsupported date. The message includes the current one-year UTC date bounds. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + invalidDate: + summary: Date must use the required UTC calendar-date format + value: + error: "BAD_REQUEST" + message: "date must use YYYY-MM-DD. Supported one-year UTC date range: <= date < " + '404': + description: No complete volume data is available for the requested date. This includes dates before prediction markets launched and post-launch dates with a missing source hour. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "NOT_FOUND" + message: "Prediction market volume data is not available for the requested date" + '503': + description: The canonical source is invalid or temporarily unavailable. The endpoint does not return a partial result. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "SERVICE_UNAVAILABLE" + message: "Prediction market volume data is temporarily unavailable" + + /v1/prediction-markets/terms: + get: + tags: + - Terms + summary: Get prediction market terms + description: Returns the latest Prediction Markets terms content. This endpoint is public so clients can display the terms before asking an authenticated account to accept them. + operationId: getPredictionMarketsTerms + responses: + '200': + description: Latest Prediction Markets terms + content: + application/json: + schema: + $ref: '#/components/schemas/PredictionMarketsTerms' + examples: + terms: + summary: Latest terms + value: + termsType: "PredictionsMarket" + version: 3 + content: "These are the prediction market terms." + updatedAt: "2026-05-18T17:00:00Z" + '404': + description: No Prediction Markets terms are configured + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "TermsNotFound" + message: "No terms configured for prediction markets" + '500': + $ref: '#/components/responses/InternalError' + + /v1/prediction-markets/terms/status: + get: + tags: + - Terms + summary: Get prediction market terms status + description: Returns whether the authenticated account group has accepted the latest Prediction Markets terms. Requires authentication and OrderStatus permission. + operationId: getPredictionMarketsTermsStatus + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + responses: + '200': + description: Terms acceptance status + content: + application/json: + schema: + $ref: '#/components/schemas/PredictionMarketsTermsStatus' + examples: + accepted: + summary: Latest terms accepted + value: + hasAcceptedLatest: true + acceptedVersion: 3 + latestVersion: 3 + needsAcceptance: + summary: Latest terms not accepted + value: + hasAcceptedLatest: false + acceptedVersion: 2 + latestVersion: 3 + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + + /v1/prediction-markets/terms/accept: + post: + tags: + - Terms + summary: Accept prediction market terms + description: Accepts the latest configured Prediction Markets terms for the authenticated account group. Requires authentication and NewOrder permission. The actor is recorded from the OAuth client when OAuth is used, otherwise from the API key session. + operationId: acceptPredictionMarketsTerms + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + responses: + '200': + description: Terms accepted + content: + application/json: + schema: + $ref: '#/components/schemas/AcceptPredictionMarketsTermsResponse' + example: + success: true + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: No Prediction Markets terms are configured + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "TermsNotFound" + message: "No terms configured for prediction markets" + '500': + $ref: '#/components/responses/InternalError' + + /v1/prediction-markets/order: + post: + tags: + - Trading + summary: Place order + description: | + Place a new prediction market order. Supports limit and stop-limit order types. Requires authentication and NewOrder permission. + + Before sending orders, check `GET /v1/prediction-markets/terms/status`. If `hasAcceptedLatest` is `false`, display `GET /v1/prediction-markets/terms` and call `POST /v1/prediction-markets/terms/accept`, then retry the order. + + Validate each order's quantity and price against the instrument-specific `quantityIncrement`, `quantityMinimum`, `priceIncrement`, and `priceMinimum` returned in the contract metadata. Do not assume a fixed quantity or price grid across instruments. + + ### Stop-Limit Orders + A stop-limit order is an order type that allows for order placement when a price reaches a specified level. Stop-limit orders take in both a `price` and a `stopPrice` as parameters. The `stopPrice` is the price that triggers the order to be placed on the continuous live order book at the `price`. For buy orders, the `stopPrice` must be greater than or equal to the last trade price and less than or equal to the `price`; for sell orders, the `stopPrice` must be less than or equal to the last trade price and greater than or equal to the `price`. Both `price` and `stopPrice` must be in the 0-1 range. + operationId: placeOrder + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderRequest' + examples: + buyYes: + summary: Buy YES contract + description: Place a limit order to buy 100 YES contracts at 0.65 + value: + symbol: "GEMI-FEDJAN26-DN25" + orderType: "limit" + side: "buy" + quantity: "100" + price: "0.65" + outcome: "yes" + timeInForce: "good-til-cancel" + sellNo: + summary: Sell NO contract + description: Place a limit order to sell 50 NO contracts at 0.35 + value: + symbol: "GEMI-FEDJAN26-DN25" + orderType: "limit" + side: "sell" + quantity: "50" + price: "0.35" + outcome: "no" + timeInForce: "good-til-cancel" + makerOnly: + summary: Maker-only order (post-only) + description: Place a maker-only order that will be cancelled if it would match immediately. Useful for ensuring you provide liquidity and qualify for maker rebates. + value: + symbol: "GEMI-FEDJAN26-DN25" + orderType: "limit" + side: "buy" + quantity: "100" + price: "0.55" + outcome: "yes" + timeInForce: "good-til-cancel" + makerOrCancel: true + buyYesStopLimit: + summary: Buy YES stop-limit + description: Place a stop-limit order to buy 100 YES contracts at 0.70 once the market reaches the stop trigger of 0.65. + value: + symbol: "GEMI-FEDJAN26-DN25" + orderType: "stop-limit" + side: "buy" + quantity: "100" + price: "0.70" + stopPrice: "0.65" + outcome: "yes" + timeInForce: "good-til-cancel" + sellNoStopLimit: + summary: Sell NO stop-limit + description: Place a stop-limit order to sell 50 NO contracts at 0.40 once the market falls to the stop trigger of 0.45. + value: + symbol: "GEMI-FEDJAN26-DN25" + orderType: "stop-limit" + side: "sell" + quantity: "50" + price: "0.40" + stopPrice: "0.45" + outcome: "no" + timeInForce: "good-til-cancel" + responses: + '201': + description: Order created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/OrderResponse' + examples: + orderPlaced: + summary: Order successfully placed + value: + orderId: 12345678901 + status: "open" + symbol: "GEMI-FEDJAN26-DN25" + side: "buy" + outcome: "yes" + orderType: "limit" + quantity: "100" + filledQuantity: "0" + remainingQuantity: "100" + price: "0.65" + avgExecutionPrice: null + createdAt: "2025-12-15T10:30:00.000Z" + updatedAt: "2025-12-15T10:30:00.000Z" + cancelledAt: null + contractMetadata: + contractId: "contract_123" + contractName: "FEDJAN26-DN25" + contractTicker: "FEDJAN26-DN25" + eventTicker: "FEDJAN26" + eventName: "Will Fed Funds Rate drop at least 0.25% at January 2026 meeting?" + category: "economics" + contractStatus: "active" + imageUrl: "https://example.com/fed.png" + expiryDate: "2026-01-31T23:59:59.000Z" + resolvedAt: null + description: "Resolves YES if Federal Reserve lowers the target rate by 0.25% or more at the January 2026 FOMC meeting" + stopLimitOrderPlaced: + summary: Stop-limit order successfully placed + value: + orderId: 12345678902 + status: "open" + symbol: "GEMI-FEDJAN26-DN25" + side: "buy" + outcome: "yes" + orderType: "stop-limit" + quantity: "100" + filledQuantity: "0" + remainingQuantity: "100" + price: "0.70" + stopPrice: "0.65" + avgExecutionPrice: null + createdAt: "2025-12-15T10:30:00.000Z" + updatedAt: "2025-12-15T10:30:00.000Z" + cancelledAt: null + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + description: Order rejected (e.g., insufficient funds) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/order/batch: + post: + tags: + - Trading + summary: Place a batch of orders + description: | + Place between 1 and 20 prediction market orders in one authenticated request. The complete payload is signed once using the standard private REST authentication headers. Each entry accepts the same fields as `POST /v1/prediction-markets/order`. + + The operation is synchronous and non-atomic. Gemini validates the entire batch before submitting any orders. If the batch or any entry fails up-front validation, the request fails and no orders are submitted. After validation succeeds, orders are submitted sequentially and each result is returned in request order. An exchange rejection for one order does not stop later orders from being submitted; it appears as an `error` and `message` for that entry in the `200` response. + operationId: placeOrderBatch + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOrderBatchRequest' + examples: + twoOrders: + summary: Place two orders + value: + orders: + - symbol: "GEMI-FEDJAN26-DN25" + orderType: "limit" + side: "buy" + quantity: "100" + price: "0.65" + outcome: "yes" + timeInForce: "good-til-cancel" + - symbol: "GEMI-FEDJAN26-DN25" + orderType: "limit" + side: "sell" + quantity: "50" + price: "0.35" + outcome: "no" + timeInForce: "good-til-cancel" + responses: + '200': + description: Batch processed. Results are returned in request order and may contain both successful orders and per-entry errors. + content: + application/json: + schema: + $ref: '#/components/schemas/PlaceOrderBatchResponse' + examples: + partialSuccess: + summary: One order accepted and one order rejected + value: + results: + - order: + orderId: 12345678901 + status: "open" + symbol: "GEMI-FEDJAN26-DN25" + side: "buy" + outcome: "yes" + orderType: "limit" + timeInForce: "good-til-cancel" + quantity: "100" + filledQuantity: "0" + remainingQuantity: "100" + price: "0.65" + createdAt: "2025-12-15T10:30:00.000Z" + updatedAt: "2025-12-15T10:30:00.000Z" + - error: "InsufficientFunds" + message: "Insufficient funds" + '400': + description: Invalid payload, empty batch, more than 20 entries, or an invalid order. No orders are submitted. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/AuthErrorResponse' + - $ref: '#/components/schemas/PredictionMarketsError' + '401': + description: Authentication is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthErrorResponse' + '403': + description: The account is not permitted to place orders or has not accepted the current Prediction Markets terms. No orders are submitted. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/AuthErrorResponse' + - $ref: '#/components/schemas/AccountGroupBlockedError' + - $ref: '#/components/schemas/TermsNotAcceptedError' + - $ref: '#/components/schemas/RestrictedSellOnlyError' + '409': + description: The request nonce conflicts with a previously submitted request. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthErrorResponse' + '422': + description: An order failed an up-front risk check. No orders are submitted. + content: + application/json: + schema: + $ref: '#/components/schemas/PredictionMarketsError' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/PredictionMarketsError' + '503': + description: Prediction markets or batch orders are temporarily unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/PredictionMarketsError' + + /v1/prediction-markets/order/cancel: + post: + tags: + - Trading + summary: Cancel order + description: Cancel an existing prediction market order. Requires authentication and CancelOrder permission. + operationId: cancelOrder + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - orderId + properties: + orderId: + type: integer + format: int64 + description: The order ID to cancel + example: 12345678 + responses: + '200': + description: Order cancelled successfully + content: + application/json: + schema: + type: object + properties: + result: + type: string + example: "ok" + message: + type: string + example: "Order 12345678 cancelled successfully" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: Order not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: Order cannot be cancelled (e.g., already filled) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/order/batch/cancel: + post: + tags: + - Trading + summary: Cancel a batch of orders + description: | + Cancel between 1 and 20 prediction market orders in one authenticated request. The complete payload is signed once using the standard private REST authentication headers. Each order ID may be a JSON integer or a quoted numeric string. + + The operation is synchronous and non-atomic. Gemini validates all order IDs before attempting any cancellation. If the batch is empty, contains more than 20 entries, or contains an invalid ID, the request fails and no orders are cancelled. After validation succeeds, cancellations are attempted sequentially and each result is returned in request order. A rejection for one cancellation does not stop later cancellations from being attempted; it appears as an `error` and `message` for that entry in the `200` response. + operationId: cancelOrderBatch + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CancelOrderBatchRequest' + examples: + twoOrders: + summary: Cancel two orders + value: + orderIds: + - 12345678 + - "12345679" + responses: + '200': + description: Batch processed. Results are returned in request order and may contain both successful cancellations and per-entry errors. + content: + application/json: + schema: + $ref: '#/components/schemas/CancelOrderBatchResponse' + examples: + partialSuccess: + summary: One order cancelled and one order not found + value: + results: + - orderId: 12345678 + result: "ok" + - orderId: 12345679 + error: "OrderNotFound" + message: "Order 12345679 not found" + '400': + description: Invalid payload, empty batch, more than 20 entries, or an invalid order ID. No orders are cancelled. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/AuthErrorResponse' + - $ref: '#/components/schemas/PredictionMarketsError' + '401': + description: Authentication is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthErrorResponse' + '403': + description: The account is not permitted to cancel orders. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthErrorResponse' + '409': + description: The request nonce conflicts with a previously submitted request. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/PredictionMarketsError' + '503': + description: Prediction markets or batch orders are temporarily unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/PredictionMarketsError' + + # /v1/prediction-markets/order/quote: + # get: + # tags: + # - Trading + # summary: Get price quote + # description: | + # Get a price quote for buying/selling contracts. Provide either `shares` OR `totalSpend`, not both. + # Requires authentication. + # operationId: getQuote + # security: + # - apiKey: [] + # parameters: + # - name: symbol + # in: query + # required: true + # description: The contract instrument symbol + # schema: + # type: string + # example: "GEMI-FEDJAN26-DN25" + # - name: shares + # in: query + # description: Number of shares to quote (mutually exclusive with totalSpend) + # schema: + # type: string + # example: "100" + # - name: totalSpend + # in: query + # description: Total amount to spend in USD (mutually exclusive with shares) + # schema: + # type: string + # example: "50.00" + # responses: + # '200': + # description: Successful quote + # content: + # application/json: + # schema: + # oneOf: + # - $ref: '#/components/schemas/QuoteByQuantityResponse' + # - $ref: '#/components/schemas/QuoteBySpendResponse' + # '400': + # $ref: '#/components/responses/BadRequest' + # '401': + # $ref: '#/components/responses/Unauthorized' + # '404': + # description: Contract not found + # content: + # application/json: + # schema: + # $ref: '#/components/schemas/Error' + # '500': + # $ref: '#/components/responses/InternalError' + # '503': + # $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/orders/active: + post: + tags: + - Positions + summary: Get active orders + description: Returns a list of currently open (active) orders. Requires authentication. + operationId: getActiveOrders + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + symbol: + type: string + description: Filter by contract instrument symbol + example: "GEMI-FEDJAN26-DN25" + limit: + type: integer + description: Maximum number of results to return (default 50, max 100) + default: 50 + maximum: 100 + offset: + type: integer + description: Number of results to skip for pagination + default: 0 + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/OrdersResponse' + examples: + activeOrders: + summary: List of active orders + value: + orders: + - orderId: 12345678901 + status: "open" + symbol: "GEMI-FEDJAN26-DN25" + side: "buy" + outcome: "yes" + orderType: "limit" + quantity: "100" + filledQuantity: "25" + remainingQuantity: "75" + price: "0.65" + avgExecutionPrice: "0.64" + createdAt: "2025-12-15T10:30:00.000Z" + updatedAt: "2025-12-15T11:00:00.000Z" + cancelledAt: null + contractMetadata: + contractId: "contract_123" + contractName: "FEDJAN26-DN25" + contractTicker: "FEDJAN26-DN25" + eventTicker: "FEDJAN26" + eventName: "Will Fed Funds Rate drop at least 0.25% at January 2026 meeting?" + category: "economics" + contractStatus: "active" + imageUrl: "https://example.com/fed.png" + expiryDate: "2026-01-31T23:59:59.000Z" + resolvedAt: null + description: "Resolves YES if Federal Reserve lowers the target rate by 0.25% or more at the January 2026 FOMC meeting" + - orderId: 12345678902 + status: "open" + symbol: "GEMI-FEDJAN26-DN25" + side: "sell" + outcome: "yes" + orderType: "limit" + quantity: "50" + filledQuantity: "0" + remainingQuantity: "50" + price: "0.65" + avgExecutionPrice: null + createdAt: "2025-12-15T12:00:00.000Z" + updatedAt: "2025-12-15T12:00:00.000Z" + cancelledAt: null + contractMetadata: + contractId: "contract_123" + contractName: "FEDJAN26-DN25" + contractTicker: "FEDJAN26-DN25" + eventTicker: "FEDJAN26" + eventName: "Will Fed Funds Rate drop at least 0.25% at January 2026 meeting?" + category: "economics" + contractStatus: "active" + imageUrl: "https://example.com/fed.png" + expiryDate: "2026-01-31T23:59:59.000Z" + resolvedAt: null + description: "Resolves YES if Federal Reserve lowers the target rate by 0.25% or more at the January 2026 FOMC meeting" + pagination: + limit: 50 + offset: 0 + count: 2 + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/orders/history: + post: + tags: + - Positions + summary: Get order history + description: >- + Returns historical orders (filled or cancelled) for the authenticated user. Use `status: filled` with `from` and `to` to retrieve fully filled orders in a bounded time window. The range is `[from, to)`: `from` is inclusive and `to` is exclusive. A time-bounded response contains at most `limit` results and ignores `offset`; split high-volume periods into non-overlapping ranges. Use `/orders/active` for open orders. + operationId: getOrderHistory + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + status: + type: string + description: Filter by order status + enum: [filled, cancelled] + symbol: + type: string + description: Filter by contract instrument symbol + example: "GEMI-FEDJAN26-DN25" + limit: + type: integer + description: Maximum number of results to return. Defaults to 50 and is capped at 1000. + default: 50 + minimum: 1 + maximum: 1000 + offset: + type: integer + description: Number of results to skip for pagination. Offset is ignored when `from` or `to` is supplied. + default: 0 + minimum: 0 + maximum: 10000 + from: + type: integer + format: int64 + description: Inclusive start of the order-closed time range, expressed as Unix epoch milliseconds. Use with `to` for a UTC daily window. + example: 1775001600000 + to: + type: integer + format: int64 + description: Exclusive end of the order-closed time range, expressed as Unix epoch milliseconds. `from` must not be later than `to`. + example: 1775088000000 + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/OrdersResponse' + '400': + description: Invalid status parameter or date range + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/positions: + post: + tags: + - Positions + summary: Get positions + description: | + Returns current filled positions for the authenticated user. All query parameters are optional; omitting them preserves the legacy unpaginated, unsorted behavior. + operationId: getPositions + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + parameters: + - name: eventTicker + in: query + required: false + description: Filter positions to a single event ticker (e.g. `FEDJAN26`). Positions on sub-events whose `parentEventTicker` matches the value may also be included. + schema: + type: string + - name: limit + in: query + required: false + description: Maximum number of positions to return. Clamped to `[1, 1000]` when supplied. Omit for legacy unpaginated behavior. + schema: + type: integer + minimum: 1 + maximum: 1000 + - name: offset + in: query + required: false + description: Number of positions to skip for pagination. Floor-clamped to `0` when supplied. Ignored when `limit` is omitted (the response is unpaginated). + schema: + type: integer + minimum: 0 + - name: sort + in: query + required: false + description: | + Sort order. Accepts `positionValue`, `unrealizedPnl`, or `expiryDate` (case-insensitive), optionally prefixed with `+` (ascending) or `-` (descending). A bare field name uses each field's default direction: `positionValue` and `unrealizedPnl` default to descending; `expiryDate` defaults to ascending (soonest-first). `unrealizedPnl` and `expiryDate` sort NULLS LAST so positions without the sort key sink to the bottom regardless of direction. `instrumentId` ascending is the final tiebreaker for stable pagination across quote ticks. A malformed `sort` value silently falls back to `-positionValue` — no `400` is returned. + schema: + type: string + enum: + - positionValue + - '+positionValue' + - '-positionValue' + - unrealizedPnl + - '+unrealizedPnl' + - '-unrealizedPnl' + - expiryDate + - '+expiryDate' + - '-expiryDate' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/PositionsResponse' + examples: + positions: + summary: User positions + value: + positions: + - symbol: "GEMI-FEDJAN26-DN25" + instrumentId: 1001 + totalQuantity: "125" + quantityOnHold: "10" + avgPrice: "0.63" + outcome: "yes" + contractMetadata: + contractId: "contract_123" + contractName: "FEDJAN26-DN25" + contractTicker: "FEDJAN26-DN25" + eventTicker: "FEDJAN26" + eventName: "Will Fed Funds Rate drop at least 0.25% at January 2026 meeting?" + category: "economics" + contractStatus: "active" + imageUrl: "https://example.com/fed.png" + eventImageUrl: "https://example.com/fed-event.png" + eventType: "binary" + expiryDate: "2026-01-31T23:59:59.000Z" + resolvedAt: null + resolutionSide: null + description: "Resolves YES if Federal Reserve lowers the target rate by 0.25% or more at the January 2026 FOMC meeting" + sortOrder: null + parentEventTicker: null + template: "binary" + color: null + startTime: null + prices: + buy: + yes: "0.63" + no: "0.37" + sell: + yes: "0.61" + no: "0.35" + bestBid: "0.61" + bestAsk: "0.63" + lastTradePrice: "0.62" + resolutionSide: null + isAboveAutoStartThreshold: false + isLive: true + realizedPl: "0" + marketValue: "78.75" + unrealizedPnl: "0" + unrealizedPct: 0 + - symbol: "GEMI-FEDJAN26-DN25" + instrumentId: 1001 + totalQuantity: "200" + quantityOnHold: "0" + avgPrice: "0.36" + outcome: "no" + contractMetadata: + contractId: "contract_123" + contractName: "FEDJAN26-DN25" + contractTicker: "FEDJAN26-DN25" + eventTicker: "FEDJAN26" + eventName: "Will Fed Funds Rate drop at least 0.25% at January 2026 meeting?" + category: "economics" + contractStatus: "active" + imageUrl: "https://example.com/fed.png" + eventImageUrl: "https://example.com/fed-event.png" + eventType: "binary" + expiryDate: "2026-01-31T23:59:59.000Z" + resolvedAt: null + resolutionSide: null + description: "Resolves YES if Federal Reserve lowers the target rate by 0.25% or more at the January 2026 FOMC meeting" + sortOrder: null + parentEventTicker: null + template: "binary" + color: null + startTime: null + prices: + buy: + yes: "0.37" + no: "0.63" + sell: + yes: "0.35" + no: "0.61" + bestBid: "0.35" + bestAsk: "0.37" + lastTradePrice: "0.36" + resolutionSide: null + isAboveAutoStartThreshold: false + isLive: true + realizedPl: "0" + marketValue: "72.00" + unrealizedPnl: "0" + unrealizedPct: 0 + total: 2 + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/positions/settled: + post: + tags: + - Positions + summary: Get settled positions + description: | + Returns historically settled positions for the authenticated user. Each entry represents a position in a contract that has resolved. + - `payout` — the amount received from settlement + - `resolutionSide` — indicates which outcome (`yes` or `no`) won. + + This endpoint differs from [Get positions](#operation/getPositions) in that it returns closed positions from settled contracts rather than current open positions. + operationId: getSettledPositions + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + parameters: + - name: eventTicker + in: query + description: Optional event ticker to filter settled positions to a single event (e.g. `FEDJAN26`). If omitted, all settled positions for the account are returned. + required: false + schema: + type: string + - name: limit + in: query + required: false + description: Maximum number of settled positions to return. + schema: + type: integer + default: 1000 + minimum: 1 + maximum: 1000 + - name: offset + in: query + required: false + description: Number of settled positions to skip for pagination. + schema: + type: integer + default: 0 + minimum: 0 + - name: sort + in: query + required: false + description: | + Sort order. Accepts `date` or `payout`, optionally prefixed with `+` (ascending) or `-` (descending). A bare field name defaults to descending. `date` ascending is rejected and silently falls back to the default order — settled positions are conceptually ordered most-recent-first. A malformed `sort` value also falls back silently; no `400` is returned. + schema: + type: string + enum: + - date + - '-date' + - payout + - '+payout' + - '-payout' + example: "-payout" + - name: search + in: query + required: false + description: | + Case-insensitive substring filter. Matches against the event name, contract name, event ticker, or any ancestor category name in the contract's category subtree (up to four levels). Whitespace is trimmed; inputs under 3 characters are dropped (GIN trigram lookup floor); inputs over 64 characters are truncated. + schema: + type: string + - name: category + in: query + required: false + description: | + Filter to settled positions whose contract's event belongs to the named category (or any of its descendants in the category tree). Whitespace is trimmed; empty values are ignored. + schema: + type: string + example: "sports" + - name: withCashOuts + in: query + required: false + description: | + Opt-in flag. When `true`, the response carries new sibling fields (`cashOuts`, `totalCashOutProceeds`, `totalCashOutCostBasis`, `totalCashOutNetProfit`) populated with the qualifying cash-outs in the same account-scoped time window as the returned page's settled positions. When `false` (default) the response shape is byte-identical to the pre-`withCashOuts` contract: the `positions[]` element schema is unchanged regardless of the flag. + schema: + type: boolean + default: false + example: true + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/SettledPositionsResponse' + examples: + settledPositions: + summary: Settled positions with one winning and one losing position + value: + positions: + - accountId: 12345 + instrumentId: 1001 + instrumentSymbol: "GEMI-FEDJAN26-DN25" + position: "125" + positionQuantity: "125" + outcome: "yes" + payout: "125.00" + resolutionSide: "yes" + settledAt: "2026-01-31T23:59:59.000Z" + contractMetadata: + contractId: "contract_123" + contractName: "FEDJAN26-DN25" + contractTicker: "FEDJAN26-DN25" + eventTicker: "FEDJAN26" + eventName: "Will Fed Funds Rate drop at least 0.25% at January 2026 meeting?" + category: "economics" + contractStatus: "resolved" + imageUrl: "https://example.com/fed.png" + eventImageUrl: "https://example.com/fed-event.png" + eventType: "binary" + expiryDate: "2026-01-31T23:59:59.000Z" + resolvedAt: "2026-01-31T23:59:59.000Z" + resolutionSide: "yes" + description: "Resolves YES if Federal Reserve lowers the target rate by 0.25% or more at the January 2026 FOMC meeting" + sortOrder: null + parentEventTicker: null + template: "binary" + color: null + startTime: null + costBasis: "78.75" + realizedPnl: "0" + netProfit: "46.25" + - accountId: 12345 + instrumentId: 1002 + instrumentSymbol: "GEMI-FEDJAN26-NOCUT" + position: "-200" + positionQuantity: "200" + outcome: "no" + payout: "0" + resolutionSide: "yes" + settledAt: "2026-01-31T23:59:59.000Z" + contractMetadata: + contractId: "contract_124" + contractName: "FEDJAN26-NOCUT" + contractTicker: "FEDJAN26-NOCUT" + eventTicker: "FEDJAN26" + eventName: "Will Fed Funds Rate drop at least 0.25% at January 2026 meeting?" + category: "economics" + contractStatus: "resolved" + imageUrl: "https://example.com/fed.png" + eventImageUrl: "https://example.com/fed-event.png" + eventType: "binary" + expiryDate: "2026-01-31T23:59:59.000Z" + resolvedAt: "2026-01-31T23:59:59.000Z" + resolutionSide: "yes" + description: "Resolves YES if Federal Reserve lowers the target rate by 0.25% or more at the January 2026 FOMC meeting" + sortOrder: null + parentEventTicker: null + template: "binary" + color: null + startTime: null + costBasis: "72.00" + realizedPnl: "0" + netProfit: "-72.00" + total: 2 + settledPositionsWithCashOuts: + summary: Same response with `withCashOuts=true` + value: + positions: + - accountId: 12345 + instrumentId: 1001 + instrumentSymbol: "GEMI-FEDJAN26-DN25" + position: "125" + positionQuantity: "125" + outcome: "yes" + payout: "125.00" + resolutionSide: "yes" + settledAt: "2026-01-31T23:59:59.000Z" + contractMetadata: + contractId: "contract_123" + contractName: "FEDJAN26-DN25" + contractTicker: "FEDJAN26-DN25" + eventTicker: "FEDJAN26" + eventName: "Will Fed Funds Rate drop at least 0.25% at January 2026 meeting?" + category: "economics" + contractStatus: "resolved" + imageUrl: "https://example.com/fed.png" + eventImageUrl: "https://example.com/fed-event.png" + eventType: "binary" + expiryDate: "2026-01-31T23:59:59.000Z" + resolvedAt: "2026-01-31T23:59:59.000Z" + resolutionSide: "yes" + description: "Resolves YES if Federal Reserve lowers the target rate by 0.25% or more at the January 2026 FOMC meeting" + sortOrder: null + parentEventTicker: null + template: "binary" + color: null + startTime: null + costBasis: "78.75" + realizedPnl: "0" + netProfit: "46.25" + total: 1 + cashOuts: + - accountId: 12345 + instrumentId: 1003 + instrumentSymbol: "GEMI-FEDJAN26-DN25" + timestamp: "2026-01-28T10:15:00.000Z" + filledQuantity: "10" + side: "sell" + proceeds: "6.50" + costBasis: "6.30" + netProfit: "0.20" + contractMetadata: + contractId: "contract_123" + contractName: "FEDJAN26-DN25" + contractTicker: "FEDJAN26-DN25" + eventTicker: "FEDJAN26" + eventName: "Will Fed Funds Rate drop at least 0.25% at January 2026 meeting?" + category: "economics" + contractStatus: "resolved" + imageUrl: "https://example.com/fed.png" + eventImageUrl: "https://example.com/fed-event.png" + eventType: "binary" + expiryDate: "2026-01-31T23:59:59.000Z" + resolvedAt: "2026-01-31T23:59:59.000Z" + resolutionSide: "yes" + description: "Resolves YES if Federal Reserve lowers the target rate by 0.25% or more at the January 2026 FOMC meeting" + sortOrder: null + parentEventTicker: null + template: "binary" + color: null + startTime: null + totalCashOutProceeds: "6.50" + totalCashOutCostBasis: "6.30" + totalCashOutNetProfit: "0.20" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/metrics/volume: + post: + tags: + - Positions + summary: Get volume metrics + description: | + Returns per-contract share volume metrics for an event, including the authenticated user's taker and maker volumes. + + All volumes are in shares (number of contracts traded), not dollar amounts. + + - `totalQty` — Total taker volume across all participants for this contract + - `userAggressorQty` — The authenticated user's taker (aggressor) volume + - `userRestingQty` — The authenticated user's maker (resting) volume, counted when another order fills against the user's resting limit order + + An optional time range can be specified to filter trades within a specific window. + operationId: getVolumeMetrics + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - eventTicker + properties: + eventTicker: + type: string + description: The event ticker symbol + example: "FED260318" + startTime: + type: integer + format: int64 + description: Start of time range filter (epoch milliseconds). If omitted, defaults to the earliest contract creation time. + endTime: + type: integer + format: int64 + description: End of time range filter (epoch milliseconds). If omitted, includes all trades up to now. + examples: + allTime: + summary: All-time volume for an event + value: + eventTicker: "FED260318" + timeRanged: + summary: Volume within a specific time range + value: + eventTicker: "FED260318" + startTime: 1772412364000 + endTime: 1772671564000 + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/VolumeMetricsResponse' + examples: + volumeMetrics: + summary: Volume metrics for a multi-contract event + value: + eventTicker: "FED260318" + contracts: + - symbol: "GEMI-FED260318-CUT25" + totalQty: "94625" + userAggressorQty: "1" + userRestingQty: "0" + - symbol: "GEMI-FED260318-CUTGT25" + totalQty: "68666" + userAggressorQty: "0" + userRestingQty: "0" + - symbol: "GEMI-FED260318-HIKE" + totalQty: "15397" + userAggressorQty: "0" + userRestingQty: "0" + - symbol: "GEMI-FED260318-MAINTAIN" + totalQty: "20400" + userAggressorQty: "5" + userRestingQty: "0" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/combos: + get: + tags: + - Combos + summary: List combo contracts + description: Returns a paginated list of combo contracts. Each combo includes its full leg breakdown and per-leg resolution status. When `status` is omitted, the endpoint returns only `Active` combos. This Combo Prediction Markets endpoint is not currently enabled in production. + operationId: listCombos + parameters: + - name: status + in: query + required: false + description: Filter by combo contract status (for example, `Active`, `Settled`, or `Voided`). Defaults to `Active` when omitted. + schema: + type: string + - name: contractId + in: query + required: false + description: Filter to combos that contain a specific underlying contract ID as a leg + schema: + type: integer + format: int64 + - name: instrumentRegistered + in: query + required: false + description: Filter by whether the combo has been registered with an instrument symbol + schema: + type: boolean + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ListCombosResponse' + examples: + activeCombos: + summary: List of active combo contracts + value: + combos: + - contract: + contractId: "456" + contractName: "BTC EOY26 > $120k AND ETH EOY26 > $4k" + contractTicker: "GEMI-CMB-0526-A7F3B2C1D4E5" + eventTicker: "GEMI-CMB-0526-A7F3B2C1D4E5" + eventName: "BTC EOY26 > $120k AND ETH EOY26 > $4k" + category: "Combo" + contractStatus: "Active" + eventType: "binary" + expiryDate: "2026-12-31T23:59:59Z" + resolvedAt: null + resolutionSide: null + parentEventTicker: null + startTime: null + legs: + - comboId: 456 + legIndex: 0 + contractId: "101" + requiredOutcome: "Yes" + legOutcome: null + resolvedAt: null + contract: + contractId: "101" + contractName: "BTC above $120,000 at year-end 2026" + contractTicker: "GEMI-BTC-EOY26-HI120000" + eventTicker: "GEMI-BTC-EOY26" + eventName: "Bitcoin Year-End 2026" + category: "Crypto" + contractStatus: "Active" + eventType: "binary" + expiryDate: "2026-12-31T23:59:59Z" + resolvedAt: null + resolutionSide: null + parentEventTicker: null + startTime: null + - comboId: 456 + legIndex: 1 + contractId: "202" + requiredOutcome: "Yes" + legOutcome: null + resolvedAt: null + contract: + contractId: "202" + contractName: "ETH above $4,000 at year-end 2026" + contractTicker: "GEMI-ETH-EOY26-HI4000" + eventTicker: "GEMI-ETH-EOY26" + eventName: "Ethereum Year-End 2026" + category: "Crypto" + contractStatus: "Active" + eventType: "binary" + expiryDate: "2026-12-31T23:59:59Z" + resolvedAt: null + resolutionSide: null + parentEventTicker: null + startTime: null + pagination: + limit: 50 + offset: 0 + total: 1 + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + post: + tags: + - Combos + summary: Create or retrieve a canonical combo + description: | + Creates a combo from two to six underlying contract legs for the authenticated account. The service canonicalizes the complete leg set, so submitting the same legs again returns the existing combo regardless of leg order. The account is derived from the authenticated API key; do not include an account ID in the request. This Combo Prediction Markets endpoint is not currently enabled in production. + + Requires signed private REST authentication, the `PredictionsNewOrder` permission, and an unrestricted trading account. A new canonical combo returns `201 Created` with `alreadyExisted: false`; an existing canonical combo returns `200 OK` with `alreadyExisted: true`. + operationId: createCombo + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComboRequest' + examples: + twoLegCombo: + summary: Create a two-leg combo + value: + legs: + - contractId: "101" + requiredOutcome: "Yes" + - contractId: "202" + requiredOutcome: "No" + responses: + '200': + description: The canonical combo already exists. + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComboResponse' + examples: + existingCombo: + summary: Existing canonical combo + value: + combo: + id: 456 + canonicalLegKey: "101:Yes|202:No" + legCount: 2 + displayName: "BTC EOY26 > $120k AND ETH EOY26 <= $4k" + status: "Active" + instrumentId: 98765 + instrumentSymbol: "GEMI-CMB-0526-A7F3B2C1D4E5" + instrumentRegistered: true + latestExpiryDate: "2026-12-31T23:59:59.000Z" + createdAt: "2026-05-01T12:00:00.000Z" + updatedAt: "2026-05-01T12:00:00.000Z" + legs: + - comboId: 456 + legIndex: 0 + contractId: "101" + requiredOutcome: "Yes" + - comboId: 456 + legIndex: 1 + contractId: "202" + requiredOutcome: "No" + alreadyExisted: true + '201': + description: A new canonical combo was created. + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComboResponse' + examples: + createdCombo: + summary: New canonical combo + value: + combo: + id: 456 + canonicalLegKey: "101:Yes|202:No" + legCount: 2 + displayName: "BTC EOY26 > $120k AND ETH EOY26 <= $4k" + status: "Active" + instrumentId: 98765 + instrumentSymbol: "GEMI-CMB-0526-A7F3B2C1D4E5" + instrumentRegistered: true + latestExpiryDate: "2026-12-31T23:59:59.000Z" + createdAt: "2026-05-01T12:00:00.000Z" + updatedAt: "2026-05-01T12:00:00.000Z" + legs: + - comboId: 456 + legIndex: 0 + contractId: "101" + requiredOutcome: "Yes" + - comboId: 456 + legIndex: 1 + contractId: "202" + requiredOutcome: "No" + alreadyExisted: false + '400': + description: The request body is malformed or fails combo validation. + content: + application/json: + schema: + $ref: '#/components/schemas/ComboWriteError' + example: + error: "InvalidInput" + code: "COMBO_VALIDATION_ERROR" + message: "a combo needs 2-6 legs" + '401': + description: Signed private REST authentication is missing or invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthErrorResponse' + '403': + description: The API key lacks `PredictionsNewOrder`, or the authenticated trading account is restricted. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthErrorResponse' + '404': + description: Combos are unavailable, or an underlying contract in the request cannot be found. + content: + application/json: + schema: + $ref: '#/components/schemas/ComboWriteError' + examples: + comboUnavailable: + summary: Combos unavailable + value: + error: "NOT_FOUND" + message: "Not Found" + legNotFound: + summary: Underlying contract not found + value: + error: "NOT_FOUND" + code: "COMBO_LEG_NOT_FOUND" + message: "One or more contracts in this combo could not be found. Update your selection and try again." + '500': + description: An unexpected error occurred while creating the combo. + content: + application/json: + schema: + $ref: '#/components/schemas/ComboWriteError' + example: + error: "InternalError" + message: "An unexpected error occurred" + + /v1/prediction-markets/combos/{instrumentSymbol}: + get: + tags: + - Combos + summary: Get combo by instrument symbol + description: Returns the full specification of a single combo contract identified by its instrument symbol, including leg breakdown and per-leg resolution status. This Combo Prediction Markets endpoint is not currently enabled in production. + operationId: getComboByInstrumentSymbol + parameters: + - name: instrumentSymbol + in: path + required: true + description: The combo contract's instrument symbol (e.g. `GEMI-CMB-0526-A7F3B2C1D4E5`) + schema: + type: string + example: "GEMI-CMB-0526-A7F3B2C1D4E5" + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ComboResponse' + examples: + comboDetail: + summary: Combo contract detail + value: + contract: + contractId: "456" + contractName: "BTC EOY26 > $120k AND ETH EOY26 > $4k" + contractTicker: "GEMI-CMB-0526-A7F3B2C1D4E5" + eventTicker: "GEMI-CMB-0526-A7F3B2C1D4E5" + eventName: "BTC EOY26 > $120k AND ETH EOY26 > $4k" + category: "Combo" + contractStatus: "Active" + eventType: "binary" + expiryDate: "2026-12-31T23:59:59Z" + resolvedAt: null + resolutionSide: null + parentEventTicker: null + startTime: null + legs: + - comboId: 456 + legIndex: 0 + contractId: "101" + requiredOutcome: "Yes" + legOutcome: null + resolvedAt: null + contract: + contractId: "101" + contractName: "BTC above $120,000 at year-end 2026" + contractTicker: "GEMI-BTC-EOY26-HI120000" + eventTicker: "GEMI-BTC-EOY26" + eventName: "Bitcoin Year-End 2026" + category: "Crypto" + contractStatus: "Active" + eventType: "binary" + expiryDate: "2026-12-31T23:59:59Z" + resolvedAt: null + resolutionSide: null + parentEventTicker: null + startTime: null + - comboId: 456 + legIndex: 1 + contractId: "202" + requiredOutcome: "Yes" + legOutcome: null + resolvedAt: null + contract: + contractId: "202" + contractName: "ETH above $4,000 at year-end 2026" + contractTicker: "GEMI-ETH-EOY26-HI4000" + eventTicker: "GEMI-ETH-EOY26" + eventName: "Ethereum Year-End 2026" + category: "Crypto" + contractStatus: "Active" + eventType: "binary" + expiryDate: "2026-12-31T23:59:59Z" + resolvedAt: null + resolutionSide: null + parentEventTicker: null + startTime: null + '404': + description: Combo not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "NOT_FOUND" + message: "Combo not found" + '500': + $ref: '#/components/responses/InternalError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + + /v1/prediction-markets/maker-rebate/rates: + get: + tags: + - Rewards + summary: Get maker-rebate rate schedule + description: | + Returns the current Maker Rebate rate rules. Public endpoint; no authentication required. + + Each rule defines a `rebate_multiplier_bps` (basis points of the maker fee that is rebated) and an `effective_from` timestamp. An optional `category` scopes the rule to a single market category (omitted rules apply to all categories). An optional `effective_to` marks a rule as superseded. + + Returns `503` with `error: "Maker rebate program is not currently available"` when the program is disabled. + operationId: getMakerRebateRates + parameters: + - name: category + in: query + description: Filter to rules that apply to this category (e.g. `Crypto`, `Sports`). When omitted, returns all rules. + required: false + schema: + type: string + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/MakerRebateRatesResponse' + examples: + allCategories: + summary: Full rate schedule + value: + rate_rules: + - id: 12 + rebate_multiplier_bps: 5000 + effective_from: "2026-03-19T00:00:00Z" + category: "Crypto" + - id: 13 + rebate_multiplier_bps: 2500 + effective_from: "2026-03-19T00:00:00Z" + category: "Sports" + effective_to: "2026-04-19T00:00:00Z" + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "An unexpected error occurred" + '503': + description: Maker rebate program is not currently available + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "Maker rebate program is not currently available" + + /v1/prediction-markets/maker-rebate/payouts: + post: + tags: + - Rewards + summary: List maker-rebate payouts + description: | + Returns the authenticated account's Maker Rebate payout history. Most recent payout first. + + Pagination is read from the `limit` and `offset` query parameters: `limit` is clamped to `[1, 100]` (default 50), `offset` is clamped to `[0, +∞)` (default 0). + + Requires authentication with `OrderStatus` permission. Returns `503` when the program is disabled. + operationId: listMakerRebatePayouts + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + parameters: + - name: limit + in: query + description: Maximum number of payouts to return (default 50, clamped to [1, 100]). + required: false + schema: + type: integer + default: 50 + minimum: 1 + maximum: 100 + - name: offset + in: query + description: Number of payouts to skip (default 0). + required: false + schema: + type: integer + default: 0 + minimum: 0 + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/MakerRebatePayoutsResponse' + examples: + samplePayouts: + summary: Two recent payouts + value: + payouts: + - id: 9182 + total_volume_usd: "12450.00" + total_rebate_usd: "6.23" + total_fill_count: 187 + status: "PAID" + paid_at: "2026-05-20T21:00:00Z" + created_at: "2026-05-20T20:55:12Z" + - id: 9173 + total_volume_usd: "8920.50" + total_rebate_usd: "4.46" + total_fill_count: 124 + status: "PAID" + paid_at: "2026-05-19T21:00:00Z" + created_at: "2026-05-19T20:55:08Z" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "An unexpected error occurred" + '503': + description: Maker rebate program is not currently available + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "Maker rebate program is not currently available" + + /v1/prediction-markets/maker-rebate/summary/total: + get: + tags: + - Rewards + summary: Get maker-rebate lifetime summary + description: | + Returns lifetime totals for the authenticated account's Maker Rebate payouts. When both `dateFrom` and `dateTo` are provided, the totals are restricted to payouts paid within that inclusive Eastern Time window. + + Either provide both date parameters or omit both. Dates must be in `YYYY-MM-DD` format, `dateTo` must be on or after `dateFrom`, and the range must not exceed 5 years. + + Requires authentication with `OrderStatus` permission. Returns `503` when the program is disabled. + operationId: getMakerRebateLifetimeSummary + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + parameters: + - name: dateFrom + in: query + description: Inclusive start of the payout date window (`YYYY-MM-DD`, Eastern Time). Must be provided together with `dateTo`. + required: false + schema: + type: string + format: date + example: "2026-04-01" + - name: dateTo + in: query + description: Inclusive end of the payout date window (`YYYY-MM-DD`, Eastern Time). Must be on or after `dateFrom` and within 5 years of it. + required: false + schema: + type: string + format: date + example: "2026-05-01" + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/MakerRebateLifetimeSummary' + examples: + lifetime: + summary: Lifetime totals (no date filter) + value: + total_earned_usd: "152.40" + total_fill_count: 4218 + total_volume_usd: "304800.00" + payout_count: 27 + first_payout_date: "2026-03-19" + last_payout_date: "2026-05-20" + emptyHistory: + summary: Account has never received a payout + value: + total_earned_usd: "0" + total_fill_count: 0 + total_volume_usd: "0" + payout_count: 0 + first_payout_date: null + last_payout_date: null + '400': + description: Invalid date parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "BAD_REQUEST" + message: "date_to must be on or after date_from" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "INTERNAL_ERROR" + message: "An unexpected error occurred" + '503': + description: Maker rebate program is not currently available + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "SERVICE_UNAVAILABLE" + message: "Maker rebate program is not currently available" + + /v1/prediction-markets/liquidity-rewards/config: + get: + tags: + - Rewards + summary: Get liquidity-rewards program config + description: | + Returns the Liquidity Rewards program configuration. Public endpoint; no authentication required. + + When the program is fully configured the response includes `max_spread_cents`, `min_payout_threshold_usd`, and `enabled: true`. When the program is not yet fully configured, the response collapses to `{ "enabled": false }` only. + + Returns `503` when the program is not currently available. + operationId: getLiquidityRewardsConfig + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/LiquidityRewardsConfig' + examples: + enabled: + summary: Program enabled and configured + value: + max_spread_cents: 10 + min_payout_threshold_usd: "1.00" + enabled: true + disabledShape: + summary: Program flag on but config incomplete upstream + value: + enabled: false + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "INTERNAL_ERROR" + message: "An unexpected error occurred" + '503': + description: Liquidity rewards program is not currently available + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "SERVICE_UNAVAILABLE" + message: "Liquidity rewards program is not currently available" + + /v1/prediction-markets/liquidity-rewards/events: + get: + tags: + - Rewards + summary: List liquidity-rewards events + description: | + Returns the paginated list of events currently participating in the Liquidity Rewards program. Public endpoint; no authentication required. + + `category` accepts a comma-separated list of category names (whitespace trimmed, empty entries dropped). `sort` controls ordering. `limit` is clamped to `[1, 100]` (default 50); `offset` is clamped to `[0, +∞)` (default 0). `last_score_date` is the most recent date for which scoring data has been written, or `null` when no scoring has run yet. + + Returns `503` when the program is not currently available. + operationId: listLiquidityRewardsEvents + parameters: + - name: category + in: query + description: Comma-separated list of category names. Whitespace is trimmed and empty entries are dropped. + required: false + schema: + type: string + example: "Crypto,Sports" + - name: search + in: query + description: Filter events by title substring (case-insensitive). + required: false + schema: + type: string + - name: sort + in: query + description: Sort order for the returned events. Defaults to `daily_pool_desc`. + required: false + schema: + type: string + default: daily_pool_desc + enum: + - daily_pool_desc + - daily_pool_asc + - ends_soonest + - ends_latest + - title_asc + - title_desc + - category_asc + - category_desc + - competition_asc + - competition_desc + - name: limit + in: query + description: Maximum number of events to return (default 50, clamped to [1, 100]). + required: false + schema: + type: integer + default: 50 + minimum: 1 + maximum: 100 + - name: offset + in: query + description: Number of events to skip (default 0). + required: false + schema: + type: integer + default: 0 + minimum: 0 + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/LiquidityRewardsEventsResponse' + examples: + sampleEvents: + summary: Two scored events + value: + events: + - event_ticker: "BTC2605202100" + title: "BTC above $95,000?" + category: "Crypto" + daily_pool_usd: "500.00" + pool_source: "event_override" + ends_at: "2026-05-20T21:00:00Z" + qualifying_maker_count: 14 + icon_url: "https://example.com/btc.png" + - event_ticker: "FRENCHOPEN-FINAL" + title: "French Open 2026 — Men's Final winner" + category: "Sports" + daily_pool_usd: "250.00" + pool_source: "category_default" + ends_at: "2026-06-07T15:00:00Z" + qualifying_maker_count: 7 + pagination: + limit: 50 + offset: 0 + total: 2 + last_score_date: "2026-05-19" + emptyBeforeScoring: + summary: Program just turned on, no scoring yet + value: + events: [] + pagination: + limit: 50 + offset: 0 + total: 0 + last_score_date: null + '400': + description: Invalid sort or pagination parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "BAD_REQUEST" + message: "sort must be one of: daily_pool_desc, daily_pool_asc, ends_soonest, ends_latest, title_asc, title_desc, category_asc, category_desc, competition_asc, competition_desc (got 'foo')" + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "INTERNAL_ERROR" + message: "An unexpected error occurred" + '503': + description: Liquidity rewards program is not currently available + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "SERVICE_UNAVAILABLE" + message: "Liquidity rewards program is not currently available" + + /v1/prediction-markets/liquidity-rewards/summary/daily: + get: + tags: + - Rewards + summary: Get liquidity-rewards daily summary + description: | + Returns daily Liquidity Rewards payouts for the authenticated account within the requested date window. Both `dateFrom` and `dateTo` are required and must be in `YYYY-MM-DD` format; `dateTo` must be on or after `dateFrom`. + + Each daily entry includes the total USD reward for that day, the payout status (e.g. `PENDING`, `PAID`), the paid-at timestamp (if paid), and per-event score breakdowns showing how the day's reward was distributed across events the account scored on. + + Requires authentication with `OrderStatus` permission. Returns `503` when the program is not currently available. + operationId: getLiquidityRewardsDailySummary + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + parameters: + - name: dateFrom + in: query + description: Inclusive start of the date window (`YYYY-MM-DD`, Eastern Time). + required: true + schema: + type: string + format: date + example: "2026-05-01" + - name: dateTo + in: query + description: Inclusive end of the date window (`YYYY-MM-DD`, Eastern Time). Must be on or after `dateFrom`. + required: true + schema: + type: string + format: date + example: "2026-05-07" + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/LiquidityRewardsDailySummaryResponse' + examples: + sampleWeek: + summary: One week of daily summaries + value: + daily_summaries: + - payout_date: "2026-05-07" + total_reward_usd: "12.45" + payout_status: "PAID" + paid_at: "2026-05-08T21:00:00Z" + events: + - event_id: 1234567890 + event_name: "BTC above $95,000?" + category_name: "Crypto" + normalized_score: "0.4521" + snapshot_count: 1180 + total_snapshots: 1440 + event_reward_usd: "8.20" + - event_id: 9876543210 + event_name: "ETH above $4,000?" + category_name: "Crypto" + normalized_score: "0.2341" + snapshot_count: 920 + total_snapshots: 1440 + event_reward_usd: "4.25" + - payout_date: "2026-05-06" + total_reward_usd: "0" + payout_status: "ZERO_AMOUNT" + paid_at: null + events: [] + '400': + description: Invalid date parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "date_to must be on or after date_from" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "An unexpected error occurred" + '503': + description: Liquidity rewards program is not currently available + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "Liquidity incentive program is not currently available" + + /v1/prediction-markets/liquidity-rewards/summary/total: + get: + tags: + - Rewards + summary: Get liquidity-rewards lifetime summary + description: | + Returns lifetime totals for the authenticated account's Liquidity Rewards payouts. When both `dateFrom` and `dateTo` are provided, the totals are restricted to payouts paid within that inclusive Eastern Time window. + + Either provide both date parameters or omit both. Dates must be in `YYYY-MM-DD` format, `dateTo` must be on or after `dateFrom`, and the range must not exceed 5 years. + + Requires authentication with `OrderStatus` permission. Returns `503` when the program is not currently available. + operationId: getLiquidityRewardsLifetimeSummary + security: + - apiKey: [] + payloadAuth: [] + signatureAuth: [] + parameters: + - name: dateFrom + in: query + description: Inclusive start of the payout date window (`YYYY-MM-DD`, Eastern Time). Must be provided together with `dateTo`. + required: false + schema: + type: string + format: date + example: "2026-04-01" + - name: dateTo + in: query + description: Inclusive end of the payout date window (`YYYY-MM-DD`, Eastern Time). Must be on or after `dateFrom` and within 5 years of it. + required: false + schema: + type: string + format: date + example: "2026-05-01" + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/LiquidityRewardsLifetimeSummary' + examples: + lifetime: + summary: Lifetime totals + value: + total_earned_usd: "84.20" + payout_count: 12 + first_payout_date: "2026-05-08" + last_payout_date: "2026-05-20" + emptyHistory: + summary: Account has never received a payout + value: + total_earned_usd: "0" + payout_count: 0 + first_payout_date: null + last_payout_date: null + '400': + description: Invalid date parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "BAD_REQUEST" + message: "date_to must be on or after date_from" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "INTERNAL_ERROR" + message: "An unexpected error occurred" + '503': + description: Liquidity rewards program is not currently available + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: "SERVICE_UNAVAILABLE" + message: "Liquidity rewards program is not currently available" + +components: + securitySchemes: + apiKey: + type: apiKey + in: header + name: X-GEMINI-APIKEY + description: Gemini API key with appropriate permissions + payloadAuth: + type: apiKey + in: header + name: X-GEMINI-PAYLOAD + description: Base64-encoded private REST payload. See Gemini private REST authentication. + signatureAuth: + type: apiKey + in: header + name: X-GEMINI-SIGNATURE + description: Hex HMAC-SHA384 signature of the payload using the API secret. + + parameters: + Limit: + name: limit + in: query + description: Maximum number of results to return (max 500) + schema: + type: integer + default: 50 + minimum: 1 + maximum: 500 + Offset: + name: offset + in: query + description: Number of results to skip for pagination + schema: + type: integer + default: 0 + minimum: 0 + SportFilter: + name: sport + in: query + description: Filter by `sportsMarket.sport`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`; valid combinations with no matching events return an empty result. + schema: + type: array + items: + $ref: '#/components/schemas/SportsMarketSport' + style: form + explode: true + SportsMarketTypeFilter: + name: sports_market_type + in: query + description: Filter by `sportsMarket.type`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. + schema: + type: array + items: + $ref: '#/components/schemas/SportsMarketType' + style: form + explode: true + SportsMarketSubjectFilter: + name: sports_market_subject + in: query + description: Filter by `sportsMarket.subject`. Repeat the parameter to match any supplied value (OR). Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. + schema: + type: array + items: + $ref: '#/components/schemas/SportsMarketSubject' + style: form + explode: true + SportsMarketScopeFilter: + name: sports_market_scope + in: query + description: Filter by `sportsMarket.scope.type`. Repeat the parameter to match any supplied value (OR). Ordinal and range qualifiers are not inferred by this filter. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. + schema: + type: array + items: + $ref: '#/components/schemas/SportsMarketScopeType' + style: form + explode: true + SportsMarketMetricFilter: + name: sports_market_metric + in: query + description: Filter by `sportsMarket.metric`. Repeat the parameter to match any supplied value (OR). A metric can be queried without `sport`; use `sport` when its sport-specific meaning matters. Sports-market filters compose independently using AND. Unsupported enum values return `400 Bad Request`. + schema: + type: array + items: + $ref: '#/components/schemas/SportsMarketMetric' + style: form + explode: true + + responses: + BadRequest: + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Unauthorized: + description: Authentication required or invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + InternalError: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + ServiceUnavailable: + description: Prediction markets feature is temporarily unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + schemas: + Error: + type: object + properties: + error: + type: string + description: Error code + example: "InvalidInput" + message: + type: string + description: Human-readable error message + example: "orderId is required" + + PredictionMarketsError: + type: object + additionalProperties: false + required: + - error + properties: + error: + type: string + description: Prediction Markets error class + example: "InvalidInput" + field: + type: string + description: Request field associated with the error, when available + example: "orders" + message: + type: string + description: Human-readable error detail, when available + example: "orders must contain between 1 and 20 entries" + + AuthErrorResponse: + type: object + additionalProperties: false + required: + - result + - reason + - message + properties: + result: + type: string + enum: [error] + reason: + type: string + description: Authentication or authorization error class + example: "MissingNonce" + message: + type: string + description: Human-readable authentication or authorization detail + example: "Must provide unique monotonic increasing 'nonce' field in payload" + + AccountGroupBlockedError: + type: object + additionalProperties: false + required: + - error + - code + properties: + error: + type: string + enum: ["This account is not permitted to trade prediction markets"] + code: + type: string + enum: [ACCOUNT_GROUP_BLOCKED] + + TermsNotAcceptedError: + type: object + additionalProperties: false + required: + - error + - message + properties: + error: + type: string + enum: [TERMS_NOT_ACCEPTED] + message: + type: string + enum: ["Prediction markets terms must be accepted before placing orders"] + + RestrictedSellOnlyError: + type: object + additionalProperties: false + required: + - error + - message + properties: + error: + type: string + enum: [ACCOUNT_RESTRICTED_SELL_ONLY] + message: + type: string + enum: ["Your account is restricted to selling existing positions; buying is not permitted."] + + PredictionMarketsTerms: + type: object + required: + - termsType + - version + - content + - updatedAt + properties: + termsType: + type: string + description: Terms type identifier + example: "PredictionsMarket" + version: + type: integer + description: Latest terms version + example: 3 + content: + type: string + description: Terms content to display before acceptance + example: "These are the prediction market terms." + updatedAt: + type: string + format: date-time + description: UTC timestamp when the terms content was last updated + example: "2026-05-18T17:00:00Z" + + PredictionMarketsTermsStatus: + type: object + required: + - hasAcceptedLatest + properties: + hasAcceptedLatest: + type: boolean + description: Whether the account group has accepted the latest configured Prediction Markets terms + example: false + acceptedVersion: + type: integer + nullable: true + description: Latest terms version accepted by the account group, if any + example: 2 + latestVersion: + type: integer + nullable: true + description: Latest configured Prediction Markets terms version, if available + example: 3 + + AcceptPredictionMarketsTermsResponse: + type: object + required: + - success + properties: + success: + type: boolean + example: true + + MarketStatus: + type: string + enum: [approved, active, closed, under_review, settled, invalid] + description: Status of a prediction market + + MarketType: + type: string + enum: [binary, categorical] + description: Type of prediction market + + SportsMarketSport: + type: string + enum: [american_football, athletics, australian_rules_football, baseball, basketball, boxing, chess, cricket, cycling, darts, esports, golf, hockey, lacrosse, mixed_martial_arts, motorsports, rugby, sailing, soccer, tennis] + description: Sport whose rules give the market's scope and metric their sport-specific meaning. + + SportsMarketType: + type: string + enum: [moneyline, spread, total, prop, correct_score, to_advance, futures, other] + description: Conventional sports-market family. `subject`, `scope`, and `metric` provide detail within the family. This classification is independent of the event's structural `type` (`binary` or `categorical`). + + SportsMarketSubject: + type: string + enum: [contest, team, player, participant, other] + description: What the market is about. `participant` covers non-player entrants such as drivers and horses. + + SportsMarketScopeType: + type: string + enum: [full_contest, regulation, half, quarter, period, inning, team_innings, over, powerplay, set, game, round, hole, match_day, session, super_over, race, sprint, qualifying, practice, stage, lap, series, season, tournament, competition, other] + description: Unit covered by the market. `full_contest` follows the market's official final-result rules; `regulation` covers scheduled regulation play only. Ordinal and range qualifiers are represented separately on `SportsMarketScope`. + + SportsMarketMetric: + type: string + description: Statistic measured by the market. Interpret shared metric names using `sportsMarket.sport`. + enum: + - aces + - assists + - balls_faced + - birdies + - blocked_shots + - blocks + - bogeys + - boundaries + - break_points_won + - cards + - catches + - clean_sheets + - completed_passes + - control_time + - corners + - defensive_rebounds + - double_double + - double_faults + - doubles + - eagles + - earned_runs + - errors + - faceoff_wins + - fairways_hit + - fantasy_points + - fastest_lap + - field_goals_made + - finishing_position + - fouls + - fours + - free_throws_made + - fumbles + - games + - goals + - goals_allowed + - greens_in_regulation + - grid_position + - hits + - hits_allowed + - hits_runs_rbis + - holes_in_one + - home_runs + - innings_pitched + - interceptions_thrown + - kicking_points + - knockdowns + - laps_completed + - laps_led + - lap_time + - longest_pass_completion + - longest_reception + - longest_rush + - maiden_overs + - offensive_rebounds + - offsides + - pars + - passes + - pass_attempts + - pass_completions + - passing_touchdowns + - passing_yards + - penalty_minutes + - pitching_outs_recorded + - pit_stops + - points + - points_assists + - points_rebounds + - points_rebounds_assists + - positions_gained + - power_play_points + - putts + - qualifying_position + - rebounds + - rebounds_assists + - receiving_touchdowns + - receiving_yards + - receptions + - red_cards + - retirements + - rounds + - runs + - runs_batted_in + - runs_conceded + - rush_attempts + - rushing_touchdowns + - rushing_yards + - sacks + - safety_cars + - saves + - sets + - shots + - shots_on_goal + - shots_on_target + - shutouts + - significant_strikes + - singles + - sixes + - steals + - stolen_bases + - strokes + - strikeouts + - submission_attempts + - tackles + - takedowns + - three_pointers_made + - tiebreaks_won + - total_bases + - total_points_won + - total_strikes + - touchdowns + - triples + - triple_double + - turnovers + - walks + - wickets + - wins + - yellow_cards + - other + + SportsMarketScope: + type: object + additionalProperties: false + description: Settlement scope. `ordinal` identifies one unit; `start` and `end` identify an inclusive range of units. + required: + - type + properties: + type: + $ref: '#/components/schemas/SportsMarketScopeType' + ordinal: + type: integer + format: int32 + minimum: 0 + description: Optional ordinal within the scope type, such as half `1` or quarter `4`. + start: + type: integer + format: int32 + minimum: 0 + description: Optional inclusive start of a scope range, such as inning `1`. + end: + type: integer + format: int32 + minimum: 0 + description: Optional inclusive end of a scope range, such as inning `5`. + + SportsMarket: + type: object + additionalProperties: false + description: Atomic sports-market classification shared by every contract grouped under the event. Present only for sports events. All fields except `metric` are required together. + required: + - sport + - type + - subject + - scope + properties: + sport: + $ref: '#/components/schemas/SportsMarketSport' + type: + $ref: '#/components/schemas/SportsMarketType' + subject: + $ref: '#/components/schemas/SportsMarketSubject' + scope: + $ref: '#/components/schemas/SportsMarketScope' + metric: + $ref: '#/components/schemas/SportsMarketMetric' + + OrderType: + type: string + enum: [limit, stop-limit] + description: Order type. `stop-limit` orders require a `stopPrice` that triggers a limit order at `price` when the market reaches the trigger. + + OrderSide: + type: string + enum: [buy, sell] + + Outcome: + type: string + enum: ["yes", "no"] + description: The outcome being traded (Yes or No) + + TimeInForce: + type: string + enum: [good-til-cancel, immediate-or-cancel, fill-or-kill] + default: good-til-cancel + description: | + Order execution behavior: + - `good-til-cancel` - Order remains active until filled or cancelled (default) + - `immediate-or-cancel` - Fill immediately or cancel remaining + - `fill-or-kill` - Fill entire order immediately or cancel + + OrderStatus: + type: string + enum: [open, filled, cancelled] + + PositionStatus: + type: string + enum: [active, resolved, cancelled] + + Pagination: + type: object + properties: + limit: + type: integer + example: 50 + offset: + type: integer + example: 0 + total: + type: integer + example: 100 + + PaginationSimple: + type: object + properties: + limit: + type: integer + offset: + type: integer + count: + type: integer + description: Number of items in current response + + OrderBook: + type: object + properties: + bids: + type: array + items: + $ref: '#/components/schemas/OrderBookEntry' + asks: + type: array + items: + $ref: '#/components/schemas/OrderBookEntry' + + OrderBookEntry: + type: object + properties: + side: + $ref: '#/components/schemas/OrderSide' + price: + type: string + example: "0.65" + quantity: + type: string + example: "1000" + + OrderBookDepth: + type: object + properties: + bids: + type: array + items: + $ref: '#/components/schemas/OrderBookLevel' + asks: + type: array + items: + $ref: '#/components/schemas/OrderBookLevel' + lastUpdateTime: + type: string + format: date-time + + OrderBookLevel: + type: object + properties: + price: + type: string + quantity: + type: string + orderCount: + type: integer + + Contract: + type: object + description: | + Contract quantity and price validation is instrument-specific. Clients must validate order quantities and prices against the returned increment and minimum fields rather than assuming a fixed grid. + properties: + id: + type: string + label: + type: string + description: Human-readable label for the contract's YES-space proposition (e.g., "SOL > $90") + abbreviatedName: + type: string + nullable: true + description: Short form label (e.g., ">$90") + description: + type: object + description: Rich text description + prices: + $ref: '#/components/schemas/ContractPrices' + color: + type: string + nullable: true + status: + $ref: '#/components/schemas/MarketStatus' + imageUrl: + type: string + nullable: true + priceHistory: + type: array + nullable: true + items: + $ref: '#/components/schemas/PricePoint' + createdAt: + type: string + format: date-time + expiryDate: + type: string + format: date-time + nullable: true + resolutionSide: + $ref: '#/components/schemas/Outcome' + resolvedAt: + type: string + format: date-time + nullable: true + termsAndConditionsUrl: + type: string + ticker: + type: string + instrumentSymbol: + type: string + quantityIncrement: + type: string + nullable: true + description: Contract quantity grid from instrument refdata (for example, "0.01"). + quantityMinimum: + type: string + nullable: true + description: Minimum contract quantity from instrument refdata (for example, "1.00"). + priceIncrement: + type: string + nullable: true + description: Contract price grid from instrument refdata (for example, "0.0001"). + quoteAssetPrecision: + type: integer + nullable: true + description: Decimal places supported by the instrument's quote asset. + priceMinimum: + type: string + nullable: true + description: Minimum contract price and anchor for the instrument price grid (for example, "0.0001"). + effectiveDate: + type: string + format: date-time + nullable: true + marketState: + type: string + nullable: true + description: Trading state of the contract + enum: [open, closed] + sortOrder: + type: integer + nullable: true + description: Display order within the event + strike: + $ref: '#/components/schemas/Strike' + source: + type: string + nullable: true + deprecated: true + description: 'Deprecated: use the event-level `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation (e.g., "GRR-KAIKO_BTCUSD_60S"). Present for crypto Up/Down contracts.' + example: "GRR-KAIKO_BTCUSD_60S" + settlementValue: + type: string + nullable: true + description: The observed settlement price. Only present after the contract is settled. + example: "87654.32" + + StrikeType: + type: string + description: >- + Strike or condition inequality type for contract threshold evaluation. + - `reference`: Crypto Up/Down reference strike price captured at `availableAt` time. + - `above`: Higher/Lower contract threshold. + - `spread`: Point, run, or goal handicap spread line. + - `over`: Total or prop threshold evaluated as strict greater than (`>`). + - `over_or_equal`: Total or prop threshold evaluated as greater than or equal to (`>=`). + - `under`: Total or prop threshold evaluated as strict less than (`<`). + - `under_or_equal`: Position, rank, or total threshold evaluated as less than or equal to (`<=`). + enum: + - reference + - above + - spread + - over + - over_or_equal + - under + - under_or_equal + example: "spread" + + Strike: + type: object + description: Strike price or contract threshold information for Up/Down crypto contracts and sports prediction market contracts. + properties: + value: + type: string + nullable: true + description: >- + The strike price value. Null for "reference" type strikes where the value is determined at availableAt time. + For sports contracts, this represents the derived numeric strike value (e.g. spread margin, total line, or position/rank threshold). + example: "87500.00" + type: + $ref: '#/components/schemas/StrikeType' + availableAt: + type: string + format: date-time + nullable: true + description: When the strike price becomes available + example: "2026-03-27T19:45:00.000Z" + + SourceDetails: + type: object + nullable: true + description: >- + Structured data source information for price observation. Replaces the deprecated + flat `source` string on the event and contract. Present for crypto Up/Down events. + Both fields are omitted when not available. + properties: + agency: + type: string + nullable: true + description: The data provider / vendor name. + example: "Kaiko" + index: + type: string + nullable: true + description: The specific data feed identifier (the value previously carried by the flat `source` field). + example: "GRR-KAIKO_BTCUSD_60S" + + PricePoint: + type: object + properties: + timestamp: + type: string + format: date-time + price: + type: string + + ContractPrices: + type: object + nullable: true + description: Current bid/ask pricing for the contract + properties: + buy: + type: object + description: Buy prices for each outcome + properties: + "yes": + type: string + description: Price to buy YES outcome + example: "0.42" + "no": + type: string + description: Price to buy NO outcome + example: "0.58" + sell: + type: object + description: Sell prices for each outcome + properties: + "yes": + type: string + description: Price to sell YES outcome + example: "0.42" + "no": + type: string + description: Price to sell NO outcome + example: "0.58" + bestBid: + type: string + nullable: true + description: Highest buy offer + example: "0.49" + bestAsk: + type: string + nullable: true + description: Lowest sell offer + example: "0.54" + lastTradePrice: + type: string + nullable: true + description: Most recent transaction price + example: "0.75" + + Event: + type: object + description: A prediction market event containing one or more tradeable contracts + properties: + id: + type: string + title: + type: string + example: "Will Bitcoin reach $100k by end of 2028?" + slug: + type: string + example: "bitcoin-100k-2028" + description: + type: string + nullable: true + imageUrl: + type: string + nullable: true + type: + $ref: '#/components/schemas/MarketType' + category: + type: string + example: "crypto" + series: + type: string + nullable: true + sportsMarket: + $ref: '#/components/schemas/SportsMarket' + ticker: + type: string + description: The event ticker (e.g., "BTC100K2028") + example: "BTC100K2028" + status: + $ref: '#/components/schemas/MarketStatus' + resolvedAt: + type: string + format: date-time + nullable: true + createdAt: + type: string + format: date-time + contracts: + type: array + description: Tradeable contracts within this event + items: + $ref: '#/components/schemas/Contract' + contractOrderbooks: + type: object + additionalProperties: + $ref: '#/components/schemas/OrderBook' + volume: + type: string + description: Total trading volume in USD + example: "125000.00" + liquidity: + type: string + description: Total liquidity in USD + example: "50000.00" + tags: + type: array + nullable: true + items: + type: string + effectiveDate: + type: string + format: date-time + expiryDate: + type: string + format: date-time + nullable: true + subcategory: + $ref: '#/components/schemas/Subcategory' + source: + type: string + nullable: true + deprecated: true + description: 'Deprecated: use `sourceDetails` (`agency` + `index`) instead. Data source identifier for price observation. Aggregated from contracts for crypto Up/Down events.' + example: "GRR-KAIKO_BTCUSD_60S" + sourceDetails: + $ref: '#/components/schemas/SourceDetails' + settlement: + $ref: '#/components/schemas/Settlement' + + Subcategory: + type: object + nullable: true + description: Nested category information for the event + properties: + id: + type: integer + description: Category identifier + example: 35 + slug: + type: string + description: URL-friendly category identifier + example: "crypto_solana" + name: + type: string + description: Display name + example: "Solana" + path: + type: array + description: Category hierarchy path + items: + type: string + example: ["Crypto", "Solana"] + + Settlement: + type: object + description: Settlement information for resolved events + properties: + value: + type: string + nullable: true + description: The observed settlement value (e.g., the price at expiry for crypto contracts) + example: "87654.32" + + EventsResponse: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Event' + pagination: + $ref: '#/components/schemas/Pagination' + + ContractMetadata: + type: object + properties: + contractId: + type: string + contractName: + type: string + contractTicker: + type: string + eventTicker: + type: string + eventName: + type: string + category: + type: string + contractStatus: + type: string + eventType: + type: string + description: Event type ("binary" or "categorical") + expiryDate: + type: string + format: date-time + nullable: true + resolvedAt: + type: string + format: date-time + nullable: true + resolutionSide: + type: string + nullable: true + description: Winning outcome if resolved ("yes" or "no") + parentEventTicker: + type: string + nullable: true + description: Parent event ticker for sub-events + startTime: + type: string + format: date-time + nullable: true + description: Start datetime (ISO 8601) + + ComboLeg: + type: object + required: + - comboId + - legIndex + - contractId + - requiredOutcome + properties: + comboId: + type: integer + format: int64 + description: Internal ID of the parent combo contract + example: 456 + legIndex: + type: integer + description: Zero-based position of this leg in the combo + example: 0 + contractId: + type: string + description: Internal ID of the underlying single contract, represented as a decimal string + example: "101" + requiredOutcome: + type: string + enum: ["Yes", "No"] + description: The outcome this leg must settle for the combo to settle YES + example: "Yes" + legOutcome: + type: string + nullable: true + description: The outcome this leg has settled to, if resolved (`"Yes"` or `"No"`). Null while the leg is still active. + example: null + resolvedAt: + type: string + format: date-time + nullable: true + description: UTC timestamp when this leg resolved. Null while still active. + example: null + contract: + nullable: true + allOf: + - $ref: '#/components/schemas/ContractMetadata' + description: Full metadata for the underlying single contract + + ComboResponse: + type: object + required: + - contract + - legs + properties: + contract: + allOf: + - $ref: '#/components/schemas/ContractMetadata' + description: Metadata for the combo contract itself (ticker, status, expiry, etc.) + legs: + type: array + description: Ordered list of legs that make up this combo + items: + $ref: '#/components/schemas/ComboLeg' + + ListCombosResponse: + type: object + required: + - combos + - pagination + properties: + combos: + type: array + description: List of combo contracts matching the query + items: + $ref: '#/components/schemas/ComboResponse' + pagination: + allOf: + - $ref: '#/components/schemas/Pagination' + + CreateComboRequest: + type: object + description: A canonical combo definition. The authenticated account is derived from the signed request and is not a request field. + required: + - legs + properties: + legs: + type: array + description: Two to six distinct underlying contract legs. The service canonicalizes their complete set, so leg order does not create a distinct combo. + minItems: 2 + maxItems: 6 + items: + $ref: '#/components/schemas/CreateComboLeg' + + CreateComboLeg: + type: object + required: + - contractId + - requiredOutcome + properties: + contractId: + type: string + description: Underlying contract ID as a decimal string. + example: "101" + requiredOutcome: + type: string + enum: ["Yes", "No"] + description: Required settlement outcome for this leg. + example: "Yes" + + CreateComboResponse: + type: object + required: + - combo + - alreadyExisted + properties: + combo: + $ref: '#/components/schemas/ComboSummary' + alreadyExisted: + type: boolean + description: "`false` when this request created the canonical combo; `true` when the canonical combo already existed." + + ComboSummary: + type: object + required: + - id + - canonicalLegKey + - legCount + - instrumentRegistered + - legs + properties: + id: + type: integer + format: int64 + description: Internal combo ID. + example: 456 + canonicalLegKey: + type: string + description: Canonical identity of the complete combo leg set. + example: "101:Yes|202:No" + legCount: + type: integer + format: int32 + description: Number of legs in the combo. + example: 2 + displayName: + type: string + description: Human-readable combo name, when available. + status: + type: string + description: Current combo status, when available. + instrumentId: + type: integer + format: int64 + description: Associated instrument ID, when available. + instrumentSymbol: + type: string + description: Associated instrument symbol, when available. + example: "GEMI-CMB-0526-A7F3B2C1D4E5" + instrumentRegistered: + type: boolean + description: Whether the combo has been registered with an instrument symbol. + latestExpiryDate: + type: string + format: date-time + description: Latest expiry among the underlying legs, when available. + createdAt: + type: string + format: date-time + description: Creation time, when available. + updatedAt: + type: string + format: date-time + description: Most recent update time, when available. + legs: + type: array + description: Canonically ordered combo legs. + items: + $ref: '#/components/schemas/ComboSummaryLeg' + + ComboSummaryLeg: + type: object + required: + - comboId + - legIndex + - contractId + - requiredOutcome + properties: + comboId: + type: integer + format: int64 + description: Parent combo ID. + legIndex: + type: integer + format: int32 + description: Zero-based leg position in canonical order. + contractId: + type: string + description: Underlying contract ID as a decimal string. + requiredOutcome: + type: string + enum: ["Yes", "No"] + description: Required settlement outcome for the leg. + legOutcome: + type: string + enum: ["Yes", "No"] + nullable: true + description: Settled outcome for the leg, when resolved. + resolvedAt: + type: string + format: date-time + nullable: true + description: Resolution time for the leg, when resolved. + contract: + allOf: + - $ref: '#/components/schemas/ContractMetadata' + description: Underlying contract metadata, when available. + + ComboWriteError: + type: object + required: + - error + - message + properties: + error: + type: string + description: Error class. + example: "InvalidInput" + code: + type: string + description: Machine-readable code for validation or missing-leg errors, when available. + example: "COMBO_VALIDATION_ERROR" + message: + type: string + description: Human-readable error detail. + example: "a combo needs 2-6 legs" + + OrderRequest: + type: object + required: + - symbol + - orderType + - side + - quantity + - price + - outcome + properties: + symbol: + type: string + description: Contract instrument symbol + example: "GEMI-FEDJAN26-DN25" + orderType: + $ref: '#/components/schemas/OrderType' + side: + $ref: '#/components/schemas/OrderSide' + quantity: + type: string + format: decimal + description: Number of contracts + example: "100" + price: + type: string + format: decimal + description: Limit price (0-1 range) + example: "0.65" + stopPrice: + type: string + format: decimal + description: The price to trigger a stop-limit order (0-1 range). Only available for stop-limit orders. See [Stop-Limit Orders](#operation/placeOrder) above for `stopPrice`/`price` constraints. + example: "0.60" + outcome: + $ref: '#/components/schemas/Outcome' + timeInForce: + $ref: '#/components/schemas/TimeInForce' + makerOrCancel: + type: boolean + description: Set to `true` to require maker-only behavior. If the order would immediately take liquidity, the order is cancelled instead of filling. + default: false + + PlaceOrderBatchRequest: + type: object + required: + - orders + properties: + orders: + type: array + minItems: 1 + maxItems: 20 + description: Orders to submit. Every entry is validated before any order is submitted. All orders use the account associated with the authenticated request. + items: + $ref: '#/components/schemas/OrderRequest' + + BatchOrderResponse: + type: object + description: An accepted order returned for one batch entry. + required: + - orderId + - status + - symbol + - side + - outcome + - orderType + - timeInForce + - quantity + - filledQuantity + - remainingQuantity + - price + - createdAt + - updatedAt + properties: + orderId: + type: integer + format: int64 + example: 12345678 + hashOrderId: + type: string + description: Hashed order ID; omitted when unavailable + clientOrderId: + type: string + description: Client-provided order ID; omitted when unavailable + globalOrderId: + type: string + description: Global order ID; omitted when unavailable + status: + type: string + enum: [open, filled, cancelled, closed] + symbol: + type: string + side: + $ref: '#/components/schemas/OrderSide' + outcome: + $ref: '#/components/schemas/Outcome' + orderType: + $ref: '#/components/schemas/OrderType' + timeInForce: + type: string + enum: [good-til-cancel, immediate-or-cancel, fill-or-kill, maker-or-cancel] + quantity: + type: string + description: Original order quantity + filledQuantity: + type: string + description: Amount filled so far + remainingQuantity: + type: string + description: Amount remaining to fill + price: + type: string + description: Limit price + stopPrice: + type: string + description: Stop trigger price; omitted unless populated for a `stop-limit` order + avgExecutionPrice: + type: string + description: Average price of fills; omitted when unavailable + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + cancelledAt: + type: string + format: date-time + description: Cancellation time; omitted unless the order was cancelled + contractMetadata: + $ref: '#/components/schemas/ContractMetadata' + promoCashApplied: + type: string + description: Promotional cash reserved or applied to the order; omitted when unavailable + fundsOnHold: + type: string + description: Cash reserved for the unfilled portion of a resting buy order; omitted when unavailable + + PlaceOrderBatchSuccessResult: + type: object + additionalProperties: false + required: + - order + properties: + order: + $ref: '#/components/schemas/BatchOrderResponse' + + PlaceOrderBatchErrorResult: + type: object + additionalProperties: false + required: + - error + - message + properties: + error: + type: string + description: Error class for a rejected entry + example: "InsufficientFunds" + message: + type: string + description: Human-readable detail for a rejected entry + example: "Insufficient funds" + + PlaceOrderBatchResult: + description: Exactly one outcome is present. Accepted entries contain `order`; rejected entries contain `error` and `message`. + oneOf: + - $ref: '#/components/schemas/PlaceOrderBatchSuccessResult' + - $ref: '#/components/schemas/PlaceOrderBatchErrorResult' + + PlaceOrderBatchResponse: + type: object + required: + - results + properties: + results: + type: array + minItems: 1 + maxItems: 20 + description: One result for each submitted order, in request order. + items: + $ref: '#/components/schemas/PlaceOrderBatchResult' + + CancelOrderBatchRequest: + type: object + required: + - orderIds + properties: + orderIds: + type: array + minItems: 1 + maxItems: 20 + description: Order IDs to cancel. Each ID may be an integer or a quoted numeric string. All IDs are validated before any cancellation is attempted. + items: + oneOf: + - type: integer + format: int64 + example: 12345678 + - type: string + pattern: '^\d+$' + example: "12345678" + + CancelOrderBatchSuccessResult: + type: object + additionalProperties: false + required: + - orderId + - result + properties: + orderId: + type: integer + format: int64 + description: Order ID from the corresponding request entry. + example: 12345678 + result: + type: string + enum: [ok] + + CancelOrderBatchErrorResult: + type: object + additionalProperties: false + required: + - orderId + - error + - message + properties: + orderId: + type: integer + format: int64 + description: Order ID from the corresponding request entry. + example: 12345678 + error: + type: string + description: Error class for a rejected cancellation + example: "OrderNotFound" + message: + type: string + description: Human-readable detail for a rejected cancellation + example: "Order 12345678 not found" + + CancelOrderBatchResult: + description: Exactly one outcome is present. Successful entries contain `orderId` and `result`; rejected entries contain `orderId`, `error`, and `message`. + oneOf: + - $ref: '#/components/schemas/CancelOrderBatchSuccessResult' + - $ref: '#/components/schemas/CancelOrderBatchErrorResult' + + CancelOrderBatchResponse: + type: object + required: + - results + properties: + results: + type: array + minItems: 1 + maxItems: 20 + description: One result for each requested cancellation, in request order. + items: + $ref: '#/components/schemas/CancelOrderBatchResult' + + OrderResponse: + type: object + properties: + orderId: + type: integer + format: int64 + example: 12345678 + hashOrderId: + type: string + nullable: true + clientOrderId: + type: string + nullable: true + globalOrderId: + type: string + nullable: true + status: + $ref: '#/components/schemas/OrderStatus' + symbol: + type: string + side: + $ref: '#/components/schemas/OrderSide' + outcome: + $ref: '#/components/schemas/Outcome' + orderType: + $ref: '#/components/schemas/OrderType' + quantity: + type: string + description: Original order quantity + filledQuantity: + type: string + description: Amount filled so far + remainingQuantity: + type: string + description: Amount remaining to fill + price: + type: string + description: Limit price + stopPrice: + type: string + nullable: true + description: Stop trigger price (populated for `stop-limit` orders) + avgExecutionPrice: + type: string + nullable: true + description: Average price of fills + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + cancelledAt: + type: string + format: date-time + nullable: true + contractMetadata: + $ref: '#/components/schemas/ContractMetadata' + + OrdersResponse: + type: object + properties: + orders: + type: array + items: + $ref: '#/components/schemas/OrderResponse' + pagination: + $ref: '#/components/schemas/PaginationSimple' + + # QuoteByQuantityResponse: + # type: object + # properties: + # symbol: + # type: string + # quantity: + # type: string + # description: Requested quantity + # bestPrice: + # type: string + # description: Best available price + # executionPrice: + # type: string + # description: Expected execution price + # estimatedSpend: + # type: string + # description: Estimated total spend before fees + # estimatedFees: + # type: string + # description: Estimated fees + # totalCost: + # type: string + # description: Total cost including fees + # liquidity: + # type: string + # description: Available liquidity + # priceImpact: + # type: string + # nullable: true + # description: Expected price impact + # marketDepth: + # $ref: '#/components/schemas/OrderBookDepth' + # warning: + # type: string + # nullable: true + # description: Warning message if applicable + # quoteExpiry: + # type: string + # format: date-time + # description: When this quote expires + + # QuoteBySpendResponse: + # type: object + # properties: + # symbol: + # type: string + # totalSpend: + # type: string + # description: Requested spend amount + # bestPrice: + # type: string + # executionPrice: + # type: string + # estimatedQuantity: + # type: string + # description: Estimated quantity for the spend + # estimatedFees: + # type: string + # totalCost: + # type: string + # liquidity: + # type: string + # priceImpact: + # type: string + # nullable: true + # marketDepth: + # $ref: '#/components/schemas/OrderBookDepth' + # warning: + # type: string + # nullable: true + # quoteExpiry: + # type: string + # format: date-time + + Position: + type: object + properties: + symbol: + type: string + instrumentId: + type: integer + format: int64 + totalQuantity: + type: string + description: Total position size + quantityOnHold: + type: string + description: Quantity currently on hold from open orders + avgPrice: + type: string + description: Average entry price + outcome: + $ref: '#/components/schemas/Outcome' + contractMetadata: + $ref: '#/components/schemas/ContractMetadata' + prices: + $ref: '#/components/schemas/PositionPrices' + resolutionSide: + type: string + nullable: true + description: Winning outcome ("yes" or "no") if the contract has resolved + isAboveAutoStartThreshold: + type: boolean + description: Whether the position is above the auto-start threshold + isLive: + type: boolean + description: Whether the market is currently live/active + realizedPl: + type: string + nullable: true + description: Realized profit/loss from sells + marketValue: + type: string + description: Mark-to-market value of the position in USD at the current sell price (bestBid for YES, bestAsk for NO). **Absent** from the response when the held outcome has no live sell quote (no liquidity to sell into) — surface a no-liquidity state rather than a price the user cannot transact at. `lastTradePrice` is still returned for display. Treat as `Optional`. + example: "65.00" + unrealizedPnl: + type: string + description: Unrealized P&L in USD (`marketValue - costBasis`). **Absent** whenever `marketValue` is absent. Treat as `Optional`. + example: "12.50" + unrealizedPct: + type: number + format: double + description: Unrealized P&L as a percentage of cost basis. Expressed as a percent (e.g. `12.5` represents 12.5%, **not** `0.125`); rounded to 4 decimal places. **Absent** when there is no live sell quote, or when cost basis is zero. Treat as `Optional`. + example: 23.81 + + PositionPrices: + type: object + nullable: true + description: Current bid/ask/last-trade prices for the contract + required: + - buy + - sell + properties: + buy: + type: object + properties: + yes: + type: string + nullable: true + no: + type: string + nullable: true + sell: + type: object + properties: + yes: + type: string + nullable: true + no: + type: string + nullable: true + bestBid: + type: string + nullable: true + bestAsk: + type: string + nullable: true + lastTradePrice: + type: string + nullable: true + + PositionsResponse: + type: object + properties: + positions: + type: array + items: + $ref: '#/components/schemas/Position' + total: + type: integer + nullable: true + description: Total number of positions (for pagination) + + SettledPosition: + type: object + description: A historically settled position in a resolved prediction market contract. + properties: + accountId: + type: integer + format: int64 + description: Account that held the position + instrumentId: + type: integer + format: int64 + description: Unique instrument identifier for the contract + instrumentSymbol: + type: string + description: Contract instrument symbol + example: "GEMI-FEDJAN26-DN25" + position: + type: string + description: Signed position held at settlement. Positive values represent a `yes` position; negative values represent a `no` position. + example: "125" + positionQuantity: + type: string + description: Absolute quantity held at settlement (unsigned) + example: "125" + outcome: + $ref: '#/components/schemas/Outcome' + payout: + type: string + description: Payout received from settlement. `0` when the position lost. + example: "125.00" + resolutionSide: + allOf: + - $ref: '#/components/schemas/Outcome' + description: The winning outcome of the contract + settledAt: + type: string + format: date-time + description: Settlement timestamp (ISO 8601) + contractMetadata: + $ref: '#/components/schemas/ContractMetadata' + costBasis: + type: string + nullable: true + description: Total amount spent to enter the position, net of any prior realized P&L from partial sells. Omitted when cost-basis data is not available. + example: "78.75" + realizedPnl: + type: string + nullable: true + description: Realized profit or loss recorded from sells prior to settlement. Omitted when not available. + example: "0" + netProfit: + type: string + nullable: true + description: Net profit for the position, computed as `payout - costBasis + realizedPnl`. Omitted when `costBasis` is not available. + example: "46.25" + + SettledPositionsResponse: + type: object + properties: + positions: + type: array + items: + $ref: '#/components/schemas/SettledPosition' + total: + type: integer + nullable: true + description: Total number of settled positions across all pages for the current filter set. + totalPayout: + type: string + description: Sum of `payout` across all settled positions in the filter set. Retained for binary back-compat with the legacy response shape; **field is absent (not `null`) on the unified backend** because computing a roll-up over the full filtered set would require a separate aggregate query (deferred until a partner asks). Play's default `OptionHandlers` omits absent `Option` fields rather than emitting `null`. + totalCostBasis: + type: string + description: Sum of `costBasis` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). + totalNetProfit: + type: string + description: Sum of `netProfit` across all settled positions in the filter set. Retained for binary back-compat; **field is absent (not `null`) on the unified backend** (see `totalPayout`). + cashOuts: + type: array + description: | + Cash-outs (early sells before contract resolution) in the same account-scoped time window as the returned page's settled positions. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. `positions[]` pagination is unaffected — `limit`/`offset` continue to scope `positions[]` only. + items: + $ref: '#/components/schemas/CashedOutPosition' + totalCashOutProceeds: + type: string + description: Sum of `cashOuts[].proceeds` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. + example: "120.00" + totalCashOutCostBasis: + type: string + description: Sum of `cashOuts[].costBasis` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. + example: "100.00" + totalCashOutNetProfit: + type: string + description: Sum of `cashOuts[].netProfit` over the returned cash-outs. Field is absent (not `null`) when `withCashOuts=true` is not passed on the request. + example: "20.00" + + CashedOutPosition: + type: object + required: + - accountId + - instrumentId + - instrumentSymbol + - timestamp + - filledQuantity + - side + - proceeds + - costBasis + - netProfit + description: | + A qualifying cash-out (early sell before contract resolution) with cost-basis context. Exposed only via the `withCashOuts=true` sibling array on `POST /v1/prediction-markets/positions/settled`. Distinct from `SettledPosition` — cash-outs don't have a `payout` or `resolutionSide` since the contract hadn't resolved when the user sold. + properties: + accountId: + type: integer + format: int64 + description: Account that held the position. + example: 456 + instrumentId: + type: integer + format: int64 + description: Contract instrument ID. + example: 16789219 + instrumentSymbol: + type: string + description: Contract instrument symbol. + example: "GEMI-FEDJAN26-DN25" + timestamp: + type: string + format: date-time + description: Wall-clock timestamp when the cash-out order closed (ISO 8601). + example: "2026-05-15T14:30:00.000Z" + filledQuantity: + type: string + description: Quantity sold (cumulative filled quantity on the cash-out order). + example: "10" + side: + type: string + enum: [sell] + description: Always `sell` for cash-outs. + example: "sell" + proceeds: + type: string + description: Amount received from the sale in USD. For prediction sells, proceeds flow through `cash_balance` rather than `closed_orders.total_spend`, so the value is derived from position-balance snapshots before/after the fill. + example: "10.50" + costBasis: + type: string + description: Cost basis allocated proportionally to the filled quantity (`(costBasisSpend / costBasisPositionBalance) * filledQuantity`). + example: "10.00" + netProfit: + type: string + description: Realized P&L from this cash-out fill (`proceeds - costBasis`). Equals the ledger `realized_pl` delta on the position-balance row pair around the fill; falls back to `0` under transient market-data lag so a missing post-fill snapshot can't poison the page. + example: "0.50" + contractMetadata: + $ref: '#/components/schemas/ContractMetadata' + + ContractShareVolume: + type: object + properties: + symbol: + type: string + description: Contract instrument symbol + example: "GEMI-FED260318-CUT25" + totalQty: + type: string + description: Total taker volume across all participants (in shares) + example: "94625" + userAggressorQty: + type: string + nullable: true + description: The authenticated user's taker (aggressor) volume (in shares) + example: "1" + userRestingQty: + type: string + nullable: true + description: The authenticated user's maker (resting) volume (in shares) + example: "0" + + VolumeMetricsResponse: + type: object + properties: + eventTicker: + type: string + description: The event ticker + example: "FED260318" + contracts: + type: array + items: + $ref: '#/components/schemas/ContractShareVolume' + + PredictionMarketVolumeCategory: + type: object + required: + - categoryPath + - volume + properties: + categoryPath: + type: array + description: Display-name path from the top-level category to this category. It replaces recursive child nodes. + items: + type: string + example: ["Sports", "Football", "Pro Football"] + volume: + allOf: + - $ref: '#/components/schemas/PredictionMarketVolumeDecimal' + description: Total volume for this category, including all descendant categories. + + PredictionMarketHourlyVolumeCategory: + type: object + required: + - periodStart + - categoryPath + - volume + properties: + periodStart: + type: string + format: date-time + description: Inclusive UTC start of this hourly period. + example: "2026-07-20T00:00:00Z" + categoryPath: + type: array + description: Display-name path from the top-level category to this category. It replaces recursive child nodes. + items: + type: string + example: ["Sports", "Football", "Pro Football"] + volume: + allOf: + - $ref: '#/components/schemas/PredictionMarketVolumeDecimal' + description: Total volume for this category in this hour, including all descendant categories. + + PredictionMarketVolumeDecimal: + type: string + pattern: '^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$' + description: Non-negative decimal string. Preserve it as a string to avoid floating-point precision loss. + example: "143567.25" + + MakerRebateRateRule: + type: object + required: + - id + - rebate_multiplier_bps + - effective_from + properties: + id: + type: integer + format: int64 + description: Stable identifier for this rate rule. + example: 12 + rebate_multiplier_bps: + type: integer + format: int32 + description: Portion of the maker fee that is rebated, in basis points (10000 bps = 100%). + example: 5000 + effective_from: + type: string + format: date-time + nullable: true + description: ISO-8601 timestamp at which this rule becomes effective. Always present; in practice never `null`. + example: "2026-03-19T00:00:00Z" + category: + type: string + description: Market category this rule applies to. When absent, the rule applies to all categories. + example: "Crypto" + effective_to: + type: string + format: date-time + description: ISO-8601 timestamp after which this rule is superseded. Omitted when the rule is still current. + example: "2026-04-19T00:00:00Z" + + MakerRebateRatesResponse: + type: object + required: + - rate_rules + properties: + rate_rules: + type: array + items: + $ref: '#/components/schemas/MakerRebateRateRule' + + MakerRebatePayout: + type: object + required: + - id + - total_volume_usd + - total_rebate_usd + - total_fill_count + - status + - paid_at + - created_at + properties: + id: + type: integer + format: int64 + description: Stable payout identifier. + example: 9182 + total_volume_usd: + type: string + description: Total qualifying maker volume contributing to this payout, in USD. + example: "12450.00" + total_rebate_usd: + type: string + description: Total rebate paid, in USD. + example: "6.23" + total_fill_count: + type: integer + format: int32 + description: Number of qualifying maker fills that contributed to the payout. + example: 187 + status: + type: string + description: Payout status (e.g. `PENDING`, `PAID`). + example: "PAID" + paid_at: + type: string + format: date-time + nullable: true + description: ISO-8601 timestamp at which the rebate was credited. Always present; `null` for payouts that have not yet been paid. + example: "2026-05-20T21:00:00Z" + created_at: + type: string + format: date-time + nullable: true + description: ISO-8601 timestamp at which the payout row was created. Always present. + example: "2026-05-20T20:55:12Z" + + MakerRebatePayoutsResponse: + type: object + required: + - payouts + properties: + payouts: + type: array + items: + $ref: '#/components/schemas/MakerRebatePayout' + + MakerRebateLifetimeSummary: + type: object + required: + - total_earned_usd + - total_fill_count + - total_volume_usd + - payout_count + - first_payout_date + - last_payout_date + properties: + total_earned_usd: + type: string + description: Sum of `total_rebate_usd` across payouts in the window. + example: "152.40" + total_fill_count: + type: integer + format: int64 + description: Sum of qualifying maker fills across payouts in the window. + example: 4218 + total_volume_usd: + type: string + description: Sum of qualifying maker volume (USD) across payouts in the window. + example: "304800.00" + payout_count: + type: integer + format: int32 + description: Number of payouts in the window. Always present; `0` when no payouts exist in the window. + example: 27 + first_payout_date: + type: string + format: date + nullable: true + description: Date of the earliest payout in the window, or `null` if no payouts exist. + example: "2026-03-19" + last_payout_date: + type: string + format: date + nullable: true + description: Date of the most recent payout in the window, or `null` if no payouts exist. + example: "2026-05-20" + + LiquidityRewardsConfig: + type: object + required: + - enabled + properties: + max_spread_cents: + type: integer + format: int32 + description: Quotes wider than this spread score zero in the scoring algorithm. Only present when `enabled` is `true`. + example: 10 + min_payout_threshold_usd: + type: string + description: Daily reward amounts below this threshold are suppressed (sub-threshold accounts get no row at all). Only present when `enabled` is `true`. + example: "1.00" + enabled: + type: boolean + description: 'True when the program is fully configured upstream. When false, the response collapses to `{ "enabled": false }` only.' + example: true + + LiquidityRewardEvent: + type: object + required: + - event_ticker + - title + - category + - daily_pool_usd + - pool_source + - ends_at + - qualifying_maker_count + properties: + event_ticker: + type: string + description: Event ticker (e.g. `BTC2605202100`). + example: "BTC2605202100" + title: + type: string + description: Event title. + example: "BTC above $95,000?" + category: + type: string + description: Market category. + example: "Crypto" + daily_pool_usd: + type: string + description: Daily USD reward pool budgeted for this event. + example: "500.00" + pool_source: + type: string + enum: + - event_override + - category_default + - unspecified + description: Whether the pool came from a per-event override or the category default. + example: "event_override" + ends_at: + type: string + format: date-time + nullable: true + description: ISO-8601 timestamp at which the event ends and stops scoring. `null` when the underlying event has no end timestamp set. + example: "2026-05-20T21:00:00Z" + qualifying_maker_count: + type: integer + format: int32 + description: Number of accounts that met qualifying-maker criteria in the most recent snapshot window for this event. + example: 14 + icon_url: + type: string + description: Optional URL for the event icon. Omitted when not configured. + example: "https://example.com/btc.png" + + LiquidityRewardsEventsResponse: + type: object + required: + - events + - pagination + - last_score_date + properties: + events: + type: array + items: + $ref: '#/components/schemas/LiquidityRewardEvent' + pagination: + $ref: '#/components/schemas/Pagination' + last_score_date: + type: string + format: date + nullable: true + description: Most recent date for which scoring has been written. `null` when no scoring has run yet. + example: "2026-05-19" + + LiquidityEventScore: + type: object + required: + - event_id + - event_name + - category_name + - normalized_score + - snapshot_count + - total_snapshots + - event_reward_usd + properties: + event_id: + type: integer + format: int64 + description: Stable event identifier. + example: 1234567890 + event_name: + type: string + description: Event title. + example: "BTC above $95,000?" + category_name: + type: string + description: Market category. + example: "Crypto" + normalized_score: + type: string + description: This account's normalized score for the event on the scoring date (0-1 range as a decimal string). + example: "0.4521" + snapshot_count: + type: integer + format: int32 + description: Number of snapshots in which this account had a qualifying quote. + example: 1180 + total_snapshots: + type: integer + format: int32 + description: Total snapshots taken for the event on the scoring date. + example: 1440 + event_reward_usd: + type: string + description: Portion of the day's total reward attributed to this event. + example: "8.20" + + LiquidityDailySummary: + type: object + required: + - payout_date + - total_reward_usd + - payout_status + - paid_at + - events + properties: + payout_date: + type: string + format: date + description: Date the payout applies to (Eastern Time). + example: "2026-05-07" + total_reward_usd: + type: string + description: Total USD reward for the day across all events the account scored on. + example: "12.45" + payout_status: + type: string + description: Status of the day's payout (e.g. `PENDING`, `PAID`, `ZERO_AMOUNT`). + example: "PAID" + paid_at: + type: string + format: date-time + nullable: true + description: ISO-8601 timestamp the day's payout was credited. Always present; `null` if not yet paid. + example: "2026-05-08T21:00:00Z" + events: + type: array + description: Per-event score breakdown showing how the day's total was distributed. + items: + $ref: '#/components/schemas/LiquidityEventScore' + + LiquidityRewardsDailySummaryResponse: + type: object + required: + - daily_summaries + properties: + daily_summaries: + type: array + items: + $ref: '#/components/schemas/LiquidityDailySummary' + + LiquidityRewardsLifetimeSummary: + type: object + required: + - total_earned_usd + - payout_count + - first_payout_date + - last_payout_date + properties: + total_earned_usd: + type: string + description: Sum of `total_reward_usd` across daily payouts in the window. + example: "84.20" + payout_count: + type: integer + format: int32 + description: Number of daily payouts in the window. Always present; `0` when no payouts exist in the window. + example: 12 + first_payout_date: + type: string + format: date + nullable: true + description: Date of the earliest payout in the window, or `null` if no payouts exist. + example: "2026-05-08" + last_payout_date: + type: string + format: date + nullable: true + description: Date of the most recent payout in the window, or `null` if no payouts exist. + example: "2026-05-20" diff --git a/specs/openapi/rest.yaml b/specs/openapi/rest.yaml new file mode 100644 index 00000000..8feec896 --- /dev/null +++ b/specs/openapi/rest.yaml @@ -0,0 +1,10475 @@ +openapi: 3.0.3 +info: + title: REST API + description: | + The Gemini Crypto Exchange REST API allows programmatic access to trade cryptocurrencies + and manage your account on the Gemini Exchange platform. The API provides both public and + private endpoints for market data, order management, and account operations. + + version: "1.0.0" + contact: + name: Gemini Trading Support + email: trading@gemini.com +servers: + - url: https://api.gemini.com + description: Production server + - url: https://api.sandbox.gemini.com + description: Sandbox server for testing + +paths: + /v1/symbols: + get: + tags: + - Market Data + summary: List Symbols + operationId: listSymbols + description: This endpoint retrieves all available symbols for trading. + responses: + '200': + description: The full list of supported symbols. + content: + application/json: + schema: + type: array + items: + type: string + description: An array of supported [symbols](/market-data/symbols-and-minimums#all-supported-symbols). + example: ["aavegusd","aaveusd","aligusd","aliusd","ampgusd","ampusd","ankrgusd","ankrusd","apegusd","apeusd","api3gusd","api3usd","arbgusd","arbusd","atomgusd","atomusd","avaxgusd","avaxgusdperp","avaxusd","axsgusd","axsusd","batgusd","batusd","bchgusd","bchgusdperp","bchusd","bnbgusdperp","bomegusd","bomegusdperp","bomeusd","bonkgusd","bonkgusdperp","bonkusd","btceur","btcgbp","btcgusd","btcgusdperp","btcsgd","btcusd","btcusdt","chillguygusd","chillguyusd","chzgusd","chzusd","compgusd","compusd","crvgusd","crvusd","ctxgusd","ctxusd","cubegusd","cubeusd","daigusd","daiusd","dogebtc","dogeeth","dogegusd","dogegusdperp","dogeusd","dotgusd","dotgusdperp","dotusd","efilfil","elongusd","elonusd","ensgusd","ensusd","ethbtc","etheur","ethgbp","ethgusd","ethgusdperp","ethsgd","ethusd","ethusdt","fetgusd","fetusd","filgusd","filusd","flokigusd","flokigusdperp","flokiusd","ftmgusd","ftmusd","galagusd","galausd","gmtgusd","gmtusd","goatgusd","goatgusdperp","goatusd","grtgusd","grtusd","gusdgbp","gusdsgd","gusdusd","hntgusd","hntusd","hypegusdperp","imxgusd","imxusd","injgusd","injgusdperp","injusd","iotxgusd","iotxusd","ksl2gusdperp","kt5gusdperp","ldogusd","ldousd","linkbtc","linketh","linkgusd","linkgusdperp","linkusd","lptgusd","lptusd","lrcgusd","lrcusd","ltcbtc","ltceth","ltcgusd","ltcgusdperp","ltcusd","managusd","manausd","maskgusd","maskusd","maticgusd","maticusd","mewgusd","mewgusdperp","mewusd","mkrgusd","mkrusd","moodenggusd","moodenggusdperp","moodengusd","opgusd","opgusdperp","opusd","oxtgusd","oxtusd","paxggusd","paxgusd","pepegusd","pepegusdperp","pepeusd","pnutgusd","pnutgusdperp","pnutusd","polgusdperp","popcatgusd","popcatgusdperp","popcatusd","pythgusd","pythgusdperp","pythusd","qntgusd","qntusd","raregusd","rareusd","rengusd","renusd","rlusdusd","rndrgusd","rndrusd","samogusd","samousd","sandgusd","sandusd","shibgusd","shibgusdperp","shibusd","sklgusd","sklusd","solbtc","soleth","solgusd","solgusdperp","solusd","storjgusd","storjusd","sushigusd","sushiusd","trumpgusdperp","umagusd","umausd","unigusd","unigusdperp","uniusd","usdcusd","usdtgusd","usdtusd","wifgusd","wifgusdperp","wifusd","xrpgusd","xrpgusdperp","xrpusd","xtzgusd","xtzusd","yfigusd","yfiusd","zecgusd","zecusd","zrxgusd","zrxusd"] + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + + /v1/symbols/details/{symbol}: + get: + tags: + - Market Data + summary: Get Symbol Details + operationId: getSymbolDetails + description: This endpoint retrieves extra detail on supported symbols, such as minimum order size, tick size, quote increment and more. + parameters: + - $ref: '#/components/parameters/symbolParam' + responses: + '200': + description: Instrument responses examples + content: + application/json: + schema: + $ref: '#/components/schemas/SymbolDetails' + examples: + spot: + summary: Spot instrument + description: Spot instrument response + value: + symbol: BTCUSD + base_currency: BTC + quote_currency: USD + tick_size: 0.00000001 + quote_increment: 0.01 + min_order_size: "0.00001" + status: open + wrap_enabled: false + product_type: spot + contract_type: vanilla + contract_price_currency: USD + perpetual: + summary: Perpetual Swap instrument + description: Perpetual Swap instrument response + value: + symbol: BTCETHPERP + base_currency: BTC + quote_currency: ETH + tick_size: 0.0001 + quote_increment: 0.5 + min_order_size: "0.0001" + status: open + wrap_enabled: false + product_type: swap + contract_type: linear + contract_price_currency: GUSD + + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v2/networks/{network}/assets: + get: + tags: + - Market Data + summary: Get Assets for Network + operationId: getAssetsForNetwork + description: | + This endpoint retrieves the enabled assets (tokens) available on a specified blockchain network, filtered by your account's access permissions. + + This authenticated endpoint returns only the assets where your account has deposit and withdraw access enabled on the specified network. + + Use this endpoint to discover all tokens that support deposits and withdrawals on a particular blockchain network. + + The `assets` field in the response is always an array, sorted alphabetically, containing one or more enabled asset codes. + + ### Roles + The API key you use to access this endpoint must have the Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + - name: network + in: path + required: true + schema: + type: string + description: | + Blockchain network identifier (lowercase). Supported networks include: `ethereum`, `solana`, `bitcoin`, `optimism`, `arbitrum`, `base`, `monad`, `avalanche`, `litecoin`, `bitcoincash`, `dogecoin`, `zcash`, `filecoin`, `tezos`, `polkadot`, `cosmos`, `xrpl`, `linea`, and more. + example: ethereum + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + responses: + '200': + description: The response will be a JSON object containing the network name and its supported assets. + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkAssets' + examples: + multi-asset-network: + summary: Network with many assets (Ethereum) + value: + network: "ethereum" + assets: ["AAVE", "BAT", "DAI", "ETH", "LINK", "MATIC", "UNI", "USDC", "USDT", "WBTC"] + single-asset-network: + summary: Network with single asset (Bitcoin) + value: + network: "bitcoin" + assets: ["BTC"] + stablecoin-network: + summary: Network popular for stablecoins (Solana) + value: + network: "solana" + assets: ["BONK", "JTO", "JUP", "PYTH", "RAY", "RENDER", "SOL", "USDC"] + '400': + description: The supplied network is not supported or has no enabled assets. + content: + application/json: + schema: + type: object + properties: + errorMessage: + type: string + examples: + unsupported-network: + summary: Unsupported network + value: + errorMessage: "Supplied value 'foochain' is not a supported network. Please refer to the Supported Networks section at docs.gemini.com and correct your API request." + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v2/network/{token}: + get: + tags: + - Market Data + summary: Get Network + operationId: getTokenNetworkV2 + description: | + + + This endpoint retrieves the associated network(s) for a requested token, filtered by your account's access permissions. + + This authenticated endpoint returns only the networks where your account has both deposit and withdraw access enabled. This supports the multinetwork deposit and withdrawal flow. + + Many tokens are available on multiple blockchain networks. For example, USDC is available on Optimism, Solana, Base, Arbitrum, Avalanche, and Ethereum. Use this endpoint to discover which networks your account can deposit to and withdraw from for a given token. + + The `network` field in the response is always an array, which may contain one or more supported networks. + + ### Roles + The API key you use to access this endpoint must have the Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + - name: token + in: path + required: true + schema: + type: string + description: Token identifier. `BTC`, `ETH`, `USDC`, `SOL` etc. See [**symbols and minimums**](/market-data/symbols-and-minimums) + example: USDC + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + responses: + '200': + description: The response will be a JSON object containing the token and its available networks for the authenticated account. + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkToken' + examples: + single-network: + summary: Single network token (BTC) + value: + token: BTC + network: ["bitcoin"] + multi-network: + summary: Multi-network token (USDC) + value: + token: USDC + network: ["optimism", "solana", "base", "arbitrum", "avalanche", "ethereum"] + '400': + $ref: '#/components/responses/BadRequest' + '404': + description: Returned when the token is not supported or the account has no available networks for the requested token. + content: + application/json: + schema: + type: object + properties: + result: + type: string + example: error + reason: + type: string + example: UnsupportedNetwork + message: + type: string + example: "UnsupportedNetwork: INVALIDTOKEN" + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/pubticker/{symbol}: + get: + tags: + - Market Data + summary: Get Ticker + operationId: getTicker + description: | + This endpoint retrieves information about recent trading activity for the symbol. + + + parameters: + - $ref: '#/components/parameters/symbolParam' + responses: + '200': + description: The current ticker for the symbol + content: + application/json: + schema: + $ref: '#/components/schemas/Ticker' + example: + bid: "977.59" + ask: "977.35" + last: "977.65" + volume: + BTC: "2210.505328803" + USD: "2135477.463379586263" + timestamp: 1483018200000 + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/feepromos: + get: + tags: + - Market Data + summary: List Fee Promos + operationId: listFeePromos + description: This endpoint retrieves symbols that currently have fee promos. + responses: + '200': + description: The response will be a JSON object + content: + application/json: + schema: + $ref: '#/components/schemas/FeePromos' + example: + symbols: ["BTCGUSDPERP"] + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/book/{symbol}: + get: + tags: + - Market Data + summary: Get Current Order Book + operationId: getCurrentOrderBook + x-zudoku-playground-enabled: false # Disable playground for this endpoint due to CORS configuration on server + description: | + This will return the current order book as two arrays (bids / asks). + + + parameters: + - $ref: '#/components/parameters/symbolParam' + - name: limit_bids + in: query + description: Limit the number of bid (offers to buy) price levels returned. Default is 50. May be 0 to return the full order book on this side. + required: false + schema: + type: number + minimum: 1 + - name: limit_asks + in: query + description: Limit the number of ask (offers to sell) price levels returned. Default is 50. May be 0 to return the full order book on this side. + required: false + schema: + type: number + minimum: 1 + responses: + '200': + description: The response will be two arrays. The bids and the asks are grouped by price, so each entry may represent multiple orders at that price. Each element of the array will be a JSON object. + content: + application/json: + schema: + $ref: '#/components/schemas/OrderBook' + example: + bids: [ + { + price: "3607.85", + amount: "6.643373", + timestamp: "1547147541" + } + ] + asks: [ + { + price: "3607.86", + amount: "14.68205084", + timestamp: "1547147541" + } + ] + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/trades/{symbol}: + get: + tags: + - Market Data + summary: List Trades + operationId: listTrades + x-zudoku-playground-enabled: false # Disable playground for this endpoint due to CORS configuration on server + description: | + + + This will return the trades that have executed since the specified timestamp. Timestamps are either seconds or milliseconds since the epoch (1970-01-01). See the [Data Types](/data-types) section about `timestamp` for information on this. + + Each request will show at most 500 records. + + If no `since` or `timestamp` is specified, then it will show the most recent trades; otherwise, it will show the most recent trades that occurred after that timestamp. + parameters: + - $ref: '#/components/parameters/symbolParam' + - name: timestamp + in: query + description: | + Only return trades after this timestamp. See [**Timestamps**](/rest/~schemas#timestamp-type) for more information. If not present, will show the most recent trades. For backwards compatibility, you may also use the alias `since`. With timestamp, there is a 90-day hard limit. + required: false + schema: + $ref: '#/components/schemas/TimestampType' + description: Timestamp in milliseconds + - name: since_tid + in: query + description: | + Only retuns trades that executed after this tid. since_tid trumps timestamp parameter which has no effect if provided too. You may set since_tid to zero to get the earliest available trade history data. + required: false + schema: + type: number + - name: limit_trades + in: query + description: | + The maximum number of trades to return. The default is 50. + required: false + schema: + type: number + minimum: 0 + default: 50 + - name: include_breaks + in: query + description: | + Whether to display broken trades. False by default. Can be `1` or `true` to activate + required: false + schema: + type: boolean + default: false + responses: + '200': + description: The response will be an array of JSON objects, sorted by timestamp, with the newest trade shown first. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Trade' + example: + timestamp: 1547146811 + timestampms: 1547146811357 + tid: 5335307668 + price: "3610.85" + amount: "0.27413495" + exchange: "gemini" + type: "buy" + broken: true + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/pricefeed: + get: + tags: + - Market Data + summary: List Prices + operationId: listPrices + responses: + '200': + description: Response is a list of objects, one for each pair. + content: + application/json: + schema: + $ref: '#/components/schemas/PriceFeedResponse' + example: [ + { + "pair":"BTCUSD", + "price":"9500.00", + "percentChange24h": "5.23" + }, + { + "pair":"ETHUSD", + "price":"257.54", + "percentChange24h": "4.85" + }, + { + "pair":"BCHUSD", + "price":"450.10", + "percentChange24h": "-2.91" + }, + { + "pair":"LTCUSD", + "price":"79.50", + "percentChange24h": "7.63" + } + ] + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/fundingamount/{symbol}: + get: + tags: + - Market Data + summary: Get Funding Amount + operationId: getFundingAmount + parameters: + - name: symbol + in: path + required: true + schema: + type: string + description: | + Trading pair symbol

+ + `BTCGUSDPERP`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + example: BTCGUSDPERP + responses: + '200': + description: The response will be an object + content: + application/json: + schema: + $ref: '#/components/schemas/FundingAmountResponse' + example: { + symbol: BTCGUSDPERP, + fundingDateTime: "2025-04-22T18:00:00.000Z", + fundingTimestampMilliSecs: 1745344800000, + nextFundingTimestamp: 1745348400000, + fundingAmount: -1.50991, + estimatedFundingAmount: -2.10595 + } + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/nextfundingtimestamp/{symbol}: + get: + tags: + - Market Data + summary: Get Next Funding Timestamp + operationId: getNextFundingTimestamp + parameters: + - name: symbol + in: path + required: true + schema: + type: string + description: | + Trading pair symbol

+ + `BTCGUSDPERP`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + example: BTCGUSDPERP + responses: + '200': + description: The response will be an integer timestamp in milliseconds. + content: + application/json: + schema: + type: integer + format: int64 + example: 1745348400000 + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/fundingamountreport/records.xlsx: + get: + tags: + - Market Data + summary: Get Funding Amount Report File + operationId: getFundingAmountReportFile + description: | + ### Examples + - `symbol=BTCGUSDPERP&fromDate=2024-04-10&toDate=2024-04-25&numRows=1000`
+ Compare and obtain the minimum records between (2024-04-10 to 2024-04-25) and 1000. If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch the minimum between 360 and 1000 records only. + + - `symbol=BTCGUSDPERP&numRows=2024-04-10&toDate=2024-04-25`
+ If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch 360 records only. + + - `symbol=BTCGUSDPERP&numRows=1000`
+ Fetch maximum 1000 records starting from Now to a historical date + + - `symbol=BTCGUSDPERP`
+ Fetch maximum 8760 records starting from Now to a historical date + + parameters: + - name: symbol + in: query + description: | + Trading pair symbol

+ + `BTCGUSDPERP`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + required: true + schema: + type: string + - name: fromDate + in: query + description: Mandatory if `toDate` is specified, else optional. If empty, will only fetch records by numRows value. + required: false + schema: + type: string + format: date + - name: toDate + in: query + description: Mandatory if `fromDate` is specified, else optional. If empty, will only fetch records by numRows value. + required: false + schema: + type: string + format: date + - name: numRows + in: query + description: If empty, default value '8760' + required: false + schema: + type: integer + + responses: + '200': + description: The response will be an excel / csv file. filename=FundingAmount_{SYMBOL}.{xlsx,csv} + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename=FundingAmount_{SYMBOL}.{xlsx,csv} + content: + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: string + format: binary + text/csv: + schema: + type: string + format: binary + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/order/new: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: Create New Order + operationId: createNewOrder + description: | + If you wish orders to be automatically cancelled when your session ends, see the [require heartbeat](/authentication/api-key#require-heartbeat) section, or manually send the [cancel all session orders](/rest/orders#cancel-all-session-orders) message. + + + + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See Roles for more information. + + The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + + ### Margin Orders + Set `margin_order: true` to place an order using borrowed funds on a margin-enabled account. This allows you to trade with leverage beyond your available balance. + + **Important**: Margin trading amplifies both gains and losses. Monitor your account using the [Margin Account Summary](/rest/margin-trading#get-margin-account-summary) endpoint and preview order impacts with [Order Preview](/rest/margin-trading#preview-margin-order-impact) before placing margin orders. + + ### Stop-Limit Orders + A Stop-Limit order is an order type that allows for order placement when a price reaches a specified level. Stop-Limit orders take in both a `price` and and a `stop_price` as parameters. The `stop_price` is the price that triggers the order to be placed on the continous live order book at the `price`. For buy orders, the `stop_price` must be below the `price` while sell orders require the `stop_price` to be greater than the `price`. + + + ### What about market orders? + The API doesn't directly support market orders because they provide you with no price protection. + + Instead, use the “immediate-or-cancel” order execution option, coupled with an aggressive limit price (i.e. very high for a buy order or very low for a sell order), to achieve the same result. + + ### Order execution options + Note that `options` is an array. If you omit `options` or provide an empty array, your order will be a standard limit order - it will immediately fill against any open orders at an equal or better price, then the remainder of the order will be posted to the order book. + + If you specify more than one option (or an unsupported option) in the `options` array, the exchange will reject your order. + + No `options` can be applied to stop-limit orders at this time. + + The available limit order options are: + + | Option | Description | + |--------|-------------| + | `"maker-or-cancel"` | This order will only add liquidity to the order book.

If any part of the order could be filled immediately, the whole order will instead be canceled before any execution occurs.

If that happens, the response back from the API will indicate that the order has already been canceled (`"is_cancelled": true` in JSON).

*Note: some other exchanges call this option "post-only".* | + | `"immediate-or-cancel"` | This order will only remove liquidity from the order book.

It will fill whatever part of the order it can immediately, then cancel any remaining amount so that no part of the order is added to the order book.

If the order doesn't fully fill immediately, the response back from the API will indicate that the order has already been canceled (`"is_cancelled": true` in JSON). | + | `"fill-or-kill"` | This order will only remove liquidity from the order book.

It will fill the entire order immediately or cancel.

If the order doesn't fully fill immediately, the response back from the API will indicate that the order has already been canceled (`"is_cancelled": true` in JSON). | + + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewOrderRequest' + examples: + limitOrder: + summary: Limit Order + description: JSON limit order payload + value: + request: /v1/order/new + nonce: + client_order_id: "470135" + symbol: BTCUSD + amount: "5" + price: "3633.00" + side: buy + type: exchange limit + stopLimitOrder: + summary: Stop-Limit Order + description: JSON stop-limit order payload + value: + request: /v1/order/new + nonce: + client_order_id: "921841" + symbol: BTCUSD + amount: "0.1" + price: "10500" + side: buy + type: exchange stop limit + stop_price: "10000" + marginOrder: + summary: Margin Order + description: JSON margin order payload using borrowed funds + value: + request: /v1/order/new + nonce: + client_order_id: "384729" + symbol: ETHUSD + amount: "2.5" + price: "3200.00" + side: buy + type: exchange limit + margin_order: true + responses: + '200': + description: Response will be the fields included in Order Status + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/LimitOrderResponse' + - $ref: '#/components/schemas/StopLimitOrderResponse' + examples: + limitOrder: + summary: Limit Order + description: JSON limit order response + value: + order_id: "106817811" + id: "106817811" + symbol: BTCUSD + exchange: gemini + avg_execution_price: "3632.8508430064554" + side: buy + type: exchange limit + timestamp: "1547220404" + timestampms: 1547220404836 + is_live: true + is_cancelled: false + is_hidden: false + was_forced: false + executed_amount: "3.7567928949" + remaining_amount: "1.2432071051" + client_order_id: "20190110-4738721" + options: [] + price: "3633.00" + original_amount: "5" + stopLimitOrder: + summary: Stop-Limit Order + description: JSON stop-limit order response + value: + order_id: "7419662" + id: "7419662" + symbol: BTCUSD + exchange: gemini + avg_execution_price: "0.00" + side: buy + type: stop-limit + timestamp: "1572378649" + timestampms: 1572378649018 + is_live: true + is_cancelled: false + is_hidden: false + was_forced: false + executed_amount: "0" + options: [] + stop_price: "10400.00" + price: "10500.00" + original_amount: "0.01" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/order/cancel: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: Cancel Order + operationId: cancelOrder + description: | + This will cancel an order. If the order is already canceled, the message will succeed but have no effect. + + + + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles) for more information. + + The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + ### All Cancellation Reasons + Under unique circumstances, orders may be automatically cancelled by the exchange. These scenarios are detailed in the table below: + + | Cancel Reason | Description | + |---------------|-------------| + | `MakerOrCancelWouldTake` | Occurs when the "maker-or-cancel" execution option is included in the order request and any part of the requested order could be filled immediately. | + | `ExceedsPriceLimits` | Occurs when there is not sufficient liquidity on the order book to support the entered trade. Orders will be automatically cancelled when liquidity conditions are such that the order would move price +/- 5%. | + | `SelfCrossPrevented` | Occurs when a user enters a bid that is higher than that user's lowest open ask or enters an ask that is lower than their highest open bid on the same pair. | + | `ImmediateOrCancelWouldPost` | Occurs when the "immediate-or-cancel" execution option is included in the order request and the requested order cannot be fully filled immediately. This type of cancellation will only cancel the unfulfilled part of any impacted order. | + | `FillOrKillWouldNotFill` | Occurs when the "fill-or-kill" execution option is included in the new order request and the entire order cannot be filled immediately.

Unlike "immediate-or-cancel" orders, this execution option will result in the entire order being cancelled rather than just the unfulfilled portion. | + | `Requested` | Cancelled via user request to /v1/order/cancel endpoint. | + | `MarketClosed` | Occurs when an order is placed for a trading pair that is currently closed. | + | `TradingClosed` | Occurs when an order is placed while the exchange is closed for trading. | + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CancelOrderRequest' + examples: + cancelOrder: + summary: Cancel Order Example + description: JSON payload to cancel an order using its order_id + value: + request: /v1/order/cancel + nonce: + order_id: 106817811 + responses: + '200': + description: Response will be the fields included in Order Status. If the order was already canceled, then the request will have no effect and the status will be returned. Note the *is_cancelled* node will have a value of 'true' + content: + application/json: + schema: + $ref: '#/components/schemas/CancelOrderResponse' + examples: + cancelledOrder: + summary: Cancelled Order + description: JSON response for a cancelled order + value: + order_id: "106817811" + id: "106817811" + symbol: "btcusd" + exchange: "gemini" + avg_execution_price: "3632.85101103" + side: "buy" + type: "exchange limit" + timestamp: "1495742383" + timestampms: 1495742383345 + is_live: false + is_cancelled: true + is_hidden: false + was_forced: false + executed_amount: "3.7610296649" + remaining_amount: "1.2389703351" + reason: "Requested" + options: [] + price: "2960.00" + original_amount: "5" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/order/cancel/all: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: Cancel All Active Orders + operationId: cancelAllActiveOrders + description: | + This will cancel all outstanding orders created by all [sessions](/authentication/api-key#sessions) owned by this account, including interactive orders placed through the UI. + + + + Typically [Cancel All Session Orders](/rest/orders#cancel-all-session-orders) is preferable, so that only orders related to the current connected session are cancelled. + + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CancelAllOrdersRequest' + examples: + cancelAllOrders: + summary: Cancel All Orders + description: JSON payload to cancel all orders + value: + request: /v1/order/cancel/all + nonce: + responses: + '200': + description: JSON response + content: + application/json: + schema: + $ref: '#/components/schemas/CancelAllResult' + example: + result: "ok" + details: { + cancelRejects: [], + cancelledOrders: [ + 330429106, + 330429079, + 330429082 + ] + } + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/order/cancel/session: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: Cancel All Session Orders + operationId: cancelAllSessionOrders + description: | + This will cancel all orders opened by this [session](/authentication/api-key#sessions). + + This will have the same effect as [heartbeat](/authentication/api-key#require-heartbeat) expiration if "Require Heartbeat" is selected for the session. + + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CancelAllOrdersBySessionRequest' + examples: + cancelAllSessionOrders: + summary: Cancel All Session Orders + description: JSON payload to cancel all orders opened by this session + value: + request: /v1/order/cancel/session + nonce: + responses: + '200': + description: JSON response + content: + application/json: + schema: + $ref: '#/components/schemas/CancelAllResult' + example: + result: "ok" + details: { + cancelRejects: [330429345], + cancelledOrders: [] + } + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/order/status: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: Get Order Status + operationId: getOrderStatus + description: | + + + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderStatusRequest' + examples: + orderStatusRequest: + value: + request: /v1/order/status + nonce: + order_id: 123456789012345 + include_trades: true + responses: + '200': + description: The order status + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + examples: + limitBuyResponse: + summary: Limit Buy Response + description: JSON response for limit buy + value: + order_id: "123456789012345" + id: "123456789012345" + symbol: "btcusd" + exchange: "gemini" + avg_execution_price: "400.00" + side: "buy" + type: "exchange limit" + timestamp: "1494870642" + timestampms: 1494870642156 + is_live: false + is_cancelled: false + is_hidden: false + was_forced: false + executed_amount: "3" + remaining_amount: "0" + options: [] + price: "400.00" + original_amount: "3" + includeTradesResponse: + summary: Include Trades Response + description: JSON response for market buy with include_trades as True + value: + avg_execution_price: "22728.94" + exchange: "gemini" + executed_amount: "0.0219983861" + id: "17379712927" + is_cancelled: false + is_hidden: false + is_live: false + options: [] + order_id: "17379712927" + remaining_amount: "0" + side: "buy" + symbol: "btcusd" + timestamp: "1608229172" + timestampms: 1608229172627 + trades: [ + { + aggressor: true, + amount: "0.0219983861", + exchange: gemini, + fee_amount: "0.00", + fee_currency: USD, + order_id: "17379712927", + price: "22728.94", + tid: 17379712930, + timestamp: 1608229172, + timestampms: 1608229172627, + type: Buy + } + ] + type: "market buy" + was_forced: false + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/orders: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: List Active Orders + operationId: listActiveOrders + description: | + + + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The API endpoint path + example: /v1/orders + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + example: primary + examples: + basic: + summary: Basic Request + description: Basic request to get active orders + value: + request: /v1/orders + nonce: + withAccount: + summary: With Account Parameter + description: Request with account parameter for Master API keys + value: + request: /v1/orders + nonce: + account: primary + responses: + '200': + description: The active orders + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Order' + examples: + multipleOrders: + summary: Multiple Active Orders + description: Response with multiple active orders + value: + - order_id: "107421210" + id: "107421210" + symbol: "ethusd" + exchange: "gemini" + avg_execution_price: "0.00" + side: "sell" + type: "exchange limit" + timestamp: "1547241628" + timestampms: 1547241628042 + is_live: true + is_cancelled: false + is_hidden: false + was_forced: false + executed_amount: "0" + remaining_amount: "1" + options: [] + price: "125.51" + original_amount: "1" + - order_id: "107421205" + id: "107421205" + symbol: "ethusd" + exchange: "gemini" + avg_execution_price: "125.41" + side: "buy" + type: "exchange limit" + timestamp: "1547241626" + timestampms: 1547241626991 + is_live: true + is_cancelled: false + is_hidden: false + was_forced: false + executed_amount: "0.029147" + remaining_amount: "0.970853" + options: [] + price: "125.42" + original_amount: "1" + emptyOrders: + summary: No Active Orders + description: Response when there are no active orders + value: [] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/orders/history: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: List Past Orders + operationId: listPastOrders + description: | + This API retrieves (closed) orders history for an account. + + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + + ### How to retrieve your order history + + To retrieve your full order history walking backwards, + + 1. Initial request: `POST` to https://api.gemini.com/v1/orders/history with a JSON payload including a `timestamp` key with value `0` and a `limit_orders` key with value `500` + 2. When you receive the list of orders, it will be sorted by `timestamp` descending - so the first element in the list will have the highest `timestamp` value. For this example, say that value is `X`. + 3. Create a second `POST` request with a JSON payload including a `timestamp` key with value `X+1` and a `limit_orders` key with value `500`. + 4. Take the first element of the list returned with highest `timestamp` value `Y` and create a third `POST` request with a JSON payload including a `timestamp` key with value `Y+1` and a `limit_orders` key with value `500`. + 5. Continue creating `POST` requests and retrieving orders until an empty list is returned. + + ### Break Types + + In the rare event that a trade has been reversed (broken), the trade that is broken will have this flag set. The field will contain one of these values + + |Value|Description| + |--- |--- | + |manual|The trade was reversed manually. This means that all fees, proceeds, and debits associated with the trade have been credited or debited to the account seperately. That means that this reported trade must be included for order for the account balance to be correct.| + |full|The trade was fully broken. The reported trade should not be accounted for. It will be as though the transfer of fund associated with the trade had simply not happened.| + + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The API endpoint `/v1/orders/history` + nonce: + $ref: '#/components/schemas/Nonce' + symbol: + type: string + description: "The symbol to retrieve orders for" + limit_orders: + type: integer + description: "The maximum number of orders to return. Default is 50, max is 500." + default: 50 + timestamp: + title: '#/components/schemas/TimestampType' + description: "In iso datetime with timezone format from that date you will get order history" + allOf: + - $ref: '#/components/schemas/TimestampType' + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group." + examples: + basic: + summary: Basic Request + description: Basic request to get order history + value: + request: /v1/orders/history + nonce: + limit_orders: 50 + withSymbolAndTimestamp: + summary: With Symbol and Timestamp + description: Request with symbol and timestamp filters + value: + request: /v1/orders/history + nonce: + symbol: btcusd + timestamp: 1591084414000 + limit_orders: 50 + withAccount: + summary: With Account Parameter + description: Request with account parameter for Master API keys + value: + request: /v1/orders/history + nonce: + account: primary + limit_orders: 100 + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Order' + examples: + cancelledOrder: + summary: Cancelled Order + description: Response with a cancelled order + value: + - order_id: "73751560172006688" + id: "73751560172006688" + symbol: "ethgusd" + exchange: "gemini" + avg_execution_price: "0.00" + side: "buy" + type: "exchange limit" + timestamp: "1695629298" + timestampms: 1695629298505 + is_live: false + is_cancelled: true + is_hidden: false + was_forced: false + executed_amount: "0" + client_order_id: "fb5321b0-2114-47fd-8cca-531a66d7feaf" + options: [] + price: "420.00" + original_amount: "0.69" + remaining_amount: "0.69" + trades: [] + completedOrder: + summary: Completed Order + description: Response with a completed order + value: + - order_id: "107421205" + id: "107421205" + symbol: "btcusd" + exchange: "gemini" + avg_execution_price: "3633.00" + side: "buy" + type: "exchange limit" + timestamp: "1547241626" + timestampms: 1547241626991 + is_live: false + is_cancelled: false + is_hidden: false + was_forced: false + executed_amount: "5" + remaining_amount: "0" + options: [] + price: "3633.00" + original_amount: "5" + trades: [ + { + price: "3633.00", + amount: "5", + timestamp: 1547241627, + timestampms: 1547241627000, + type: "Buy", + aggressor: true, + fee_currency: "USD", + fee_amount: "9.0825", + tid: 106921823, + order_id: "107421205", + exchange: "gemini" + } + ] + emptyHistory: + summary: Empty History + description: Response when there are no orders in history + value: [] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/mytrades: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: List Past Trades + operationId: listPastTrades + description: | + + + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + ### How to retrieve your trade history + + To retrieve your full trade history walking backwards, + + 1. Initial request: `POST` to https://api.gemini.com/v1/mytrades with a JSON payload including a `timestamp` key with value 0 and a `limit_trades` key with value `500` + 2. When you receive the list of trades, it will be sorted by `timestamp` descending - so the first element in the list will have the highest `timestamp` value. For this example, say that value is `X`. + 3. Create a second `POST` request with a JSON payload including a `timestamp` key with value `X+1` and a `limit_trades` key with value `500`. + 4. Take the first element of the list returned with highest `timestamp` value `Y` and create a third `POST` request with a JSON payload including a `timestamp` key with value `Y+1` and a `limit_trades` key with value `500`. + 5. Continue creating `POST` requests and retrieving trades until an empty list is returned. + + ### Break Types + + In the rare event that a trade has been reversed (broken), the trade that is broken will have this flag set. The field will contain one of these values + + |Value|Description| + |--- |--- | + |manual|The trade was reversed manually. This means that all fees, proceeds, and debits associated with the trade have been credited or debited to the account seperately. That means that this reported trade must be included for order for the account balance to be correct.| + |full|The trade was fully broken. The reported trade should not be accounted for. It will be as though the transfer of fund associated with the trade had simply not happened.| + + + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MyTradesRequest' + examples: + basic: + summary: Basic Request + description: Basic request to get past trades for a symbol + value: + request: /v1/mytrades + nonce: + symbol: btcusd + withLimitAndTimestamp: + summary: With Limit and Timestamp + description: Request with limit and timestamp parameters + value: + request: /v1/mytrades + nonce: + symbol: btcusd + limit_trades: 100 + timestamp: 1591084414000 + withAccount: + summary: With Account Parameter + description: Request with account parameter for Master API keys + value: + request: /v1/mytrades + nonce: + symbol: btcusd + account: primary + responses: + '200': + description: The past trades + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/MyTrade' + examples: + multipleTrades: + summary: Multiple Trades + description: Response with multiple trades + value: + - price: "3648.09" + amount: "0.0027343246" + timestamp: 1547232911 + timestampms: 1547232911021 + type: "Buy" + aggressor: true + fee_currency: "USD" + fee_amount: "0.024937655575035" + tid: 107317526 + order_id: "107317524" + exchange: "gemini" + is_clearing_fill: false + symbol: "BTCUSD" + - price: "3633.00" + amount: "0.00423677" + timestamp: 1547220640 + timestampms: 1547220640195 + type: "Buy" + aggressor: false + fee_currency: "USD" + fee_amount: "0.038480463525" + tid: 106921823 + order_id: "106817811" + exchange: "gemini" + is_clearing_fill: false + symbol: "BTCUSD" + withClientOrderId: + summary: Trade with Client Order ID + description: Response with a trade that includes client_order_id + value: + - price: "42000.00" + amount: "0.5" + timestamp: 1616492376 + timestampms: 1616492376594 + type: "Sell" + aggressor: true + fee_currency: "USD" + fee_amount: "52.50" + tid: 123456789 + order_id: "123456789" + client_order_id: "my-custom-id-12345" + exchange: "gemini" + is_clearing_fill: false + symbol: "BTCUSD" + emptyTrades: + summary: No Trades + description: Response when there are no trades matching the criteria + value: [] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/tradevolume: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: Get Trading Volume + operationId: getTradingVolume + description: | + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The API endpoint path + example: /v1/tradevolume + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + example: primary + examples: + basic: + summary: Basic Request + description: Basic request to get trade volume + value: + request: /v1/tradevolume + nonce: + withAccount: + summary: With Account Parameter + description: Request with account parameter for Master API keys + value: + request: /v1/tradevolume + nonce: + account: primary + responses: + '200': + description: The trade volume + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TradeVolume' + examples: + multipleSymbols: + summary: Multiple Symbols + description: Response with trade volume for multiple symbols + value: + - - symbol: btcusd + base_currency: BTC + notional_currency: USD + data_date: "2019-01-10" + total_volume_base: 8.06021756 + maker_buy_sell_ratio: 1 + buy_maker_base: 6.06021756 + buy_maker_notional: 23461.3515203844 + buy_maker_count: 34 + sell_maker_base: 0 + sell_maker_notional: 0 + sell_maker_count: 0 + buy_taker_base: 0 + buy_taker_notional: 0 + buy_taker_count: 0 + sell_taker_base: 2 + sell_taker_notional: 7935.66 + sell_taker_count: 2 + - symbol: ltcusd + base_currency: LTC + notional_currency: USD + data_date: "2019-01-11" + total_volume_base: 3 + maker_buy_sell_ratio: 0 + buy_maker_base: 0 + buy_maker_notional: 0 + buy_maker_count: 0 + sell_maker_base: 0 + sell_maker_notional: 0 + sell_maker_count: 0 + buy_taker_base: 3 + buy_taker_notional: 98.22 + buy_taker_count: 3 + sell_taker_base: 0 + sell_taker_notional: 0 + sell_taker_count: 0 + singleSymbol: + summary: Single Symbol + description: Response with trade volume for a single symbol + value: + - - symbol: ethusd + base_currency: ETH + notional_currency: USD + data_date: "2019-01-10" + total_volume_base: 25.5 + maker_buy_sell_ratio: 0.75 + buy_maker_base: 15.5 + buy_maker_notional: 3875.00 + buy_maker_count: 12 + sell_maker_base: 5.0 + sell_maker_notional: 1250.00 + sell_maker_count: 4 + buy_taker_base: 2.5 + buy_taker_notional: 625.00 + buy_taker_count: 2 + sell_taker_base: 2.5 + sell_taker_notional: 625.00 + sell_taker_count: 2 + emptyVolume: + summary: No Trade Volume + description: Response when there is no trade volume + value: [[]] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/balances: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Get Available Balances + operationId: getAvailableBalances + description: | + + + This will show the available balances in the supported currencies + + + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `balances:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - account + properties: + request: + type: string + description: The API endpoint path + example: /v1/balances + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + type: string + example: primary + showPendingBalances: + description: | + Whether to include pending balances such as in-flight crypto deposits or withdrawals in the balances response. + + > **Note:** Setting this field to `true` will result in slower response times due to additional database lookups required to retrieve pending balance information. + default: false + type: boolean + example: false + example: + request: /v1/balances + nonce: + account: primary + showPendingBalances: false + responses: + '200': + description: The account balances + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Balance' + examples: + multipleBalances: + summary: Multiple Balances + description: Response with multiple currency balances (showPendingBalances = false) + value: + - type: "exchange" + currency: "BTC" + amount: "5.0" + available: "4.5" + availableForWithdrawal: "4.5" + _timestamp: "2024-03-16T00:00:00.000000Z" + - type: "exchange" + currency: "USD" + amount: "15000.00" + available: "5000.00" + availableForWithdrawal: "5000.00" + _timestamp: "2024-03-16T00:00:00.000000Z" + - type: "exchange" + currency: "ETH" + amount: "10.0" + available: "10.0" + availableForWithdrawal: "10.0" + _timestamp: "2024-03-16T00:00:00.000000Z" + emptyBalances: + summary: Empty Balances + description: Response when there are no balances + value: [] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/notionalvolume: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: Get Notional Trading Volume + operationId: getNotionalTradingVolume + description: | + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The API endpoint path + example: /v1/notionalvolume + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + example: primary + symbol: + type: string + description: "Optional. The symbol to get fee promotions or specific fee schedule rates for." + example: btcusd + examples: + basic: + summary: Basic Request + description: Basic request to get notional volume + value: + request: /v1/notionalvolume + nonce: + withSymbol: + summary: With Symbol Parameter + description: Request with symbol parameter for fee promotions + value: + request: /v1/notionalvolume + nonce: + symbol: btcusd + withAccount: + summary: With Account Parameter + description: Request with account parameter for Master API keys + value: + request: /v1/notionalvolume + nonce: + account: primary + responses: + '200': + description: The notional volume + content: + application/json: + schema: + $ref: '#/components/schemas/NotionalVolume' + examples: + standardResponse: + summary: Standard Response + description: Response with notional volume and fee information + value: + web_maker_fee_bps: 25 + web_taker_fee_bps: 35 + web_auction_fee_bps: 25 + api_maker_fee_bps: 10 + api_taker_fee_bps: 35 + api_auction_fee_bps: 20 + fix_maker_fee_bps: 10 + fix_taker_fee_bps: 35 + fix_auction_fee_bps: 20 + notional_30d_volume: 150.00 + last_updated_ms: 1551371446000 + date: "2019-02-28" + notional_1d_volume: [ + { + date: "2019-02-22", + notional_volume: 75.00 + }, + { + date: "2019-02-14", + notional_volume: 75.00 + } + ] + withFeeTier: + summary: With Fee Tier + description: Response including fee tier information + value: + web_maker_fee_bps: 25 + web_taker_fee_bps: 35 + web_auction_fee_bps: 25 + api_maker_fee_bps: 0 + api_taker_fee_bps: 10 + api_auction_fee_bps: 10 + fix_maker_fee_bps: 0 + fix_taker_fee_bps: 10 + fix_auction_fee_bps: 10 + notional_30d_volume: 15000000.00 + api_notional_30d_volume: 12500000.00 + last_updated_ms: 1551371446000 + date: "2019-02-28" + fee_tier: { + tier: "0bps", + api_maker_fee_bps: 0, + api_taker_fee_bps: 10 + } + notional_1d_volume: [ + { + date: "2019-02-28", + notional_volume: 500000.00 + }, + { + date: "2019-02-27", + notional_volume: 750000.00 + } + ] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/margin/account: + post: + x-zudoku-playground-enabled: false + tags: + - Margin Trading + summary: Get Margin Account Summary + operationId: getMarginAccount + description: | + Retrieves comprehensive margin account information including collateral, leverage, buying/selling power, and liquidation risk. + + This endpoint provides real-time margin statistics for spot margin trading accounts, helping you monitor your account health and manage risk. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `balances:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + ### Account Type + This endpoint is only available for margin trading accounts. Standard exchange accounts will receive an error. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/margin/account" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group." + example: + request: "/v1/margin/account" + nonce: + + responses: + '200': + description: Margin account summary with risk statistics + content: + application/json: + schema: + $ref: '#/components/schemas/MarginAccountSummary' + example: + marginAssetValue: + currency: "USD" + value: "10000.00" + availableCollateral: + currency: "USD" + value: "8500.00" + notionalValue: + currency: "USD" + value: "15000.00" + totalBorrowed: + currency: "USD" + value: "5000.00" + leverage: "1.5" + buyingPower: + currency: "USD" + value: "8500.00" + sellingPower: + currency: "USD" + value: "8500.00" + liquidationRisk: + lossPercentage: "0.1550" + liquidationPrice: + currency: "USD" + value: "50000.00" + interestRate: + rate: "0.00001141552511" + interval: "hour" + reservedBuyOrders: + currency: "USD" + value: "1000.00" + reservedSellOrders: + currency: "USD" + value: "500.00" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/margin/rates: + post: + x-zudoku-playground-enabled: false + tags: + - Margin Trading + summary: Get Margin Interest Rates + operationId: getMarginRates + description: | + Retrieves current margin interest rates for all borrowable assets. + + Returns hourly, daily, and annual borrow rates for each currency that can be borrowed on margin. Interest is charged on borrowed amounts at the hourly rate and compounds over time. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `balances:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + ### Account Type + This endpoint is only available for margin trading accounts. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/margin/rates" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group." + example: + request: "/v1/margin/rates" + nonce: + + responses: + '200': + description: Current margin interest rates for all borrowable assets + content: + application/json: + schema: + $ref: '#/components/schemas/MarginRatesResponse' + example: + rates: + - currency: "BTC" + borrowRate: "0.00001141552511" + borrowRateDaily: "0.00027397260264" + borrowRateAnnual: "0.1" + lastUpdated: 1700000000000 + - currency: "ETH" + borrowRate: "0.00001141552511" + borrowRateDaily: "0.00027397260264" + borrowRateAnnual: "0.1" + lastUpdated: 1700000000000 + - currency: "USD" + borrowRate: "0.00000913242009" + borrowRateDaily: "0.00021917808216" + borrowRateAnnual: "0.08" + lastUpdated: 1700000000000 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/margin/order/preview: + post: + x-zudoku-playground-enabled: false + tags: + - Margin Trading + summary: Preview Margin Order Impact + operationId: previewMarginOrder + description: | + Previews the margin impact of a hypothetical spot order without actually placing it. + + Returns both pre-order and post-order margin risk statistics, allowing you to understand how an order would affect your margin account before execution. This is useful for risk management and planning trades. + + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + ### Account Type + This endpoint is only available for margin trading accounts. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - symbol + - side + - type + properties: + request: + type: string + description: The literal string "/v1/margin/order/preview" + nonce: + $ref: '#/components/schemas/Nonce' + symbol: + type: string + description: The trading pair symbol (e.g., "btcusd") + example: "btcusd" + side: + type: string + enum: [buy, sell] + description: The order side + example: "buy" + type: + type: string + enum: [market, limit] + description: The order type + example: "limit" + amount: + type: string + format: decimal + description: The order amount in base currency (required for limit orders and sell market orders) + example: "0.5" + price: + type: string + format: decimal + description: The limit price (required for limit orders) + example: "50000.00" + totalSpend: + type: string + format: decimal + description: Total spend in quote currency (required for buy market orders) + example: "25000.00" + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group." + examples: + limitBuy: + summary: Limit Buy Order Preview + description: Preview a limit buy order + value: + request: "/v1/margin/order/preview" + nonce: + symbol: "btcusd" + side: "buy" + type: "limit" + amount: "0.5" + price: "50000.00" + marketBuy: + summary: Market Buy Order Preview + description: Preview a market buy order using total spend + value: + request: "/v1/margin/order/preview" + nonce: + symbol: "ethusd" + side: "buy" + type: "market" + totalSpend: "5000.00" + marketSell: + summary: Market Sell Order Preview + description: Preview a market sell order + value: + request: "/v1/margin/order/preview" + nonce: + symbol: "btcusd" + side: "sell" + type: "market" + amount: "0.25" + + responses: + '200': + description: Pre-order and post-order margin risk statistics + content: + application/json: + schema: + $ref: '#/components/schemas/MarginOrderPreview' + example: + preorder: + marginAssetValue: + currency: "USD" + value: "10000.00" + availableCollateral: + currency: "USD" + value: "8500.00" + notionalValue: + currency: "USD" + value: "15000.00" + totalBorrowed: + currency: "USD" + value: "5000.00" + leverage: "1.5" + reservedBuyOrders: + currency: "USD" + value: "0.00" + reservedSellOrders: + currency: "USD" + value: "0.00" + buyingPower: + currency: "USD" + value: "8500.00" + sellingPower: + currency: "USD" + value: "8500.00" + postorder: + marginAssetValue: + currency: "USD" + value: "10000.00" + availableCollateral: + currency: "USD" + value: "6000.00" + notionalValue: + currency: "USD" + value: "40000.00" + totalBorrowed: + currency: "USD" + value: "30000.00" + leverage: "4.0" + reservedBuyOrders: + currency: "USD" + value: "0.00" + reservedSellOrders: + currency: "USD" + value: "0.00" + buyingPower: + currency: "USD" + value: "6000.00" + sellingPower: + currency: "USD" + value: "6000.00" + liquidationRisk: + lossPercentage: "0.6000" + liquidationPrice: + currency: "USD" + value: "30000.00" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/heartbeat: + post: + x-zudoku-playground-enabled: false + tags: + - Session + summary: Heartbeat + operationId: sendHeartbeat + description: | + This will prevent a [session](/authentication/api-key#private-api-invocation) from timing out and canceling orders if the [require heartbeat](/authentication/api-key#require-heartbeat) flag has been set. Note that this is only required if no other private API requests have been made. The arrival of any message resets the heartbeat timer. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Heartbeat' + example: + request: "/v1/heartbeat" + nonce: + responses: + '200': + description: The heartbeat was received successfully + content: + application/json: + schema: + type: object + properties: + result: + type: string + example: ok + description: ok + example: + result: ok + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/wrap/{symbol}: + post: + x-zudoku-playground-enabled: false + tags: + - Orders + summary: Wrap Order + operationId: wrapOrder + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + parameters: + - $ref: '#/components/parameters/symbolParam' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - amount + properties: + request: + type: string + description: The literal string "/v1/wrap/symbol" + nonce: + $ref: '#/components/schemas/Nonce' + amount: + type: string + description: The amount to wrap + side: + type: string + enum: [buy, sell] + description: '"buy" or "sell"' + client_order_id: + type: string + description: A client-specified order id + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + example: + request: "/v1/wrap/GUSDUSD" + nonce: + amount: "1" + side: "buy" + client_order_id: "4ac6f45f-baf1-40f8-83c5-001e3ea73c7f" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + orderId: + type: string + description: The order ID + pair: + type: string + description: Trading pair symbol + price: + type: string + description: The price of the order + priceCurrency: + type: string + description: The currency in which the order is priced + side: + type: string + description: Either "buy" or "sell" + quantity: + type: string + description: The amount that was executed + quantityCurrency: + type: string + description: The currency label for the quantity field + totalSpend: + type: string + description: Total quantity spent for the order + totalSpendCurrency: + type: string + description: Currency of the totalSpend + fee: + type: string + description: The amount charged + feeCurrency: + type: string + description: Currency that the fee was paid in + depositFee: + type: string + description: The deposit fee quantity + depositFeeCurrency: + type: string + description: Currency in which depositFee is taken + example: + orderId: 429135395 + pair: "GUSDUSD" + price: "1" + priceCurrency: "USD" + side: "buy" + quantity: "1" + quantityCurrency: "GUSD" + totalSpend: "1" + totalSpendCurrency: "USD" + fee: "0" + feeCurrency: "USD" + depositFee: "0" + depositFeeCurrency: "USD" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/notionalbalances/{currency}: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Get Notional Balances + operationId: getNotionalBalances + description: | + + + This will show the available balances in the supported currencies as well as the notional value in the currency specified. + + + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `balances:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + parameters: + - $ref: '#/components/parameters/currencyParam' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/notionalbalances/currency" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + basic: + summary: Basic Request + description: Basic request to get notional balances in USD + value: + request: "/v1/notionalbalances/usd" + nonce: + withAccount: + summary: With Account Parameter + description: Request with account parameter for Master API keys + value: + request: "/v1/notionalbalances/usd" + nonce: + account: "primary" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NotionalBalance' + example: + - currency: "BTC" + amount: "1154.62034001" + amountNotional: "10386000.59" + available: "1129.10517279" + availableNotional: "10161000.71" + availableForWithdrawal: "1129.10517279" + availableForWithdrawalNotional: "10161000.71" + - currency: "USD" + amount: "18722.79" + amountNotional: "18722.79" + available: "14481.62" + availableNotional: "14481.62" + availableForWithdrawal: "14481.62" + availableForWithdrawalNotional: "14481.62" + - currency: "ETH" + amount: "20124.50369697" + amountNotional: "100621.31" + available: "20124.50369697" + availableNotional: "100621.31" + availableForWithdrawal: "20124.50369697" + availableForWithdrawalNotional: "100621.31" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/addresses/{network}: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: List Deposit Addresses + operationId: listDepositAddresses + description: | + + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `addresses:read` or `addresses:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + parameters: + - $ref: '#/components/parameters/networkParam' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/addresses/network" + nonce: + $ref: '#/components/schemas/Nonce' + timestamp: + $ref: '#/components/schemas/TimestampType' + description: Only returns addresses created on or after this timestamp + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + basic: + summary: Basic Request + description: Basic request to get Bitcoin deposit addresses + value: + request: "/v1/addresses/bitcoin" + nonce: + withTimestamp: + summary: With Timestamp + description: Request with timestamp filter + value: + request: "/v1/addresses/ethereum" + nonce: + timestamp: 1591084414000 + withAccount: + summary: With Account Parameter + description: Request with account parameter for Master API keys + value: + request: "/v1/addresses/bitcoin" + nonce: + account: "primary" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Address' + examples: + bitcoinAddresses: + summary: Bitcoin Addresses + description: Response with Bitcoin deposit addresses + value: + - address: "n2saq73aDTu42bRgEHd8gd4to1gCzHxrdj" + timestamp: 1424285102000 + label: "my bitcoin address" + - address: "n2wpl14aJEu10bRgMNd0gdjH8dHJ3h2a3ks" + timestamp: 1824785101000 + ethereumAddresses: + summary: Ethereum Addresses + description: Response with Ethereum deposit addresses + value: + - address: "0x1f7a49d62d5256d0a80d31a07f57b578c3e40183" + timestamp: 1591084414000 + label: "main eth address" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/deposit/{network}/newAddress: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Create New Deposit Address + operationId: createNewDepositAddress + description: | + + + ### Roles + The API key you use to access this endpoint must have the Fund Manager role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `addresses:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + parameters: + - $ref: '#/components/parameters/networkParam' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/deposit/network/newAddress" + nonce: + $ref: '#/components/schemas/Nonce' + label: + type: string + description: A label for the address + legacy: + type: boolean + description: Whether to generate a legacy P2SH-P2PKH litecoin address. False by default. + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + basicBitcoin: + summary: Basic Bitcoin Address Request + description: Basic request to create a new Bitcoin deposit address + value: + request: "/v1/deposit/bitcoin/newAddress" + nonce: + label: "optional test label" + legacyLitecoin: + summary: Legacy Litecoin Address Request + description: Request to create a legacy Litecoin deposit address + value: + request: "/v1/deposit/litecoin/newAddress" + nonce: + label: "LTC legacy deposit address" + legacy: true + withAccount: + summary: With Account Parameter + description: Request with account parameter for Master API keys + value: + request: "/v1/deposit/ethereum/newAddress" + nonce: + label: "ETH deposit address" + account: "primary" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Address' + examples: + bitcoinAddress: + summary: Bitcoin Address + description: Response with a new Bitcoin deposit address + value: + network: "bitcoin" + address: "n2saq73aDTu42bRgEHd8gd4to1gCzHxrdj" + label: "optional test label" + litecoinAddress: + summary: Litecoin Address + description: Response with a new Litecoin deposit address + value: + network: "litecoin" + address: "MJRSgZ3UUFcTBTBAcN38XAXvZLwRe8WVw7" + label: "LTC legacy deposit address" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v2/transfers: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: List Past Transfers + operationId: listPastTransfers + description: | + + + This endpoint shows deposits and withdrawals in supported currencies with full multichain (multi-network) support. It returns accurate status information for transfers on **all supported networks** including Solana, Arbitrum, Optimism, Base, Avalanche, and Ethereum. + + Each transfer in the response includes a `network` field identifying the blockchain network, along with network-specific `feeAmount`, `feeCurrency`, and `txHash` values. + + This endpoint does not currently show cancelled advances, returned outgoing wires or ACH transactions, or other exceptional transaction circumstances. + + Fiat transfers between non-derivative and derivatives accounts are prohibited. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v2/transfers" + nonce: + $ref: '#/components/schemas/Nonce' + currency: + type: string + description: Currency code, see symbols and minimums + network: + type: string + description: Filter transfers by blockchain network (e.g., `ethereum`, `solana`, `arbitrum`, `optimism`, `base`, `avalanche`) + timestamp: + $ref: '#/components/schemas/TimestampType' + description: Only return transfers after this timestamp + limit_transfers: + type: integer + description: The maximum number of transfers to return. The default is 10 and the maximum is 50. + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + show_completed_deposit_advances: + type: boolean + description: Whether to display completed deposit advances. True by default. + examples: + basic: + summary: Basic Request + description: Basic request to get all transfers across all networks + value: + request: "/v2/transfers" + nonce: + withCurrency: + summary: With Currency Filter + description: Request with currency filter for USDC transfers + value: + request: "/v2/transfers" + nonce: + currency: "USDC" + withNetwork: + summary: With Network Filter + description: Request filtered to a specific network + value: + request: "/v2/transfers" + nonce: + currency: "USDC" + network: "solana" + withFilters: + summary: With Multiple Filters + description: Request with timestamp, limit, and account filters + value: + request: "/v2/transfers" + nonce: + timestamp: 1591084414000 + limit_transfers: 25 + account: "primary" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/V2Transfer' + examples: + multiNetworkTransfers: + summary: Multi-Network Transfers + description: Response with transfers across different networks + value: + - type: "Withdrawal" + status: "Complete" + timestampms: 1772818620354 + eid: 368598436530 + currency: "USDC" + amount: "0.01" + network: "ethereum" + feeAmount: "0.118856" + feeCurrency: "USDC" + txHash: "714a15c4fd2d37629d27e56a63aaec5f91e0053dde3eb357be71663c1197b391" + destination: "0x83cFb8C13f06716b449E5D24F5b4cc6Cc64a189A" + withdrawalId: "69ab0b0f-3c76-433d-84a5-c8bcc6718411" + - type: "Withdrawal" + status: "Complete" + timestampms: 1772034480391 + eid: 368474658614 + currency: "USDC" + amount: "1799" + network: "solana" + feeAmount: "0.202536" + feeCurrency: "USDC" + txHash: "4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi" + withdrawalId: "9b7a5100-b3ee-4820-9b5b-c2dd7db6559b" + - type: "Deposit" + status: "Advanced" + timestampms: 1771990797452 + eid: 309356152 + currency: "ETH" + amount: "100" + network: "ethereum" + feeAmount: "0" + feeCurrency: "ETH" + txHash: "605c5fa8bf99458d24d61e09941bc443ddc44839d9aaa508b14b296c0c8269b2" + adminTransactions: + summary: Administrative Transactions + description: Response with administrative credits and debits (no network field) + value: + - type: "AdminDebit" + status: "Complete" + timestampms: 1626990636645 + eid: 1001248356 + currency: "BTC" + amount: "6" + purpose: "Administrative debit" + - type: "AdminCredit" + status: "Complete" + timestampms: 1626990476421 + eid: 1001245359 + currency: "BTC" + amount: "4" + purpose: "Fee reimbursement credit" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/custodyaccountfees: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: List Custody Fee Transfers + operationId: listCustodyFeeTransfers + description: | + + + This endpoint shows Custody fee records in the supported currencies. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/custodyaccountfees" + nonce: + $ref: '#/components/schemas/Nonce' + timestamp: + $ref: '#/components/schemas/TimestampType' + description: Only return Custody fee records on or after this timestamp + limit_transfers: + type: integer + description: The maximum number of Custody fee records to return. The default is 10 and the maximum is 50. + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + basic: + summary: Basic Request + description: Basic request to get custody account fees + value: + request: "/v1/custodyaccountfees" + nonce: + withFilters: + summary: With Filters + description: Request with timestamp and limit filters + value: + request: "/v1/custodyaccountfees" + nonce: + timestamp: 1652279000000 + limit_transfers: 20 + withAccount: + summary: With Account Parameter + description: Request with account parameter for Master API keys + value: + request: "/v1/custodyaccountfees" + nonce: + account: "primary" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CustodyFeeTransfer' + examples: + multipleFees: + summary: Multiple Fee Records + description: Response with multiple custody fee records + value: + - txTime: 1657236174056 + feeAmount: "10" + feeCurrency: "BTC" + eid: 256627 + eventType: "Withdrawal" + - txTime: 1652279045196 + feeAmount: "10000000" + feeCurrency: "ETH" + eid: 15364 + eventType: "CustodyFeeDebit" + - txTime: 1652279025196 + feeAmount: "1850" + feeCurrency: "WFIL" + eid: 9016 + eventType: "RiaFeeDebit" + - txTime: 1652279025196 + feeAmount: "1850" + feeCurrency: "WFIL" + eid: 9016 + eventType: "RiaFeeCredit" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v2/withdraw/{network}/{ticker}/feeEstimate: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Get Gas Fee Estimation + operationId: getGasFeeEstimation + description: | + + + API users will not be aware of the transfer fees before starting the withdrawal process. This endpoint allows you to find out the estimated gas fees before you start a withdrawal. It requires specifying the blockchain network and ticker, which is useful for tokens that exist on multiple networks (e.g. USDC on Ethereum vs Solana). + + ### Roles + The API key you use to access this endpoint can have the Trader, Fund Manager, Auditor, WealthManager or Administrator role assigned. See [Roles](#roles) for more information. + + parameters: + - name: network + in: path + description: The blockchain network for the withdrawal (e.g. `ethereum`, `bitcoin`, `solana`) + required: true + schema: + type: string + example: ethereum + - name: ticker + in: path + description: The currency code for the withdrawal (e.g. `eth`, `btc`, `sol`, `usdc`) + required: true + schema: + type: string + example: eth + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + description: Sample payload for ETH fee estimation on Ethereum network + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FeeEstimateV2Request' + examples: + ethOnEthereum: + summary: ETH on Ethereum + description: Estimate withdrawal fee for ETH on Ethereum network + value: + request: "/v2/withdraw/ethereum/eth/feeEstimate" + nonce: + address: "0x31c2105b8dea834167f32f7ea7d877812e059230" + amount: "0.01" + usdcOnSolana: + summary: USDC on Solana + description: Estimate withdrawal fee for USDC on Solana network + value: + request: "/v2/withdraw/solana/usdc/feeEstimate" + nonce: + address: "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV" + amount: "100" + btcOnBitcoin: + summary: BTC on Bitcoin + description: Estimate withdrawal fee for BTC on Bitcoin network + value: + request: "/v2/withdraw/bitcoin/btc/feeEstimate" + nonce: + address: "mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR" + amount: "0.5" + responses: + '200': + description: Successful fee estimation response + content: + application/json: + schema: + $ref: '#/components/schemas/FeeEstimateV2Response' + examples: + ethResponse: + summary: ETH Fee Estimate Response + description: JSON response for ETH withdrawal fee estimate + value: + currency: "ETH" + fee: 0.001 + isOverride: false + monthlyLimit: 1 + monthlyRemaining: 1 + usdcOnSolanaResponse: + summary: USDC on Solana Fee Estimate Response + description: JSON response for USDC on Solana withdrawal fee estimate + value: + currency: "USDC" + fee: 0.01 + isOverride: false + monthlyLimit: 10 + monthlyRemaining: 8 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + + /v2/withdraw/{network}/{ticker}: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Withdraw Crypto Funds + operationId: withdrawCryptoFunds + description: | + + + Withdraw cryptocurrency funds to an approved address, with explicit network selection. + + The key improvement over v1 is the explicit `network` path parameter, which allows you to specify exactly which blockchain network to use for the withdrawal. This is especially important for tokens available on multiple networks (e.g., USDC on Ethereum, Solana, Base, Arbitrum, etc.). + + Before you can withdraw cryptocurrency funds to an approved address, you need three things: + + 1. You must have an approved address list for your account + 2. The address you want to withdraw funds to needs to already be on that approved address list + 3. An API key with the Fund Manager role added + + If you would like to withdraw via API to addresses that are not on your approved address list, please reach out to trading@gemini.com. We can enable this feature for you provided a set of approved IP addresses. This functionality is only available for exchange accounts. Pre-approved IP addresses and addresses added to your approved address list are required to enable withdrawal APIs for custody accounts. + + Use the [Get Network](/rest/market-data#get-network) endpoint to discover which networks support withdrawals for a given token. + + See [Roles](/roles#roles) for more information on how to add the Fund Manager role to the API key you want to use. + + ### Roles + The API key you use to access this endpoint must have the Fund Manager role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `crypto:send` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + parameters: + - $ref: '#/components/parameters/networkParam' + - name: ticker + in: path + required: true + schema: + type: string + description: The cryptocurrency ticker code (e.g., `btc`, `eth`, `usdc`). See [Symbols and minimums](/market-data/symbols-and-minimums). + example: eth + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - address + - amount + properties: + address: + type: string + description: The destination address for the withdrawal + amount: + type: string + description: The amount to withdraw + memo: + type: string + description: Required for certain networks that use memos (e.g., Solana, XRP, Cosmos). The destination tag or memo for the withdrawal. + clientTransferId: + type: string + format: uuid + description: A unique UUID for idempotent withdrawals. If provided, duplicate requests with the same `clientTransferId` will not create additional withdrawals. + examples: + btcWithdrawal: + summary: BTC Withdrawal on Bitcoin + description: JSON payload for BTC withdrawal on the Bitcoin network + value: + address: "mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR" + amount: "1" + ethWithdrawal: + summary: ETH Withdrawal on Ethereum + description: JSON payload for ETH withdrawal on the Ethereum network with client transfer ID + value: + address: "0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28" + amount: "2.34567" + clientTransferId: "AA97B177-9383-4934-8543-0F91A7A02838" + usdcWithdrawalSolana: + summary: USDC Withdrawal on Solana + description: JSON payload for USDC withdrawal on the Solana network + value: + address: "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV" + amount: "100" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/WithdrawCryptoFundsResponse' + examples: + btcWithdrawalResponse: + summary: BTC Withdrawal Response + description: JSON response for BTC withdrawal on Bitcoin network + value: + withdrawalId: "02176a83-a6b1-4202-9b85-1c1c92dd25c4" + address: "mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR" + amount: "1" + currency: "BTC" + fee: "0" + ethWithdrawalResponse: + summary: ETH Withdrawal Response + description: JSON response for ETH withdrawal on Ethereum network + value: + withdrawalId: "82de1a28-05a5-4f5c-9b3a-d78b1e3e0c91" + address: "0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28" + amount: "2.34567" + currency: "ETH" + fee: "0.001" + usdcWithdrawalSolanaResponse: + summary: USDC Withdrawal on Solana Response + description: JSON response for USDC withdrawal on Solana network + value: + withdrawalId: "f4a1c7b3-2e8d-4a9f-b6c5-1d3e7f8a9b0c" + address: "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV" + amount: "100" + currency: "USDC" + fee: "0" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/clearing/new: + post: + x-zudoku-playground-enabled: false + tags: + - Clearing + summary: Create New Clearing Order + operationId: createNewClearingOrder + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `clearing:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - symbol + - amount + - price + - side + properties: + request: + type: string + description: The literal string "/v1/clearing/new" + nonce: + $ref: '#/components/schemas/Nonce' + symbol: + type: string + description: The trading pair + amount: + type: string + description: The amount to trade + price: + type: string + description: The price + side: + type: string + enum: [buy, sell] + description: The direction of the trade + counterparty_id: + type: string + description: The counterparty ID + expires_in_hrs: + type: integer + description: The number of hours until the order expires + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + newClearingOrder: + summary: New Clearing Order + description: JSON payload for creating a new clearing order + value: + request: "/v1/clearing/new" + nonce: + counterparty_id: "OM9VNL1G" + expires_in_hrs: 24 + symbol: "btcusd" + amount: "100" + price: "9500.00" + side: "buy" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ClearingOrder' + example: + result: "AwaitConfirm" + clearing_id: "0OQGOZXW" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/clearing/status: + post: + x-zudoku-playground-enabled: false + tags: + - Clearing + summary: Get Clearing Order + operationId: getClearingOrder + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `clearing:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - clearing_id + properties: + request: + type: string + description: The literal string "/v1/clearing/status" + nonce: + $ref: '#/components/schemas/Nonce' + clearing_id: + type: string + description: The clearing ID + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + orderStatusRequest: + summary: Order Status Request + description: JSON payload for checking clearing order status + value: + request: "/v1/clearing/status" + nonce: + clearing_id: "OM9VNL1G" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ClearingOrder' + example: + result: "ok" + status: "Confirmed" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/clearing/cancel: + post: + x-zudoku-playground-enabled: false + tags: + - Clearing + summary: Cancel Clearing Order + operationId: cancelClearingOrder + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `clearing:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - clearing_id + properties: + request: + type: string + description: The literal string "/v1/clearing/cancel" + nonce: + $ref: '#/components/schemas/Nonce' + clearing_id: + type: string + description: The clearing ID + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + cancelOrderRequest: + summary: Cancel Order Request + description: JSON payload for canceling a clearing order + value: + request: "/v1/clearing/cancel" + nonce: + clearing_id: "P0521QDV" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + result: + type: string + description: Status of the cancel operation + details: + type: string + description: Detailed description of the result + examples: + successfulCancel: + summary: Successful Cancel + description: JSON response for successful order cancellation + value: + result: "ok" + details: "P0521QDV order canceled" + failedCancel: + summary: Failed Cancel + description: JSON response for failed order cancellation + value: + result: "failed" + details: "Unable to cancel order P0521QDV" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/clearing/confirm: + post: + x-zudoku-playground-enabled: false + tags: + - Clearing + summary: Confirm Clearing Order + operationId: confirmClearingOrder + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `clearing:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - clearing_id + - symbol + - amount + - price + - side + properties: + request: + type: string + description: The literal string "/v1/clearing/confirm" + nonce: + $ref: '#/components/schemas/Nonce' + clearing_id: + type: string + description: The clearing ID + symbol: + type: string + description: The trading pair + amount: + type: string + description: The amount to trade + price: + type: string + description: The price + side: + type: string + enum: [buy, sell] + description: The direction of the trade + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + confirmOrderRequest: + summary: Confirm Order Request + description: JSON payload for confirming a clearing order + value: + request: "/v1/clearing/confirm" + nonce: + clearing_id: "OM9VNL1G" + symbol: "btcusd" + amount: "100" + price: "9500.00" + side: "sell" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + result: + type: string + description: Status of the confirmation operation + examples: + successfulConfirm: + summary: Successful Confirmation + description: JSON response for successful order confirmation + value: + result: "confirmed" + failedConfirm: + summary: Failed Confirmation + description: JSON response for failed order confirmation + value: + result: "error" + reason: "InvalidSide" + message: "Invalid side for symbol BTCUSD: 'buy'" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/clearing/list: + post: + x-zudoku-playground-enabled: false + tags: + - Clearing + summary: List Clearing Orders + operationId: listClearingOrders + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `clearing:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/clearing/list" + nonce: + $ref: '#/components/schemas/Nonce' + symbol: + type: string + description: Trading pair + counterparty: + type: string + description: counterparty_id or counterparty_alias + side: + type: string + enum: [buy, sell] + description: '"buy" or "sell"' + expiration_start: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp. Requires `expiration_end` if set + expiration_end: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp. Requires `expiration_start` if set + submission_start: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp. Requires `submission_end` if set + submission_end: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp. Requires `submission_start` if set + funded: + type: boolean + description: Default value false if not set + status: + type: string + description: Filter by status + timestamp: + $ref: '#/components/schemas/TimestampType' + description: Only return orders after this timestamp + limit_orders: + type: integer + description: The maximum number of orders to return + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + basic: + summary: Basic Request + description: Basic request to get clearing orders + value: + request: "/v1/clearing/list" + nonce: + withFilters: + summary: With Filters + description: Request with multiple filters + value: + request: "/v1/clearing/list" + nonce: + symbol: "BTCEUR" + counterparty: "KQ4P3XWE" + side: "buy" + expiration_start: 1642222800000 + expiration_end: 1642309200000 + submission_start: 1641790800000 + submission_end: 1641791100000 + funded: false + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + result: + type: string + description: Status of the operation + orders: + type: array + items: + type: object + properties: + clearing_id: + type: string + description: A unique identifier for the clearing order + order_id: + type: string + description: Only provided if order was submitted with it + counterparty_id: + type: string + description: A symbol that corresponds with a counterparty + counterparty_alias: + type: string + description: Counterparty alias + broker_id: + type: string + description: A symbol that corresponds with a broker id + symbol: + type: string + description: Trading pair + side: + type: string + enum: [buy, sell] + description: '"buy" or "sell"' + price: + type: number + format: decimal + description: The price the clearing order was executed at + quantity: + type: number + format: decimal + description: The amount that was executed + status: + type: string + description: A description of the status of the order + submission: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp + expiration: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp + examples: + successfulList: + summary: Successful List + description: JSON response with clearing orders + value: + result: "success" + orders: [ + { + clearing_id: "9LVQE9X5", + counterparty_id: "YZ43LX81", + symbol: "BTCEUR", + side: "sell", + price: 2, + quantity: 10, + status: "AwaitTargetConfirm", + submission: 1641790800020, + expiration: 1641963600000 + }, + { + clearing_id: "2MYR07XP", + order_id: "trade1SrcOrderId", + counterparty_id: "KQ4P3XWE", + broker_id: "WV4V1DGN", + symbol: "BTCEUR", + side: "buy", + price: 1, + quantity: 50, + status: "AwaitSourceConfirm", + submission: 1641790800016, + expiration: 1642222800000 + }, + { + clearing_id: "EM8WO7LQ", + order_id: "trade4SrcOrderId", + broker_id: "WV4V1DGN", + symbol: "BTCEUR", + side: "buy", + price: 4, + quantity: 8, + status: "AwaitSourceTargetConfirm", + submission: 1641790800028, + expiration: 1642136400000 + } + ] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/clearing/broker/list: + post: + x-zudoku-playground-enabled: false + tags: + - Clearing + summary: List Clearing Brokers + operationId: listClearingBrokers + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `clearing:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/clearing/broker/list" + nonce: + $ref: '#/components/schemas/Nonce' + symbol: + type: string + description: Trading pair + expiration_start: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp. Requires `expiration_end` if set + expiration_end: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp. Requires `expiration_start` if set + submission_start: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp. Requires `submission_end` if set + submission_end: + $ref: '#/components/schemas/TimestampType' + description: UTC timestamp. Requires `submission_start` if set + funded: + type: boolean + description: Default value false if not set + status: + type: string + description: Filter by status + timestamp: + $ref: '#/components/schemas/TimestampType' + description: Only return orders after this timestamp + limit_orders: + type: integer + description: The maximum number of orders to return + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + examples: + basic: + summary: Basic Request + description: Basic request to get broker clearing orders + value: + request: "/v1/clearing/broker/list" + nonce: + withFilters: + summary: With Filters + description: Request with multiple filters + value: + request: "/v1/clearing/broker/list" + nonce: + symbol: "BTCEUR" + expiration_start: 1642222800000 + expiration_end: 1642309200000 + submission_start: 1641790800000 + submission_end: 1641791100000 + funded: false + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + result: + type: string + description: Status of the operation + orders: + type: array + items: + type: object + properties: + clearing_id: + type: string + description: A unique identifier for the clearing order + source_counterparty_id: + type: string + description: Source counterparty id + source_order_id: + type: string + description: Only provided if order was submitted with it + target_counterparty_id: + type: string + description: Only provided if target counterparty was already set + target_order_id: + type: string + description: Only provided if target counterparty set this field + symbol: + type: string + description: Trading pair + source_side: + type: string + enum: [buy, sell] + description: '"buy" or "sell"' + price: + type: number + format: decimal + description: The price the clearing order was executed at + quantity: + type: number + format: decimal + description: The amount that was executed + status: + type: string + description: A description of the status of the order + submission: + type: integer + description: UTC timestamp + expiration: + type: integer + description: UTC timestamp + examples: + successfulList: + summary: Successful List + description: JSON response with broker clearing orders + value: + result: "success" + orders: [ + { + clearing_id: "9LVQ98X5", + source_counterparty_id: "R54L3DG1", + source_order_id: "trade1SrcOrderId", + target_counterparty_id: "KQ4P3XWE", + target_order_id: "trade1TgtOrderId", + symbol: "BTCEUR", + source_side: "buy", + price: 1, + quantity: 50, + status: "AwaitSourceConfirm", + submission: 1641790800016, + expiration: 1642222800000 + }, + { + clearing_id: "VXQ341X4", + source_counterparty_id: "R54L3DG1", + source_order_id: "trade4SrcOrderId", + symbol: "BTCEUR", + source_side: "buy", + price: 4, + quantity: 8, + status: "AwaitSourceTargetConfirm", + submission: 1641790800028, + expiration: 1642136400000 + } + ] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/clearing/broker/new: + post: + x-zudoku-playground-enabled: false + tags: + - Clearing + summary: Create New Broker Order + operationId: createNewBrokerOrder + description: | + Gemini Clearing also allows for brokers to facilitate trades between two Gemini customers. A broker can submit a new Gemini Clearing order that must then be confirmed by each counterparty before settlement. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - source_counterparty_id + - target_counterparty_id + - symbol + - amount + - expires_in_hrs + - price + - side + properties: + request: + type: string + description: The literal string "/v1/clearing/broker/new" + nonce: + $ref: '#/components/schemas/Nonce' + source_counterparty_id: + type: string + description: A symbol that corresponds with the counterparty sourcing the clearing trade + target_counterparty_id: + type: string + description: A symbol that corresponds with the counterparty where the clearing trade is targeted + symbol: + type: string + description: The [symbol](/market-data/symbols-and-minimums) of the order + amount: + type: string + format: decimal + description: Quoted decimal amount to purchase + expires_in_hrs: + type: integer + format: float + description: The number of hours before the trade expires. Your counterparty will need to confirm the order before this time expires. + price: + type: string + format: decimal + description: Quoted decimal amount to spend per unit + side: + type: string + enum: [buy, sell] + description: | + "buy" or "sell". This side will be assigned to the `source_counterparty_id`. The opposite side will be sent to the `target_counterparty_id` + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the broker account on which to place the order. Only available for exchange accounts. + examples: + brokerOrderInitiation: + summary: Broker order initiation sample + description: Sample payload for broker order initiation + value: + request: "/v1/clearing/broker/new" + nonce: + source_counterparty_id: R485E04Q + target_counterparty_id: Z4929ZDY + symbol: ethusd + amount: "175.00" + expires_in_hrs: 1.0 + price: "200" + side: sell + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + result: + type: string + description: Will return `AwaitSourceTargetConfirm`, meaning the order is waiting for both the source and the target parties to confirm the order + clearing_id: + type: string + description: A unique identifier for the clearing order. + example: { + result: AwaitSourceTargetConfirm, + clearing_id: "8EM7NVXD" + } + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/clearing/trades: + post: + x-zudoku-playground-enabled: false + tags: + - Clearing + summary: List Clearing Trades + operationId: listClearingTrades + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `clearing:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/clearing/trades" + nonce: + $ref: '#/components/schemas/Nonce' + timestamp_nanos: + type: integer + description: Only return transfers on or after this timestamp in nanos + limit_per_account: + type: integer + description: The maximum number of clearing trades to return. The default is 100 and the maximum is 300. + account: + type: string + description: Only required when using a master api-key. The name of the account within the subaccount group. + symbol: + type: string + description: The trading pair + timestamp: + $ref: '#/components/schemas/TimestampType' + description: Only return trades after this timestamp + limit_trades: + type: integer + description: The maximum number of trades to return + examples: + basic: + summary: Basic Request + description: Basic request to get clearing trades + value: + request: "/v1/clearing/trades" + nonce: + withTimestamp: + summary: With Timestamp + description: Request with timestamp filter + value: + request: "/v1/clearing/trades" + nonce: + timestamp_nanos: 1630382206000000000 + limit_per_account: 50 + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + results: + type: array + items: + type: object + properties: + sourceAccount: + type: string + description: A account that corresponds with the counterparty sourcing the clearing trade + targetAccount: + type: string + description: A account that corresponds with the counterparty where the clearing trade is targeted + pair: + type: string + description: The trading pair of the clearing trade + sourceSide: + type: string + enum: [buy, sell] + description: '"buy" or "sell"' + price: + type: string + description: The price the clearing order was executed at + quantity: + type: string + description: The amount that was executed + clearingId: + type: string + description: The clearing ID + status: + type: string + description: A description of the status of the order + expirationTimeMs: + $ref: '#/components/schemas/TimestampType' + description: The time that the clearing trade expires + createdMs: + $ref: '#/components/schemas/TimestampType' + description: The time that the clearing trade was created + lastUpdatedMs: + $ref: '#/components/schemas/TimestampType' + description: The last time the clearing trade was updated + hasBroker: + type: boolean + description: Broker trade + wasNotified: + type: boolean + description: Broker was notified + examples: + successfulTrades: + summary: Successful Trades + description: JSON response with clearing trades + value: + results: [ + { + sourceAccount: "primary", + targetAccount: "primary", + pair: "BTCUSD", + sourceSide: "buy", + price: "1", + quantity: "1000", + clearingId: "41M23L5Q", + status: "Settled", + expirationTimeMs: 1662567706120, + createdMs: 1662481306139, + lastUpdatedMs: 1662481561668, + hasBroker: false, + wasNotified: false + }, + { + sourceAccount: "primary", + targetAccount: "primary", + pair: "BTCUSD", + sourceSide: "buy", + price: "12", + quantity: "1000", + clearingId: "0EMOYLJ5", + status: "AwaitTargetConfirm", + expirationTimeMs: 1662567728123, + createdMs: 1662481328126, + lastUpdatedMs: 1662481561415, + hasBroker: false, + wasNotified: true + } + ] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + /v1/instant/quote: + post: + x-zudoku-playground-enabled: false + tags: + - Instant + summary: Get Instant Quote + operationId: getInstantQuote + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - side + - symbol + - nonce + - totalSpend + properties: + request: + type: string + description: The literal string "/v1/instant/quote/" + side: + type: string + enum: [buy, sell] + description: '"buy" or "sell"' + symbol: + type: string + description: The [symbol](/market-data/symbols-and-minimums) for the order. Instant includes order books denominated in a [supported currency](https://support.gemini.com/hc/en-us/articles/360000032663-Does-Gemini-support-fiat-currencies-other-than-USD), as `CCY2` + nonce: + $ref: '#/components/schemas/Nonce' + totalSpend: + type: string + description: Quoted decimal amount to spend on the order. Must comply with [stated minimums](/market-data/symbols-and-minimums). The `totalSpend` will be `CCY2` in `buy` orders and `CCY1` in `sell` orders. + paymentMethodUuid: + type: string + description: uuid provided as `bankId` in [Payment Methods API](/fund-management#list-payment-methods) + paymentMethodType: + type: string + description: Method used to specify payment method in `buy` order. Can be "AccountBalancePaymentType" to use funds available in USD balance held on Gemini, "BankAccountType" to initial an ACH from a linked bank account, or "CardAccountType" to use a linked debit card to fund the purchase. + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + examples: + buyQuote: + summary: Buy Quote Request + description: JSON payload for BTCUSD buy quote + value: + request: "/v1/instant/quote" + nonce: + symbol: "btcusd" + side: "buy" + totalSpend: "100" + sellQuote: + summary: Sell Quote Request + description: JSON payload for ETHUSD sell quote + value: + request: "/v1/instant/quote" + nonce: + symbol: "ethusd" + side: "sell" + totalSpend: "1" + responses: + '200': + description: Sample Responses + content: + application/json: + schema: + $ref: '#/components/schemas/InstantQuote' + examples: + btcBuyResponse: + summary: BTC Buy Quote Response + description: Sample BTCUSD Buy Response + value: + quoteId: 1328 + maxAgeMs: 60000 + pair: BTCUSD + price: "6445.07" + priceCurrency: USD + side: buy + quantity: "0.01505181" + quantityCurrency: BTC + fee: "2.9900309233" + feeCurrency: USD + depositFee: "0" + depositFeeCurrency: USD + totalSpend: "100" + totalSpendCurrency: USD + ethSellResponse: + summary: ETH Sell Quote Response + description: Sample ETHUSD Sell Response + value: + quoteId: 20930 + maxAgeMs: 60000 + pair: ETHUSD + price: "225.42" + priceCurrency: USD + side: sell + quantity: "1" + quantityCurrency: ETH + fee: "2.99" + feeCurrency: USD + depositFee: "0" + depositFeeCurrency: USD + totalSpend: "1" + totalSpendCurrency: ETH + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/instant/execute: + post: + x-zudoku-playground-enabled: false + tags: + - Instant + summary: Execute Instant Order + operationId: executeInstantOrder + description: | + ### Roles + The API key you use to access this endpoint must have the Trader role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `orders:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - quoteId + - symbol + - side + - quantity + - price + - fee + properties: + request: + type: string + description: The literal string "/v1/instant/execute" + nonce: + $ref: '#/components/schemas/Nonce' + symbol: + type: string + description: The symbol for the order. + side: + type: string + enum: [buy, sell] + description: '"buy" or "sell"' + quantity: + type: string + description: The quantity of the asset bought or sold. quantity must match quantity returned in the quote + price: + type: string + description: The price from the quote. price must match price returned in the quote + fee: + type: string + description: The fee for the order. fee must match fee returned in the quote + quoteId: + type: integer + description: Unique ID for the quote. quoteId must match quoteId returned in the quote + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts. + examples: + executeBuyOrder: + summary: Execute Buy Instant Order + description: Sample Instant Order Execution Request Payload BTCUSD Buy + value: + request: "/v1/instant/execute" + nonce: + symbol: BTCUSD + side: buy + quantity: "0.01505181" + price: "6445.07" + fee: "2.9900309233" + quoteId: 1328 + executeSellOrder: + summary: Execute Sell Instant Order + description: Sample Instant Order Execution Request Payload ETHUSD Sell + value: + request: "/v1/instant/execute" + nonce: + symbol: ETHUSD + side: sell + quantity: "1" + price: "225.42" + fee: "2.99" + quoteId: 20930 + + responses: + '200': + description: JSON response + content: + application/json: + schema: + type: object + properties: + orderId: + type: integer + description: The ID for the executed order + pair: + type: string + description: The symbol for the order. + price: + type: string + description: The price at which the order was executed + priceCurrency: + type: string + description: The currency in which the order is priced. Matches `CCY2` in the symbol + side: + type: string + description: Either "buy" or "sell" + quantity: + type: string + description: The quantity of the asset bought or sold + quantityCurrency: + type: string + description: The currency label for the `quantity` field. + totalSpend: + type: string + description: Total quantity to spend for the order. Will be the sum inclusive of all fees and amount to be traded. + totalSpendCurrency: + type: string + description: Currency of the `totalSpend` to be spent on the order + fee: + type: string + description: The fee quantity charged for the order + feeCurrency: + type: string + description: The currency label for the fee. + depositFee: + type: string + description: The deposit fee quantity. Will be applied if a debit card is used for the order. Will return 0 if there is no `depositFee` + depositFeeCurrency: + type: string + description: Currency in which `depositFee` is taken + examples: + btcusdBuy: + summary: Sample BTCUSD Buy + description: Sample Response BTCUSD Buy + value: + orderId: 375089415 + pair: BTCUSD + price: "6445.07" + priceCurrency: USD + side: buy + quantity: "0.01505181" + quantityCurrency: BTC + totalSpend: "100" + totalSpendCurrency: USD + fee: "2.9900309233" + feeCurrency: USD + depositFee: "0" + depositFeeCurrency: USD + ethusdSell: + summary: Sample ETHUSD Sell + description: Sample Response ETHUSD Sell + value: + orderId: 377326322 + pair: ETHUSD + price: "225.42" + priceCurrency: USD + side: sell + quantity: "1" + quantityCurrency: ETH + totalSpend: "0.1" + totalSpendCurrency: ETH + fee: "2.99" + feeCurrency: USD + depositFee: "0" + depositFeeCurrency: USD + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/payments/addbank: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Add Bank + operationId: addBank + description: | + The add bank API allows for banking information to be sent in via API. However, for the bank to be verified, you must still send in a wire for any amount from the bank account. + + ### Roles + This API requires the FundManager role. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `banks:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - accountnumber + - routing + - type + - name + properties: + request: + type: string + description: The literal string "/v1/payments/addbank" + nonce: + $ref: '#/components/schemas/Nonce' + accountnumber: + type: string + description: Account number of bank account to be added + routing: + type: string + description: Routing number of bank account to be added + type: + type: string + enum: [checking, savings] + description: Type of bank account to be added. Accepts `checking` or `savings` + name: + type: string + description: The name of the bank account as shown on your account statements + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Master API keys can get all account names using the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). + example: + request: "/v1/payments/addbank" + nonce: + accountnumber: "account-number-string" + routing: "routing-number-string" + type: "checking" + name: "Satoshi Nakamoto Checking" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/AddBankResponse' + example: + referenceId: "BankAccountRefId(18428)" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/payments/addbank/cad: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Add Bank CAD + operationId: addBankCAD + description: | + The add bank API allows for CAD banking information to be sent in via API. However, for the bank to be verified, you must still send in a wire for any amount from the bank account. + + ### Roles + This API requires the FundManager role. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `banks:create` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - swiftcode + - accountNumber + - type + - name + properties: + request: + type: string + description: The literal string "/v1/payments/addbank/cad" + nonce: + $ref: '#/components/schemas/Nonce' + swiftcode: + type: string + description: The account SWIFT code + accountNumber: + type: string + description: Account number of bank account to be added + institutionNumber: + type: string + description: The institution number of the account - optional but recommended. + branchnnumber: + type: string + description: The branch number - optional but recommended. + type: + type: string + enum: [checking, savings] + description: Type of bank account to be added. Accepts `checking` or `savings` + name: + type: string + description: The name of the bank account as shown on your account statements + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Master API keys can get all account names using the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). + example: + request: "/v1/payments/addbank/cad" + nonce: + swiftcode: "swift-code-string" + accountnumber: "account-number-string" + institutionnumber: "institution-number-string" + branchnumber: "branch-number-string" + type: "checking" + name: "Satoshi Nakamoto Checking" + account: "account-string" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + result: + type: string + description: Status of the request. "OK" indicates the account has been created successfully. + example: + result: "OK" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/payments/methods: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: List Payment Methods + operationId: listPaymentMethods + description: | + The payments methods API will return data on balances in the account and linked banks. + + ### Roles + The API key you use to access this endpoint can be either a Master or Account level key with any role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `banks:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/payments/methods" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Master API keys can get all account names using the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). + example: + request: "/v1/payments/methods" + account: "primary" + nonce: + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/PaymentMethodsResponse' + example: + balances: + - type: "exchange" + currency: "USD" + amount: "50893484.26" + available: "50889972.01" + availableForWithdrawal: "50889972.01" + banks: + - bank: "Jpmorgan Chase Bank Checking - 1111" + bankId: "97631a24-ca40-4277-b3d5-38c37673d029" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/account: + post: + x-zudoku-playground-enabled: false + tags: + - Account Administration + summary: Get Account Detail + operationId: getAccountDetail + description: | + The account API will return detail about the specific account requested such as users, country codes, etc. + + ### Roles + The API key you use to access this endpoint can be either a Master or Account level key with any role assigned. See [Roles](/roles#roles) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/account" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Master API keys can get all account names using the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). + example: + request: "/v1/account" + account: "primary" + nonce: + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + account: + type: object + description: Contains information on the requested account + properties: + accountName: + type: string + description: The name of the account provided upon creation. Will default to `Primary` + shortName: + type: string + description: Nickname of the specific account (will take the name given, remove all symbols, replace all " " with "-" and make letters lowercase) + type: + type: string + description: The type of account. Will return either `exchange` or `custody` + created: + $ref: '#/components/schemas/TimestampType' + description: The timestamp of account creation, displayed as number of milliseconds since 1970-01-01 UTC. This will be transmitted as a JSON number + users: + type: array + description: Contains an array of JSON objects with user information for the requested account + items: + type: object + properties: + name: + type: string + description: Full legal name of the user + lastSignIn: + type: string + description: Timestamp of the last sign for the user. Formatted as yyyy-MM-dd'T'HH:mm:ss.SSS'Z' + status: + type: string + description: Returns user status. Will inform of `active` users or otherwise not active + countryCode: + type: string + description: 2 Letter country code indicating residence of user + isVerified: + type: boolean + description: Returns verification status of user + memo_reference_code: + type: string + description: Returns wire memo reference code for linked bank account + virtual_account_number: + type: string + description: Virtual account number for the account. Only populated if applicable for the account + example: + account: + accountName: "Primary" + shortName: "primary" + type: "exchange" + created: "1498245007981" + users: + - name: "Satoshi Nakamoto" + lastSignIn: "2020-07-21T13:37:39.453Z" + status: "Active" + countryCode: "US" + isVerified: true + - name: "Gemini Support" + lastSignIn: "2018-07-11T20:04:36.073Z" + status: "Suspended" + countryCode: "US" + isVerified: false + memo_reference_code: "GEMPJBRDZ" + virtual_account_number: "123456" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/approvedAddresses/account/{network}: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: List Approved Addresses + operationId: listApprovedAddresses + description: | + Allows viewing of Approved Address list. + + ### Roles + This API can accept any role. See [Roles](/roles#roles) for more information. + + parameters: + - $ref: '#/components/parameters/networkParam' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/approvedAddresses/account/:network" where `:network` can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to view the approved address list. + example: + request: "/v1/approvedAddresses/account/ethereum" + nonce: + account: "primary" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApprovedAddressesResponse' + example: + approvedAddresses: [ + { + network: "ethereum", + scope: "account", + label: "api_added_ETH_address", + status: "pending-time", + createdAt: "1602692572349", + address: "0x0000000000000000000000000000000000000000" + }, + { + network: "ethereum", + scope: "group", + label: "api_added_ETH_address", + status: "pending-time", + createdAt: "1602692542296", + address: "0x0000000000000000000000000000000000000000" + }, + { + network: "ethereum", + scope: "group", + label: "hardware_wallet", + status: "active", + createdAt: "1602087433270", + address: "0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28" + }, + { + network: "ethereum", + scope: "account", + label: "hardware_wallet", + status: "active", + createdAt: "1602086832986", + address: "0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28" + } + ] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/approvedAddresses/{network}/request: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Create New Approved Address + operationId: createNewApprovedAddress + description: | + Allows for creation of an approved withdrawal address. Once the request is made, the 7 day waiting period will begin. Please note that all approved address requests are subject to the 7 day waiting period. + + If you add an address using an account-scoped API key, then the address will be added to your account specific approved address list. If you use a master-scoped API key, the address will be added to your group-level approved address list unless you specify an account. + + This endpoint is subject to additional security constraints and is only accessible via API keys which have configured Trusted IP controls. + + Please reach out to trading@gemini.com if you have any questions about approved addresses. + + ### Roles + This API requires the FundManager role. See [Roles](/roles#roles) for more information. + + parameters: + - $ref: '#/components/parameters/networkParam' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - address + - label + properties: + request: + type: string + description: The literal string "/v1/approvedAddresses/:network/request" where `:network` can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` + nonce: + $ref: '#/components/schemas/Nonce' + address: + type: string + description: A string of the address to be added to the approved address list. + label: + type: string + description: The label of the approved address. + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to add the approved address. + memo: + type: string + description: it would be present if applicable, it will be present for cosmos address. + example: + request: "/v1/approvedAddresses/ethereum/request" + nonce: + address: "0x0000000000000000000000000000000000000000" + label: api_added_ETH_address + account: primary + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApprovedAddressMessage' + example: + message: "Approved address addition is now waiting a 7-day approval hold before activation." + + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/approvedAddresses/{network}/remove: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Remove Approved Address + operationId: removeApprovedAddress + description: | + Allows for removal of active or time-pending addresses from the Approved Address list. Addresses that are pending approval from another user on the account cannot be removed via API. + + ### Roles + This API requires the FundManager role. See [Roles](/roles#roles) for more information. + + parameters: + - $ref: '#/components/parameters/networkParam' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - address + properties: + request: + type: string + description: The literal string "/v1/approvedAddresses/:network/remove" where `:network` can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` + nonce: + $ref: '#/components/schemas/Nonce' + address: + type: string + description: A string of the address to be removed from the approved address list. + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to remove the approved address. + example: + request: "/v1/approvedAddresses/ethereum/remove" + nonce: + address: "0x0000000000000000000000000000000000000000" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApprovedAddressMessage' + example: + message: "0x0000000000000000000000000000000000000000 removed from group pending-time approved addresses." + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/account/create: + post: + x-zudoku-playground-enabled: false + tags: + - Account Administration + summary: Create New Account + operationId: createNewAccount + description: | + A Master API key can create a new exchange account within the group. This API will return the name of your new account for use with the account parameter in when using Master API keys to perform account level functions. Please see the [example](/account-admin-endpoints#using-master-api-keys). + + ### Roles + The API key you use to access this endpoint must be a Master level key and have the Administrator role assigned. See [Roles](/roles#roles) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - name + properties: + request: + type: string + description: The literal string "/v1/account/create" + nonce: + $ref: '#/components/schemas/Nonce' + name: + type: string + description: A unique name for the new account + type: + type: string + description: Either `exchange` or `custody` is accepted. Will generate an exchange account if `exchange` or parameter is missing. Will generate a custody account if `custody`. + examples: + createAccount: + summary: Create Account Example + description: JSON payload to create a new account + value: + request: "/v1/account/create" + nonce: + name: "My Secondary Account" + type: "exchange" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + account: + type: string + description: Account reference string for use in APIs based off the provided `name` field + type: + type: string + description: Will return the type of account generated. `exchange` if an exchange account was created, `custody` if a custody account was created + example: + account: "my-secondary-account" + type: "exchange" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/account/rename: + post: + x-zudoku-playground-enabled: false + tags: + - Account Administration + summary: Rename Account + operationId: renameAccount + description: | + A Master or Account level API key can rename an account within the group. + + ### Roles + The API key you use to access this endpoint can be either a Master or Account level API key and must have the Administrator role assigned. See [Roles](/roles#roles) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/account/rename". + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: Only required when using a master api-key. The shortname of the account within the subaccount group. Master API keys can get all account shortnames from the `account` field returned by the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group). + newName: + type: string + description: A unique name for the new account. If not provided, name will not change. + newAccount: + type: string + description: A unique shortname for the new account. If not provided, shortname will not change. + examples: + renameAccount: + summary: Rename Account Example + description: JSON payload to rename an account + value: + request: "/v1/account/rename" + nonce: + account: "my-exchange-account" + newName: "My Exchange Account New Name" + newAccount: "my-exchange-account-new-name" + responses: + '200': + description: An element containing the updated name of the account. + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: New name for the account based off the provided `newName` field. Only returned if `newName` was provided in the request. + account: + type: string + description: New shortname for the account based off the provided `newAccount` field. Only returned if `newAccount` was provided in the request. + example: + name: "My Exchange Account New Name" + account: "my-exchange-account-new-name" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/account/list: + post: + x-zudoku-playground-enabled: false + tags: + - Account Administration + summary: List Accounts in Group + operationId: listAccountsInGroup + description: | + A Master API key can be used to get the accounts within the group. A maximum of 500 accounts can be listed in a single API call. + + ### Roles + The API key you use to access this endpoint must be a Master level key. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `account:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/account/list" + nonce: + $ref: '#/components/schemas/Nonce' + limit_accounts: + type: integer + description: The maximum number of accounts to return. Maximum and default values are both 500. + timestamp: + $ref: '#/components/schemas/TimestampType' + description: Only return accounts created on or before the supplied timestamp. If not provided, the 500 most recently created accounts are returned. + example: + request: "/v1/account/list" + nonce: + limit_accounts: 100 + timestamp: 1632485834721 + responses: + '200': + description: The response will be a JSON object containing all accounts within the master group + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + description: The name of the account provided upon creation + account: + type: string + description: Nickname of the specific account (will take the name given, remove all symbols, replace all " " with "-" and make letters lowercase) + type: + type: string + description: Either "exchange" or "custody" depending on type of account + counterparty_id: + type: string + description: The Gemini clearing counterparty ID associated with the API key making the request. Will return `None` for custody accounts + created: + $ref: '#/components/schemas/TimestampType' + description: The timestamp of account creation, displayed as number of milliseconds since 1970-01-01 UTC. This will be transmitted as a JSON number + status: + type: string + description: Either "open" or "closed" + example: + - name: "Primary" + account: "primary" + type: "exchange" + counterparty_id: "EMONNYXH" + created: 1495127793000 + status: "open" + - name: "My Custody Account" + account: "my-custody-account" + type: "custody" + counterparty_id: null + created: 1565970772000 + status: "open" + - name: "Other exchange account!" + account: "other-exchange-account" + type: "exchange" + counterparty_id: "EMONNYXK" + created: 1565970772000 + status: "closed" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/account/transfer/{currency}: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Transfer Between Accounts + operationId: transferBetweenAccounts + description: | + This API allows you to execute an internal transfer between any two accounts within your Master Group. In the scenario of exchange account to exchange account there will be no activity on a blockchain network. All other combinations will result in a movement of funds on a blockchain network. + + Gemini Custody account withdrawals will not occur until the daily custody run occurs. In the case of funds moving from a Gemini Custody account to a Gemini Exchange account, the exchange account will get a precredit for the amount to be received. The exchange account will be able to trade these funds but will be unable to withdraw until the funds are processed on the blockchain and received. + + Gemini Custody accounts request withdrawals to approved addresses in all cases and require the request to come from an approved IP address. Please reach out to trading@gemini.com to enable API withdrawals for custody accounts. + + Gemini Custody accounts do not support fiat currency transfers. + + Fiat transfers between non-derivative and derivatives accounts are prohibited. + + ### Roles + The API key you use to access this endpoint must be a Master level key and have the Fund Manager role assigned. See [Roles](/roles#roles) for more information. + + parameters: + - $ref: '#/components/parameters/currencyParam' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - sourceAccount + - targetAccount + - amount + properties: + request: + type: string + description: The string `/v1/account/transfer/:currency` where `:currency` is replaced with either `usd` or a supported crypto-currency, e.g. `gusd`, `btc`, `eth`, `aave`, etc. See [Symbols and minimums](/market-data/symbols-and-minimums). + nonce: + $ref: '#/components/schemas/Nonce' + sourceAccount: + type: string + description: Nickname of the account you are transferring from. Use the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group) to get all account names in the group. + targetAccount: + type: string + description: Nickname of the account you are transferring to. Use the [Get Accounts endpoint](/rest/account-administration#list-accounts-in-group) to get all account names in the group. + amount: + type: string + description: Quoted decimal amount to withdraw + clientTransferId: + type: string + description: A unique identifier for the internal transfer, in uuid4 format + withdrawalId: + type: string + description: Unique ID of the requested withdrawal. + examples: + basicTransfer: + summary: Basic Transfer + description: JSON payload for a basic internal transfer + value: + request: "/v1/account/transfer/btc" + nonce: + sourceAccount: "primary" + targetAccount: "my-secondary-account" + amount: "1.0" + withClientId: + summary: With Client Transfer ID + description: Transfer with a client-supplied identifier + value: + request: "/v1/account/transfer/eth" + nonce: + sourceAccount: "primary" + targetAccount: "my-custody-account" + amount: "1" + clientTransferId: "AA97B177-9383-4934-8543-0F91A7A02838" + responses: + '200': + description: JSON response + content: + application/json: + schema: + type: object + properties: + fromAccount: + type: string + description: Source account where funds are sent from + toAccount: + type: string + description: Target account to receive funds in the internal transfer + amount: + type: string + description: Quantity of assets being transferred + fee: + type: string + description: Fee taken for the transfer. Exchange account to exchange account transfers will always be free and will not be deducted from the free monthly transfer amount for that account. + currency: + type: string + description: Display Name. Can be `Bitcoin`, `Ether`, `Zcash`, `Litecoin`, `Dollar`, etc. + withdrawalId: + type: string + description: _Excludes_ exchange to exchange. Unique ID of the requested withdrawal + uuid: + type: string + description: _Only_ for exchange to exchange. Unique ID of the completed transfer + message: + type: string + description: Message describing result of withdrawal. Will inform of success, failure, or pending blockchain transaction. + txHash: + type: string + description: _Only for Ethereum network transfers. Excludes exchange to exchange transfers_. Transaction hash for ethereum network transfer. + example: + "fromAccount": my-account + "toAccount": my-other-account + "amount": "1" + "currency": Bitcoin + "uuid": "9c153d64-83ba-4532-a159-ebe3f6797766" + "message": Success, transfer completed. + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/transactions: + post: + x-zudoku-playground-enabled: false + tags: + - Fund Management + summary: Get Transaction History + operationId: getTransactionHistory + description: | + + + This endpoint shows trade detail and transactions. There is a `continuation_token` that is a pagination token used for subsequent requests. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned and have the master account scope. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `history:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/transactions" + nonce: + $ref: '#/components/schemas/Nonce' + timestamp_nanos: + description: Only return transfers on or after this timestamp in nanos. If this is defined, do not define “continuation_token”. + allOf: + - $ref: '#/components/schemas/TimestampType' + limit: + type: integer + description: The maximum number of transfers to return. The default is 100 and the maximum is 300. + default: 100 + continuation_token: + type: string + description: For subsequent requests, use the returned `continuation_token` value for next page. If this is defined, do not define “timestamp_nanos”. + example: + request: "/v1/transactions" + nonce: + timestamp_nanos: 1630382206000000000 + limit: 50 + continuation_token: "daccgrp_123421:n712621873886999872349872349:a71289723498273492374978424:m2:iForward" + responses: + '200': + description: The response will be an array of JSON objects, sorted by trade and transfer as well as a continuationToken to be used in subsequent requests. + content: + application/json: + schema: + type: object + properties: + results: + type: array + description: Results will contain either a list of Trade or Transfer responses + items: + $ref: '#/components/schemas/Transaction' + examples: + tradeResponse: + summary: Trade Response + description: Trade Response + value: + results: + - account: primary + amount: "0.001" + clientOrderId: "" + price: "1730.95" + timestampms: 1659201465069 + side: SIDE_TYPE_BUY + isAggressor: true + feeAssetCode: ETH + feeAmount: "0.000000000000000605" + orderId: 73716687406755680 + exchange: gemini + isAuctionFill: false + isClearingFill: false + symbol: ETHUSD + tid: 144115199446910513 + type: trade + - account: primary + amount: "0.001" + clientOrderId: "" + price: "1679.02" + timestampms: 1659201465222 + side: SIDE_TYPE_SELL + isAggressor: true + feeAssetCode: ETH + feeAmount: "0.00000000000000000587" + orderId: 73716687406755680 + exchange: gemini + isAuctionFill: false + isClearingFill: false + symbol: ETHUSD + tid: 144115199446910494 + type: trade + continuationToken: "daccgrp_1500611:n7126218738869976937315434496:a7126216029949884380085223424:m2:iForward" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + + /v1/oauth/revokeByToken: + post: + x-zudoku-playground-enabled: false + tags: + - OAuth + summary: Revoke OAuth Token + operationId: revokeOAuthToken + description: | + The `access_token` may be revoked at any time by using `v1/oauth/revokeByToken`. Once a token is revoked or expires, it can no longer be used to make requests. + + This endpoint is only available using an `access_token` and will revoke the token used to make the request. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + properties: + request: + type: string + description: The literal string "/v1/oauth/revokeByToken" + examples: + revokeToken: + summary: Revoke Token Request + description: JSON payload to revoke an OAuth token + value: + request: "/v1/oauth/revokeByToken" + + responses: + '200': + description: An object that indicates the access_token has been revoked. + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: A message that indicates the token has been revoked for the account + examples: + revokeTokenResponse: + summary: Revoke Token Response + description: JSON response for a successful token revocation + value: + message: "OAuth tokens and codes have been revoked for 00000000-0000-0000-0000-000000000000 on your account." + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/balances/staking: + post: + x-zudoku-playground-enabled: false + tags: + - Staking + summary: List Staking Balances + operationId: listStakingBalances + description: | + This will show the available balance in Staking as well as the available balance for withdrawal. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/balances/staking" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group." + example: + request: "/v1/balances/staking" + nonce: + account: primary + responses: + '200': + description: The staking balances + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/StakingBalance' + example: + - type: "Staking" + currency: "MATIC" + balance: 10 + available: 0 + availableForWithdrawal: 10 + balanceByProvider: + "62b21e17-2534-4b9f-afcf-b7edb609dd8d": + balance: 10 + - type: "Staking" + currency: "ETH" + balance: 3 + available: 0 + availableForWithdrawal: 3 + balanceByProvider: + "62b21e17-2534-4b9f-afcf-b7edb609dd8d": + balance: 3 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/staking/stake: + post: + x-zudoku-playground-enabled: false + tags: + - Staking + summary: Stake Crypto Funds + operationId: stakeCryptoFunds + description: | + Initiates Staking deposits. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Trader role assigned. See [Roles](/roles#roles) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - providerId + - currency + - amount + properties: + request: + type: string + description: The literal string "v1/staking/stake" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group." + providerId: + type: string + description: Provider Id, in uuid4 format. providerId is accessible from the [Staking rates](#list-staking-rates) response + currency: + type: string + description: Currency code, see [symbols](/market-data/symbols-and-minimums) + amount: + type: string + format: decimal + description: The amount of currency to deposit + example: + request: "v1/staking/stake" + nonce: + providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: "MATIC" + amount: 30 + responses: + '200': + description: The staking deposit transaction + content: + application/json: + schema: + $ref: '#/components/schemas/StakingDeposit' + example: + transactionId: "65QN4XM5" + providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: "MATIC" + amount: 30 + rates: + rate: 540 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/staking/history: + post: + x-zudoku-playground-enabled: false + tags: + - Staking + summary: List Staking Event History + operationId: listStakingEventHistory + description: | + This will show all staking deposits, redemptions and interest accruals. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + + ### How to iterate through all transactions: + To retrieve your full Staking history walking backwards, + + 1. Initial request: `POST` to https://api.gemini.com/v1/staking/history with a JSON payload including `sortAsc` set to `false` and a limit key with value `500`. + 2. When you receive the list of Staking transactions, they will be sorted by `datetime` descending - so the last element in the list will have the lowest `timestamp` value. For this example, say that value is `X`. + 3. Create a second `POST` request with a JSON payload including a `until` timestamp key with value `X-1`, `sortAsc` set to `false`, and a limit key with value `500`. + 4. Take the last element of the list returned with lowest `datetime` value `Y` and create a third `POST` request with a JSON payload including a `until` timestamp key with value `Y-1`, `sortAsc` set to false, and a `limit` key with value `500`. + 5. Continue creating `POST` requests and retrieving Staking transactions until an empty list is returned. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/staking/history" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group." + since: + description: "In iso datetime with timezone format. Defaults to the timestamp of the first deposit into Staking." + allOf: + - $ref: '#/components/schemas/TimestampType' + until: + description: "In iso datetime with timezone format, default to current time as of server time" + allOf: + - $ref: '#/components/schemas/TimestampType' + limit: + type: integer + description: "The maximum number of transactions to return. Default is 50, max is 500." + default: 50 + providerId: + type: string + description: "Borrower Id, in uuid4 format. providerId is accessible from the [Staking rates](#list-staking-rates) response" + currency: + type: string + description: "Currency code, see [symbols](/market-data/symbols-and-minimums)" + interestOnly: + type: boolean + description: "Toggles whether to only return daily interest transactions. Defaults to false." + default: false + sortAsc: + type: boolean + description: "Toggles whether to sort the transactions in ascending order by datetime. Defaults to false." + default: false + example: + request: "/v1/staking/history" + nonce: + account: primary + since: "2022-11-01T00:00:00.000Z" + until: "2022-11-03T00:00:00.000Z" + limit: 50 + responses: + '200': + description: Staking transaction history + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/StakingHistory' + example: + - providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + transactions: + - transactionId: "MPZ7LDD8" + transactionType: "Redeem" + amountCurrency: "MATIC" + amount: 20 + dateTime: 1667418560153 + - transactionId: "65QN4XM5" + transactionType: "Deposit" + amountCurrency: "MATIC" + amount: 30 + dateTime: 1667418287795 + - transactionId: "YP22OK4P" + transactionType: "Deposit" + amountCurrency: "ETH" + amount: 3 + dateTime: 1667397368929 + - transactionId: "TQN9OPN" + transactionType: "Interest" + amountCurrency: "MATIC" + amount: 0.01 + priceCurrency: "USD" + priceAmount: 0.1 + dateTime: 1667418287795 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/staking/rates: + get: + tags: + - Staking + summary: List Staking Rates + operationId: listStakingRates + description: | + This will return the current Gemini Staking interest rates (in bps). When including the specific asset(s) in the request, the response will include the specific assets' (e.g. `eth`, `matic`) Staking rate. When not including the specific asset in the request, the response will include all Staking rates. + responses: + '200': + description: JSON response with staking rates + content: + application/json: + schema: + $ref: '#/components/schemas/StakingRateResponse' + example: + "62bb4d27-a9c8-4493-a737-d4fa33994f1f": + MATIC: + providerId: "62bb4d27-a9c8-4493-a737-d4fa33994f1f" + rate: 95.8909 + apyPct: 0.96 + ratePct: 0.958909 + depositUsdLimit: 500000 + ETH: + providerId: "62bb4d27-a9c8-4493-a737-d4fa33994f1f" + rate: 228.0197 + apyPct: 2.31 + ratePct: 2.280197 + depositUsdLimit: 500000 + SOL: + providerId: "62bb4d27-a9c8-4493-a737-d4fa33994f1f" + rate: 321.5282 + apyPct: 3.27 + ratePct: 3.215282 + depositUsdLimit: 500000 + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/staking/rewards: + post: + x-zudoku-playground-enabled: false + tags: + - Staking + summary: List Staking Rewards + operationId: listStakingRewards + description: | + This will show the historical Staking reward payments and accrual. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Auditor role assigned. See [Roles](/roles#roles) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - since + properties: + request: + type: string + description: The literal string "/v1/staking/rewards" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group." + since: + type: string + description: In iso datetime with timezone format + until: + type: string + description: "In iso datetime with timezone format, default to current time as of server time" + providerId: + type: string + description: "Borrower Id, in uuid4 format. providerId is accessible from the [Staking rates](#list-staking-rates) response" + currency: + type: string + description: "Currency code, see [symbols](/market-data/symbols-and-minimums)" + example: + request: "/v1/staking/rewards" + nonce: + since: "2022-08-20T00:00:00.000Z" + until: "2022-11-05T00:00:00.000Z" + providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: "ETH" + responses: + '200': + description: A nested JSON object, organized by provider, then currency + content: + application/json: + schema: + $ref: '#/components/schemas/StakingRewardsResponse' + example: + "62b21e17-2534-4b9f-afcf-b7edb609dd8d": + MATIC: + providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: "MATIC" + accrualTotal: 0.103994 + ratePeriods: + - providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: "MATIC" + apyPct: 5.75 + ratePct: 5.592369 + numberOfAccruals: 1 + accrualTotal: 0.0065678 + firstAccrualAt: "2022-08-23T20:00:00.000Z" + lastAccrualAt: "2022-08-23T20:00:00.000Z" + - providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: "MATIC" + apyPct: 5.2 + ratePct: 5.073801 + numberOfAccruals: 1 + accrualTotal: 0.0037971687995651837 + firstAccrualAt: "2022-10-28T20:00:00.000Z" + lastAccrualAt: "2022-10-28T20:00:00.000Z" + ETH: + providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: "ETH" + accrualTotal: 0.017999076209977 + ratePeriods: + - providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: "ETH" + apyPct: 0.66 + ratePct: 0.65913408 + numberOfAccruals: 1 + accrualTotal: 0.00014802170517505 + firstAccrualAt: "2022-11-02T20:00:00.000Z" + lastAccrualAt: "2022-11-02T20:00:00.000Z" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/staking/unstake: + post: + x-zudoku-playground-enabled: false + tags: + - Staking + summary: Unstake Crypto Funds + operationId: unstakeCryptoFunds + description: | + Initiates Staking withdrawals. + + ### Roles + The API key you use to access this endpoint must have the Trader, Fund Manager or Trader role assigned. See [Roles](/roles#roles) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - providerId + - currency + - amount + properties: + request: + type: string + description: The literal string "v1/staking/unstake" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group." + providerId: + type: string + description: Provider Id, in uuid4 format. providerId is accessible from the [Staking rates](#list-staking-rates) response + currency: + type: string + description: Currency code, see [symbols](/market-data/symbols-and-minimums) + amount: + type: string + format: decimal + description: The amount of currency to withdraw + example: + request: "v1/staking/unstake" + nonce: + providerId: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: "MATIC" + amount: 20 + responses: + '200': + description: The staking withdrawal transaction + content: + application/json: + schema: + $ref: '#/components/schemas/StakingWithdrawal' + example: + transactionId: "MPZ7LDD8" + amount: 20 + amountPaidSoFar: 20 + amountRemaining: 0 + currency: "MATIC" + requestInitiated: "2022-11-02T19:49:20.153Z" + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/roles: + post: + x-zudoku-playground-enabled: false + tags: + - Account Administration + summary: Roles Endpoint + operationId: getRoles + description: | + The `v1/roles` endpoint will return a string of the role of the current API key. The response fields will be different for account-level and master-level API keys. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/roles" + example: /v1/roles + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + example: + request: "/v1/roles" + nonce: + responses: + '200': + description: The response will be a JSON object indicating the assigned roles to the set of API keys used to call `/v1/roles`. The `Auditor` role cannot be combined with other roles. `Fund Manager` and `Trader` can be combined. + content: + application/json: + schema: + $ref: '#/components/schemas/RoleResponse' + examples: + accountLevel: + summary: Account-scoped key + description: Successful response for account-scoped key + value: + isAuditor: false + isFundManager: true + isTrader: true + masterLevel: + summary: Master-scoped key + description: Successful response for master-scoped key + value: + counterparty_id: "EMONNYXJ" + isAuditor: false + isFundManager: true + isTrader: true + isAccountAdmin: true + + /v1/margin: + post: + x-zudoku-playground-enabled: false + tags: + - Derivatives + summary: Get Account Margin + operationId: getAccountMargin + description: | + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See Roles for more information. + + The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + - symbol + properties: + request: + type: string + description: The API endpoint path + example: /v1/margin + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + example: primary + symbol: + type: string + description: Trading pair symbol. See [symbols and minimums](/market-data/symbols-and-minimums) + example: + request: "/v1/margin" + nonce: + symbol: "BTC-GUSD-PERP" + responses: + '200': + description: JSON object + content: + application/json: + schema: + $ref: '#/components/schemas/MarginResponse' + example: + margin_assets_value: "9800" + initial_margin: "6000" + available_margin: "3800" + margin_maintenance_limit: "5800" + leverage: "12.34567" + notional_value: "1300" + estimated_liquidation_price: "1300" + initial_margin_positions: "3500" + reserved_margin: "2500" + reserved_margin_buys: "1800" + reserved_margin_sells: "700" + buying_power: "0.19" + selling_power: "0.19" + + /v1/perpetuals/fundingPayment: + post: + x-zudoku-playground-enabled: false + tags: + - Derivatives + summary: List Funding Payments + operationId: listFundingPayments + description: | + + + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See Roles for more information. + + The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - name: since + in: query + description: If specified, only return funding payments after this point. Default value is 24h in past. See [**Timestamps**](/rest/~schemas#timestamp-type) for more information + required: false + schema: + $ref: '#/components/schemas/TimestampType' + - name: to + in: query + description: If specified, only returns funding payment until this point. Default value is now. See [**Timestamps**](/rest/~schemas#timestamp-type) for more information + required: false + schema: + $ref: '#/components/schemas/TimestampType' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The API endpoint path + example: /v1/perpetuals/fundingPayment + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + example: primary + example: + request: "/v1/perpetuals/fundingPayment" + nonce: + responses: + '200': + description: The response will be an array of funding payment objects. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/FundingPayment' + example: + - eventType: "Hourly Funding Transfer" + hourlyFundingTransfer: + eventType: "Hourly Funding Transfer" + timestamp: 1683730803940 + assetCode: "GUSD" + action: "Debit" + quantity: + currency: "GUSD" + value: "4.78958" + - eventType: "Hourly Funding Transfer" + hourlyFundingTransfer: + eventType: "Hourly Funding Transfer" + timestamp: 1683734406746 + assetCode: "GUSD" + action: "Debit" + quantity: + currency: "GUSD" + value: "4.78958" + instrumentSymbol: "BTCGUSDPERP" + + /v1/perpetuals/fundingpaymentreport/records.xlsx: + get: + x-zudoku-playground-enabled: false + tags: + - Derivatives + summary: Get Funding Payment Report File + operationId: getFundingPaymentReportFile + description: | + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See Roles for more information. + + The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + + ### Examples + - `&fromDate=2024-04-10&toDate=2024-04-25&numRows=1000`
+ Compare and obtain the minimum records between (2024-04-10 to 2024-04-25) and 1000. If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch the minimum between 360 and 1000 records only. + + - `&numRows=2024-04-10&toDate=2024-04-25`
+ If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch 360 records only. + + - `&numRows=1000`
+ Fetch maximum 1000 records starting from Now to a historical date + + - ``
+ Fetch maximum 8760 records starting from Now to a historical date + parameters: + - name: fromDate + in: query + description: If empty, will only fetch records by numRows value. + required: false + schema: + type: string + format: date + - name: toDate + in: query + description: If empty, will only fetch records by numRows value. + required: false + schema: + type: string + format: date + - name: numRows + in: query + description: If empty, default value '8760' + required: false + schema: + type: integer + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: false + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The API endpoint path + example: /v1/perpetuals/fundingpaymentreport/records.xlsx + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + example: primary + example: + request: "/v1/perpetuals/fundingpaymentreport/records.xlsx?fromDate=2024-04-10&toDate=2024-04-25&numRows=1000" + nonce: + responses: + '200': + description: XLSX file downloaded containing funding payment report. + headers: + Content-Disposition: + schema: + type: string + example: attachment; filename=FundingPayment_Report.xlsx + content: + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: string + format: binary + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/perpetuals/fundingpaymentreport/records.json: + post: + x-zudoku-playground-enabled: false + tags: + - Derivatives + summary: Get Funding Payment Report JSON + operationId: getFundingPaymentReportJson + description: | + This endpoint retrieves funding payment report in JSON format. + + ### Examples + - `&fromDate=2024-04-10&toDate=2024-04-25&numRows=1000`
+ Compare and obtain the minimum records between (2024-04-10 to 2024-04-25) and 1000. If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch the minimum between 360 and 1000 records only. + + - `&numRows=2024-04-10&toDate=2024-04-25`
+ If (2024-04-10 to 2024-04-25) contains 360 records. Then fetch 360 records only. + + - `&numRows=1000`
+ Fetch maximum 1000 records starting from Now to a historical date + + - ``
+ Fetch maximum 8760 records starting from Now to a historical date + parameters: + - name: fromDate + in: query + description: If empty, will only fetch records by numRows value. + required: false + schema: + type: string + format: date + - name: toDate + in: query + description: If empty, will only fetch records by numRows value. + required: false + schema: + type: string + format: date + - name: numRows + in: query + description: If empty, default value '8760' + required: false + schema: + type: integer + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The API endpoint path + example: /v1/perpetuals/fundingpaymentreport/records.json?fromDate=2024-04-10&toDate=2024-04-25&numRows=1000 + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + example: primary + example: + request: "/v1/perpetuals/fundingpaymentreport/records.json?fromDate=2024-04-10&toDate=2024-04-25&numRows=1000" + nonce: + responses: + '200': + description: JSON response containing funding payment report. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/FundingPaymentReportItem' + example: + - eventType: "Hourly Funding Transfer" + timestamp: 1713344403617 + assetCode: "GUSD" + action: "Credit" + quantity: + currency: "GUSD" + value: "35.81084" + instrumentSymbol: "BTCGUSDPERP" + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/positions: + post: + x-zudoku-playground-enabled: false + tags: + - Derivatives + summary: Get Open Positions + operationId: getOpenPositions + description: | + ### Roles + The API key you use to access this endpoint must have the Trader or Auditor role assigned. See [Roles](/roles#roles) for more information. + + The OAuth scope must have `orders:read` assigned to access this endpoint. See [OAuth Scopes](/authentication/oauth#oauth-scopes) for more information. + parameters: + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/positions" + nonce: + $ref: '#/components/schemas/Nonce' + account: + type: string + description: Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which the orders were placed. Only available for exchange accounts. + example: + request: "/v1/positions" + nonce: + account: "primary" + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + openPositions: + type: array + items: + $ref: '#/components/schemas/OpenPosition' + example: [ + { + symbol: btcgusdperp, + instrument_type: perp, + quantity: "0.2", + notional_value: "4000.036", + realised_pnl: "1234.5678", + unrealised_pnl: "999.946", + average_cost: "15000.45", + mark_price: "20000.18" + } + ] + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v1/riskstats/{symbol}: + get: + tags: + - Derivatives + summary: Get Risk Stats + operationId: getRiskStats + parameters: + - name: symbol + in: path + required: true + schema: + type: string + description: | + Perps Trading pair symbol

+ + `BTCGUSDPERP`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + responses: + '200': + description: The response will be an json object + content: + application/json: + schema: + $ref: '#/components/schemas/RiskStatsResponse' + example: { + product_type: PerpetualSwapContract, + mark_price: "30080.00", + index_price: "30079.046", + open_interest: "14.439", + open_interest_notional: "434325.12" + } + + /v2/ticker/{symbol}: + get: + tags: + - Market Data + summary: Get Ticker V2 + operationId: getTickerV2 + description: This endpoint retrieves information about recent trading activity for the provided symbol. + parameters: + - name: symbol + in: path + required: true + schema: + type: string + description: Trading pair symbol + example: BTCUSD + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/TickerInfo' + example: + symbol: "BTCUSD" + open: "9121.76" + high: "9440.66" + low: "9106.51" + close: "9347.66" + changes: [ + "9365.1", + "9386.16", + "9373.41", + "9322.56", + "9268.89", + "9265.38", + "9245", + "9231.43", + "9235.88", + "9265.8", + "9295.18", + "9295.47", + "9310.82", + "9335.38", + "9344.03", + "9261.09", + "9265.18", + "9282.65", + "9260.01", + "9225", + "9159.5", + "9150.81", + "9118.6", + "9148.01" + ] + bid: "9345.70" + ask: "9347.67" + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v2/candles/{symbol}/{time_frame}: + get: + tags: + - Market Data + summary: List Candles + operationId: listCandles + description: This endpoint retrieves time-intervaled data for the provided symbol. + parameters: + - name: symbol + in: path + required: true + schema: + type: string + description: Trading pair symbol + example: BTCUSD + - name: time_frame + in: path + required: true + schema: + type: string + enum: [1m, 5m, 15m, 30m, 1h, 6h, 1d] + description: | + Time range for each candle: + * `1m` - 1 minute + * `5m` - 5 minutes + * `15m` - 15 minutes + * `30m` - 30 minutes + * `1h` - 1 hour + * `6h` - 6 hours + * `1day` - 1 day + example: 15m + responses: + '200': + description: The response will be an array of arrays + content: + application/json: + schema: + $ref: '#/components/schemas/CandleResponse' + example: [ + [ + 1559755800000, + 7781.6, + 7820.23, + 7776.56, + 7819.39, + 34.7624802159 + ], + [ + 1559755800000, + 7781.6, + 7829.46, + 7776.56, + 7817.28, + 43.4228281059 + ] + ] + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v2/derivatives/candles/{symbol}/{time_frame}: + get: + tags: + - Market Data + summary: List Derivative Candles + operationId: listDerivativeCandles + description: This endpoint retrieves time-intervaled data for the provided perpetual symbol. + parameters: + - name: symbol + in: path + required: true + schema: + type: string + description: "Trading pair symbol. Available only for perpetual pairs like `BTCGUSDPERP`" + example: BTCGUSDPERP + - name: time_frame + in: path + required: true + schema: + type: string + enum: [1m] + description: "Time range for each candle. `1m`: 1 minute (only)" + example: 1m + responses: + '200': + description: The response will be an array of arrays + content: + application/json: + schema: + $ref: '#/components/schemas/CandleResponse' + example: [ + [ + 1714126740000, + 68038, + 68038, + 68038, + 68038, + 0 + ], + [ + 1714126680000, + 68038, + 68038, + 68038, + 68038, + 0 + ] + ] + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + + /v2/fxrate/{symbol}/{timestamp}: + get: + tags: + - Market Data + summary: FX Rate + operationId: getFXRate + description: | + We have a growing international institutional customer base. When pulling market data for charting, it can be useful to have access to our FX rate for the relevant currency at that time. + + Please note, Gemini does not offer foreign exchange services. This endpoint is for historical reference only and does not provide any guarantee of future exchange rates. + + **Roles** + The API key you use to access this endpoint must have the Auditor role assigned. See Roles for more information. + + **Supported Pairs** + + ` + [AUDUSD, CADUSD, COPUSD, EURUSD, CHFUSD, HKDUSD, NZDUSD, GBPUSD, BRLUSD, INRUSD, SGDUSD, KRWUSD, JPYUSD, CNYUSD] + ` + parameters: + - $ref: '#/components/parameters/symbolParam' + - $ref: '#/components/parameters/timestampParam' + - $ref: '#/components/parameters/apiKeyAuth' + - $ref: '#/components/parameters/signatureAuth' + - $ref: '#/components/parameters/payloadAuth' + - $ref: '#/components/parameters/contentType' + - $ref: '#/components/parameters/contentLength' + - $ref: '#/components/parameters/cacheControl' + + + security: + - apiKeyAuth: [] + signatureAuth: [] + payloadAuth: [] + + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/FxRate' + example: + fxPair: AUDUSD + rate: "0.69" + asOf: 1594651859000 + provider: bcb + benchmark: Spot + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/ApiKeyIpFilteringFailure' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalError' + +components: + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: X-GEMINI-APIKEY + description: Your API key + payloadAuth: + type: apiKey + in: header + name: X-GEMINI-PAYLOAD + description: Base64-encoded JSON payload + signatureAuth: + type: apiKey + in: header + name: X-GEMINI-SIGNATURE + description: HEX-encoded HMAC-SHA384 of payload signed with API secret + + schemas: + # Custom Data types + TimestampType: + description: timestamp + oneOf: + - type: string + description: | + Gemini strongly recommends using milliseconds instead of seconds for timestamps. + + | Timestamp format | Example | Supported request type | + |-----------------------|-----------------------|------------------------| + | string (seconds) | `1495127793` | `POST` only | + | string (milliseconds) | `1495127793000` | `POST` only | + example: "1495127793000" + - type: integer + format: int64 + description: | + Gemini strongly recommends using milliseconds instead of seconds for timestamps. + + | Timestamp format | Example | Supported request type | + |-----------------------------|---------------------------|------------------------| + | whole number (seconds) | `1495127793` | `GET`, `POST` | + | whole number (milliseconds) | `1495127793000` | `GET`, `POST` | + example: 1495127793000 + Nonce: + oneOf: + - type: TimestampType + $ref: '#/components/schemas/TimestampType' + example: 1495127793000 + - type: integer + example: 1495127793000 + description: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + + # Common schema components + ErrorResponse: + type: object + properties: + result: + type: string + description: Error + reason: + type: string + description: A short description + message: + type: string + description: Detailed error message + + SymbolDetails: + type: object + properties: + symbol: + type: string + example: BTCUSD + description: The requested symbol. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols) + base_currency: + type: string + example: BTC + description: CCY1 or the top currency. (i.e `BTC` in `BTCUSD`) + quote_currency: + type: string + example: USD + description: CCY2 or the quote currency. (i.e `USD` in `BTCUSD`) + tick_size: + type: number + format: decimal + example: 0.00000001 + description: The number of decimal places in the `base_currency`. (i.e `1e-8`) + quote_increment: + type: number + format: decimal + example: 0.01 + description: The number of decimal places in the `quote_currency` (i.e `0.01`) + min_order_size: + type: string + example: "0.00001" + description: The minimum order size in `base_currency` units (i.e `0.00001`) + status: + type: string + example: open + description: Status of the current order book. Can be `open`, `closed`, `cancel_only`, `post_only`, `limit_only`. + wrap_enabled: + type: boolean + example: false + description: | + When `True`, symbol can be wrapped using this endpoint: + `POST https://api.gemini.com/v1/wrap/:symbol` + product_type: + type: string + example: spot + description: Instrument type `spot` / `swap` -- where `swap` signifies `perpetual swap`. + contract_type: + type: string + example: vanilla + description: | + `vanilla` / `linear` / `inverse` where `vanilla` is for spot + while `linear` is for perpetual swap and `inverse` is a special case perpetual swap where the perpetual contract will be settled in base currency. + contract_price_currency: + type: string + example: USD + description: | + CCY2 or the quote currency for spot instrument (i.e. `USD` in `BTCUSD`) + Or collateral currency of the contract in case of perpetual swap instrument. + + Ticker: + type: object + properties: + bid: + type: string + format: decimal + description: The highest bid currently available + example: "977.59" + ask: + type: string + format: decimal + description: The lowest ask currently available + example: "977.35" + last: + type: string + format: decimal + description: The price of the last executed trade + example: "977.65" + volume: + type: object + description: Information about the 24 hour volume on the exchange. See properties below + properties: + timestamp: + $ref: '#/components/schemas/TimestampType' + example: 1483018200000 + description: The end of the 24-hour period over which volume was measured. [timestamp (ms)](/rest/~schemas#timestamp-type) + price_symbol: + type: string + format: decimal + description: The volume denominated in the price currency + example: "2210.505328803" + quantity_symbol: + type: string + format: decimal + description: The volume denominated in the quantity currency + example: "2135477.463379586263" + + OrderBook: + type: object + properties: + bids: + type: array + description: The bid price levels currently on the book. These are offers to buy at a given price. + items: + $ref: '#/components/schemas/OrderBookEntry' + asks: + type: array + description: The ask price levels currently on the book. These are offers to sell at a given price. + items: + $ref: '#/components/schemas/OrderBookEntry' + + OrderBookEntry: + type: object + properties: + price: + type: string + format: decimal + description: The price + amount: + type: string + format: decimal + description: The total quantity remaining at the price + timestamp: + type: string + description: "**DO NOT USE** - this field is included for compatibility reasons only and is just populated with a dummy value." + + Trade: + type: object + properties: + timestamp: + $ref: '#/components/schemas/TimestampType' + example: 1547146811 + description: The time that the trade was executed + timestampms: + $ref: '#/components/schemas/TimestampType' + example: 1547146811357 + description: The time that the trade was executed in milliseconds + tid: + type: integer + format: int64 + example: 5335307668 + description: The trade ID number + price: + type: string + format: decimal + example: "3610.85" + description: The price the trade was executed at + amount: + type: string + format: decimal + example: "0.27413495" + description: The amount that was traded + exchange: + type: string + example: gemini + description: Will always be "gemini" + type: + type: string + enum: [buy, sell] + example: buy + description: | + - `buy` means that an ask was removed from the book by an incoming buy order. + - `sell` means that a bid was removed from the book by an incoming sell order. + broken: + type: boolean + example: "false" + description: Whether the trade was broken or not. Broken trades will not be displayed by default; use the `include_breaks` to display them. + + Heartbeat: + type: object + properties: + request: + type: string + description: The literal string `/v1/heartbeat` + nonce: + oneOf: + - type: string + description: | + Gemini strongly recommends using milliseconds instead of seconds for timestamps. + + | Timestamp format | Example | Supported request type | + |-----------------------|-----------------------|------------------------| + | string (seconds) | `'1495127793'` | `POST` only | + | string (milliseconds) | `'1495127793000'` | `POST` only | + - type: integer + format: int64 + description: | + Gemini strongly recommends using milliseconds instead of seconds for timestamps. + + | Timestamp format | Example | Supported request type | + |-----------------------------|---------------------------|------------------------| + | whole number (seconds) | `1495127793` | `GET`, `POST` | + | whole number (milliseconds) | `1495127793000` | `GET`, `POST` | + + description: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + + NewOrderRequest: + type: object + required: + - request + - nonce + - symbol + - amount + - price + - side + - type + + properties: + request: + type: string + description: The literal string "/v1/order/new" + example: /v1/order/new + nonce: + type: number + description: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + client_order_id: + type: string + description: "*Recommended*. A [client-specified order id](/client-order-id)" + symbol: + type: string + description: The [symbol](/market-data/symbols-and-minimums) for the new order + example: BTCUSD + amount: + type: string + description: Quoted decimal amount to purchase + example: "5" + price: + type: string + description: Quoted decimal amount to spend per unit + example: "3633.00" + side: + type: string + enum: [buy, sell] + example: buy + type: + type: string + enum: [exchange limit, exchange stop limit, exchange market] + description: The order type. "exchange limit" for all order types except for stop-limit orders. "exchange stop limit" for stop-limit orders. + example: exchange limit + options: + type: array + items: + type: string + enum: [maker-or-cancel, immediate-or-cancel, fill-or-kill] + description: "An optional array containing at most one supported order execution option. See Order execution options for details." + example: ["maker-or-cancel"] + stop_price: + type: string + description: "The price to trigger a stop-limit order. Only available for stop-limit orders." + margin_order: + type: boolean + description: "Set to `true` to place this order on a margin account using borrowed funds. Defaults to `false`. Only available for margin-enabled accounts. See [Margin Trading](/margin/account-summary) for details." + example: false + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + + CancelOrderRequest: + type: object + required: + - request + - nonce + - order_id + properties: + request: + type: string + description: The literal string "/v1/order/cancel" + example: /v1/order/cancel + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + order_id: + type: integer + format: int64 + x-unsigned-int64: true + description: The order ID given by `/order/new` + example: 106817811 + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to cancel the order. Only available for exchange accounts." + example: primary + + CancelAllOrdersRequest: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/order/cancel/all" + example: "/v1/order/cancel/all" + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to cancel the orders. Only available for exchange accounts." + example: primary + + CancelAllOrdersBySessionRequest: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The literal string "/v1/order/cancel/session" + example: "/v1/order/cancel/session" + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to cancel the orders. Only available for exchange accounts." + example: primary + + OrderStatusRequest: + type: object + required: + - request + - nonce + - order_id + properties: + request: + type: string + description: The API endpoint path + example: /v1/order/status + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + example: primary + order_id: + type: integer + format: int64 + x-unsigned-int64: true + description: The order id to get information on. The `order_id` represents a whole number and is transmitted as an unsigned 64-bit integer in JSON format. `order_id` cannot be used in combination with `client_order_id`. + example: 123456789012345 + client_order_id: + type: string + description: "The `client_order_id` used when placing the order. `client_order_id` cannot be used in combination with `order_id`" + include_trades: + type: boolean + description: "Either `True` or `False`. If `True` the endpoint will return individual trade details of all fills from the order." + + MyTradesRequest: + type: object + required: + - request + - nonce + properties: + request: + type: string + description: The API endpoint path + example: /v1/mytrades + nonce: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + title: The nonce, as described in [Private API Invocation](/authentication/api-key#private-api-invocation) + account: + type: string + description: "Required for Master API keys as described in [Private API Invocation](/authentication/api-key#private-api-invocation). The name of the account within the subaccount group. Specifies the account on which you intend to place the order. Only available for exchange accounts." + example: primary + symbol: + type: string + description: "The [symbol](/market-data/symbols-and-minimums) to retrieve trades for" + example: btcusd + limit_trades: + type: integer + description: "The maximum number of trades to return. Default is 50, max is 500." + example: 50 + timestamp: + $ref: '#/components/schemas/TimestampType' + description: "Only return trades on or after this timestamp. See [Data Types: Timestamps](/rest/~schemas#timestamp-type) for more information. If not present, will show the most recent orders." + example: 1591084414000 + + LimitOrderResponse: + type: object + title: Limit Order Response + properties: + order_id: + type: string + id: + type: string + symbol: + type: string + exchange: + type: string + avg_execution_price: + type: string + side: + type: string + enum: [buy, sell] + type: + type: string + enum: [exchange limit, exchange stop limit, exchange market] + timestamp: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + timestampms: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + is_live: + type: boolean + is_cancelled: + type: boolean + is_hidden: + type: boolean + was_forced: + type: boolean + executed_amount: + type: string + remaining_amount: + type: string + format: double + client_order_id: + type: string + options: + type: array + items: + type: string + price: + type: string + format: double + original_amount: + type: string + format: double + + StopLimitOrderResponse: + type: object + title: Stop-Limit Order Response + properties: + order_id: + type: string + id: + type: string + symbol: + type: string + exchange: + type: string + avg_execution_price: + type: string + side: + type: string + enum: [buy, sell] + type: + type: string + enum: [exchange stop limit] + timestamp: + $ref: '#/components/schemas/TimestampType' + timestampms: + $ref: '#/components/schemas/TimestampType' + is_live: + type: boolean + is_cancelled: + type: boolean + is_hidden: + type: boolean + was_forced: + type: boolean + executed_amount: + type: string + options: + type: array + items: + type: string + stop_price: + type: string + format: double + price: + type: string + format: double + original_amount: + type: string + format: double + + CancelOrderResponse: + type: object + properties: + order_id: + type: string + format: integer + id: + type: string + format: integer + symbol: + type: string + exchange: + type: string + avg_execution_price: + type: string + format: double + side: + type: string + enum: [buy, sell] + type: + type: string + enum: [exchange limit, exchange stop limit, exchange market] + timestamp: + $ref: '#/components/schemas/TimestampType' + timestampms: + $ref: '#/components/schemas/TimestampType' + is_live: + type: boolean + is_cancelled: + type: boolean + is_hidden: + type: boolean + was_forced: + type: boolean + executed_amount: + type: string + format: double + remaining_amount: + type: string + format: double + reason: + type: string + enum: [MakerOrCancelWouldTake, ExceedsPriceLimits, SelfCrossPrevented, ImmediateOrCancelWouldPost, FillOrKillWouldNotFill, Requested, MarketClosed, TradingClosed] + options: + type: array + items: + type: string + price: + type: string + format: double + original_amount: + type: string + format: double + + Order: + type: object + properties: + order_id: + type: string + format: integer + description: The order id + client_order_id: + type: string + format: integer + description: An optional [client-specified order id](/client-order-id#client-order-id) + symbol: + type: string + description: The [symbol](/market-data/symbols-and-minimums#symbols-and-minimums) of the order + exchange: + type: string + description: Will always be "gemini" + price: + type: string + format: decimal + description: The price the order was issued at + avg_execution_price: + type: string + format: decimal + description: The average price at which this order as been executed so far. 0 if the order has not been executed at all. + side: + type: string + enum: [buy, sell] + type: + type: string + enum: [exchange limit, exchange stop limit, exchange market] + description: Description of the order + options: + type: array + items: + type: string + description: An array containing at most one supported order execution option. See [Order execution options](/rest/orders#create-new-order) for details. + timestamp: + $ref: '#/components/schemas/TimestampType' + description: The timestamp the order was submitted. Note that for compatibility reasons, this is returned as a string. We recommend using the timestampms field instead. + timestampms: + $ref: '#/components/schemas/TimestampType' + description: The timestamp the order was submitted in milliseconds. + is_live: + type: boolean + description: "`true` if the order is active on the book (has remaining quantity and has not been canceled)" + is_cancelled: + type: boolean + description: '`true` if the order has been canceled. Note the spelling, "cancelled" instead of "canceled". This is for compatibility reasons.' + reason: + type: string + description: Populated with the reason your order was canceled, if available. + was_forced: + type: boolean + description: Will always be `false`. + executed_amount: + type: string + format: decimal + description: The amount of the order that has been filled. + remaining_amount: + type: string + format: decimal + description: The amount of the order that has not been filled. + original_amount: + type: string + format: decimal + description: The originally submitted amount of the order. + is_hidden: + type: boolean + description: Will always return `false`. + trades: + type: array + items: + type: object + properties: + price: + type: string + format: decimal + description: The price that the execution happened at + amount: + type: string + format: decimal + description: The quantity that was executed + timestamp: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + description: The time that the trade happened in epoch seconds + timestampms: + type: TimestampType + $ref: '#/components/schemas/TimestampType' + description: The time that the trade happened in milliseconds + type: + type: string + enum: [Buy, Sell] + example: Buy + description: Will be either "Buy" or "Sell", indicating the side of the original order + aggressor: + type: boolean + description: If `true`, this order was the taker in the trade + fee_currency: + type: string + example: USD + description: Currency that the fee was paid in + fee_amount: + type: string + format: decimal + example: "1.23" + description: The amount charged + tid: + type: integer + example: 17379712930 + description: Unique identifier for the trade + order_id: + type: string + example: 123456789 + description: The order that this trade executed against + exchange: + type: string + example: gemini + description: Will always be "gemini" + break: + type: string + description: "Will only be present if the trade is broken. See `Break Types` below for more information." + description: "Contains an array of JSON objects with trade details." + + CancelAllResult: + type: object + properties: + result: + type: string + example: ok + details: + type: object + description: cancelledOrders/cancelRejects with IDs of both + properties: + cancelledOrders: + type: array + items: + type: integer + cancelRejects: + type: array + items: + type: integer + + MyTrade: + type: object + properties: + price: + type: string + example: 9100.00 + amount: + type: string + example: 1.5 + timestamp: + $ref: '#/components/schemas/TimestampType' + example: 1591084414 + timestampms: + $ref: '#/components/schemas/TimestampType' + example: 1591084414622 + type: + type: string + enum: [Buy, Sell] + example: Buy + aggressor: + type: boolean + example: true + fee_currency: + type: string + example: USD + fee_amount: + type: string + example: 13.65 + tid: + type: integer + format: int64 + example: 123456789 + order_id: + type: string + example: 123456789 + client_order_id: + type: string + exchange: + type: string + example: gemini + is_auction_fill: + type: boolean + example: false + break: + type: string + enum: ["", "trade correct"] + example: "" + + TradeVolume: + type: object + properties: + symbol: + type: string + example: btcusd + base_currency: + type: string + example: BTC + quote_currency: + type: string + example: USD + notional_currency: + type: string + example: USD + data_date: + type: string + example: 2020-06-02 + total_volume_base: + type: string + example: 10.5 + maker_buy_sell_ratio: + type: string + example: 1.2 + buy_maker_base: + type: string + example: 5.5 + buy_maker_notional: + type: string + example: 50050.00 + buy_maker_count: + type: integer + example: 10 + sell_maker_base: + type: string + example: 5.0 + sell_maker_notional: + type: string + example: 45500.00 + sell_maker_count: + type: integer + example: 8 + buy_taker_base: + type: string + example: 8.5 + buy_taker_notional: + type: string + example: 77350.00 + buy_taker_count: + type: integer + example: 15 + sell_taker_base: + type: string + example: 7.5 + sell_taker_notional: + type: string + example: 68250.00 + sell_taker_count: + type: integer + example: 12 + + Balance: + type: object + properties: + type: + type: string + enum: [exchange] + example: exchange + currency: + type: string + example: BTC + description: The currency symbol + amount: + type: number + example: 10.5 + description: | + The confirmed balance for the currency (also referred to as `confirmedBalance`). For crypto withdrawals, this value is **not** reduced until the withdrawal has been confirmed on the blockchain. This delay protects against blockchain reorganizations. Use the `available` field instead if you need balances that immediately reflect holds. + available: + type: number + example: 9.0 + description: | + The amount available for trading. This value is reduced **immediately** when an order hold or withdrawal hold is placed, making it the recommended field for tracking real-time spendable balances. + availableForWithdrawal: + type: number + example: 9.0 + description: The amount available for withdrawal + pendingWithdrawal: + type: number + example: 1.0 + description: The amount pending withdrawal + pendingDeposit: + type: number + example: 0.5 + description: The amount pending deposit + _timestamp: + type: string + format: date-time + example: "2024-03-16T00:00:00.000000Z" + description: Server-side monotonically increasing clock value as an ISO 8601 timestamp. Clients can use this value to detect and filter out stale responses that may occur due to load balancing or potential stale servers. + + NotionalVolume: + type: object + properties: + date: + type: string + format: date + example: '2020-06-02' + last_updated_ms: + type: integer + example: 1591084414622 + web_maker_fee_bps: + type: integer + example: 25 + web_taker_fee_bps: + type: integer + example: 35 + web_auction_fee_bps: + type: integer + example: 25 + api_maker_fee_bps: + type: integer + example: 10 + api_taker_fee_bps: + type: integer + example: 35 + api_auction_fee_bps: + type: integer + example: 20 + fix_maker_fee_bps: + type: integer + example: 10 + fix_taker_fee_bps: + type: integer + example: 35 + fix_auction_fee_bps: + type: integer + example: 20 + notional_30d_volume: + type: string + example: 1000000.00 + notional_1d_volume: + type: array + items: + type: object + properties: + date: + type: string + description: UTC date in `yyyy-MM-dd` format + notional_volume: + type: string + format: decimal + description: Notional volume value in USD for this single day + api_notional_30d_volume: + type: string + example: 750000.00 + fee_tier: + type: object + properties: + tier: + type: string + example: '0bps' + api_maker_fee_bps: + type: integer + example: 0 + api_taker_fee_bps: + type: integer + example: 10 + + NotionalBalance: + type: object + properties: + currency: + type: string + description: Currency code, see symbols and minimums + amount: + type: string + description: The current balance + amountNotional: + type: string + description: Amount, in notional + available: + type: string + description: The amount that is available to trade + availableNotional: + type: string + description: Available, in notional + availableForWithdrawal: + type: string + description: The amount that is available to withdraw + availableForWithdrawalNotional: + type: string + description: AvailableForWithdrawal, in notional + + Address: + type: object + properties: + address: + type: string + description: String representation of the cryptocurrency address + timestamp: + $ref: '#/components/schemas/TimestampType' + description: Creation date of the address + label: + type: string + description: If you provided a label when creating the address, it will be echoed back here + memo: + type: string + description: It would be present if applicable, it will be present for cosmos address + network: + type: string + description: The blockchain network for the address + + Transfer: + type: object + properties: + type: + type: string + enum: [Deposit, Withdrawal] + status: + type: string + enum: [Complete, Pending] + timestampms: + $ref: '#/components/schemas/TimestampType' + description: The timestamp in milliseconds + eid: + type: integer + format: int64 + description: The transfer ID + currency: + type: string + description: The currency transferred + amount: + type: string + description: The amount transferred + txHash: + type: string + description: The transaction hash if applicable + + V2Transfer: + type: object + properties: + type: + type: string + enum: [Deposit, Withdrawal, Reward, AdminDebit, AdminCredit] + description: The type of the transfer + status: + type: string + enum: [Complete, Pending, Advanced] + description: The status of the transfer + timestampms: + $ref: '#/components/schemas/TimestampType' + description: The timestamp in milliseconds + eid: + type: integer + format: int64 + description: The transfer event ID + currency: + type: string + description: The currency transferred + amount: + type: string + description: The amount transferred + network: + type: string + description: The blockchain network the transfer was executed on (e.g., `ethereum`, `solana`, `arbitrum`, `optimism`, `base`, `avalanche`). Not present for fiat or administrative transfers. + feeAmount: + type: string + description: The fee charged for the transfer + feeCurrency: + type: string + description: The currency in which the fee was charged + txHash: + type: string + description: The on-chain transaction hash, if applicable + method: + type: string + description: The transfer method (e.g., `ACH`, `CreditCard`) + destination: + type: string + description: The destination address for withdrawals + withdrawalId: + type: string + description: The unique withdrawal identifier + outputIdx: + type: integer + description: The output index for withdrawals + purpose: + type: string + description: The purpose or reason for administrative transfers + + WithdrawCryptoFundsResponse: + type: object + description: Response returned after submitting a v2 cryptocurrency withdrawal. + properties: + withdrawalId: + type: string + description: A unique ID for the withdrawal + address: + type: string + description: Standard string format of the withdrawal destination address + amount: + type: string + description: The withdrawal amount + currency: + type: string + description: The currency code of the withdrawn asset + fee: + type: string + description: The fee charged for the withdrawal + + InstantQuote: + type: object + properties: + quoteId: + type: integer + description: Unique ID for the quote. This is used in the execution of the order + maxAgeMs: + type: integer + description: Number of milliseconds until this quote price expires. Once expired, you will need to request a new quote + pair: + type: string + description: The symbol passed in the quote request + price: + type: string + description: The quoted price of the asset. This will not change when attempting execution + priceCurrency: + type: string + description: The currency in which the order is priced. Matches `CCY2` in the symbol + side: + type: string + enum: [buy, sell] + description: Either "buy" or "sell" + quantity: + type: string + description: The quantity of the asset to be bought or sold + quantityCurrency: + type: string + description: The currency label for the `quantity` field. Matches `CCY1` in the symbol + fee: + type: string + description: The fee quantity to be taken for the order upon execution + feeCurrency: + type: string + description: The currency label for the order + depositFee: + type: string + description: The deposit fee quantity. Will be applied if a debit card is used for the order. Will return 0 if there is no `depositFee` + depositFeeCurrency: + type: string + description: Currency in which `depositFee` is taken + totalSpend: + type: string + description: Total quantity to spend for the order. Will be the sum inclusive of all fees and amount to be traded. + totalSpendCurrency: + type: string + description: Currency of the `totalSpend` to be spent on the order + + ClearingOrder: + type: object + properties: + clearing_id: + type: string + description: The clearing ID + symbol: + type: string + description: The trading pair + price: + type: string + description: The order price + amount: + type: string + description: The order amount + side: + type: string + enum: [buy, sell] + status: + type: string + description: The order status + timestamp: + $ref: '#/components/schemas/TimestampType' + description: The timestamp + timestampms: + type: integer + description: The timestamp in milliseconds + is_confirmed: + type: boolean + description: Whether the order is confirmed + + Account: + type: object + properties: + name: + type: string + description: The account name + account_id: + type: string + description: The account ID + is_default: + type: boolean + description: Whether the account is the default account + created: + type: string + description: The creation date + + Transaction: + oneOf: + - type: object + description: Trade Reponse + title: Trade Reponse + properties: + account: + type: string + description: The account. + amount: + type: string + description: The quantity that was executed. + clientOrderId: + type: string + description: The client order ID, if defined. Otherwise an empty string. + price: + type: string + description: The price that the execution happened at. + timestampms: + $ref: '#/components/schemas/TimestampType' + description: The time that the trade happened in milliseconds. + side: + type: string + description: Indicating the side of the original order. + isAggressor: + type: boolean + description: If true, this order was the taker in the trade. + feeAssetCode: + type: string + description: The symbol that the trade was for + feeAmount: + type: string + description: The fee amount charged + orderId: + type: integer + format: int64 + description: The order that this trade executed against. + exchange: + type: string + description: Will always be "gemini". + isAuctionFill: + type: boolean + description: True if the trade was a auction trade and not an on-exchange trade. + isClearingFill: + type: boolean + description: True if the trade was a clearing trade and not an on-exchange trade. + tid: + type: integer + format: int64 + description: The trade ID. + symbol: + type: string + description: The symbol that the trade was for. + - type: object + description: Transfer Reponse + title: Transfer Reponse + properties: + timestampms: + $ref: '#/components/schemas/TimestampType' + description: The time that the trade happened in milliseconds. + source: + type: string + description: The account you are transferring from. + destination: + type: string + description: The account you are transferring to. + operationReason: + type: string + description: The operation reason. + status: + type: string + description: The status of the transfer. + eid: + type: integer + format: int64 + description: Transfer event id. + currency: + type: string + description: Currency code, see symbols + amount: + type: string + description: The quantity that was transferred. + method: + type: string + description: Type of transfer method. + correlationId: + type: integer + format: int64 + description: Correlation ID. + transferType: + type: string + description: Transfer type. + bankId: + type: string + description: Bank ID. + purpose: + type: string + description: Purpose. + transactionHash: + type: string + description: Supplies the transaction hash when available. + transferId: + type: string + description: Transfer ID. + withdrawalId: + type: string + description: Withdrawal ID. + clientTransferId: + type: string + description: Client Transfer ID. Client transfer ID is an optional client-supplied unique identifier for each withdrawal or internal transfer. + advanceEid: + type: integer + format: int64 + description: Deposit advance event ID. + pendingEid: + type: integer + format: int64 + description: Pending event ID. + withdrawalEid: + type: integer + format: int64 + description: Withdrawal event ID. + feeId: + type: string + description: Fee ID. + + + RevokeOauthTokenResponse: + type: object + properties: + message: + type: string + description: A message that indicates the token has been revoked for the account + + NetworkToken: + type: object + properties: + token: + type: string + description: The requested token identifier. + network: + type: array + items: + type: string + description: | + Array of supported blockchain networks for the token. Many tokens (especially stablecoins like USDC, USDT) are available on multiple networks. + + Supported networks include: `bitcoin`, `ethereum`, `solana`, `optimism`, `arbitrum`, `base`, `monad`, `avalanche`, `litecoin`, `bitcoincash`, `dogecoin`, `zcash`, `filecoin`, `tezos`, `polkadot`, `cosmos`, `xrpl`, `linea`, and more. + example: ["optimism", "solana", "base", "arbitrum", "monad", "avalanche", "ethereum"] + + NetworkAssets: + type: object + properties: + network: + type: string + description: The blockchain network identifier. + example: "ethereum" + assets: + type: array + items: + type: string + description: | + Alphabetically sorted array of enabled asset/token codes available on this network. Assets include both exchange-tradable and custody-supported tokens. + example: ["AAVE", "BAT", "DAI", "ETH", "LINK", "MATIC", "UNI", "USDC", "USDT", "WBTC"] + + FeePromos: + type: object + properties: + symbols: + type: array + items: + type: string + description: Symbols that currently have fee promos + + PriceFeedResponse: + type: array + items: + type: object + properties: + pair: + type: string + description: Trading pair symbol. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols) + price: + type: string + description: Current price of the pair on the Gemini order book + percentChange24h: + type: string + description: 24 hour change in price of the pair on the Gemini order book + + ApprovedAddress: + type: object + properties: + network: + type: string + description: The network of the approved address. Network can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` + scope: + type: string + description: Will return the scope of the address as either "account" or "group" + label: + type: string + description: The label assigned to the address + status: + type: string + description: The status of the address that will return as "active", "pending-time" or "pending-mua". The remaining time is exactly 7 days after the initial request. "pending-mua" is for multi-user accounts and will require another administator or fund manager on the account to approve the address. + createdAt: + type: string + description: UTC timestamp in millisecond of when the address was created. + address: + type: string + description: The address on the approved address list. + + ApprovedAddressesResponse: + type: object + description: Response envelope containing the approved withdrawal addresses. + properties: + approvedAddresses: + type: array + description: Array of approved addresses on both the account and group level. + items: + $ref: '#/components/schemas/ApprovedAddress' + + ApprovedAddressMessage: + type: object + properties: + message: + type: string + description: Status or confirmation message for the approved address request or removal. + result: + type: string + description: Result status (e.g. ok). + + CustodyFeeTransfer: + type: object + properties: + txTime: + type: integer + description: Time of Custody fee record in milliseconds + feeAmount: + type: string + description: The fee amount charged + feeCurrency: + type: string + description: Currency that the fee was paid in + eid: + type: integer + description: Custody fee event id + eventType: + type: string + description: Custody fee event type + + PaymentMethodsResponse: + type: object + properties: + balances: + type: array + description: Array of JSON objects with available fiat currencies and their balances. + items: + $ref: '#/components/schemas/PaymentMethodBalance' + banks: + type: array + description: Array of JSON objects with banking information + items: + $ref: '#/components/schemas/PaymentMethodBank' + + PaymentMethodBalance: + type: object + properties: + type: + type: string + description: Account type. Will always be `exchange` + currency: + type: string + description: Symbol for fiat balance. + amount: + type: string + description: Total account balance for currency. + available: + type: string + description: Total amount available for trading + availableForWithdrawal: + type: string + description: Total amount available for withdrawal + + PaymentMethodBank: + type: object + properties: + bank: + type: string + description: Name of bank account + bankId: + type: string + description: Unique identifier for bank account + + AddBankResponse: + type: object + properties: + referenceId: + type: string + description: Reference ID for the new bank addition request. Once received, send in a wire from the requested bank account to verify it and enable withdrawals to that account. + result: + type: string + description: Status result (e.g. ok). + + OpenPosition: + type: object + properties: + symbol: + type: string + description: The [symbol](/market-data/symbols-and-minimums) of the order. + instrument_type: + type: string + description: The type of instrument. Either "spot" or "perp". + quantity: + type: string + format: decimal + description: The position size. Value will be negative for shorts. + notional_value: + type: string + format: decimal + description: The value of position; calculated as (`quantity` * `mark_price`). Value will be negative for shorts. + realised_pnl: + type: string + format: decimal + description: The current P&L that has been realised from the position. + unrealised_pnl: + type: string + format: decimal + description: Current Mark to Market value of the positions. + average_cost: + type: string + format: decimal + description: The average price of the current position. + mark_price: + type: string + format: decimal + description: The current Mark Price for the Asset or the position. + + FundingAmountResponse: + type: object + properties: + symbol: + type: string + description: The requested symbol. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols) + fundingDateTime: + type: string + description: UTC date time in format `yyyy-MM-ddThh:mm:ss.SSSZ` format + fundingTimestampMilliSecs: + type: number + format: long + description: Current funding amount Epoc time. + nextFundingTimestamp: + type: number + format: long + description: Next funding amount Epoc time. + amount: + type: number + format: decimal + description: The dollar amount for a Long 1 position held in the symbol for funding period (1 hour) + estimatedFundingAmount: + type: number + format: decimal + description: The estimated dollar amount for a Long 1 position held in the symbol for next funding period (1 hour) + + StakingBalance: + type: object + properties: + type: + type: string + description: Will always be "Staking" + example: "Staking" + currency: + type: string + description: Currency code, see symbols and minimums + example: "MATIC" + balance: + type: number + format: decimal + description: The current Staking balance + example: 10 + available: + type: number + format: decimal + description: The amount that is available to trade + example: 0 + availableForWithdrawal: + type: number + format: decimal + description: The Staking amount that is available to redeem to exchange account + example: 10 + balanceByProvider: + type: object + additionalProperties: + type: object + properties: + balance: + type: number + format: decimal + description: The current Staking balance per providerId + example: 10 + + StakingDeposit: + type: object + properties: + transactionId: + type: string + description: A unique identifier for the staking transaction + example: "65QN4XM5" + providerId: + type: string + description: Provider Id, in uuid4 format + example: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: + type: string + description: Currency code, see [symbols](/market-data/symbols-and-minimums) + example: "MATIC" + amount: + type: number + format: decimal + description: The amount deposited + example: 30 + accrualTotal: + type: number + format: decimal + description: The total accrual + rates: + type: object + description: A JSON object including one or many rates. If more than one rate it would be an array of rates. + properties: + rate: + type: integer + description: Staking interest rate in bps (Expressed as a simple rate. Interest on Staking balances compounds daily. In mobile and web applications, APYs are derived from this rate and rounded to 1/10th of a percent.) + example: 540 + + StakingTransaction: + type: object + properties: + transactionId: + type: string + description: A unique identifier for the staking transaction + example: "MPZ7LDD8" + transactionType: + type: string + description: Can be any one of the following - Deposit, Redeem, Interest, RedeemPayment, AdminRedeem, AdminCreditAdjustment, AdminDebitAdjustment + enum: [Deposit, Redeem, Interest, RedeemPayment, AdminRedeem, AdminCreditAdjustment, AdminDebitAdjustment] + example: "Redeem" + amountCurrency: + type: string + description: Currency code + example: "MATIC" + amount: + type: number + format: decimal + description: The amount that is defined by the transactionType above + example: 20 + priceCurrency: + type: string + description: A supported three-letter fiat currency code, e.g. usd + example: "USD" + priceAmount: + type: number + format: decimal + description: Current market price of the underlying token at the time of the reward + example: 0.1 + dateTime: + $ref: '#/components/schemas/TimestampType' + description: The time of the transaction in milliseconds + example: 1667418560153 + + StakingHistory: + type: object + properties: + providerId: + type: string + description: Provider Id, in uuid4 format + example: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + transactions: + type: array + items: + $ref: '#/components/schemas/StakingTransaction' + + StakingRate: + type: object + properties: + providerId: + type: string + description: Provider Id, in uuid4 format + example: "62bb4d27-a9c8-4493-a737-d4fa33994f1f" + rate: + type: number + format: decimal + description: Staking interest rate in bps (Expressed as a simple rate. Interest on Staking balances compounds daily. In mobile and web applications, APYs are derived from this rate and rounded to 1/10th of a percent.) + example: 429.386 + apyPct: + type: number + format: decimal + description: Staking interest APY (Expressed as a percentage derived from the rate and rounded to 1/10th of a percent.) + example: 4.39 + ratePct: + type: number + format: decimal + description: "`rate` expressed as a percentage" + example: 4.29386 + depositUsdLimit: + type: integer + description: Maximum new amount in USD notional of this crypto that can participate in Gemini Staking per account per month + example: 500000 + + StakingRateProvider: + type: object + description: Currency Symbol Keys + properties: + currency_symbol: + $ref: '#/components/schemas/StakingRate' + + StakingRateResponse: + type: object + description: Provider UUID Keys + properties: + provider_uuid: + $ref: '#/components/schemas/StakingRateProvider' + + StakingRewardPeriod: + type: object + properties: + providerId: + type: string + description: Provider Id, in uuid4 format + example: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: + type: string + description: Currency code, see [symbols](/market-data/symbols-and-minimums) + example: "MATIC" + apyPct: + type: number + format: decimal + description: Staking reward rate expressed as an APY at time of accrual. Interest on Staking balances compounds daily based on the simple rate which is available from `/v1/staking/rates/` + example: 5.75 + ratePct: + type: number + format: decimal + description: Rate expressed as a percentage + example: 5.592369 + numberOfAccruals: + type: integer + description: Number of accruals in the specific aggregate, typically one per day. If the rate is adjusted, new accruals are added. + example: 1 + accrualTotal: + type: number + format: decimal + description: The total accrual + example: 0.0065678 + firstAccrualAt: + type: string + description: Time of first accrual. In iso datetime with timezone format + example: "2022-08-23T20:00:00.000Z" + lastAccrualAt: + type: string + description: Time of last accrual. In iso datetime with timezone format + example: "2022-08-23T20:00:00.000Z" + + StakingRewards: + type: object + properties: + providerId: + type: string + description: Provider Id, in uuid4 format + example: "62b21e17-2534-4b9f-afcf-b7edb609dd8d" + currency: + type: string + description: Currency code, see [symbols](/market-data/symbols-and-minimums) + example: "MATIC" + accrualTotal: + type: number + format: decimal + description: The total accrual + example: 0.103994 + ratePeriods: + type: array + description: Array of JSON objects with period accrual information + items: + $ref: '#/components/schemas/StakingRewardPeriod' + + StakingRewardsProvider: + type: object + description: Currency Symbol Keys + properties: + currency_symbol: + $ref: '#/components/schemas/StakingRewards' + + + StakingRewardsResponse: + type: object + description: Provider UUID Keys + properties: + provider_uuid: + $ref: '#/components/schemas/StakingRewardsProvider' + + StakingWithdrawal: + type: object + properties: + transactionId: + type: string + description: A unique identifier for the staking transaction + example: "MPZ7LDD8" + amount: + type: number + format: decimal + description: The amount deposited + example: 20 + amountPaidSoFar: + type: number + format: decimal + description: The amount redeemed successfully + example: 20 + amountRemaining: + type: number + format: decimal + description: The amount pending to be redeemed + example: 0 + currency: + type: string + description: Currency code + example: "MATIC" + requestInitiated: + type: string + description: In ISO datetime with timezone format + example: "2022-11-02T19:49:20.153Z" + + FeeEstimateRequest: + type: object + required: + - request + - nonce + - address + - amount + - account + properties: + request: + type: string + description: The string `/v1/withdraw/{currencyCodeLowerCase}/feeEstimate` where `:currencyCodeLowerCase` is replaced with the currency code of a supported crypto-currency, e.g. `eth`, `aave`, etc. See [Symbols and minimums](/market-data/symbols-and-minimums) + example: /v1/withdraw/eth/feeEstimate + nonce: + $ref: '#/components/schemas/Nonce' + address: + type: string + description: Standard string format of cryptocurrency address + example: "0x31c2105b8dea834167f32f7ea7d877812e059230" + amount: + type: string + description: Quoted decimal amount to withdraw + example: '0.01' + account: + type: string + description: The name of the account within the subaccount group. + example: primary + + FeeEstimateResponse: + type: object + properties: + currency: + type: string + description: Currency code, see [symbols](/market-data/symbols-and-minimums). + example: ETH + fee: + type: string + description: The estimated gas fee + example: "{currency: 'ETH', value: '0'}" + isOverride: + type: boolean + description: Value that shows if an override on the customer's account for free withdrawals exists + example: false + monthlyLimit: + type: integer + description: Total nunber of allowable fee-free withdrawals + example: 1 + monthlyRemaining: + type: integer + description: Total number of allowable fee-free withdrawals left to use + example: 1 + + FeeEstimateV2Request: + type: object + required: + - request + - nonce + - address + - amount + properties: + request: + type: string + description: The string `/v2/withdraw/{network}/{ticker}/feeEstimate` where `{network}` is the blockchain network (e.g. `ethereum`, `bitcoin`, `solana`) and `{ticker}` is the currency code (e.g. `eth`, `btc`, `sol`). See [Symbols and minimums](/market-data/symbols-and-minimums) + example: /v2/withdraw/ethereum/eth/feeEstimate + nonce: + $ref: '#/components/schemas/Nonce' + address: + type: string + description: Standard string format of the destination cryptocurrency address + example: "0x31c2105b8dea834167f32f7ea7d877812e059230" + amount: + type: string + description: Quoted decimal amount to withdraw + example: '0.01' + account: + type: string + description: Required for Master API keys. The name of the account within the subaccount group. + example: primary + memo: + type: string + description: It would be present if applicable, it will be present for cosmos address. + + FeeEstimateV2Response: + type: object + properties: + currency: + type: string + description: Currency code, see [symbols](/market-data/symbols-and-minimums). + example: ETH + fee: + type: number + format: decimal + description: The estimated withdrawal fee as a decimal amount + example: 0.001 + isOverride: + type: boolean + description: Whether an override on the customer's account for free withdrawals exists + example: false + monthlyLimit: + type: integer + description: Total number of allowable fee-free withdrawals + example: 1 + monthlyRemaining: + type: integer + description: Total number of allowable fee-free withdrawals remaining + example: 1 + + RoleResponse: + type: object + required: + - isAuditor + - isFundManager + - isTrader + properties: + isAuditor: + type: boolean + description: "`True` if the Auditor role is assigned to the API keys. `False` otherwise." + isFundManager: + type: boolean + description: "`True` if the Fund Manager role is assigned to the API keys. `False` otherwise." + isTrader: + type: boolean + description: "`True` if the Trader role is assigned to the API keys. `False` otherwise." + counterparty_id: + type: string + description: _Only returned for master-level API keys_. The Gemini clearing counterparty ID associated with the API key making the request. + isAccountAdmin: + type: boolean + description: _Only returned for master-level API keys_.`True` if the Administrator role is assigned to the API keys. `False` otherwise. + + MarginResponse: + type: object + properties: + margin_assets_value: + type: string + format: decimal + description: The $ equivalent value of all the assets available in the current trading account that can contribute to funding a derivatives position. + initial_margin: + type: string + format: decimal + description: The $ amount that is being required by the accounts current positions and open orders. + available_margin: + type: string + format: decimal + description: The difference between the `margin_assets_value` and `initial_margin`. + margin_maintenance_limit: + type: string + format: decimal + description: The minimum amount of `margin_assets_value` required before the account is moved to liquidation status. + leverage: + type: string + format: decimal + description: The ratio of Notional Value to Margin Assets Value. + notional_value: + type: string + format: decimal + description: The $ value of the current position. + estimated_liquidation_price: + type: string + format: decimal + description: The estimated price for the asset at which liquidation would occur. + initial_margin_positions: + type: string + format: decimal + description: The contribution to `initial_margin` from open positions. + reserved_margin: + type: string + format: decimal + description: The contribution to `initial_margin` from open orders. + reserved_margin_buys: + type: string + format: decimal + description: The contribution to `initial_margin` from open BUY orders. + reserved_margin_sells: + type: string + format: decimal + description: The contribution to `initial_margin` from open SELL orders. + buying_power: + type: string + format: decimal + description: The amount of that product the account could purchase based on current `initial_margin` and `margin_assets_value`. + selling_power: + type: string + format: decimal + description: The amount of that product the account could sell based on current `initial_margin` and `margin_assets_value`. + + MoneyAmount: + type: object + properties: + currency: + type: string + description: The currency code (e.g., "USD", "BTC", "ETH") + example: "USD" + value: + type: string + format: decimal + description: The amount in the specified currency + example: "10000.00" + required: + - currency + - value + + LiquidationRisk: + type: object + properties: + lossPercentage: + type: string + format: decimal + description: The percentage loss from current value that would trigger liquidation, formatted as decimal (e.g., "0.1550" = 15.50%) + example: "0.1550" + liquidationPrice: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The estimated price at which liquidation would occur (optional, may not be present for all positions) + required: + - lossPercentage + + InterestRateInfo: + type: object + properties: + rate: + type: string + format: decimal + description: The interest rate as a decimal string + example: "0.00001141552511" + interval: + type: string + enum: [hour] + description: The time interval for the rate (currently only "hour" is supported) + example: "hour" + required: + - rate + - interval + + MarginAccountSummary: + type: object + properties: + marginAssetValue: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The total value of all assets available in the margin account that can contribute to funding positions + availableCollateral: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The amount of collateral available for new positions or withdrawals + notionalValue: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The total value of all open positions + totalBorrowed: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The total amount currently borrowed across all currencies + leverage: + type: string + format: decimal + description: The current leverage ratio (notionalValue / marginAssetValue) + example: "1.5" + buyingPower: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The maximum value that can be purchased with available collateral + sellingPower: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The maximum value that can be sold with available collateral + liquidationRisk: + allOf: + - $ref: '#/components/schemas/LiquidationRisk' + description: Liquidation risk information (only present if positions exist) + interestRate: + allOf: + - $ref: '#/components/schemas/InterestRateInfo' + description: Current interest rate on borrowed amounts (only present if borrows exist) + reservedBuyOrders: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: Collateral reserved for open buy orders + reservedSellOrders: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: Collateral reserved for open sell orders + required: + - marginAssetValue + - availableCollateral + - notionalValue + - totalBorrowed + - leverage + - buyingPower + - sellingPower + - reservedBuyOrders + - reservedSellOrders + + MarginInterestRate: + type: object + properties: + currency: + type: string + description: The currency code (e.g., "BTC", "ETH", "USD") + example: "BTC" + borrowRate: + type: string + format: decimal + description: The hourly borrow rate as a decimal + example: "0.00001141552511" + borrowRateDaily: + type: string + format: decimal + description: The daily borrow rate (hourly rate × 24) + example: "0.00027397260264" + borrowRateAnnual: + type: string + format: decimal + description: The annualized borrow rate (daily rate × 365) + example: "0.1" + lastUpdated: + type: integer + format: int64 + description: Unix timestamp in milliseconds when the rate was last updated + example: 1700000000000 + required: + - currency + - borrowRate + - borrowRateDaily + - borrowRateAnnual + - lastUpdated + + MarginRatesResponse: + type: object + properties: + rates: + type: array + items: + $ref: '#/components/schemas/MarginInterestRate' + description: Array of interest rates for all borrowable currencies + required: + - rates + + MarginRiskStats: + type: object + properties: + marginAssetValue: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The total value of all assets available in the margin account + availableCollateral: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The amount of collateral available for new positions + notionalValue: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The total value of all open positions + totalBorrowed: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The total amount currently borrowed + leverage: + type: string + format: decimal + description: The leverage ratio + example: "1.5" + reservedBuyOrders: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: Collateral reserved for open buy orders + reservedSellOrders: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: Collateral reserved for open sell orders + buyingPower: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The maximum value that can be purchased + sellingPower: + allOf: + - $ref: '#/components/schemas/MoneyAmount' + description: The maximum value that can be sold + liquidationRisk: + allOf: + - $ref: '#/components/schemas/LiquidationRisk' + description: Liquidation risk information (only present if applicable) + required: + - marginAssetValue + - availableCollateral + - notionalValue + - totalBorrowed + - leverage + - reservedBuyOrders + - reservedSellOrders + - buyingPower + - sellingPower + + MarginOrderPreview: + type: object + properties: + preorder: + allOf: + - $ref: '#/components/schemas/MarginRiskStats' + description: Margin risk statistics before the order would be executed + postorder: + allOf: + - $ref: '#/components/schemas/MarginRiskStats' + description: Margin risk statistics after the order would be executed + required: + - preorder + - postorder + + Quantity: + type: object + properties: + currency: + type: string + description: The currency code of the quantity. + value: + type: string + format: decimal + description: The value of the quantity. + required: + - currency + - value + + FundingTransfer: + type: object + properties: + eventType: + type: string + description: Event type + timestamp: + allOf: + - $ref: '#/components/schemas/TimestampType' + description: Time of the funding payment + assetCode: + type: string + description: Asset symbol + action: + type: string + enum: [Credit, Debit] + description: Credit or Debit + quantity: + allOf: + - $ref: '#/components/schemas/Quantity' + description: A nested JSON object describing the transaction amount + instrumentSymbol: + type: string + description: Symbol of the underlying instrument. **Note** that this is only attached to requests from 16th April 2024 onwards. + required: + - eventType + - timestamp + - assetCode + - action + - quantity + + FundingPayment: + type: object + required: + - eventType + - hourlyFundingTransfer + properties: + eventType: + type: string + enum: [Hourly Funding Transfer] + description: Event type + hourlyFundingTransfer: + $ref: '#/components/schemas/FundingTransfer' + + FundingPaymentReportItem: + type: object + required: + - eventType + - timestamp + - assetCode + - action + - quantity + properties: + eventType: + type: string + enum: [Hourly Funding Transfer] + description: Event type + timestamp: + allOf: + - $ref: '#/components/schemas/TimestampType' + description: Time of the funding payment + assetCode: + type: string + description: Asset symbol + action: + type: string + enum: [Credit, Debit] + description: Credit or Debit + quantity: + allOf: + - $ref: '#/components/schemas/Quantity' + description: A nested JSON object describing the transaction amount + instrumentSymbol: + type: string + description: Symbol of the underlying instrument. **Note** that this is only attached to requests from 16th April 2024 onwards. + + RiskStatsResponse: + type: object + properties: + product_type: + type: string + enum: [PerpetualSwapContract] + description: Contract type for which the symbol data is fetched + mark_price: + type: string + format: decimal + description: Current mark price at the time of request + index_price: + type: string + format: decimal + description: Current index price at the time of request + open_interest: + type: string + format: decimal + description: string representation of decimal value of open interest + open_interest_notional: + type: string + format: decimal + description: string representation of decimal value of open interest notional + + FxRate: + type: object + properties: + fxPair: + type: string + description: The requested currency pair + example: AUDUSD + rate: + type: number + format: double + description: The exchange rate + example: 0.69 + asOf: + $ref: '#/components/schemas/TimestampType' + description: The timestamp (in Epoch time format) that the requested fxrate has been retrieved for + example: 1594651859000 + provider: + type: string + description: The market data provider + example: bcb + benchmark: + type: string + description: The market for which the retrieved price applies to + example: Spot + + Candle: + type: array + items: + type: number + format: double + minItems: 6 + maxItems: 6 + description: Array of [timestamp (ms), open, high, low, close, volume] + example: + - [1559755800000, 7781.6, 7820.23, 7776.56, 7819.39, 34.7624802159] + - [1559755800000, 7781.6, 7829.46, 7776.56, 7817.28, 43.4228281059] + + CandleResponse: + type: array + items: + $ref: '#/components/schemas/Candle' + + TickerInfo: + type: object + properties: + symbol: + type: string + description: The trading pair symbol + example: BTCUSD + open: + type: string + format: decimal + description: Open price from 24 hours ago + example: "9121.76" + high: + type: string + format: decimal + description: High price from 24 hours ago + example: "9440.66" + low: + type: string + format: decimal + description: Low price from 24 hours ago + example: "9106.51" + close: + type: string + format: decimal + description: Close price (most recent trade) + example: "9347.66" + changes: + type: array + description: Hourly prices descending for past 24 hours + items: + type: string + format: decimal + example: ["9365.1", "9386.16", "9373.41", "9322.56", "9268.89", "9265.38"] + bid: + type: string + format: decimal + description: Current best bid + example: "9345.70" + ask: + type: string + format: decimal + description: Current best offer + example: "9347.67" + + parameters: + timestampParam: + name: timestamp + in: path + required: true + schema: + $ref: '#/components/schemas/TimestampType' + description: | + The timestamp to pull the FX rate for. + + Gemini strongly recommends using milliseconds instead of seconds for timestamps. + example: 1591084414622 + + symbolParam: + name: symbol + in: path + required: true + schema: + type: string + description: | + Trading pair symbol

+ + `BTCUSD`, etc. See [**symbols and minimums**](/market-data/symbols-and-minimums#all-supported-symbols). + + currencyParam: + name: currency + in: path + required: true + schema: + type: string + description: Either a fiat currency, e.g. `usd` or `gbp`, or a supported crypto-currency, e.g. `gusd`, `btc`, `eth`, `aave`, etc. + + networkParam: + name: network + in: path + required: true + schema: + type: string + description: Can be `bitcoin`, `ethereum`, `bitcoincash`, `litecoin`, `zcash`, `filecoin`, `dogecoin`, `tezos`, `solana`, `polkadot`, `avalanche`, `cosmos`, or `xrpl` + + apiKeyAuth: + name: X-GEMINI-APIKEY + in: header + required: true + description: Your API key + schema: + type: string + payloadAuth: + name: X-GEMINI-PAYLOAD + in: header + required: true + description: Base64-encoded JSON payload + schema: + type: string + signatureAuth: + name: X-GEMINI-SIGNATURE + in: header + required: true + description: HEX-encoded HMAC-SHA384 of payload signed with API secret + schema: + type: string + contentType: + name: Content-Type + in: header + required: false + schema: + type: string + default: "text/plain" + contentLength: + name: Content-Length + in: header + required: false + schema: + type: string + default: "0" + cacheControl: + name: Cache-Control + in: header + required: false + schema: + type: string + default: no-cache + + responses: + BadRequest: + description: Bad request - malformed request or invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + result: error + reason: InvalidSignature + message: Invalid signature for this request + Unauthorized: + description: Unauthorized - missing or invalid authentication + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/ErrorResponse' + example: + result: error + reason: MissingApikeyHeader + message: Must provide 'X-GEMINI-APIKEY' header + ApiKeyIpFilteringFailure: + description: ApiKey fails IP Filtering Check + content: + application/json: + schema: + type: object + $ref: '#/components/schemas/ErrorResponse' + example: + result: error + reason: ApiKeyIpFilteringFailure + message: ApiKey fails IP Filtering Check for some accounts + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + result: error + reason: EndpointNotFound + message: API entry point not found + TooManyRequests: + description: Too many requests - you have exceeded the rate limit + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + result: error + reason: Too Many Requests + message: Too Many Requests + InternalError: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + result: error + reason: Internal Server Error + message: Unexpected server error occurred. diff --git a/specs/overlays/gemini-wire-exceptions.yaml b/specs/overlays/gemini-wire-exceptions.yaml new file mode 100644 index 00000000..18e82254 --- /dev/null +++ b/specs/overlays/gemini-wire-exceptions.yaml @@ -0,0 +1,37 @@ +version: 1 +exceptions: + - id: rest-nonce-json-type + summary: Signed REST payload nonce is an unquoted JSON number in TypeScript and a JSON string in Go. + typescript: src/transport/http.ts:1036 writes rawJSON(nonce) + go: auth/hmac.go:276-283 marshals the nonce string + tolerance: Conformance compares the nonce by decimal text, accepting a JSON number or a numeric JSON string. + cases: [http/hmac-requests/private-post-with-body, http/hmac-requests/private-post-empty-body] + - id: rest-payload-key-order + summary: Signed payload key order differs (TypeScript insertion order, Go lexicographic). + tolerance: Signature is verified against each SDK's own base64 payload, so key order is free on the wire. + cases: [http/hmac-requests/private-post-with-body] + - id: ws-subscription-id-scope + summary: Subscribe frame id is session-local in TypeScript and process-global in Go; neither exposes an injection seam. + tolerance: Conformance asserts method and params, and that id is a positive integer. + cases: [websocket/subscriptions/public-trades] + - id: ws-inbound-symbol-case + summary: Go uppercases the inbound top-level symbol; TypeScript delivers wire casing unchanged. + go: websocket/client_dispatch.go:113-158 + tolerance: Conformance compares symbol fields case-insensitively. + cases: [websocket/events/trade] + - id: int64-static-typing + summary: Static int64 typing differs — TypeScript keys off format int64 only; Go additionally widens the overlay's wide-integer names. + tolerance: Declared in specs/overlays/numeric-types.yaml as appliesTo [go]; runtime exactness is asserted instead. + cases: [json/wide-integer-unsafe] + - id: http-406-insufficient-funds + summary: TypeScript maps bare HTTP 406 to InsufficientFunds; Go has no 406 sentinel and needs the reason field. + tolerance: Fixtures always carry the InsufficientFunds reason, which both SDKs classify. + cases: [] + - id: rest-query-signing + summary: TypeScript can include the query string in the signed request path (signQuery); Go signs URL path only. + tolerance: Signing fixtures use query-free paths. + cases: [] + - id: candle-timeframe-spelling + summary: Spec enum is 1m/5m/15m/30m/1h/6h/1d while its prose mentions 1day; the Go service forwards the caller's string unchanged and its own test uses 1hr. + tolerance: Unresolved upstream; no conformance case asserts a timeframe spelling. + cases: [] diff --git a/specs/overlays/numeric-types.yaml b/specs/overlays/numeric-types.yaml new file mode 100644 index 00000000..9d9459cd --- /dev/null +++ b/specs/overlays/numeric-types.yaml @@ -0,0 +1,78 @@ +version: 1 + +# Raw-byte format aliases applied before the document is parsed. +formatAliases: + appliesTo: [go] + reason: >- + openapi-typescript maps the unaliased formats to `number`; widening the + published TypeScript types to bigint would break @gemini-markets/sdk + consumers. Go rewrites them so wire-wide integers never land on a + platform-sized int. + rules: + - from: long + to: int64 + - from: integer + to: int64 + +# `format: decimal` runtime representation per language. +decimalFormat: + appliesTo: [go, typescript] + format: decimal + go: + numberSchema: types.DecimalNumber + stringSchema: types.Decimal + importPath: github.com/gemini/developer-platform/packages/sdk-go/types + importAlias: types + typescript: + numberSchema: number + stringSchema: string + exactArithmetic: src/utils/decimal.ts + +# Identifier/timestamp fields whose wire values exceed a platform int. +# Matched against schema property names AND parameter names; schemas with +# allOf/anyOf/oneOf keep their declared unions. +wideIntegers: + appliesTo: [go] + reason: >- + TypeScript preserves exactness at parse time (parseLosslessJson promotes + unsafe integers to bigint), so it needs no name list; widening the generated + static types would break the published package. + matches: [schemaProperty, parameterName] + skipComposedSchemas: true + properties: + - cancelRejects + - cancelledOrders + - eid + - last_updated_ms + - order_id + - quoteId + - since_tid + - tid + - timestamp_nanos + - timestampms + - txTime + +# Unsigned 64-bit request fields, declared by the specification itself. +unsignedIntegers: + appliesTo: [go, typescript] + extension: x-unsigned-int64 + locations: + - schema: CancelOrderRequest + property: order_id + - schema: OrderStatusRequest + property: order_id + +# Overrides the published specification cannot express. +schemaOverrides: + appliesTo: [go] + reason: >- + Go needs an explicit exact-decimal or int64 type where the specification + declares a bare JSON number; TypeScript keeps the generated number/string. + decimalFields: + - schema: Balance + properties: [amount, available, availableForWithdrawal, pendingWithdrawal, pendingDeposit] + int64Fields: + - schema: NewOrderRequest + property: nonce + int64OneOfVariants: + - schema: Nonce diff --git a/specs/refresh.mjs b/specs/refresh.mjs new file mode 100644 index 00000000..b1d40c23 --- /dev/null +++ b/specs/refresh.mjs @@ -0,0 +1,26 @@ +import { createHash } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const specsDir = dirname(fileURLToPath(import.meta.url)); +const manifestPath = join(specsDir, "SOURCES.json"); +const manifest = JSON.parse(await readFile(manifestPath, "utf8")); + +for (const entry of manifest.specs) { + const response = await fetch(entry.url); + if (!response.ok) throw new Error(`failed to fetch ${entry.url}: HTTP ${response.status}`); + + const bytes = Buffer.from(await response.arrayBuffer()); + const sha256 = createHash("sha256").update(bytes).digest("hex"); + const unchanged = entry.sha256 === sha256; + const destination = join(specsDir, entry.path); + await mkdir(dirname(destination), { recursive: true }); + await writeFile(destination, bytes); + + entry.sha256 = sha256; + if (!unchanged) entry.fetchedAt = new Date().toISOString(); + console.log(`${unchanged ? "unchanged" : "updated"} ${entry.id} ${sha256}`); +} + +await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); From 76cfffef1405798c9acab40fd45697e372ef368d Mon Sep 17 00:00:00 2001 From: Kevin Nguy Date: Thu, 17 Sep 2026 11:30:58 -0700 Subject: [PATCH 2/6] chore: add Kevin as contract codeowner --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 62efce59..e08df563 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,5 +15,5 @@ /packages/sdk-go/** @fuller @ximt # Cross-SDK API contract and conformance fixtures. -/specs/** @fuller @ximt -/conformance/** @fuller @ximt +/specs/** @fuller @ximt @kevinnguy +/conformance/** @fuller @ximt @kevinnguy From 08805294c6cae6d654c1ae940512c8936b982986 Mon Sep 17 00:00:00 2001 From: Kevin Nguy Date: Thu, 17 Sep 2026 11:35:28 -0700 Subject: [PATCH 3/6] chore: assign codeowners to principal engineers --- .github/CODEOWNERS | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e08df563..8f36df0d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,19 +1,19 @@ # Release and security-sensitive repository controls. -/.github/CODEOWNERS @fuller @ximt -/.github/workflows/** @fuller @ximt +/.github/CODEOWNERS @gemini/principal-engineers +/.github/workflows/** @gemini/principal-engineers /.github/workflows/publish-typescript-sdk.yml @gemini/principal-engineers -/SECURITY.md @fuller @ximt +/SECURITY.md @gemini/principal-engineers # Published TypeScript SDK source. -/packages/sdk-typescript/** @fuller @ximt +/packages/sdk-typescript/** @gemini/principal-engineers # Published TypeScript SDK release metadata. /packages/sdk-typescript/package.json @gemini/principal-engineers /packages/sdk-typescript/package-lock.json @gemini/principal-engineers # Published Go SDK source, module metadata, and release workflow. -/packages/sdk-go/** @fuller @ximt +/packages/sdk-go/** @gemini/principal-engineers # Cross-SDK API contract and conformance fixtures. -/specs/** @fuller @ximt @kevinnguy -/conformance/** @fuller @ximt @kevinnguy +/specs/** @gemini/principal-engineers +/conformance/** @gemini/principal-engineers From 8975529c377273e8ebfb50135e453cb8ec475429 Mon Sep 17 00:00:00 2001 From: Kevin Nguy Date: Thu, 17 Sep 2026 13:30:35 -0700 Subject: [PATCH 4/6] test: enforce conformance status and id contracts --- packages/sdk-typescript/src/tests/conformance/errors.test.ts | 1 + .../sdk-typescript/src/tests/conformance/ws-events.test.ts | 3 ++- .../src/tests/conformance/ws-subscriptions.test.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/sdk-typescript/src/tests/conformance/errors.test.ts b/packages/sdk-typescript/src/tests/conformance/errors.test.ts index 2e404c5c..f0efb929 100644 --- a/packages/sdk-typescript/src/tests/conformance/errors.test.ts +++ b/packages/sdk-typescript/src/tests/conformance/errors.test.ts @@ -95,6 +95,7 @@ async function runCase(value: ErrorFixture): Promise { observed = error; } assert.ok(observed instanceof ApiError, "HTTP error must be an ApiError"); + assert.equal(observed.status, value.response.status, "HTTP status must be preserved"); const expectedClass = CLASS_BY_KIND[value.expect.kind]; assert.ok(observed instanceof expectedClass, `expected ${value.expect.kind} to map to ${expectedClass.name}`); if (value.expect.reason !== undefined) assert.equal(observed.reason, value.expect.reason); diff --git a/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts b/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts index 894e3df5..2f826a39 100644 --- a/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts +++ b/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts @@ -98,7 +98,8 @@ test("wsEvent fixtures route and decode typed events", async () => { .map((frame) => parseBoundaryRecord(frame)) .find((frame) => frame.method === "SUBSCRIBE"); assert.ok(subscribeFrame); - assert.equal(subscribeFrame.id, 1); + assert.equal(typeof subscribeFrame.id, "number"); + assert.ok(Number.isSafeInteger(subscribeFrame.id) && subscribeFrame.id > 0, "subscription IDs must be positive integers"); socket.fireMessage({ data: `{"id":${String(subscribeFrame.id)},"status":200}` }); await stream.ready; diff --git a/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts b/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts index 8bf7e688..795c1a84 100644 --- a/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts +++ b/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts @@ -108,7 +108,7 @@ test("wsSubscription fixtures emit exact subscribe frames", async () => { assert.equal(frame.method, expected.method); assert.deepEqual(frame.params, expectedParams(expected.params)); assert.equal(typeof frame.id, "number"); - assert.equal(frame.id, 1, "subscription IDs are scoped to the WebSocket session"); + assert.ok(Number.isSafeInteger(frame.id) && frame.id > 0, "subscription IDs must be positive integers"); socket.fireMessage({ data: `{"id":${String(frame.id)},"status":200}` }); await stream.ready; From 5e70983ce81cee868273239d8b870c0a3d9f94a0 Mon Sep 17 00:00:00 2001 From: Kevin Nguy Date: Thu, 17 Sep 2026 13:35:26 -0700 Subject: [PATCH 5/6] test: narrow conformance WebSocket IDs --- .../src/tests/conformance/ws-events.test.ts | 8 ++++++-- .../src/tests/conformance/ws-subscriptions.test.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts b/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts index 2f826a39..e8eede89 100644 --- a/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts +++ b/packages/sdk-typescript/src/tests/conformance/ws-events.test.ts @@ -98,8 +98,12 @@ test("wsEvent fixtures route and decode typed events", async () => { .map((frame) => parseBoundaryRecord(frame)) .find((frame) => frame.method === "SUBSCRIBE"); assert.ok(subscribeFrame); - assert.equal(typeof subscribeFrame.id, "number"); - assert.ok(Number.isSafeInteger(subscribeFrame.id) && subscribeFrame.id > 0, "subscription IDs must be positive integers"); + assert.ok( + typeof subscribeFrame.id === "number" && + Number.isSafeInteger(subscribeFrame.id) && + subscribeFrame.id > 0, + "subscription IDs must be positive integers", + ); socket.fireMessage({ data: `{"id":${String(subscribeFrame.id)},"status":200}` }); await stream.ready; diff --git a/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts b/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts index 795c1a84..527d573d 100644 --- a/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts +++ b/packages/sdk-typescript/src/tests/conformance/ws-subscriptions.test.ts @@ -107,8 +107,12 @@ test("wsSubscription fixtures emit exact subscribe frames", async () => { assert.ok(expected); assert.equal(frame.method, expected.method); assert.deepEqual(frame.params, expectedParams(expected.params)); - assert.equal(typeof frame.id, "number"); - assert.ok(Number.isSafeInteger(frame.id) && frame.id > 0, "subscription IDs must be positive integers"); + assert.ok( + typeof frame.id === "number" && + Number.isSafeInteger(frame.id) && + frame.id > 0, + "subscription IDs must be positive integers", + ); socket.fireMessage({ data: `{"id":${String(frame.id)},"status":200}` }); await stream.ready; From 81af2f808c4e46a2cb5cafd4f42d08226e31331d Mon Sep 17 00:00:00 2001 From: Kevin Nguy Date: Thu, 17 Sep 2026 14:10:57 -0700 Subject: [PATCH 6/6] docs: correct conformance fixture count --- conformance/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conformance/README.md b/conformance/README.md index 35bf2306..ed04b621 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -29,7 +29,7 @@ platform-sized floating-point value. ## Case schemas -The five `kind` values are described below. Fields not marked optional are +The six `kind` values are described below. Fields not marked optional are required. `id` is always the full path `/` and `kind` must match its manifest suite. @@ -182,7 +182,7 @@ exception. Wide update IDs are compared by decimal text. A new SDK runner should iterate suites from `manifest.json` rather than hard-code case names, load each case from its suite directory, and implement -all five schemas above. It should use an in-memory HTTP/WebSocket double, +all six schemas above. It should use an in-memory HTTP/WebSocket double, perform independent signature verification, preserve raw numeric text, and fail on missing, extra, or unlisted fixtures. It must also compare the manifest exception IDs and every case exception reference with the reviewed