New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

fishnet-auth

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fishnet-auth

Reverse CAPTCHA authentication for AI agents. Prove you're a machine, not a human.

latest
npmnpm
Version
0.1.0-beta.4
Version published
Weekly downloads
8
33.33%
Maintainers
1
Weekly downloads
 
Created
Source

fishnet-auth

Reverse CAPTCHA authentication for AI agents. Prove you're a machine, not a human.

fishnet-auth is an open-source auth library that lets game servers and agent-facing APIs verify that incoming clients are real AI agents -- not humans, not dumb bots. Think NextAuth, but instead of proving "I'm human," your clients prove "I can think."

Drop it into your Next.js, Hono, or Express app. Bring your own database. You own everything.

Why

CAPTCHAs prove you're human. But what if your platform is for AI agents?

Agent-facing platforms -- games, simulations, competitions -- need the opposite: proof that a client has LLM-level reasoning capability. API keys alone don't prove intelligence. fishnet-auth does.

The idea: On every auth request, the server generates a set of micro-tasks that are trivial for an LLM but impractical for a human to solve within the time constraint. Tasks are generated procedurally, verified deterministically, and cost zero LLM tokens on your server.

How It Works

1. Agent reads a public seed from your server (rotates every 5 min)
2. Agent solves tasks derived from that seed (their LLM, their cost)
3. Agent submits answers in a single POST request
4. Server re-derives tasks from seed, verifies answers (~1ms, pure computation)
5. Server issues credentials via your database

One HTTP round trip. Stateless verification. No LLM cost on your server.

+------------------------------+
|         Your Game API        |
+------------------------------+
|      fishnet-auth (this lib)      |  <- you install this
|  challenge gen . verification |
|  seed rotation . middleware   |
+------------------------------+
|       Your DB Adapter        |  <- you configure this
|  (Supabase / Redis / Drizzle)|
+------------------------------+

Quick Start

Install

npm install fishnet-auth

1. Add the auth route

// app/api/agent-auth/[[...fishnet-auth]]/route.ts

import { fishnetAuth } from 'fishnet-auth';
import { SupabaseAdapter } from 'fishnet-auth/adapters/supabase';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
);

export const { GET, POST } = fishnetAuth({
  secret: process.env.FISHNET_AUTH_SECRET!,
  adapter: SupabaseAdapter(supabase),
});

Note: The route uses an optional catch-all [[...fishnet-auth]] (double brackets), not a required catch-all [...fishnet-auth]. This ensures POST /api/agent-auth (with no sub-path) resolves correctly.

2. Protect your routes

// app/api/game/route.ts

import { getAgent } from 'fishnet-auth';

export async function POST(req: Request) {
  const agent = await getAgent(req);

  if (!agent) {
    return Response.json({ error: 'unauthorized' }, { status: 401 });
  }

  // agent.id, agent.name, agent.createdAt
}

Alternatively, use createGetAgent to avoid reliance on global state:

import { createGetAgent } from 'fishnet-auth';
import { SupabaseAdapter } from 'fishnet-auth/adapters/supabase';

const getAgent = createGetAgent({
  secret: process.env.FISHNET_AUTH_SECRET!,
  adapter: SupabaseAdapter(supabase),
});

export async function POST(req: Request) {
  const agent = await getAgent(req);
  if (!agent) return Response.json({ error: 'unauthorized' }, { status: 401 });
}

That's it. Two files and your API is agent-authenticated.

Agent-Side SDK

Agents authenticate using the client SDK. They bring their own LLM as the solver:

import { AgentAuthClient } from 'fishnet-auth/client';

const auth = new AgentAuthClient({
  serverUrl: 'https://your-game.com',
  name: 'MyPokerBot',
  solver: async (tasks) => {
    // Agent uses their own LLM to solve the challenge tasks
    const response = await myLLM.complete(formatTasks(tasks));
    return parseAnswers(response);
  },
});

const { apiKey } = await auth.authenticate();

// Use the key for all subsequent requests
fetch('https://your-game.com/api/game/action', {
  headers: { Authorization: `Bearer ${apiKey}` },
});

Agents can also implement the protocol manually -- it's just two HTTP calls:

# 1. Get the current seed and your tasks
curl "https://your-game.com/.well-known/fishnet-auth?name=MyBot"

# 2. Solve and submit
curl -X POST https://your-game.com/api/agent-auth \
  -H "Content-Type: application/json" \
  -d '{"name":"MyBot","seed":"a8f2c9","answers":["xK9mQ2nL","apple",...]}'

Client Configuration

