SMB owners often lack the technical skills to build complex financial models in spreadsheets or Python, and sharing sensitive data with unvetted code execution environments is risky.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
This reference recipe builds an AI-powered code sandbox that lets small business owners generate financial models by describing them in plain English. It uses xAI’s Grok model to generate code, then executes that code in an isolated E2B sandbox with strict safety policies, cost telemetry, session continuity, and spreadsheet export. By the end, you’ll have a full-stack Next.js API with three endpoints, nine services, and a complete test suite.
Prerequisites
Node.js >= 22 and pnpm installed (pnpm --version should return 10.x)
An xAI API key for Grok (XAI_API_KEY) — get one at the xAI console
An AWS account with DynamoDB access (or a local DynamoDB like DynamoDB Local) for session storage
Familiarity with TypeScript and Next.js App Router basics
Step 1: Scaffold the project and install dependencies
The scaffold agent has already created the Next.js 16 project with pnpm install and the correct config files. Your first action is to extend .env.example with the secrets your services will need, then verify all dependencies are installed.
Open .env.example and append entries for every environment variable the recipe requires:
env
# Env vars used by xai-grok-code-sandbox-for-small-business-financial-modeling.# The builder adds entries here as it wires up each integration.# Keep placeholders only — never commit real values.NODE_ENV=developmentXAI_API_KEY=<your-xai-api-key>E2B_API_KEY=<your-e2b-api-key>AWS_REGION=us-east-1AWS_ACCESS_KEY_ID=<your-access-key>AWS_SECRET_ACCESS_KEY=<your-secret>DYNAMODB_SESSION_TABLE_NAME=sessionsDAILY_COST_BUDGET=10.0XAI_MODEL=grok-3
Copy .env.example to .env and fill in your real values:
terminal
cp .env.example .env
Verify next.config.ts has the instrumentationHook flag set — this is required because the project uses src/instrumentation.ts for startup validation:
Expected output: Running pnpm typecheck should pass with no errors (the project may not compile yet if you haven’t created all source files — that’s fine for now).
Step 2: Create the shared types and config layer
Every service in this project shares common types and configuration. Create src/lib/types.ts first — it defines the core data contracts:
ts
import type { ConfidenceService } from "../services/confidence-service.js";import type { GrokClient } from "../services/grok-client.js";import type { CostTelemetryService } from "../services/cost-telemetry.js";import type { RepairService } from "../services/repair-service.js";import type { SandboxService } from "../services/sandbox-service.js";import type { FirewallService } from "../services/firewall-service.js";import type { SessionService } from "../services/session-service.js";import type { SpreadsheetService } from "../services/spreadsheet-service.js";export interface AnalysisRequest { prompt: string; sessionId?: string;}export interface AnalysisResult { code: string; output: string; costUsd: number; sessionId: string; spreadsheetBuffer?: Buffer; contentType: "text" | "spreadsheet";}export interface SandboxResult { stdout: string; stderr: string; exitCode: number;}export interface ServiceDeps { confidenceService: Pick<ConfidenceService, keyof ConfidenceService>; grokClient: Pick<GrokClient, keyof GrokClient>; costTelemetry: Pick<CostTelemetryService, keyof CostTelemetryService>; repairService: Pick<RepairService, keyof RepairService>; sandboxService: Pick<SandboxService, keyof SandboxService>; firewallService: Pick<FirewallService, keyof FirewallService>; sessionService: Pick<SessionService, keyof SessionService>; spreadsheetService: Pick<SpreadsheetService, keyof SpreadsheetService>;}
Next, create src/lib/config.ts — a strongly typed config loader that validates required secrets at startup:
Finally, create src/lib/token-counter.ts — a simple token counter that implements the TokenCounter interface from @reaatech/session-continuity. It approximates tokens as Math.ceil(content.length / 4):
ts
import type { TokenCounter, Message } from "@reaatech/session-continuity";export class SimpleTokenCounter implements TokenCounter { readonly model = "grok-3"; readonly tokenizer = "simple-char-count"; count(text: string): number { return Math.ceil(text.length / 4); } countMessages(messages: Message[]): number { let total = 0; for (const msg of messages) { if (typeof msg.content === "string") { total += this.count(msg.content); } else if (Array.isArray(msg.content)) { for (const block of msg.content) { if ("text" in block && typeof block.text === "string") { total += this.count(block.text); } } } total += 4; } return total; }}
Expected output:pnpm typecheck should now pass since all imports reference types that will be created next.
Step 3: Create the xAI Grok client
The GrokClient wraps the OpenAI SDK configured with xAI’s API base URL. It sends user prompts to Grok and returns the generated code plus token counts.
Expected output: The class uses new OpenAI({ baseURL: "https://api.x.ai/v1", apiKey }) to connect to xAI’s Grok endpoint. When generateCode() is called, it sends the prompt via the Chat Completions API and returns the model’s response text and token usage.
Step 4: Add cost telemetry
The CostTelemetryService wraps @reaatech/llm-cost-telemetry to record every LLM call and check daily budget.
Expected output:recordCall() uses generateId() for unique span IDs, calculateCostFromTokens(totalTokens, 0.15) with Grok’s approximate per-million-token rate of $0.15, and now() for timestamps. checkBudget() reads the daily limit from the loadConfig() budget (which reads env vars like DAILY_COST_BUDGET).
Step 5: Add structured repair for LLM output
LLMs don’t always return valid JSON. The RepairService uses @reaatech/structured-repair-core to fix malformed output — stripping markdown fences, fixing JSON syntax, coercing types, and matching fuzzy keys. Create src/services/repair-service.ts:
Expected output:repairCodeOutput() calls repair(schema, raw) which runs six graduated strategies — strip fences, extract JSON from prose, fix syntax, coerce types, fuzzy-match keys, and remove extra fields. If the LLM returns json\n{"code":"print(1)","language":"python"}\n, the repair engine strips the fences and returns clean { code: "print(1)", language: "python" }.
Step 6: Add the confidence router
The ConfidenceService wraps @reaatech/confidence-router to decide whether a user prompt is clear enough to route to code generation, needs clarification, or should fall back. Create src/services/confidence-service.ts:
Expected output: With routeThreshold: 0.8, a prediction with confidence 0.92 returns { type: "ROUTE", target: "code_generation" }. Predictions between 0.3 and 0.8 return "CLARIFY", and predictions below 0.3 return "FALLBACK".
Step 7: Create the E2B sandbox service
The SandboxService executes code in an isolated E2B sandbox. Create src/services/sandbox-service.ts:
ts
import Sandbox from "e2b";import type { SandboxResult } from "../lib/types.js";export class SandboxService { async executeCode(code: string): Promise<SandboxResult> { let sandbox; try { sandbox = await Sandbox.create(); } catch (err) { throw new Error(`Failed to create E2B sandbox: ${(err as Error).message}`); } const result = await sandbox.commands.run(code); return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, }; }}
Expected output:Sandbox.create() spins up a cloud sandbox using your E2B_API_KEY. sandbox.commands.run(code) executes the code string and returns { stdout, stderr, exitCode }. The sandbox is disposed automatically by E2B.
Step 8: Create the firewall service
The FirewallService wraps @reaatech/tool-use-firewall-core to scan generated code for dangerous patterns before execution. It blocks commands containing patterns like rm -rf, curl, /etc/, or ... Create src/services/firewall-service.ts:
ts
import { Logger, redact, PolicyViolationError, type Middleware, type InterceptorResponse, type RequestContext,} from "@reaatech/tool-use-firewall-core";const DANGEROUS_PATTERNS = ["rm -rf", "curl", "/etc/", ".."];export class FirewallService { private log: Logger; constructor() { this.log = new Logger("FirewallService"); } createSandboxPolicyMiddleware(): Middleware { return { execute(context: RequestContext) { const args = context.arguments; const code = typeof args?.code === 'string' ? args.code : typeof args?.command === 'string' ? args.command : ''; for (const pattern of DANGEROUS_PATTERNS) { if (code.includes(pattern)) { throw new PolicyViolationError({ message: "dangerous command blocked", }); } } return Promise.resolve({ action: "CONTINUE" as const, reason: "safe" }); }, }; } redactSensitiveData(obj: Record<string, unknown>): Record<string, unknown> { return redact(obj) as Record<string, unknown>; } buildPipeline( ...middlewares: Middleware[] ): (ctx: RequestContext) => Promise<InterceptorResponse> { return async (ctx: RequestContext) => { for (const middleware of middlewares) { const result = await middleware.execute(ctx); if (result.action === "BLOCK") { return { allowed: false, action: "BLOCK" as const, reason: result.reason }; } } return { allowed: true, action: "CONTINUE" as const }; }; }}
Expected output: The buildPipeline() method chains middlewares sequentially. If any middleware returns { action: "BLOCK" } or throws PolicyViolationError, the pipeline short-circuits and returns { allowed: false }. Safe code passes through and returns { allowed: true }.
Step 9: Create the session service with DynamoDB
The session service manages multi-turn conversations using @reaatech/session-continuity with a custom DynamoDB storage adapter. It stores sessions and messages in DynamoDB with optimistic concurrency, token budget enforcement, and sliding-window compression.
First, create the DynamoDBStorageAdapter and SessionService in src/services/session-service.ts:
ts
import { SessionManager, type IStorageAdapter, type Session, type Message, type SessionId, type MessageId, type HealthStatus, type UpdateSessionOptions, type SessionFilters, type MessageQueryOptions,} from "@reaatech/session-continuity";import { DynamoDBClient, PutItemCommand, GetItemCommand, UpdateItemCommand, DeleteItemCommand, QueryCommand, ScanCommand, BatchWriteItemCommand } from "@aws-sdk/client-dynamodb";import type { AttributeValue } from "@aws-sdk/client-dynamodb";import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";import { SimpleTokenCounter } from "../lib/token-counter.js";
Expected output: The adapter uses a DynamoDB single-table design with composite keys (pk=sessionId, sk=SESSION for sessions, pk=sessionId, sk=MSG#<id> for messages). The SessionManager enforces an 8192-token budget with 1024 reserved tokens; when exceeded, it applies sliding-window compression targeting 7000 tokens.
Step 10: Create the spreadsheet service
When the sandbox output contains tabular JSON data, the SpreadsheetService converts it to an xlsx buffer for download. Create src/services/spreadsheet-service.ts:
Expected output:generateXlsxBuffer([{ name: "Q3 Revenue", value: 50000 }]) produces a valid xlsx buffer. parseXlsxBuffer() reads it back, so the round-trip preserves the original data.
Step 11: Wire everything with the Orchestrator
The Orchestrator is the central coordinator. It accepts a ServiceDeps object and runs the full pipeline: load/create session → classify intent → call Grok → record cost → check budget → repair output → firewall scan → execute in sandbox → optionally generate spreadsheet → return result. Create src/services/orchestrator.ts:
ts
import type { AnalysisResult, ServiceDeps } from "../lib/types.js";import { logger } from "../lib/logger.js";import type { Message } from "@reaatech/session-continuity";export class Orchestrator { private deps: ServiceDeps; constructor(deps: ServiceDeps) { this.deps = deps; } async analyze(prompt: string, sessionId?: string): Promise<AnalysisResult> { try { let currentSessionId = sessionId;
Expected output: The orchestration pipeline is: session → confidence → LLM → cost → budget → repair → firewall → sandbox → spreadsheet. Every external call is wrapped in try/catch so a failure at any stage returns a partial result with error details instead of crashing.
Step 12: Create the API route handlers
The application exposes three endpoints: POST /api/analyze (the main analysis pipeline), POST /api/sessions (create session), GET /api/sessions (list sessions), and GET /api/sessions/[id] (single session lookup).
Create app/api/analyze/route.ts:
ts
import { NextRequest, NextResponse } from "next/server";import { Orchestrator } from "@/src/services/orchestrator.js";import { GrokClient } from "@/src/services/grok-client.js";import { ConfidenceService } from "@/src/services/confidence-service.js";import { CostTelemetryService } from "@/src/services/cost-telemetry.js";import { RepairService } from "@/src/services/repair-service.js";import { SandboxService } from "@/src/services/sandbox-service.js";import { FirewallService } from "@/src/services/firewall-service.js";import { SessionService } from "@/src/services/session-service.js";import { SpreadsheetService } from "@/src/services/spreadsheet-service.js";import { loadConfig } from "@/src/lib/config.js";import { DynamoDBClient } from "@aws-sdk/client-dynamodb";import OpenAI from "openai";import { SessionNotFoundError } from "@reaatech/session-continuity";export async function POST(req: NextRequest) { try { const body = await req.json() as { prompt?: string; sessionId?: string }; const prompt = body.prompt; if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) { return NextResponse.json({ error: "prompt is required and must be a non-empty string" }, { status: 400 }); } const sessionId = body.sessionId; const config = loadConfig(); const dynamoClient = new DynamoDBClient({ region: config.awsRegion, credentials: { accessKeyId: config.awsAccessKeyId, secretAccessKey: config.awsSecretAccessKey, }, }); const orchestrator = new Orchestrator({ confidenceService: new ConfidenceService(), grokClient: new GrokClient({ xaiApiKey: config.xaiApiKey, model: config.xaiModel }), costTelemetry: new CostTelemetryService(), repairService: new RepairService(), sandboxService: new SandboxService(), firewallService: new FirewallService(), sessionService: new SessionService({ tableName: config.dynamodbSessionTableName, dynamoClient }), spreadsheetService: new SpreadsheetService(), }); const result = await orchestrator.analyze(prompt, sessionId); return NextResponse.json(result); } catch (err) { if (err instanceof OpenAI.APIError) { return NextResponse.json({ error: `OpenAI API error: ${err.message}` }, { status: 502 }); } if (err instanceof SessionNotFoundError) { return NextResponse.json({ error: "Session not found" }, { status: 404 }); } return NextResponse.json({ error: `Internal error: ${(err as Error).message}` }, { status: 500 }); }}
Create app/api/sessions/route.ts:
ts
import { NextRequest, NextResponse } from "next/server";import { SessionService } from "@/src/services/session-service.js";import { DynamoDBClient } from "@aws-sdk/client-dynamodb";import { loadConfig } from "@/src/lib/config.js";function createSessionService(): SessionService { const config = loadConfig(); const dynamoClient = new DynamoDBClient({ region: config.awsRegion, credentials: { accessKeyId: config.awsAccessKeyId, secretAccessKey: config.awsSecretAccessKey, }, }); return new SessionService({ tableName: config.dynamodbSessionTableName, dynamoClient });}export async function POST(req: NextRequest) { try { const body = await req.json() as { userId?: string }; const userId = body.userId ?? "anonymous"; const service = createSessionService(); const session = await service.createSession(userId); return NextResponse.json(session, { status: 201 }); } catch (err) { return NextResponse.json({ error: (err as Error).message }, { status: 500 }); }}export async function GET(req: NextRequest) { try { const userId = req.nextUrl.searchParams.get("userId"); if (!userId) { return NextResponse.json({ error: "userId query parameter is required" }, { status: 400 }); } const service = createSessionService(); const sessions = await service.listSessions(userId); return NextResponse.json(sessions); } catch (err) { return NextResponse.json({ error: (err as Error).message }, { status: 500 }); }}
Create app/api/sessions/[id]/route.ts:
ts
import { NextRequest, NextResponse } from "next/server";import { SessionService } from "@/src/services/session-service.js";import { DynamoDBClient } from "@aws-sdk/client-dynamodb";import { loadConfig } from "@/src/lib/config.js";import { SessionNotFoundError } from "@reaatech/session-continuity";export async function GET( _req: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const { id } = await params; if (!id || id.trim().length === 0) { return NextResponse.json({ error: "Invalid session ID" }, { status: 400 }); } const config = loadConfig(); const dynamoClient = new DynamoDBClient({ region: config.awsRegion, credentials: { accessKeyId: config.awsAccessKeyId, secretAccessKey: config.awsSecretAccessKey, }, }); const service = new SessionService({ tableName: config.dynamodbSessionTableName, dynamoClient }); const session = await service.getSession(id); return NextResponse.json(session); } catch (err) { if (err instanceof SessionNotFoundError) { return NextResponse.json({ error: "Session not found" }, { status: 404 }); } return NextResponse.json({ error: (err as Error).message }, { status: 400 }); }}
Expected output:POST /api/analyze with {"prompt": "build a revenue model"} returns { code, output, costUsd, sessionId, contentType }. POST /api/sessions with {"userId": "alice"} returns 201 with the new session. GET /api/sessions/alice?userId=alice returns the list of sessions. GET /api/sessions/my-id returns the session object.
Step 13: Add instrumentation startup validation
The src/instrumentation.ts file runs at Next.js startup to validate config. It loads dotenv, checks required env vars are set, and logs the result:
Expected output: When you run pnpm dev, the terminal shows "Configuration validated successfully" via pino. If XAI_API_KEY is missing, it logs "Configuration validation failed" but doesn’t crash the dev server.
Step 14: Set up the barrel export
Create src/index.ts with barrel exports of the public API:
ts
export type { AnalysisRequest, AnalysisResult, SandboxResult, ServiceDeps } from "./lib/types.js";export { Orchestrator } from "./services/orchestrator.js";
Step 15: Run the full test suite
The project includes 17 test files plus a shared setup module covering every service, route handler, and config. Here’s the test setup (tests/setup.ts) that mocks external services:
Expected output:pnpm typecheck exits 0 (zero TypeScript errors). pnpm lint exits 0 (no ESLint violations). pnpm test runs vitest with coverage and reports numFailedTests=0 with at least 3 total tests and coverage >= 90% on all four metrics (lines, branches, functions, statements).
Once all three pass, run the preflight validator:
terminal
node /home/rick/solutions-worker/bin/preflight.js
Expected output:{"ok": true, ...} with no findings.
Next steps
Add a web UI — build a chat-like Next.js page with streaming responses and a spreadsheet download button. The POST /api/analyze endpoint already returns spreadsheetBuffer as a Buffer when the sandbox output is tabular data.
Implement real budget tracking — the current checkBudget() always returns { allowed: true }. Wire it to a real DynamoDB cost ledger or integrate with @reaatech/llm-cost-telemetry’s aggregation package to track daily spend across tenants.
Extend the firewall rules — the dangerous-patterns list is minimal. Add rules for file-system writes, network calls, or process spawning. Each rule is a new Middleware that can be composed via buildPipeline().
Add multi-model support — configure XAI_MODEL to switch between grok-3, grok-3-mini, or even an OpenAI model by changing the base URL and model name.
function
serializeDates
(value
:
unknown
)
:
unknown
{
if (value instanceof Date) return value.toISOString();
if (Array.isArray(value)) return value.map(serializeDates);
if (value !== null && typeof value === "object") {
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
result[k] = serializeDates(v);
}
return result;
}
return value;
}
export class DynamoDBStorageAdapter implements IStorageAdapter {