Three developers, one hackathon, and a viral meme turned into a mission. This is the story of B40 Life Simulator, a financial literacy game inspired by the real B40 experience and the financial struggles faced by Malaysian youth. It balances social impact with a clever technical strategy. By pairing Convex for real-time gameplay with TiDB Cloud for behavioral analytics, the team solved a critical challenge: decoupling high-speed user interaction from complex data aggregation.
Their architecture demonstrates just how accessible distributed SQL has become. Using a “fire-and-forget” sync pattern, the team utilized TiDB Cloud to transform raw player choices into deep insights—like identifying specific financial failure points—without slowing down the game. It is a perfect blueprint for developers looking to add powerful, scalable analytics to any application in record time.
Check out their story below and discover how they brought home the “Best Use of TiDB” trophy in the Cursor x Anthropic Hackathon Malaysia.
Gamifying Financial Literacy: Solving the B40 Struggle
We’re Melon (Adam), Rimba (Redzwan), and PekNga (Shafeeq)—three Malaysian developers who registered for the Cursor Hackathon as DGD (Dark Game Dev), our edgy team name. We’re actually Wayang Studio, a small creative studio that’s spent countless hours discussing ideas for making our own game someday.

Walking into Monash University for the hackathon briefing, the nerves hit hard. For Adam and Shafeeq, this was their first hackathon ever.
“Banyak team compete wei. And we only have one day. Can we even finish something?”
We had a secret weapon though: prompt engineering. We asked ChatGPT to analyze the last 5 years of hackathon winners. The AI came back with patterns: social impact, creative use of technology, solving real problems people care about. Then it recommended an idea that clicked with all of us: a life simulator focused on financial literacy.
Combined with a viral B40 meme we’d all seen, the concept crystallized.
“What if… We made that into a game? Like, actually simulate the B40 experience?”
Beyond the meme, we genuinely cared about the message. 60% of Malaysian youth can’t handle a RM1,000 emergency. Financial literacy taught through boring pamphlets doesn’t work. We’ve all seen the statistics, the infographics, the well-meaning campaigns. Numbers don’t create understanding. They don’t build empathy.
Growing up in Malaysia, we’ve witnessed the struggles of the B40 firsthand. Fresh graduates crushed by RM30,000 PTPTN student loans. Single parents juggling work and childcare on RM1,800 a month. The impossible choices between paying bills, buying groceries, or staying sane.
What if you could experience financial struggle in a game? Learn by living it before real life teaches you the hard way.
And just like that, B40 Life Simulator was born. From AI research + a meme. (The best ideas often are.)

The Technical Stack: Choosing TiDB Cloud and Convex for Performance
We had a strategy: the hackathon had 19 different tracks to compete in. What if we designed our game to qualify for as many as possible? We moved from the open area to the workshop room upstairs, attending sessions to learn new tools while simultaneously implementing them into our idea. Multitasking level: chaos.
The TiDB workshop was especially valuable. It was actually our first time getting hands-on with TiDB, and the learning curve turned out to be much smoother than we expected. We learned about distributed SQL, TiDB Cloud setup, and how to structure analytics queries. Plus, we got TiDB shirts. Looking fresh while coding, you know how it is.
By the end, we’d integrated 10 out of 19 tracks into our game. Here’s what we chose and why:
| Layer | Technology | Reason for Selection |
| Frontend | Next.js, React | Fast iteration, familiar patterns |
| Game State | Convex | Real-time sync without manual infra |
| AI Logic | Claude (Anthropic) | Stateful dialogue and memory |
| Analytics | TiDB Cloud Starter | SQL analytics with zero setup |
| Deployment | Vercel | Push-and-forget |
With our stack chosen, we had to figure out the architecture. How do we optimize for both instant gameplay and deep analytics?
The Architecture: Separation of Concerns
We designed B40 Life Simulator with a clear separation of concerns: Convex for real-time gameplay, and TiDB for analytics and research data. This architecture allowed us to optimize for both player experience and data insights.

The core insight: gameplay and analytics have different performance requirements. Gameplay needs low latency (< 100ms response time). Analytics needs aggregation across thousands of sessions. Trying to do both in one database creates conflicts.
Here’s how the data flows: Player → Next.js → Convex (instant gameplay) → TiDB (analytics via fire-and-forget sync).