OptionTypeDefaultDescription
serverUrlstringrequiredThe game server base URL
namestringrequiredAgent name
solverfunctionrequiredSolver function (your LLM)
discoveryPathstring/.well-known/fishnet-authCustom discovery path
authPathstringdiscovered from serverCustom auth path
maxRetriesnumber2Max retries on failure
retryDelaynumber1000Retry delay in ms (exponential backoff)

Protocol Spec

Discovery

GET /.well-known/fishnet-auth?name=MyBot
{
  "version": "0.1",
  "seed": "a8f2c9",
  "seedExpiresAt": "2026-02-07T12:05:00Z",
  "taskTypes": ["reverse", "nthWord", "jsonBuild", "arraySort", "caesarShift", "pattern", "transform", "firstLetters", "wordLengths", "filterWords", "interleave"],
  "taskCount": 5,
  "minCorrect": 5,
  "authEndpoint": "/api/agent-auth",
  "tasks": [
    { "type": "reverse", "instruction": "Reverse this string exactly.", "input": "xK9mQ2nL" },
    { "type": "nthWord", "instruction": "What is word number 7 in this list? (1-indexed)", "input": ["quantum", "nebula", "prism", "velvet", "cascade", "ember", "fractal", "horizon", "lattice"] }
  ]
}

The minCorrect field tells the agent how many tasks it must answer correctly to pass. This allows operators to configure a pass threshold (e.g., 4 out of 5 must be correct) rather than requiring perfect scores.

Authenticate

POST /api/agent-auth
{
  "name": "MyPokerBot",
  "seed": "a8f2c9",
  "answers": ["LnQ2m9Kx", "cascade", ...]
}

Success (200):

{
  "agentId": "ag_k8x2m9f1",
  "apiKey": "ag_a8Kx92mN...",
  "expiresAt": "2026-03-07T12:00:00Z"
}

Failure (401):

{
  "error": "challenge_failed",
  "message": "Challenge verification failed. Please try again."
}

Seed Rotation

Seeds rotate on a configurable interval (default: 5 minutes). The server accepts both the current and previous seed to handle requests in flight during rotation. Each (seed, agentName) pair produces a unique task set, preventing answer sharing between agents.

Task Types

Tasks are designed to be trivial for LLMs, impractical for humans at volume, and deterministically verifiable with zero LLM cost. Each task has a difficulty tier that controls which task pool is available at each difficulty level.

Difficulty Tiers

PresetIncluded TiersDescription
easyeasyOnly the simplest tasks. Best for smaller models.
standardeasy + standardBalanced mix. The default.
hardeasy + standard + hardAll tasks, including multi-step and complex operations.

Task Reference

Task TypeDifficultyExampleWhy It Works
reverseeasyReverse the string xK9mQ2nLFast for LLMs, tedious at speed for humans
arraySorteasySort this array and return comma-separatedMechanical but multi-step
patterneasyComplete this repeating sequencePattern recognition in bulk
firstLetterseasyConcatenate first letter of each wordExtraction across a list
interleaveeasyInterleave characters from two stringsParallel traversal
nthWordstandardWhat is word 7 of this 10-word list?Precise indexing under time pressure
jsonBuildstandardBuild JSON from these words + rulesStructural reasoning
caesarShiftstandardShift each letter by N positions in the alphabetAlgorithmic, wrapping cipher
transformstandardApply these 3 transforms to this stringMulti-step string manipulation
wordLengthsstandardReturn character count of each wordBulk measurement
filterWordsstandardReturn words with more than 5 characters, sortedFilter + sort reasoning

New task types can be added over time. The server declares supported types in the discovery endpoint. The diversity of task types is itself a defense -- writing a non-LLM solver for all types is more effort than just using an LLM.

Configuration

fishnetAuth({
  // Required
  secret: string,            // HMAC secret for seed generation
  adapter: FishnetAuthAdapter,   // Your database adapter

  // Optional
  credentials: {
    type: 'api-key' | 'jwt',   // Default: 'api-key'
    prefix: string,              // e.g. 'clawpoker_' -- default: 'ag_'
    expiresIn: string,           // e.g. '30d' -- default: '7d'
    jwtSecret: string,           // Required if type is 'jwt'
  },

  difficulty: 'easy' | 'standard' | 'hard', // Default: 'standard'
  taskCount: number,                         // Default: 5
  minCorrect: number,                        // Default: taskCount (all must be correct)
  seedRotationSeconds: number,               // Default: 300 (5 min)
  taskTypes: string[],                       // Default: all for selected difficulty

  // Callbacks
  onVerified: (agent: { name: string; id: string }) => Promise<void>,
  onFailed: (info: { name: string; reason: string }) => Promise<void>,
});

Configuration Details

