Integration Examples
Copy-pasteable SDK integration examples for Next.js, Express, Vercel Edge Functions, and Bun — including streaming and BYOK key forwarding.
Ready-to-use examples for common server environments. Each example is self-contained and copy-pasteable. All examples use the @askverdict/sdk package and assume you have set the ASKVERDICT_API_KEY environment variable.
Install the SDK first: npm install @askverdict/sdk (or pnpm add @askverdict/sdk / bun add @askverdict/sdk).
1. Next.js API Route
This example adds two API routes to a Next.js App Router project: one to create a verdict and one to stream its events through to the browser.
Route handler — create verdict
// app/api/verdicts/route.ts
import { NextRequest, NextResponse } from "next/server";
import { AskVerdictClient, AskVerdictError } from "@askverdict/sdk";
const client = new AskVerdictClient({
apiKey: process.env.ASKVERDICT_API_KEY!,
});
export async function POST(req: NextRequest) {
try {
const body = await req.json() as { question: string; mode?: string };
if (!body.question || typeof body.question !== "string") {
return NextResponse.json(
{ error: "question is required" },
{ status: 400 },
);
}
const result = await client.createVerdict({
question: body.question,
mode: (body.mode as "fast" | "balanced" | "thorough") ?? "balanced",
});
return NextResponse.json({ id: result.id, streamUrl: result.streamUrl });
} catch (error) {
if (error instanceof AskVerdictError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.status ?? 500 },
);
}
console.error("[POST /api/verdicts] Unexpected error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}Route handler — stream verdict events
Proxy the SSE stream to the browser so your API key is never exposed on the client.
// app/api/verdicts/[id]/stream/route.ts
import { NextRequest } from "next/server";
import { AskVerdictClient, AskVerdictError, type StreamEvent } from "@askverdict/sdk";
const client = new AskVerdictClient({
apiKey: process.env.ASKVERDICT_API_KEY!,
});
export async function GET(
_req: NextRequest,
{ params }: { params: { id: string } },
) {
const { id } = params;
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
function write(event: StreamEvent) {
const data = `event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`;
controller.enqueue(encoder.encode(data));
}
try {
for await (const event of client.streamVerdict(id)) {
write(event);
if (event.type === "stream:end" || event.type === "debate:complete") {
break;
}
}
} catch (error) {
if (error instanceof AskVerdictError) {
const errData = `event: error\ndata: ${JSON.stringify({ code: error.code, message: error.message })}\n\n`;
controller.enqueue(encoder.encode(errData));
}
} finally {
controller.close();
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}Client-side consumption
// components/VerdictStream.tsx
"use client";
import { useState } from "react";
export function VerdictStream() {
const [events, setEvents] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
async function run(question: string) {
setLoading(true);
setEvents([]);
// 1. Create the verdict
const res = await fetch("/api/verdicts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question, mode: "balanced" }),
});
const { id } = await res.json() as { id: string };
// 2. Stream events from your proxy route
const source = new EventSource(`/api/verdicts/${id}/stream`);
source.addEventListener("debate:complete", (e) => {
setEvents((prev) => [...prev, `DONE: ${e.data}`]);
source.close();
setLoading(false);
});
source.addEventListener("agent:argument", (e) => {
setEvents((prev) => [...prev, `ARG: ${e.data}`]);
});
source.onerror = () => {
source.close();
setLoading(false);
};
}
return (
<div>
<button onClick={() => run("Should we migrate to a monorepo?")} disabled={loading}>
{loading ? "Running…" : "Start Debate"}
</button>
<ul>
{events.map((e, i) => <li key={i}>{e}</li>)}
</ul>
</div>
);
}2. Express Middleware
Add AskVerdict to an existing Express application. This example exposes a /verdicts endpoint and a BYOK (bring your own key) pattern where the caller's provider key is forwarded per-request.
// src/verdicts-router.ts
import { Router, type Request, type Response, type NextFunction } from "express";
import { AskVerdictClient, AskVerdictError } from "@askverdict/sdk";
const router = Router();
// Shared client (uses your server API key for billing)
const defaultClient = new AskVerdictClient({
apiKey: process.env.ASKVERDICT_API_KEY!,
});
// ── POST /verdicts — create a verdict ─────────────────────────────────────────
router.post("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const { question, mode, context } = req.body as {
question: string;
mode?: string;
context?: string;
};
if (!question) {
return res.status(400).json({ error: "question is required" });
}
const result = await defaultClient.createVerdict({
question,
mode: (mode as "fast" | "balanced" | "thorough") ?? "fast",
context,
});
res.json({ id: result.id, streamUrl: result.streamUrl });
} catch (error) {
next(error);
}
});
// ── GET /verdicts/:id — fetch a verdict ───────────────────────────────────────
router.get("/:id", async (req: Request, res: Response, next: NextFunction) => {
try {
const { verdict } = await defaultClient.getVerdict(req.params.id!);
res.json(verdict);
} catch (error) {
next(error);
}
});
// ── GET /verdicts/:id/stream — proxy SSE ─────────────────────────────────────
router.get("/:id/stream", async (req: Request, res: Response) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders();
try {
for await (const event of defaultClient.streamVerdict(req.params.id!)) {
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`);
if (event.type === "stream:end" || event.type === "debate:complete") break;
}
} catch (error) {
res.write(`event: error\ndata: ${JSON.stringify({ message: "Stream failed" })}\n\n`);
} finally {
res.end();
}
});
// ── Error middleware ──────────────────────────────────────────────────────────
router.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
if (error instanceof AskVerdictError) {
return res.status(error.status ?? 500).json({
error: error.message,
code: error.code,
});
}
console.error("[verdicts-router] Unexpected error:", error);
res.status(500).json({ error: "Internal server error" });
});
export default router;BYOK key forwarding
When your users bring their own AI provider keys, forward them per-request. The key is never stored — it lives only in memory for the duration of the request.
// src/byok-middleware.ts
import { Router, type Request, type Response } from "express";
import { AskVerdictClient } from "@askverdict/sdk";
const byokRouter = Router();
byokRouter.post("/verdicts", async (req: Request, res: Response) => {
// User provides their own API key — forward it per-request
const userApiKey = req.headers["x-user-api-key"] as string | undefined;
if (!userApiKey) {
return res.status(400).json({ error: "x-user-api-key header is required for BYOK" });
}
// Create a request-scoped client using the user's key
const client = new AskVerdictClient({
apiKey: userApiKey, // Their key, not yours
});
const { question } = req.body as { question: string };
const result = await client.createVerdict({ question, mode: "balanced" });
res.json({ id: result.id });
});
export default byokRouter;3. Vercel Edge Function
Edge Functions run in Vercel's edge network with the Web APIs (no Node.js built-ins). The SDK uses native fetch and works without modification.
// app/api/edge-verdict/route.ts
import { NextRequest, NextResponse } from "next/server";
import { AskVerdictClient, AskVerdictError } from "@askverdict/sdk";
// Mark as Edge Runtime
export const runtime = "edge";
export async function POST(req: NextRequest) {
const client = new AskVerdictClient({
apiKey: process.env.ASKVERDICT_API_KEY!,
});
try {
const { question, mode } = await req.json() as {
question: string;
mode?: "fast" | "balanced" | "thorough";
};
// Create verdict
const result = await client.createVerdict({
question,
mode: mode ?? "fast",
});
return NextResponse.json({ id: result.id, streamUrl: result.streamUrl });
} catch (error) {
if (error instanceof AskVerdictError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.status ?? 500 },
);
}
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}Edge streaming with TransformStream
Stream debate events directly from the edge to the browser with zero intermediate buffering.
// app/api/edge-stream/[id]/route.ts
import { NextRequest } from "next/server";
import { AskVerdictClient, AskVerdictError, type StreamEvent } from "@askverdict/sdk";
export const runtime = "edge";
export async function GET(
_req: NextRequest,
{ params }: { params: { id: string } },
) {
const client = new AskVerdictClient({
apiKey: process.env.ASKVERDICT_API_KEY!,
});
const encoder = new TextEncoder();
const body = new ReadableStream({
async start(controller) {
function enqueue(event: StreamEvent) {
const line = `event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`;
controller.enqueue(encoder.encode(line));
}
try {
for await (const event of client.streamVerdict(params.id)) {
enqueue(event);
if (event.type === "stream:end" || event.type === "debate:complete") break;
}
} catch (err) {
if (err instanceof AskVerdictError) {
enqueue({
id: "",
type: "error",
data: { code: err.code, message: err.message },
timestamp: Date.now(),
});
}
} finally {
controller.close();
}
},
});
return new Response(body, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no", // Disable nginx buffering
},
});
}Set X-Accel-Buffering: no to prevent proxies and CDNs from buffering your SSE stream. Vercel automatically handles this for Edge Functions, but it is good practice when the response passes through nginx.
4. Bun Server
The SDK uses native fetch which Bun ships natively. No polyfills or extra configuration required.
// src/server.ts
import { AskVerdictClient, AskVerdictError } from "@askverdict/sdk";
const client = new AskVerdictClient({
apiKey: Bun.env.ASKVERDICT_API_KEY!,
});
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
// POST /verdicts — create
if (req.method === "POST" && url.pathname === "/verdicts") {
return handleCreate(req);
}
// GET /verdicts/:id — fetch
const getMatch = url.pathname.match(/^\/verdicts\/([^/]+)$/);
if (req.method === "GET" && getMatch) {
return handleGet(getMatch[1]!);
}
// GET /verdicts/:id/stream — SSE proxy
const streamMatch = url.pathname.match(/^\/verdicts\/([^/]+)\/stream$/);
if (req.method === "GET" && streamMatch) {
return handleStream(streamMatch[1]!);
}
return new Response("Not found", { status: 404 });
},
});
console.log(`Listening on http://localhost:${server.port}`);
// ── Handlers ──────────────────────────────────────────────────────────────────
async function handleCreate(req: Request): Promise<Response> {
try {
const { question, mode } = await req.json() as {
question: string;
mode?: "fast" | "balanced" | "thorough";
};
const result = await client.createVerdict({ question, mode: mode ?? "fast" });
return Response.json({ id: result.id, streamUrl: result.streamUrl });
} catch (error) {
return errorResponse(error);
}
}
async function handleGet(id: string): Promise<Response> {
try {
const { verdict } = await client.getVerdict(id);
return Response.json(verdict);
} catch (error) {
return errorResponse(error);
}
}
async function handleStream(id: string): Promise<Response> {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
try {
for await (const event of client.streamVerdict(id)) {
const line = `event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`;
controller.enqueue(encoder.encode(line));
if (event.type === "stream:end" || event.type === "debate:complete") break;
}
} catch {
controller.enqueue(encoder.encode("event: error\ndata: {}\n\n"));
} finally {
controller.close();
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
});
}
function errorResponse(error: unknown): Response {
if (error instanceof AskVerdictError) {
return Response.json(
{ error: error.message, code: error.code },
{ status: error.status ?? 500 },
);
}
console.error("Unexpected error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}Bun with BYOK key forwarding
// Extend the server to forward per-user provider keys
async function handleCreateBYOK(req: Request): Promise<Response> {
const userKey = req.headers.get("x-provider-key");
if (!userKey) {
return Response.json(
{ error: "x-provider-key header required for BYOK mode" },
{ status: 400 },
);
}
// Construct a per-request client — key lives only in memory
const byokClient = new AskVerdictClient({ apiKey: userKey });
try {
const { question } = await req.json() as { question: string };
const result = await byokClient.createVerdict({ question });
return Response.json({ id: result.id });
} catch (error) {
return errorResponse(error);
}
}Shared Utilities
These helpers are reusable across all environments.
Singleton client factory
// lib/sdk-client.ts
import { AskVerdictClient } from "@askverdict/sdk";
let _client: AskVerdictClient | null = null;
export function getClient(): AskVerdictClient {
if (!_client) {
const apiKey = process.env.ASKVERDICT_API_KEY;
if (!apiKey) {
throw new Error("ASKVERDICT_API_KEY environment variable is not set.");
}
_client = new AskVerdictClient({ apiKey });
}
return _client;
}Wait for completed verdict
// lib/wait-for-verdict.ts
import type { AskVerdictClient, DebateResponse } from "@askverdict/sdk";
export async function waitForVerdict(
client: AskVerdictClient,
id: string,
pollIntervalMs = 3_000,
timeoutMs = 300_000,
): Promise<DebateResponse> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const { verdict } = await client.getVerdict(id);
if (verdict.status === "completed") return verdict;
if (verdict.status === "failed") {
throw new Error(`Verdict ${id} failed`);
}
await new Promise((r) => setTimeout(r, pollIntervalMs));
}
throw new Error(`Verdict ${id} did not complete within ${timeoutMs}ms`);
}Was this page helpful?