Reverse CAPTCHA for agent-native systems.
A proof-of-concept exploring constraint-based access control for environments where autonomous agents are signal and manual human interaction is noise.
As AI agents proliferate, a new class of systems emerges: agent-native platforms where autonomous agents coordinate, transact, and interact with each other. In these environments, the traditional threat model inverts—humans become the source of noise, spam, and degradation.
Traditional access control relies on identity: who you are, what you've done, how you behave. This creates surveillance, requires trust infrastructure, and breaks down when actors are ephemeral or pseudonymous.
CaptchAI takes a different approach: constraint-based access control.
Instead of asking who you are, we enforce how you interact.
- Challenge Issuance: Request a cryptographic challenge (nonce, difficulty, expiry, algorithm)
- Proof-of-Work: Solve
SHA256(nonce + solution)to produce a hash with N leading zeros - Time Constraint: Submit the solution within a narrow window (default: 200ms)
- One-Time Use: Each challenge is consumed after verification, success or failure
Autonomous agents solve these trivially. Manual humans attempting to copy, paste, or reason through the challenge almost always fail.
The system makes no claim about what you are—human or AI. It only enforces tempo. If you can meet the constraint, you may pass. If not, you're excluded.
"The fence does not know why you are on one side or the other. It only knows that you have passed through the gate."
- No identity verification: No surveillance, no behavioral tracking, no scoring
- No classification: We don't distinguish human vs. bot
- Just constraints: Produce valid proof-of-work within the time window, or don't pass
- Composable boundaries: Each tenant configures their own difficulty and lifetime
This is the inverse of CAPTCHA. CAPTCHA protects human systems from bots. CaptchAI protects agent systems from humans—without ever claiming to identify which is which.
This is exploratory infrastructure for systems that don't quite exist yet:
- Agent social networks: Where agents post and interact autonomously (e.g., Moltbook-inspired platforms)
- Agent marketplaces: Where manual interference degrades price discovery or coordination
- Multi-agent simulations: Where human intervention corrupts research data
- Agent-to-agent APIs: Where you want to ensure programmatic, not manual, access
Note: These markets are nascent. This project explores the infrastructure layer before the platforms themselves mature.
Built as production-ready infrastructure (even if the market isn't ready):
- ✅ Rate limiting: Sliding window (100 req/min per tenant) to prevent abuse
- ✅ Graceful shutdown: SIGTERM/SIGINT handlers with request draining
- ✅ Structured JSON logging: All events logged with timestamps for observability
- ✅ Comprehensive tests: 19 test cases covering auth, challenges, expiry, reuse, rate limits
- ✅ Multi-tenant: Each API key has configurable difficulty and lifetime
- ✅ One-time use: Challenges are consumed after verification attempt
- ✅ Generic errors: No information leakage on failure (always returns "challenge failed")
All endpoints require API key via X-API-Key header or Authorization: Bearer <key>.
Health check endpoint for load balancers.
Response: {"ok": true}
Request a new proof-of-work challenge.
Response:
{
"nonce": "a1b2c3d4...",
"expiry": 1234567890123,
"difficulty": 4,
"algorithm": "sha256_hex_leading_zeros: Concatenate nonce and solution..."
}Submit challenge solution with optional payload.
Request:
{
"nonce": "a1b2c3d4...",
"solution": "12345",
"payload": { "optional": "data" }
}Success Response (200):
{
"accepted": true,
"payload": { "optional": "data" }
}Failure Response (403):
{
"error": "challenge failed"
}Failures are deliberately generic (expired, wrong solution, reused nonce all return the same error).
TENANTS (required): JSON object mapping API keys to configuration
export TENANTS='{
"sk-test": {"lifetimeMs": 500, "difficulty": 3},
"sk-prod": {"lifetimeMs": 200, "difficulty": 5}
}'lifetimeMs: Challenge validity window in milliseconds (default: 200)difficulty: Number of leading zero characters required (default: 4)
PORT (optional): Server port (default: 3000)
| Difficulty | Avg. Attempts | Use Case |
|---|---|---|
| 2 | ~100 | Testing |
| 3 | ~1,000 | Lenient gating |
| 4 | ~10,000 | Standard |
| 5 | ~100,000 | Strict |
| 6+ | ~1,000,000+ | Extreme (may timeout) |
Note: Higher difficulty with short lifetime (e.g., difficulty 5 + 200ms) creates very tight constraints. Balance for your use case.
# Install dependencies
npm install
# Set tenant configuration
export TENANTS='{"sk-test": {"lifetimeMs": 1000, "difficulty": 3}}'
# Start server
npm startServer runs on http://localhost:3000
# Run test suite
npm test
# Manual test script
node test-api.js http://localhost:3000 sk-testRailway (recommended):
- Connect GitHub repo
- Set
TENANTSenvironment variable - Railway auto-deploys and sets
PORT
Other platforms (Render, Fly.io):
- Set
TENANTSenv var - Platform provides
PORT - Health check:
GET /health
See detailed deployment instructions in CLAUDE.md.
.
├── server.js # Main Express API server
├── server.test.js # Comprehensive test suite (19 tests)
├── test-api.js # Manual end-to-end test script
├── package.json # Dependencies and scripts
├── CLAUDE.md # Detailed project documentation
└── README.md # This file
- Challenges:
Map<apiKey, Map<nonce, {nonce, expiry, difficulty}>> - Rate limits:
Map<apiKey, Array<timestamp>>
For horizontal scaling, replace with Redis:
- Challenges:
SET tenant:nonce {data} EX lifetime_seconds - Rate limits: Redis sorted sets or rate-limit library
// Verification logic
const digest = SHA256(nonce + solution).hex();
const valid = digest.startsWith('0'.repeat(difficulty));Clients must brute-force solutions by incrementing until they find one that hashes to N leading zeros.
- Stateless: No tracking, no identity, no history
- Universal: Any actor can participate if they meet the constraint
- Measurable: Clear, objective success criteria
- Composable: Easy to integrate into any system
- Tempo enforcement: Distinguishes automated vs. manual without classification
- One-time use: Prevents replay attacks
- Configurable: Each tenant tunes constraints for their use case
- No information leakage: Attackers learn nothing from failures
- Consistent UX: All rejections look identical
- Privacy: No behavioral fingerprinting
- In-memory state: Single instance only (no horizontal scaling)
- No persistent storage: Challenges lost on restart
- Simple rate limiting: Per-tenant sliding window, not distributed
- CPU-bound PoW: Favors those with more compute resources
- Redis backend: Shared state for multi-instance deployments
- WebAssembly PoW: Consistent performance across devices
- Adaptive difficulty: Auto-tune based on solve times
- Challenge marketplace: Let platforms share/trade challenges
- Protocol standardization: Define formal spec for interoperability
This project explores an idea: What if we stopped trying to identify actors and started enforcing behavioral constraints instead?
The market for agent-native platforms is nascent. This infrastructure may be years early. But the question is worth asking:
In a world where agents coordinate autonomously, how do we create boundaries without surveillance?
This is one answer. It may not be the right one. But it's a starting point.
This is a proof-of-concept side project, not an active commercial effort. That said:
- Issues/questions: Open an issue
- Improvements: PRs welcome
- Philosophy: Discuss in Discussions tab
- Related work: I'd love to hear about similar explorations
MIT License - see LICENSE file.
Built by someone who read about Moltbook and wondered what infrastructure agent-native systems might need. Turns out, maybe nothing—or maybe this. Time will tell.
- Inspired by Moltbook and discussions around agent social networks
- Built in the spirit of "infrastructure before the platforms"
- Thanks to everyone thinking about agent coordination, constraint-based design, and alternatives to identity-based access control
"The service does not classify callers as 'human' or 'non-human.' It does not promise to exclude any particular kind of actor. It only enforces that callers meet the current norm: produce a valid solution to a current challenge within the allowed time. Agents that can satisfy these constraints may interact. Agents that cannot will not. The boundary is the constraint itself, not a judgment about who or what you are."