feat: agentPolicy() — decide what to do with an agent, by intent - #20
Merged
Conversation
Classification tells you which agent knocked. It doesn't tell you what to do about it, and the obvious answer — "block the AI bots" — is wrong in a way that costs real money. A vendor's bulk crawler and its user-facing fetcher are opposite economics wearing similar names. GPTBot is corpus collection: you get nothing per fetch. ChatGPT-User is a person who just asked about you and an assistant going to read the page on their behalf — that is a distribution channel, and charging for it means charging for your own marketing. Same for ClaudeBot vs Claude-User, PerplexityBot vs Perplexity-User. Measured on 30 days of apideck.com AI traffic, the split is not marginal: retrieval 310,882 events 60.1% <- allow, this is demand training 199,013 events 38.5% <- meter or charge unknown 7,579 events 1.5% tooling 4 events 0.0% A blanket bot block would have discarded 60% of that traffic, all of it people asking questions about the product. agentPolicy() maps a request to one of allow / meter / charge / block via an `intent` of retrieval / training / search / tooling / unknown, with a knob per intent and an allowList that overrides everything. Defaults are deliberately conservative: meter training, allow everything else. Notes on the design: - Intent is an explicit list, not a pattern guess. A `-User` suffix is not the marker: OpenAI's ChatGPT-User fetches server-side from Azure while Claude Code's Claude-User runs on a developer's laptop. Guessing here is how you end up billing your own demand. - Applebot is `search`; Applebot-Extended is `training`. Same vendor, and the distinction is the entire point of Apple publishing two tokens. - Only 'spoofed' blocks when verify is on. 'unverifiable' must not block, or you refuse every vendor without a published feed plus every coding agent running on someone's own machine. - No payment rails, no network calls, no dependencies. This is the policy layer; settlement belongs to Stripe's MPP or an x402 SDK, and the library should not hold money or inherit PCI scope. Tests: 216 -> 229.
Rebased onto 0.12. The original version imported verifyRequest directly, which would have pulled the published IP range tables back into the root bundle and undone the size work in #21. agentPolicy now takes the same injected-verifier shape as trackVisit: `verify` is a function rather than a boolean, supplied from `@apideck/agent-analytics/verify` by callers who want spoofed identities to block. Root bundle stays free of range data — verified against dist.
Gdewilde
force-pushed
the
feat/agent-policy
branch
from
August 2, 2026 08:44
2efb7e2 to
cf957b4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Classification tells you which agent knocked. It doesn't tell you what to do about it — and the obvious answer, "block the AI bots", is wrong in a way that costs real money.
The distinction this is built on
A vendor's bulk crawler and its user-facing fetcher have opposite economics while wearing similar names:
GPTBot— corpus collection. You get nothing back per fetch.ChatGPT-User— a person just asked about you, and an assistant is reading the page for them. That's a distribution channel. Charging for it means charging for your own marketing.Same for
ClaudeBotvsClaude-User,PerplexityBotvsPerplexity-User.It isn't a marginal split
Measured on 30 days of AI traffic to apideck.com:
allow— this is demandmeterallowallowA blanket bot block would have discarded 60% of that traffic — all of it people asking questions about the product.
API
Maps a request to
allow/meter/charge/blockvia anintentofretrieval/training/search/tooling/unknown. One knob per intent, plus anallowListthat overrides everything. Defaults are conservative: meter training, allow the rest.Verified against the real UA shapes:
Design notes
-Usersuffix is not the marker — OpenAI'sChatGPT-Userfetches server-side from Azure, while Claude Code'sClaude-Userruns on a developer's laptop. Guessing here is exactly how you end up billing your own demand.Applebotissearch;Applebot-Extendedistraining. Same vendor, and that distinction is the whole reason Apple publishes two tokens.spoofedblocks whenverifyis on.unverifiablemust never block, or you refuse every vendor without a published feed plus every coding agent running locally — the same trap feat: verify claimed crawler identity against published IP ranges #18 documented.Why now
Stripe and Tempo's Machine Payments Protocol (18 March 2026) and x402 both settle at the HTTP layer — the same layer this middleware already occupies. Metering is the prerequisite for either, and the retrieval/training split is the policy that makes metering sane rather than self-harming.
This PR is the classification and policy half only. A
paymentRequired()helper emitting the 402 challenge would be the natural follow-up, and should stay an adapter over someone else's rail.Tests: 216 → 229.
🤖 Generated with Claude Code