I Built a Full Game Studio Inside OpenCode — 48 AI Agents, 100% Free

TL;DR: I made a config pack that turns OpenCode into a structured game development studio with 48 specialized agents and 37 workflow skills — all running on Big Pickle (free). Drop it in your ~/.config/opencode/ and your next game gets a full team.

The Problem With AI-Assisted Game Dev

Solo game dev with AI is genuinely powerful. But there’s a fundamental issue nobody talks about: a single AI session has no structure.

Nothing stops you from hardcoding magic numbers at 2am. Nobody’s checking whether your inventory system actually fits the game’s vision. There’s no QA pass, no architecture review, no one asking “wait, does this mechanic even make sense?”

You end up with a chat assistant, not a team.

What I Built

OpenCode Game Studios is a drop-in configuration for OpenCode that transforms it into a proper studio hierarchy:

Tier 1 — Directors (Strategic)
  creative-director · technical-director · producer

Tier 2 — Department Leads
  game-designer · lead-programmer · art-director
  audio-director · narrative-director · qa-lead · release-manager

Tier 3 — Specialists (22 agents)
  gameplay-programmer · engine-programmer · ai-programmer
  level-designer · systems-designer · sound-designer · writer
  performance-analyst · qa-tester · accessibility-specialist ...

Plus dedicated engine specialists for Godot, Unity, and Unreal — 5 agents per engine covering shaders, scripting, DOTS, GAS, blueprints, replication, UI systems.

48 agents total.

How It Works

Every agent knows its role in the studio. The creative-director guards the vision. The lead-programmer owns architecture decisions. The qa-tester finds edge cases. You call whoever you need:

@game-designer "Design a combat system for my roguelike"
@lead-programmer "Review this ECS architecture"
@godot-shader-specialist "Write a 2D water shader with refraction"
@qa-tester "What are the edge cases in this save system?"

And there are 37 workflow skills via slash commands:

/start           — Kick off a new game project with proper setup
/sprint-plan     — Plan your week with task breakdown
/code-review     — Full architectural review
/balance-check   — Game economy and difficulty audit
/brainstorm      — Structured creative session
/release-checklist — Pre-launch verification
/patch-notes     — Auto-generate from your git history

The Collaboration Protocol

Every task follows a mandatory flow: Question → Options → Decision → Draft → Approval

Agents must ask “May I write this to [filepath]?” before touching your code. They show drafts before requesting approval. Multi-file changes need explicit sign-off for the entire changeset.

You make all final decisions. The agents provide expertise, not autonomy. This single constraint has made the difference between sessions that produce focused output and sessions that spin forever.

100% Free

Everything runs on OpenCode’s Big Pickle model. No API keys, no tokens burning, no credit card. The config defaults every agent to opencode/big-pickle. You can override specific agents to premium models if you want, but the whole setup works out of the box for free.

Getting Started

# Install OpenCode
npm install -g opencode

# Clone the studio config
git clone https://github.com/TraftG/opencode-game-studio
cd opencode-game-studio

