A Soroban-powered talent marketplace enabling developers in the Global South to access global opportunities through milestone-based XLM escrow payments, transparent smart contracts, and borderless financial infrastructure.
Built on Stellar + Soroban for the hackathon.
- Project Structure
- Tech Stack
- Prerequisites
- Getting Started
- Smart Contract
- Frontend Setup
- Environment Variables
- Deployed Contract
- Contributing
.
├── contracts/
│ └── mile-stack/
│ ├── src/
│ │ ├── lib.rs # Contract entry point & function implementations
│ │ ├── types.rs # Data types: MilestoneStatus, Milestone, Project, DataKey (+ Reputation)
│ │ ├── storage.rs # Storage helpers: load/save project, update_milestone, get/increment_reputation
│ │ └── test/
│ │ ├── mod.rs # Shared test helpers
│ │ ├── types.rs # Data structure tests
│ │ ├── create_project.rs # create_project tests
│ │ ├── fund_milestone.rs # fund_milestone tests
│ │ ├── approve_milestone.rs
│ │ ├── dispute_milestone.rs
│ │ ├── mark_complete.rs # mark_complete tests
│ │ ├── resolve_dispute.rs # resolve_dispute + initialize tests
│ │ ├── view_functions.rs
│ │ ├── lifecycle.rs # Auth guards + end-to-end lifecycle test
│ │ └── reputation.rs # On-chain reputation score tests (4 tests)
│ └── Cargo.toml
├── mile-stack-frontend/ # Next.js 16 frontend
├── supabase/
│ └── migrations/ # SQL migration files
│ ├── 20260604000000_create_listings_and_applications.sql
│ ├── 20260604000001_create_project_metadata.sql
│ ├── 20260607000000_tighten_rls_policies.sql
│ └── 20260607000001_create_freelancer_profiles.sql
├── Cargo.toml
└── README.md
| Layer | Technology |
|---|---|
| Smart Contracts | Rust + Soroban SDK 25 |
| Blockchain | Stellar (Testnet) |
| Payments | XLM (native Stellar token) |
| Frontend | Next.js 16, Tailwind CSS v4, TypeScript |
| Wallet | Freighter browser extension |
| Database | Supabase (off-chain marketplace data) |
- Git
- Rust (stable toolchain)
- Stellar CLI
- Node.js 18+
- Freighter Wallet browser extension (for the demo)
- A free Supabase project
git clone https://github.com/johneliud/mile-stack.git
cd mile-stackcargo testExpected output:
running 54 tests
test test::approve_milestone::test_approve_milestone_records_client_auth ... ok
test test::approve_milestone::test_approve_milestone_rejects_already_released_milestone ... ok
test test::approve_milestone::test_approve_milestone_rejects_funded_milestone ... ok
test test::approve_milestone::test_approve_milestone_rejects_pending_milestone ... ok
test test::approve_milestone::test_approve_milestone_releases_xlm_to_freelancer ... ok
test test::approve_milestone::test_approve_milestone_updates_status_to_released ... ok
test test::create_project::test_create_project_persists_correctly ... ok
test test::create_project::test_create_project_rejects_empty_milestones ... ok
test test::create_project::test_create_project_rejects_mismatched_milestone_lengths ... ok
test test::create_project::test_create_project_requires_client_auth ... ok
test test::create_project::test_create_project_returns_incrementing_ids ... ok
test test::dispute_milestone::test_dispute_milestone_client_can_dispute ... ok
test test::dispute_milestone::test_dispute_milestone_does_not_affect_siblings ... ok
test test::dispute_milestone::test_dispute_milestone_freelancer_can_dispute ... ok
test test::dispute_milestone::test_dispute_milestone_locks_funds_in_contract ... ok
test test::dispute_milestone::test_dispute_milestone_records_caller_auth ... ok
test test::dispute_milestone::test_dispute_milestone_rejects_already_released_milestone ... ok
test test::dispute_milestone::test_dispute_milestone_rejects_pending_milestone ... ok
test test::dispute_milestone::test_dispute_milestone_rejects_unauthorized_caller ... ok
test test::fund_milestone::test_fund_milestone_records_client_auth ... ok
test test::fund_milestone::test_fund_milestone_rejects_already_funded_milestone ... ok
test test::fund_milestone::test_fund_milestone_transfers_xlm_to_contract ... ok
test test::fund_milestone::test_fund_milestone_updates_status_to_funded ... ok
test test::lifecycle::test_full_project_lifecycle ... ok
test test::lifecycle::test_only_client_can_approve ... ok
test test::lifecycle::test_only_client_can_fund ... ok
test test::mark_complete::test_mark_complete_auth_required ... ok
test test::mark_complete::test_mark_complete_does_not_affect_siblings ... ok
test test::mark_complete::test_mark_complete_rejects_non_freelancer ... ok
test test::mark_complete::test_mark_complete_rejects_pending_milestone ... ok
test test::mark_complete::test_mark_complete_records_freelancer_auth ... ok
test test::mark_complete::test_mark_complete_updates_status_to_completed ... ok
test test::reputation::test_reputation_accumulates_across_milestones ... ok
test test::reputation::test_reputation_increments_on_approve_milestone ... ok
test test::reputation::test_reputation_is_per_freelancer ... ok
test test::reputation::test_reputation_starts_at_zero ... ok
test test::resolve_dispute::test_initialize_double_init_rejected ... ok
test test::resolve_dispute::test_initialize_sets_resolver ... ok
test test::resolve_dispute::test_resolve_dispute_auth_required ... ok
test test::resolve_dispute::test_resolve_dispute_records_resolver_auth ... ok
test test::resolve_dispute::test_resolve_dispute_refunds_to_client ... ok
test test::resolve_dispute::test_resolve_dispute_rejects_funded_milestone ... ok
test test::resolve_dispute::test_resolve_dispute_rejects_non_resolver ... ok
test test::resolve_dispute::test_resolve_dispute_releases_to_freelancer ... ok
test test::types::test_initial_project_count_is_zero ... ok
test test::view_functions::test_get_milestone_panics_for_out_of_range_index ... ok
test test::view_functions::test_get_milestone_panics_for_unknown_project ... ok
test test::view_functions::test_get_milestone_returns_correct_fields ... ok
test test::view_functions::test_get_project_count_tracks_multiple_projects ... ok
test test::view_functions::test_get_project_panics_for_unknown_id ... ok
test test::view_functions::test_get_project_returns_correct_fields ... ok
test test::view_functions::test_view_functions_do_not_require_auth ... ok
test result: ok. 54 passed; 0 failed
stellar contract buildOutput:
target/wasm32v1-none/release/mile_stack.wasm
Generate and fund a deployer account (skip if the milestack-deployer key already exists):
stellar keys generate milestack-deployer --network testnet
stellar keys fund milestack-deployer --network testnetDeploy the contract:
stellar contract deploy \
--wasm target/wasm32v1-none/release/mile_stack.wasm \
--source milestack-deployer \
--network testnetThe contract ID printed to stdout is your NEXT_PUBLIC_CONTRACT_ID.
The contract must be initialized once to set the dispute resolver address. Use the deployer's public key (or any trusted admin account):
stellar contract invoke \
--id <CONTRACT_ID> \
--source milestack-deployer \
--network testnet \
-- initialize \
--resolver <RESOLVER_ADDRESS>Replace <CONTRACT_ID> with the ID from the previous step and <RESOLVER_ADDRESS> with the admin's Stellar public key. This call can only succeed once - any subsequent call will panic.
Pending → Funded → Completed → Released
↘ Disputed ──────────────→ Released (via resolver)
The freelancer calls mark_complete after finishing work on a Funded milestone. Only then can the client call approve_milestone to release payment. Either party can call dispute_milestone at any time while funds are escrowed; a designated resolver account settles the dispute by calling resolve_dispute.
| Type | Description |
|---|---|
MilestoneStatus |
Pending → Funded → Completed → Released or Disputed |
Milestone |
Title, XLM amount, status, freelancer address |
Project |
ID, client address, milestone list, creation timestamp |
DataKey |
Storage keys: Project(id), ProjectCount, Resolver, Reputation(address) |
| Function | Auth | Description |
|---|---|---|
initialize(resolver) |
Deployer (one-time) | Sets the dispute resolver address; panics if called again |
create_project(client, freelancer, titles, amounts) |
Client | Creates a new escrow project; returns project ID |
fund_milestone(project_id, milestone_index, token) |
Client | Locks milestone XLM in contract escrow (must be Pending) |
mark_complete(caller, project_id, milestone_index) |
Freelancer | Signals work is done; transitions Funded → Completed |
approve_milestone(project_id, milestone_index, token) |
Client | Releases escrowed XLM to the freelancer; increments the freelancer's on-chain reputation score |
dispute_milestone(caller, project_id, milestone_index) |
Client or Freelancer | Flags milestone as Disputed, funds stay locked |
resolve_dispute(caller, project_id, milestone_index, token, release_to_freelancer) |
Resolver | Settles a Disputed milestone - pays winner, marks Released |
get_project_count() |
None | Returns total number of projects |
get_project(project_id) |
None | Fetch a full project by ID |
get_milestone(project_id, milestone_index) |
None | Fetch a single milestone |
get_reputation(freelancer) |
None | Returns the number of milestones successfully approved for the given address |
cd mile-stack-frontend
# Install dependencies
npm install
# Copy environment variables
cp .env.example .env.local
# Fill in values - see Environment Variables section below
# Start the development server
npm run dev
# Seed 10 demo listings into Supabase (requires DEMO_CLIENT_ADDRESS in .env.local)
npm run seed
# Run the headless E2E integration test (funds accounts via Friendbot, exercises the full flow)
npm run test:e2eOpen http://localhost:3000.
On first use, connect your Freighter wallet and select your role (I want to hire or I want to work). The role is saved in localStorage and determines your navigation, dashboard, and landing page CTAs. You can switch roles at any time via the role chip in the navbar.
See mile-stack-frontend/README.md for full frontend documentation including Supabase setup.
Create .env.local inside mile-stack-frontend/ using .env.example as a template:
| Variable | Value / Description |
|---|---|
NEXT_PUBLIC_STELLAR_RPC_URL |
https://soroban-testnet.stellar.org |
NEXT_PUBLIC_CONTRACT_ID |
CAGH37UE6W66FDEI7HPWLGMSWQD4Z7SRZVW3AJLBVL6CUN47UYRUBIFN |
NEXT_PUBLIC_NETWORK_PASSPHRASE |
Test SDF Network ; September 2015 |
NEXT_PUBLIC_XLM_TOKEN_ID |
CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC |
NEXT_PUBLIC_SIMULATION_SOURCE |
A funded testnet account public key (for read-only contract simulations) |
NEXT_PUBLIC_SUPABASE_URL |
Your Supabase project URL (Project Settings > API) |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Your Supabase anon/public key (Project Settings > API) |
DEMO_CLIENT_ADDRESS |
Client public key used by npm run seed to author demo listings |
INTEGRATION_CLIENT_SECRET |
(optional) Persistent testnet secret for the E2E test client account; leave blank to auto-generate via Friendbot |
INTEGRATION_FREELANCER_SECRET |
(optional) Persistent testnet secret for the E2E test freelancer account |
Pre-created and funded testnet accounts for testing and live demos. Testnet only - no real value.
| Role | Public Key | Secret Key |
|---|---|---|
| Client | GBRDKWQ4RB4JNIB3JUJXXNC2D4SAPUILUPLJAQIUVOLDYQYSHOHYOFMX |
SBPC4PYKLSCQVYA3HBDTLDFPCMKVQ4B6YQV3X2QFPOKPG5ZWDNOBCTBW |
| Freelancer | GCJTJMXZ43MF6W5SJVWOOJKPFCP7K4AURSBNLTTMWNABMU6T4DW2ERHF |
SBMGNNTQRHV2KIARRXKJRPZU35VBJFMWAEJTJNPOJ52SRIWWSD3AHQEY |
Import each secret key into Freighter (set to Testnet), then run npm run seed inside mile-stack-frontend/ to populate the demo listing. See docs/DEMO.md for the full walkthrough.
| Network | Contract ID |
|---|---|
| Testnet | CAGH37UE6W66FDEI7HPWLGMSWQD4Z7SRZVW3AJLBVL6CUN47UYRUBIFN |
Deployer / Resolver: GCQQCRKG3C5DPPWTVTYYWEJLIPHUVBKQYT6HIOBX7I37UVSY7DNMNFZR
Deploy transaction: b5b91f2f43f746a0d08d741450af7a92747ee5f8f837ac3c96605033b3c4b8ff
Initialize transaction: ea7bde1d6446dde399714c79e35027ca3bcb5ac3be77388adace8c5981365690
Native XLM token (testnet): CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
| Name | GitHub |
|---|---|
| John Eliud Odhiambo | @johneliud |
| Abigael Nyangasi | @IjayAbby |
| Praise Nyuthe | @PraiseNyuthe |
- Pick an open issue from the issue tracker
- Create a branch:
git checkout -b feat/<issue-number>-short-description - Make your changes and ensure tests pass:
cargo test - Open a PR referencing the issue with
Closes #<issue-number>
