ImpactAI Documentation
Everything you need to understand, run, extend, and deploy ImpactAI — the platform that turns any community cause into a Solana token in 60 seconds.
Overview
ImpactAI is a Next.js 14 web application that lets anyone launch a Solana token for a real-world community cause — clean energy, clean water, education, healthcare — with no code, no bank account, and no smart contract knowledge.
The platform is built on three foundations:
Quick start
Clone the repo and install dependencies:
git clone https://github.com/your-username/impactai.git
cd impactai
npm installCopy the example environment file:
cp .env.local.example .env.localAdd your Gemini API key (minimum required to run locally):
# .env.local
GEMINI_API_KEY=your-gemini-key
NEXTAUTH_SECRET=any-random-string-for-local-dev
NEXTAUTH_URL=http://localhost:3000Start the development server:
npm run dev
# Open http://localhost:3000Environment variables
All variables live in .env.local which is never committed to Git.
| Variable | Required | Description |
|---|---|---|
| GEMINI_API_KEY | ✅ Yes | Gemini API key — get at aistudio.google.com/app/apikey |
| NEXTAUTH_SECRET | ✅ Yes | Random secret for signing JWT sessions |
| NEXTAUTH_URL | ✅ Yes | Full URL of your app (http://localhost:3000 locally) |
| KV_REST_API_URL | ✅ Yes | Upstash Redis REST URL — stores launched tokens & rate limits |
| KV_REST_API_TOKEN | ✅ Yes | Upstash Redis REST token (auth for the above) |
| GOOGLE_CLIENT_ID | Optional | Google OAuth app client ID |
| GOOGLE_CLIENT_SECRET | Optional | Google OAuth app client secret |
| GITHUB_CLIENT_ID | Optional | GitHub OAuth app client ID |
| GITHUB_CLIENT_SECRET | Optional | GitHub OAuth app client secret |
| BAGS_API_KEY | Optional | Bags SDK key for production token launch |
KV_REST_API_URL / KV_REST_API_TOKEN automatically. The app also accepts STORAGE_URL / STORAGE_TOKEN or UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN — whichever is present. Without storage configured, the app still runs: token persistence and rate limiting simply no-op (fail open).Project structure
impactai/
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ ├── auth/[...nextauth]/route.ts # NextAuth handler
│ │ │ ├── generate/route.ts # Gemini AI generation (rate-limited)
│ │ │ └── tokens/
│ │ │ ├── route.ts # GET list / POST save tokens
│ │ │ ├── [id]/route.ts # GET / PATCH a single token
│ │ │ └── stats/route.ts # Aggregate platform stats
│ │ ├── docs/page.tsx # This page
│ │ ├── launch/page.tsx # Token creation app
│ │ ├── portfolio/page.tsx # Creator's launched tokens
│ │ ├── layout.tsx # Root layout + providers
│ │ ├── page.tsx # Landing page
│ │ └── globals.css
│ ├── components/
│ │ ├── AuthModal.tsx # Wallet + OAuth modal
│ │ ├── AuthSessionProvider.tsx # NextAuth SessionProvider
│ │ ├── FeeSplitVisual.tsx # Interactive fee bars
│ │ ├── GeneratingScreen.tsx # AI loading animation
│ │ ├── HomeScreen.tsx # Cause input screen
│ │ ├── LaunchedScreen.tsx # Post-launch dashboard (saves token)
│ │ ├── LiveLaunchesList.tsx # Live launches list
│ │ ├── Navbar.tsx # Header with auth state
│ │ ├── PreviewScreen.tsx # Token review screen
│ │ ├── ThemeProvider.tsx # next-themes wrapper
│ │ ├── ThemeToggle.tsx # Light/dark/system toggle
│ │ └── TickerBar.tsx # Live launches ticker
│ ├── lib/
│ │ ├── constants.ts # Static data & AI prompt
│ │ ├── tokens.ts # Redis token store + stats
│ │ ├── ratelimit.ts # Upstash rate limiter (fail-open)
│ │ ├── useConnectedUser.ts # Unified wallet + OAuth identity
│ │ ├── useAnimatedCounter.ts # Animated number hook
│ │ └── utils.ts # cn(), formatCurrency(), etc.
│ └── types/
│ ├── index.ts # App + UI types
│ └── token.ts # LiveToken & PlatformStats
├── .env.local.example
├── next.config.js
├── tailwind.config.ts
└── tsconfig.jsonHow it works
The full user journey from cause description to live token takes four steps:
User describes their cause
A single text input on the launch page. The user types one sentence describing the community problem they want to solve — e.g. "Solar panels for 200 homes in rural Ethiopia". Example prompts are provided to lower the barrier.
AI generates the token identity
The cause text is sent to the Gemini API via a Next.js route. Gemini returns a JSON object containing a token name, ticker (4–6 capital letters), description, relevant emoji, cause wallet label, and a pre-written viral share hook.
User reviews and launches
The generated token is shown in a preview screen. The user can regenerate, edit their cause, or click Launch. In production this calls bagsSDK.launchToken() which deploys the token on Solana and registers the fee split configuration.
Fees flow to the cause
Every on-chain trade of the token triggers the fee split: 40% goes to a designated cause wallet, 30% is redistributed to all token holders as cashback, 20% goes to the creator, and 10% to the platform. No manual distribution needed.
Fee splits
The fee split is set once at token launch via the Bags SDK and applies automatically to every trade thereafter. The four recipients are:
Token launch flow
In production, replace the mock setScreen("launched") call in src/app/launch/page.tsx with the real Bags SDK call:
npm install @bagsfm/bags-sdk @solana/web3.js @solana/wallet-adapter-reactimport { BagsSDK } from "@bagsfm/bags-sdk";
const sdk = new BagsSDK({ apiKey: process.env.BAGS_API_KEY });
const result = await sdk.launchToken({
name: token.name,
ticker: token.ticker,
description: token.description,
feeShares: {
cause: "0.40", // 40% → community wallet
holders: "0.30", // 30% → token holders
creator: "0.20", // 20% → you
platform: "0.10", // 10% → ImpactAI
},
causeWallet: "SOLANA_WALLET_ADDRESS_FOR_CAUSE",
metadata: {
image: token.emoji,
tags: token.tags,
},
});
console.log("Token live:", result.mintAddress);Token storage
Launched tokens are persisted to Upstash Redis via src/lib/tokens.ts. Each token is stored as a JSON value keyed by id, plus a sorted set (scored by creation time) so lists come back newest-first.
LaunchedScreensaves the token once on mount viaPOST /api/tokens, then pollsGET /api/tokens/[id]every 20s for live stats.TickerBarandLiveLaunchesListreadGET /api/tokensto render the home page launches.- Stats start at zero — a mock launch has no on-chain volume until real Bags trades exist.
Portfolio
/portfolio shows the tokens launched by the connected user, with aggregate summary cards (tokens, total raised, supporters, on-chain txns) and a card per token. It polls GET /api/tokens every 15s and filters client-side by creator identity.
Attribution uses a stable creatorId derived by creatorIdOf() in src/lib/useConnectedUser.ts: wallet users key off their address (wallet:<addr>), social users off their display name (user:<name>). The same id is written on the token at launch and used to filter the portfolio.
A Portfolio link appears in the navbar and the profile dropdown once a user is connected, plus a View my tokens button on the launched screen.
Wallet connection
The AuthModal component handles three Solana wallets by calling their browser extension APIs directly — no adapter library required:
| Wallet | Detection | Connect call |
|---|---|---|
| Phantom | window.solana.isPhantom | window.solana.connect() |
| Solflare | window.solflare.isSolflare | window.solflare.connect() |
| Backpack | window.xnft.solana | window.xnft.solana.connect() |
When a wallet is detected, the modal shows a green Detected badge. If not installed, it shows an install link to the wallet's website. After connecting, the public key is shortened and shown in the navbar.
OAuth — Google & GitHub
Social login uses NextAuth.js v4 with JWT sessions (no database required).
Setting up Google OAuth
- Go to console.cloud.google.com and create a new project.
- Navigate to APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID.
- Set Application type to Web application.
- Add
http://localhost:3000/api/auth/callback/googleas an authorised redirect URI. - Copy the Client ID and Client Secret into
.env.local.
Setting up GitHub OAuth
- Go to github.com/settings/developers.
- Click OAuth Apps → New OAuth App.
- Set the Authorization callback URL to
http://localhost:3000/api/auth/callback/github. - Click Register application, then generate a client secret.
- Copy both values into
.env.local.
https://impactai.vercel.app/api/auth/callback/google, and update NEXTAUTH_URL in your Vercel environment variables.Session management
Identity is unified through the useConnectedUser() hook (src/lib/useConnectedUser.ts), which merges two sources and survives navigation and reloads:
- NextAuth session — stored in a signed JWT cookie, persists across page loads. Read via
useSession(). - Wallet state — persisted to
localStorage(keyimpactai:wallet-user) so a connected wallet is still recognized on/portfolioand after a refresh.
Wallet identity takes priority; the hook returns a single user plus walletUser, setWalletUser, and disconnect:
const { user, walletUser, setWalletUser, disconnect } = useConnectedUser();
// user = persisted wallet user, else the NextAuth session user
// creatorIdOf(user) → stable id used to attribute & filter launched tokensPOST /api/generate
Route that calls the Gemini API and returns a structured token object. The cause text is sanitized (control characters stripped, whitespace collapsed) and length-validated before it reaches the model, and the endpoint is rate-limited per client IP (10 requests / minute) to protect against abuse and runaway cost.
Request
POST /api/generate
Content-Type: application/json
{
"cause": "Solar panels for 200 homes in rural Ethiopia"
}Response
{
"token": {
"name": "EthioSolar",
"ticker": "ESOL",
"description": "EthioSolar is lighting up 200 homes...",
"emoji": "☀️",
"causeWallet": "Solar panel installation for 200 rural homes",
"viralHook": "I just launched a solar token — trade it and power Ethiopia",
"tags": ["energy", "africa", "solar"]
}
}Errors
| Status | Reason |
|---|---|
| 400 | Cause text missing or under 5 characters |
| 400 | Cause text over 300 characters |
| 429 | Rate limit exceeded — too many requests from this IP |
| 500 | GEMINI_API_KEY not set in environment |
| 502 | Gemini API returned a non-200 response |
| 502 | AI response empty or could not be parsed as valid JSON |
Tokens API
CRUD-ish endpoints over the Redis token store. Backed by src/lib/tokens.ts.
| Endpoint | Description |
|---|---|
| GET /api/tokens | List all launched tokens, newest first |
| POST /api/tokens | Save a launched token (sets creatorId, stats start at 0) |
| GET /api/tokens/[id] | Fetch a single token by id (used for live-stat polling) |
| PATCH /api/tokens/[id] | Update a token's stats / proof photos |
| GET /api/tokens/stats | Aggregate platform stats (totals + countries) |
POST body & saved shape
POST /api/tokens
{
"name": "EthioSolar",
"ticker": "ESOL",
"description": "...",
"emoji": "☀️",
"causeWallet": "...",
"viralHook": "...",
"tags": ["energy", "solar"],
"creatorId": "wallet:7xk...", // stable creator identity
"creatorWallet": "7xk...",
"creatorDisplay": "7xk…9fA"
}
// → 201 { token: LiveToken } (raised/supporters/volume24h/change24h/txCount = 0)NextAuth routes
The catch-all handler at src/app/api/auth/[...nextauth]/route.ts exposes these endpoints automatically:
GET /api/auth/signin # Sign-in page redirect
GET /api/auth/signout # Signs the user out
GET /api/auth/session # Returns current session JSON
GET /api/auth/callback/google # Google OAuth callback
GET /api/auth/callback/github # GitHub OAuth callback
GET /api/auth/csrf # CSRF tokenDeploy to Vercel
# Install Vercel CLI
npm i -g vercel
# Deploy (first time sets up the project)
vercel
# Set environment variables
vercel env add GEMINI_API_KEY
vercel env add NEXTAUTH_SECRET
vercel env add NEXTAUTH_URL # https://your-app.vercel.app
# Storage — or add a Vercel KV / Upstash store, which injects these for you
vercel env add KV_REST_API_URL
vercel env add KV_REST_API_TOKEN
vercel env add GOOGLE_CLIENT_ID
vercel env add GOOGLE_CLIENT_SECRET
vercel env add GITHUB_CLIENT_ID
vercel env add GITHUB_CLIENT_SECRET
# Deploy to production
vercel --prodProduction checklist
Wiring up Bags SDK
The current codebase has a placeholder launch. Here is how to make it real:
1. Install the SDK:
npm install @bagsfm/bags-sdk @solana/web3.js2. In src/app/launch/page.tsx, find the handleLaunch function and replace it:
const handleLaunch = useCallback(async () => {
if (!user && !walletUser) { setAuthOpen(true); return; }
const { BagsSDK } = await import("@bagsfm/bags-sdk");
const sdk = new BagsSDK({ apiKey: process.env.NEXT_PUBLIC_BAGS_API_KEY });
try {
const result = await sdk.launchToken({
name: token!.name,
ticker: token!.ticker,
description: token!.description,
feeShares: {
cause: "0.40",
holders: "0.30",
creator: "0.20",
platform: "0.10",
},
causeWallet: "YOUR_CAUSE_SOLANA_WALLET_ADDRESS",
});
console.log("Mint address:", result.mintAddress);
setScreen("launched");
} catch (err) {
setError("Launch failed. Check your Bags API key.");
}
}, [token, user, walletUser]);Community tokens. Real change. Built for #BagsHackathon.