# Apply globally
cp -r .opencode/* ~/.config/opencode/

# Start your studio
opencode

That’s it. Open a session, type @creative-director and describe your game idea. Watch it ask you the right questions instead of just vibing.

Why I Made This

I kept running into the same wall: the AI was capable, but sessions had no memory of earlier decisions and no consistent “voice” keeping the project coherent. A creative director who remembers that this is a horror game, not an action game. A QA lead who always checks for save corruption. A technical director who enforces the performance budget you set on day one.

Structured agents with clear roles solved this better than any amount of prompt tweaking.

What’s Next

I’m actively using this on my own projects. If you build something with it or have ideas for new agents/skills, PRs are very welcome.

⭐ github.com/TraftG/opencode-game-studio

Built by @TraftG — reach out on Telegram if you have questions about the setup.

Your Vibe-Coded App Will Break in Production. Here’s Why.

Your Vibe-Coded App Will Break in Production. Here’s Why.

170 out of 1,645 Lovable-built apps had data-exposure vulnerabilities. That’s 10.3% — one in ten apps leaking user data before a single real customer signed up. This stat comes from a May 2025 security audit, and the problem has only gotten worse as vibe coding tools have grown in popularity.

I run a software development company that has rescued 12 vibe-coded apps in the past 6 months. Founders come to us after burning $5,000-$15,000 in Lovable and Bolt.new credits, with an app that works in demos but falls apart under real traffic. The problems are always the same.

The 80/20 Wall: Where Vibe Coding Breaks Down

AI-generated code is brilliant for the first 80% of a project. Landing pages, CRUD forms, basic auth flows, dashboard layouts — Lovable and Bolt.new handle these faster than any human developer.

The last 20% is where projects die:

  • Edge cases. What happens when two users submit the same form simultaneously? When a payment webhook fires twice? When a user’s session expires mid-checkout?
  • Integrations. Stripe webhooks, OAuth flows with refresh tokens, email delivery with bounce handling, file uploads with virus scanning.
  • Performance under load. Your demo works with 3 concurrent users. What happens at 300? At 3,000?

Lovable’s own documentation acknowledges this: the platform is designed for prototyping and MVPs, not production workloads. Bolt.new’s token consumption scales with codebase size, meaning the same edit costs more tokens as your project grows — creating a perverse incentive where fixing bugs gets progressively more expensive.

Vibe Coding Security Problems: The Hidden Risks

The security issues in AI-generated code aren’t random — they follow predictable patterns:

1. Exposed API Keys

Vibe coding tools frequently embed API keys directly in client-side code. We found Stripe secret keys, Supabase service role keys, and SendGrid API keys in the frontend bundle of 4 out of 12 apps we audited.

// This was in a production Lovable app's client bundle
const supabase = createClient(
  'https://xyz.supabase.co',
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' // service_role key - full DB access
);

The service_role key bypasses Row Level Security entirely. Anyone who opens browser DevTools can read, modify, or delete every row in every table.

2. Missing Server-Side Validation

AI-generated apps typically validate inputs on the client only. The server accepts whatever arrives:

// Client-side validation exists...
if (amount > 0 && amount < 10000) {
  await fetch('/api/transfer', { body: JSON.stringify({ amount }) });
}

// ...but the API endpoint trusts the input blindly
app.post('/api/transfer', async (req, res) => {
  const { amount } = req.body; // No server-side validation
  await transferFunds(amount);  // Negative amounts? Infinity? Strings?
});

3. Broken Authentication

Session handling in vibe-coded apps is typically the weakest link. We’ve seen JWT tokens with no expiration, refresh tokens stored in localStorage (XSS-vulnerable), and password reset flows that don’t invalidate previous tokens.

For the full security and architecture audit checklist, we published a detailed production-readiness guide covering every vulnerability pattern we’ve encountered.

Lovable to Production: What Actually Needs to Change

Taking a Lovable or Bolt.new app to production isn’t about rewriting everything. It’s about fixing the 20% that AI tools get wrong. Here’s our audit checklist after 12 production rescues:

Authentication (2-3 days of work)

  • Move from client-side Supabase auth to server-side session management
  • Implement proper refresh token rotation
  • Add rate limiting on login endpoints (prevent brute force)
  • Set up email verification and password reset with token expiration

Database Security (1-2 days)

  • Enable Row Level Security on every Supabase table
  • Move service_role key to server-side only
  • Add database-level input validation constraints
  • Set up automated backups

API Hardening (2-3 days)

  • Move all business logic to API routes (not client-side)
  • Add server-side input validation on every endpoint
  • Implement rate limiting per user/IP
  • Add request logging and monitoring

Deployment and Infrastructure (1-2 days)

  • Set up proper CI/CD pipeline (not manual deploys)
  • Configure environment variables (remove hardcoded keys)
  • Set up error tracking (Sentry or similar)
  • Add uptime monitoring and alerting

Performance (1-2 days)

  • Add database indexes on frequently queried columns
  • Implement API response caching where appropriate
  • Optimize image loading (lazy load, proper formats)
  • Load test with expected traffic + 3x headroom

Total: 7-12 days of professional development work. That’s the gap between a vibe-coded prototype and a production app. Not a rewrite — a hardening sprint.

The Credit Burn Problem: When AI Tools Cost More Than Developers

A pattern we see repeatedly: founders burn $3,000-$8,000 in Lovable credits trying to fix bugs that a developer would solve in 2 hours.

Lovable users report burning 400 credits in under an hour fixing circular bugs. Bolt users describe “endless error loops” where each fix creates a new problem. The AI doesn’t understand the architectural context — it fixes symptoms, not causes.

Here’s the math that founders discover too late:

  • Lovable Pro: $100/month for 5,000 credits
  • Average credits to fix a production bug: 200-500
  • Average bugs in a production launch: 15-30
  • Total credits to stabilize: 3,000-15,000 credits = $600-$3,000

Compare: a senior developer at $40/hour spends 2-4 hours per bug, fixes the root cause (not a workaround), and the fix stays fixed. Total for 15-30 bugs: $1,200-$4,800 — and you get a codebase that’s actually maintainable.

The vibe coding tools aren’t the problem. Using them past their intended scope is.

Production-Ready Checklist: AI App to Real Product

Before launching any vibe-coded app to real users:

Security

  • [ ] All API keys in environment variables, zero in client code
  • [ ] Server-side validation on every API endpoint
  • [ ] Row Level Security enabled on all database tables
  • [ ] JWT tokens expire, refresh tokens rotate
  • [ ] HTTPS enforced, security headers configured

Reliability

  • [ ] Error tracking (Sentry) configured and tested
  • [ ] Database backups running automatically
  • [ ] Health check endpoints monitored
  • [ ] Rate limiting on all public endpoints
  • [ ] Graceful error handling (no white screens)

Performance

  • [ ] Load tested at 3x expected traffic
  • [ ] Database queries use indexes
  • [ ] Static assets cached via CDN
  • [ ] API responses under 200ms for common operations

Operations

  • [ ] CI/CD pipeline (no manual deploys)
  • [ ] Staging environment for testing
  • [ ] Logging with structured output
  • [ ] Runbook for common failure scenarios

When to Keep Vibe Coding, When to Hire Developers

Vibe coding is perfect for:

  • Internal tools used by your team only
  • Landing pages and marketing sites
  • Proof-of-concept demos for investors
  • Personal projects and side projects

Hire developers when:

  • Real users will enter payment information
  • You need to handle sensitive data (health records, financial data)
  • Expected traffic exceeds 100 concurrent users
  • The app has complex integrations (payment processors, email, SMS)
  • You plan to iterate on the product for more than 3 months

The smartest founders we work with use vibe coding for the prototype, validate the idea, then bring in engineers for the production build. They get the speed of AI tools for exploration and the reliability of human engineers for execution.

The worst approach? Trying to make a vibe-coded prototype handle production load. That’s how you get the 10.3% data-exposure stat.

What We Tell Every Founder Who Calls Us

Your Lovable or Bolt.new app isn’t garbage. It’s a prototype. A very good one, probably. The architecture just needs professional hardening — typically 7-12 days of focused work.

Don’t rewrite from scratch. Don’t burn more credits trying to fix it with AI. And definitely don’t launch it to real users with payment processing in its current state.

Get a proper MVP architecture review, fix the security gaps, add monitoring, and ship it right. The total cost of hardening is almost always less than what founders spend on credits trying to fix problems the AI tools created.

Build fast with AI. Ship with engineers. That’s the playbook for 2026.

Yash Korat is the CEO of Geminate Solutions, a custom software development company that has shipped 50+ products for startups across the US, UK, and Australia. His team has rescued 12 vibe-coded apps in the past 6 months alone.

Build AI Agents in Node.js That Handle 10K Requests/Day

Build AI Agents in Node.js That Handle 10K Requests/Day

Most AI agent tutorials teach you to build a toy. A chatbot that answers trivia questions. A script that summarizes text. Cool for a weekend project — useless in production.

I’ve spent the last 18 months shipping AI agents that handle 10,000+ requests per day for SaaS companies. Agents that book appointments, process refunds, triage support tickets, and orchestrate multi-step workflows without human intervention.

Here’s everything I learned about building AI agents with Node.js that actually survive contact with real users.

What Are AI Agents (And What They’re Not)

An AI agent is not a chatbot with extra steps. A chatbot responds to messages. An agent acts.

The difference comes down to three properties:

  1. Autonomy — the agent decides what to do next based on context
  2. Tool use — it can call external APIs, query databases, send emails
  3. Looping — it keeps working until the task is complete, not just one response

Think of it this way: a chatbot is a calculator. An AI agent is an accountant. The accountant decides which calculations to run, pulls the right numbers from your books, and delivers the finished report.

In March 2026, every major LLM provider supports agentic patterns natively. Claude’s tool_use API, OpenAI’s function calling, and Gemini’s function declarations all follow the same core loop: prompt, think, act, observe, repeat.

The real challenge isn’t getting an agent to work. It’s getting it to work reliably at scale.

The Agent Loop: Build an AI Agent in Node.js

Every production AI agent follows this pattern:

User Input -> LLM Reasoning -> Tool Selection -> Tool Execution -> Result Observation -> (loop or respond)

Here’s the minimal implementation using the Claude API:

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

const tools = [
  {
    name: "search_orders",
    description: "Search customer orders by email or order ID",
    input_schema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Email or order ID" },
        status: { type: "string", enum: ["pending", "shipped", "delivered", "refunded"] }
      },
      required: ["query"]
    }
  },
  {
    name: "process_refund",
    description: "Issue a refund for a specific order",
    input_schema: {
      type: "object",
      properties: {
        order_id: { type: "string" },
        reason: { type: "string" },
        amount_cents: { type: "number" }
      },
      required: ["order_id", "reason"]
    }
  }
];

async function runAgent(userQuery) {
  let messages = [{ role: "user", content: userQuery }];

  while (true) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      system: "You are a customer support agent. Use tools to look up orders and process refunds.",
      tools,
      messages
    });

    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find(b => b.type === "text");
      return textBlock?.text || "Task completed.";
    }

    const toolBlocks = response.content.filter(b => b.type === "tool_use");
    if (toolBlocks.length === 0) break;

    messages.push({ role: "assistant", content: response.content });

    const toolResults = [];
    for (const block of toolBlocks) {
      const result = await executeTool(block.name, block.input);
      toolResults.push({
        type: "tool_result",
        tool_use_id: block.id,
        content: JSON.stringify(result)
      });
    }
    messages.push({ role: "user", content: toolResults });
  }
}

This is the foundation. The while(true) loop is intentional — the agent keeps calling tools until it has enough information to respond.

Claude API in Node.js: Tool Calling That Works

The executeTool function is where your business logic lives:

async function executeTool(name, input) {
  const toolHandlers = {
    search_orders: async ({ query, status }) => {
      const orders = await db.orders.findMany({
        where: {
          OR: [{ email: query }, { id: query }],
          ...(status && { status })
        },
        take: 10
      });
      return { orders, count: orders.length };
    },

    process_refund: async ({ order_id, reason, amount_cents }) => {
      const order = await db.orders.findUnique({ where: { id: order_id } });
      if (!order) return { error: "Order not found" };
      if (order.status === "refunded") return { error: "Already refunded" };

      const refund = await stripe.refunds.create({
        payment_intent: order.paymentIntentId,
        amount: amount_cents || undefined
      });

      await db.orders.update({
        where: { id: order_id },
        data: { status: "refunded", refundReason: reason }
      });

      return { success: true, refund_id: refund.id };
    }
  };

  const handler = toolHandlers[name];
  if (!handler) return { error: `Unknown tool: ${name}` };

  try {
    return await handler(input);
  } catch (err) {
    return { error: err.message };
  }
}

Two patterns to notice: every tool returns structured data (never raw errors), and every tool validates inputs before executing side effects. Never trust the LLM to validate — validate in the handler.

Production Error Handling: Deploy AI Agents That Don’t Crash

The tutorial code above will crash in production within hours. Here’s what you actually need:

async function runAgentWithGuards(userQuery, maxIterations = 10) {
  let messages = [{ role: "user", content: userQuery }];
  let iterations = 0;

  while (iterations < maxIterations) {
    iterations++;
    let response;

    try {
      response = await client.messages.create({
        model: "claude-sonnet-4-6",
        max_tokens: 1024,
        tools,
        messages
      });
    } catch (err) {
      if (err.status === 429) {
        const delay = Math.min(1000 * Math.pow(2, iterations), 30000);
        await new Promise(r => setTimeout(r, delay));
        iterations--;
        continue;
      }
      throw new Error(`Agent failed: ${err.message}`);
    }

    if (response.stop_reason === "end_turn") {
      const text = response.content.find(b => b.type === "text");
      return {
        answer: text?.text || "Done.",
        iterations,
        tokensUsed: response.usage.input_tokens + response.usage.output_tokens
      };
    }

    const toolBlocks = response.content.filter(b => b.type === "tool_use");
    if (toolBlocks.length === 0) break;

    messages.push({ role: "assistant", content: response.content });

    const toolResults = [];
    for (const block of toolBlocks) {
      try {
        const result = await executeTool(block.name, block.input);
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: JSON.stringify(result)
        });
      } catch (toolErr) {
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: JSON.stringify({ error: toolErr.message }),
          is_error: true
        });
      }
    }
    messages.push({ role: "user", content: toolResults });
  }

  return { answer: "Could not complete within allowed steps.", iterations, maxedOut: true };
}

Three production patterns that save you at 3 AM:

  1. Max iterations cap. Without it, a confused agent loops forever burning API credits. We use 10 for simple agents, 25 for complex workflows.
  2. Exponential backoff on 429/529. Claude and OpenAI both rate-limit under load. Crashing on a 429 is amateur hour.
  3. Tool-level error isolation. If one tool throws, catch it and return the error as a tool result. The LLM can often recover.

Agentic AI Patterns for SaaS Applications

Shipping a single agent is the easy part. Shipping an agent system for a SaaS product means solving coordination, state management, and cost control.

Here are the patterns we use at Geminate Solutions when building AI-powered features for production SaaS:

Pattern 1: Router Agent to Specialist Agents

Don’t build one mega-agent. Build a lightweight router that classifies intent and delegates to specialists. A billing agent has Stripe tools only. A technical agent has logs and deployment tools. Each specialist has a narrow tool set, which reduces hallucination and keeps token costs down.

Pattern 2: Human-in-the-Loop Checkpoints

For high-stakes actions (refunds over $100, account deletions), pause the agent loop and request human approval via Slack webhook. This isn’t optional for B2B SaaS — your customers will demand it.

Pattern 3: Conversation Memory with Sliding Window

Long conversations blow up your token budget. Keep the system prompt + last 6 messages + a compressed summary of earlier messages. The Claude API SaaS integration guide covers token optimization strategies in depth.

Hybrid Automation: When Code Meets No-Code

Not every AI workflow needs custom code. For internal operations — lead scoring, email triage, content pipelines — combining Node.js agents with tools like n8n gives you the best of both worlds.

The pattern: n8n handles the trigger (new email, form submission, scheduled job) and calls your Node.js agent via HTTP webhook. The agent does the reasoning, n8n handles the plumbing. This hybrid approach cuts development time by 40-60% for internal automation.

AI Agent Architecture: Production Reference

Here’s the architecture handling 10K+ daily requests for one of our SaaS clients:

API Gateway (Express/Fastify) - Rate limiting, auth, request queue
  |
  Router Agent (Claude Haiku) - Intent classification, fast + cheap
  |
  +-- Support Agent (Sonnet) - 6 tools (tickets, orders, FAQ)
  +-- Sales Agent (Sonnet) - 4 tools (CRM, calendar, email)
  +-- Ops Agent (Sonnet) - 8 tools (deploy, logs, alerts)
  |
  Tool Execution Layer - Postgres, Stripe, SendGrid, Slack
  |
  Observability - Token tracking, latency, cost per conversation

Key decisions: Haiku for routing (200ms, 1/10th cost), Sonnet for reasoning. Tool execution layer is shared across agents. Observability is non-negotiable — track tokens per conversation and cost per resolution.

Deploy Your AI Agent: Production Checklist

Before you ship:

  • Max iteration cap to prevent infinite loops
  • Request timeout (30s simple, 120s multi-step)
  • Rate limit handling with exponential backoff
  • Tool error isolation with structured error returns
  • Token budget tracking with daily spend alerts
  • Input sanitization before LLM processing
  • Output guardrails validating response format
  • Human-in-the-loop for high-risk actions
  • Fallback path routing to human support on failure
  • Cost monitoring with 80% threshold alerts

Start with a single, well-scoped agent. Customer support triage is the best first project — clear intent classification, bounded tool set, measurable ROI.

The teams shipping the fastest right now aren’t the ones with the most ML engineers. They’re the ones who treat AI agents as software engineering problems — with proper error handling, observability, testing, and deployment pipelines.

Build it like you’d build any production system. Because that’s exactly what it is.

Yash Korat is the CEO of Geminate Solutions, a custom software development company shipping AI-powered products for startups across the US, UK, and Australia. His team integrates Claude, GPT-4, and Gemini APIs into production SaaS applications.

Query Live AI Inference Pricing with the ATOM MCP Server

If you’ve ever tried to compare LLM pricing across vendors you know how painful it is. One charges per token, another per character, another per request. Cached input discounts exist but good luck finding them. Context window pricing is buried. And by the time you’ve normalized everything into a spreadsheet something changed on a pricing page and your numbers are stale.

This is the problem ATOM was built to solve. It tracks 2,583 SKUs across 47 vendors, normalizes everything to a common unit, and exposes it all through an MCP server your agents can query directly.

Here’s how to set it up and what you can actually do with it.

What MCP gives you here

Model Context Protocol lets AI agents connect to external data sources through a standardized interface. Claude, Cursor, Windsurf and others support it natively.

Instead of pasting a pricing table into your prompt and hoping it’s current, you give your agent a live connection to the source. It queries, reasons, and acts on real numbers.

Setting up the ATOM MCP server

ATOM’s server is published on npm, Smithery, and the official MCP registry.

Claude Desktop

Add this to your claude_desktop_config.json and restart:

{
  "mcpServers": {
    "atom-pricing": {
      "command": "npx",
      "args": ["-y", "atom-mcp-server"]
    }
  }
}

Cursor or Windsurf

Add the server endpoint in your MCP settings:

https://atom-mcp-server-production.up.railway.app/mcp

Any other MCP client

The server supports both HTTP SSE and stdio transport. Run it locally via npx or point at the Railway endpoint above.

The tools

The free tier includes 4 tools that give you macro market intelligence with no login required. MCP PRO ($49/mo) unlocks the remaining 4, which give you model-level and vendor-level detail.

Free tier

Tool What it does
list_vendors All 47 tracked vendors with type and region
get_kpis 6 live market KPIs updated weekly
get_index_benchmarks 14 AIPI price indexes by modality and tier
get_market_stats Aggregate supply and cost structure data

MCP PRO

Tool What it does
search_models Filter by context size, tool support, modality, price
get_model_detail Full spec and pricing for a specific model
compare_prices Cross-vendor comparison for a model family
get_vendor_catalog Full SKU list for a specific vendor

What it looks like in practice

Check what the market looks like right now (free)

get_kpis

This week’s numbers:

  • Output tokens cost 3.84x more than input tokens on average
  • Cached input saves 69.7% vs standard input pricing
  • Open source models run 80% cheaper than closed source equivalents
  • Only 20.3% of SKUs in the index offer cached pricing at all
  • The price gap between small and large models in the same family is 4.8x

These are median figures across all tracked SKUs, recalculated every Monday.

Find the cheapest model with 100K+ context and tool calling (PRO)

search_models
context_window_min: 100000
tool_calling: true
sort_by: input_price_asc

Returns model-level results with normalized per-token pricing across vendors. The spread between cheapest and most expensive for functionally similar models is typically over 30x. That difference compounds fast at any real usage volume.

Compare vendors for a specific model family (PRO)

compare_prices
model_family: "Llama 3.3 70B"

Returns every vendor offering that model with normalized pricing so you can make a direct comparison without doing any unit conversion yourself.

Why this is useful for agent architecture

If you’re building anything that makes a lot of LLM calls, model routing based on cost and capability is a real decision you’re making, consciously or not. The cheapest model that can handle a task should handle it.

With ATOM connected your agent can check current prices before picking a model, catch when a vendor changes pricing, estimate the cost of a planned workload before running it, and compare vendors for a specific capability requirement. That reasoning used to mean a spreadsheet someone had to maintain. Now it’s a tool call.

A note on the data

ATOM uses a chained matched-model methodology, the same logic you’d apply to a commodity price index. Every SKU is normalized to a common unit, timestamped, and verified. The point of the methodology is to eliminate composition bias so week-over-week comparisons are actually meaningful and not just reflecting which vendors got added or dropped.

Full methodology at a7om.com/methodology.

Try it

Run npx atom-mcp-server or search “ATOM” on Smithery. Free tier covers 4 tools with no login. MCP PRO is at a7om.com/mcp.

The inference market now has a benchmark. Might as well use it.

How We Built EdTech Apps Serving 500K+ Users

How We Built EdTech Apps Serving 500K+ Users

The global EdTech market crossed $340 billion in 2025, growing at 16.3% CAGR according to HolonIQ’s annual report. Yet 70% of education startups fail within two years — not because of bad ideas, but because of bad architecture decisions made in month one.

Our team at Geminate Solutions has shipped four EdTech platforms: Youth Pathshala (250K daily active users), VSmart Academy (200K+ students), Your CA Buddy (95% exam pass rate), and Academic Edutech (live virtual classrooms). This article shares what we learned building them — the architecture patterns, tech stack choices, and scaling strategies that actually worked.

EdTech App Architecture That Survives 250K Daily Users

Most education platforms start as simple CRUD apps. Videos go up, students watch, quizzes get submitted. This works until 10,000 concurrent users hit your system during a live exam.

Here is the architecture we use after four production EdTech builds:

Client Layer: Flutter App (iOS + Android + Web)
  - Offline Content Cache
  - Video Player (HLS)
  - Real-time Quiz Engine

API Gateway (Rate Limited)
  |
  +-- Content Service (Node.js)
  +-- Auth & Payment Service
  +-- Analytics Service (Events)
  |
  Media Pipeline: S3 -> CloudFront -> HLS (Adaptive Bitrate)

Three non-negotiable principles:

  1. Separate content delivery from content management. Your CMS writes to a database. Your CDN serves pre-processed video. These should never share infrastructure.
  2. Event-driven analytics from day one. When Youth Pathshala hit 100K users, we needed per-lesson completion rates, average watch time, and drop-off points. Retrofitting analytics into a monolith is painful.
  3. Offline-first on mobile. In India, where 60% of our users access content, network connectivity is unreliable. The app must function without internet.

Why Flutter Works for Cross-Platform Education Apps

We evaluated React Native, native iOS/Android, and Flutter across all four EdTech projects. Flutter won every time for education apps. Here is why:

Custom UI for learning interfaces. Education apps need progress rings, interactive quizzes, drag-and-drop exercises, animated explanations. Flutter’s widget composition model makes building these 3-4x faster than platform-native alternatives.

Single codebase, three platforms. Youth Pathshala ships to iOS, Android, and web from one Dart codebase. Our 4-person Flutter team maintains all three. A native approach would have needed 8-10 developers.

Offline content sync. Flutter’s sqflite + Hive combination handles local content caching well. Students download course modules on Wi-Fi and study offline. When connectivity returns, progress syncs automatically.

For the Your CA Buddy app, our Flutter developers built a custom PDF annotation layer and timed mock-exam engine that contributed to that 95% pass rate among active users.

Live Streaming Classrooms: WebRTC vs HLS

Academic Edutech needed live virtual classrooms with real-time interaction. This forced us to make a critical choice: WebRTC or HLS.

WebRTC gives sub-second latency. Teachers ask a question, students respond instantly. But WebRTC connections are peer-to-peer at heart, and scaling beyond 50 concurrent viewers per room requires a Selective Forwarding Unit (SFU).

HLS scales to millions through CDN but introduces 6-15 seconds of latency. Fine for pre-recorded content, terrible for live Q&A.

Our hybrid approach:

  • Teacher’s video stream goes via WebRTC to media server
  • Media server sends WebRTC to first 200 students (active participants)
  • Media server sends HLS transcode for overflow audience (passive viewers)
  • Chat and quiz responses travel via WebSocket (separate channel, always real-time)

This architecture handled Academic Edutech’s 500-student live classes with consistent quality. The WebSocket chat layer meant even HLS-stream students could ask questions with zero perceived delay.

Essential EdTech App Features: LMS Feature Checklist

After building four platforms, here are the features that actually drive retention — ranked by impact on our metrics:

Tier 1: Launch Features (MVP)

  • Structured course catalog with search and filters
  • Video lessons with adaptive bitrate streaming
  • Progress tracking per-lesson, per-module, per-course
  • Push notifications for new content and reminders
  • Payment gateway integration (Razorpay for India, Stripe for global)
  • Basic quiz engine with auto-grading

Tier 2: Retention Features (Month 2-3)

  • Offline downloads with DRM protection
  • Discussion forums per course/lesson
  • Certificates with verification URLs
  • Spaced repetition reminders based on Ebbinghaus curve
  • Parent/admin dashboard with progress reports

Tier 3: Scale Features (Month 4+)

  • Live classes with recording and replay
  • AI-powered doubt resolution (we use Claude API for this now)
  • Adaptive learning paths based on quiz performance
  • Multi-language content support
  • Instructor analytics

VSmart Academy’s retention jumped 34% after we added spaced repetition reminders in month three. Youth Pathshala’s DAU grew from 80K to 250K after offline downloads launched. Features should follow data, not assumptions.

EdTech App Development Cost: Realistic Breakdown

Every founder asks “how much does it cost to build an education app?” Here is an honest breakdown based on our four production builds:

MVP (3-4 months): $25,000 – $45,000

  • Flutter app (iOS + Android): 60% of budget
  • Node.js backend + PostgreSQL: 20% of budget
  • Video hosting + CDN setup: 10% of budget
  • UI/UX design: 10% of budget

Production-Ready (6-8 months): $60,000 – $120,000

  • Everything in MVP, plus live streaming, offline sync, admin dashboard, multi-role support, automated certificates

Enterprise Scale (12+ months): $150,000 – $300,000

  • Everything above, plus AI-powered adaptive learning, white-label capability, multi-tenant architecture

Where most founders waste money: Building custom video infrastructure. Use Mux or Cloudflare Stream. The encoding pipeline alone can consume $30,000+ in development time.

Where you should invest more: The content authoring tool for instructors. If teachers struggle to upload and organize content, your platform dies regardless of how good the student experience is.

Scaling an EdTech Platform From 10K to 250K Users

Youth Pathshala’s growth from 10,000 to 250,000 daily active users forced five architectural changes:

  1. Database read replicas (at 25K DAU) — Response times dropped from 800ms to 120ms
  2. Video CDN multi-region (at 50K DAU) — Load times went from 4 seconds to under 1 second
  3. Microservice extraction (at 80K DAU) — Deploy frequency went up, incidents went down
  4. Event queue for analytics (at 120K DAU) — Redis queue with batch processing
  5. Connection pooling and caching (at 200K DAU) — PgBouncer + Redis handled the 200K to 250K jump

The pattern: every 2-3x growth multiplier exposes the next bottleneck. Plan for the current order of magnitude plus one.

EdTech MVP: Ship in 90 Days

If you are building an EdTech product in 2026, here is the 90-day checklist we give every client:

Week 1-2: Flutter project setup, Node.js backend, PostgreSQL schema, CI/CD pipeline, video hosting account

Week 3-6: Authentication, course catalog, video player with HLS, progress tracking, payment integration

Week 7-10: Push notifications, quiz engine, discussion/comments, offline downloads, admin panel

Week 11-12: Load testing, security audit, App Store submission, monitoring setup

Week 13: Staged rollout (10% to 50% to 100%)

This timeline assumes a 4-5 person team. Solo developers should plan for 5-6 months.

The EdTech market rewards speed to market and content quality over technical sophistication. Ship the MVP, watch how students actually use it, then invest engineering effort where the data points.

Yash Korat is the CEO of Geminate Solutions, a custom software development company that has shipped 4 EdTech platforms serving 500K+ combined users. His team built Youth Pathshala (250K daily active users) and VSmart Academy (200K+ students).

Inside SQLite’s Frontend: Join Table Ordering

git-lrc

Hello, I’m Maneshwar. I’m building git-lrc, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. Star Us to help devs discover the project. Do give it a try and share your feedback for improving the product.

Even if your WHERE clause is perfectly optimized, a bad join order can still make your query slow.

How SQLite Executes Joins

SQLite uses a very simple but effective strategy for joins.

It always executes joins as nested loops.

That means for a query like:

SELECT * FROM A JOIN B;

SQLite will do something like:

for each row in A:
    for each row in B:
        process the combination

The important detail here is that the order of tables determines how these loops are nested.

  • The first table becomes the outer loop
  • The last table becomes the inner loop

So the order in your FROM clause directly affects execution.

Why Order Matters

Nested loops can be very expensive if the outer loop has too many rows.

For example, if table A has 1 million rows and table B has 1 million rows:

  • A outer → B inner = 1M × 1M operations
  • B outer → A inner = also large, but may differ depending on indexes

The goal is to reduce the number of iterations as early as possible.

SQLite Doesn’t Always Follow Your Order

Although the default behavior follows the FROM clause order, SQLite can reorder tables if it finds a better execution plan.

It uses a greedy algorithm to decide the order.

Instead of trying all possible combinations, it builds the order step by step:

  • First, pick the table that is cheapest to process
  • Then pick the next cheapest
  • Continue until all tables are placed

If two options are equally good, SQLite falls back to the original order in the query.

What “Cheapest” Means

SQLite estimates cost using several factors:

  • Availability of indexes
  • How selective those indexes are
  • Whether sorting can be avoided
  • Estimated number of rows to scan

For example:

  • An index that reduces 1 million rows to 1 row is extremely valuable
  • An index that reduces 1 million rows to 900,000 rows is not very useful

This is where the ANALYZE command becomes important. It collects statistics so SQLite can better estimate how effective an index is.

Index Nested Loop Joins

If a table has an index on the join column, SQLite often places it as the inner loop.

This allows efficient lookups for each row from the outer loop.

This strategy is called an index nested loop join.

Instead of scanning the entire inner table, SQLite uses the index to quickly find matching rows.

Special Cases: INNER vs OUTER Joins

Inner joins are flexible.

SQLite can freely reorder tables in inner joins because the result does not depend on order.

Outer joins are different.

A LEFT OUTER JOIN is not commutative, meaning order matters for correctness. Because of this, SQLite does not reorder outer joins.

However, tables involved in inner joins around an outer join may still be reordered if it improves performance.

WHERE Clause Integration

If you use ON or USING clauses in joins, SQLite internally converts them into additional WHERE clause conditions before optimization.

This means all the WHERE clause optimizations you saw earlier still apply here.

Forcing Join Order

In most cases, SQLite’s automatic reordering works well and you do not need to worry about it.

But if you want to force a specific order, you can use a CROSS JOIN.

Example:

SELECT * FROM table1 CROSS JOIN table2;

Here:

  • table1 will always be the outer loop
  • table2 will always be the inner loop

This gives you manual control when needed.

In the next part, we will look at index selection, where SQLite decides exactly which index to use when multiple options are available.

git-lrc
*AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.*

Any feedback or contributors are welcome! It’s online, source-available, and ready for anyone to use.

⭐ Star it on GitHub:

GitHub logo

HexmosTech
/
git-lrc

Free, Unlimited AI Code Reviews That Run on Commit

AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.

See It In Action

See git-lrc catch serious security issues such as leaked credentials, expensive cloud
operations, and sensitive material in log statements

git-lrc-intro-60s.mp4

Why

  • 🤖 AI agents silently break things. Code removed. Logic changed. Edge cases gone. You won’t notice until production.
  • 🔍 Catch it before it ships. AI-powered inline comments show you exactly what changed and what looks wrong.
  • 🔁 Build a…

The Firedancer Security Checklist: 7 DeFi Assumptions That Break in Solana’s Multi-Client Era — And the Defense Patterns to Fix Each One

Solana’s single-client era is over. Firedancer — Jump Crypto’s ground-up C/C++ validator rewrite — is live on mainnet, and by mid-2026, a meaningful share of stake is expected to run it alongside Agave (the original Rust client) and Jito-Solana.

This is the most significant architectural shift in Solana’s history. Client diversity is a net positive for network resilience (Ethereum’s post-Merge experience proves it), but it also invalidates assumptions that DeFi protocols have baked into their smart contracts since day one.

I’ve audited Solana programs that would silently break under multi-client conditions. Here are the seven assumptions that need to die — and the concrete defense patterns to replace them.

1. “400ms Slots Are Guaranteed”

The old assumption: Every slot takes ~400ms. Liquidation bots, oracle updates, and time-sensitive instructions can rely on this cadence.

Why it breaks: Firedancer leaders can produce blocks faster than Agave validators can verify them. When a high-performance Firedancer leader fills a block to capacity, slower validators may skip votes — stretching effective finality beyond 400ms for that slot.

The defense pattern:

// Anchor: Never assume wall-clock time from slot math
use anchor_lang::prelude::*;

#[account]
pub struct TimeGuard {
    pub last_oracle_slot: u64,
    pub last_oracle_unix: i64,
}

pub fn validate_oracle_freshness(
    guard: &TimeGuard,
    clock: &Clock,
    max_slot_drift: u64,   // e.g., 5 slots
    max_time_drift: i64,   // e.g., 10 seconds
) -> Result<()> {
    // Check BOTH slot distance AND unix timestamp
    let slot_drift = clock.slot.saturating_sub(guard.last_oracle_slot);
    let time_drift = clock.unix_timestamp.saturating_sub(guard.last_oracle_unix);

    require!(
        slot_drift <= max_slot_drift && time_drift <= max_time_drift,
        ErrorCode::StaleOracle
    );
    Ok(())
}

Key insight: Use Clock::unix_timestamp as the primary time reference and slot numbers only as a secondary sanity check. Never convert slots to seconds with a hardcoded multiplier.

2. “Transaction Ordering Is Deterministic Within a Block”

The old assumption: Transactions in a block execute in a predictable, sequential order.

Why it breaks: Firedancer’s transaction scheduler uses a fundamentally different algorithm than Agave’s. While both produce valid blocks, the ordering of non-conflicting transactions can differ between clients. This matters for protocols that implicitly depend on execution order — particularly those using priority fees as a sequencing mechanism.

The defense pattern:

// Make your program order-independent with explicit sequence numbers
#[account]
pub struct OrderedAction {
    pub sequence: u64,
    pub expected_state_hash: [u8; 32], // hash of expected pre-state
}

pub fn execute_ordered(
    ctx: Context<ExecuteOrdered>,
    action: OrderedAction,
) -> Result<()> {
    let state = &ctx.accounts.protocol_state;

    // Verify we're operating on the expected state
    let current_hash = hash_state(state);
    require!(
        action.expected_state_hash == current_hash,
        ErrorCode::StateMismatch
    );

    // Process with explicit ordering
    require!(
        action.sequence == state.next_sequence,
        ErrorCode::SequenceError
    );

    // ... execute logic ...
    Ok(())
}

Key insight: If your protocol’s correctness depends on transaction ordering, you have a latent bug. Make state transitions explicitly sequenced or provably order-independent.

3. “Compute Unit Costs Are Stable”

The old assumption: A given instruction costs roughly the same CUs across validator versions.

Why it breaks: Firedancer reimplements the BPF/SBF runtime. Identical programs can consume slightly different CU counts on Firedancer vs. Agave due to implementation differences in the virtual machine. Programs that calculate fees or refunds based on metered CU consumption may produce inconsistent results.

The defense pattern:

// Don't calculate fees from metered CUs
// Instead, use fixed fee schedules per operation type

pub const FEE_TABLE: &[(OperationType, u64)] = &[
    (OperationType::Swap, 5000),        // lamports
    (OperationType::Deposit, 3000),
    (OperationType::Withdraw, 4000),
    (OperationType::Liquidate, 8000),
];

pub fn calculate_fee(op: OperationType) -> u64 {
    FEE_TABLE.iter()
        .find(|(t, _)| *t == op)
        .map(|(_, fee)| *fee)
        .unwrap_or(5000) // safe default
}

Key insight: Treat CU metering as a resource limit, not a pricing mechanism. Fixed fee schedules are deterministic regardless of which client processes the transaction.

4. “My RPC Node’s View Is the Network’s View”

The old assumption: simulateTransaction on your RPC node gives you an accurate preview of what will happen on-chain.

Why it breaks: If your RPC runs Agave but the current leader runs Firedancer (or vice versa), simulation results can diverge from actual execution — particularly for edge cases in CPI depth handling, account data serialization, and error codes.

The defense pattern:

# Multi-client simulation: query both client types
import httpx
import asyncio

AGAVE_RPC = "https://your-agave-rpc.example.com"
FIREDANCER_RPC = "https://your-firedancer-rpc.example.com"

async def simulate_multi_client(tx_base64: str) -> dict:
    """Simulate on both clients and flag divergence."""
    async with httpx.AsyncClient() as client:
        payload = {
            "jsonrpc": "2.0", "id": 1,
            "method": "simulateTransaction",
            "params": [tx_base64, {"encoding": "base64"}]
        }

        agave, firedancer = await asyncio.gather(
            client.post(AGAVE_RPC, json=payload),
            client.post(FIREDANCER_RPC, json=payload),
        )

    a_result = agave.json()["result"]["value"]
    f_result = firedancer.json()["result"]["value"]

    diverged = (
        a_result.get("err") != f_result.get("err") or
        abs(a_result["unitsConsumed"] - f_result["unitsConsumed"]) > 1000
    )

    if diverged:
        print(f"WARNING: CLIENT DIVERGENCE: Agave={a_result['err']}, "
              f"Firedancer={f_result['err']}")

    return {
        "diverged": diverged,
        "agave": a_result,
        "firedancer": f_result,
    }

Key insight: For high-value transactions (liquidations, large swaps, governance votes), simulate against both client implementations before submitting. Treat divergence as a red flag.

5. “Finality = Optimistic Confirmation”

The old assumption: Once a transaction is “confirmed” (optimistic confirmation at ~5% of stake), it’s safe to act on.

Why it breaks: In a multi-client network, optimistic confirmation can be misleading if client diversity is unevenly distributed. If 60% of stake runs Agave and 40% runs Firedancer, a transaction confirmed by Agave validators alone hasn’t been validated by a truly independent implementation. A consensus bug in Agave could still cause a rollback.

The defense pattern:

import { Connection, Commitment } from '@solana/web3.js';

async function waitForRobustFinality(
  connection: Connection,
  signature: string,
  minConfirmations: number = 32,
): Promise<boolean> {
  // Step 1: Wait for "finalized" (supermajority, 2/3+ stake)
  const result = await connection.confirmTransaction(
    signature,
    'finalized' as Commitment,
  );

  if (result.value.err) {
    throw new Error(`Transaction failed: ${JSON.stringify(result.value.err)}`);
  }

  // Step 2: Verify the slot is rooted
  const status = await connection.getSignatureStatus(signature);
  if (!status.value?.confirmationStatus === 'finalized') {
    throw new Error('Transaction not yet rooted');
  }

  // Step 3: For bridges — add a slot buffer
  const txSlot = status.value.slot;
  const SAFETY_BUFFER = 10;

  while (true) {
    const currentSlot = await connection.getSlot('finalized');
    if (currentSlot >= txSlot + SAFETY_BUFFER) break;
    await new Promise(r => setTimeout(r, 400));
  }

  return true;
}

Key insight: Bridges and cross-chain protocols should require finalized commitment level plus a slot buffer. The cost of waiting 15-20 seconds is negligible compared to the cost of a consensus-bug-induced rollback.

6. “Error Codes Are Portable”

The old assumption: Custom program error codes (and their wrapping in TransactionError) behave identically across clients.

Why it breaks: Firedancer and Agave may surface different error metadata for the same underlying failure — different log formatting, different wrapping of CPI errors, and potentially different handling of instruction-level error propagation. Bot code that pattern-matches on error strings can break silently.

The defense pattern:

// Define error codes as explicit, documented constants
#[error_code]
pub enum ProtocolError {
    #[msg("Oracle price is stale")]
    StaleOracle = 6000,
    #[msg("Insufficient collateral")]
    Undercollateralized = 6001,
    #[msg("Rate limit exceeded")]
    RateLimited = 6002,
    #[msg("Sequence mismatch")]
    SequenceMismatch = 6003,
}
// Bot-side: parse error codes numerically
function parseProtocolError(err: any): number | null {
  const match = JSON.stringify(err).match(/"Custom":(\d+)/);
  return match ? parseInt(match[1]) : null;
}

Key insight: Numeric error codes from your program are portable. Everything else — log messages, error wrapping, instruction trace formatting — is client-implementation-specific.

7. “Account Data Layout Is Just My Concern”

The old assumption: As long as my program serializes/deserializes correctly, account data layout doesn’t matter for security.

Why it breaks: Firedancer’s account storage and snapshot system is independent from Agave’s. During network upgrades, validator restarts, or snapshot loading, subtle differences in how each client handles account data boundaries, padding, and rent-exempt minimums can surface.

The defense pattern:

// Always use discriminators — Anchor does this automatically
// For raw programs, implement it manually:

pub const ACCOUNT_DISCRIMINATOR: [u8; 8] = [0x01, 0x02, 0x03, 0x04,
                                              0x05, 0x06, 0x07, 0x08];

pub fn deserialize_checked(data: &[u8]) -> Result<MyState> {
    require!(data.len() >= 8 + std::mem::size_of::<MyState>(),
             ErrorCode::InvalidAccountData);

    require!(data[..8] == ACCOUNT_DISCRIMINATOR,
             ErrorCode::InvalidDiscriminator);

    let state = MyState::try_from_slice(&data[8..])?;

    require!(state.version <= CURRENT_VERSION,
             ErrorCode::UnsupportedVersion);

    Ok(state)
}

Key insight: Use Anchor’s built-in discriminator system. If you’re writing raw programs, implement 8-byte discriminators, version fields, and post-deserialization invariant checks.

The Multi-Client Security Audit Checklist

Before Firedancer adoption reaches critical mass, audit your programs against this checklist:

# Check Risk Level
1 No hardcoded slot-to-time conversions Critical
2 State transitions are order-independent or explicitly sequenced Critical
3 Fees/refunds don’t depend on metered CU consumption High
4 Bot infrastructure simulates against both clients High
5 Bridge/cross-chain ops use finalized + slot buffer Critical
6 Error handling uses numeric codes, not string matching High
7 All accounts use discriminators and version fields High
8 Oracle freshness uses unix timestamps, not slot math Critical
9 No implicit dependency on leader client implementation High
10 Integration tests run against both Agave and Firedancer test validators Medium

Setting Up Multi-Client Testing

Here’s the minimum viable CI configuration:

# .github/workflows/multi-client-test.yml
name: Multi-Client Security Tests
on: [push, pull_request]

jobs:
  test-agave:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: coral-xyz/setup-anchor@v3
        with:
          anchor-version: '0.30.1'
          solana-version: '1.18.26'
      - run: anchor test

  test-firedancer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: coral-xyz/setup-anchor@v3
        with:
          anchor-version: '0.30.1'
      - run: |
          curl -sSf https://install.firedancer.io | sh
          fdctl test-validator &
          sleep 5
          anchor test --skip-local-validator

  compare-results:
    needs: [test-agave, test-firedancer]
    runs-on: ubuntu-latest
    steps:
      - run: echo "Both clients passed"

The Bigger Picture: Lessons From Ethereum

Ethereum’s multi-client journey offers clear warnings:

  • The Prysm dominance problem (2021-2022): When one client ran >66% of validators, a bug in that client could have caused irreversible finality failures.
  • The consensus bug of October 2023: Besu and Nethermind briefly diverged on state root calculations, causing temporary chain splits.

Solana is entering this same phase. The difference is that Solana’s DeFi protocols are often more latency-sensitive than Ethereum’s, making multi-client divergence more dangerous for real-time operations like liquidations and oracle updates.

The silver lining: Client diversity makes the network dramatically more resilient to single-implementation bugs. Every outage Solana experienced before Firedancer was fundamentally a monoculture problem. The multi-client era fixes the existential risk — but only if DeFi protocols update their assumptions to match.

TL;DR

Firedancer is great for Solana. But it invalidates seven assumptions that most DeFi programs silently depend on:

  1. Slot timing — use unix timestamps, not slot math
  2. Transaction ordering — make logic order-independent
  3. CU costs — use fixed fee schedules
  4. RPC accuracy — simulate against both clients
  5. Finality — use finalized + buffer for high-value ops
  6. Error codes — match numerically, not by string
  7. Data layout — use discriminators and versioning

The protocols that audit for multi-client correctness now will survive the transition. The ones that don’t will discover these bugs the expensive way.

This is part of my DeFi Security Research series. I audit smart contracts and build security tooling. If your protocol hasn’t been audited for multi-client compatibility, now is the time.

How to Monetize Your MCP Server in 2026: The Complete Guide

There are over 20,000 MCP servers in the wild. Less than 5% make a single dollar. The rest are donated labor — talented developers building useful tools and getting nothing for it. This guide walks through adding per-call billing to any MCP server in under 5 minutes using SettleGrid, so your tools can start earning while you sleep.

The Opportunity: 95% of MCP Servers Earn Nothing

There are over 20,000 MCP servers listed on Glama, 19,000 on mcp.so, 12,500+ on the Official MCP Registry, and 6,000+ on Smithery. The MCP SDK is downloaded over 120 million times per month. Yet less than 5% of these servers generate any revenue at all. The total agent-to-tool payment volume globally is under $50K per day. This is not because developers do not want to earn money. It is because, until recently, adding billing to an MCP server required building payment infrastructure from scratch: Stripe integration, usage metering, invoice generation, webhook handling, and fraud detection.

SettleGrid eliminates that barrier. Two lines of code wrap your existing handler with per-call billing, usage metering, and Stripe payouts. No Stripe integration. No usage dashboards. No billing code. You focus on building great tools; SettleGrid handles the money.

Step 1: Install the SettleGrid SDK

Run npm install @settlegrid/mcp in your MCP server project. The SDK exports a withBilling wrapper that intercepts tool calls, meters usage, and settles payments automatically. It works with any MCP server implementation that follows the Model Context Protocol specification.

Alternatively, scaffold a new project with npx create-settlegrid-tool. The CLI creates a complete project with TypeScript, billing hooks, test harnesses, and deployment configs for Vercel, Railway, and Fly.io. You can pass flags like --category data or --pricing per-call to skip the prompts.

Verify your setup with npx settlegrid doctor. This checks your Node.js version, validates your tsconfig.json, and confirms the SDK can reach the SettleGrid API.

Step 2: Set Your Pricing

Configure pricing in your settlegrid.config.ts file. The simplest model is per-invocation: set a price in cents and every successful tool call charges that amount. For example, pricing: { model: "per-call", amount: 5 } charges 5 cents per call.

SettleGrid supports six pricing models: per-call, per-token, per-byte, per-second, tiered (different prices per method), and outcome-based (charge only on success). Start with per-call if you are unsure. It is the easiest to reason about and the easiest for consumers to understand.

Price based on value, not cost. A data enrichment tool that saves an agent 30 seconds of research is worth 10 to 25 cents, regardless of whether your compute cost is 0.1 cents. Most first-time tool builders underprice by 3 to 5 times.

Step 3: Wrap Your Handler

The core integration is two lines of code. Import settlegrid from @settlegrid/mcp, initialize it with your tool slug, and wrap your existing handler:

const sg = settlegrid.init({ toolSlug: "my-tool" })
const billed = sg.wrap(yourHandler, { costCents: 5 })

That is it. The wrap function intercepts every tool call, verifies the caller has sufficient credits, meters the usage, and settles the payment after your handler returns a successful response. Failed calls are not charged. Rate-limited calls return structured errors that agents can retry.

Your handler code does not change at all. The wrap function is transparent: it passes the same input to your handler and returns the same output to the caller. The only difference is that each call is now metered and billed.

Step 4: Deploy and Publish

Deploy your MCP server to any hosting provider. The SDK is runtime-agnostic and works in Node.js, Deno, and Bun. For serverless deployments, the SDK batches metering events and flushes them asynchronously to avoid adding latency.

Publish your tool to the SettleGrid marketplace by running npx settlegrid publish. Your listing appears in the explore page, category pages, search results, and the Discovery API that AI agents use to find tools. Listings go live within minutes.

Connect Stripe for payouts in the SettleGrid dashboard under Settings. SettleGrid uses Stripe Connect to pay tool publishers. Earnings transfer to your Stripe balance on a rolling 7-day schedule. You keep 95% of every transaction on the Free tier.

Step 5: Monitor and Grow

Use the SettleGrid dashboard to monitor usage, revenue, and consumer behavior in real time. Track calls per minute, p50/p95 latency, error rate, revenue by day, and top consumers. Set up alerts for anomalies.

Review your pricing after the first week. If your tool has high adoption but low revenue, your price is too low. If adoption is slow despite strong discovery metrics, your price may be too high or your listing needs improvement.

Add the SettleGrid badge to your GitHub README with npx settlegrid badge. Tools with README badges see 3x more discovery traffic. Register in MCP directories, write content about your tool, and leverage cross-promotion with complementary tools to accelerate growth.

Get Started

SettleGrid is free forever for most developers. The free tier includes 25,000 operations per month at 0% platform fee with 95% revenue share.

SettleGrid — the settlement layer for AI agent payments. Free tier, 95% revenue share, 2 lines of code.

Pull SEC EDGAR Filings into Your App with One API Call

If you’ve ever tried scraping SEC EDGAR for company filings, you know the pain: inconsistent HTML, rate limits, and parsing headaches. The SEC EDGAR Filings API wraps all of that complexity into a single REST endpoint so you can search 10-K, 10-Q, 8-K, and other filings by company name or CIK number.

What It Does

The API queries the SEC’s EDGAR database and returns structured filing data — filing type, date, description, and direct links to the documents. Whether you’re building a fintech dashboard, a compliance tool, or a research pipeline, this saves hours of scraping work.

Supported filing types include:

  • 10-K — Annual reports with full financial statements
  • 10-Q — Quarterly financial updates
  • 8-K — Current reports for material events (earnings, acquisitions, leadership changes)

Quick Start

A single GET request is all you need. Pass a company name or CIK as the company parameter:

const response = await fetch(
  'https://web-production-1fe0f.up.railway.app/api/sec-edgar-filings/search?company=Apple',
  {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    }
  }
);

const filings = await response.json();

filings.forEach(filing => {
  console.log(`${filing.type} | ${filing.date} | ${filing.description}`);
});

That’s it. No API key configuration for testing, no OAuth dance, no XML parsing.

Use Cases

  • Investment Research Platforms — Let users search filings directly from your app instead of navigating EDGAR manually.
  • Compliance Monitoring — Track when companies you follow submit new 8-K filings for material events.
  • Financial Data Pipelines — Feed filing metadata into your data warehouse for trend analysis across quarterly reports.
  • AI/LLM Applications — Retrieve filing documents to feed into summarization or extraction models.

Why Use an API Instead of Scraping?

EDGAR’s raw interface wasn’t designed for programmatic access. You’ll deal with rate limiting, inconsistent response formats, and CIK-to-company-name resolution. This API handles all of that and returns clean JSON.

Try It Out

The API is live on RapidAPI with a free tier to get started. Test endpoints directly in the browser, grab your API key, and integrate in minutes:

SEC EDGAR Filings on RapidAPI →

If you’re building anything in the fintech or compliance space, this is a solid shortcut. Give it a spin and let me know what you build with it.

We Spent Days Fighting a Zebra Card Printer. So You Don’t Have To.

If you’ve ever tried to programmatically control a Zebra ZC350 card printer, you already know the pain. And if you haven’t, let me save you some time: it’s considerable. I want to save you the pain, even if you are a competitor.

Feed a card. Encode an NFC chip. Print a badge. Eject it. Sounds simple. It wasn’t.

We built an open-source bridge called Dazzle because a client needed automated card issuance inside a larger workflow, and getting from “nice hardware” to “working software” was much harder than it should have been.

If you’re going to have zebra problems, you might as well answer them with Dazzle. Bonus points if you get the reference.

At one point we spent hours staring at APDU responses that all came back 6900, trying to figure out whether we were talking to the card, the reader, the SAM, or something undocumented in between.

So we open-sourced the thing we wish we’d had on day one.

What Dazzle Does

Dazzle is a .NET 8 helper that runs as a child process and speaks JSON over stdin/stdout.

Your app, whether it’s Node.js, Python, or anything else that can spawn a process and read lines, sends commands like:

  • feed
  • smartcard.connect
  • smartcard.transmit
  • eject

The helper talks to the Zebra printer and its internal smart-card encoder, then sends structured results back.

The goal is simple: keep the ugly vendor-specific parts out of your app code.

A typical flow looks like this:

const printer = new ZebraCardPrinter();
await printer.start();
await printer.connect("192.168.1.100");

await printer.feed();
const readers = await printer.getReaders();
await printer.smartcardConnect(readers.contactless);

const uid = await printer.smartcardTransmit("FFCA000000");
console.log("Card UID:", uid.response);

await printer.smartcardDisconnect();
await printer.eject();

The point is to make card issuance feel like normal application code.

Why We Built It

At PurpleOwl, we build custom business software for small and mid-sized companies. In this case, the project needed NFC-enabled ID card issuance as part of a larger operational workflow.

The Zebra ZC350 hardware is solid. The integration story was not.

What Made This Hard

Here are the problems that made us build Dazzle.

1. The SDK Isn’t Distributed Like a Normal Dependency

There is no clean npm install or dotnet add package path for the Zebra Card SDK.

You download a large installer, dig through C:Program FilesZebra Technologies for DLLs, manually copy them into your project, and add references one by one.

Every new machine gets the same ritual. No normal dependency management. No clean reproducible setup.

2. USB Discovery Was Surprisingly Fragile

Finding a USB-connected printer should have been a solved problem.

Instead, the relevant class could live in a separate assembly that was not always loaded, and the property holding the USB address changed names across SDK versions:

  • SymbolicName
  • UsbSymbolicName
  • Address
  • DeviceId

We ended up using reflection to locate the type, invoke the discovery method, and probe multiple property names until one worked.

3. Connections Could Fail Without Looking Failed

The TCP connection to the printer can drop while still leaving you with an object that appears usable.

So IsConnected might look fine right up until the next SDK call hangs, throws from somewhere deep in the stack, or returns nonsense.

Dazzle wraps operations with reconnection logic because trusting the reported connection state turned out to be a mistake.

4. “Card Present” Did Not Mean “Card Ready”

After feeding a card into the encoder station, there is a delay before the PC/SC reader fully recognizes it.

During that window, the card can show up as present but unpowered. Connect too early and you get RemovedCard even though the card is physically there.

So Dazzle waits until the card is both present and powered before trying to connect.

The Cindy0 Problem

This one cost us the most time.

We connected to the printer’s internal smart-card encoder over PC/SC, decoded the ATR’s historical bytes to ASCII, and got:

Cindy0

Every APDU came back 6900.

Read UID? 6900.

Authenticate? 6900.

Write? 6900.

We assumed we were talking to the wrong slot, maybe the SAM instead of the contactless card, so we tried configuration changes, different share modes, transparent RF commands, IOCTLs, and a lot of other dead ends.

The real answer was stranger: Cindy0 is the Elatec TWN4 reader’s virtual slot. It is not the card. It is a command channel that accepts the reader’s “Simple Protocol” wrapped in APDUs.

So standard PC/SC card commands were failing because we were not actually talking to a card.

The actual contactless card appears on a different logical slot, but only after you use that command channel to search for a tag first.

That detail exists in the TWN4 PC/SC docs. It is not surfaced clearly in the Zebra integration story.

Five hours disappeared because a virtual slot was named Cindy0.

What Ships In The Repo

Dazzle includes:

  • ZebraCardHelper, the .NET helper that manages printer and encoder communication, APDUs, tunneling, and reconnection logic
  • zebra-card-printer.js, a Node.js wrapper that handles process lifecycle, JSON messaging, request/response correlation, and timeouts
  • PAINPOINTS.md, a write-up of the gotchas, race conditions, and undocumented behaviors we ran into

If you are doing anything with Zebra card printers and NFC encoding, that pain-points document alone may save you a lot of time.

Why JSON Over stdio

We used JSON over standard input/output because it is simple and portable. Each request has an id, and each response echoes it back.

{ "id": 1, "cmd": "connect", "ip": "192.168.1.100" }
{ "id": 1, "ok": true, "data": { "connected": true } }

{ "id": 2, "cmd": "feed" }
{ "id": 2, "ok": true, "data": { "fed": true } }

{ "id": 3, "cmd": "smartcard.transmit", "apdu": "FFCA000000" }
{ "id": 3, "ok": true, "data": { "response": "04a23b1a9070809000" } }

That keeps the integration language-agnostic. Node.js, Python, Go, or anything else that can spawn a child process and read lines can use it.

Who This Is For

If you’re building a system that issues NFC-encoded cards, employee badges, access cards, membership cards, or anything similar on Zebra ZC350 hardware, this project is for you.

It is also for the developer who is already deep in the Zebra SDK and wondering whether the weirdness is their fault.

It probably is not.

Why We Open-Sourced It

We’re a five-person shop. We build custom ERP, CRM, and PSA systems. We built Dazzle because a client needed it, and we released it because it felt wasteful to let every future developer rediscover the same problems from scratch.

The hardware is solid. The integration layer does not need to be this punishing.

If Dazzle saves someone else from losing half a day to Cindy0, publishing it was worth it.

Links

If you try it and run into edge cases we haven’t covered yet, I’d genuinely love to hear what you found.