/ 01
The problem
Crowdfunding works, but slowly and at a cost. Platforms take high fees, money takes days to settle across borders, and investors get little visibility after they've paid. Their 'equity' is often a line in a spreadsheet, and refunds for failed campaigns are handled by hand.
A blockchain fits each of those pain points. Ownership can be a token in the investor's wallet. Campaign rules, like 'refund everyone if the goal isn't met by the deadline', can be code that nobody has to remember to run. Holders can vote on how milestone funds are released.
/ 02
Approach
I split the product into two layers that could be built and tested independently. The first is a conventional web product: a Next.js front end and a NestJS API on Postgres that handle accounts, campaign pages, a founder wizard and investment records. The second is a suite of Soroban contracts on Stellar that holds the rules: tokens, the campaign lifecycle, investing, refunds and governance.
Before writing any interface code I wrote a design spec, an editorial fintech look with one brand blue, a dark hero and monospaced numbers, and turned it into CSS tokens. That kept a large surface (landing, discovery, campaign detail, wizard, dashboard) visually consistent without a UI kit.
The contracts are deliberately small and composable. Each one does one job and calls the others through typed clients, so the refund logic can't drift from the campaign's real status.
/ 03
What I built
Campaign discovery
Search plus sector and stage filters over live campaigns from the API, with funding progress shown on every card.
Campaign detail & invest box
Tabs for overview, team, financials and roadmap, a milestone timeline, and an invest card that shows how many tokens your amount buys as you type.
Five-step founder wizard
Basic info, then token setup, milestones, a preview and submit. It creates the campaign, its milestones and its token config in a single nested database write.
Investment bookkeeping
The API checks that the campaign is active, records the investment, updates the amount raised and marks the campaign funded when it reaches its goal.
Investor dashboard
A sidebar app with portfolio, investments, a Freighter wallet connection, governance proposals and integrations.
Five Soroban contracts
Token (mint, burn, transfer), Campaign (goal, deadline, milestones), Investment (price-based minting, per-investor caps), Refund and Governance. Covered by 23 tests.
/ 04
How it works
- Front end
- Next.js 16
- React 19
- TypeScript
- Freighter wallet
- API
- NestJS 11
- JWT auth
- Swagger docs
- Render
- Data
- PostgreSQL
- Prisma
- On-chain
- Token
- Campaign
- Investment
- Refund
- Governance
- Stellar testnet
/ 05
Hard parts
- 1
Contracts that call each other
Refunds depend on three other contracts. A refund is only valid once the campaign is cancelled, and it has to burn exactly the tokens the investor holds. The refund contract imports the others' compiled WASM to get typed clients and checks every precondition before it burns anything. The trade-off is a fixed build order: token first, then campaign and investment, then refund and governance.
let status = campaign_client.get_status(); if status != STATUS_CANCELLED { panic!("campaign is not in refundable state"); } let held = token_client.balance(&investor); let record = investment_client.get_investment(&investor); token_client.burn(&investor, &held); - 2
A campaign as a small state machine
A campaign is Open until it either reaches its goal (Funded) or is cancelled (Cancelled). Anyone can call try_expire() once the deadline passes, so refunds never wait for an admin to act. Milestone releases are admin-only, checked with require_auth() and an explicit comparison against the stored admin, and every balance change uses checked arithmetic to rule out overflow.
- 3
Whole tokens only
Ownership tokens are indivisible, so the investment contract mints amount / price with floor division and rejects an investment too small to buy a single token. An optional per-investor cap stops one wallet from buying up a round.
- 4
Token-weighted voting, and its known gap
Voting power is the voter's token balance at the moment they vote, and a per-proposal record blocks double votes from the same address. Because there's no snapshot, tokens could be moved to a second wallet and voted again. The fix is to snapshot balances when a proposal is created, and it's first on the list for the next version.
/ 06
Design
The look is editorial fintech. A near-black hero carries floating product-card mockups, then the page alternates white and soft-grey bands, with a single electric blue for everything you can act on. Inter carries the text and JetBrains Mono carries every number (amounts raised, token prices, vote counts), which gives the money a precise, trading-terminal feel.
The components repeat throughout: pill buttons, 24px cards, filter chips, progress bars and a numbered milestone timeline. That's what lets the discovery page, the wizard and the dashboard read as one product. On narrow phones the landing hero still needs work, because the mockup cards overlap the intro copy and need to stack below it.
- Brand blue#0052FF
- Ink#0A0B0D
- Body#5B616E
- Surface#EEF0F3
- Up#05B169
- Down#CF202F
/ 07
Outcome
5 smart contracts deployed to Stellar testnet · 23 contract tests
- Five contracts (token, campaign, investment, refund and governance) deployed to Stellar testnet, with 23 contract tests across the suite.
- Full-stack web app live on Vercel with a NestJS API on Render: campaign discovery, detail pages, a founder wizard, JWT auth and a 15-endpoint REST API with Swagger docs.
- Next milestone: connect the app to the chain. That means Freighter-signed calls to the investment contract, real transaction hashes on each investment, and USDC escrow in the campaign contract.
/ 08
What I learned
- Build the rules and the product separately, then join them. The contracts could be tested in isolation and the UI could be demoed end to end, but the joining step deserves its own milestone from the start.
- On-chain logic needs adversarial thinking. Every rule, from who can release milestones and when refunds open to how votes are counted, is something a stranger will try to bend.
- A written design spec scales better than taste. Turning it into tokens first made a dozen screens consistent without a component library.
/ 09
Screens




