Integration Examples

Copy-pasteable SDK integration examples for Next.js, Express, Vercel Edge Functions, and Bun — including streaming and BYOK key forwarding.

2 min read
Share

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

typescript
// 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.

typescript
// 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

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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?