diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..559ea5ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# Dependencies +node_modules/ +vendor/ + +# Build output +dist/ +build/ +out/ +*.js.map + +# TypeScript +*.tsbuildinfo + +# Environment & secrets +.env +.env.* +!.env.example +*.pem +*.key + +# OS +.DS_Store +Thumbs.db + +# Editors +.vscode/ +.idea/ +*.swp +*.swo + +# Logs +logs/ +*.log +npm-debug.log* + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ + +# Go +*.exe +*.test +*.out + +# Temp +tmp/ +.tmp/ + +# SDK generation +scripts/rest.yaml +packages/sdk-go/ +packages/sdk-python/ +packages/sdk-typescript/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..cf148872 --- /dev/null +++ b/LICENSE @@ -0,0 +1,194 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship made available under + the License, as indicated by a copyright notice that is included in + or attached to the work (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and derivative works thereof. + + "Contribution" shall mean, as submitted to the Licensor for inclusion + in the Work by the copyright owner or by an individual or Legal Entity + authorized to submit on behalf of the copyright owner. For the purposes + of this definition, "submitted" means any form of electronic, verbal, + or written communication sent to the Licensor or its representatives, + including but not limited to communication on electronic mailing lists, + source code control systems, and issue tracking systems that are managed + by, or on behalf of, the Licensor for the purpose of discussing and + improving the Work, but excluding communication that is conspicuously + marked or designated in writing by the copyright owner as "Not a + Contribution." + + "Contributor" shall mean Licensor and any Legal Entity on behalf of + whom a Contribution has been received by the Licensor and incorporated + within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a cross-claim + or counterclaim in a lawsuit) alleging that the Work or any + Contribution embodied within the Work constitutes direct or contributory + patent infringement, then any patent licenses granted to You under + this License for that Work shall terminate as of the date such + litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative + Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, You must include a readable copy of the + attribution notices contained within such NOTICE file, in + at least one of the following places: within a NOTICE text + file distributed as part of the Derivative Works; within + the Source form or documentation, if provided along with the + Derivative Works; or, within a display generated by the + Derivative Works, if and wherever such third-party notices + normally appear. The contents of the NOTICE file are for + informational purposes only and do not modify the License. + You may add Your own attribution notices within Derivative + Works that You distribute, alongside or in addition to the + NOTICE text from the Work, provided that such additional + attribution notices cannot be construed as modifying the License. + + You may add Your own license statement for Your modifications and + may provide additional grant of rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the + Contribution, and to permit persons to whom the Contribution is + furnished to do so. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or reproducing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or exemplary damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or all other + commercial damages or losses), even if such Contributor has been + advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format in question. It may also be + included in a separate file called "LICENSE" accompanying the + work, or embedded in the source code comments. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md new file mode 100644 index 00000000..a31a4fc4 --- /dev/null +++ b/packages/mcp-server/README.md @@ -0,0 +1,136 @@ +# Gemini MCP Server + +An MCP (Model Context Protocol) server that exposes the [Gemini cryptocurrency exchange API](https://docs.gemini.com/rest-api/) as tools for AI assistants like Claude. + +## Setup + +### 1. Get Gemini API credentials + +Create an API key at [exchange.gemini.com/settings/api](https://exchange.gemini.com/settings/api). You'll need both the API key and secret. + +> **Master API keys:** If your key starts with `master-`, it has access to multiple sub-accounts and you must also specify which account to target via `GEMINI_ACCOUNT` (e.g. `"primary"`). The server will exit with a clear error on startup if this is missing. + +### 2. Install and build + +```bash +git clone && cd packages/mcp-server +npm install +npm run build +``` + +### 3. Add to your MCP client + +Credentials are passed as environment variables in your MCP client config — do not use `.env` files. + +**Claude Code** — add via the CLI: + +```bash +claude mcp add gemini -s user -e GEMINI_API_KEY=your_key -e GEMINI_API_SECRET=your_secret -- node /absolute/path/to/mcp-server/dist/index.js +``` + +For a Master API key, also pass `GEMINI_ACCOUNT`: + +```bash +claude mcp add gemini -s user -e GEMINI_API_KEY=master-xxxx -e GEMINI_API_SECRET=your_secret -e GEMINI_ACCOUNT=primary -- node /absolute/path/to/mcp-server/dist/index.js +``` + +The `-s user` flag registers the server globally across all your projects. Omit it to register only for the current project. + +**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "gemini": { + "command": "node", + "args": ["/absolute/path/to/mcp-server/dist/index.js"], + "env": { + "GEMINI_API_KEY": "your_api_key_here", + "GEMINI_API_SECRET": "your_api_secret_here", + "GEMINI_ACCOUNT": "primary" + } + } + } +} +``` + +**ChatGPT** — add to your ChatGPT desktop app's MCP config file at `~/.chatgpt/mcp.json`: + +```json +{ + "servers": { + "gemini": { + "command": "node", + "args": ["/absolute/path/to/mcp-server/dist/index.js"], + "env": { + "GEMINI_API_KEY": "your_api_key_here", + "GEMINI_API_SECRET": "your_api_secret_here", + "GEMINI_ACCOUNT": "primary" + } + } + } +} +``` + +> **Sandbox testing:** Set `GEMINI_API_BASE_URL=https://api.sandbox.gemini.com` to use Gemini's sandbox environment. + +## Examples + +### Check prices and balances + +> "What's the current price of BTC and ETH?" + +The assistant will call `gemini_get_ticker` for both symbols and return the latest bid/ask/last prices. + +> "Show me my account balances." + +Calls `gemini_get_balances` and returns your holdings across all currencies. + +### Place and manage orders + +> "Buy 0.01 BTC at $60,000." + +The assistant calls `gemini_new_order` with symbol `btcusd`, side `buy`, amount `0.01`, and price `60000`. + +> "Cancel all my open orders." + +Calls `gemini_cancel_all_active_orders` to clear your order book. + +### Market analysis + +> "Show me the 1-hour candles for ETH/USD over the last day." + +Calls `gemini_get_candles` with symbol `ethusd` and time frame `1hr`. + +> "What's the order book depth for SOLUSD?" + +Calls `gemini_get_order_book` and returns current bids and asks. + +### Staking + +> "What are my current staking positions and rewards?" + +Calls `gemini_get_staking_balances` to show staked assets and accrued rewards. + +> "Stake 1 ETH." + +Calls `gemini_stake` with the specified currency and amount. + +## Available tools + +| Category | Tools | Auth required | +|-----------------|---------------------------------------------------------------------------|---------------| +| **Market Data** | Tickers, candles, order book, trades, auctions, price feed, funding rates | No | +| **Orders** | Place, cancel, status, active orders, trade history, volume | Yes | +| **Funds** | Balances, transfers, deposit addresses, withdrawals, bank accounts | Yes | +| **Account** | Account details, sub-accounts, roles, approved addresses | Yes | +| **Margin** | Margin account, preview, positions, funding payments | Yes | +| **Staking** | Balances, history, rates, stake, unstake | Yes | + +## Development + +```bash +npm run dev # Run with hot reload via tsx +npm run build # Compile TypeScript +npm start # Run compiled output +``` diff --git a/packages/mcp-server/package-lock.json b/packages/mcp-server/package-lock.json new file mode 100644 index 00000000..3d8daae7 --- /dev/null +++ b/packages/mcp-server/package-lock.json @@ -0,0 +1,1718 @@ +{ + "name": "gemini-agents", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gemini-agents", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", + "zod": "^3.25.0", + "zod-to-json-schema": "^3.24.5" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.0.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", + "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", + "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json new file mode 100644 index 00000000..761d72ca --- /dev/null +++ b/packages/mcp-server/package.json @@ -0,0 +1,21 @@ +{ + "name": "gemini-agents", + "version": "1.0.0", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsx src/index.ts", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.27.1", + "zod": "^3.25.0", + "zod-to-json-schema": "^3.24.5" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.0.0", + "typescript": "^5.7.0" + } +} diff --git a/packages/mcp-server/skills-lock.json b/packages/mcp-server/skills-lock.json new file mode 100644 index 00000000..70306b0c --- /dev/null +++ b/packages/mcp-server/skills-lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "skills": { + "gemini-candles": { + "source": "/Users/steven.osborn/dev/gemini-agents", + "sourceType": "local", + "computedHash": "8e21547cb5b64ce987f37088b23c9941ae0ead3013c861c98286aa2a360f50ba" + } + } +} diff --git a/packages/mcp-server/src/auth/signer.ts b/packages/mcp-server/src/auth/signer.ts new file mode 100644 index 00000000..400f7096 --- /dev/null +++ b/packages/mcp-server/src/auth/signer.ts @@ -0,0 +1,24 @@ +import { createHmac } from 'crypto'; + +export function buildSignedHeaders( + endpoint: string, + body: Record, + apiKey: string, + apiSecret: string +): Record { + const nonce = Math.floor(Date.now() / 1000); + const payload = JSON.stringify({ request: endpoint, nonce, ...body }); + const encodedPayload = Buffer.from(payload).toString('base64'); + const signature = createHmac('sha384', apiSecret) + .update(encodedPayload) + .digest('hex'); + + return { + 'Content-Type': 'text/plain', + 'Content-Length': '0', + 'X-GEMINI-APIKEY': apiKey, + 'X-GEMINI-PAYLOAD': encodedPayload, + 'X-GEMINI-SIGNATURE': signature, + 'Cache-Control': 'no-cache', + }; +} diff --git a/packages/mcp-server/src/client/http.ts b/packages/mcp-server/src/client/http.ts new file mode 100644 index 00000000..5bb38441 --- /dev/null +++ b/packages/mcp-server/src/client/http.ts @@ -0,0 +1,48 @@ +import { config } from '../config.js'; +import { buildSignedHeaders } from '../auth/signer.js'; + +const REQUEST_TIMEOUT_MS = 30_000; + +export class GeminiHttpClient { + private baseUrl: string; + private apiKey: string; + private apiSecret: string; + + constructor() { + this.baseUrl = config.baseUrl; + this.apiKey = config.apiKey; + this.apiSecret = config.apiSecret; + } + + async publicGet(endpoint: string, params?: Record): Promise { + const url = new URL(`${this.baseUrl}${endpoint}`); + if (params) { + for (const [k, v] of Object.entries(params)) { + url.searchParams.set(k, v); + } + } + const res = await fetch(url.toString(), { + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Gemini API error ${res.status}: ${text}`); + } + return res.json() as Promise; + } + + async authenticatedPost(endpoint: string, body: Record = {}): Promise { + const fullBody = config.account ? { ...body, account: config.account } : body; + const headers = buildSignedHeaders(endpoint, fullBody, this.apiKey, this.apiSecret); + const res = await fetch(`${this.baseUrl}${endpoint}`, { + method: 'POST', + headers, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Gemini API error ${res.status}: ${text}`); + } + return res.json() as Promise; + } +} diff --git a/packages/mcp-server/src/config.ts b/packages/mcp-server/src/config.ts new file mode 100644 index 00000000..08f10f9d --- /dev/null +++ b/packages/mcp-server/src/config.ts @@ -0,0 +1,6 @@ +export const config = { + apiKey: process.env.GEMINI_API_KEY ?? '', + apiSecret: process.env.GEMINI_API_SECRET ?? '', + baseUrl: process.env.GEMINI_API_BASE_URL ?? 'https://api.gemini.com', + account: process.env.GEMINI_ACCOUNT ?? '', +}; diff --git a/packages/mcp-server/src/datasources/account.ts b/packages/mcp-server/src/datasources/account.ts new file mode 100644 index 00000000..f7c77866 --- /dev/null +++ b/packages/mcp-server/src/datasources/account.ts @@ -0,0 +1,37 @@ +import type { GeminiHttpClient } from '../client/http.js'; +import type { Account, AccountDetail, ApprovedAddress } from '../types/account.js'; + +export async function getAccount(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/account'); +} + +export async function createAccount( + client: GeminiHttpClient, + name: string, + type: string +): Promise> { + return client.authenticatedPost>('/v1/account/create', { name, type }); +} + +export async function getAccounts(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/account/list'); +} + +export async function getRoles(client: GeminiHttpClient): Promise> { + return client.authenticatedPost>('/v1/roles'); +} + +export async function getApprovedAddresses( + client: GeminiHttpClient, + network: string +): Promise { + return client.authenticatedPost(`/v1/approvedAddresses/account/${network}`); +} + +export async function sessionHeartbeat(client: GeminiHttpClient): Promise> { + return client.authenticatedPost>('/v1/heartbeat'); +} + +export async function revokeOAuthToken(client: GeminiHttpClient): Promise> { + return client.authenticatedPost>('/v1/oauth/token/revoke'); +} diff --git a/packages/mcp-server/src/datasources/funds.ts b/packages/mcp-server/src/datasources/funds.ts new file mode 100644 index 00000000..b9b63cb2 --- /dev/null +++ b/packages/mcp-server/src/datasources/funds.ts @@ -0,0 +1,91 @@ +import type { GeminiHttpClient } from '../client/http.js'; +import type { Balance, Transfer, DepositAddress, WithdrawalResult, InternalTransferResult, GasFeeEstimate } from '../types/funds.js'; + +export async function getBalances(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/balances'); +} + +export async function getNotionalBalances(client: GeminiHttpClient, currency: string): Promise[]> { + return client.authenticatedPost[]>(`/v1/notionalbalances/${currency}`); +} + +export async function getTransfers( + client: GeminiHttpClient, + limitTransfers?: number, + currency?: string +): Promise { + const body: Record = {}; + if (limitTransfers !== undefined) body['limit_transfers'] = limitTransfers; + if (currency !== undefined) body['currency'] = currency; + return client.authenticatedPost('/v1/transfers', body); +} + +export async function getDepositAddresses( + client: GeminiHttpClient, + network: string +): Promise { + return client.authenticatedPost(`/v1/addresses/${network}`); +} + +export async function getNewDepositAddress( + client: GeminiHttpClient, + network: string, + label?: string +): Promise { + const body: Record = {}; + if (label) body['label'] = label; + return client.authenticatedPost(`/v1/deposit/${network}/newAddress`, body); +} + +export async function internalTransfer( + client: GeminiHttpClient, + currency: string, + sourceAccount: string, + targetAccount: string, + amount: string +): Promise { + return client.authenticatedPost('/v1/account/transfer', { + currency, + sourceAccount, + targetAccount, + amount, + }); +} + +export async function addBank(client: GeminiHttpClient, body: Record): Promise> { + return client.authenticatedPost>('/v1/payments/addbank', body); +} + +export async function getBankPaymentMethods(client: GeminiHttpClient): Promise[]> { + return client.authenticatedPost[]>('/v1/payments/methods'); +} + +export async function cryptoWithdrawal( + client: GeminiHttpClient, + currency: string, + address: string, + amount: string, + memo?: string +): Promise { + const body: Record = { address, amount }; + if (memo) body['memo'] = memo; + return client.authenticatedPost(`/v1/withdraw/${currency}`, body); +} + +export async function getGasFeeEstimate( + client: GeminiHttpClient, + currency: string, + address: string, + amount: string +): Promise { + return client.authenticatedPost(`/v1/withdraw/${currency.toLowerCase()}/feeEstimate`, { address, amount }); +} + +export async function fiatWithdrawal( + client: GeminiHttpClient, + accountId: string, + amount: string, + currency: string +): Promise> { + return client.authenticatedPost>('/v1/withdraw/usd', { accountId, amount, currency }); +} diff --git a/packages/mcp-server/src/datasources/margin.ts b/packages/mcp-server/src/datasources/margin.ts new file mode 100644 index 00000000..54517e03 --- /dev/null +++ b/packages/mcp-server/src/datasources/margin.ts @@ -0,0 +1,52 @@ +import type { GeminiHttpClient } from '../client/http.js'; +import type { MarginAccount, MarginPreview, FundingPayment, ClearingOrder } from '../types/margin.js'; + +export async function getMarginAccount(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/margin/account'); +} + +export async function getMarginPreview( + client: GeminiHttpClient, + symbol: string, + side: string, + quantity: string, + price: string +): Promise { + return client.authenticatedPost('/v1/margin/preview', { symbol, side, quantity, price }); +} + +export async function getOpenPositions(client: GeminiHttpClient): Promise[]> { + return client.authenticatedPost[]>('/v1/positions'); +} + +export async function getFundingPayments( + client: GeminiHttpClient, + since?: number, + until?: number +): Promise { + const body: Record = {}; + if (since !== undefined) body['since'] = since; + if (until !== undefined) body['until'] = until; + return client.authenticatedPost('/v1/fundingpayments', body); +} + +export async function clearingNewOrder( + client: GeminiHttpClient, + body: Record +): Promise { + return client.authenticatedPost('/v1/clearing/new', body); +} + +export async function clearingBrokerNewOrder( + client: GeminiHttpClient, + body: Record +): Promise { + return client.authenticatedPost('/v1/clearing/broker/new', body); +} + +export async function clearingOrderStatus( + client: GeminiHttpClient, + clearingId: string +): Promise { + return client.authenticatedPost('/v1/clearing/status', { clearing_id: clearingId }); +} diff --git a/packages/mcp-server/src/datasources/market.ts b/packages/mcp-server/src/datasources/market.ts new file mode 100644 index 00000000..4e2bc69f --- /dev/null +++ b/packages/mcp-server/src/datasources/market.ts @@ -0,0 +1,89 @@ +import type { GeminiHttpClient } from '../client/http.js'; +import type { TickerV2, Trade, OrderBook, PriceFeed, FundingAmount } from '../types/market.js'; +import { CandleTimeFrame } from '../types/market.js'; + +export async function getSymbols(client: GeminiHttpClient): Promise { + return client.publicGet('/v1/symbols'); +} + +export async function getSymbolDetails(client: GeminiHttpClient, symbol: string): Promise> { + return client.publicGet>(`/v1/symbols/details/${symbol}`); +} + +export async function getNetworkCodes(client: GeminiHttpClient, token: string): Promise> { + return client.publicGet>(`/v1/network/${token}`); +} + +export async function getTicker(client: GeminiHttpClient, symbol: string): Promise { + return client.publicGet(`/v2/ticker/${symbol}`); +} + +export async function getTickerV1(client: GeminiHttpClient, symbol: string): Promise> { + return client.publicGet>(`/v1/pubticker/${symbol}`); +} + +export async function getCandles( + client: GeminiHttpClient, + symbol: string, + timeFrame: CandleTimeFrame +): Promise { + return client.publicGet(`/v2/candles/${symbol}/${timeFrame}`); +} + +export async function getDerivativeCandles( + client: GeminiHttpClient, + symbol: string, + timeFrame: CandleTimeFrame +): Promise { + return client.publicGet(`/v2/derivatives/candles/${symbol}/${timeFrame}`); +} + +export async function getFeePromos(client: GeminiHttpClient): Promise> { + return client.publicGet>('/v1/feepromos'); +} + +export async function getRecentTrades( + client: GeminiHttpClient, + symbol: string, + limitTrades?: number, + since?: number +): Promise { + const params: Record = {}; + if (limitTrades !== undefined) params['limit_trades'] = String(limitTrades); + if (since !== undefined) params['since'] = String(since); + return client.publicGet(`/v1/trades/${symbol}`, params); +} + +export async function getOrderBook( + client: GeminiHttpClient, + symbol: string, + limitBids?: number, + limitAsks?: number +): Promise { + const params: Record = {}; + if (limitBids !== undefined) params['limit_bids'] = String(limitBids); + if (limitAsks !== undefined) params['limit_asks'] = String(limitAsks); + return client.publicGet(`/v1/book/${symbol}`, params); +} + +export async function getAuctionHistory( + client: GeminiHttpClient, + symbol: string, + limitAuctions?: number +): Promise> { + const params: Record = {}; + if (limitAuctions !== undefined) params['limit_auction_results'] = String(limitAuctions); + return client.publicGet>(`/v1/auction/${symbol}/history`, params); +} + +export async function getPriceFeed(client: GeminiHttpClient): Promise { + return client.publicGet('/v1/pricefeed'); +} + +export async function getFundingAmounts(client: GeminiHttpClient): Promise { + return client.publicGet('/v1/fundingamounts'); +} + +export async function getCurrentFundingRate(client: GeminiHttpClient, symbol: string): Promise> { + return client.publicGet>(`/v1/perpetuals/fundingrates/${symbol}`); +} diff --git a/packages/mcp-server/src/datasources/orders.ts b/packages/mcp-server/src/datasources/orders.ts new file mode 100644 index 00000000..ce322676 --- /dev/null +++ b/packages/mcp-server/src/datasources/orders.ts @@ -0,0 +1,83 @@ +import type { GeminiHttpClient } from '../client/http.js'; +import type { Order, MyTrade, TradeVolume, NotionalVolume } from '../types/orders.js'; +import { getTicker } from './market.js'; + +export async function newOrder( + client: GeminiHttpClient, + symbol: string, + amount: string, + price: string, + side: string, + type: string, + options?: string[], + clientOrderId?: string +): Promise { + const body: Record = { symbol, amount, price, side, type }; + if (options) body['options'] = options; + if (clientOrderId) body['client_order_id'] = clientOrderId; + + try { + return await client.authenticatedPost('/v1/order/new', body); + } catch (err: unknown) { + const isInvalidOrderType = + err instanceof Error && err.message.includes('InvalidOrderType'); + const isMarketOrder = + typeof type === 'string' && type.toLowerCase().includes('market'); + + if (isInvalidOrderType && isMarketOrder) { + const ticker = await getTicker(client, symbol); + const fallbackPrice = side === 'buy' ? ticker.ask : ticker.bid; + const fallbackBody: Record = { + symbol, + amount, + price: fallbackPrice, + side, + type: 'exchange limit', + }; + if (clientOrderId) fallbackBody['client_order_id'] = clientOrderId; + return client.authenticatedPost('/v1/order/new', fallbackBody); + } + + throw err; + } +} + +export async function cancelOrder(client: GeminiHttpClient, orderId: string): Promise { + return client.authenticatedPost('/v1/order/cancel', { order_id: orderId }); +} + +export async function cancelAllSessionOrders(client: GeminiHttpClient): Promise> { + return client.authenticatedPost>('/v1/order/cancel/session'); +} + +export async function cancelAllActiveOrders(client: GeminiHttpClient): Promise> { + return client.authenticatedPost>('/v1/order/cancel/all'); +} + +export async function getOrderStatus(client: GeminiHttpClient, orderId: string): Promise { + return client.authenticatedPost('/v1/order/status', { order_id: orderId }); +} + +export async function getActiveOrders(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/orders'); +} + +export async function getMyTrades( + client: GeminiHttpClient, + symbol: string, + limitTrades?: number, + timestamp?: number +): Promise { + const body: Record = { symbol }; + if (limitTrades !== undefined) body['limit_trades'] = limitTrades; + if (timestamp !== undefined) body['timestamp'] = timestamp; + return client.authenticatedPost('/v1/mytrades', body); +} + +export async function getTradeVolume(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/tradevolume'); +} + +export async function getNotionalVolume(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/notionalvolume'); +} diff --git a/packages/mcp-server/src/datasources/staking.ts b/packages/mcp-server/src/datasources/staking.ts new file mode 100644 index 00000000..f53a758f --- /dev/null +++ b/packages/mcp-server/src/datasources/staking.ts @@ -0,0 +1,32 @@ +import type { GeminiHttpClient } from '../client/http.js'; +import type { StakingBalance, StakingHistory, StakingRate, StakingResult } from '../types/staking.js'; + +export async function getStakingBalances(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/balances/staking'); +} + +export async function getStakingHistory(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/staking/history'); +} + +export async function getStakingRates(client: GeminiHttpClient): Promise { + return client.authenticatedPost('/v1/staking/rates'); +} + +export async function stake( + client: GeminiHttpClient, + currency: string, + amount: string, + providerId: string +): Promise { + return client.authenticatedPost('/v1/staking/stake', { currency, amount, providerId }); +} + +export async function unstake( + client: GeminiHttpClient, + currency: string, + amount: string, + providerId: string +): Promise { + return client.authenticatedPost('/v1/staking/unstake', { currency, amount, providerId }); +} diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts new file mode 100644 index 00000000..b6326ee6 --- /dev/null +++ b/packages/mcp-server/src/index.ts @@ -0,0 +1,35 @@ +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { createServer } from './server.js'; +import { config } from './config.js'; + +function validateConfig(): void { + if (!config.apiKey) { + console.error('Error: GEMINI_API_KEY is not set.'); + console.error('Set it in your MCP client config under "env". See README for details.'); + process.exit(1); + } + if (!config.apiSecret) { + console.error('Error: GEMINI_API_SECRET is not set.'); + console.error('Set it in your MCP client config under "env". See README for details.'); + process.exit(1); + } + if (config.apiKey.startsWith('master-') && !config.account) { + console.error('Error: GEMINI_ACCOUNT is required when using a Master API key.'); + console.error('Master API keys must specify which account to use (e.g. "primary").'); + console.error('Add GEMINI_ACCOUNT to your MCP client config under "env". See README for details.'); + process.exit(1); + } +} + +async function main(): Promise { + validateConfig(); + const server = createServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); + // Server is running, listening on stdio +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts new file mode 100644 index 00000000..ada53d5f --- /dev/null +++ b/packages/mcp-server/src/server.ts @@ -0,0 +1,66 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { + ListToolsRequestSchema, + CallToolRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { GeminiHttpClient } from './client/http.js'; +import { + createMarketTools, + createOrderTools, + createFundTools, + createAccountTools, + createMarginTools, + createStakingTools, +} from './tools/index.js'; +import type { ToolDefinition } from './tools/index.js'; + +export function createServer(): Server { + const server = new Server( + { name: 'gemini-agents', version: '1.0.0' }, + { capabilities: { tools: {} } } + ); + + const client = new GeminiHttpClient(); + + const allTools: ToolDefinition[] = [ + ...createMarketTools(client), + ...createOrderTools(client), + ...createFundTools(client), + ...createAccountTools(client), + ...createMarginTools(client), + ...createStakingTools(client), + ]; + + const toolMap = new Map(allTools.map((t) => [t.name, t])); + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: allTools.map((t) => ({ + name: t.name, + description: t.description, + inputSchema: zodToJsonSchema(t.inputSchema), + })), + })); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const tool = toolMap.get(request.params.name); + if (!tool) { + return { + content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], + isError: true, + }; + } + + const parsed = tool.inputSchema.safeParse(request.params.arguments ?? {}); + if (!parsed.success) { + return { + content: [{ type: 'text', text: `Invalid arguments: ${parsed.error.message}` }], + isError: true, + }; + } + + return tool.handler(parsed.data); + }); + + return server; +} diff --git a/packages/mcp-server/src/tools/account.ts b/packages/mcp-server/src/tools/account.ts new file mode 100644 index 00000000..7be78c1c --- /dev/null +++ b/packages/mcp-server/src/tools/account.ts @@ -0,0 +1,57 @@ +import { z } from 'zod'; +import type { GeminiHttpClient } from '../client/http.js'; +import type { ToolDefinition } from './index.js'; +import { wrapHandler } from './index.js'; +import * as account from '../datasources/account.js'; + +export function createAccountTools(client: GeminiHttpClient): ToolDefinition[] { + return [ + { + name: 'gemini_get_account', + description: 'Get account details', + inputSchema: z.object({}), + handler: wrapHandler(() => account.getAccount(client)), + }, + { + name: 'gemini_create_account', + description: 'Create a new sub-account', + inputSchema: z.object({ + name: z.string().describe('Account name'), + type: z.string().describe('Account type: exchange or custody'), + }), + handler: wrapHandler(({ name, type }: { name: string; type: string }) => + account.createAccount(client, name, type) + ), + }, + { + name: 'gemini_get_accounts', + description: 'List all accounts', + inputSchema: z.object({}), + handler: wrapHandler(() => account.getAccounts(client)), + }, + { + name: 'gemini_get_roles', + description: 'Get API key roles and permissions', + inputSchema: z.object({}), + handler: wrapHandler(() => account.getRoles(client)), + }, + { + name: 'gemini_get_approved_addresses', + description: 'Get approved withdrawal addresses for a network', + inputSchema: z.object({ network: z.string().describe('Network name') }), + handler: wrapHandler(({ network }: { network: string }) => account.getApprovedAddresses(client, network)), + }, + { + name: 'gemini_session_heartbeat', + description: 'Send a heartbeat to keep the session alive', + inputSchema: z.object({}), + handler: wrapHandler(() => account.sessionHeartbeat(client)), + }, + { + name: 'gemini_revoke_oauth_token', + description: 'Revoke the current OAuth token', + inputSchema: z.object({}), + handler: wrapHandler(() => account.revokeOAuthToken(client)), + }, + ]; +} diff --git a/packages/mcp-server/src/tools/funds.ts b/packages/mcp-server/src/tools/funds.ts new file mode 100644 index 00000000..cb44d6c1 --- /dev/null +++ b/packages/mcp-server/src/tools/funds.ts @@ -0,0 +1,117 @@ +import { z } from 'zod'; +import type { GeminiHttpClient } from '../client/http.js'; +import type { ToolDefinition } from './index.js'; +import { wrapHandler } from './index.js'; +import * as funds from '../datasources/funds.js'; + +export function createFundTools(client: GeminiHttpClient): ToolDefinition[] { + return [ + { + name: 'gemini_get_balances', + description: 'Get account balances for all currencies', + inputSchema: z.object({}), + handler: wrapHandler(() => funds.getBalances(client)), + }, + { + name: 'gemini_get_notional_balances', + description: 'Get account balances converted to a notional currency', + inputSchema: z.object({ currency: z.string().describe('Notional currency e.g. usd') }), + handler: wrapHandler(({ currency }: { currency: string }) => funds.getNotionalBalances(client, currency)), + }, + { + name: 'gemini_get_transfers', + description: 'Get transfer history', + inputSchema: z.object({ + limitTransfers: z.number().optional().describe('Maximum number of transfers to return'), + currency: z.string().optional().describe('Filter by currency'), + }), + handler: wrapHandler(({ limitTransfers, currency }: { limitTransfers?: number; currency?: string }) => + funds.getTransfers(client, limitTransfers, currency) + ), + }, + { + name: 'gemini_get_deposit_addresses', + description: 'Get deposit addresses for a network', + inputSchema: z.object({ network: z.string().describe('Network name e.g. bitcoin, ethereum') }), + handler: wrapHandler(({ network }: { network: string }) => funds.getDepositAddresses(client, network)), + }, + { + name: 'gemini_get_new_deposit_address', + description: 'Generate a new deposit address for a network', + inputSchema: z.object({ + network: z.string().describe('Network name'), + label: z.string().optional().describe('Label for the address'), + }), + handler: wrapHandler(({ network, label }: { network: string; label?: string }) => + funds.getNewDepositAddress(client, network, label) + ), + }, + { + name: 'gemini_withdraw', + description: 'Withdraw cryptocurrency to an external address', + inputSchema: z.object({ + currency: z.string().describe('Currency to withdraw'), + address: z.string().describe('Destination address'), + amount: z.string().describe('Amount to withdraw'), + memo: z.string().optional().describe('Memo for the transaction'), + }), + handler: wrapHandler(({ currency, address, amount, memo }: { + currency: string; address: string; amount: string; memo?: string; + }) => funds.cryptoWithdrawal(client, currency, address, amount, memo)), + }, + { + name: 'gemini_internal_transfer', + description: 'Transfer funds between Gemini accounts', + inputSchema: z.object({ + currency: z.string().describe('Currency to transfer'), + sourceAccount: z.string().describe('Source account name'), + targetAccount: z.string().describe('Target account name'), + amount: z.string().describe('Amount to transfer'), + }), + handler: wrapHandler(({ currency, sourceAccount, targetAccount, amount }: { + currency: string; sourceAccount: string; targetAccount: string; amount: string; + }) => funds.internalTransfer(client, currency, sourceAccount, targetAccount, amount)), + }, + { + name: 'gemini_add_bank', + description: 'Add a bank account for fiat transfers', + inputSchema: z.object({ + accountNumber: z.string().describe('Bank account number'), + routing: z.string().describe('Routing number'), + type: z.string().describe('Account type: checking or savings'), + name: z.string().describe('Account holder name'), + }), + handler: wrapHandler((body: Record) => funds.addBank(client, body)), + }, + { + name: 'gemini_get_bank_payment_methods', + description: 'Get available bank payment methods', + inputSchema: z.object({}), + handler: wrapHandler(() => funds.getBankPaymentMethods(client)), + }, + { + name: 'gemini_get_gas_fee_estimate', + description: 'Estimate the gas fee for a cryptocurrency withdrawal, including fee-free withdrawal allowance', + inputSchema: z.object({ + currency: z.string().describe('Cryptocurrency code e.g. eth, btc'), + address: z.string().describe('Destination address'), + amount: z.string().describe('Amount to withdraw'), + }), + handler: wrapHandler(({ currency, address, amount }: { currency: string; address: string; amount: string }) => + funds.getGasFeeEstimate(client, currency, address, amount) + ), + }, + { + name: 'gemini_fiat_withdrawal', + description: 'Withdraw fiat currency to a bank account', + inputSchema: z.object({ + accountId: z.string().describe('Bank account ID'), + amount: z.string().describe('Amount to withdraw'), + currency: z.string().describe('Currency e.g. USD'), + }), + handler: wrapHandler(({ accountId, amount, currency }: { accountId: string; amount: string; currency: string }) => + funds.fiatWithdrawal(client, accountId, amount, currency) + ), + }, + ]; +} diff --git a/packages/mcp-server/src/tools/index.ts b/packages/mcp-server/src/tools/index.ts new file mode 100644 index 00000000..06845b73 --- /dev/null +++ b/packages/mcp-server/src/tools/index.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; + +export interface ToolDefinition { + name: string; + description: string; + inputSchema: S; + handler: (args: z.infer) => Promise; +} + +export function wrapHandler( + handler: (args: z.infer) => Promise +): (args: z.infer) => Promise { + return async (args: z.infer): Promise => { + try { + const result = await handler(args); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { + content: [{ type: 'text', text: `Error: ${message}` }], + isError: true, + }; + } + }; +} + +export { createMarketTools } from './market.js'; +export { createOrderTools } from './orders.js'; +export { createFundTools } from './funds.js'; +export { createAccountTools } from './account.js'; +export { createMarginTools } from './margin.js'; +export { createStakingTools } from './staking.js'; diff --git a/packages/mcp-server/src/tools/margin.ts b/packages/mcp-server/src/tools/margin.ts new file mode 100644 index 00000000..b3c7af75 --- /dev/null +++ b/packages/mcp-server/src/tools/margin.ts @@ -0,0 +1,79 @@ +import { z } from 'zod'; +import type { GeminiHttpClient } from '../client/http.js'; +import type { ToolDefinition } from './index.js'; +import { wrapHandler } from './index.js'; +import * as margin from '../datasources/margin.js'; + +export function createMarginTools(client: GeminiHttpClient): ToolDefinition[] { + return [ + { + name: 'gemini_get_margin_account', + description: 'Get margin account information', + inputSchema: z.object({}), + handler: wrapHandler(() => margin.getMarginAccount(client)), + }, + { + name: 'gemini_get_margin_preview', + description: 'Preview a margin order', + inputSchema: z.object({ + symbol: z.string().describe('Trading symbol'), + side: z.enum(['buy', 'sell']).describe('Order side'), + quantity: z.string().describe('Order quantity'), + price: z.string().describe('Order price'), + }), + handler: wrapHandler(({ symbol, side, quantity, price }: { + symbol: string; side: 'buy' | 'sell'; quantity: string; price: string; + }) => margin.getMarginPreview(client, symbol, side, quantity, price)), + }, + { + name: 'gemini_get_open_positions', + description: 'Get open perpetual contract positions', + inputSchema: z.object({}), + handler: wrapHandler(() => margin.getOpenPositions(client)), + }, + { + name: 'gemini_get_funding_payments', + description: 'Get funding payment history for perpetual contracts', + inputSchema: z.object({ + since: z.number().optional().describe('Start timestamp (ms)'), + until: z.number().optional().describe('End timestamp (ms)'), + }), + handler: wrapHandler(({ since, until }: { since?: number; until?: number }) => + margin.getFundingPayments(client, since, until) + ), + }, + { + name: 'gemini_clearing_new_order', + description: 'Place a new clearing order', + inputSchema: z.object({ + symbol: z.string().describe('Symbol'), + amount: z.string().describe('Amount'), + price: z.string().describe('Price'), + side: z.enum(['buy', 'sell']).describe('Side'), + type: z.string().describe('Order type'), + clearingId: z.string().optional().describe('Clearing ID'), + }), + handler: wrapHandler((body: Record) => margin.clearingNewOrder(client, body)), + }, + { + name: 'gemini_clearing_broker_new_order', + description: 'Place a new clearing broker order', + inputSchema: z.object({ + symbol: z.string().describe('Symbol'), + amount: z.string().describe('Amount'), + price: z.string().describe('Price'), + side: z.enum(['buy', 'sell']).describe('Side'), + type: z.string().describe('Order type'), + }), + handler: wrapHandler((body: Record) => margin.clearingBrokerNewOrder(client, body)), + }, + { + name: 'gemini_clearing_order_status', + description: 'Get the status of a clearing order', + inputSchema: z.object({ clearingId: z.string().describe('Clearing order ID') }), + handler: wrapHandler(({ clearingId }: { clearingId: string }) => + margin.clearingOrderStatus(client, clearingId) + ), + }, + ]; +} diff --git a/packages/mcp-server/src/tools/market.ts b/packages/mcp-server/src/tools/market.ts new file mode 100644 index 00000000..e00290fa --- /dev/null +++ b/packages/mcp-server/src/tools/market.ts @@ -0,0 +1,122 @@ +import { z } from 'zod'; +import type { GeminiHttpClient } from '../client/http.js'; +import type { ToolDefinition } from './index.js'; +import { wrapHandler } from './index.js'; +import { CandleTimeFrame } from '../types/market.js'; +import * as market from '../datasources/market.js'; + +export function createMarketTools(client: GeminiHttpClient): ToolDefinition[] { + return [ + { + name: 'gemini_get_symbols', + description: 'Get all available trading symbols on Gemini', + inputSchema: z.object({}), + handler: wrapHandler(() => market.getSymbols(client)), + }, + { + name: 'gemini_get_symbol_details', + description: 'Get details for a specific trading symbol', + inputSchema: z.object({ symbol: z.string().describe('Trading symbol e.g. btcusd') }), + handler: wrapHandler(({ symbol }: { symbol: string }) => market.getSymbolDetails(client, symbol)), + }, + { + name: 'gemini_get_ticker', + description: 'Get ticker information for a symbol (v2)', + inputSchema: z.object({ symbol: z.string().describe('Trading symbol e.g. btcusd') }), + handler: wrapHandler(({ symbol }: { symbol: string }) => market.getTicker(client, symbol)), + }, + { + name: 'gemini_get_ticker_v1', + description: 'Get ticker information for a symbol (v1)', + inputSchema: z.object({ symbol: z.string().describe('Trading symbol e.g. btcusd') }), + handler: wrapHandler(({ symbol }: { symbol: string }) => market.getTickerV1(client, symbol)), + }, + { + name: 'gemini_get_candles', + description: 'Get OHLC candle data for a symbol', + inputSchema: z.object({ + symbol: z.string().describe('Trading symbol e.g. btcusd'), + timeFrame: z.nativeEnum(CandleTimeFrame).describe('Time frame for candles'), + }), + handler: wrapHandler(({ symbol, timeFrame }: { symbol: string; timeFrame: CandleTimeFrame }) => + market.getCandles(client, symbol, timeFrame) + ), + }, + { + name: 'gemini_get_derivative_candles', + description: 'Get OHLC candle data for a derivative symbol', + inputSchema: z.object({ + symbol: z.string().describe('Derivative symbol'), + timeFrame: z.nativeEnum(CandleTimeFrame).describe('Time frame for candles'), + }), + handler: wrapHandler(({ symbol, timeFrame }: { symbol: string; timeFrame: CandleTimeFrame }) => + market.getDerivativeCandles(client, symbol, timeFrame) + ), + }, + { + name: 'gemini_get_fee_promos', + description: 'Get current fee promotions', + inputSchema: z.object({}), + handler: wrapHandler(() => market.getFeePromos(client)), + }, + { + name: 'gemini_get_recent_trades', + description: 'Get recent trades for a symbol', + inputSchema: z.object({ + symbol: z.string().describe('Trading symbol'), + limitTrades: z.number().optional().describe('Maximum number of trades to return'), + since: z.number().optional().describe('Only return trades after this timestamp (ms)'), + }), + handler: wrapHandler(({ symbol, limitTrades, since }: { symbol: string; limitTrades?: number; since?: number }) => + market.getRecentTrades(client, symbol, limitTrades, since) + ), + }, + { + name: 'gemini_get_order_book', + description: 'Get current order book for a symbol', + inputSchema: z.object({ + symbol: z.string().describe('Trading symbol'), + limitBids: z.number().optional().describe('Maximum number of bids to return'), + limitAsks: z.number().optional().describe('Maximum number of asks to return'), + }), + handler: wrapHandler(({ symbol, limitBids, limitAsks }: { symbol: string; limitBids?: number; limitAsks?: number }) => + market.getOrderBook(client, symbol, limitBids, limitAsks) + ), + }, + { + name: 'gemini_get_auction_history', + description: 'Get auction history for a symbol', + inputSchema: z.object({ + symbol: z.string().describe('Trading symbol'), + limitAuctions: z.number().optional().describe('Maximum number of auctions to return'), + }), + handler: wrapHandler(({ symbol, limitAuctions }: { symbol: string; limitAuctions?: number }) => + market.getAuctionHistory(client, symbol, limitAuctions) + ), + }, + { + name: 'gemini_get_price_feed', + description: 'Get price feed for all symbols', + inputSchema: z.object({}), + handler: wrapHandler(() => market.getPriceFeed(client)), + }, + { + name: 'gemini_get_funding_amounts', + description: 'Get current funding amounts for perpetual contracts', + inputSchema: z.object({}), + handler: wrapHandler(() => market.getFundingAmounts(client)), + }, + { + name: 'gemini_get_current_funding_rate', + description: 'Get the current funding rate for a perpetual symbol', + inputSchema: z.object({ symbol: z.string().describe('Perpetual symbol') }), + handler: wrapHandler(({ symbol }: { symbol: string }) => market.getCurrentFundingRate(client, symbol)), + }, + { + name: 'gemini_get_network_codes', + description: 'Get network codes for token deposits', + inputSchema: z.object({ token: z.string().describe('Token symbol e.g. gusd, usdc, btc') }), + handler: wrapHandler(({ token }: { token: string }) => market.getNetworkCodes(client, token)), + }, + ]; +} diff --git a/packages/mcp-server/src/tools/orders.ts b/packages/mcp-server/src/tools/orders.ts new file mode 100644 index 00000000..8e8e4af6 --- /dev/null +++ b/packages/mcp-server/src/tools/orders.ts @@ -0,0 +1,81 @@ +import { z } from 'zod'; +import type { GeminiHttpClient } from '../client/http.js'; +import type { ToolDefinition } from './index.js'; +import { wrapHandler } from './index.js'; +import * as orders from '../datasources/orders.js'; + +export function createOrderTools(client: GeminiHttpClient): ToolDefinition[] { + return [ + { + name: 'gemini_new_order', + description: 'Place a new order. Note: "exchange market" order type is not supported for all symbols (e.g. ETHUSD). If placing a market-style order, prefer "exchange limit" with the current ask price (for buys) or bid price (for sells) to achieve immediate fill. The handler will automatically fall back to a limit order at the current market price if a market order is rejected with InvalidOrderType.', + inputSchema: z.object({ + symbol: z.string().describe('Trading symbol e.g. btcusd'), + amount: z.string().describe('Amount to buy or sell'), + price: z.string().describe('Price per unit'), + side: z.enum(['buy', 'sell']).describe('Order side'), + type: z.string().describe('Order type e.g. exchange limit'), + options: z.array(z.string()).optional().describe('Order options'), + clientOrderId: z.string().optional().describe('Client-specified order ID'), + }), + handler: wrapHandler(({ symbol, amount, price, side, type, options, clientOrderId }: { + symbol: string; amount: string; price: string; side: 'buy' | 'sell'; + type: string; options?: string[]; clientOrderId?: string; + }) => orders.newOrder(client, symbol, amount, price, side, type, options, clientOrderId)), + }, + { + name: 'gemini_cancel_order', + description: 'Cancel an active order', + inputSchema: z.object({ orderId: z.string().describe('Order ID to cancel') }), + handler: wrapHandler(({ orderId }: { orderId: string }) => orders.cancelOrder(client, orderId)), + }, + { + name: 'gemini_cancel_all_session_orders', + description: 'Cancel all orders in the current session', + inputSchema: z.object({}), + handler: wrapHandler(() => orders.cancelAllSessionOrders(client)), + }, + { + name: 'gemini_cancel_all_active_orders', + description: 'Cancel all active orders', + inputSchema: z.object({}), + handler: wrapHandler(() => orders.cancelAllActiveOrders(client)), + }, + { + name: 'gemini_get_order_status', + description: 'Get the status of an order', + inputSchema: z.object({ orderId: z.string().describe('Order ID') }), + handler: wrapHandler(({ orderId }: { orderId: string }) => orders.getOrderStatus(client, orderId)), + }, + { + name: 'gemini_get_active_orders', + description: 'Get all active orders', + inputSchema: z.object({}), + handler: wrapHandler(() => orders.getActiveOrders(client)), + }, + { + name: 'gemini_get_my_trades', + description: 'Get trade history for a symbol', + inputSchema: z.object({ + symbol: z.string().describe('Trading symbol'), + limitTrades: z.number().optional().describe('Maximum number of trades to return'), + timestamp: z.number().optional().describe('Only return trades after this timestamp'), + }), + handler: wrapHandler(({ symbol, limitTrades, timestamp }: { symbol: string; limitTrades?: number; timestamp?: number }) => + orders.getMyTrades(client, symbol, limitTrades, timestamp) + ), + }, + { + name: 'gemini_get_trade_volume', + description: 'Get trade volume statistics', + inputSchema: z.object({}), + handler: wrapHandler(() => orders.getTradeVolume(client)), + }, + { + name: 'gemini_get_notional_volume', + description: 'Get notional volume and fee tier information', + inputSchema: z.object({}), + handler: wrapHandler(() => orders.getNotionalVolume(client)), + }, + ]; +} diff --git a/packages/mcp-server/src/tools/staking.ts b/packages/mcp-server/src/tools/staking.ts new file mode 100644 index 00000000..ec953e7d --- /dev/null +++ b/packages/mcp-server/src/tools/staking.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; +import type { GeminiHttpClient } from '../client/http.js'; +import type { ToolDefinition } from './index.js'; +import { wrapHandler } from './index.js'; +import * as staking from '../datasources/staking.js'; + +export function createStakingTools(client: GeminiHttpClient): ToolDefinition[] { + return [ + { + name: 'gemini_get_staking_balances', + description: 'Get staking balances and rewards', + inputSchema: z.object({}), + handler: wrapHandler(() => staking.getStakingBalances(client)), + }, + { + name: 'gemini_get_staking_history', + description: 'Get staking event history (deposits, withdrawals, rewards)', + inputSchema: z.object({}), + handler: wrapHandler(() => staking.getStakingHistory(client)), + }, + { + name: 'gemini_get_staking_rates', + description: 'Get current staking interest rates', + inputSchema: z.object({}), + handler: wrapHandler(() => staking.getStakingRates(client)), + }, + { + name: 'gemini_stake', + description: 'Stake cryptocurrency', + inputSchema: z.object({ + currency: z.string().describe('Currency to stake'), + amount: z.string().describe('Amount to stake'), + providerId: z.string().describe('Staking provider ID'), + }), + handler: wrapHandler(({ currency, amount, providerId }: { currency: string; amount: string; providerId: string }) => + staking.stake(client, currency, amount, providerId) + ), + }, + { + name: 'gemini_unstake', + description: 'Unstake cryptocurrency', + inputSchema: z.object({ + currency: z.string().describe('Currency to unstake'), + amount: z.string().describe('Amount to unstake'), + providerId: z.string().describe('Staking provider ID'), + }), + handler: wrapHandler(({ currency, amount, providerId }: { currency: string; amount: string; providerId: string }) => + staking.unstake(client, currency, amount, providerId) + ), + }, + ]; +} diff --git a/packages/mcp-server/src/types/account.ts b/packages/mcp-server/src/types/account.ts new file mode 100644 index 00000000..e31422af --- /dev/null +++ b/packages/mcp-server/src/types/account.ts @@ -0,0 +1,43 @@ +export interface Account { + account: { + accountName: string; + shortName: string; + type: string; + created: string; + }; + users: Array<{ + name: string; + lastSignIn: string; + status: string; + countryCode: string; + isVerified: boolean; + }>; + memo_reference_code: string; +} + +export interface AccountDetail { + name: string; + session: string; + roles: string[]; + last_sign_in_time: string; + agreement: string; + created: string; + sandbox: boolean; + status: string; +} + +export interface Role { + role: string; + active: boolean; +} + +export interface ApprovedAddress { + label: string; + address: string; +} + +export interface OAuthToken { + scope: string; + expires_in: number; + token_type: string; +} diff --git a/packages/mcp-server/src/types/common.ts b/packages/mcp-server/src/types/common.ts new file mode 100644 index 00000000..4f412635 --- /dev/null +++ b/packages/mcp-server/src/types/common.ts @@ -0,0 +1,9 @@ +export type OrderSide = 'buy' | 'sell'; +export type OrderType = 'exchange limit' | 'exchange stop limit' | 'exchange market' | 'limit' | 'market'; +export type OrderStatus = 'live' | 'cancelled' | 'closed' | 'filling'; + +export interface ApiError { + result: 'error'; + reason: string; + message: string; +} diff --git a/packages/mcp-server/src/types/funds.ts b/packages/mcp-server/src/types/funds.ts new file mode 100644 index 00000000..bbbabbac --- /dev/null +++ b/packages/mcp-server/src/types/funds.ts @@ -0,0 +1,52 @@ +export interface Balance { + type: string; + currency: string; + amount: string; + available: string; + availableForWithdrawal: string; +} + +export interface Transfer { + type: string; + status: string; + timestampms: number; + eid: number; + currency: string; + amount: string; + method?: string; + txHash?: string; + outputIdx?: number; + destination?: string; + purpose?: string; +} + +export interface DepositAddress { + currency: string; + address: string; + timestamp: number; + label?: string; +} + +export interface WithdrawalResult { + address: string; + amount: string; + txHash?: string; + withdrawalId?: string; + message?: string; +} + +export interface GasFeeEstimate { + currency: string; + fee: string; + isOverride: boolean; + monthlyLimit: number; + monthlyRemaining: number; +} + +export interface InternalTransferResult { + sourceAccount: Record; + targetAccount: Record; + currency: string; + amount: string; + balances: Record; +} diff --git a/packages/mcp-server/src/types/margin.ts b/packages/mcp-server/src/types/margin.ts new file mode 100644 index 00000000..8fe5a7e2 --- /dev/null +++ b/packages/mcp-server/src/types/margin.ts @@ -0,0 +1,40 @@ +export interface MarginAccount { + amount: string; + available: string; + currency: string; + maintenanceMarginRequirementAmount: string; + percentUsed: number; + type: string; +} + +export interface MarginPreview { + amount: string; + currency: string; + available: string; + collateral: string; + haircut: string; + percentUsed: number; +} + +export interface FundingPayment { + assetName: string; + contractType: string; + eventTime: number; + fundingPaymentAmount: string; + fundingPaymentCurrency: string; + fundingRate: string; + fundingRateBps: number; + positionAmount: string; + positionAmountCurrency: string; + positionSide: string; +} + +export interface ClearingOrder { + order_id: string; + symbol: string; + price: string; + amount: string; + side: string; + clearing_id: string; + status: string; +} diff --git a/packages/mcp-server/src/types/market.ts b/packages/mcp-server/src/types/market.ts new file mode 100644 index 00000000..ee4f62ac --- /dev/null +++ b/packages/mcp-server/src/types/market.ts @@ -0,0 +1,63 @@ +export enum CandleTimeFrame { + OneMinute = '1m', + FiveMinutes = '5m', + FifteenMinutes = '15m', + ThirtyMinutes = '30m', + OneHour = '1hr', + SixHours = '6hr', + OneDay = '1day', +} + +export interface TickerV2 { + symbol: string; + open: string; + high: string; + low: string; + close: string; + changes: string[]; + bid: string; + ask: string; +} + +export interface Trade { + timestamp: number; + timestampms: number; + tid: number; + price: string; + amount: string; + exchange: string; + type: string; +} + +export interface OrderBookEntry { + price: string; + amount: string; +} + +export interface OrderBook { + bids: OrderBookEntry[]; + asks: OrderBookEntry[]; +} + +export interface AuctionStatus { + closed_until_ms: number; + last_auction_eid: number; + last_auction_price: string; + last_auction_quantity: string; + last_highest_bid_price: string; + last_lowest_ask_price: string; + next_auction_ms: number; + next_update_ms: number; +} + +export interface PriceFeed { + pair: string; + price: string; + percentChange24h: string; +} + +export interface FundingAmount { + symbol: string; + fundingDateTime: string; + fundingRate: string; +} diff --git a/packages/mcp-server/src/types/orders.ts b/packages/mcp-server/src/types/orders.ts new file mode 100644 index 00000000..c77f66f3 --- /dev/null +++ b/packages/mcp-server/src/types/orders.ts @@ -0,0 +1,88 @@ +import type { OrderSide } from './common.js'; + +export type { OrderSide }; + +export interface Order { + order_id: string; + id: string; + symbol: string; + exchange: string; + avg_execution_price: string; + side: OrderSide; + type: string; + timestamp: string; + timestampms: number; + is_live: boolean; + is_cancelled: boolean; + is_hidden: boolean; + was_forced: boolean; + executed_amount: string; + remaining_amount: string; + client_order_id?: string; + options: string[]; + price: string; + original_amount: string; + status: string; +} + +export interface MyTrade { + price: string; + amount: string; + timestamp: number; + timestampms: number; + type: string; + aggressor: boolean; + fee_currency: string; + fee_amount: string; + tid: number; + order_id: string; + exchange: string; + is_auction_fill: boolean; + is_clearing_fill: boolean; + symbol: string; + client_order_id?: string; + break?: string; +} + +export interface OrderStatusDetail extends Order { + trades?: MyTrade[]; +} + +export interface TradeVolume { + symbol: string; + base_currency: string; + notional_currency: string; + data_date: string; + total_volume_base: number; + maker_buy_sell_ratio: number; + buy_maker_base: number; + buy_maker_notional: number; + buy_maker_count: number; + sell_maker_base: number; + sell_maker_notional: number; + sell_maker_count: number; + buy_taker_base: number; + buy_taker_notional: number; + buy_taker_count: number; + sell_taker_base: number; + sell_taker_notional: number; + sell_taker_count: number; +} + +export interface NotionalVolume { + web_maker_fee_bps: number; + web_taker_fee_bps: number; + web_auction_fee_bps: number; + api_maker_fee_bps: number; + api_taker_fee_bps: number; + api_auction_fee_bps: number; + fix_maker_fee_bps: number; + fix_taker_fee_bps: number; + fix_auction_fee_bps: number; + block_maker_fee_bps: number; + block_taker_fee_bps: number; + notional_30d_volume: number; + last_updated_ms: number; + date: string; + notional_1d_volume: Array<{ date: string; notional_volume: number }>; +} diff --git a/packages/mcp-server/src/types/staking.ts b/packages/mcp-server/src/types/staking.ts new file mode 100644 index 00000000..691fa330 --- /dev/null +++ b/packages/mcp-server/src/types/staking.ts @@ -0,0 +1,34 @@ +export interface StakingBalance { + currency: string; + balance: string; + available: string; + stakingRewards: { + balance: string; + currency: string; + }; + rewardsRate: string; + providerName: string; +} + +export interface StakingHistory { + type: string; + datetime: string; + amount: string; + currency: string; + status: string; +} + +export interface StakingRate { + symbol: string; + rate: string; + providerName: string; +} + +export interface StakingResult { + datetime: string; + txHash: string; + providerId: string; + currency: string; + amount: string; + status: string; +} diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json new file mode 100644 index 00000000..486fed81 --- /dev/null +++ b/packages/mcp-server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src/**/*"] +} diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 00000000..1a933d2b --- /dev/null +++ b/samples/README.md @@ -0,0 +1,255 @@ +# Gemini API Samples + +Multi-language examples demonstrating how to use the Gemini cryptocurrency exchange API. + +## Project Structure + +``` +├── .env.example # Environment variables template +├── typescript/ # TypeScript examples +│ ├── src/ +│ │ ├── getTicker.ts # Get ticker data (public, REST) +│ │ ├── priceFeed.ts # Get prices for all pairs (public, REST) +│ │ ├── orderBook.ts # Get order book bids/asks (public, REST) +│ │ ├── tradeHistory.ts # Get recent trades (public, REST) +│ │ ├── wsBookTicker.ts # Stream best bid/ask updates (public, WebSocket) +│ │ ├── wsTrades.ts # Stream real-time trades (public, WebSocket) +│ │ ├── placeOrder.ts # Place an order (private, REST) +│ │ ├── balances.ts # Get account balances (private, REST) +│ │ ├── pmPrices.ts # Stream prediction market prices (private, WebSocket) +│ │ ├── pmOrder.ts # Place prediction market order (private, WebSocket) +│ │ └── test_parse.ts # Test parsing of API responses (utility) +├── python/ # Python examples +│ ├── get_ticker.py # Get ticker data (public, REST) +│ ├── price_feed.py # Get prices for all pairs (public, REST) +│ ├── order_book.py # Get order book bids/asks (public, REST) +│ ├── trade_history.py # Get recent trades (public, REST) +│ ├── ws_book_ticker.py # Stream best bid/ask updates (public, WebSocket) +│ ├── ws_trades.py # Stream real-time trades (public, WebSocket) +│ ├── place_order.py # Place an order (private, REST) +│ ├── balances.py # Get account balances (private, REST) +│ ├── pm_prices.py # Stream prediction market prices (private, WebSocket) +│ ├── pm_order.py # Place prediction market order (private, WebSocket) +│ └── test_parse.py # Test parsing of API responses (utility) +└── go/ # Go examples + ├── get_ticker.go # Get ticker data (public, REST) + ├── price_feed.go # Get prices for all pairs (public, REST) + ├── order_book.go # Get order book bids/asks (public, REST) + ├── trade_history.go # Get recent trades (public, REST) + ├── ws_book_ticker.go # Stream best bid/ask updates (public, WebSocket) + ├── ws_trades.go # Stream real-time trades (public, WebSocket) + ├── place_order.go # Place an order (private, REST) + ├── balances.go # Get account balances (private, REST) + ├── pm_prices.go # Stream prediction market prices (private, WebSocket) + ├── pm_order.go # Place prediction market order (private, WebSocket) + └── test_parse.go # Test parsing of API responses (utility) +``` + +## Quick Start + +### Public REST Endpoints (No API Key Required) +```bash +# Get ticker data for a symbol +python3 python/get_ticker.py btcusd +npx ts-node typescript/src/getTicker.ts ethusd +go run go/get_ticker.go dogeusd + +# Get prices for all trading pairs +python3 python/price_feed.py +npx ts-node typescript/src/priceFeed.ts +go run go/price_feed.go + +# Get order book (bids/asks) +python3 python/order_book.py btcusd +npx ts-node typescript/src/orderBook.ts ethusd +go run go/order_book.go btcusd + +# Get recent trade history +python3 python/trade_history.py btcusd +npx ts-node typescript/src/tradeHistory.ts btcusd +go run go/trade_history.go btcusd +``` + +### Public WebSocket Streams (No API Key Required) +```bash +# Stream real-time trades +python3 python/ws_trades.py btcusd +npx ts-node typescript/src/wsTrades.ts btcusd +go run go/ws_trades.go btcusd + +# Stream best bid/ask updates +python3 python/ws_book_ticker.py btcusd +npx ts-node typescript/src/wsBookTicker.ts btcusd +go run go/ws_book_ticker.go btcusd +``` + +### Private Endpoints (API Key Required) + +1. **Add your API credentials to `.env`:** + ```bash + cp .env.example .env + # Edit .env and add your real GEMINI_API_KEY and GEMINI_API_SECRET + ``` + +2. **Run examples:** + ```bash + # Get account balances + python3 python/balances.py + npx ts-node typescript/src/balances.ts + go run go/balances.go + + # Place an order + python3 python/place_order.py + npx ts-node typescript/src/placeOrder.ts + go run go/place_order.go + + # Stream prediction market prices (WebSocket) + python3 python/pm_prices.py + npx ts-node typescript/src/pmPrices.ts + go run go/pm_prices.go + + # Place prediction market order (WebSocket) + python3 python/pm_order.py + npx ts-node typescript/src/pmOrder.ts + go run go/pm_order.go + ``` + +## Getting Started + +### Configuration + +The base URL is configured via environment variables. Copy the example file: + +```bash +cp .env.example .env +``` + +The default configuration uses the production Gemini API. For testing, you can change `GEMINI_BASE_URL` to the sandbox environment: + +```bash +GEMINI_BASE_URL=https://api.sandbox.gemini.com/v1 +``` + +### Public REST Endpoints (No Authentication Required) + +These examples use public API endpoints and don't require any API keys. + +| Example | Description | Endpoint | +|---------|-------------|----------| +| `getTicker` / `get_ticker` | Ticker data for a symbol | `/v1/pubticker/{symbol}` | +| `priceFeed` / `price_feed` | Prices for all trading pairs | `/v1/pricefeed` | +| `orderBook` / `order_book` | Order book (bids/asks) for a symbol | `/v1/book/{symbol}` | +| `tradeHistory` / `trade_history` | Recent trades for a symbol | `/v1/trades/{symbol}` | + +```bash +# Install dependencies first +cd typescript && npm install # TypeScript +cd python && pip install -r requirements.txt # Python +cd go && go mod download # Go + +# Then run any example with an optional symbol argument (defaults to btcusd) +python3 python/get_ticker.py ethusd +npx ts-node typescript/src/getTicker.ts ethusd +go run go/get_ticker.go ethusd +``` + +### Public WebSocket Streams (No Authentication Required) + +| Example | Description | Stream | +|---------|-------------|--------| +| `wsTrades` / `ws_trades` | Real-time trade stream | `@trade` | +| `wsBookTicker` / `ws_book_ticker` | Best bid/ask updates | `@bookTicker` | + +```bash +# Stream real-time trades (Ctrl+C to stop) +python3 python/ws_trades.py btcusd +npx ts-node typescript/src/wsTrades.ts btcusd +go run go/ws_trades.go btcusd +``` + +### Private Endpoints (Authentication Required) + +The authenticated examples require Gemini API credentials. + +| Example | Description | Protocol | +|---------|-------------|----------| +| `balances` | Get account balances | REST | +| `placeOrder` / `place_order` | Place an order | REST | +| `pmPrices` / `pm_prices` | Stream prediction market prices | WebSocket | +| `pmOrder` / `pm_order` | Place prediction market order | WebSocket | + +1. **Get API Credentials:** + - Sign up at [Gemini](https://www.gemini.com/) + - Create API keys in your account settings + - Use the sandbox environment for testing: https://exchange.sandbox.gemini.com/ + +2. **Configure Environment:** + ```bash + cp .env.example .env + # Edit .env and add your GEMINI_API_KEY and GEMINI_API_SECRET + ``` + + Your `.env` file should look like: + ``` + GEMINI_BASE_URL=https://api.gemini.com/v1 + GEMINI_API_KEY=account-xxxxxxxxxxxxxx + GEMINI_API_SECRET=xxxxxxxxxxxxxx + ``` + + **For testing, use the sandbox:** + ``` + GEMINI_BASE_URL=https://api.sandbox.gemini.com/v1 + GEMINI_API_KEY=your-sandbox-api-key + GEMINI_API_SECRET=your-sandbox-api-secret + ``` + +3. **Run Examples:** + ```bash + # Get balances + python3 python/balances.py + npx ts-node typescript/src/balances.ts + go run go/balances.go + + # Place an order + python3 python/place_order.py + npx ts-node typescript/src/placeOrder.ts + go run go/place_order.go + + # Stream prediction market prices (Ctrl+C to stop) + python3 python/pm_prices.py + npx ts-node typescript/src/pmPrices.ts + go run go/pm_prices.go + ``` + +## API Documentation + +- [Gemini API Documentation](https://docs.gemini.com/rest-api/) +- [Public Ticker API](https://docs.gemini.com/rest-api/#ticker) +- [Order Book API](https://docs.gemini.com/rest-api/#current-order-book) +- [Trade History API](https://docs.gemini.com/rest-api/#trade-history) +- [Price Feed API](https://docs.gemini.com/rest-api/#price-feed) +- [WebSocket Market Data](https://docs.gemini.com/websocket-api/#market-data-v2) +- [Private Endpoints](https://docs.gemini.com/rest-api/#authenticated-api-invocation) + +## Authentication + +The Gemini API uses a specific authentication method for private endpoints: + +1. **Payload**: Create a JSON object with `request` path and `nonce` (current timestamp in milliseconds) +2. **Base64 Encode**: Encode the payload to base64 +3. **HMAC Signature**: Create an HMAC-SHA384 signature of the base64 payload using your API secret +4. **Headers**: Send the request with: + - `X-GEMINI-APIKEY`: Your API key + - `X-GEMINI-PAYLOAD`: Base64 encoded payload + - `X-GEMINI-SIGNATURE`: Hex-encoded HMAC signature + +All implementations handle this authentication automatically. + +## Notes + +- Public endpoints can be called without authentication +- Private endpoints require valid API credentials +- Use the sandbox environment for testing to avoid real trades +- All examples use lowercase symbols (e.g., `btcusd` not `BTCUSD`) +- WebSocket streams run continuously — press Ctrl+C to stop +- Each example is available in all three languages (TypeScript, Python, Go) \ No newline at end of file diff --git a/samples/go/balances.go b/samples/go/balances.go new file mode 100644 index 00000000..d72187a4 --- /dev/null +++ b/samples/go/balances.go @@ -0,0 +1,120 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "time" + + "github.com/joho/godotenv" +) + +type BalancePayload struct { + Request string `json:"request"` + Nonce string `json:"nonce"` +} + +type Balance struct { + Type string `json:"type"` + Currency string `json:"currency"` + Amount string `json:"amount"` + Available string `json:"available"` + AvailableForWithdrawal string `json:"availableForWithdrawal"` + PendingWithdrawal string `json:"pendingWithdrawal,omitempty"` + PendingDeposit string `json:"pendingDeposit,omitempty"` +} + +func getBalances() error { + // Load .env file + _ = godotenv.Load() + + baseURL := os.Getenv("GEMINI_BASE_URL") + if baseURL == "" { + baseURL = "https://api.gemini.com/v1" + } + + apiKey := os.Getenv("GEMINI_API_KEY") + apiSecret := os.Getenv("GEMINI_API_SECRET") + + if apiKey == "" || apiSecret == "" { + return fmt.Errorf("GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file") + } + + // Create the payload + payload := BalancePayload{ + Request: "/v1/balances", + Nonce: strconv.FormatInt(time.Now().UnixMilli(), 10), + } + + payloadJSON, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("error marshaling payload: %w", err) + } + + // Base64 encode the payload + encodedPayload := base64.StdEncoding.EncodeToString(payloadJSON) + + // Create the signature + h := hmac.New(sha512.New384, []byte(apiSecret)) + h.Write([]byte(encodedPayload)) + signature := hex.EncodeToString(h.Sum(nil)) + + // Make the request + url := fmt.Sprintf("%s/balances", baseURL) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadJSON)) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-GEMINI-APIKEY", apiKey) + req.Header.Set("X-GEMINI-PAYLOAD", encodedPayload) + req.Header.Set("X-GEMINI-SIGNATURE", signature) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + var balances []Balance + if err := json.Unmarshal(body, &balances); err != nil { + return fmt.Errorf("error parsing JSON: %w", err) + } + + prettyJSON, _ := json.MarshalIndent(balances, "", " ") + fmt.Println("Account balances:") + fmt.Println(string(prettyJSON)) + + // Summary + fmt.Printf("\nTotal currencies: %d\n", len(balances)) + for _, balance := range balances { + fmt.Printf("%s: %s available (%s total)\n", balance.Currency, balance.Available, balance.Amount) + } + + return nil +} + +func main() { + if err := getBalances(); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/go/get_ticker.go b/samples/go/get_ticker.go new file mode 100644 index 00000000..b3374310 --- /dev/null +++ b/samples/go/get_ticker.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/joho/godotenv" +) + +type TickerData struct { + Bid string `json:"bid"` + Ask string `json:"ask"` + Last string `json:"last"` + Volume map[string]interface{} `json:"volume"` +} + +func getTicker(symbol string) error { + // Load .env file + _ = godotenv.Load() + + baseURL := os.Getenv("GEMINI_BASE_URL") + if baseURL == "" { + baseURL = "https://api.gemini.com/v1" + } + + // Symbol should be lowercase for Gemini API + symbolLower := strings.ToLower(symbol) + url := fmt.Sprintf("%s/pubticker/%s", baseURL, symbolLower) + + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + var ticker TickerData + if err := json.Unmarshal(body, &ticker); err != nil { + return fmt.Errorf("error parsing JSON: %w", err) + } + + fmt.Printf("Ticker data for %s:\n", symbol) + fmt.Printf(" Bid: %s\n", ticker.Bid) + fmt.Printf(" Ask: %s\n", ticker.Ask) + fmt.Printf(" Last: %s\n", ticker.Last) + return nil +} + +func main() { + args := os.Args[1:] + + // Show help message + if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { + fmt.Println("Usage: go run get_ticker.go [symbol]") + fmt.Println("Example: go run get_ticker.go ethusd") + fmt.Println("Default symbol: btcusd") + os.Exit(0) + } + + // Get symbol from command line or use default + symbol := "btcusd" + if len(args) > 0 { + symbol = args[0] + } + + if err := getTicker(symbol); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/go/go.mod b/samples/go/go.mod new file mode 100644 index 00000000..8bf0e270 --- /dev/null +++ b/samples/go/go.mod @@ -0,0 +1,8 @@ +module github.com/example/api-examples + +go 1.21 + +require ( + github.com/gorilla/websocket v1.5.3 + github.com/joho/godotenv v1.5.1 +) diff --git a/samples/go/go.sum b/samples/go/go.sum new file mode 100644 index 00000000..09f4ebfc --- /dev/null +++ b/samples/go/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/samples/go/order_book.go b/samples/go/order_book.go new file mode 100644 index 00000000..097bc011 --- /dev/null +++ b/samples/go/order_book.go @@ -0,0 +1,112 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "os" + "strconv" + "strings" + + "github.com/joho/godotenv" +) + +type OrderBookEntry struct { + Price string `json:"price"` + Amount string `json:"amount"` +} + +type OrderBook struct { + Bids []OrderBookEntry `json:"bids"` + Asks []OrderBookEntry `json:"asks"` +} + +func getOrderBook(symbol string) error { + // Load .env file + _ = godotenv.Load() + + baseURL := os.Getenv("GEMINI_BASE_URL") + if baseURL == "" { + baseURL = "https://api.gemini.com/v1" + } + + symbolLower := strings.ToLower(symbol) + url := fmt.Sprintf("%s/book/%s", baseURL, symbolLower) + + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + var book OrderBook + if err := json.Unmarshal(body, &book); err != nil { + return fmt.Errorf("error parsing JSON: %w", err) + } + + fmt.Printf("Order book for %s:\n\n", symbol) + + // Show top 5 asks in reverse order + fmt.Println("Asks (top 5):") + limit := int(math.Min(5, float64(len(book.Asks)))) + for i := limit - 1; i >= 0; i-- { + fmt.Printf(" %12s | %s\n", book.Asks[i].Price, book.Asks[i].Amount) + } + + fmt.Println(" ------------|------------") + + // Show top 5 bids + fmt.Println("Bids (top 5):") + limit = int(math.Min(5, float64(len(book.Bids)))) + for i := 0; i < limit; i++ { + fmt.Printf(" %12s | %s\n", book.Bids[i].Price, book.Bids[i].Amount) + } + + // Calculate spread + if len(book.Asks) > 0 && len(book.Bids) > 0 { + askPrice, err := strconv.ParseFloat(book.Asks[0].Price, 64) + if err != nil { + return fmt.Errorf("error parsing ask price: %w", err) + } + bidPrice, err := strconv.ParseFloat(book.Bids[0].Price, 64) + if err != nil { + return fmt.Errorf("error parsing bid price: %w", err) + } + fmt.Printf("\nSpread: %.2f\n", askPrice-bidPrice) + } + + return nil +} + +func main() { + args := os.Args[1:] + + // Show help message + if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { + fmt.Println("Usage: go run order_book.go [symbol]") + fmt.Println("Example: go run order_book.go ethusd") + fmt.Println("Default symbol: btcusd") + os.Exit(0) + } + + // Get symbol from command line or use default + symbol := "btcusd" + if len(args) > 0 { + symbol = args[0] + } + + if err := getOrderBook(symbol); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/go/place_order.go b/samples/go/place_order.go new file mode 100644 index 00000000..c518cc48 --- /dev/null +++ b/samples/go/place_order.go @@ -0,0 +1,137 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "time" + + "github.com/joho/godotenv" +) + +type OrderParams struct { + Symbol string `json:"symbol"` + Side string `json:"side"` + Type string `json:"type"` + Quantity string `json:"quantity"` + Price string `json:"price,omitempty"` +} + +type OrderPayload struct { + Request string `json:"request"` + Nonce string `json:"nonce"` + Symbol string `json:"symbol"` + Amount string `json:"amount"` + Price string `json:"price,omitempty"` + Side string `json:"side"` + Type string `json:"type"` +} + +type OrderResponse struct { + OrderID string `json:"order_id"` + Symbol string `json:"symbol"` + Side string `json:"side"` + Type string `json:"type"` + Price string `json:"price"` + OriginalAmount string `json:"original_amount"` +} + +func placeOrder(params OrderParams) error { + // Load .env file + _ = godotenv.Load() + + baseURL := os.Getenv("GEMINI_BASE_URL") + if baseURL == "" { + baseURL = "https://api.gemini.com/v1" + } + + apiKey := os.Getenv("GEMINI_API_KEY") + apiSecret := os.Getenv("GEMINI_API_SECRET") + + if apiKey == "" || apiSecret == "" { + return fmt.Errorf("GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file") + } + + // Create the payload + payload := OrderPayload{ + Request: "/v1/order/new", + Nonce: strconv.FormatInt(time.Now().UnixMilli(), 10), + Symbol: params.Symbol, + Amount: params.Quantity, + Price: params.Price, + Side: params.Side, + Type: params.Type, + } + + payloadJSON, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("error marshaling payload: %w", err) + } + + // Base64 encode the payload + encodedPayload := base64.StdEncoding.EncodeToString(payloadJSON) + + // Create the signature + h := hmac.New(sha512.New384, []byte(apiSecret)) + h.Write([]byte(encodedPayload)) + signature := hex.EncodeToString(h.Sum(nil)) + + // Make the request + url := fmt.Sprintf("%s/order/new", baseURL) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadJSON)) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-GEMINI-APIKEY", apiKey) + req.Header.Set("X-GEMINI-PAYLOAD", encodedPayload) + req.Header.Set("X-GEMINI-SIGNATURE", signature) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + var result OrderResponse + if err := json.Unmarshal(body, &result); err != nil { + return fmt.Errorf("error parsing JSON: %w", err) + } + + fmt.Printf("Order placed: %s %s %s @ %s (order_id: %s, type: %s)\n", + result.Side, result.OriginalAmount, result.Symbol, result.Price, result.OrderID, result.Type) + return nil +} + +func main() { + order := OrderParams{ + Symbol: "BTCUSD", + Side: "buy", + Type: "limit", + Quantity: "0.01", + Price: "50000", + } + + if err := placeOrder(order); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/go/pm_order.go b/samples/go/pm_order.go new file mode 100644 index 00000000..519aa3e1 --- /dev/null +++ b/samples/go/pm_order.go @@ -0,0 +1,183 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "os" + "os/signal" + "strconv" + "time" + + "github.com/gorilla/websocket" + "github.com/joho/godotenv" +) + +type PmOrderMessage struct { + X string `json:"X"` // Status + S string `json:"S"` // Side + O string `json:"O"` // Outcome + P string `json:"p"` // Price + Q string `json:"q"` // Quantity +} + +type PmTickerMessage struct { + S string `json:"s"` // Symbol + BidPrice string `json:"b"` // Best bid price + AskPrice string `json:"a"` // Best ask price +} + +func placePredictionOrder(symbol, side, price, quantity, outcome string) error { + // Load .env file + _ = godotenv.Load() + + wsURL := os.Getenv("GEMINI_WS_URL") + if wsURL == "" { + wsURL = "wss://ws.gemini.com" + } + + apiKey := os.Getenv("GEMINI_API_KEY") + apiSecret := os.Getenv("GEMINI_API_SECRET") + + if apiKey == "" || apiSecret == "" { + return fmt.Errorf("GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file") + } + + // Authentication — signs a timestamp with your secret so Gemini + // can verify your identity. + nonce := strconv.FormatInt(time.Now().Unix(), 10) + payload := base64.StdEncoding.EncodeToString([]byte(nonce)) + h := hmac.New(sha512.New384, []byte(apiSecret)) + h.Write([]byte(payload)) + sig := hex.EncodeToString(h.Sum(nil)) + + headers := http.Header{} + headers.Set("X-GEMINI-APIKEY", apiKey) + headers.Set("X-GEMINI-NONCE", nonce) + headers.Set("X-GEMINI-PAYLOAD", payload) + headers.Set("X-GEMINI-SIGNATURE", sig) + + conn, _, err := websocket.DefaultDialer.Dial(wsURL, headers) + if err != nil { + return fmt.Errorf("error connecting: %w", err) + } + defer conn.Close() + + fmt.Printf("Connected to %s\n", wsURL) + fmt.Printf("Subscribing to %s prices and order updates...\n\n", symbol) + + // Subscribe to book ticker and order updates + sub := struct { + ID string `json:"id"` + Method string `json:"method"` + Params []string `json:"params"` + }{ + ID: "1", + Method: "subscribe", + Params: []string{ + fmt.Sprintf("%s@bookTicker", symbol), + "orders@account", + }, + } + if err := conn.WriteJSON(sub); err != nil { + return fmt.Errorf("error subscribing: %w", err) + } + + // Graceful shutdown + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + done := make(chan struct{}) + orderPlaced := false + + go func() { + defer close(done) + for { + _, message, err := conn.ReadMessage() + if err != nil { + fmt.Println("Connection closed") + return + } + + // Check for ticker data to trigger order placement + var ticker PmTickerMessage + if err := json.Unmarshal(message, &ticker); err == nil { + if !orderPlaced && ticker.BidPrice != "" && ticker.AskPrice != "" && ticker.S == symbol { + fmt.Printf("Best bid: $%s Best ask: $%s\n", ticker.BidPrice, ticker.AskPrice) + fmt.Println("Placing order...") + orderPlaced = true + + order := struct { + ID string `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params"` + }{ + ID: "2", + Method: "order.place", + Params: map[string]string{ + "symbol": symbol, + "side": side, + "type": "LIMIT", + "timeInForce": "GTC", + "price": price, + "quantity": quantity, + "eventOutcome": outcome, + }, + } + conn.WriteJSON(order) + } + } + + // Check for order lifecycle updates + var orderMsg PmOrderMessage + if err := json.Unmarshal(message, &orderMsg); err == nil { + switch orderMsg.X { + case "NEW", "OPEN", "FILLED", "PARTIALLY_FILLED": + fmt.Printf("Order %s: side=%s outcome=%s price=$%s qty=%s\n", + orderMsg.X, orderMsg.S, orderMsg.O, orderMsg.P, orderMsg.Q) + } + } + } + }() + + select { + case <-done: + return nil + case <-interrupt: + fmt.Println("\nClosing connection...") + conn.WriteMessage( + websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), + ) + <-done + return nil + } +} + +func main() { + args := os.Args[1:] + + // Show help message + if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { + fmt.Println("Usage: go run pm_order.go [symbol]") + fmt.Println("Example: go run pm_order.go GEMI-PRES2028-VANCE") + fmt.Println("Default symbol: GEMI-PRES2028-VANCE") + fmt.Println("\nPlaces a BUY LIMIT order for 100 YES contracts at $0.27.") + fmt.Println("Edit the script to change side, price, quantity, or outcome.") + os.Exit(0) + } + + // Get symbol from command line or use default + symbol := "GEMI-PRES2028-VANCE" + if len(args) > 0 { + symbol = args[0] + } + + if err := placePredictionOrder(symbol, "BUY", "0.27", "100", "YES"); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/go/pm_prices.go b/samples/go/pm_prices.go new file mode 100644 index 00000000..015e3861 --- /dev/null +++ b/samples/go/pm_prices.go @@ -0,0 +1,142 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "os" + "os/signal" + "strconv" + "time" + + "github.com/gorilla/websocket" + "github.com/joho/godotenv" +) + +type PmBookTickerMessage struct { + S string `json:"s"` // Symbol + BidPrice string `json:"b"` // Best bid price + BidQty string `json:"B"` // Best bid quantity + AskPrice string `json:"a"` // Best ask price + AskQty string `json:"A"` // Best ask quantity +} + +func streamPredictionPrices(symbol string) error { + // Load .env file + _ = godotenv.Load() + + wsURL := os.Getenv("GEMINI_WS_URL") + if wsURL == "" { + wsURL = "wss://ws.gemini.com" + } + + apiKey := os.Getenv("GEMINI_API_KEY") + apiSecret := os.Getenv("GEMINI_API_SECRET") + + if apiKey == "" || apiSecret == "" { + return fmt.Errorf("GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file") + } + + // Authentication — signs a timestamp with your secret so Gemini + // can verify your identity. + nonce := strconv.FormatInt(time.Now().Unix(), 10) + payload := base64.StdEncoding.EncodeToString([]byte(nonce)) + h := hmac.New(sha512.New384, []byte(apiSecret)) + h.Write([]byte(payload)) + sig := hex.EncodeToString(h.Sum(nil)) + + headers := http.Header{} + headers.Set("X-GEMINI-APIKEY", apiKey) + headers.Set("X-GEMINI-NONCE", nonce) + headers.Set("X-GEMINI-PAYLOAD", payload) + headers.Set("X-GEMINI-SIGNATURE", sig) + + conn, _, err := websocket.DefaultDialer.Dial(wsURL, headers) + if err != nil { + return fmt.Errorf("error connecting: %w", err) + } + defer conn.Close() + + fmt.Printf("Connected to %s\n", wsURL) + fmt.Printf("Subscribing to %s book ticker...\n\n", symbol) + + // Subscribe to book ticker stream + sub := struct { + ID string `json:"id"` + Method string `json:"method"` + Params []string `json:"params"` + }{ + ID: "1", + Method: "subscribe", + Params: []string{fmt.Sprintf("%s@bookTicker", symbol)}, + } + if err := conn.WriteJSON(sub); err != nil { + return fmt.Errorf("error subscribing: %w", err) + } + + // Graceful shutdown + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + done := make(chan struct{}) + + go func() { + defer close(done) + for { + _, message, err := conn.ReadMessage() + if err != nil { + fmt.Println("Connection closed") + return + } + + var ticker PmBookTickerMessage + if err := json.Unmarshal(message, &ticker); err != nil { + continue + } + + if ticker.BidPrice != "" && ticker.AskPrice != "" { + fmt.Printf("%s bid: $%s (%s contracts) ask: $%s (%s contracts)\n", + ticker.S, ticker.BidPrice, ticker.BidQty, ticker.AskPrice, ticker.AskQty) + } + } + }() + + select { + case <-done: + return nil + case <-interrupt: + fmt.Println("\nClosing connection...") + conn.WriteMessage( + websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), + ) + <-done + return nil + } +} + +func main() { + args := os.Args[1:] + + // Show help message + if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { + fmt.Println("Usage: go run pm_prices.go [symbol]") + fmt.Println("Example: go run pm_prices.go GEMI-PRES2028-VANCE") + fmt.Println("Default symbol: GEMI-PRES2028-VANCE") + os.Exit(0) + } + + // Get symbol from command line or use default + symbol := "GEMI-PRES2028-VANCE" + if len(args) > 0 { + symbol = args[0] + } + + if err := streamPredictionPrices(symbol); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/go/price_feed.go b/samples/go/price_feed.go new file mode 100644 index 00000000..4cd6434e --- /dev/null +++ b/samples/go/price_feed.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/joho/godotenv" +) + +type PriceFeedEntry struct { + Pair string `json:"pair"` + Price string `json:"price"` + PercentChange24h string `json:"percentChange24h"` +} + +func getPriceFeed() error { + // Load .env file + _ = godotenv.Load() + + baseURL := os.Getenv("GEMINI_BASE_URL") + if baseURL == "" { + baseURL = "https://api.gemini.com/v1" + } + + url := fmt.Sprintf("%s/pricefeed", baseURL) + + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + var pairs []PriceFeedEntry + if err := json.Unmarshal(body, &pairs); err != nil { + return fmt.Errorf("error parsing JSON: %w", err) + } + + fmt.Printf("Price feed (%d pairs):\n\n", len(pairs)) + + for _, entry := range pairs { + sign := "" + if len(entry.PercentChange24h) > 0 && entry.PercentChange24h[0] != '-' { + sign = "+" + } + fmt.Printf("%-12s %12s (%s%s%%)\n", entry.Pair, entry.Price, sign, entry.PercentChange24h) + } + + return nil +} + +func main() { + args := os.Args[1:] + + // Show help message + if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { + fmt.Println("Usage: go run price_feed.go") + fmt.Println("Fetches current prices for all trading pairs.") + os.Exit(0) + } + + if err := getPriceFeed(); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/go/test_parse.go b/samples/go/test_parse.go new file mode 100644 index 00000000..16b50e28 --- /dev/null +++ b/samples/go/test_parse.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" +) + +type Balance struct { + Type string `json:"type"` + Currency string `json:"currency"` + Amount string `json:"amount"` + Available string `json:"available"` + AvailableForWithdrawal string `json:"availableForWithdrawal"` + PendingWithdrawal string `json:"pendingWithdrawal,omitempty"` + PendingDeposit string `json:"pendingDeposit,omitempty"` +} + +func main() { + // Sample response from Gemini API + sampleResponse := `[ + { + "type": "exchange", + "currency": "BTC", + "amount": "5.0", + "available": "4.5", + "availableForWithdrawal": "4.5", + "pendingWithdrawal": "0.25", + "pendingDeposit": "0.25" + }, + { + "type": "exchange", + "currency": "USD", + "amount": "15000.00", + "available": "5000.00", + "availableForWithdrawal": "5000.00" + }, + { + "type": "exchange", + "currency": "ETH", + "amount": "10.0", + "available": "10.0", + "availableForWithdrawal": "10.0" + } +]` + + // Parse the JSON + var balances []Balance + if err := json.Unmarshal([]byte(sampleResponse), &balances); err != nil { + log.Fatal("Error parsing JSON:", err) + } + + // Display the parsed data + fmt.Println("✅ Successfully parsed balances response!") + fmt.Printf("\nTotal currencies: %d\n\n", len(balances)) + + for _, balance := range balances { + fmt.Printf("Currency: %s\n", balance.Currency) + fmt.Printf(" Type: %s\n", balance.Type) + fmt.Printf(" Amount: %s\n", balance.Amount) + fmt.Printf(" Available: %s\n", balance.Available) + fmt.Printf(" Available for Withdrawal: %s\n", balance.AvailableForWithdrawal) + if balance.PendingWithdrawal != "" { + fmt.Printf(" Pending Withdrawal: %s\n", balance.PendingWithdrawal) + } + if balance.PendingDeposit != "" { + fmt.Printf(" Pending Deposit: %s\n", balance.PendingDeposit) + } + fmt.Println() + } + + // Pretty print the full response + prettyJSON, _ := json.MarshalIndent(balances, "", " ") + fmt.Println("Full JSON response:") + fmt.Println(string(prettyJSON)) +} diff --git a/samples/go/trade_history.go b/samples/go/trade_history.go new file mode 100644 index 00000000..ab299e35 --- /dev/null +++ b/samples/go/trade_history.go @@ -0,0 +1,87 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "github.com/joho/godotenv" +) + +type Trade struct { + Timestamp int64 `json:"timestamp"` + TimestampMs int64 `json:"timestampms"` + TID int64 `json:"tid"` + Price string `json:"price"` + Amount string `json:"amount"` + Exchange string `json:"exchange"` + Type string `json:"type"` +} + +func getTradeHistory(symbol string) error { + // Load .env file + _ = godotenv.Load() + + baseURL := os.Getenv("GEMINI_BASE_URL") + if baseURL == "" { + baseURL = "https://api.gemini.com/v1" + } + + symbolLower := strings.ToLower(symbol) + url := fmt.Sprintf("%s/trades/%s", baseURL, symbolLower) + + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) + } + + var trades []Trade + if err := json.Unmarshal(body, &trades); err != nil { + return fmt.Errorf("error parsing JSON: %w", err) + } + + fmt.Printf("Recent trades for %s (%d trades):\n\n", symbol, len(trades)) + + for _, trade := range trades { + t := time.UnixMilli(trade.TimestampMs).UTC().Format(time.RFC3339) + fmt.Printf("[%s] %-4s %s @ %s (tid: %d)\n", t, trade.Type, trade.Amount, trade.Price, trade.TID) + } + + return nil +} + +func main() { + args := os.Args[1:] + + // Show help message + if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { + fmt.Println("Usage: go run trade_history.go [symbol]") + fmt.Println("Example: go run trade_history.go ethusd") + fmt.Println("Default symbol: btcusd") + os.Exit(0) + } + + // Get symbol from command line or use default + symbol := "btcusd" + if len(args) > 0 { + symbol = args[0] + } + + if err := getTradeHistory(symbol); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/go/ws_book_ticker.go b/samples/go/ws_book_ticker.go new file mode 100644 index 00000000..ba8a1a29 --- /dev/null +++ b/samples/go/ws_book_ticker.go @@ -0,0 +1,130 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/signal" + "time" + + "github.com/gorilla/websocket" + "github.com/joho/godotenv" +) + +type BookTickerSubscribeMessage struct { + ID string `json:"id"` + Method string `json:"method"` + Params []string `json:"params"` +} + +type BookTickerMessage struct { + U int64 `json:"u"` // Update ID + E int64 `json:"E"` // Event time (nanoseconds) + S string `json:"s"` // Symbol + BidPrice string `json:"b"` // Best bid price + BidQty string `json:"B"` // Best bid quantity + AskPrice string `json:"a"` // Best ask price + AskQty string `json:"A"` // Best ask quantity +} + +func streamBookTicker(symbol string) error { + // Load .env file + _ = godotenv.Load() + + wsURL := os.Getenv("GEMINI_WS_URL") + if wsURL == "" { + wsURL = "wss://ws.gemini.com" + } + + conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + if err != nil { + return fmt.Errorf("error connecting: %w", err) + } + defer conn.Close() + + fmt.Printf("Connected to %s\n", wsURL) + fmt.Printf("Subscribing to %s book ticker...\n\n", symbol) + + // Subscribe to book ticker stream + sub := BookTickerSubscribeMessage{ + ID: "1", + Method: "subscribe", + Params: []string{fmt.Sprintf("%s@bookTicker", symbol)}, + } + if err := conn.WriteJSON(sub); err != nil { + return fmt.Errorf("error subscribing: %w", err) + } + + // Graceful shutdown + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + done := make(chan struct{}) + + go func() { + defer close(done) + for { + _, message, err := conn.ReadMessage() + if err != nil { + fmt.Println("Connection closed") + return + } + + // Check if this is a subscription confirmation + var raw map[string]json.RawMessage + if err := json.Unmarshal(message, &raw); err != nil { + continue + } + if _, ok := raw["result"]; ok { + continue + } + if _, ok := raw["id"]; ok { + continue + } + + var ticker BookTickerMessage + if err := json.Unmarshal(message, &ticker); err != nil { + continue + } + + t := time.Unix(0, ticker.E).UTC().Format(time.RFC3339) + fmt.Printf("[%s] %s bid %s x %s | ask %s x %s\n", + t, ticker.S, ticker.BidPrice, ticker.BidQty, ticker.AskPrice, ticker.AskQty) + } + }() + + select { + case <-done: + return nil + case <-interrupt: + fmt.Println("\nClosing connection...") + conn.WriteMessage( + websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), + ) + <-done + return nil + } +} + +func main() { + args := os.Args[1:] + + // Show help message + if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { + fmt.Println("Usage: go run ws_book_ticker.go [symbol]") + fmt.Println("Example: go run ws_book_ticker.go ethusd") + fmt.Println("Default symbol: btcusd") + os.Exit(0) + } + + // Get symbol from command line or use default + symbol := "btcusd" + if len(args) > 0 { + symbol = args[0] + } + + if err := streamBookTicker(symbol); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/go/ws_trades.go b/samples/go/ws_trades.go new file mode 100644 index 00000000..73680bba --- /dev/null +++ b/samples/go/ws_trades.go @@ -0,0 +1,132 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/signal" + "time" + + "github.com/gorilla/websocket" + "github.com/joho/godotenv" +) + +type TradeMessage struct { + E int64 `json:"E"` // Event time (nanoseconds) + S string `json:"s"` // Symbol + T int64 `json:"t"` // Trade ID + P string `json:"p"` // Price + Q string `json:"q"` // Quantity + M bool `json:"m"` // Buyer is maker +} + +type SubscribeMessage struct { + ID string `json:"id"` + Method string `json:"method"` + Params []string `json:"params"` +} + +func streamTrades(symbol string) error { + // Load .env file + _ = godotenv.Load() + + wsURL := os.Getenv("GEMINI_WS_URL") + if wsURL == "" { + wsURL = "wss://ws.gemini.com" + } + + conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + if err != nil { + return fmt.Errorf("error connecting: %w", err) + } + defer conn.Close() + + fmt.Printf("Connected to %s\n", wsURL) + fmt.Printf("Subscribing to %s trades...\n\n", symbol) + + // Subscribe to trade stream + sub := SubscribeMessage{ + ID: "1", + Method: "subscribe", + Params: []string{fmt.Sprintf("%s@trade", symbol)}, + } + if err := conn.WriteJSON(sub); err != nil { + return fmt.Errorf("error subscribing: %w", err) + } + + // Graceful shutdown + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + done := make(chan struct{}) + + go func() { + defer close(done) + for { + _, message, err := conn.ReadMessage() + if err != nil { + fmt.Println("Connection closed") + return + } + + // Check if this is a subscription confirmation + var raw map[string]json.RawMessage + if err := json.Unmarshal(message, &raw); err != nil { + continue + } + if _, ok := raw["result"]; ok { + continue + } + if _, ok := raw["id"]; ok { + continue + } + + var trade TradeMessage + if err := json.Unmarshal(message, &trade); err != nil { + continue + } + + side := "buy " + if trade.M { + side = "sell" + } + t := time.Unix(0, trade.E).UTC().Format(time.RFC3339) + fmt.Printf("[%s] %s %s %s @ %s\n", t, trade.S, side, trade.Q, trade.P) + } + }() + + select { + case <-done: + return nil + case <-interrupt: + fmt.Println("\nClosing connection...") + conn.WriteMessage( + websocket.CloseMessage, + websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), + ) + <-done + return nil + } +} + +func main() { + args := os.Args[1:] + + // Show help message + if len(args) > 0 && (args[0] == "--help" || args[0] == "-h") { + fmt.Println("Usage: go run ws_trades.go [symbol]") + fmt.Println("Example: go run ws_trades.go ethusd") + fmt.Println("Default symbol: btcusd") + os.Exit(0) + } + + // Get symbol from command line or use default + symbol := "btcusd" + if len(args) > 0 { + symbol = args[0] + } + + if err := streamTrades(symbol); err != nil { + fmt.Println("Error:", err) + } +} diff --git a/samples/python/balances.py b/samples/python/balances.py new file mode 100644 index 00000000..08dc44d8 --- /dev/null +++ b/samples/python/balances.py @@ -0,0 +1,65 @@ +import os +import json +import base64 +import hmac +import hashlib +import time +import requests +from dotenv import load_dotenv + +load_dotenv() + +GEMINI_BASE_URL = os.getenv('GEMINI_BASE_URL', 'https://api.gemini.com/v1') +GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') +GEMINI_API_SECRET = os.getenv('GEMINI_API_SECRET') + +def get_balances(): + """Fetch account balances.""" + if not GEMINI_API_KEY or not GEMINI_API_SECRET: + print('Error: GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file') + return + + # Create the payload + payload = { + 'request': '/v1/balances', + 'nonce': str(int(time.time() * 1000)), + } + + # Base64 encode the payload + encoded_payload = base64.b64encode(json.dumps(payload).encode()).decode() + + # Create the signature + signature = hmac.new( + GEMINI_API_SECRET.encode(), + encoded_payload.encode(), + hashlib.sha384 + ).hexdigest() + + # Make the request + url = f"{GEMINI_BASE_URL}/balances" + headers = { + 'Content-Type': 'application/json', + 'X-GEMINI-APIKEY': GEMINI_API_KEY, + 'X-GEMINI-PAYLOAD': encoded_payload, + 'X-GEMINI-SIGNATURE': signature, + } + + try: + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + balances = response.json() + + print('Account balances:') + print(json.dumps(balances, indent=2)) + + # Summary + print(f'\nTotal currencies: {len(balances)}') + for balance in balances: + print(f"{balance['currency']}: {balance['available']} available ({balance['amount']} total)") + except requests.exceptions.RequestException as e: + print(f'Error fetching balances: {e}') + if hasattr(e, 'response') and e.response is not None: + print(f'Response: {e.response.text}') + +if __name__ == '__main__': + get_balances() diff --git a/samples/python/get_ticker.py b/samples/python/get_ticker.py new file mode 100644 index 00000000..0353f4b9 --- /dev/null +++ b/samples/python/get_ticker.py @@ -0,0 +1,34 @@ +import os +import requests +from dotenv import load_dotenv + +load_dotenv() + +GEMINI_BASE_URL = os.getenv('GEMINI_BASE_URL', 'https://api.gemini.com/v1') + +def get_ticker(symbol: str): + """Fetch ticker data for a given symbol.""" + # Symbol should be lowercase for Gemini API + symbol_lower = symbol.lower() + url = f"{GEMINI_BASE_URL}/pubticker/{symbol_lower}" + + try: + response = requests.get(url) + response.raise_for_status() + print('Ticker data:', response.json()) + except requests.exceptions.RequestException as e: + print(f'Error fetching ticker: {e}') + +if __name__ == '__main__': + import sys + + # Show help message + if '--help' in sys.argv or '-h' in sys.argv: + print('Usage: python3 get_ticker.py [symbol]') + print('Example: python3 get_ticker.py ethusd') + print('Default symbol: btcusd') + sys.exit(0) + + # Get symbol from command line or use default + symbol = sys.argv[1] if len(sys.argv) > 1 else 'btcusd' + get_ticker(symbol) diff --git a/samples/python/order_book.py b/samples/python/order_book.py new file mode 100644 index 00000000..1f6652d5 --- /dev/null +++ b/samples/python/order_book.py @@ -0,0 +1,54 @@ +import os +import requests +from dotenv import load_dotenv + +load_dotenv() + +GEMINI_BASE_URL = os.getenv('GEMINI_BASE_URL', 'https://api.gemini.com/v1') + + +def get_order_book(symbol: str): + """Fetch the current order book for a given symbol.""" + symbol_lower = symbol.lower() + url = f"{GEMINI_BASE_URL}/book/{symbol_lower}" + + try: + response = requests.get(url) + response.raise_for_status() + data = response.json() + + bids = data['bids'] + asks = data['asks'] + + print(f'Order book for {symbol}:\n') + + print('Asks (top 5):') + for ask in reversed(asks[:5]): + print(f" {ask['price']:>12} | {ask['amount']}") + + print(' ------------|------------') + + print('Bids (top 5):') + for bid in bids[:5]: + print(f" {bid['price']:>12} | {bid['amount']}") + + if asks and bids: + spread = float(asks[0]['price']) - float(bids[0]['price']) + print(f'\nSpread: {spread:.2f}') + except requests.exceptions.RequestException as e: + print(f'Error fetching order book: {e}') + + +if __name__ == '__main__': + import sys + + # Show help message + if '--help' in sys.argv or '-h' in sys.argv: + print('Usage: python3 order_book.py [symbol]') + print('Example: python3 order_book.py ethusd') + print('Default symbol: btcusd') + sys.exit(0) + + # Get symbol from command line or use default + symbol = sys.argv[1] if len(sys.argv) > 1 else 'btcusd' + get_order_book(symbol) diff --git a/samples/python/place_order.py b/samples/python/place_order.py new file mode 100644 index 00000000..096c7615 --- /dev/null +++ b/samples/python/place_order.py @@ -0,0 +1,70 @@ +import os +import json +import base64 +import hmac +import hashlib +import time +import requests +from dotenv import load_dotenv +from typing import Literal + +load_dotenv() + +GEMINI_BASE_URL = os.getenv('GEMINI_BASE_URL', 'https://api.gemini.com/v1') +GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') +GEMINI_API_SECRET = os.getenv('GEMINI_API_SECRET') + +def place_order( + symbol: str, + side: Literal['buy', 'sell'], + order_type: Literal['market', 'limit'], + quantity: float, + price: float = None +): + """Place an order.""" + if not GEMINI_API_KEY or not GEMINI_API_SECRET: + print('Error: GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file') + return + + # Create the payload + payload = { + 'request': '/v1/order/new', + 'nonce': str(int(time.time() * 1000)), + 'symbol': symbol, + 'amount': str(quantity), + 'side': side, + 'type': order_type, + } + + if price is not None: + payload['price'] = str(price) + + # Base64 encode the payload + encoded_payload = base64.b64encode(json.dumps(payload).encode()).decode() + + # Create the signature + signature = hmac.new( + GEMINI_API_SECRET.encode(), + encoded_payload.encode(), + hashlib.sha384 + ).hexdigest() + + url = f"{GEMINI_BASE_URL}/order/new" + headers = { + 'Content-Type': 'application/json', + 'X-GEMINI-APIKEY': GEMINI_API_KEY, + 'X-GEMINI-PAYLOAD': encoded_payload, + 'X-GEMINI-SIGNATURE': signature, + } + + try: + response = requests.post(url, headers=headers, json=payload) + response.raise_for_status() + print('Order placed:', response.json()) + except requests.exceptions.RequestException as e: + print(f'Error placing order: {e}') + if hasattr(e, 'response') and e.response is not None: + print(f'Response: {e.response.text}') + +if __name__ == '__main__': + place_order('BTCUSD', 'buy', 'limit', 0.01, 50000) diff --git a/samples/python/pm_order.py b/samples/python/pm_order.py new file mode 100644 index 00000000..6435384f --- /dev/null +++ b/samples/python/pm_order.py @@ -0,0 +1,117 @@ +import os +import json +import hmac +import hashlib +import base64 +import signal +import sys +import time +from websocket import WebSocketApp +from dotenv import load_dotenv + +load_dotenv() + +GEMINI_WS_URL = os.getenv('GEMINI_WS_URL', 'wss://ws.gemini.com') +GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') +GEMINI_API_SECRET = os.getenv('GEMINI_API_SECRET') + + +def place_prediction_order(symbol: str, side: str, price: str, quantity: str, outcome: str): + """Place a prediction market order via WebSocket.""" + if not GEMINI_API_KEY or not GEMINI_API_SECRET: + print('Error: GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file') + return + + # Authentication — signs a timestamp with your secret so Gemini + # can verify your identity. + nonce = str(int(time.time())) + payload = base64.b64encode(nonce.encode()).decode() + signature = hmac.new( + GEMINI_API_SECRET.encode(), + payload.encode(), + hashlib.sha384 + ).hexdigest() + + order_placed = False + + def on_open(ws): + print(f'Connected to {GEMINI_WS_URL}') + print(f'Subscribing to {symbol} prices and order updates...\n') + + ws.send(json.dumps({ + 'id': '1', + 'method': 'subscribe', + 'params': [ + f'{symbol}@bookTicker', # live prices + 'orders@account', # your order updates + ], + })) + + def on_message(ws, data): + nonlocal order_placed + msg = json.loads(data) + + # Once we see a price, place an order + if not order_placed and msg.get('b') and msg.get('a') and msg.get('s') == symbol: + print(f"Best bid: ${msg['b']} Best ask: ${msg['a']}") + print('Placing order...') + order_placed = True + + ws.send(json.dumps({ + 'id': '2', + 'method': 'order.place', + 'params': { + 'symbol': symbol, + 'side': side, + 'type': 'LIMIT', + 'timeInForce': 'GTC', + 'price': price, + 'quantity': quantity, + 'eventOutcome': outcome, + }, + })) + + # Order lifecycle updates from orders@account stream + # X = status, S = side, O = outcome, p = price, q = quantity + if msg.get('X') in ['NEW', 'OPEN', 'FILLED', 'PARTIALLY_FILLED']: + print(f"Order {msg['X']}: side={msg['S']} outcome={msg['O']} price=${msg['p']} qty={msg['q']}") + + def on_error(ws, err): + print(f'WebSocket error: {err}') + + def on_close(ws, status_code, msg): + print('Connection closed') + + ws = WebSocketApp( + GEMINI_WS_URL, + header={ + 'X-GEMINI-APIKEY': GEMINI_API_KEY, + 'X-GEMINI-NONCE': nonce, + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature, + }, + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close, + ) + + # Graceful shutdown + signal.signal(signal.SIGINT, lambda *_: (print('\nClosing connection...'), ws.close(), sys.exit(0))) + + ws.run_forever() + + +if __name__ == '__main__': + # Show help message + if '--help' in sys.argv or '-h' in sys.argv: + print('Usage: python3 pm_order.py [symbol]') + print('Example: python3 pm_order.py GEMI-PRES2028-VANCE') + print('Default symbol: GEMI-PRES2028-VANCE') + print('\nPlaces a BUY LIMIT order for 100 YES contracts at $0.27.') + print('Edit the script to change side, price, quantity, or outcome.') + sys.exit(0) + + # Get symbol from command line or use default + symbol = sys.argv[1] if len(sys.argv) > 1 else 'GEMI-PRES2028-VANCE' + place_prediction_order(symbol, 'BUY', '0.27', '100', 'YES') diff --git a/samples/python/pm_prices.py b/samples/python/pm_prices.py new file mode 100644 index 00000000..e5c3ccff --- /dev/null +++ b/samples/python/pm_prices.py @@ -0,0 +1,87 @@ +import os +import json +import hmac +import hashlib +import base64 +import signal +import sys +import time +from websocket import WebSocketApp +from dotenv import load_dotenv + +load_dotenv() + +GEMINI_WS_URL = os.getenv('GEMINI_WS_URL', 'wss://ws.gemini.com') +GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') +GEMINI_API_SECRET = os.getenv('GEMINI_API_SECRET') + + +def stream_prediction_prices(symbol: str): + """Stream real-time prediction market prices for a given symbol.""" + if not GEMINI_API_KEY or not GEMINI_API_SECRET: + print('Error: GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file') + return + + # Authentication — signs a timestamp with your secret so Gemini + # can verify your identity. + nonce = str(int(time.time())) + payload = base64.b64encode(nonce.encode()).decode() + signature = hmac.new( + GEMINI_API_SECRET.encode(), + payload.encode(), + hashlib.sha384 + ).hexdigest() + + def on_open(ws): + print(f'Connected to {GEMINI_WS_URL}') + print(f'Subscribing to {symbol} book ticker...\n') + + ws.send(json.dumps({ + 'id': '1', + 'method': 'subscribe', + 'params': [f'{symbol}@bookTicker'], + })) + + def on_message(ws, data): + msg = json.loads(data) + + if msg.get('b') and msg.get('a'): + print(f"{msg['s']} bid: ${msg['b']} ({msg['B']} contracts) ask: ${msg['a']} ({msg['A']} contracts)") + + def on_error(ws, err): + print(f'WebSocket error: {err}') + + def on_close(ws, status_code, msg): + print('Connection closed') + + ws = WebSocketApp( + GEMINI_WS_URL, + header={ + 'X-GEMINI-APIKEY': GEMINI_API_KEY, + 'X-GEMINI-NONCE': nonce, + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature, + }, + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close, + ) + + # Graceful shutdown + signal.signal(signal.SIGINT, lambda *_: (print('\nClosing connection...'), ws.close(), sys.exit(0))) + + ws.run_forever() + + +if __name__ == '__main__': + # Show help message + if '--help' in sys.argv or '-h' in sys.argv: + print('Usage: python3 pm_prices.py [symbol]') + print('Example: python3 pm_prices.py GEMI-PRES2028-VANCE') + print('Default symbol: GEMI-PRES2028-VANCE') + sys.exit(0) + + # Get symbol from command line or use default + symbol = sys.argv[1] if len(sys.argv) > 1 else 'GEMI-PRES2028-VANCE' + stream_prediction_prices(symbol) diff --git a/samples/python/price_feed.py b/samples/python/price_feed.py new file mode 100644 index 00000000..0c6cb6bb --- /dev/null +++ b/samples/python/price_feed.py @@ -0,0 +1,38 @@ +import os +import requests +from dotenv import load_dotenv + +load_dotenv() + +GEMINI_BASE_URL = os.getenv('GEMINI_BASE_URL', 'https://api.gemini.com/v1') + + +def get_price_feed(): + """Fetch current prices for all trading pairs.""" + url = f"{GEMINI_BASE_URL}/pricefeed" + + try: + response = requests.get(url) + response.raise_for_status() + pairs = response.json() + + print(f'Price feed ({len(pairs)} pairs):\n') + + for entry in pairs: + change = float(entry['percentChange24h']) + sign = '+' if change >= 0 else '' + print(f"{entry['pair']:<12} {entry['price']:>12} ({sign}{entry['percentChange24h']}%)") + except requests.exceptions.RequestException as e: + print(f'Error fetching price feed: {e}') + + +if __name__ == '__main__': + import sys + + # Show help message + if '--help' in sys.argv or '-h' in sys.argv: + print('Usage: python3 price_feed.py') + print('Fetches current prices for all trading pairs.') + sys.exit(0) + + get_price_feed() diff --git a/samples/python/requirements.txt b/samples/python/requirements.txt new file mode 100644 index 00000000..3a0ca266 --- /dev/null +++ b/samples/python/requirements.txt @@ -0,0 +1,3 @@ +requests==2.31.0 +python-dotenv==1.0.0 +websocket-client==1.7.0 diff --git a/samples/python/test_parse.py b/samples/python/test_parse.py new file mode 100644 index 00000000..0f1e4ccf --- /dev/null +++ b/samples/python/test_parse.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Test parsing the Gemini balances response""" +import json + +# Sample response from Gemini API +sample_response = """[ + { + "type": "exchange", + "currency": "BTC", + "amount": "5.0", + "available": "4.5", + "availableForWithdrawal": "4.5", + "pendingWithdrawal": "0.25", + "pendingDeposit": "0.25" + }, + { + "type": "exchange", + "currency": "USD", + "amount": "15000.00", + "available": "5000.00", + "availableForWithdrawal": "5000.00" + }, + { + "type": "exchange", + "currency": "ETH", + "amount": "10.0", + "available": "10.0", + "availableForWithdrawal": "10.0" + } +]""" + +# Parse the JSON +balances = json.loads(sample_response) + +# Display the parsed data +print("✅ Successfully parsed balances response!") +print(f"\nTotal currencies: {len(balances)}\n") + +for balance in balances: + print(f"Currency: {balance['currency']}") + print(f" Type: {balance['type']}") + print(f" Amount: {balance['amount']}") + print(f" Available: {balance['available']}") + print(f" Available for Withdrawal: {balance['availableForWithdrawal']}") + if 'pendingWithdrawal' in balance: + print(f" Pending Withdrawal: {balance['pendingWithdrawal']}") + if 'pendingDeposit' in balance: + print(f" Pending Deposit: {balance['pendingDeposit']}") + print() + +# Pretty print the full response +print("Full JSON response:") +print(json.dumps(balances, indent=2)) diff --git a/samples/python/trade_history.py b/samples/python/trade_history.py new file mode 100644 index 00000000..23e1c534 --- /dev/null +++ b/samples/python/trade_history.py @@ -0,0 +1,42 @@ +import os +import requests +from datetime import datetime, timezone +from dotenv import load_dotenv + +load_dotenv() + +GEMINI_BASE_URL = os.getenv('GEMINI_BASE_URL', 'https://api.gemini.com/v1') + + +def get_trade_history(symbol: str): + """Fetch recent trade history for a given symbol.""" + symbol_lower = symbol.lower() + url = f"{GEMINI_BASE_URL}/trades/{symbol_lower}" + + try: + response = requests.get(url) + response.raise_for_status() + trades = response.json() + + print(f'Recent trades for {symbol} ({len(trades)} trades):\n') + + for trade in trades: + time = datetime.fromtimestamp(trade['timestampms'] / 1000, tz=timezone.utc).isoformat() + print(f"[{time}] {trade['type']:<4} {trade['amount']} @ {trade['price']} (tid: {trade['tid']})") + except requests.exceptions.RequestException as e: + print(f'Error fetching trade history: {e}') + + +if __name__ == '__main__': + import sys + + # Show help message + if '--help' in sys.argv or '-h' in sys.argv: + print('Usage: python3 trade_history.py [symbol]') + print('Example: python3 trade_history.py ethusd') + print('Default symbol: btcusd') + sys.exit(0) + + # Get symbol from command line or use default + symbol = sys.argv[1] if len(sys.argv) > 1 else 'btcusd' + get_trade_history(symbol) diff --git a/samples/python/ws_book_ticker.py b/samples/python/ws_book_ticker.py new file mode 100644 index 00000000..1239e8ac --- /dev/null +++ b/samples/python/ws_book_ticker.py @@ -0,0 +1,67 @@ +import os +import json +import signal +import sys +from datetime import datetime, timezone +from websocket import WebSocketApp +from dotenv import load_dotenv + +load_dotenv() + +GEMINI_WS_URL = os.getenv('GEMINI_WS_URL', 'wss://ws.gemini.com') + + +def stream_book_ticker(symbol: str): + """Stream real-time best bid/ask for a given symbol.""" + + def on_open(ws): + print(f'Connected to {GEMINI_WS_URL}') + print(f'Subscribing to {symbol} book ticker...\n') + + ws.send(json.dumps({ + 'id': '1', + 'method': 'subscribe', + 'params': [f'{symbol}@bookTicker'], + })) + + def on_message(ws, data): + msg = json.loads(data) + + # Skip subscription confirmations + if 'result' in msg or 'id' in msg: + return + + time = datetime.fromtimestamp(msg['E'] / 1_000_000_000, tz=timezone.utc).isoformat() + print(f"[{time}] {msg['s']} bid {msg['b']} x {msg['B']} | ask {msg['a']} x {msg['A']}") + + def on_error(ws, err): + print(f'WebSocket error: {err}') + + def on_close(ws, status_code, msg): + print('Connection closed') + + ws = WebSocketApp( + GEMINI_WS_URL, + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close, + ) + + # Graceful shutdown + signal.signal(signal.SIGINT, lambda *_: (print('\nClosing connection...'), ws.close(), sys.exit(0))) + + ws.run_forever() + + +if __name__ == '__main__': + # Show help message + if '--help' in sys.argv or '-h' in sys.argv: + print('Usage: python3 ws_book_ticker.py [symbol]') + print('Example: python3 ws_book_ticker.py ethusd') + print('Default symbol: btcusd') + sys.exit(0) + + # Get symbol from command line or use default + symbol = sys.argv[1] if len(sys.argv) > 1 else 'btcusd' + stream_book_ticker(symbol) diff --git a/samples/python/ws_trades.py b/samples/python/ws_trades.py new file mode 100644 index 00000000..cd689c1f --- /dev/null +++ b/samples/python/ws_trades.py @@ -0,0 +1,68 @@ +import os +import json +import signal +import sys +from datetime import datetime, timezone +from websocket import WebSocketApp +from dotenv import load_dotenv + +load_dotenv() + +GEMINI_WS_URL = os.getenv('GEMINI_WS_URL', 'wss://ws.gemini.com') + + +def stream_trades(symbol: str): + """Stream real-time trades for a given symbol.""" + + def on_open(ws): + print(f'Connected to {GEMINI_WS_URL}') + print(f'Subscribing to {symbol} trades...\n') + + ws.send(json.dumps({ + 'id': '1', + 'method': 'subscribe', + 'params': [f'{symbol}@trade'], + })) + + def on_message(ws, data): + msg = json.loads(data) + + # Skip subscription confirmations + if 'result' in msg or 'id' in msg: + return + + side = 'sell' if msg['m'] else 'buy ' + time = datetime.fromtimestamp(msg['E'] / 1_000_000_000, tz=timezone.utc).isoformat() + print(f"[{time}] {msg['s']} {side} {msg['q']} @ {msg['p']}") + + def on_error(ws, err): + print(f'WebSocket error: {err}') + + def on_close(ws, status_code, msg): + print('Connection closed') + + ws = WebSocketApp( + GEMINI_WS_URL, + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close, + ) + + # Graceful shutdown + signal.signal(signal.SIGINT, lambda *_: (print('\nClosing connection...'), ws.close(), sys.exit(0))) + + ws.run_forever() + + +if __name__ == '__main__': + # Show help message + if '--help' in sys.argv or '-h' in sys.argv: + print('Usage: python3 ws_trades.py [symbol]') + print('Example: python3 ws_trades.py ethusd') + print('Default symbol: btcusd') + sys.exit(0) + + # Get symbol from command line or use default + symbol = sys.argv[1] if len(sys.argv) > 1 else 'btcusd' + stream_trades(symbol) diff --git a/samples/test_data.json b/samples/test_data.json new file mode 100644 index 00000000..2db8aef5 --- /dev/null +++ b/samples/test_data.json @@ -0,0 +1,25 @@ +[ + { + "type": "exchange", + "currency": "BTC", + "amount": "5.0", + "available": "4.5", + "availableForWithdrawal": "4.5", + "pendingWithdrawal": "0.25", + "pendingDeposit": "0.25" + }, + { + "type": "exchange", + "currency": "USD", + "amount": "15000.00", + "available": "5000.00", + "availableForWithdrawal": "5000.00" + }, + { + "type": "exchange", + "currency": "ETH", + "amount": "10.0", + "available": "10.0", + "availableForWithdrawal": "10.0" + } +] diff --git a/samples/typescript/package-lock.json b/samples/typescript/package-lock.json new file mode 100644 index 00000000..5e6ee176 --- /dev/null +++ b/samples/typescript/package-lock.json @@ -0,0 +1,564 @@ +{ + "name": "api-examples-typescript", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "api-examples-typescript", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "^1.4.0", + "dotenv": "^16.0.3", + "ws": "^8.16.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/ws": "^8.5.10", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", + "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/samples/typescript/package.json b/samples/typescript/package.json new file mode 100644 index 00000000..dcac744f --- /dev/null +++ b/samples/typescript/package.json @@ -0,0 +1,33 @@ +{ + "name": "api-examples-typescript", + "version": "1.0.0", + "description": "TypeScript examples for API usage", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "get-ticker": "ts-node src/getTicker.ts", + "balances": "ts-node src/balances.ts", + "place-order": "ts-node src/placeOrder.ts", + "ws-trades": "ts-node src/wsTrades.ts", + "trade-history": "ts-node src/tradeHistory.ts", + "price-feed": "ts-node src/priceFeed.ts", + "order-book": "ts-node src/orderBook.ts", + "ws-book-ticker": "ts-node src/wsBookTicker.ts", + "pm-prices": "ts-node src/pmPrices.ts", + "pm-order": "ts-node src/pmOrder.ts" + }, + "keywords": ["api", "examples"], + "author": "", + "license": "MIT", + "dependencies": { + "dotenv": "^16.0.3", + "axios": "^1.4.0", + "ws": "^8.16.0" + }, + "devDependencies": { + "@types/ws": "^8.5.10", + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "ts-node": "^10.9.0" + } +} diff --git a/samples/typescript/src/balances.ts b/samples/typescript/src/balances.ts new file mode 100644 index 00000000..1f941a78 --- /dev/null +++ b/samples/typescript/src/balances.ts @@ -0,0 +1,67 @@ +import axios from 'axios'; +import * as crypto from 'crypto'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_BASE_URL = process.env.GEMINI_BASE_URL || 'https://api.gemini.com/v1'; +const GEMINI_API_KEY = process.env.GEMINI_API_KEY; +const GEMINI_API_SECRET = process.env.GEMINI_API_SECRET; + +interface Balance { + type: string; + currency: string; + amount: string; + available: string; + availableForWithdrawal: string; + pendingWithdrawal?: string; + pendingDeposit?: string; +} + +async function getBalances(): Promise { + if (!GEMINI_API_KEY || !GEMINI_API_SECRET) { + console.error('Error: GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file'); + return; + } + + try { + // Create the payload + const payload = { + request: '/v1/balances', + nonce: Date.now().toString(), + }; + + // Base64 encode the payload + const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64'); + + // Create the signature + const signature = crypto + .createHmac('sha384', GEMINI_API_SECRET) + .update(encodedPayload) + .digest('hex'); + + // Make the request + const response = await axios.post(`${GEMINI_BASE_URL}/balances`, payload, { + headers: { + 'Content-Type': 'application/json', + 'X-GEMINI-APIKEY': GEMINI_API_KEY, + 'X-GEMINI-PAYLOAD': encodedPayload, + 'X-GEMINI-SIGNATURE': signature, + }, + }); + + console.log('Account balances:'); + console.log(JSON.stringify(response.data, null, 2)); + + // Summary + console.log(`\nTotal currencies: ${response.data.length}`); + response.data.forEach(balance => { + console.log(`${balance.currency}: ${balance.available} available (${balance.amount} total)`); + }); + } catch (error: any) { + console.error('Error fetching balances:', error.response?.data || error.message); + } +} + +// Example usage +getBalances(); diff --git a/samples/typescript/src/getTicker.ts b/samples/typescript/src/getTicker.ts new file mode 100644 index 00000000..aa5fc5c7 --- /dev/null +++ b/samples/typescript/src/getTicker.ts @@ -0,0 +1,30 @@ +import axios from 'axios'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_BASE_URL = process.env.GEMINI_BASE_URL || 'https://api.gemini.com/v1'; + +async function getTicker(symbol: string): Promise { + try { + // Symbol should be lowercase for Gemini API + const symbolLower = symbol.toLowerCase(); + const response = await axios.get(`${GEMINI_BASE_URL}/pubticker/${symbolLower}`); + + console.log('Ticker data:', response.data); + } catch (error) { + console.error('Error fetching ticker:', error); + } +} + +// Get symbol from command line or use default +const symbol = process.argv[2] || 'btcusd'; + +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log('Usage: npx ts-node src/getTicker.ts [symbol]'); + console.log('Example: npx ts-node src/getTicker.ts ethusd'); + console.log('Default symbol: btcusd'); + process.exit(0); +} + +getTicker(symbol); diff --git a/samples/typescript/src/orderBook.ts b/samples/typescript/src/orderBook.ts new file mode 100644 index 00000000..f2102e96 --- /dev/null +++ b/samples/typescript/src/orderBook.ts @@ -0,0 +1,47 @@ +import axios from 'axios'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_BASE_URL = process.env.GEMINI_BASE_URL || 'https://api.gemini.com/v1'; + +async function getOrderBook(symbol: string): Promise { + try { + const symbolLower = symbol.toLowerCase(); + const response = await axios.get(`${GEMINI_BASE_URL}/book/${symbolLower}`); + + const { bids, asks } = response.data; + + console.log(`Order book for ${symbol}:\n`); + + console.log('Asks (top 5):'); + for (const ask of asks.slice(0, 5).reverse()) { + console.log(` ${ask.price.padStart(12)} | ${ask.amount}`); + } + + console.log(' ------------|------------'); + + console.log('Bids (top 5):'); + for (const bid of bids.slice(0, 5)) { + console.log(` ${bid.price.padStart(12)} | ${bid.amount}`); + } + + if (asks.length > 0 && bids.length > 0) { + console.log(`\nSpread: ${(parseFloat(asks[0].price) - parseFloat(bids[0].price)).toFixed(2)}`); + } + } catch (error) { + console.error('Error fetching order book:', error); + } +} + +// Get symbol from command line or use default +const symbol = process.argv[2] || 'btcusd'; + +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log('Usage: npx ts-node src/orderBook.ts [symbol]'); + console.log('Example: npx ts-node src/orderBook.ts ethusd'); + console.log('Default symbol: btcusd'); + process.exit(0); +} + +getOrderBook(symbol); diff --git a/samples/typescript/src/placeOrder.ts b/samples/typescript/src/placeOrder.ts new file mode 100644 index 00000000..2f1c8312 --- /dev/null +++ b/samples/typescript/src/placeOrder.ts @@ -0,0 +1,76 @@ +import axios from 'axios'; +import * as crypto from 'crypto'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_BASE_URL = process.env.GEMINI_BASE_URL || 'https://api.gemini.com/v1'; +const GEMINI_API_KEY = process.env.GEMINI_API_KEY; +const GEMINI_API_SECRET = process.env.GEMINI_API_SECRET; + +interface OrderParams { + symbol: string; + side: 'buy' | 'sell'; + type: 'market' | 'limit'; + quantity: string; + price?: string; +} + +async function placeOrder(params: OrderParams): Promise { + if (!GEMINI_API_KEY || !GEMINI_API_SECRET) { + console.error('Error: GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file'); + return; + } + + try { + // Create the payload + const payload: Record = { + request: '/v1/order/new', + nonce: Date.now().toString(), + symbol: params.symbol, + amount: params.quantity, + side: params.side, + type: params.type, + }; + + if (params.price !== undefined) { + payload.price = params.price; + } + + // Base64 encode the payload + const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64'); + + // Create the signature + const signature = crypto + .createHmac('sha384', GEMINI_API_SECRET) + .update(encodedPayload) + .digest('hex'); + + // Make the request + const response = await axios.post( + `${GEMINI_BASE_URL}/order/new`, + payload, + { + headers: { + 'Content-Type': 'application/json', + 'X-GEMINI-APIKEY': GEMINI_API_KEY, + 'X-GEMINI-PAYLOAD': encodedPayload, + 'X-GEMINI-SIGNATURE': signature, + }, + } + ); + + console.log('Order placed:', response.data); + } catch (error: any) { + console.error('Error placing order:', error.response?.data || error.message); + } +} + +// Example usage +placeOrder({ + symbol: 'BTCUSD', + side: 'buy', + type: 'limit', + quantity: '0.01', + price: '50000', +}); diff --git a/samples/typescript/src/pmOrder.ts b/samples/typescript/src/pmOrder.ts new file mode 100644 index 00000000..25ae34bf --- /dev/null +++ b/samples/typescript/src/pmOrder.ts @@ -0,0 +1,110 @@ +import * as crypto from 'crypto'; +import WebSocket from 'ws'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_WS_URL = process.env.GEMINI_WS_URL || 'wss://ws.gemini.com'; +const GEMINI_API_KEY = process.env.GEMINI_API_KEY; +const GEMINI_API_SECRET = process.env.GEMINI_API_SECRET; + +function placePredictionOrder(symbol: string, side: string, price: string, quantity: string, outcome: string): void { + if (!GEMINI_API_KEY || !GEMINI_API_SECRET) { + console.error('Error: GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file'); + return; + } + + // Authentication — signs a timestamp with your secret so Gemini + // can verify your identity. + const nonce = Math.floor(Date.now() / 1000).toString(); + const payload = Buffer.from(nonce).toString('base64'); + const signature = crypto + .createHmac('sha384', GEMINI_API_SECRET) + .update(payload) + .digest('hex'); + + const ws = new WebSocket(GEMINI_WS_URL, { + headers: { + 'X-GEMINI-APIKEY': GEMINI_API_KEY, + 'X-GEMINI-NONCE': nonce, + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature, + }, + }); + + ws.on('open', () => { + console.log(`Connected to ${GEMINI_WS_URL}`); + console.log(`Subscribing to ${symbol} prices and order updates...\n`); + + ws.send(JSON.stringify({ + id: '1', + method: 'subscribe', + params: [ + `${symbol}@bookTicker`, // live prices + 'orders@account', // your order updates + ], + })); + }); + + let orderPlaced = false; + + ws.on('message', (raw: WebSocket.Data) => { + const data = JSON.parse(raw.toString()); + + // Once we see a price, place an order + if (!orderPlaced && data.b && data.a && data.s === symbol) { + console.log(`Best bid: $${data.b} Best ask: $${data.a}`); + console.log('Placing order...'); + orderPlaced = true; + + ws.send(JSON.stringify({ + id: '2', + method: 'order.place', + params: { + symbol, + side, + type: 'LIMIT', + timeInForce: 'GTC', + price, + quantity, + eventOutcome: outcome, + }, + })); + } + + // Order lifecycle updates from orders@account stream + // X = status, S = side, O = outcome, p = price, q = quantity + if (['NEW', 'OPEN', 'FILLED', 'PARTIALLY_FILLED'].includes(data.X)) { + console.log(`Order ${data.X}: side=${data.S} outcome=${data.O} price=$${data.p} qty=${data.q}`); + } + }); + + ws.on('error', (err: Error) => { + console.error('WebSocket error:', err.message); + }); + + ws.on('close', () => { + console.log('Connection closed'); + }); + + // Graceful shutdown + process.on('SIGINT', () => { + console.log('\nClosing connection...'); + ws.close(); + process.exit(0); + }); +} + +// Example usage — adjust parameters as needed +const symbol = process.argv[2] || 'GEMI-PRES2028-VANCE'; + +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log('Usage: npx ts-node src/pmOrder.ts [symbol]'); + console.log('Example: npx ts-node src/pmOrder.ts GEMI-PRES2028-VANCE'); + console.log('Default symbol: GEMI-PRES2028-VANCE'); + console.log('\nPlaces a BUY LIMIT order for 100 YES contracts at $0.27.'); + console.log('Edit the script to change side, price, quantity, or outcome.'); + process.exit(0); +} + +placePredictionOrder(symbol, 'BUY', '0.27', '100', 'YES'); diff --git a/samples/typescript/src/pmPrices.ts b/samples/typescript/src/pmPrices.ts new file mode 100644 index 00000000..222f5f3b --- /dev/null +++ b/samples/typescript/src/pmPrices.ts @@ -0,0 +1,80 @@ +import * as crypto from 'crypto'; +import WebSocket from 'ws'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_WS_URL = process.env.GEMINI_WS_URL || 'wss://ws.gemini.com'; +const GEMINI_API_KEY = process.env.GEMINI_API_KEY; +const GEMINI_API_SECRET = process.env.GEMINI_API_SECRET; + +function streamPredictionPrices(symbol: string): void { + if (!GEMINI_API_KEY || !GEMINI_API_SECRET) { + console.error('Error: GEMINI_API_KEY and GEMINI_API_SECRET must be set in .env file'); + return; + } + + // Authentication — signs a timestamp with your secret so Gemini + // can verify your identity. + const nonce = Math.floor(Date.now() / 1000).toString(); + const payload = Buffer.from(nonce).toString('base64'); + const signature = crypto + .createHmac('sha384', GEMINI_API_SECRET) + .update(payload) + .digest('hex'); + + const ws = new WebSocket(GEMINI_WS_URL, { + headers: { + 'X-GEMINI-APIKEY': GEMINI_API_KEY, + 'X-GEMINI-NONCE': nonce, + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature, + }, + }); + + ws.on('open', () => { + console.log(`Connected to ${GEMINI_WS_URL}`); + console.log(`Subscribing to ${symbol} book ticker...\n`); + + ws.send(JSON.stringify({ + id: '1', + method: 'subscribe', + params: [`${symbol}@bookTicker`], + })); + }); + + ws.on('message', (raw: WebSocket.Data) => { + const data = JSON.parse(raw.toString()); + + if (data.b && data.a) { + console.log(`${data.s} bid: $${data.b} (${data.B} contracts) ask: $${data.a} (${data.A} contracts)`); + } + }); + + ws.on('error', (err: Error) => { + console.error('WebSocket error:', err.message); + }); + + ws.on('close', () => { + console.log('Connection closed'); + }); + + // Graceful shutdown + process.on('SIGINT', () => { + console.log('\nClosing connection...'); + ws.close(); + process.exit(0); + }); +} + +// Get symbol from command line or use default +const symbol = process.argv[2] || 'GEMI-PRES2028-VANCE'; + +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log('Usage: npx ts-node src/pmPrices.ts [symbol]'); + console.log('Example: npx ts-node src/pmPrices.ts GEMI-PRES2028-VANCE'); + console.log('Default symbol: GEMI-PRES2028-VANCE'); + process.exit(0); +} + +streamPredictionPrices(symbol); diff --git a/samples/typescript/src/priceFeed.ts b/samples/typescript/src/priceFeed.ts new file mode 100644 index 00000000..b1e03862 --- /dev/null +++ b/samples/typescript/src/priceFeed.ts @@ -0,0 +1,31 @@ +import axios from 'axios'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_BASE_URL = process.env.GEMINI_BASE_URL || 'https://api.gemini.com/v1'; + +async function getPriceFeed(): Promise { + try { + const response = await axios.get(`${GEMINI_BASE_URL}/pricefeed`); + + const pairs = response.data; + console.log(`Price feed (${pairs.length} pairs):\n`); + + for (const entry of pairs) { + const change = parseFloat(entry.percentChange24h); + const sign = change >= 0 ? '+' : ''; + console.log(`${entry.pair.padEnd(12)} ${entry.price.padStart(12)} (${sign}${entry.percentChange24h}%)`); + } + } catch (error) { + console.error('Error fetching price feed:', error); + } +} + +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log('Usage: npx ts-node src/priceFeed.ts'); + console.log('Fetches current prices for all trading pairs.'); + process.exit(0); +} + +getPriceFeed(); diff --git a/samples/typescript/src/tradeHistory.ts b/samples/typescript/src/tradeHistory.ts new file mode 100644 index 00000000..ea608462 --- /dev/null +++ b/samples/typescript/src/tradeHistory.ts @@ -0,0 +1,35 @@ +import axios from 'axios'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_BASE_URL = process.env.GEMINI_BASE_URL || 'https://api.gemini.com/v1'; + +async function getTradeHistory(symbol: string): Promise { + try { + const symbolLower = symbol.toLowerCase(); + const response = await axios.get(`${GEMINI_BASE_URL}/trades/${symbolLower}`); + + const trades = response.data; + console.log(`Recent trades for ${symbol} (${trades.length} trades):\n`); + + for (const trade of trades) { + const time = new Date(trade.timestampms).toISOString(); + console.log(`[${time}] ${trade.type.padEnd(4)} ${trade.amount} @ ${trade.price} (tid: ${trade.tid})`); + } + } catch (error) { + console.error('Error fetching trade history:', error); + } +} + +// Get symbol from command line or use default +const symbol = process.argv[2] || 'btcusd'; + +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log('Usage: npx ts-node src/tradeHistory.ts [symbol]'); + console.log('Example: npx ts-node src/tradeHistory.ts ethusd'); + console.log('Default symbol: btcusd'); + process.exit(0); +} + +getTradeHistory(symbol); diff --git a/samples/typescript/src/wsBookTicker.ts b/samples/typescript/src/wsBookTicker.ts new file mode 100644 index 00000000..536006d4 --- /dev/null +++ b/samples/typescript/src/wsBookTicker.ts @@ -0,0 +1,58 @@ +import WebSocket from 'ws'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_WS_URL = process.env.GEMINI_WS_URL || 'wss://ws.gemini.com'; + +function streamBookTicker(symbol: string): void { + const ws = new WebSocket(GEMINI_WS_URL); + + ws.on('open', () => { + console.log(`Connected to ${GEMINI_WS_URL}`); + console.log(`Subscribing to ${symbol} book ticker...\n`); + + ws.send(JSON.stringify({ + id: '1', + method: 'subscribe', + params: [`${symbol}@bookTicker`], + })); + }); + + ws.on('message', (data: WebSocket.Data) => { + const msg = JSON.parse(data.toString()); + + // Skip subscription confirmations + if (msg.result || msg.id) return; + + const time = new Date(msg.E / 1_000_000).toISOString(); + console.log(`[${time}] ${msg.s} bid ${msg.b} x ${msg.B} | ask ${msg.a} x ${msg.A}`); + }); + + ws.on('error', (err: Error) => { + console.error('WebSocket error:', err.message); + }); + + ws.on('close', () => { + console.log('Connection closed'); + }); + + // Graceful shutdown + process.on('SIGINT', () => { + console.log('\nClosing connection...'); + ws.close(); + process.exit(0); + }); +} + +// Get symbol from command line or use default +const symbol = process.argv[2] || 'btcusd'; + +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log('Usage: npx ts-node src/wsBookTicker.ts [symbol]'); + console.log('Example: npx ts-node src/wsBookTicker.ts ethusd'); + console.log('Default symbol: btcusd'); + process.exit(0); +} + +streamBookTicker(symbol); diff --git a/samples/typescript/src/wsTrades.ts b/samples/typescript/src/wsTrades.ts new file mode 100644 index 00000000..07fd593c --- /dev/null +++ b/samples/typescript/src/wsTrades.ts @@ -0,0 +1,59 @@ +import WebSocket from 'ws'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const GEMINI_WS_URL = process.env.GEMINI_WS_URL || 'wss://ws.gemini.com'; + +function streamTrades(symbol: string): void { + const ws = new WebSocket(GEMINI_WS_URL); + + ws.on('open', () => { + console.log(`Connected to ${GEMINI_WS_URL}`); + console.log(`Subscribing to ${symbol} trades...\n`); + + ws.send(JSON.stringify({ + id: '1', + method: 'subscribe', + params: [`${symbol}@trade`], + })); + }); + + ws.on('message', (data: WebSocket.Data) => { + const msg = JSON.parse(data.toString()); + + // Skip subscription confirmations + if (msg.result || msg.id) return; + + const side = msg.m ? 'sell' : 'buy '; + const time = new Date(msg.E / 1_000_000).toISOString(); + console.log(`[${time}] ${msg.s} ${side} ${msg.q} @ ${msg.p}`); + }); + + ws.on('error', (err: Error) => { + console.error('WebSocket error:', err.message); + }); + + ws.on('close', () => { + console.log('Connection closed'); + }); + + // Graceful shutdown + process.on('SIGINT', () => { + console.log('\nClosing connection...'); + ws.close(); + process.exit(0); + }); +} + +// Get symbol from command line or use default +const symbol = process.argv[2] || 'btcusd'; + +if (process.argv.includes('--help') || process.argv.includes('-h')) { + console.log('Usage: npx ts-node src/wsTrades.ts [symbol]'); + console.log('Example: npx ts-node src/wsTrades.ts ethusd'); + console.log('Default symbol: btcusd'); + process.exit(0); +} + +streamTrades(symbol); diff --git a/samples/typescript/test_parse.ts b/samples/typescript/test_parse.ts new file mode 100644 index 00000000..ec9a92b5 --- /dev/null +++ b/samples/typescript/test_parse.ts @@ -0,0 +1,64 @@ +#!/usr/bin/env ts-node +/** + * Test parsing the Gemini balances response + */ + +interface Balance { + type: string; + currency: string; + amount: string; + available: string; + availableForWithdrawal: string; + pendingWithdrawal?: string; + pendingDeposit?: string; +} + +// Sample response from Gemini API +const sampleResponse: Balance[] = [ + { + type: "exchange", + currency: "BTC", + amount: "5.0", + available: "4.5", + availableForWithdrawal: "4.5", + pendingWithdrawal: "0.25", + pendingDeposit: "0.25" + }, + { + type: "exchange", + currency: "USD", + amount: "15000.00", + available: "5000.00", + availableForWithdrawal: "5000.00" + }, + { + type: "exchange", + currency: "ETH", + amount: "10.0", + available: "10.0", + availableForWithdrawal: "10.0" + } +]; + +// Display the parsed data +console.log("✅ Successfully parsed balances response!"); +console.log(`\nTotal currencies: ${sampleResponse.length}\n`); + +sampleResponse.forEach(balance => { + console.log(`Currency: ${balance.currency}`); + console.log(` Type: ${balance.type}`); + console.log(` Amount: ${balance.amount}`); + console.log(` Available: ${balance.available}`); + console.log(` Available for Withdrawal: ${balance.availableForWithdrawal}`); + if (balance.pendingWithdrawal) { + console.log(` Pending Withdrawal: ${balance.pendingWithdrawal}`); + } + if (balance.pendingDeposit) { + console.log(` Pending Deposit: ${balance.pendingDeposit}`); + } + console.log(); +}); + +// Pretty print the full response +console.log("Full JSON response:"); +console.log(JSON.stringify(sampleResponse, null, 2)); diff --git a/samples/typescript/tsconfig.json b/samples/typescript/tsconfig.json new file mode 100644 index 00000000..9b415ad8 --- /dev/null +++ b/samples/typescript/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/skills/gemini-candles/SKILL.md b/skills/gemini-candles/SKILL.md new file mode 100644 index 00000000..5b553838 --- /dev/null +++ b/skills/gemini-candles/SKILL.md @@ -0,0 +1,28 @@ +--- +name: gemini-candles +description: Display candlestick charts for Gemini trading pairs in the terminal +--- + +# gemini-candles + +Display terminal-based candlestick charts for any Gemini trading pair using live market data. + +## When to use + +When the user asks to see a candlestick chart, candle chart, price chart, or OHLC chart for a trading pair on Gemini. + +## Instructions + +1. Determine the trading pair symbol (e.g., `btcusd`, `ethusd`) and timeframe from the user's request. Default to `1d` if no timeframe is specified. Valid timeframes: `1m`, `5m`, `15m`, `30m`, `1hr`, `6hr`, `1day`. + +2. Download candle data and convert to CSV entirely on disk using a single bash command. Do NOT use WebFetch or any tool that loads the API response into context. Use curl and jq piped together: + ```bash + (echo "timestamp,open,high,low,close,volume" && curl -s "https://api.gemini.com/v2/candles/{symbol}/{time_frame}" | jq -r '.[] | [.[0], .[1], .[2], .[3], .[4], .[5]] | @csv') > /tmp/gemini_candles.csv + ``` + +3. Run the chart using npx: + ```bash + npx -y @neabyte/candlestick-cli -f /tmp/gemini_candles.csv -t "{SYMBOL} {timeframe}" 2>&1 | tail -n +8 + ``` + +4. Display the chart output to the user. **IMPORTANT: Output the raw terminal output of the candlestick-cli command verbatim inside a code block. Do NOT summarize, interpret, or describe the chart. The user needs to see the actual rendered ASCII chart, not a description of it.**