OptionTypeDefaultDescription
secretstringrequiredHMAC secret for deterministic seed generation
adapterFishnetAuthAdapterrequiredDatabase adapter for storing agents and auth state
credentials.type'api-key' | 'jwt''api-key'Credential format issued on success
credentials.prefixstring'ag_'Prefix for generated API keys
credentials.expiresInstring'7d'Duration string (e.g. '30d', '12h', '3600s')
credentials.jwtSecretstring--Required when credentials.type is 'jwt'
difficulty'easy' | 'standard' | 'hard''standard'Controls which task tiers are included
taskCountnumber5Number of tasks per challenge
minCorrectnumbertaskCountMinimum correct answers to pass (must satisfy 1 <= minCorrect <= taskCount)
seedRotationSecondsnumber300Seed rotation interval in seconds
taskTypesstring[]all for difficultyRestrict to specific task types
onVerifiedfunction--Callback after successful verification
onFailedfunction--Callback after failed verification

Pass Threshold (minCorrect)

By default, an agent must answer all tasks correctly to pass. The minCorrect option lets you relax this:

fishnetAuth({
  secret: process.env.FISHNET_AUTH_SECRET!,
  adapter: MemoryAdapter(),
  taskCount: 5,
  minCorrect: 4, // 4 out of 5 is enough
});

This is useful when targeting models that occasionally stumble on edge cases but can reliably solve most tasks. The minCorrect value is exposed in the discovery endpoint so agents know the threshold upfront.

Database Adapters

fishnet-auth doesn't own your data. You bring your own database via an adapter.

Supabase

import { SupabaseAdapter } from 'fishnet-auth/adapters/supabase';

SupabaseAdapter(supabaseClient);

Redis

import { RedisAdapter } from 'fishnet-auth/adapters/redis';

RedisAdapter(redisClient);

Drizzle (Postgres, MySQL, SQLite)

import { DrizzleAdapter } from 'fishnet-auth/adapters/drizzle';

DrizzleAdapter({ db, agentsTable, authLogTable, eq, and });

Memory (Development)

import { MemoryAdapter } from 'fishnet-auth/adapters/memory';

MemoryAdapter();
// In-memory store, resets on restart -- for dev/testing only

Build Your Own

Implement five methods:

interface FishnetAuthAdapter {
  createAgent(agent: AgentRecord): Promise<void>;
  getAgentByKey(apiKey: string): Promise<AgentRecord | null>;
  revokeAgent(id: string): Promise<void>;
  hasCompletedAuth(seed: string, name: string): Promise<boolean>;
  markAuthCompleted(seed: string, name: string): Promise<void>;
}

Framework Support

Next.js (App Router)

// app/api/agent-auth/[[...fishnet-auth]]/route.ts
import { fishnetAuth } from 'fishnet-auth';
export const { GET, POST } = fishnetAuth({ ... });

The fishnetAuth() call returns { GET, POST, engine }. The engine is a FishnetAuthEngine instance you can use directly if needed.

To validate agent tokens in your protected routes:

import { getAgent } from 'fishnet-auth';

export async function POST(req: Request) {
  const agent = await getAgent(req);
  if (!agent) return Response.json({ error: 'unauthorized' }, { status: 401 });
  // agent.id, agent.name, agent.apiKey, agent.createdAt, agent.expiresAt
}

If you prefer to avoid module-level global state, use the createGetAgent factory:

import { createGetAgent } from 'fishnet-auth';

const getAgent = createGetAgent({
  secret: process.env.FISHNET_AUTH_SECRET!,
  adapter: SupabaseAdapter(supabase),
});

Hono

import { fishnetAuth } from 'fishnet-auth/hono';
app.route('/api/agent-auth', fishnetAuth({ ... }));

Express

import { fishnetAuthRouter } from 'fishnet-auth/express';
app.use('/api/agent-auth', fishnetAuthRouter({ ... }));

Generic (Web Standard Request/Response)

import { createGenericHandler } from 'fishnet-auth/generic';
const { handleDiscovery, handleAuth, handleProtect, engine } = createGenericHandler(config);
// Works with Deno, Bun, Cloudflare Workers, Vercel Edge

Core API

For advanced use cases, you can use the core engine and utility functions directly:

import {
  FishnetAuthEngine,
  generateChallenge,
  verifyAnswers,
  generateSeed,
  isValidSeed,
  createSeededRng,
  getAllTaskTypes,
  getTaskDefinition,
  getTaskDefinitions,
} from 'fishnet-auth/core';

FishnetAuthEngine

The engine class encapsulates the full auth lifecycle:

const engine = new FishnetAuthEngine(config);

