I'm a Mechatronics Engineering student who just shipped an AI academic platform for Nigerian university students. This is the technical breakdown of how it's built.
THE ARCHITECTURE DECISIONS
When you're building for Nigeria with zero funding, you can't make the same architectural decisions as well-funded startups. Every choice matters because every API call costs money you don't have.
Here's how I built this.
SEMANTIC SEARCH WITHOUT BREAKING THE BANK
The core problem: A student uploads 500MB of course materials. They ask a question. I need to find the relevant sections instantly without:
- Calling the entire corpus through Claude/GPT-4 (prohibitively expensive)
- Using expensive vector search services (Pinecone costs $$)
- Processing the same documents repeatedly (waste)
The solution: Local vector embeddings + Qdrant (self-hosted).
Architecture:
Student uploads materials
↓
Convert to chunks (1024 tokens, 200-token overlap)
↓
Generate embeddings (using nomic-embed-text, free via Ollama on a cheap VPS)
↓
Store in Qdrant (lightweight, self-hosted, ~2GB RAM for millions of vectors)
↓
On query: embed the question, cosine-similarity search, retrieve top-K chunks
↓
Pass only relevant chunks to Claude (not the entire corpus)
↓
Response generated with context
Why this works:
- Embeddings are done once, offline
- Queries are lightning-fast (Qdrant is optimized for this)
- I'm not paying per-token for irrelevant material processing
- Running Ollama on a cheap VPS (₦15k/month) is infinitely cheaper than Pinecone's subscription
Real numbers:
- Average query: 50 tokens → Claude call
- Without vector search: 5000 tokens → 100x more expensive
- Annual saving: enough to sustain the infrastructure
VERIFICATION PIPELINE: NEVER TRUST THE MODEL
Math hallucinations will make students fail exams. This can't happen.
Here's the verification flow:
def generate_and_verify_answer(question, context):
for attempt in range(3):
answer = generate_answer_with_claude(question, context)
if contains_math(answer):
# Extract LaTeX expressions
expressions = extract_latex(answer)
# Verify each with SymPy
for expr in expressions:
try:
symbolic_result = sympy.simplify(expr)
# Also numerically evaluate
numeric_check = float(symbolic_result)
# If this passes, mark as verified
answer = mark_as_verified(answer, expr)
except Exception as e:
# Math is wrong, log it, try again
log_verification_failure(expr, e)
answer = None
break
if answer and answer_passes_quality_check(answer):
return answer
# If we got here, answer failed verification
# Regenerate with explicit instruction to fix it
add_to_context("Previous attempt was mathematically incorrect. Recalculate.")
# After 3 failures, flag for human review
return flag_for_human_review(question, context)
The workflow:
- Generate answer with Claude (using system prompt emphasizing accuracy)
- Extract all mathematical expressions
- Verify with SymPy (both symbolic and numeric evaluation)
- If verification fails, regenerate with error context
- Retry up to 3 times
- If still failing, queue for human review
Cost optimization:
- Only math-heavy courses run full verification
- Lightweight semantic similarity checks for straight recall questions
- Caching verified answers (if Question A and Question B are semantically similar, reuse the verified response)
This prevents hallucinations without tripling my API costs.
OFFLINE-FIRST PWA: WORKING ON ₦40K PHONES
Nigeria's internet is unreliable. Many students use cheap Android phones. I can't require:
- Constant connectivity
- Large app downloads (uses data they have to pay for)
- Installation from Play Store (requires Google Play account verification that many students lack)
Solution: Progressive Web App (PWA) with service workers and IndexedDB.
Tech stack:
- Frontend: React + Vite (small bundle size is critical)
- State management: Zustand (lighter than Redux)
- Offline storage: IndexedDB + Dexie.js wrapper
- Sync: Custom sync engine (similar to Firestore's approach, but local-first)
- Service Workers: Cache-first for static assets, network-first for API calls
How it works:
// Service worker: cache static assets aggressively
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js',
// Core UI assets only
]);
})
);
});
// On fetch: serve from cache first, update in background
self.addEventListener('fetch', (event) => {
if (event.request.method === 'GET') {
event.respondWith(
caches.open('v1').then((cache) => {
return cache.match(event.request).then((response) => {
// Return cached version immediately
const fetchPromise = fetch(event.request).then((networkResponse) => {
// But also fetch fresh version and cache it
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
}
});
For data:
// IndexedDB for course materials and past answers
const db = new Dexie('UniUI');
db.version(1).stores({
courses: '++id, userId',
courseContent: '++id, courseId', // Stores embeddings locally
answers: '++id, courseId, timestamp',
syncQueue: '++id' // Queue API calls when offline
});
// When offline, queue the request
async function askQuestion(question, courseId) {
if (!navigator.onLine) {
// Store in sync queue
await db.syncQueue.add({
type: 'ask_question',
question,
courseId,
timestamp: Date.now()
});
// Show locally cached similar answers
return db.answers
.where('courseId').equals(courseId)
.toArray();
}
// Online: fetch new answer
return callAPI('/api/ask', { question, courseId });
}
// Sync when connection returns
window.addEventListener('online', async () => {
const queue = await db.syncQueue.toArray();
for (const item of queue) {
await processQueueItem(item);
await db.syncQueue.delete(item.id);
}
});
Results:
- Initial load: ~2.5MB (heavily gzipped)
- Works completely offline after first visit
- Syncs automatically when connection returns
- Runs on phones with 512MB RAM (real Android phones in Nigeria)
- Zero dependency on Play Store
PAYMENT INTEGRATION: LOCAL METHODS FIRST
International payment processors (Stripe, PayPal) don't serve Nigeria well. Most students don't have international cards.
Solution: Paystack (Nigerian payment processor).
Integration:
from paystack import Paystack
paystack = Paystack(secret_key=settings.PAYSTACK_SECRET)
# Plan-based billing
@app.post('/api/subscribe')
async def subscribe(user_id: str, plan: str, email: str):
plan_amount_kobo = {
'basic': 50000, # ₦500
'pro': 200000, # ₦2,000
'premium': 500000 # ₦5,000
}[plan]
# Initialize payment
response = paystack.transaction.initialize(
email=email,
amount=plan_amount_kobo,
metadata={
'user_id': user_id,
'plan': plan,
'usage_type': 'subscription'
}
)
# Store pending transaction
db.pending_transactions.insert({
'user_id': user_id,
'reference': response['data']['reference'],
'plan': plan,
'status': 'pending'
})
return {'auth_url': response['data']['authorization_url']}
# Webhook for payment confirmation
@app.post('/webhooks/paystack')
async def verify_payment(request: Request):
signature = request.headers.get('x-paystack-signature')
# Verify webhook is from Paystack
body = await request.body()
computed_sig = hmac.new(
settings.PAYSTACK_SECRET.encode(),
body,
hashlib.sha512
).hexdigest()
if signature != computed_sig:
return {'error': 'invalid signature'}, 401
data = await request.json()
if data['event'] == 'charge.success':
reference = data['data']['reference']
# Verify transaction with Paystack
payment = paystack.transaction.verify(reference)
if payment['data']['status'] == 'success':
# Activate subscription
transaction = db.pending_transactions.find_one(
{'reference': reference}
)
user_id = transaction['user_id']
plan = transaction['plan']
db.subscriptions.insert({
'user_id': user_id,
'plan': plan,
'started': datetime.now(),
'expires': datetime.now() + timedelta(days=30),
'paystack_customer_code': payment['data']['customer']['customer_code']
})
# Clean up
db.pending_transactions.delete_one({'reference': reference})
return {'status': 'ok'}
Features:
- Bank transfer support (for students without cards)
- Card payments (Visa, Mastercard, Verve)
- Recurring billing for subscriptions
- Chargebacks handled by Paystack
- No international payment complexity
INFRASTRUCTURE: BUILDING LEAN
Since I have zero funding, every infrastructure decision is about cost.
Stack:
- Backend: FastAPI on Railway or Render (free tier, then ₦5k/month)
- Database: PostgreSQL (free tier Render or DigitalOcean)
- Vector DB: Qdrant (self-hosted on same server as backend)
- Embeddings: Ollama running locally (one-time setup, no recurring cost)
- LLM: Claude API (pay per token, ~₦0.003 per response)
- File storage: Cloudflare R2 (₦0.015/GB, rivals S3)
- Frontend hosting: Vercel (free tier)
- Monitoring: Datadog free tier or custom dashboards
Monthly cost breakdown:
- Server: ₦5,000-₦10,000
- Database: ₦0-₦5,000 (free tier covers 5GB, very few large DBs)
- LLM API: ₦50,000-₦100,000 (depends on usage)
- Storage: ₦1,000-₦5,000
- Domain + SSL: ₦0 (free Let's Encrypt)
Total: ₦56,000-₦120,000 per month
At ₦500 per user, I break even after 112-240 paying users. I have over 1,000 early signups.
AI BOTS: AUTOMATION TO SCALE SOLO
I can't hire a team. So I built AI bots to handle operations.
Tessy (Infrastructure bot):
- Monitors server health every 5 minutes
- Alerts via Telegram if CPU > 80%, RAM > 90%, disk > 85%
- Auto-generates daily infrastructure reports
- Logs analysis with Claude (detects patterns in failures)
import anthropic
async def analyze_logs(logs: str):
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
messages=[
{
"role": "user",
"content": f"""Analyze these logs for patterns, anomalies, or issues:
{logs}
Specifically look for:
1. Error spikes
2. Unusual patterns
3. Performance degradation
4. Security concerns
5. Recommendations
Be concise."""
}
]
)
return response.content[0].text
Treasure (Support bot):
- Responds to support emails with Sendgrid
- Categorizes issues (bug, feature request, billing, etc.)
- Generates responses for 70% of common issues
- Escalates complex issues to me with context
Three others:
- Content generator (creates study materials)
- Analytics (generates daily growth reports)
- Morning briefing bot (sends me a 2-minute summary every morning)
This costs me: $0. They run on the same server.
WHAT'S NEXT (TECHNICALLY)
- Wolfram Alpha integration – For advanced math/science
- Multimodal support – Accept handwritten notes, images, videos
- Real-time collaboration – Study with classmates simultaneously
- Model fine-tuning – Train Claude on top 1000 FUTO courses for better specificity
- Podcasts – Auto-generate audio summaries of materials
THE PHILOSOPHY
Every technical decision serves one goal: make quality education accessible to a student on a ₦40,000 phone with ₦500/month.
That constraint forces good engineering:
- Caching instead of recomputing
- Local-first instead of cloud-first
- Verification instead of hope
- Free tools instead of premium services
- Boring, reliable tech instead of shiny new frameworks
This is infrastructure engineering. It should be invisible.
Uni UI is live at app.uniui.com.ng.
If you're building for Africa, building lean, or wrestling with verification in AI systems, let's chat.
The future of Nigerian edtech is being built right now. And it's built on pragmatic engineering.
Uni UI – Your Semester, Supercharged.