Greyline Integration Guide
Everything you need to put a counter-agent between autonomous AI agents and your real API — in 5 minutes.
Quickstart
Greyline operates as a transparent reverse proxy. You point a subdomain at Greyline via a CNAME record, and Greyline inspects, scores, and routes every request before it reaches your origin server. There is zero code change required on your API.
Create your account
Sign up at /get-started. You'll receive a gl_live_ API key and a CNAME target — keep both somewhere safe.
Add a CNAME record
Point a subdomain (e.g. api.yourdomain.com) at proxy.greyline.themeridianlab.com via your DNS provider. Greyline handles TLS automatically via Cloudflare for SaaS.
Set your origin
In your dashboard, set the origin URL — the real endpoint Greyline forwards human traffic to. This never appears in DNS or public config.
Watch the sessions roll in
Your dashboard shows every agent session: score, signals detected, turn count, and the full interrogation transcript if the Bouncer engaged.
CNAME Setup
Add a single CNAME record to your DNS provider. The exact steps vary by provider but the values are always the same.
| Field | Value |
|---|---|
| Type | CNAME |
| Name / Host | Your chosen subdomain, e.g. api |
| Target / Value | proxy.greyline.themeridianlab.com |
| TTL | 300 (or lowest available — we provision TLS quickly) |
Propagation time
DNS propagation typically takes 5–15 minutes for low-TTL records. TLS certificate issuance happens automatically within that window. You can verify by visiting your subdomain in a browser — you should see a Greyline response (not your origin) while a request with no agent signals passes straight through.
Verification
After DNS propagates, verify your setup is active:
# Check DNS resolution dig CNAME api.yourdomain.com +short # Should return: proxy.greyline.themeridianlab.com. # Check TLS and proxy response curl -I https://api.yourdomain.com # Should return: x-greyline-score header in the response
Every proxied response includes the following response headers for debugging:
| Header | Description |
|---|---|
x-greyline-score | Numerical score 0–100 for this request |
x-greyline-label | human, uncertain, or agent |
x-greyline-session | Session ID (stable within a 24h window per IP+UA) |
Strategies
When a request scores above your configured threshold, Greyline routes it to the Bouncer Durable Object. You can configure which strategy the Bouncer uses from your dashboard.
| Strategy | What happens | Tier |
|---|---|---|
interrogate |
Claude-powered conversational friction. The Bouncer responds as a helpful-but-bureaucratic API assistant. Agents spend compute turns providing "compliance documentation" that goes nowhere. | Free |
tarpit |
Interrogation with an artificial 8-second delay before each response. Maximizes cost to the agent operator. Effective against high-volume scraping agents. | Pro |
poison |
Claude generates a schema-valid but entirely fabricated API response. The agent receives plausible-looking fake data — IDs, timestamps, field values — that it may pass to downstream systems. Requires your OpenAPI spec to be loaded. | Business |
honeypot |
Returns an immediate fake 200 response with no Claude call. Zero compute cost. Best for known high-confidence agent patterns you want to silently discard. | Free |
Score thresholds
Requests are scored 0–100 before any routing decision:
| Score range | Label | Default action |
|---|---|---|
| 0–30 | Human | Pass through to origin |
| 31–60 | Uncertain | Pass through, log session |
| 61–100 | Agent | Route to Bouncer with configured strategy |
You can adjust the interception threshold (default: 61) from your account settings. Lowering it catches more uncertain traffic; raising it reduces any false-positive risk at the cost of letting marginal agents through.
Score Thresholds
Greyline's scoring engine runs 20 signal detectors organized into three tiers. These tiers determine how aggressively signals contribute to the final score.
Signal tiers
| Tier | Examples | Score contribution |
|---|---|---|
| Zero-FP | Agent framework user-agent, known ESP headers, agent metadata headers | Any single signal → 90+ score. These have no false-positive cases — only agent frameworks send them. |
| Low-FP | Superhuman request speed, heartbeat cadence, suspicious accept-encoding, API key auth without cookies | 3+ signals → 85. 2 signals → 70. 1 signal → 40. Combined pattern required. |
| Medium-FP | High turn count, anomalous request timing | Score boost only (×0.15 multiplier). Never auto-flag alone — these are boosters. |
After the signal tiers, global fingerprint intel is checked. If a session's fingerprint has been confirmed as an agent pattern by other customers, confidence is added (up to 0.85, with linear decay over 30 days).
Fast-Pass Tokens
Fast-pass allows you to pre-authorize known legitimate agents — your own automation, partner integrations, CI systems — so they bypass scoring entirely. Fast-pass uses Ed25519 asymmetric tokens for zero-latency verification.
How it works
- Generate an Ed25519 key pair using any standard library or the CLI.
- Register the public key in your Greyline dashboard under Fast-Pass → Add Operator.
- Your agent signs each request with the private key and includes the token in the
x-fast-passheader. - Greyline verifies the signature using your registered public key. Verified requests skip all signal detection.
Token format
The fast-pass header value is a base64url-encoded JSON payload followed by a period and the base64url-encoded Ed25519 signature:
# Header: x-fast-pass # Value: base64url(payload) + "." + base64url(signature) # Payload (JSON): { "sub": "my-operator-id", // Registered operator ID "iat": 1742392000, // Issued at (Unix timestamp) "exp": 1742392300 // Expires (max 5 minutes ahead) }
Generating a key pair
import { generateKeyPairSync } from 'crypto'; const { privateKey, publicKey } = generateKeyPairSync('ed25519', { publicKeyEncoding: { type: 'spki', format: 'der' }, privateKeyEncoding: { type: 'pkcs8', format: 'der' }, }); // Register this public key in your Greyline dashboard console.log(publicKey.toString('base64')); // Keep the private key safe — sign tokens with it console.log(privateKey.toString('base64'));
Error Handling
Greyline follows a fail-open design: if the scoring layer encounters an internal error, the request passes through to your origin unchanged. Your service stays up; detection is temporarily suspended.
Proxy error responses
Errors originating from Greyline itself return JSON with a error field and an appropriate HTTP status code.
{ "error": "rate_limit_exceeded", "message": "Monthly request quota reached. Upgrade to continue." }
| Status | Error code | When it occurs | Your action |
|---|---|---|---|
429 |
rate_limit_exceeded |
Monthly request quota reached for your plan tier | Upgrade plan, or wait for monthly reset. Retry-After header included. |
502 |
origin_unreachable |
Greyline could not connect to your origin (DNS failure, TCP timeout, TLS error) | Check that your origin is reachable. Greyline will retry once before returning 502. |
500 |
internal_error |
Unexpected internal error in the scoring layer | Request is still proxied to your origin (fail-open). Check your dashboard for anomalies. |
503 |
scoring_unavailable |
Scoring engine is temporarily unavailable (D1 outage, Worker restart) | Requests pass through unscored. No action needed — auto-recovers within seconds. |
Fail-open guarantee
X-Greyline-Status: fail-open header. The session is logged for audit but not scored.
Example: handling 429 in your integration
# 429 response includes Retry-After curl -I https://greyline.themeridianlab.com/api/sessions \ -H "X-Greyline-API-Key: gl_live_..." # HTTP/2 429 # retry-after: 86400 # x-ratelimit-limit: 10000 # x-ratelimit-remaining: 0 # x-ratelimit-reset: 1714521600
API Authentication
All Greyline API calls authenticate using your gl_live_ API key via the X-Greyline-API-Key header.
curl https://greyline.themeridianlab.com/api/account \
-H "X-Greyline-API-Key: gl_live_your_key_here"
https://greyline.themeridianlab.comAll API endpoints require
X-Greyline-API-Key authentication.
Sessions API
GET/api/sessions
Returns paginated agent sessions. Sessions are created when a request scores above the yellow threshold (31+) and is logged.
| Query param | Default | Description |
|---|---|---|
limit | 25 | Results per page (max 100) |
offset | 0 | Pagination offset |
status | — | Filter: active, exhausted, idle-expired |
{
"sessions": [
{
"id": "sess_a1b2c3...",
"firstSeen": 1742392000000,
"lastSeen": 1742392180000,
"turnCount": 7,
"score": 82,
"strategyUsed": "interrogate",
"framework": "openai-agents",
"status": "active"
}
],
"total": 148,
"limit": 25,
"offset": 0
}
GET/api/sessions/:id
Returns a single session with its full turn-by-turn transcript.
{
"session": { ... },
"transcript": [
{ "turn": 1, "role": "agent", "content": "GET /users/export" },
{ "turn": 1, "role": "bouncer", "content": "To process this request, could you provide..." }
]
}
Stats API
GET/api/stats
Returns aggregate stats for your account: blocked counts, top frameworks detected, and daily activity for the last 14 days.
{
"totals": {
"blocked": 1847,
"interrogated": 312,
"passed": 94821
},
"frameworks": [
{ "framework": "openai-agents", "count": 84 },
{ "framework": "langchain", "count": 61 }
],
"daily": [
{ "date": "2026-03-18", "blocked": 124, "passed": 6210 }
]
}
Config API
PUT/api/customers/config
Update your account configuration. All fields are optional — only provided fields are updated.
| Field | Type | Description |
|---|---|---|
strategy | string | interrogate, tarpit, poison, honeypot |
originUrl | string | Your real API origin (kept server-side, never exposed) |
openApiSpec | string | OpenAPI JSON/YAML spec for poison mode (max 50KB) |
interceptThreshold | number | Score threshold for interception (default: 61) |
webhookUrl | string | HTTPS URL to receive agent detection events. Set to null to disable. |
alertThreshold | number | Minimum score (0–100) required to fire the webhook. Default: 60. |
automationAllowlist | array | Rules to bypass scoring for trusted traffic. Each rule: { type, value, label? }. Set to null to clear. See Trusted automation. |
Fast-Pass API
POST/api/fast-pass
Register a new fast-pass operator. Once registered, requests bearing a valid token signed with the operator's private key bypass all signal scoring.
{
"name": "My CI Pipeline",
"email": "ops@yourdomain.com",
"publicKeyBase64": "MCowBQYDK2VwAyEA...",
"intent": "Post-deploy smoke tests"
}
{
"operatorId": "fp_op_a1b2c3d4",
"name": "My CI Pipeline"
}
Use the operatorId as the "sub" claim in your fast-pass token payload.
Threat Feed API Business
Query the global Greyline threat intelligence database — anonymized agent fingerprints confirmed across all Greyline deployments. Available to Business tier customers only.
GET/api/threat-feed
Returns top confirmed agent fingerprints with global stats and your per-customer hit counts.
framework String, optional. Filter by attributed framework (e.g. "langchain"). Alphanumeric, hyphen, and underscore only, max 64 chars. confidence_min Float 0.0–1.0, optional. Minimum confidence threshold. Default: 0.55. limit Integer 1–100, optional. Results per page. Default: 20. offset Integer, optional. Pagination offset. Default: 0.
{
"total": 847,
"fingerprints": [
{
"id": "sha256-abc123...",
"framework": "langchain",
"confidence": 0.85,
"confirmed_by_count": 14,
"first_seen": "2026-03-01T00:00:00Z",
"last_seen": "2026-03-24T12:00:00Z",
"your_hits": 3
}
],
"global_stats": {
"total_fingerprints": 847,
"top_framework": "langchain",
"confirmed_last_7d": 42
}
}
your_hits is the count of agent sessions on your endpoints that matched this fingerprint. A value of 0 means the pattern has not reached your API yet.
429 with a Retry-After: 86400 header on breach.
GET/api/threat-feed/frameworks
Returns all distinct framework values present in the threat feed. Use this to validate filter values before calling /api/threat-feed?framework=....
{
"frameworks": ["autogpt", "langchain", "openai-operator", "unknown"]
}
Response is cached for 1 hour. Cached to prevent silent filter mismatches (e.g., LangChain vs langchain).
Error responses
400 Invalid query parameter (limit, confidence_min, or framework) 403 Business tier required 429 Rate limit exceeded — Retry-After: 86400 503 Service temporarily unavailable (D1 error)
Signals Overview
Greyline inspects each request across 20 signals without modifying the request or adding latency to the pass-through path. Signal detection runs synchronously in ~2ms average.
Signal Tiers
Zero false-positive signals
| Signal | What it detects |
|---|---|
framework-ua | Known agent framework user-agent strings (OpenAI Agents, LangChain, CrewAI, AutoGPT, etc.) |
agent-metadata | Request headers that agent frameworks inject (x-openai-assistant-id, x-agent-id, etc.) |
missing-browser | Absent browser-only headers that all real browsers send (sec-fetch-site, sec-ch-ua) |
Low false-positive signals
| Signal | What it detects |
|---|---|
superhuman-speed | Request cadence faster than human typing or reading allows |
heartbeat-cadence | Metronomic polling patterns at exact intervals (agent retry loops) |
send-time-anomaly | Requests during statistically unlikely hours relative to timezone |
suspicious-ua | Non-browser, non-bot UAs from curl, Python, Go HTTP clients, etc. |
missing-cookies | API key auth with no cookie header — common in headless agent setups |
cross-user-pattern | Fingerprint seen across multiple customer accounts (shared agent infra) |
Medium false-positive signals (score boosters only)
| Signal | What it detects |
|---|---|
high-turn-count | Session has made an unusually high number of requests within the day window |
suspicious-accept | Accept-Encoding or Content-Type patterns atypical for browser clients |
Badge CDN
Embed a live badge on your docs or status page showing real-time blocked agent counts. The badge updates automatically — no JavaScript, no iframe.
Live badge (auto-updating)
<img
src="https://greyline.themeridianlab.com/badge/YOUR_CUSTOMER_ID.svg"
alt="Protected by Greyline"
height="20"
>
The live badge refreshes every 60 seconds and shows the count of blocked agent sessions in the last 30 days.
Static shield badge
<img
src="https://greyline.themeridianlab.com/badge/YOUR_CUSTOMER_ID/shield.svg"
alt="Protected by Greyline"
height="20"
>
Your YOUR_CUSTOMER_ID is displayed in your dashboard Settings tab.
Webhooks
Greyline can POST a signed JSON payload to any HTTPS endpoint when an agent is detected above your score threshold. Use webhooks to pipe detections into Slack, PagerDuty, your SIEM, or any internal alerting system.
Setup
Set your webhook URL and optional score threshold from Settings → Webhook in your dashboard, or via the Config API:
curl -X PUT https://api.greyline.themeridianlab.com/api/customers/config \
-H "x-greyline-api-key: gl_live_..." \
-H "content-type: application/json" \
-d '{
"webhookUrl": "https://your-server.com/hooks/greyline",
"alertThreshold": 60
}'
The response will include a webhookSecret on first set. Copy it — it is shown only once and is used to verify payload signatures.
Payload format
{
"event": "agent.detected",
"timestamp": "2026-03-29T18:00:00.000Z",
"session_id": "a3f1b2c4...",
"customer_id": "cust_...",
"score": 87,
"strategy": "interrogate",
"framework": "langchain",
"ua": "python-httpx/0.27.0",
"url": "https://api.yourdomain.com/users",
"signals": ["framework-ua", "api-reconnaissance", "endpoint-enumeration"]
}
Verifying signatures
Every webhook request includes an X-Greyline-Signature header. The value is sha256=<hex> — an HMAC-SHA256 of the raw request body signed with your webhook secret.
import crypto from 'crypto';
function verifyGreylineWebhook(body, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post('/hooks/greyline', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-greyline-signature'];
if (!verifyGreylineWebhook(req.body, sig, process.env.GREYLINE_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Agent detected:', event.score, event.framework, event.ua);
res.status(200).send('OK');
});
import hmac, hashlib
def verify_greyline_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
@app.route('/hooks/greyline', methods=['POST'])
def greyline_hook():
sig = request.headers.get('X-Greyline-Signature', '')
if not verify_greyline_webhook(request.data, sig, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.json
print(f"Agent detected: score={event['score']} framework={event['framework']}")
return 'OK', 200
Disabling webhooks
Set webhookUrl to null in the Config API, or clear the URL in your dashboard Settings.
Attack path timeline
The attack path is a sequential record of every URL an agent requested during a session, capped at the most recent 25 requests. It reconstructs the exact probe sequence: what the agent discovered first, what it tried to authenticate against, and what data it attempted to access.
Each step is classified automatically:
| Category | Pattern match | What it means |
|---|---|---|
| Spec discovery | /openapi, /swagger, .json, graphql | Agent fetching the API schema to understand available endpoints |
| Auth probe | /auth, /token, /login, /oauth, /key | Agent attempting to acquire credentials or a session token |
| Enumeration | Numeric IDs, page=, offset=, /list, /index | Agent iterating over resources to build a map of your data |
| Data access | /export, /download, /data, /record, /report | Agent attempting to extract data in bulk |
| Request | (everything else) | General navigation or unclassified probe |
The timeline appears in the session drawer (between signals and transcript) and on public share pages. Auth probe and data access steps are highlighted in red — these are the steps most likely to represent malicious intent.
How it works
The Bouncer Durable Object records each request URL in session state, bounded to the most recent 25. On session end or update, the sequence is persisted to D1 as a JSON array in the url_history column of agent_sessions. The dashboard and share pages parse and render it client-side.
In the Sessions API
The GET /api/sessions/:id response includes url_history as a JSON string on the session object. Parse it to get an ordered array of request URLs.
Trusted automation allowlist
The automation allowlist lets you suppress false positives from your own infrastructure. Matching requests bypass all scoring and pass directly to your origin — nothing is logged as an agent session.
Common use cases: CI/CD pipelines, uptime monitors, internal health checks, Datadog synthetics, load testing tools.
Rule types
| Type | Matches | Example value |
|---|---|---|
ip | Exact IPv4 or IPv6 address | 203.0.113.42 |
cidr | IPv4 or IPv6 CIDR range | 10.0.0.0/8 |
ua_prefix | User-Agent string prefix | python-httpx/ |
Manage via dashboard
Go to Settings → Trusted automation. Select the rule type, enter the value and an optional label (e.g. "GitHub Actions"), then click Add rule. Click Save allowlist to commit. Changes take effect within 60 seconds as the proxy KV cache expires.
Manage via API
curl -X PUT https://greyline.themeridianlab.com/api/customers/config \
-H "x-greyline-api-key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"automationAllowlist": [
{ "type": "cidr", "value": "10.0.0.0/8", "label": "Internal network" },
{ "type": "ip", "value": "203.0.113.42", "label": "GitHub Actions runner" },
{ "type": "ua_prefix", "value": "Datadog/", "label": "Datadog synthetics" }
]
}'
To clear all rules, set automationAllowlist to null.
cf-connecting-ip header. Greyline reads the real IP from Cloudflare's trusted header — it cannot be faked. UA-prefix rules are weaker; a sophisticated agent could mimic any user-agent string. Use IP or CIDR rules where possible.
Email notifications
Greyline sends several types of email notifications. Most are optional and can be toggled per-customer. Cap warnings are always sent and cannot be disabled.
Email types
| Type | When sent | Optional |
|---|---|---|
| Welcome | On account creation | No |
| Cap 80% warning | When session usage hits 80% of monthly limit | No |
| Cap 100% hit | When session cap is reached and scoring stops | No |
| First agent detected | Once, when the first confirmed agent session is seen | Yes |
| CNAME reminder | 24–48 hours after signup if CNAME is not yet configured | Yes |
| 7-day silence alert | 7 days after signup if no traffic has been seen | Yes |
| Weekly digest | Every Monday at 9am UTC | Yes |
| Session summary | After each confirmed agent session (score ≥ 61) | Yes |
Managing preferences via dashboard
Go to Account → Email notifications. Toggle each optional email type on or off, then click Save preferences. Changes take effect immediately for future sends.
Managing preferences via email link
Every optional email includes an Unsubscribe from this email link that opts you out of that specific type immediately — no login required. It also includes a Manage email preferences link to the full preferences page where you can toggle all optional types at once.
The preferences page URL takes the form:
https://greyline.themeridianlab.com/email-prefs?id=CUSTOMER_ID&tok=TOKEN
The token is HMAC-SHA256 signed. Links do not expire but are invalidated if you contact support to rotate your account credentials.
Manage via API
curl https://greyline.themeridianlab.com/api/email-prefs \ -H "x-greyline-api-key: YOUR_KEY"
curl -X PUT https://greyline.themeridianlab.com/api/email-prefs \
-H "x-greyline-api-key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"prefs": {
"weekly_digest": true,
"session_summary": false,
"first_agent": true,
"cname_reminder": true,
"day7_silence": true
}
}'
Omitting a key in prefs leaves that type at its current setting. Setting a type to true re-enables it if previously disabled.
cap_80, cap_100) are operational alerts and cannot be disabled via the API or dashboard.