Access platform from - https://faircircle.vercel.app/
Video demo link with commentary - https://youtu.be/e8lzBxysc6o
FairCircles brings the centuries-old tradition of Rotating Savings and Credit Associations (ROSCAs) to Solana, powered by FairScale reputation scoring.
Known globally as chit funds (India), tandas (Latin America), susus (West Africa), and hui (China), ROSCAs have served over 1 billion people worldwide. FairCircles makes this financial primitive trustless, transparent, and reputation-aware.
- Overview
- How It Works
- FairScale Integration
- Complete User Flow
- Architecture | Detailed Diagrams
- Project Structure
- Setup & Installation
- Configuration
- API Documentation
- Smart Contract Details
- Development Guide
- Testing
- Deployment
- Troubleshooting
- Security Considerations
- License
Rotating Savings and Credit Associations (ROSCAs) are informal financial institutions where a group of people agree to:
- Contribute a fixed amount regularly (e.g., weekly/monthly)
- Take turns receiving the pooled funds
- Continue until everyone has received their payout
Example: 10 people contribute $100/month. Each month, one person receives $1,000. After 10 months, everyone has contributed $1,000 and received $1,000.
Traditional ROSCAs rely on social trust and face-to-face accountability. FairCircles brings this to Web3 with:
- β Trustless execution via Solana smart contracts
- β Transparent tracking of all contributions and payouts
- β Reputation-based risk management via FairScale
- β Programmable rules enforced on-chain
- β Global participation without geographic constraints
- A creator initializes a new circle with:
- Name: e.g., "Weekend Savers Circle"
- Contribution Amount: Fixed SOL per round (e.g., 1 SOL)
- Period Length: Time between rounds in seconds (e.g., 604800 = 7 days)
- Minimum FairScore: Required reputation tier (e.g., 40 for Silver+)
- Creator's FairScore: Their actual FairScore at creation time
- Creator automatically becomes the first member
- Circle is open for joining by others who meet the FairScore requirement
- Users browse available circles on the discovery page
- Each circle displays:
- Current member count (e.g., 3/10)
- Contribution amount
- Minimum FairScore requirement
- Period length
- Users can join if they meet the FairScore threshold
- Their FairScore is recorded on-chain at join time
- Once enough members join (creator decides when), the creator starts the circle
- The smart contract:
- Sorts members by FairScore (highest to lowest)
- Creates the payout order array
- Sets the circle status to Active
- Records the start timestamp
- The circle now enters its first contribution round
- During each round, all members must contribute the fixed SOL amount
- Contributions are sent to the escrow PDA controlled by the program
- The smart contract tracks who has contributed via a 2D array:
contributions[member_index][round_index] - Once all members contribute, the round is marked complete
- After a round's contributions are complete, the current payout recipient can claim
- The payout order follows the FairScore ranking established at start
- The recipient receives the entire pool (all members' contributions)
- The smart contract:
- Transfers SOL from escrow to the claimant
- Marks them as having claimed
- Increments the payout index
- Starts a new round
- The cycle continues until all members have received their payout
- After the last payout, the circle status changes to Completed
- Members can view their completion history (future badge integration)
FairCircles uses FairScale in three meaningful ways:
Purpose: Prevent low-reputation wallets from joining high-trust circles
Implementation:
- Circle creators set a minimum FairScore (0-100 scale)
- Frontend fetches user's FairScore via FairScale API before allowing join
- Smart contract validates and stores the FairScore on-chain during
join_circle - Only wallets meeting the threshold can participate
Example:
Circle: "Platinum Savers"
Min FairScore: 80 (Platinum tier)
User FairScore: 65 (Gold tier) β β Cannot join
User FairScore: 85 (Platinum tier) β β
Can join
Purpose: Reward established reputation with priority access to pooled funds
Implementation:
- When
start_circleis called, members are sorted by FairScore (descending) - The
payout_orderarray is created:[member_index_highest_score, ..., member_index_lowest_score] - Payouts follow this order throughout the circle's lifecycle
Example:
Members (at join time):
- Alice: FairScore 85 β Receives payout in Round 1
- Bob: FairScore 70 β Receives payout in Round 2
- Charlie: FairScore 45 β Receives payout in Round 3
Why This Matters:
- High-reputation users get early liquidity (reward for trust)
- Low-reputation users prove commitment first (risk mitigation)
- Creates incentive to build on-chain reputation
Purpose: Reduce default risk by requiring low-reputation members to contribute before receiving
Implementation:
- Lower FairScore = later in payout queue
- Members must contribute to multiple rounds before their payout turn
- If someone defaults (doesn't contribute), the circle can identify them on-chain
- Future enhancement: Default tracking feeds back into FairScale reputation
Example:
3-member circle:
- High FairScore member: Contributes 1x, receives payout
- Medium FairScore: Contributes 2x before receiving
- Low FairScore: Contributes 3x before receiving (proves commitment)
- Alice visits the FairCircles dApp at
http://localhost:5173 - Clicks "Connect Wallet" button in the header
- Selects Phantom wallet from the adapter options
- Approves the connection request in Phantom
- Her wallet address and FairScore appear in the header
Backend Flow:
- Frontend calls
POST http://localhost:3001/api/fairscale/score?wallet={Alice's address} - Backend proxies to FairScale API with API key in header
- Returns FairScore data to frontend
- Zustand store caches the score
- Alice sees her SOL balance in the sidebar (e.g., 2.5 SOL)
- If balance is low, she clicks "Request Airdrop" button
- Airdrop request is sent to Solana devnet
- After ~30 seconds, balance updates to 4.5 SOL
Technical Details:
useWallethook providespublicKeyandconnectedstateuseConnectionprovidesconnectionto Solana RPCsolana.tsutility handlesrequestAirdrop()function
Alice's FairScore card displays:
- Overall Score: 72 (Gold tier)
- Badges: "Diamond Hands" (Platinum badge)
- Key Metrics:
- Wallet Age: 245 days
- Transactions: 1,234
- Active Days: 187
- Platform Diversity: 8
Data Source: FairScale API /score endpoint with full feature breakdown
- Alice clicks "Create Circle" tab
- Fills out the form:
- Name: "Weekend Savers"
- Contribution: 1 SOL
- Period: 7 days (dropdown)
- Min FairScore: 60 (slider, ensures she meets it)
- Reviews the preview showing her FairScore (72) meets requirement
- Clicks "Create Circle" button
- β³ Transaction simulation runs
- Phantom prompts for approval
- β Transaction confirmed! Circle created
On-Chain Process:
// Frontend (useCircleProgram.ts)
createCircle(
name: "Weekend Savers",
contributionAmount: 1, // SOL
periodLength: 604800, // 7 days in seconds
minFairScore: 60,
creatorFairScore: 72 // Alice's actual score
)
// Smart Contract (lib.rs)
create_circle {
- Validate inputs
- Derive Circle PDA from creator's pubkey
- Initialize Circle account with data
- Store creator as first member with FairScore 72
- Set status to Forming
}Account State After Creation:
Circle {
creator: Alice's PublicKey,
name: "Weekend Savers",
contribution_amount: 1_000_000_000, // lamports
period_length: 604800,
min_fair_score: 60,
member_count: 1,
status: Forming,
members[0]: Alice's PublicKey,
fair_scores[0]: 72,
...
}- Bob discovers the circle on "Discover Circles" tab
- He sees:
- 1/10 members
- 1 SOL contribution
- 7 day period
- Min FairScore: 60
- His FairScore is 68 (Gold) β β Eligible
- Clicks "Join Circle" button
- Transaction approved, Bob is added as member 2
Similarly, Charlie (FairScore 55, Silver) joins as member 3.
On-Chain Process:
join_circle {
- Check circle is in Forming status
- Validate Bob's FairScore (68) >= min (60) β
- Add Bob to members array at index member_count
- Store Bob's FairScore in fair_scores array
- Increment member_count to 2
}- Alice navigates to "My Circles" tab
- Sees "Weekend Savers" with 3 members
- Clicks "Start Circle" button (only visible to creator)
- Smart contract sorts members by FairScore and activates the circle
Payout Order Calculation:
Members after sorting:
[0] Alice: FairScore 72 β Payout Round 1
[1] Bob: FairScore 68 β Payout Round 2
[2] Charlie: FairScore 55 β Payout Round 3
payout_order = [0, 1, 2]
status = Active
round_start_time = current_timestamp- All three members see "Contribute 1 SOL" button for Round 1
- Alice contributes:
- Clicks "Contribute"
- Phantom prompts for 1 SOL + fees
- β Contribution recorded
- Bob contributes (1 SOL)
- Charlie contributes (1 SOL)
- Pool Status: 3/3 SOL collected β Round complete
On-Chain State:
Circle {
current_round: 0,
total_pool: 3_000_000_000, // 3 SOL in lamports
contributions[0][0]: true, // Alice contributed in round 0
contributions[1][0]: true, // Bob contributed in round 0
contributions[2][0]: true, // Charlie contributed in round 0
round_contributions_complete: true,
}- Alice sees "Claim Payout" button (she's first in payout_order)
- Clicks button
- Smart contract transfers 3 SOL from escrow to Alice's wallet
- Alice's balance increases by 3 SOL (net +2 SOL since she contributed 1)
Transaction:
claim_payout {
- Verify payout_index = 0, payout_order[0] = Alice β
- Verify round_contributions_complete = true β
- Transfer 3 SOL from escrow PDA to Alice
- Mark has_claimed[0] = true
- Increment payout_index to 1
- Increment current_round to 1
- Reset round_contributions_complete to false
}Process repeats:
- All members contribute 1 SOL to Round 2
- Bob (payout_order[1]) claims 3 SOL payout
- current_round increments to 2
- All members contribute 1 SOL to Round 3
- Charlie (payout_order[2]) claims 3 SOL payout
- Circle status changes to Completed
Final State:
- Each member contributed 3 SOL total (1 SOL Γ 3 rounds)
- Each member received 3 SOL total (1 payout of 3 SOL)
- Circle successfully completed! π
π For comprehensive architecture diagrams, see Architecture.md β includes detailed component diagrams, data flows, Solana program account structures, state machines, and deployment architecture.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USER (Browser) β
β Phantom / Solflare Wallet β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββ
β FRONTEND (React 19) β β BACKEND β
β Vite 7 + TypeScript β β (Express.js 5 + TypeScript) β
β http://localhost:5173 β β http://localhost:3001 β
β β REST API β β
β ββββββββββββββββββββββββββββββ β /api/* β ββββββββββββββββββββββββββββββ β
β β Components Layer β ββββββββββββββββββΊβ β FairScale Service β β
β β Dashboard, CircleCard, β β β β Score Proxy & Normalize β β
β β CircleDetail, CreateForm β β β ββββββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββ β β β β
β ββββββββββββββββββββββββββββββ β β βΌ β
β β Hooks Layer β β β ββββββββββββββββββββββββββββββ β
β β useCircleProgram β β β β FairScale External API β β
β β useFairScore β β β β api.fairscale.xyz β β
β ββββββββββββββββββββββββββββββ β β ββββββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββ β β β
β β State (Zustand 5.0) β β ββββββββββββββββββββββββββββββββββββ
β β Circles, FairScore Cache β β
β ββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββ
β
β RPC Calls (Anchor SDK)
βΌ
ββββββββββββββββββββββββββββββββββββ
β SOLANA BLOCKCHAIN (Devnet) β
β β
β ββββββββββββββββββββββββββββββ β
β β FairCircle Solana Program β β
β β (Anchor 0.32 / Rust) β β
β β β β
β β Instructions: β β
β β β’ create_circle β β
β β β’ join_circle β β
β β β’ start_circle β β
β β β’ contribute β β
β β β’ claim_payout β β
β β β’ update_fair_score β β
β β β β
β β PDAs: β β
β β β’ Circle Account β β
β β β’ Escrow Account β β
β ββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββ
User connects wallet
β
Frontend: useFairScore() hook triggered
β
Frontend β Backend: GET /api/fairscale/score?wallet={address}
β
Backend β FairScale API: GET /score?wallet={address}
(with fairkey header)
β
FairScale API β Backend: JSON response with score, tier, badges, features
β
Backend β Frontend: Normalized FairScoreResponse
β
Frontend: Update Zustand store
β
UI: FairScoreCard.tsx displays data
User fills form and clicks "Create Circle"
β
Frontend: createCircle() in useCircleProgram hook
β
Frontend: Validate creator's FairScore meets minimum
β
Frontend β Solana: Build transaction with create_circle instruction
β
Phantom Wallet: User approves transaction
β
Solana: Process create_circle instruction
βββ Derive Circle PDA
βββ Derive Escrow PDA
βββ Initialize Circle account
βββ Transfer rent-exempt SOL
β
Solana β Frontend: Transaction signature
β
Frontend: Poll for confirmation
β
Frontend: Show success notification
β
Frontend: Refresh circles list
FAIRCIRCLES/
βββ README.md # This file
βββ Architecture.md # Detailed architecture diagrams
βββ .gitignore
β
βββ faircircle-frontend/ # React Frontend Application
β βββ public/ # Static assets
β β βββ vite.svg # Favicon
β βββ src/
β β βββ components/ # React Components
β β β βββ CircleCard.tsx # Circle preview card with join button
β β β βββ CircleDetail.tsx # Full circle view (contribute, claim)
β β β βββ CreateCircleForm.tsx # Circle creation form with validation
β β β βββ Dashboard.tsx # Main app dashboard (tabs, layout)
β β β βββ FairScoreCard.tsx # FairScore display with metrics
β β β βββ Header.tsx # Top nav with wallet connect
β β β βββ LandingPage.tsx # Landing page before wallet connect
β β β βββ Notifications.tsx # Toast notification system
β β β βββ WalletBalance.tsx # SOL balance + airdrop button
β β β βββ WalletProvider.tsx # Solana wallet adapter context
β β βββ hooks/ # Custom React Hooks
β β β βββ useCircleProgram.ts # Anchor program interactions
β β β β # Functions: createCircle, joinCircle,
β β β β # startCircle, contribute, claimPayout,
β β β β # fetchCircle, fetchAllCircles
β β β βββ useFairScore.ts # FairScore fetching & caching
β β βββ lib/ # Library Code
β β β βββ constants.ts # App constants (PROGRAM_ID, API_URL)
β β β βββ fairscale.ts # FairScale API client
β β β βββ idl.ts # Anchor IDL for smart contract
β β β βββ solana.ts # Solana utilities (balance, airdrop)
β β βββ store/ # State Management
β β β βββ useStore.ts # Zustand global store
β β βββ types/ # TypeScript Definitions
β β β βββ index.ts # FairScoreResponse, Circle, Member
β β βββ App.tsx # Root app component
β β βββ App.css # Global styles
β β βββ main.tsx # React entry point
β β βββ index.css # Tailwind directives
β βββ .env.example # Environment variables template
β βββ package.json # NPM dependencies
β βββ package-lock.json
β βββ tsconfig.json # TypeScript config
β βββ tsconfig.app.json # App-specific TS config
β βββ tsconfig.node.json # Node-specific TS config
β βββ vite.config.ts # Vite bundler config
β βββ eslint.config.js # ESLint rules
β βββ README.md # Frontend-specific docs
β
βββ backend/ # Express.js Backend API
β βββ src/
β β βββ config/ # Configuration
β β β βββ index.ts # Config loader (env vars, validation)
β β βββ middleware/ # Express Middleware
β β β βββ errorHandler.ts # Global error handler + 404
β β βββ routes/ # API Routes
β β β βββ fairscale.routes.ts # /api/fairscale/* endpoints
β β β βββ health.routes.ts # /api/health endpoint
β β βββ services/ # Business Logic
β β β βββ fairscale.service.ts # FairScale API integration
β β βββ types/ # TypeScript Definitions
β β β βββ index.ts # API response types
β β βββ index.ts # Express app entry point
β βββ dist/ # Compiled JavaScript (gitignored)
β βββ .env # Environment variables (gitignored)
β βββ .env.example # Env template
β βββ package.json # NPM dependencies
β βββ package-lock.json
β βββ tsconfig.json # TypeScript config
β βββ README.md # Backend-specific docs
β
βββ faircircle-solana-program/ # Anchor Solana Program
βββ programs/
β βββ faircircle-solana-program/
β βββ src/
β β βββ lib.rs # Solana program (Rust)
β β # Structs: Circle, CircleStatus
β β # Instructions: create_circle,
β β # join_circle, start_circle,
β β # contribute, claim_payout
β βββ Cargo.toml # Rust dependencies
β βββ Xargo.toml
βββ tests/
β βββ faircircle-solana-program.ts # Anchor tests (TypeScript)
βββ migrations/
β βββ deploy.ts # Deployment script
βββ target/ # Compiled program (gitignored)
β βββ deploy/
β β βββ faircircle_solana_program-keypair.json # Program keypair
β βββ idl/
β βββ faircircle_solana_program.json # Generated IDL
βββ app/ # Client SDK (auto-generated)
βββ Anchor.toml # Anchor project config
βββ Cargo.toml # Workspace Cargo config
βββ package.json # NPM for tests
βββ package-lock.json
βββ tsconfig.json # TypeScript for tests
βββ .gitignore
| File | Purpose | Key Exports/Features |
|---|---|---|
useCircleProgram.ts |
Solana program interactions | createCircle(), joinCircle(), startCircle(), contribute(), claimPayout(), fetchCircle(), fetchAllCircles() |
useFairScore.ts |
FairScore fetching & caching | fairScore, loading, error, refetch(), isEligible() |
fairscale.ts |
FairScale API client | fetchFairScore(wallet) - calls backend proxy |
idl.ts |
Anchor IDL | Type-safe program interface generated from Rust |
useStore.ts |
Zustand store | Global state: fairScore, notifications, circles |
constants.ts |
App constants | PROGRAM_ID, API_BASE_URL, TIER_COLORS |
| File | Purpose | Key Exports/Features |
|---|---|---|
fairscale.service.ts |
FairScale API wrapper | getFairScore(), getFairScoreOnly(), getBatchFairScores() |
fairscale.routes.ts |
API routes | GET /api/fairscale/score, GET /api/fairscale/fairScore, POST /api/fairscale/batch |
errorHandler.ts |
Error middleware | Handles 401, 404, 429, 500 errors with proper status codes |
config/index.ts |
Configuration loader | Validates env vars, exports typed config object |
| File | Purpose | Key Contents |
|---|---|---|
lib.rs |
Smart contract | Circle struct (10KB account), create_circle(), join_circle(), start_circle(), contribute(), claim_payout() instructions |
target/idl/*.json |
Generated IDL | JSON representation of program interface for clients |
Ensure you have the following installed:
# Node.js 18+ and npm
node --version # v18.0.0 or higher
npm --version # 8.0.0 or higher
# Rust and Cargo (for Solana program)
rustc --version # 1.70.0 or higher
cargo --version
# Solana CLI
solana --version # 1.18.0 or higher
solana-keygen --version
# Anchor CLI (for Solana program development)
anchor --version # 0.32.0 or higherIf you don't have these, install them:
# Node.js - via nvm (recommended)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 18
nvm use 18
# Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Solana CLI
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
# Anchor
cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
avm install 0.32.1
avm use 0.32.1git clone https://github.com/yourusername/faircircles.git
cd FAIRCIRCLEScd backend
# Install dependencies
npm install
# Copy environment template
cp .env.example .env
# Edit .env with your FairScale API key
nano .envRequired Environment Variables (.env):
# Server Configuration
PORT=3001
NODE_ENV=development
# FairScale API
FAIRSCALE_API_URL=https://api.fairscale.xyz
FAIRSCALE_API_KEY=your_fairscale_api_key_here
# CORS (frontend origin)
CORS_ORIGINS=http://localhost:5173
# Solana
SOLANA_NETWORK=devnet
SOLANA_RPC_URL=https://api.devnet.solana.comGet a FairScale API Key:
- Fill out the form: https://forms.gle/heG1hfnjao4VShUS8
- You'll receive an API key for the free tier
- Paste it into
FAIRSCALE_API_KEYin.env
# Build TypeScript
npm run build
# Start development server
npm run devβ
Backend should be running at http://localhost:3001
Verify it's working:
curl http://localhost:3001/api/health
# Expected: {"status":"ok","timestamp":"...","environment":"development","version":"1.0.0"}cd ../faircircle-solana-program
# Install dependencies
npm install
# Build the program
anchor build
# Get your program ID
solana-keygen pubkey target/deploy/faircircle_solana_program-keypair.json
# Example output: BPFLoaderUpgradeab1e11111111111111111111111
# Copy the program ID and update it in two places:
# 1. Anchor.toml
nano Anchor.toml
# Update [programs.devnet] section:
# faircircle_solana_program = "YOUR_PROGRAM_ID_HERE"
# 2. lib.rs
nano programs/faircircle-solana-program/src/lib.rs
# Update the declare_id! macro:
# declare_id!("YOUR_PROGRAM_ID_HERE");
# Rebuild with updated program ID
anchor build
# Ensure you have devnet SOL
solana config set --url devnet
solana airdrop 2
# Deploy to devnet
anchor deploy --provider.cluster devnetProgram should be deployed to Solana devnet
Copy the deployed program ID (you'll need it for the frontend).
cd ../faircircle-frontend
# Install dependencies
npm install
# Copy environment template
cp .env.example .env
# Edit .env
nano .envRequired Environment Variables (.env):
# Backend API URL
VITE_API_BASE_URL=http://localhost:3001/api
# Solana Configuration
VITE_SOLANA_NETWORK=devnet
VITE_SOLANA_RPC_URL=https://api.devnet.solana.com
# Deployed Program ID (from step 3)
VITE_PROGRAM_ID=YOUR_PROGRAM_ID_HEREUpdate Constants (if needed):
nano src/lib/constants.tsEnsure PROGRAM_ID matches your deployed program:
export const PROGRAM_ID = new PublicKey('YOUR_PROGRAM_ID_HERE');# Start development server
npm run devFrontend should be running at http://localhost:5173
- Backend Health: http://localhost:3001/api/health
- Frontend: http://localhost:5173
- Solana Program: Deployed on devnet
Try connecting your wallet and creating a circle!
| Variable | Description | Default |
|---|---|---|
VITE_API_BASE_URL |
Backend API URL | http://localhost:3001/api |
VITE_SOLANA_NETWORK |
Solana network (devnet/mainnet-beta) | devnet |
VITE_SOLANA_RPC_URL |
Solana RPC endpoint | https://api.devnet.solana.com |
VITE_PROGRAM_ID |
Deployed Anchor program address | Your deployed program ID |
| Variable | Description | Default |
|---|---|---|
PORT |
Express server port | 3001 |
NODE_ENV |
Environment (development/production) | development |
FAIRSCALE_API_URL |
FairScale API base URL | https://api.fairscale.xyz |
FAIRSCALE_API_KEY |
Your FairScale API key | Required |
CORS_ORIGINS |
Allowed CORS origins (comma-separated) | http://localhost:5173 |
SOLANA_NETWORK |
Solana network | devnet |
SOLANA_RPC_URL |
Solana RPC endpoint | https://api.devnet.solana.com |
[programs.devnet]
faircircle_solana_program = "YOUR_PROGRAM_ID"
[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"Base URL: http://localhost:3001/api
GET /health
Returns API health status.
Response (200):
{
"status": "ok",
"timestamp": "2026-01-23T10:30:00.000Z",
"environment": "development",
"version": "1.0.0"
}GET /fairscale/score
Fetches complete FairScore with metadata, badges, and features.
Query Parameters:
wallet(required): Solana wallet addresstwitter(optional): Twitter/X username (without @)
Example Request:
curl "http://localhost:3001/api/fairscale/score?wallet=7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"Response (200):
{
"wallet": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"fair_score": 72.5,
"tier": "gold",
"badges": [
{
"id": "diamond_hands",
"label": "Diamond Hands",
"description": "Long-term holder with conviction",
"tier": "platinum"
}
],
"last_updated": "2026-01-23T10:30:00.000Z",
"features": {
"lst_percentile_score": 85.2,
"major_percentile_score": 78.5,
"native_sol_percentile": 92.1,
"stable_percentile_score": 65.4,
"tx_count": 1234,
"active_days": 187,
"median_gap_hours": 12.5,
"tempo_cv": 0.45,
"burst_ratio": 0.23,
"net_sol_flow_30d": 15.7,
"median_hold_days": 45.2,
"no_instant_dumps": 1,
"conviction_ratio": 0.87,
"platform_diversity": 8,
"wallet_age_days": 245
}
}Error Responses:
400: Missing or invalid wallet address401: Invalid FairScale API key (backend misconfiguration)500: Internal server error
GET /fairscale/fairScore
Lightweight endpoint for just the score value.
Query Parameters:
wallet(required): Solana wallet address
Response (200):
{
"wallet": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"fair_score": 72.5
}POST /fairscale/batch
Fetch FairScores for multiple wallets (max 50).
Request Body:
{
"wallets": [
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"BvrL8Q7hNYC7XXbBfsoL8kkdQrwbeeYCqHerry52zSYF"
]
}Response (200):
{
"scores": {
"7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU": 72.5,
"BvrL8Q7hNYC7XXbBfsoL8kkdQrwbeeYCqHerry52zSYF": 68.3
}
}Devnet: YOUR_DEPLOYED_PROGRAM_ID
Size: ~10 KB
Seeds: ["circle", creator.key()]
pub struct Circle {
pub creator: Pubkey, // 32 bytes - Circle creator
pub name: String, // Max 50 chars
pub contribution_amount: u64, // Lamports per contribution
pub period_length: i64, // Seconds between rounds
pub min_fair_score: u8, // Minimum FairScore (0-100)
pub current_round: u8, // Current round index (0-9)
pub member_count: u8, // Number of members (1-10)
pub payout_index: u8, // Current payout position
pub total_pool: u64, // Total lamports in escrow
pub status: CircleStatus, // Forming, Active, Completed
pub created_at: i64, // Unix timestamp
pub round_start_time: i64, // Start time of current round
pub round_contributions_complete: bool, // All contributed this round?
pub bump: u8, // PDA bump seed
pub escrow_bump: u8, // Escrow PDA bump seed
// Arrays (fixed size, MAX_MEMBERS = 10)
pub members: [Pubkey; 10], // Member wallet addresses
pub fair_scores: [u8; 10], // FairScores at join time
pub payout_order: [u8; 10], // Payout sequence (sorted by score)
pub contributions: [[bool; 10]; 10], // 2D: [member][round]
pub has_claimed: [bool; 10], // Has member claimed their payout?
}pub enum CircleStatus {
Forming, // Accepting new members
Active, // Rounds in progress
Completed, // All payouts distributed
}Creates a new lending circle.
Accounts:
creator(signer, mut): Circle creator's walletcircle(init, mut): Circle PDAescrow(init, mut): Escrow PDAsystem_program: System program
Arguments:
pub fn create_circle(
ctx: Context<CreateCircle>,
name: String, // Max 50 chars
contribution_amount: u64, // Lamports
period_length: i64, // Seconds
min_fair_score: u8, // 0-100
creator_fair_score: u8, // Creator's actual score
) -> Result<()>Validations:
name.len() <= 50contribution_amount > 0period_length > 0creator_fair_score >= min_fair_score
Example:
await program.methods
.createCircle(
"Weekend Savers",
new BN(1_000_000_000), // 1 SOL
new BN(604800), // 7 days
60, // Min score: 60
72 // Creator's score: 72
)
.accounts({
creator: wallet.publicKey,
circle: circlePDA,
escrow: escrowPDA,
systemProgram: SystemProgram.programId,
})
.rpc();Join an existing circle.
Accounts:
member(signer): Joining member's walletcircle(mut): Circle PDAcreator: Circle creator (for PDA derivation)
Arguments:
pub fn join_circle(
ctx: Context<JoinCircle>,
fair_score: u8, // Member's FairScore
) -> Result<()>Validations:
- Circle status is
Forming fair_score >= circle.min_fair_scorecircle.member_count < MAX_MEMBERS(10)- Member not already in circle
Activate the circle and set payout order (creator only).
Accounts:
creator(signer): Must match circle.creatorcircle(mut): Circle PDA
Arguments: None
Process:
- Validate caller is creator
- Validate status is
Forming - Sort members by FairScore (descending)
- Populate
payout_orderarray - Set status to
Active - Record
round_start_time
Contribute SOL to the current round.
Accounts:
member(signer, mut): Contributing membercircle(mut): Circle PDAescrow(mut): Escrow PDAcreator: Circle creatorsystem_program: System program
Arguments: None (contribution amount is from circle.contribution_amount)
Validations:
- Circle status is
Active - Member is in the circle
- Member hasn't contributed to current round yet
Transfer:
- Transfers
circle.contribution_amountlamports from member to escrow - Marks
contributions[member_index][current_round] = true - If all members contributed, sets
round_contributions_complete = true
Claim the pooled funds when it's your turn.
Accounts:
claimant(signer, mut): Member claiming payoutcircle(mut): Circle PDAescrow(mut): Escrow PDAcreator: Circle creatorsystem_program: System program
Arguments: None
Validations:
- Circle status is
Active round_contributions_complete == true- Claimant is the current payout recipient:
members[payout_order[payout_index]] == claimant - Claimant hasn't already claimed:
has_claimed[member_index] == false
Transfer:
- Transfers
total_poollamports from escrow to claimant - Marks
has_claimed[member_index] = true - Increments
payout_index - If last payout, sets status to
Completed - Otherwise, increments
current_roundand resetsround_contributions_complete
- Start Backend (Terminal 1):
cd backend
npm run dev
# Server running on http://localhost:3001- Start Frontend (Terminal 2):
cd faircircle-frontend
npm run dev
# Vite dev server on http://localhost:5173- Solana Program (already deployed):
# If you need to redeploy:
cd faircircle-solana-program
anchor build
anchor deploy --provider.cluster devnet- Edit files in
faircircle-frontend/src/ - Vite hot-reloads automatically
- Check browser console for errors
- TypeScript errors appear in terminal
- Edit files in
backend/src/ tsx watchauto-restarts server- Check terminal for logs
- Test endpoints with
curlor Postman
- Edit
programs/faircircle-solana-program/src/lib.rs - Build:
anchor build - Deploy:
anchor deploy --provider.cluster devnet - Update frontend IDL:
cp target/idl/faircircle_solana_program.json \
../faircircle-frontend/src/lib/idl.json- Update
idl.tsin frontend if needed - Restart frontend
cd faircircle-frontend/src/components
touch MyNewComponent.tsximport React from 'react';
export function MyNewComponent() {
return <div>My Component</div>;
}Backend (backend/src/routes/myroute.routes.ts):
import { Router } from 'express';
const router = Router();
router.get('/myendpoint', async (req, res) => {
res.json({ message: 'Hello' });
});
export default router;Register in backend/src/index.ts:
import myRoutes from './routes/myroute.routes.js';
app.use('/api/my', myRoutes);In lib.rs:
pub fn my_instruction(ctx: Context<MyContext>) -> Result<()> {
// Your logic
Ok(())
}
#[derive(Accounts)]
pub struct MyContext<'info> {
#[account(mut)]
pub user: Signer<'info>,
}Frontend Hook:
const myInstruction = useCallback(async () => {
await program.methods
.myInstruction()
.accounts({ user: wallet.publicKey })
.rpc();
}, [program, wallet]);- Open browser DevTools (F12)
- Check Console tab for errors
- Use React DevTools extension
- Check Network tab for API calls
- Use Zustand DevTools for state
- Check terminal logs
- Add
console.log()statements - Use
morganmiddleware (already configured) - Test endpoints with curl:
curl -i http://localhost:3001/api/fairscale/score?wallet=...- Check Anchor build errors carefully
- Use
msg!()macro for logging:
msg!("Debug: value = {}", value);- View logs after transaction:
const tx = await program.methods.myInstruction()...
console.log(await connection.getTransaction(tx));- Use Solana Explorer: https://explorer.solana.com/?cluster=devnet
cd faircircle-frontend
# Run unit tests (if configured)
npm test
# Type checking
npx tsc --noEmit
# Linting
npm run lintcd backend
# Type checking
npx tsc --noEmit
# Run tests (if configured)
npm test
# Manual endpoint testing
curl http://localhost:3001/api/healthcd faircircle-solana-program
# Run Anchor tests
anchor test
# Run specific test file
anchor test --skip-local-validator tests/faircircle-solana-program.tsExample Test (tests/faircircle-solana-program.ts):
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { FaircircleSolanaProgram } from "../target/types/faircircle_solana_program";
import { expect } from "chai";
describe("faircircle-solana-program", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.FaircircleSolanaProgram as Program<FaircircleSolanaProgram>;
it("Creates a circle", async () => {
const [circlePDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("circle"), provider.wallet.publicKey.toBuffer()],
program.programId
);
await program.methods
.createCircle(
"Test Circle",
new anchor.BN(1_000_000_000),
new anchor.BN(86400),
50,
75
)
.accounts({
creator: provider.wallet.publicKey,
})
.rpc();
const circle = await program.account.circle.fetch(circlePDA);
expect(circle.name).to.equal("Test Circle");
expect(circle.memberCount).to.equal(1);
});
});User Flow Test:
- Connect wallet (Phantom)
- View FairScore card
- Request airdrop (if low SOL)
- Create a circle
- Join a circle (with second wallet)
- Start a circle (as creator)
- Contribute to round 1
- Claim payout (first member)
- Contribute to round 2
- Claim payout (second member)
- View completed circle
cd faircircle-solana-program
# Switch to mainnet
solana config set --url mainnet-beta
# Ensure you have SOL for deployment (costs ~5-10 SOL)
solana balance
# Build
anchor build
# Deploy
anchor deploy --provider.cluster mainnet-beta
# Note the program ID
solana-keygen pubkey target/deploy/faircircle_solana_program-keypair.jsonOption A: VPS (DigitalOcean, AWS EC2)
# On your server
git clone https://github.com/yourusername/faircircles.git
cd FAIRCIRCLES/backend
# Install dependencies
npm install --production
# Set environment variables
nano .env # Add production values
# Build
npm run build
# Install PM2 for process management
npm install -g pm2
# Start with PM2
pm2 start dist/index.js --name faircircles-backend
pm2 save
pm2 startup # Follow instructionsOption B: Vercel/Netlify Functions
Convert Express routes to serverless functions (refer to platform docs).
Option C: Docker
# backend/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3001
CMD ["node", "dist/index.js"]docker build -t faircircles-backend .
docker run -p 3001:3001 --env-file .env faircircles-backendOption A: Vercel (Recommended for Vite)
cd faircircle-frontend
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Follow prompts, add environment variables in Vercel dashboardOption B: Netlify
# Install Netlify CLI
npm i -g netlify-cli
# Build
npm run build
# Deploy
netlify deploy --prod --dir=distOption C: Static Hosting (AWS S3, Cloudflare Pages)
# Build
npm run build
# Upload dist/ folder to your hosting providerUpdate Environment Variables:
VITE_API_BASE_URL=https://your-backend-domain.com/api
VITE_SOLANA_NETWORK=mainnet-beta
VITE_SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
VITE_PROGRAM_ID=your_mainnet_program_idIn production backend .env:
CORS_ORIGINS=https://your-frontend-domain.comCause: FairScore data not loaded
Solution:
- Check backend is running (
http://localhost:3001/api/health) - Verify FairScale API key in
backend/.env - Frontend should gracefully handle this with fallback
Cause: Insufficient SOL balance for transaction + rent
Solution:
- Check wallet balance (need 0.5+ SOL)
- Request airdrop: Click "Request Airdrop" button or:
solana airdrop 2 YOUR_WALLET_ADDRESS --url devnet- Wait ~30 seconds for airdrop to confirm
Cause: Frontend using wrong program ID or program not deployed
Solution:
- Verify program deployed:
solana program show YOUR_PROGRAM_ID --url devnet- Update
VITE_PROGRAM_IDin frontend.env - Update
PROGRAM_IDinfaircircle-frontend/src/lib/constants.ts - Restart frontend dev server
Cause: Direct frontend β FairScale API calls blocked
Solution: Requests should go through backend proxy
- Verify
VITE_API_BASE_URL=http://localhost:3001/apiin frontend.env - Check
fairscale.tsusesAPI_BASE_URL, not direct FairScale URL
Cause: Slow RPC or network congestion
Solution:
- Use a premium RPC (Helius, QuickNode, Alchemy)
- Update
VITE_SOLANA_RPC_URLin frontend.env - Retry the transaction
cd backend
npm run dev
# Logs appear in terminal// In frontend, after transaction:
const tx = await program.methods.contribute()...
console.log("Transaction:", tx);
// View in Solana Explorer:
console.log(`https://explorer.solana.com/tx/${tx}?cluster=devnet`);# In Solana program, use msg! macro:
msg!("Debug: current_round = {}", circle.current_round);- PDA Validation: All PDAs are derived with proper seeds and bumps
- Signer Checks: Critical operations verify
ctx.accounts.user.is_signer - Ownership Checks: Accounts validated to be owned by correct programs
- State Validation: Status checks prevent operations in wrong phase
- Overflow Protection: Using checked arithmetic where needed
Known Limitations:
- No slashing for defaults (future feature)
- No multi-sig for circle creation
- Fixed max 10 members
- API Key Protection: FairScale API key in
.env, never exposed to frontend - CORS: Strict origin checking
- Input Validation: All inputs sanitized
Production Checklist:
- Use HTTPS for all endpoints
- Implement authentication if needed
- Regular security audits
- Wallet Adapter: Using official Solana Wallet Adapter
- No Private Keys: Never storing or transmitting private keys
- Transaction Preview: Users see transaction details before signing
- Error Handling: Sensitive data not leaked in error messages
| Tier |
|---|
| π Platinum |
| π₯ Gold |
| π₯ Silver |
| π₯ Bronze |
| β« Unrated |
Tier Benefits:
- Higher tier = Earlier payout position
- Access to exclusive high-reputation circles
- Future: Interest rate reductions, higher contribution limits
ROSCAs serve 1+ billion people worldwide:
- India: Chit funds - $20B+ market
- Latin America: Tandas - Primary savings for millions
- West Africa: Susus - Essential community finance
- East Asia: Hui - Centuries-old tradition
| Traditional Banking | ROSCAs | FairCircles |
|---|---|---|
| Credit checks | Social trust | FairScore reputation |
| Collateral required | No collateral | No collateral |
| High interest rates | No interest | No interest |
| Exclusionary | Community-based | Global + permissionless |
| Opaque terms | Face-to-face | Transparent on-chain |
β Trustless: Smart contracts enforce rules β Transparent: All transactions on-chain β Global: No geographic boundaries β Programmable: Custom rules per circle β Reputation: FairScale adds objective scoring β Accessible: Anyone with a wallet can participate
- Website: https://fairscale.xyz
- API Docs: https://api.fairscale.xyz
- Telegram: https://t.me/+WQlko_c5blJhN2E0
- Twitter: https://x.com/fairscalexyz
- Docs: https://docs.solana.com
- Anchor Book: https://book.anchor-lang.com
- Solana Cookbook: https://solanacookbook.com
- Devnet Faucet: https://faucet.solana.com
- Solana Explorer: https://explorer.solana.com/?cluster=devnet
- Anchor: https://www.anchor-lang.com
Contributions welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Made with β€οΈ for the Solana ecosystem