The Implementation: Building a Production-Ready TiDB Connection Layer with Cursor AI and Fire-and-Forget Sync
Cursor helped us set up the TiDB connection with SSL, write parameterized queries, and handle transactions correctly. The AI understood TiDB’s MySQL compatibility and suggested optimizations.
We’d prompt: “Set up a TiDB connection pool with SSL for analytics, with helpers for queries, single results, and transactions. Use mysql2/promise.“
Within minutes, we had production-ready connection pooling code:
import mysql from "mysql2/promise";
const tidbConfig = {
host: process.env.TIDB_HOST,
port: parseInt(process.env.TIDB_PORT || "4000"),
user: process.env.TIDB_USER,
password: process.env.TIDB_PASSWORD,
database: process.env.TIDB_DATABASE,
ssl: {
minVersion: "TLSv1.2" as const,
rejectUnauthorized: true,
},
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
};
let pool: mysql.Pool | null = null;
export function getPool(): mysql.Pool {
if (!pool) {
pool = mysql.createPool(tidbConfig);
}
return pool;
}
export async function query<T>(sql: string, params?: unknown[]): Promise<T[]> {
const connection = await getPool().getConnection();
try {
const [rows] = await connection.execute(sql, params);
return rows as T[];
} finally {
connection.release();
}
}
The singleton pattern (if (!pool)) ensures that we reuse connections across API requests, while the try/finally block guarantees connections always return to the pool. This is critical for avoiding connection leaks during the hackathon chaos.
Why We Used a “Fire-and-Forget” Sync Pattern
We implemented a fire-and-forget sync pattern: game state lives in Convex for real-time reactivity, and at key moments (week completion, game over), we sync to TiDB for analytics.
When a player completes a week, this Convex action fires. It grabs the current game state, packages it up, and sends it to our Next.js API route that handles TiDB writes:
export const syncWeeklyProgress = action({
args: {
gameId: v.id("games"),
weekCompleted: v.number(),
weekendActivity: v.optional(v.string()),
},
handler: async (ctx, args): Promise<SyncResult> => {
// Get game state from Convex
const game = await ctx.runQuery(api.games.getGame, { gameId: args.gameId });
// Get decisions for this specific week
const allDecisions = await ctx.runQuery(api.games.getAllDecisions, {
gameId: args.gameId,
});
const weekDecisions = allDecisions.filter(d => d.week === args.weekCompleted);
// Prepare snapshot data for TiDB
const snapshotData: WeeklySnapshotData = {
convex_game_id: args.gameId,
player_name: game.playerName || "Anonymous",
persona_id: game.personaId,
week: args.weekCompleted,
money: game.money,
credit_score: game.creditScore,
health: game.health,
stress: game.stress,
objectives_completed: checkObjectives(game.weeklyObjectives),
is_game_over: game.isGameOver,
// ... more fields
};
// Fire-and-forget sync to TiDB via API route
await fetch(`${baseUrl}/api/analytics/sync`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Internal-Key": process.env.INTERNAL_API_KEY,
},
body: JSON.stringify({
syncType: "weekly",
weeklySnapshot: snapshotData,
decisions: decisionsData,
}),
});
return { success: true };
},
});
The key here is the fetch call doesn’t await the TiDB write completion—we fire the request and immediately return success to the player. This keeps the UI responsive while ensuring analytics data eventually reaches TiDB.
Analytics Queries: Answering Real Questions
We built a comprehensive analytics layer that answers key research questions. For example, we wanted to know: what causes players to fail, and when does it happen?
export async function getFailurePatterns(): Promise<FailurePattern[]> {
return query(
`SELECT
failure_reason,
COUNT(*) as count,
persona_id,
AVG(weeks_completed) as avg_week_failed
FROM completed_games
WHERE failure_reason IS NOT NULL
GROUP BY failure_reason, persona_id
ORDER BY count DESC`
);
}
// Example insight: "Fresh graduates fail most often in week 2
// due to 'health_crisis' - choosing instant noodles over vegetables"
This query revealed something interesting: fresh graduates (persona_id “graduate”) consistently fail in week 2 from health crises. They prioritize saving money over nutrition, then get hit with medical bills that wipe them out. This insight informed us how we balanced the game’s difficulty.
Another critical metric: the survival funnel. How many players make it past each week?
export async function getSurvivalFunnel(): Promise<Array<{
week: number;
started: number;
survived: number;
survival_rate: number;
}>> {
const weekCounts = await query<{ week: number; count: number }>(
`SELECT week, COUNT(DISTINCT convex_game_id) as count
FROM weekly_snapshots
GROUP BY week
ORDER BY week`
);
// Calculate week-over-week retention
return weekCounts.map((wc, index) => ({
week: wc.week,
started: wc.count,
survived: wc.count - (endMap.get(wc.week) || 0),
survival_rate: Math.round(survivalRate),
}));
}
We discovered that Week 2 has a 35% drop-off rate, the steepest cliff in the game. This told us our difficulty curve might be too harsh, but it also mirrors real-world financial struggles where the second month after starting a new job (before the first paycheck) is often the hardest.
The Challenges: What Almost Broke Us (But Didn’t)
This is where things got real. Bugs appeared. Features broke. The pressure mounted.
The Parallel Session Bug: Our biggest nightmare was when the game synced one global state instead of individual sessions. When one player made a choice, everyone saw it. We finally traced it to how we were handling Convex subscriptions. The relief when we fixed it at 3am was indescribable.
“Bro, why is my character suddenly bankrupt? I didn’t do anything!”
“…that was me. Wrong tab.”
TiDB SSL Connection: Setting up secure SSL connections to TiDB Cloud required careful configuration. We had to ensure TLS 1.2 minimum and proper certificate validation while keeping connection pooling efficient.
Idempotent Syncs: With fire-and-forget syncs, we needed to handle duplicate submissions gracefully. Using ON DUPLICATE KEY UPDATE and composite unique keys (game_id + week) ensured data consistency.
Cultural Authenticity: Using Manglish naturally, not forced. Pak Ali’s “I can give you small discount, old customer lah” had to feel genuine, not performative.
We continued the grind at an Airbnb in KL, with Menara KL right in front of us, crystal clear, and the Petronas Twin Towers glowing in the distance. Pretty good motivation for an all-nighter.
Results: From Viral Meme to Hackathon Winner
After fixing the last critical bugs (and leaving a few minor ones for “character”), we finally hit publish and submit. 5am in the morning.
Eyes burning, brains fried, but somehow satisfied. We’d built something. A real game. In one day. From a meme.
“Okay, that’s it. If we win, great. If not, at least we have a game now.”
We crashed for 2 hours of sleep, then dragged ourselves back to Monash University for the results announcement. Running on fumes and hope.
Our Game by the Numbers:

