Coinly is a crypto exchange app. In simple words, it lets a person turn their normal money (USD) into crypto (like ETH), and also turn crypto back into normal money. To make this safe, people first log in, verify who they are, connect their crypto wallet, and then buy or sell.
This is a personal (pet) project, built mainly to learn and get hands-on experience with Web3 — things like smart contracts, wallets, and how a real app connects to the blockchain. It's not a production business, just a way to genuinely understand how these pieces fit together by actually building one.
Under the hood, this project has a few moving pieces working together:
- A website (frontend) where people actually use the app.
- A couple of backend services written in Go, which handle logins, user accounts, and the buy/sell logic.
- Some smart contracts written in Solidity, which live on the blockchain and actually hold/move the crypto.
You don't need to understand blockchain deeply to run this project locally. Just follow the steps below.
Anvil is just a fake local blockchain that runs on your own computer, so you can test contracts without spending real money.
cd contracts
# install/build the contracts
forge build
# run the test suite (optional, but good to check everything still works)
forge test
# start a local blockchain in its own terminal window, and leave it running
anvilOnce Anvil is running, if you actually want to deploy the contracts to it (first time only), copy contracts/.env.example to contracts/.env, fill in the values, then run:
forge script script/Deploy.s.sol:Deploy --rpc-url http://localhost:8545 --broadcastThat's it — you now have a local blockchain with the contracts deployed on it.
This is the easiest way to get the whole app (both backend services and the website) running together on your machine.
# from the root of the project
docker compose up -d --buildThis starts Postgres, Redis, the two backend services (core-service and finance), and the frontend, all together.
Each service reads its settings from its own .env file, and these files are not committed to git (they hold real secrets like Auth0 and Stripe keys). You need to create them yourself:
backend/core-service/.env— copy frombackend/core-service/.env.examplebackend/finance/.env— copy frombackend/finance/.env.examplefrontend/.env.local— copy fromfrontend/.env.example
Fill in every value in those files (Auth0 credentials, Stripe keys, etc.) before starting Docker. If a .env file is missing or has empty/wrong values, the affected service will fail to start or will crash right after starting — so it's worth double-checking these files first.
Once everything is up, you can check that things are healthy:
curl http://localhost:8080/healthz # core-service
curl http://localhost:8081/healthz # financeAnd the website itself will be available at http://localhost:5173.
This project is being built as a set of small services (microservices) instead of one big service, so each piece can grow on its own. Some of these services already exist, and some are still just planned for later — this section covers the full picture.
Planned microservices:
core-service(Go) — the identity service. Owns user accounts and KYC status, and is the one place that actually checks if a login token is valid.finance(Go) — the money/crypto service. Handles currencies, wallets, bank accounts, Stripe payments, and (soon) the actual buy/sell orders.auth-service(Go, planned) — would take over authentication/token issuance and role-based permissions as a dedicated service.notification-service(Go, planned) — would send emails/push notifications in the background (e.g. "your purchase is complete").ai-intelligence-service(Python, planned) — an AI support agent that can answer user questions and raise support tickets on its own.
How they talk to each other:
- For things that need an instant answer (like "is this user's login valid?"), services call each other directly and synchronously using gRPC.
- For things that can happen a little later and shouldn't block the user (like "send a receipt email after a purchase"), services publish events to Kafka, and whichever service cares about that event picks it up whenever it's ready.
- All services share one Postgres database for now (kept simple on purpose), and use Redis for caching and to make sure things like Stripe webhooks don't get processed twice.
How login works (Auth0):
- A user logs in through Auth0 on the frontend (this handles passwords, MFA, etc. — we don't build our own login system).
- Auth0 gives the frontend an access token.
- The frontend sends that token to whichever backend service it's calling.
core-serviceis the only service that actually talks to Auth0 to verify a token is real and fetch who the user is.- Every other service (like
finance) doesn't re-check the token itself — it just askscore-serviceover gRPC, "is this token valid, and is this user allowed to do this?", and trusts the answer.
This way, login/identity logic lives in exactly one place, and every other service can stay simple.
flowchart LR
User((User)) --> Frontend[Frontend - Next.js]
Frontend -- login --> Auth0[(Auth0)]
Frontend -- access token --> CoreService[core-service]
Frontend --> Finance[finance]
Finance -- "is this token valid?" --> CoreService
CoreService -- verifies token --> Auth0
CoreService --> Postgres[(Postgres)]
CoreService --> Redis[(Redis)]
Finance --> Postgres
Finance --> Redis[(Redis)]
Finance --> Contracts[Smart contracts on-chain]
Finance -. events .-> Kafka[[Kafka]]
Kafka -. events .-> Notification[notification-service]
Kafka -. events .-> AI[ai-intelligence-service]