Reverse-CAPTCHA for AI agents โ verify bots, not humans.
Live Demo ยท npm ยท Dev.to Article
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.
imrobot generates a pipeline of deterministic string operations (reverse, base64, rot13, hex encode, etc.) 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 string transformations โ practically impossible without tools.
seed: "a7f3b2c1d4e5f609"
1. reverse()
2. to_upper()
3. base64_encode()
4. substring(0, 12)
5. rot13()
The challenge data is embedded in the DOM via data-imrobot-challenge attribute as structured JSON, making it trivially parseable by any agent.
npm install imrobot
import { ImRobot } from 'imrobot/react'
function App() {
return (
<ImRobot
difficulty="medium"
theme="light"
onVerified={(token) => {
console.log('Robot verified!', token)
}}
/>
)
}
<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>
<script>
import ImRobot from 'imrobot/svelte'
</script>
<ImRobot
difficulty="medium"
theme="light"
onVerified={(token) => console.log('Robot verified!', token)}
/>
<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>
import {
generateChallenge,
solveChallenge,
verifyAnswer,
} from 'imrobot/core'
const challenge = generateChallenge({ difficulty: 'medium' })
const answer = solveChallenge(challenge)
const isValid = verifyAnswer(challenge, answer) // true
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.
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 listeners
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.
data-imrobot-challenge attribute (JSON)// 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()
| 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" |
| Prop | Type | Default | Description |
|---|---|---|---|
difficulty |
'easy' | 'medium' | 'hard' |
'medium' |
Number and complexity of operations |
theme |
'light' | 'dark' |
'light' |
Color theme |
ttl |
number |
300000 |
Challenge time-to-live in ms |
onVerified |
(token) => void |
โ | Callback on successful verification |
onError |
(error) => void |
โ | Callback on failed verification |
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)
signature: string // Verification signature
}
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 test
MIT