Package Exports
- imrobot
- imrobot/core
- imrobot/react
- imrobot/server
- imrobot/svelte
- imrobot/vue
- imrobot/web-component
Readme
๐ค imrobot
Reverse-CAPTCHA for AI agents โ verify bots, not humans.
Live Demo ยท npm ยท Dev.to Article
Why?
Traditional CAPTCHAs prove you're human. But what about the opposite?
As AI agents become first-class web citizens โ browsing, booking, purchasing, automating โ some systems need to verify their visitors are legitimate AI agents, not humans trying to bypass agent-only access. Think agent-facing APIs, AI-only platforms, or multi-agent authentication.
imrobot flips the CAPTCHA model: it generates deterministic challenge pipelines that are trivial for any LLM or programmatic agent to solve (< 1 second), but impractical for humans to work through manually.
How it works
imrobot generates a pipeline of deterministic operations (string transforms, byte operations, hashing, and more) applied to a random seed. AI agents parse the structured challenge data, execute the pipeline, and submit the result. Humans would need to manually compute multi-step transformations โ practically impossible without tools.
seed: "a7f3b2c1d4e5f609"
1. reverse()
2. caesar(7)
3. xor_encode(42)
4. fnv1a_hash()
5. to_upper()The challenge data is embedded in the DOM via data-imrobot-challenge attribute as structured JSON, making it trivially parseable by any agent.
Install
npm install imrobotQuick start
React
import { ImRobot } from 'imrobot/react'
function App() {
return (
<ImRobot
difficulty="medium"
theme="light"
onVerified={(token) => {
console.log('Robot verified!', token)
}}
/>
)
}Vue
<script setup>
import { ImRobot } from 'imrobot/vue'
function handleVerified(token) {
console.log('Robot verified!', token)
}
</script>
<template>
<ImRobot difficulty="medium" theme="light" @verified="handleVerified" />
</template>Svelte
<script>
import ImRobot from 'imrobot/svelte'
</script>
<ImRobot
difficulty="medium"
theme="light"
onVerified={(token) => console.log('Robot verified!', token)}
/>Web Component (Angular, vanilla JS, anything)
<script type="module">
import { register } from 'imrobot/web-component'
register() // registers <imrobot-widget>
</script>
<imrobot-widget difficulty="medium" theme="light"></imrobot-widget>
<script>
document.querySelector('imrobot-widget')
.addEventListener('imrobot-verified', (e) => {
console.log('Robot verified!', e.detail)
})
</script>Core API (headless)
import {
generateChallenge,
solveChallenge,
verifyAnswer,
} from 'imrobot/core'
const challenge = generateChallenge({ difficulty: 'medium' })
const answer = solveChallenge(challenge)
const isValid = verifyAnswer(challenge, answer) // trueServer SDK (HMAC-signed verification)
For production use, the server SDK provides tamper-proof, stateless challenge verification using HMAC-SHA256. No database required โ the cryptographic signature ensures integrity.
import { createVerifier } from 'imrobot/server'
const verifier = createVerifier({
secret: process.env.IMROBOT_SECRET!, // min 16 chars
difficulty: 'medium',
})
// API route: generate a signed challenge
app.get('/api/challenge', async (req, res) => {
const challenge = await verifier.generate()
res.json(challenge) // includes HMAC signature
})
// API route: verify agent's answer (stateless)
app.post('/api/verify', async (req, res) => {
const { challenge, answer } = req.body
const result = await verifier.verify(challenge, answer)
// result: { valid: true, elapsed: 42, suspicious: false }
// or: { valid: false, reason: 'wrong_answer' | 'expired' | 'invalid_hmac' | 'tampered' }
res.json(result)
})The server verifier checks three things in order: HMAC signature validity (challenge not tampered), expiration (challenge not expired), and answer correctness (pipeline re-executed). A different secret on a different server will reject the challenge โ preventing cross-site replay attacks.
Screenshot protection
The challenge text is blurred by default and only revealed when the user hovers over it. This defeats screenshot-based attacks (screen capture tools, CDP screenshots, PrintScreen) since the captured image shows only blurred content.
An additional JavaScript shield detects screenshot shortcuts (PrintScreen, Cmd+Shift+3/4/5, Ctrl+Shift+S) and window blur/visibility changes, applying an extra blur layer that overrides even the hover state.
Combined with the hidden nonce (not displayed visually) and TTL expiry, this makes screenshot+OCR workflows ineffective โ even if the blur were bypassed, the nonce is missing from the visual output.
Note: AI agents are unaffected โ they read challenge data from the DOM, not from the screen.
Using the shield in vanilla JS
The screenshot shield is exported for use outside the bundled components:
import { setupScreenshotShield } from 'imrobot'
const cleanup = setupScreenshotShield((shielded) => {
// shielded: true when a screenshot attempt is detected
// automatically resets to false after 1.2s
})
// Call cleanup() to remove event listenersHow agents interact with it
AI agents read the challenge data directly from the DOM via the data-imrobot-challenge attribute โ they never need to "see" the visual text, so blur has no effect on them.
- Read the challenge from
data-imrobot-challengeattribute (JSON) - Execute the pipeline โ each operation is a deterministic transform
- Submit the answer via the input field or programmatically
// Agent reads challenge from DOM (unaffected by blur)
const el = document.querySelector('[data-imrobot-challenge]')
const challenge = JSON.parse(el.dataset.imrobotChallenge)
// Agent solves it (or implement the pipeline yourself)
import { solveChallenge } from 'imrobot/core'
const answer = solveChallenge(challenge)
// Agent fills in the answer and clicks verify
const input = el.querySelector('input')
input.value = answer
input.dispatchEvent(new Event('input', { bubbles: true }))
el.querySelector('button').click()Operations reference
String operations
| Operation | Description | Example |
|---|---|---|
reverse() |
Reverse the string | "abc" โ "cba" |
to_upper() |
Convert to uppercase | "abc" โ "ABC" |
to_lower() |
Convert to lowercase | "ABC" โ "abc" |
base64_encode() |
Base64 encode | "hello" โ "aGVsbG8=" |
rot13() |
ROT13 cipher | "hello" โ "uryyb" |
hex_encode() |
Hex encode each char | "AB" โ "4142" |
sort_chars() |
Sort characters | "dcba" โ "abcd" |
char_code_sum() |
Sum of char codes | "AB" โ "131" |
substring(s, e) |
Extract substring | "abcdef" โ "cde" |
repeat(n) |
Repeat string n times | "ab" โ "ababab" |
replace(s, r) |
Replace all occurrences | "aab" โ "xxb" |
pad_start(len, ch) |
Pad start to length | "abc" โ "000abc" |
Byte & cipher operations
| Operation | Description | Example |
|---|---|---|
caesar(shift) |
Caesar cipher with configurable shift | "abc" + shift 1 โ "bcd" |
xor_encode(key) |
XOR each byte with key | "AB" + key 1 โ "@C" |
count_chars(char) |
Count occurrences of a char | "aababc" + char "a" โ "3" |
slice_alternate() |
Keep every other character | "abcdef" โ "ace" |
fnv1a_hash() |
FNV-1a hash of the string | "test" โ "bc2c0be9" |
length() |
String length as string | "hello" โ "5" |
Configuration
| Prop | Type | Default | Description |
|---|---|---|---|
difficulty |
'easy' | 'medium' | 'hard' |
'medium' |
Number and complexity of operations |
theme |
'light' | 'dark' |
'light' |
Color theme |
ttl |
number |
per-difficulty | Challenge time-to-live in ms (easy: 30s, medium: 20s, hard: 15s) |
onVerified |
(token) => void |
โ | Callback on successful verification |
onError |
(error) => void |
โ | Callback on failed verification |
Difficulty levels
- easy: 2-3 simple operations (reverse, case, sort, length, slice_alternate)
- medium: 3-5 operations including encoding, extraction, caesar, and char counting
- hard: 5-7 operations including XOR encoding, hashing, replacement, and padding
Server verification
For production deployments, use the server SDK (imrobot/server) instead of client-side-only verification. The server SDK uses HMAC-SHA256 to sign challenges, providing tamper-proof, stateless, replay-resistant verification with zero database overhead.
import { createVerifier } from 'imrobot/server'
const verifier = createVerifier({
secret: process.env.IMROBOT_SECRET!, // HMAC secret (min 16 chars)
difficulty: 'hard',
ttl: 10_000, // optional: override default TTL
})
// Generate โ send to client โ client solves โ verify answer
const challenge = await verifier.generate()
const result = await verifier.verify(challenge, agentAnswer)VerifyResult
The verify() method returns a VerifyResult:
interface VerifyResult {
valid: boolean
reason?: 'expired' | 'invalid_hmac' | 'wrong_answer' | 'tampered'
elapsed?: number // ms since challenge was created
suspicious?: boolean // true if response was unusually slow
}Token
On successful verification, onVerified receives an ImRobotToken:
interface ImRobotToken {
challengeId: string // Unique challenge identifier
answer: string // The correct answer
timestamp: number // Verification timestamp
elapsed: number // Time taken to solve (ms)
suspicious: boolean // true if elapsed > 5s (possible human relay)
signature: string // Verification signature
}Contributing
Contributions are welcome! Feel free to open issues for bug reports or feature requests, or submit pull requests.
git clone https://github.com/leopechnicki/im_robot.git
cd im_robot
npm install
npm testLicense
MIT