{"id":25864000,"url":"https://github.com/boringcontributor/btc-guessr","last_synced_at":"2026-04-11T02:35:45.165Z","repository":{"id":57861424,"uuid":"528605518","full_name":"boringContributor/btc-guessr","owner":"boringContributor","description":"BitcoinGuessr is a game to guess whether the Bitcoin price in USD is higher or lower after 60 seconds","archived":false,"fork":false,"pushed_at":"2022-08-26T07:42:54.000Z","size":2376,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-02T00:28:23.162Z","etag":null,"topics":["aws","react","serverless","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/boringContributor.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-08-24T21:50:28.000Z","updated_at":"2023-03-07T06:23:42.000Z","dependencies_parsed_at":"2023-01-16T19:00:39.656Z","dependency_job_id":null,"html_url":"https://github.com/boringContributor/btc-guessr","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/boringContributor/btc-guessr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringContributor%2Fbtc-guessr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringContributor%2Fbtc-guessr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringContributor%2Fbtc-guessr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringContributor%2Fbtc-guessr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/boringContributor","download_url":"https://codeload.github.com/boringContributor/btc-guessr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringContributor%2Fbtc-guessr/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267701307,"owners_count":24130448,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-29T02:00:12.549Z","response_time":2574,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aws","react","serverless","typescript"],"created_at":"2025-03-02T00:27:38.661Z","updated_at":"2026-04-11T02:35:40.134Z","avatar_url":"https://github.com/boringContributor.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BTC Guessr Game\n\nBTC Guessr is a game about guessing whether the Bitcoin price will be higher or lower after a 60 seconds time frame. Any user can play the game without the need to log in. Additionally, the score history is synced with local storage, which allows to close the browser and continue at a later point in time. Below sequence diagram shows the general flow of the application.\n\n![sequence.png](/diagrams/sequence.png)\n\n\n\n\n## Frontend\nThe initial application used NextJS, but I faced [errors]((https://github.com/pmndrs/zustand/issues/938)) with Zustands persist middleware in combination with NextJS. There is currently no straight forward way to opt-out SSR in NextJS, so I migrated the frontend codebase to Vite.\n\nThe frontend application is built on React 18 with the following dependencies:\n- TailwindCSS\n- React Query: takes over the long polling process\n- Zustand: lightweight state management library, which helps to sync the users history with the local storage\n- Vite: bundles React and offers a great DX\n\n### Local development\n1. Install dependencies with\n   ```\n   yarn\n   ```\n\n2. Create a `.env` or `.env.local` file with the API Gateway endpoint:\n      ```\n      VITE_BACKEND_API_URL=https://...\n    ```\n1. Run the application with\n   ```\n   yarn dev\n   ```\n\n### Testing\nSetup with [react-testing-library](https://testing-library.com/docs/react-testing-library/intro/), [vitest](https://vitest.dev/) and [msw](https://mswjs.io/).\nThere are some very basic tests executed with\n   ```\n   yarn test\n   ```\n\n### Deployment\nThe web application is deployed to Vercel, as it was the fastest way to get things up and running.\n1. Production build\n   ```\n   yarn build\n   ```\n2. Run the production build\n   ```\n   yarn preview\n   ```\n### Core Logic\nThe frontend uses 4 hooks to implement its business logic:\n1. **useBitcoinStats**: fetch the [Coingecko API](https://www.coingecko.com/en/api/documentation) and get the latest stats e.g. BTC price in USD and how it changed during the last week.\n2. **useNewGame**: HTTP POST request to the backend endpoint **/new-guess** with the payload:\n    ```\n   {\n     \"guess\": \"up\" // or \"down\"\n   }\n   ```\n   This request returns a game ID, which is stored in the React query cache. This allows the user to close/reload the browers and continue with the started processing guess. If the ID is set, the following hook is triggered.\n\n3. **useNewGuess**: HTTP GET request on the backend endpoint **/check-result/{id}**. This hook starts long polling to check if the game result is available. It polls the result every 20 seconds. The result is then stored in a history, which is also synced with the local storage.\n\n4. **useScore**: calculates the current score based on the score history.\n\n## Backend\nThe backend utilizes AWS and its CDK.\n\n### Deployment\n\nHave your [AWS config files](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) set to use profiles\n```\nexport AWS_PROFILE=xyz\n```\n\nBootstrap your CDK\n```\nyarn cdk bootstrap\n```\n\nDeploy to AWS\n```\nyarn cdk deploy\n```\n\n### Architecture\n\nWhen the user starts the game through the click of a button (either \"up\" or \"down\") a POST request is sent to the API Gateway route /new-guess. This endpoint returns a new game ID to the client. The request also triggers an EventBridge rule \"putEvent\", which starts the async execution of a Step Function State Machine. This state machine first stores the initial game date to DDB, and then waits 60 seconds before checking if the price changed and the user guessed correctly or not. In case of the price not changing, it waits another additional 20 seconds before repeating the previous check. This final result is stored with the previous DynamoDB entry. During the whole process the client has the option to get status updates, by executing a GET request on /check-result/{id}. In addition to above mentioned information, a game status of \"processing\" or \"finished\" is returned by this endpoint.\n\n\n![architecture.png](/diagrams/architecture.png)\n\n### API Gateway\nAPI Gateway is the entry point to the application. It offers two REST based endpoints\n\n#### Start a new game\n\n**URL** : `/new-guess`\n\n**Method** : `POST`\n\n**Auth required** : NO\n\n**Permissions required** : None\n\n**Data constraints**\n\nThe request body has one property **guess**, which only allows either \"down\" or \"up\"\n```json\n{\n    \"guess\": \"up\" // or \"down\"\n}\n```\n##### Success Response\n\n**Code** : `201 OK`\n\n```json\n{\n    \"id\": \"23423423-24234234-45523544-4234234\",\n}\n```\n#### Check if the game is finished or still processing\n\n**URL** : `/check-result/{id}`\n\n**Method** : `GET`\n\n**Auth required** : NO\n\n**Permissions required** : None\n\n##### Success Response\n\n**Code** : `200 OK`\n\n```json\n{\n    \"id\": \"23423423-24234234-45523544-4234234\",\n    \"guess\": \"up\",\n    \"oldPrice\": 21123.23, // the initial price\n    \"timestamp\": \"2022-08-24T17:17:17.979Z\",\n    \"isCorrectGuess\": true, // optional and only set when game is finished\n    \"gameStatus\": \"finished\", // either \"finished\" or \"processing\"\n    \"latestPrice\": 21739.37 // the final price after 60(+-) seconds only set when gameStatus is \"finished\"\n}\n```\n\n##### Error Response\n**Code** : `404 NOT FOUND`\n\nThe input validation is done through the API Gateway itself.\n\n### EventBridge\nAs there is no way to integrate Step Functions in an async workflow with\nAPI Gateway and the CDK, I use Event Bridge as a layer between API Gateway and Step Functions. There is only one rule, which triggers the execution of the Step Function workflow\nand returns the event ID, which is used as the game ID.\n```json\n{\n  \"detail-type\": [\"putEvent\"]\n}\n```\n\n### Lambda\nThere are 3 lambda functions:\n\n1. new-guess: stores the initial game data to DynamoDB\n2. handle-result: checks if the initial price differs from the current price and updates the DynamoDB entry when the game is finished\n3. check-result: allows the user to get status updates about the given game ID\n\n### Step Functions\nThere are 3 tasks:\n1. newGuessTasks: is triggered through EventBridge and starts the above mentioned lambda function, it returns the game data and a property \"waitSeconds\" with 60 seconds as its value.\n2. waitTask: gets a variable \"waitSeconds\" and waits for this amount of time\n3. handleResultTask triggers the above mentioned handle-result lambda and returns whether the price did change or not with a property \"didPriceChange\" and \"waitSeconds\" with 20 seconds. That means that as long as the price did not change it will wait 20 seconds again and again until it changed and is then resolved by updating the DynamoDB items gameStatus to \"finished\".\n\n![stepfunctionsgraph.png](/diagrams/stepfunctions.png)\n### DynamoDB\nStorage service used to keep track of the game status.\n\nExample:\n```json\n{\n    \"id\": \"23423423-24234234-45523544-4234234\",\n    \"guess\": \"up\",\n    \"oldPrice\": 21123.23, // the initial price\n    \"timestamp\": \"2022-08-24T17:17:17.979Z\",\n    \"isCorrectGuess\": true,\n    \"gameStatus\": \"finished\", // either \"finished\" or \"processing\"\n    \"latestPrice\": 21739.37\n}\n```\n\n## Project Improvements\n- The AWS CDK has a lot of types, which VSCode often has difficulties with There are newer modular CDK packages which maybe help to prevent that.\n- The Frontend is deployed to Vercel, as its fast and easy. It would be better to have a dedicated CDK construct for the frontend deployment and use AWS for it as well.\n- The backend endpoints do have very generous CORS rules which should not be used in a production environment.\n- The provisioned AWS services do have very generous IAM policies applied. A production environment should always aim for the principle of least privilige (POLP).\n - Adding a Time-To-Live (TTL) to DynamoDB entries which are not connected to a potential userID. We can not query such data and therefore should free up that space after 48h through a TTL key.\n - Cache the Coingecko API request on the backend. Option: store current price in DynamoDB every x seconds to avoid heavy requests on their API.\n\n## Feature Improvements\n- User Management: If the user signs up for an account, its userID could be attached to the already existing game history. Adding a global secondary index (GSI) \"byUser\" helps to retrieve data for a specific user and allows to implement the following improvements as well:\n- Leaderboard: Claim a username and be present in a public leaderboard\n- Persisted history of guesses: See a persisted history of your previous guesses. The local history is already stored in local storage, but this can be modified and only helps to give updates to non-authenticated players.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboringcontributor%2Fbtc-guessr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fboringcontributor%2Fbtc-guessr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboringcontributor%2Fbtc-guessr/lists"}