// Get discovery response (includes tasks if agent name provided)
const discovery = engine.getDiscovery('MyBot');

// Authenticate an agent
const result = await engine.authenticate({ name, seed, answers });

// Validate a bearer token
const agent = await engine.validateToken(apiKey);

// Extract bearer token from Authorization header
const token = engine.extractToken(request.headers.get('Authorization'));

Pure Functions

Challenge generation and verification are pure, deterministic functions with no side effects:

// Generate tasks for an agent (deterministic given same inputs)
const tasks = generateChallenge(secret, seed, agentName, taskTypes, taskCount, difficulty);

// Verify submitted answers
const result = verifyAnswers(secret, seed, agentName, answers, taskTypes, taskCount, minCorrect, difficulty);
// result: { valid: boolean, expected: number, correct: number, minCorrect: number }

Task Registry

// Get all task type names for a difficulty level
const types = getAllTaskTypes('standard'); // ['reverse', 'nthWord', 'jsonBuild', ...]

// Get a single task definition
const def = getTaskDefinition('caesarShift');

// Get filtered task definitions
const defs = getTaskDefinitions(['reverse', 'caesarShift'], 'standard');

Performance

fishnet-auth is designed for high-throughput, low-latency environments.

MetricValue
Auth overhead per agent~1-2s (mostly agent-side LLM time)
Server-side verification~0.014ms per challenge
LLM cost to your serverZero
300 concurrent agents~6.7ms total verification time
State stored during authNone (seed-based, stateless)

Seed verification is a pure function -- no database reads, no LLM calls, no external services. The only I/O is the database write after successful verification to store the new agent credential.

Security

Replay prevention: Each (seed, agentName) pair generates unique tasks. Seeds rotate every 5 minutes. Completed (seed, name) pairs are tracked to prevent reuse.

Answer sharing: Agents with different names get different tasks from the same seed, so publishing answers doesn't help other agents.

Non-LLM solvers: The diversity of task types (11 and growing) makes writing a comprehensive non-LLM solver impractical.

Seed predictability: Seeds are HMAC-derived from a server secret and time window. Without the secret, future seeds cannot be predicted.

API key security: API keys are hashed using SHA-256 before storage. Only the hashed values are stored in your database for enhanced security.

Rate limiting: fishnet-auth focuses on authentication logic and does not include built-in rate limiting. For production deployments, implement rate limiting at the infrastructure level using:

  • Reverse proxy (nginx, Apache)
  • CDN/Edge services (Cloudflare, CloudFront)
  • API Gateway (AWS API Gateway, Azure API Management)
  • Application middleware (express-rate-limit, fastify-rate-limit)
  • Network-level protection (WAF, DDoS protection)

This approach provides better performance and more flexible rate limiting policies than library-level implementations.

Exports

fishnet-auth provides multiple entry points for different use cases:

Import PathContents
fishnet-authCore engine, types, Next.js adapter (fishnetAuth, getAgent, createGetAgent)
fishnet-auth/coreCore engine, types, seed utilities, task registry
fishnet-auth/clientAgentAuthClient for agent-side authentication
fishnet-auth/nextjsfishnetAuth, getAgent, createGetAgent
fishnet-auth/honoHono framework adapter
fishnet-auth/expressExpress framework adapter
fishnet-auth/genericWeb Standard Request/Response handler
fishnet-auth/adapters/supabaseSupabase adapter
fishnet-auth/adapters/redisRedis adapter
fishnet-auth/adapters/drizzleDrizzle ORM adapter
fishnet-auth/adapters/memoryIn-memory adapter (dev/testing)

Roadmap

  • Core: seed generation, task engine, verification
  • Adapters: Supabase, Redis, Drizzle, Memory
  • Frameworks: Next.js, Hono, Express, Generic
  • Client SDK
  • Difficulty tiers (easy, standard, hard)
  • Configurable pass threshold (minCorrect)
  • Agent reputation / persistent identity across sessions
  • Portable identity across platforms
  • Rate limiting middleware
  • Dashboard / analytics hooks

Philosophy

  • You own your data. fishnet-auth is a library, not a service. No hosted backend, no vendor lock-in.
  • Zero server-side LLM cost. Challenge generation and verification are pure computation.
  • One round trip. Auth completes in a single POST request.
  • Framework agnostic. Core logic is pure functions. Framework adapters are thin wrappers.
  • Open protocol. The challenge-response spec is public. Anyone can implement a compatible server or client.

Contributing

This is early. The protocol, task types, and adapter interface are all open for discussion. If you're building agent-facing infrastructure, we'd love your input.

License

MIT

Keywords

agent

FAQs

Package last updated on 08 Feb 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts