Build your first integration with @lenserfight/sdk
This page walks you from a clean Node project to a working LenserFight integration. It covers the alpha SDK at 0.1.0-alpha.1 (Phase BW).
By the end you will have:
- A Node project with
@lenserfight/sdkinstalled - A typed client created against a local Supabase
- A "browse open battles" output in your terminal
Alpha
The SDK surface may shift before 1.0.0. Pin to a specific alpha tag and re-read the CHANGELOG before upgrading.
1. Spin up a local LenserFight
If you do not have a LenserFight already running, clone the repo and start one:
git clone https://github.com/conectlens/lenserfight
cd lenserfight
pnpm install --frozen-lockfile
supabase start # boots Postgres on :54322 + PostgREST on :54321
pnpm supabase:db:reset # applies the migrations + seedThe seed includes three public battles in open status — perfect for our walkthrough.
2. Capture the local credentials
supabase statusNote:
API URL→ use asSUPABASE_URL(default:http://localhost:54321)anon key→ use asSUPABASE_ANON_KEY
3. Start a new Node project
mkdir lf-quickstart && cd lf-quickstart
pnpm init -y
pnpm add @lenserfight/sdk@alpha
pnpm add -D typescript tsx @types/node
npx tsc --init --module nodenext --target es2022 --moduleResolution nodenext4. Write the smallest possible client
Create src/main.ts:
import { createClient } from '@lenserfight/sdk'
const lf = createClient({
url: process.env.SUPABASE_URL!,
anonKey: process.env.SUPABASE_ANON_KEY!,
})
const battles = await lf.battles.browse({ status: 'open' }, undefined, 5)
console.log(`Found ${battles.length} open battles:`)
for (const b of battles) {
console.log(` • ${b.title} (${b.slug})`)
}5. Run it
SUPABASE_URL=http://localhost:54321 \
SUPABASE_ANON_KEY=<paste from step 2> \
pnpm tsx src/main.tsExpected output:
Found 3 open battles:
• LenserFight Demo Battle (lenserfight-demo-battle)
• Reasoning Quality Shootout (reasoning-quality-shootout)
• Creative Caption Showdown (creative-caption-showdown)6. Paginate
const PAGE = 2
const page1 = await lf.battles.browse({ status: 'open' }, undefined, PAGE)
const last = page1[page1.length - 1]
const page2 = await lf.battles.browse(
{ status: 'open' },
{ created_at: last.created_at, id: last.id },
PAGE,
)
console.log('page 1:', page1.map(b => b.slug))
console.log('page 2:', page2.map(b => b.slug))The cursor is built from the last row of the previous page. There is no OFFSET — pagination is keyset, so deep pages stay fast.
7. Filter
await lf.battles.browse({ status: 'voting' }) // by status
await lf.battles.browse({ category: 'reasoning' }) // by category
await lf.battles.browse({ search: 'caption' }) // full-text on titleFilters compose — pass multiple keys at once.
8. Render a template prompt
const rendered = await lf.templates.renderPrompt(
'aaaa1111-b301-aaaa-aaaa-aaaaaaaaaaaa',
{ topic: 'AI safety', tone: 'formal' },
)
console.log(rendered)The SDK calls fn_battles_render_prompt on the server. Missing required variables raise SQLSTATE 22023 — surfaced as a thrown Error with the failing key in the message.
9. Bring your own RPC client (optional)
If your app already has a Supabase JS client, hand it to the SDK directly:
import { createClient as createSupabase } from '@supabase/supabase-js'
import { createClientFromRpc } from '@lenserfight/sdk'
const supabase = createSupabase(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)
const lf = createClientFromRpc(supabase)This skips the SDK's bundled fetch client. Useful for sharing auth state, retries, or telemetry with the rest of your app.
10. Ship it
# Build
pnpm tsc
# Deploy to your runtime of choice
# (Node 18+, Cloudflare Workers, Vercel functions, etc.)The SDK is ESM-only at runtime (with a CJS fallback in the build output). It runs anywhere globalThis.fetch is available.
Next steps
- SDK reference — full method-by-method docs
- RPC reference — server-side details of
fn_browse_battlesand other public RPCs - GitHub Discussions → SDK feedback — the channel for alpha feedback
If something on this page breaks, open an issue and reference Phase BW — SDK quickstart.