@gonzih/meatbag-api
v1.0.0
Published
Human checkpoint service — let AI agents outsource CAPTCHA solving to humans in real time
Maintainers
Readme
meatbag-api
Human checkpoint service — let AI agents outsource CAPTCHA solving to humans in real time.
AI agents hit CAPTCHAs. Vision models are expensive and unreliable against adversarial challenges. meatbag-api lets agents submit a challenge screenshot and get a human answer back in seconds via WebSocket.
Quick start
# Start Redis (required)
docker run -p 6379:6379 redis
# Start the server + solver UI
npx @gonzih/meatbag-api
# Open the solver UI in your browser
open http://localhost:3000Environment variables
| Variable | Default | Description |
|-------------|----------------------------|--------------------------|
| PORT | 3000 | HTTP server port |
| REDIS_URL | redis://localhost:6379 | Redis connection URL |
Agent SDK
import { MeatbagClient } from '@gonzih/meatbag-api';
const client = new MeatbagClient({ url: 'http://localhost:3000' });
// Resolves when a human selects tiles and clicks Submit
const tiles = await client.solve(screenshotBase64);
console.log('Human selected tiles:', tiles); // [0, 3, 6]MeatbagClient options
new MeatbagClient({
url: string; // Server URL (required)
timeout?: number; // Default solve timeout in ms (default: 300000 / 5 min)
})client.solve(screenshotBase64, options?)
Submits a CAPTCHA screenshot and waits for a human solver to respond.
| Parameter | Type | Default | Description |
|-------------------------|----------|------------------|-------------------------------------------------|
| screenshotBase64 | string | required | Base64-encoded PNG/JPEG screenshot |
| options.gridSize | string | '3x3' | Grid dimensions hint ('3x3' or '4x4') |
| options.challengeType | string | 'image_select' | Challenge type label (informational) |
| options.timeout | number | client default | Per-call timeout override in ms |
Returns Promise<number[]> — array of 0-based tile indices selected by the human.
client.getStatus(jobId)
Poll for job status without WebSocket.
const status = await client.getStatus(jobId);
// { jobId, status: 'pending'|'solved'|'expired', answer?, solvedAt?, createdAt }client.cancel(jobId)
Cancel a pending job.
await client.cancel(jobId);REST API
POST /challenge
Submit a new CAPTCHA job.
Request body:
{
"screenshot": "<base64-encoded image>",
"gridSize": "3x3",
"challengeType": "image_select"
}Response 201:
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"wsUrl": "ws://localhost:3000"
}GET /challenge/:jobId
Poll for job status.
Response 200:
{
"jobId": "...",
"status": "solved",
"answer": [0, 3, 6],
"solvedAt": "2024-01-01T12:00:00.000Z",
"createdAt": "2024-01-01T11:59:58.000Z"
}DELETE /challenge/:jobId
Cancel a pending job. Returns 204 No Content.
GET /health
Health check + queue depth.
{ "status": "ok", "queueDepth": 2 }WebSocket (Socket.io)
Agent-side events
Connect to the server URL and emit join_job after submitting a challenge:
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000');
socket.on('connect', () => socket.emit('join_job', { jobId }));
socket.on('solved', ({ jobId, tiles, solvedAt }) => {
console.log('Answer:', tiles); // [0, 3, 6]
});Human solver UI events
The browser UI handles these automatically. For custom integrations:
| Event (client → server) | Payload | Description |
|-------------------------|----------------------------|------------------------------------|
| join_solvers | — | Register as a solver |
| request_job | — | Request next pending job |
| solve | {jobId, tiles: number[]} | Submit tile selections |
| Event (server → client) | Payload | Description |
|-------------------------|--------------------------------------|-------------------------------|
| job_data | {jobId, screenshot, gridSize, ...} | Response to request_job |
| new_job | {jobId, screenshot, gridSize, ...} | Broadcast when new job posted |
| solved | {jobId, tiles, solvedAt} | Job solution (to agents) |
| no_jobs | — | No pending jobs available |
| queue_update | {depth: number} | Queue depth change |
| stats_update | {solved, avgTime} | Per-solver stats |
Job lifecycle
POST /challenge → pending → (human submits) → solved
(5 min TTL) → expired
DELETE → deletedJobs expire after 5 minutes if unsolved (Redis TTL).
Development
git clone <repo>
npm install
npm run build # build server + client
npm start # start server
# Or dev mode (separate terminals):
npm run dev:server # tsx watch
npm run dev:client # vite dev server (proxies to :3000)Architecture
┌─────────────────────────────────────────────┐
│ AI Agent │
│ MeatbagClient.solve(screenshot) │
│ └─ POST /challenge │
│ └─ socket.io join_job:{id} │
└───────────────┬─────────────────────────────┘
│ WebSocket
┌───────────────▼─────────────────────────────┐
│ meatbag-api server (Fastify + Socket.io) │
│ ┌─────────────────────────────────────┐ │
│ │ Redis │ │
│ │ captcha:job:{id} (hash, 5m TTL) │ │
│ │ captcha:queue (list, FIFO) │ │
│ └─────────────────────────────────────┘ │
│ │
│ Serves React solver UI at / │
└───────────────┬─────────────────────────────┘
│ WebSocket
┌───────────────▼─────────────────────────────┐
│ Human Solver (browser) │
│ Sees screenshot + tile grid │
│ Clicks tiles → emits solve event │
└─────────────────────────────────────────────┘License
MIT