Back in the auditorium, exhaustion hit hard. Rimba was so tired he fell asleep in his seat, mouth wide open, in front of everyone. Classic.
The Cursor track results came in. We didn’t win. Okay, fair enough. We went home that day without a trophy, but with something better: a game we were proud of and plans to expand it further.
Wait… We Actually Placed?
A week after the hackathon, the results dropped. We weren’t expecting much at this point.
Then we saw our names. Twice.
1st Place: BEST USE OF TIDB
2nd Place: BYTE IN TRACK
Three gamers who just wanted to make a game. A meme that became a mission. And somehow, against all expectations, we’d placed in two tracks.
Play the game: B40 Life Simulator
The dual-database architecture worked because we matched databases to workloads. Convex handles high-frequency writes with low latency. TiDB handles analytical queries across all sessions. The fire-and-forget sync keeps them in sync without coupling them.
Beyond the trophy, we genuinely believe B40 Life Simulator can make a difference. The analytics we collect through TiDB aren’t just numbers. They’re insights into how people make financial decisions under pressure.
Try It Yourself
The hackathon prototype is live and playable. Experience the B40 struggle firsthand. Make impossible choices. See how long you survive. Next step: rebuilding in Unity for mobile. Everyone’s welcome to try!
Tech Stack: Next.js 15, React 19, TypeScript, Convex, TiDB Cloud Starter, Claude AI, PixiJS, Tailwind CSS 4, Framer Motion, shadcn/ui, Cursor, and Vercel.
Spin up a database with 25 GiB free resources.
TiDB Cloud Dedicated
A fully-managed cloud DBaaS for predictable workloads
TiDB Cloud Starter
A fully-managed cloud DBaaS for auto-scaling workloads