@apideck/agent-analytics 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/firewall.ts"],"names":["shellQuote","json","toCli","r","parts","group","i","c","k","toJson","conditions","finish","median","ns","s","a","b","mid","recommendFirewallRules","observations","opts","out","wanted","o","requests","n","names","spoofed","spoofedIps","v","vendors","perIp","threshold","heavy","sweeping","training","budget","headlessAsns","firewallScript","recommendations","lines"],"mappings":"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","file":"firewall.js","sourcesContent":["/**\n * Recommend Vercel WAF rules from observed agent traffic.\n *\n * This generates *proposals*, never live changes. Every recommendation comes out\n * with `action: 'log'`, because a firewall rule's blast radius is unpredictable\n * until real traffic hits it and a bad `deny` takes out real users or your SEO.\n * Vercel's own guidance is log → review → preview → production; the `eventual`\n * field records where a rule is meant to end up, and `cli` emits the command for\n * the *current* stage only.\n *\n * Two hard rules, both from measurement rather than taste:\n *\n * 1. Retrieval agents and search crawlers are never proposed for blocking.\n * 60% of AI traffic on one production site is retrieval — a person asked a\n * question and an assistant went to read the page. Blocking that is\n * blocking your own distribution. The recommender emits a `bypass` rule to\n * protect them *first*, so later rules cannot catch them.\n *\n * 2. Training crawlers get rate limits, not denials, by default. The point is\n * to bound cost, not to disappear from corpora.\n *\n * Only abuse gets a denial: an identity that failed cryptographic or IP\n * verification, or a single address behaving like a scraper.\n */\n\nimport type { AgentIntent } from './policy.js'\n\n/** A Vercel WAF condition. Mirrors the CLI's `--condition` JSON. */\nexport interface FirewallCondition {\n type:\n | 'user_agent'\n | 'ip_address'\n | 'geo_as_number'\n | 'geo_country'\n | 'path'\n | 'method'\n | 'environment'\n | 'ja4_digest'\n op: 'eq' | 'neq' | 'sub' | 'pre' | 'suf' | 're' | 'inc' | 'ninc' | 'gt' | 'gte'\n value?: string | number | Array<string | number>\n key?: string\n neg?: boolean\n}\n\nexport type FirewallAction = 'log' | 'deny' | 'challenge' | 'bypass' | 'rate_limit'\n\nexport interface RateLimitSpec {\n /** Seconds, 10–3600. */\n window: number\n /** Max requests per window. */\n requests: number\n /** What happens on breach. */\n action: 'rate_limit' | 'deny' | 'challenge' | 'log'\n keys: Array<'ip' | 'ja4'>\n}\n\nexport interface FirewallRecommendation {\n name: string\n /** Why this rule is proposed, in one sentence. */\n rationale: string\n /** The measurement behind it. Never propose a rule without evidence. */\n evidence: string\n /** OR of ANDs: outer array is groups, inner is conditions within a group. */\n groups: FirewallCondition[][]\n /** Always `'log'` or `'bypass'` — see the module note. */\n action: FirewallAction\n /** Where this rule is intended to end up after review. */\n eventual: FirewallAction\n rateLimit?: RateLimitSpec\n /** How likely this is to catch traffic you wanted. */\n risk: 'low' | 'medium' | 'high'\n /** What could go wrong, when it is not obvious. */\n caveat?: string\n /** Ready-to-run CLI for the *current* stage. */\n cli: string\n /** Equivalent `--json` payload. */\n json: unknown\n}\n\n/** One aggregated slice of observed traffic. */\nexport interface TrafficObservation {\n userAgent: string\n botName: string\n intent: AgentIntent\n requests: number\n ip?: string\n /** Autonomous system number, if you resolved one. */\n asn?: number\n /** Distinct paths this slice touched — a scraper sweeps, a reader does not. */\n distinctPaths?: number\n /** Verification verdict, if you ran one. */\n verification?: 'verified' | 'spoofed' | 'unverifiable' | 'not-claimed'\n country?: string\n}\n\nexport interface RecommendOptions {\n /**\n * Requests-per-slice above which a single IP is considered abusive. Defaults\n * to 10x the median across observations, floored at 500.\n */\n abuseThreshold?: number\n /** Rate-limit budget proposed for training crawlers. Defaults to 600/hour. */\n trainingBudget?: { window: number; requests: number }\n /** Skip the protective bypass rule. Rarely a good idea. */\n omitProtectiveBypass?: boolean\n}\n\n/* -------------------------------------------------------------------------- */\n\nfunction shellQuote(json: unknown): string {\n return `'${JSON.stringify(json).replace(/'/g, `'\\\\''`)}'`\n}\n\nfunction toCli(r: Omit<FirewallRecommendation, 'cli' | 'json'>): string {\n const parts = [`vercel firewall rules add ${JSON.stringify(r.name)}`]\n r.groups.forEach((group, i) => {\n if (i > 0) parts.push(' --or')\n for (const c of group) parts.push(` --condition ${shellQuote(c)}`)\n })\n parts.push(` --action ${r.action}`)\n if (r.action === 'rate_limit' && r.rateLimit) {\n parts.push(` --rate-limit-window ${r.rateLimit.window}`)\n parts.push(` --rate-limit-requests ${r.rateLimit.requests}`)\n parts.push(` --rate-limit-action ${r.rateLimit.action}`)\n for (const k of r.rateLimit.keys) parts.push(` --rate-limit-keys ${k}`)\n }\n parts.push(' --yes')\n return parts.join(' \\\\\\n')\n}\n\nfunction toJson(r: Omit<FirewallRecommendation, 'cli' | 'json'>): unknown {\n return {\n name: r.name,\n conditionGroup: r.groups.map((conditions) => ({ conditions })),\n action: { mitigate: { action: r.action } }\n }\n}\n\nfunction finish(r: Omit<FirewallRecommendation, 'cli' | 'json'>): FirewallRecommendation {\n return { ...r, cli: toCli(r), json: toJson(r) }\n}\n\nfunction median(ns: number[]): number {\n if (!ns.length) return 0\n const s = [...ns].sort((a, b) => a - b)\n const mid = Math.floor(s.length / 2)\n return s.length % 2 ? s[mid]! : (s[mid - 1]! + s[mid]!) / 2\n}\n\n/* -------------------------------------------------------------------------- */\n\n/**\n * Turn observations into staged WAF proposals.\n *\n * @example\n * ```ts\n * const rules = recommendFirewallRules(observations)\n * for (const r of rules) {\n * console.log(`# ${r.name} — ${r.rationale}`)\n * console.log(`# evidence: ${r.evidence}`)\n * console.log(r.cli)\n * }\n * ```\n */\nexport function recommendFirewallRules(\n observations: readonly TrafficObservation[],\n opts: RecommendOptions = {}\n): FirewallRecommendation[] {\n const out: FirewallRecommendation[] = []\n\n /* 1. Protect the traffic you want, first and above everything else. --------\n Rules are evaluated top to bottom, so this has to be rule #1 or a later\n user-agent rule will swallow the agents that bring you readers. */\n if (!opts.omitProtectiveBypass) {\n const wanted = observations.filter((o) => o.intent === 'retrieval' || o.intent === 'search')\n const requests = wanted.reduce((n, o) => n + o.requests, 0)\n const names = [...new Set(wanted.map((o) => o.botName))]\n out.push(\n finish({\n name: 'Allow retrieval and search agents',\n rationale:\n 'Retrieval agents and search crawlers must never be caught by the rules below — they bring readers and rankings.',\n evidence: requests\n ? `${requests.toLocaleString('en-US')} observed requests across ${names.length} vendors (${names.slice(0, 6).join(', ')})`\n : 'no retrieval or search traffic observed yet; installed pre-emptively',\n groups: [\n [\n {\n type: 'user_agent',\n op: 'inc',\n value: [\n 'ChatGPT-User',\n 'OAI-SearchBot',\n 'Claude-User',\n 'Claude-SearchBot',\n 'Perplexity-User',\n 'Googlebot',\n 'bingbot',\n 'DuckDuckBot',\n 'Applebot'\n ]\n }\n ]\n ],\n action: 'bypass',\n eventual: 'bypass',\n risk: 'low',\n caveat:\n 'Place this rule first (`vercel firewall rules reorder ... --first`). A user-agent allowlist is spoofable, so pair with verification in middleware rather than relying on it for security — its job here is to stop your own rules misfiring.'\n })\n )\n }\n\n /* 2. Failed verification — the only class that earns a denial. ------------- */\n const spoofed = observations.filter((o) => o.verification === 'spoofed')\n const spoofedIps = [...new Set(spoofed.map((o) => o.ip).filter((v): v is string => !!v))]\n if (spoofedIps.length) {\n const requests = spoofed.reduce((n, o) => n + o.requests, 0)\n const vendors = [...new Set(spoofed.map((o) => o.botName))]\n out.push(\n finish({\n name: 'Deny impersonated crawler identities',\n rationale:\n 'These addresses claimed a crawler identity that failed verification against the vendor’s published ranges or signature.',\n evidence: `${requests.toLocaleString('en-US')} requests from ${spoofedIps.length} address${spoofedIps.length === 1 ? '' : 'es'} impersonating ${vendors.join(', ')}`,\n groups: [[{ type: 'ip_address', op: 'inc', value: spoofedIps }]],\n action: 'log',\n eventual: 'deny',\n risk: 'low',\n caveat:\n 'Verification failure is strong evidence, but confirm your edge controls x-forwarded-for before enforcing — behind a proxy that forwards a client-supplied header the verdict is worthless.'\n })\n )\n }\n\n /* 3. Single addresses behaving like scrapers. ------------------------------ */\n const perIp = observations.filter((o) => o.ip && o.verification !== 'verified')\n const threshold =\n opts.abuseThreshold ?? Math.max(500, Math.round(median(perIp.map((o) => o.requests)) * 10))\n const heavy = perIp\n .filter((o) => o.requests >= threshold)\n .sort((a, b) => b.requests - a.requests)\n .slice(0, 50)\n if (heavy.length) {\n const sweeping = heavy.filter((o) => (o.distinctPaths ?? 0) > 100)\n out.push(\n finish({\n name: 'Rate limit high-volume unverified addresses',\n rationale:\n 'A single address making orders of magnitude more requests than the median, with no verified identity.',\n evidence:\n `${heavy.length} address${heavy.length === 1 ? '' : 'es'} above ${threshold.toLocaleString('en-US')} requests` +\n (sweeping.length\n ? `; ${sweeping.length} swept >100 distinct paths, which reads as a scrape rather than a reader`\n : ''),\n groups: [[{ type: 'ip_address', op: 'inc', value: heavy.map((o) => o.ip!) }]],\n action: 'log',\n eventual: 'rate_limit',\n rateLimit: { window: 60, requests: 60, action: 'rate_limit', keys: ['ip'] },\n risk: 'medium',\n caveat:\n 'Shared egress means one address can front many real users — a corporate NAT, a mobile carrier, or a VPN. Review the dashboard before enforcing.'\n })\n )\n }\n\n /* 4. Training crawlers: bound the cost, do not disappear from corpora. ----- */\n const training = observations.filter((o) => o.intent === 'training')\n if (training.length) {\n const requests = training.reduce((n, o) => n + o.requests, 0)\n const vendors = [...new Set(training.map((o) => o.botName))]\n const budget = opts.trainingBudget ?? { window: 3600, requests: 600 }\n out.push(\n finish({\n name: 'Rate limit training crawlers',\n rationale:\n 'Bound what bulk corpus collection costs you without removing yourself from training sets.',\n evidence: `${requests.toLocaleString('en-US')} training requests from ${vendors.length} vendors (${vendors.slice(0, 6).join(', ')})`,\n groups: [\n [\n {\n type: 'user_agent',\n op: 'inc',\n value: ['GPTBot', 'ClaudeBot', 'CCBot', 'Bytespider', 'Amazonbot', 'meta-externalagent']\n }\n ]\n ],\n action: 'log',\n eventual: 'rate_limit',\n rateLimit: { window: budget.window, requests: budget.requests, action: 'rate_limit', keys: ['ip'] },\n risk: 'medium',\n caveat:\n 'Denying these removes you from future training sets, which may be exactly wrong for discoverability. Rate limit rather than deny unless you have decided otherwise. Note Vercel counters are per region, so N regions can collectively exceed the limit by ~Nx.'\n })\n )\n }\n\n /* 5. Datacenter ASNs presenting browser user agents. ---------------------- */\n const headlessAsns = [\n ...new Set(\n observations\n .filter((o) => o.asn !== undefined && /Mozilla|Chrome|Safari/i.test(o.userAgent))\n .filter((o) => o.verification !== 'verified')\n .map((o) => o.asn!)\n )\n ]\n if (headlessAsns.length) {\n const slices = observations.filter((o) => o.asn !== undefined && headlessAsns.includes(o.asn))\n const requests = slices.reduce((n, o) => n + o.requests, 0)\n out.push(\n finish({\n name: 'Challenge browser user agents from datacenter networks',\n rationale:\n 'A browser user agent arriving from a hosting network is automation wearing a costume — real browsers come from consumer ISPs.',\n evidence: `${requests.toLocaleString('en-US')} requests across ${headlessAsns.length} datacenter AS numbers`,\n groups: [\n [\n { type: 'geo_as_number', op: 'inc', value: headlessAsns },\n { type: 'user_agent', op: 'sub', value: 'Mozilla' }\n ]\n ],\n action: 'log',\n eventual: 'challenge',\n risk: 'high',\n caveat:\n 'Highest false-positive risk here. Corporate VPNs, privacy relays and some mobile carriers egress from hosting ASNs, and a challenge page breaks API clients and link unfurlers outright. Keep this in log mode for a full week before considering enforcement.'\n })\n )\n }\n\n return out\n}\n\n/** Render recommendations as a runnable, commented shell script. */\nexport function firewallScript(recommendations: readonly FirewallRecommendation[]): string {\n const lines = [\n '#!/usr/bin/env bash',\n '# Vercel WAF proposals generated from observed agent traffic.',\n '#',\n '# Every rule starts in LOG mode and blocks nothing. Vercel stages rule',\n '# changes as drafts, so nothing is live until you run:',\n '#',\n '# vercel firewall diff # review',\n '# vercel firewall publish --yes # go live',\n '#',\n '# Review each rule in the dashboard before promoting it to its eventual',\n '# action. Rules evaluate top to bottom, so keep the bypass rule first.',\n 'set -euo pipefail',\n ''\n ]\n recommendations.forEach((r, i) => {\n lines.push(`# ${i + 1}. ${r.name}`)\n lines.push(`# why: ${r.rationale}`)\n lines.push(`# evidence: ${r.evidence}`)\n lines.push(`# risk: ${r.risk} — eventual action: ${r.eventual}`)\n if (r.caveat) lines.push(`# caveat: ${r.caveat}`)\n lines.push(r.cli)\n lines.push('')\n })\n if (recommendations.length) {\n lines.push('# Keep the protective allow rule at the top of the evaluation order.')\n lines.push(\n `vercel firewall rules reorder ${JSON.stringify(recommendations[0]!.name)} --first --yes`\n )\n lines.push('')\n lines.push('vercel firewall diff')\n lines.push('echo \"Review above, then: vercel firewall publish --yes\"')\n }\n return lines.join('\\n')\n}\n"]}
@@ -0,0 +1,317 @@
1
+ import { b as AgentDecision, c as AgentPolicyOptions } from './policy-B3AakjOJ.cjs';
2
+ import { B as BotVerificationLike } from './types-Dw43eu7D.cjs';
3
+
4
+ /**
5
+ * Charge for training crawls. **EXPERIMENTAL.**
6
+ *
7
+ * The protocols this speaks are weeks old and moving. x402 and MPP are both
8
+ * live but their specs are unstable, MPP's settlement-confirmation header was
9
+ * not pinned publicly at the time of writing, and no agent in our production
10
+ * traffic has yet presented a payment credential. Expect this API to change
11
+ * without a major version while that settles — everything else in the package
12
+ * is stable, this is not.
13
+ *
14
+ * Today the industry's answer to bulk AI crawling is `Disallow` — over 2.5
15
+ * million sites block AI training in robots.txt. That leaves money on the
16
+ * table and depends on the crawler's goodwill to work at all.
17
+ *
18
+ * The alternative is to let them train and price it. That only works if you
19
+ * can tell training from retrieval, because they have opposite economics: a
20
+ * `GPTBot` fetch is corpus collection you get nothing back for, while a
21
+ * `ChatGPT-User` fetch is a person asking about you — charging for the second
22
+ * is charging for your own distribution. {@link agentPolicy} draws that line;
23
+ * this module turns a `'charge'` decision into the HTTP challenge.
24
+ *
25
+ * Two protocols, one status code. Both settle at the HTTP layer and both use
26
+ * 402, but the framing differs:
27
+ *
28
+ * x402 PAYMENT-REQUIRED: <base64 JSON> -> PAYMENT-SIGNATURE
29
+ * MPP WWW-Authenticate: Payment id="…" -> Authorization: Payment …
30
+ *
31
+ * MPP reuses standard HTTP authentication framing; x402 defines its own
32
+ * headers. They do not collide, so a single 402 can advertise both and let the
33
+ * agent pick — which is what {@link paymentRequired} does when given both.
34
+ *
35
+ * Scope: this emits the 402 and reads the client's payment header. It does not
36
+ * settle anything. Settlement belongs to an x402 facilitator or Stripe's MPP —
37
+ * a library that held money would inherit PCI scope and stop being something
38
+ * you can drop into middleware.
39
+ */
40
+
41
+ /**
42
+ * One way a client may pay. Field names follow x402's `PaymentRequirements`;
43
+ * values are yours — the library never invents an amount, network or asset.
44
+ */
45
+ interface PaymentRequirements {
46
+ scheme: string;
47
+ network: string;
48
+ maxAmountRequired: string;
49
+ resource: string;
50
+ description?: string;
51
+ mimeType?: string;
52
+ payTo: string;
53
+ maxTimeoutSeconds?: number;
54
+ asset: string;
55
+ extra?: Record<string, unknown>;
56
+ }
57
+ /** Which settlement protocol a challenge speaks. */
58
+ type PaymentProtocol = 'x402' | 'mpp';
59
+ /** x402: base64 JSON in a `PAYMENT-REQUIRED` header. */
60
+ interface X402Challenge {
61
+ protocol: 'x402';
62
+ /** Accepted payment methods, in preference order. At least one. */
63
+ accepts: readonly PaymentRequirements[];
64
+ /** Protocol version. Defaults to 1. */
65
+ x402Version?: number;
66
+ }
67
+ /**
68
+ * MPP: an RFC 9110 `WWW-Authenticate: Payment` challenge.
69
+ *
70
+ * Field values are yours. `request` carries the encoded challenge payload your
71
+ * MPP provider generates — the library does not construct or price it.
72
+ */
73
+ interface MppChallenge {
74
+ protocol: 'mpp';
75
+ /** Challenge identifier. */
76
+ id: string;
77
+ /** Authentication realm. */
78
+ realm: string;
79
+ /** Payment method, e.g. `'tempo'`. */
80
+ method: string;
81
+ /** Transaction intent, e.g. `'charge'`. */
82
+ intent?: string;
83
+ /** Encoded challenge data from your provider. */
84
+ request?: string;
85
+ }
86
+ type PaymentChallenge = X402Challenge | MppChallenge;
87
+ interface PaymentChallengeOptions {
88
+ /**
89
+ * Challenges to advertise. Supplying both an x402 and an MPP challenge is
90
+ * valid and usually correct: they use non-colliding headers, so one 402 can
91
+ * offer both and the agent takes whichever it speaks.
92
+ */
93
+ challenges: readonly PaymentChallenge[];
94
+ /**
95
+ * `Content-Signal` to send with the challenge. Defaults to
96
+ * `search=yes, ai-input=yes, ai-train=paid` — the whole point being that
97
+ * training is available rather than forbidden.
98
+ */
99
+ contentSignal?: string;
100
+ /** Extra response headers. */
101
+ headers?: Record<string, string>;
102
+ /** Human-readable body. Agents read the header; people read logs. */
103
+ body?: string;
104
+ }
105
+ /**
106
+ * Build a 402 challenge.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * const decision = agentPolicy(req, { onTraining: 'charge' })
111
+ * if (decision.action === 'charge') {
112
+ * return paymentRequired({
113
+ * challenges: [
114
+ * {
115
+ * protocol: 'x402',
116
+ * accepts: [{
117
+ * scheme: 'exact',
118
+ * network: 'base',
119
+ * maxAmountRequired: '1000', // your price, your units
120
+ * resource: req.url,
121
+ * payTo: process.env.WALLET!,
122
+ * asset: process.env.USDC!
123
+ * }]
124
+ * },
125
+ * { protocol: 'mpp', id: challengeId, realm: 'example.com', method: 'tempo', intent: 'charge' }
126
+ * ]
127
+ * })
128
+ * }
129
+ * ```
130
+ */
131
+ declare function paymentRequired(opts: PaymentChallengeOptions): Response;
132
+ /** A payment credential the client sent back, and which protocol it speaks. */
133
+ interface SubmittedPayment {
134
+ protocol: PaymentProtocol;
135
+ /** Raw header value, for handing to a facilitator. */
136
+ value: string;
137
+ }
138
+ /**
139
+ * Read the client's payment credential, whichever protocol it used.
140
+ *
141
+ * x402 sends `PAYMENT-SIGNATURE`; MPP sends `Authorization: Payment …`. The
142
+ * `Payment` scheme check matters — a site behind normal auth will also have a
143
+ * Bearer or Basic `Authorization` header, and mistaking that for a payment
144
+ * would be a security-relevant confusion.
145
+ */
146
+ declare function paymentPayload(req: Request): SubmittedPayment | null;
147
+ /**
148
+ * True when the client attached a payment credential — i.e. this is the retry
149
+ * after a 402, not a fresh unpaid request.
150
+ *
151
+ * Presence is not proof. Hand the value to your facilitator to verify and
152
+ * settle; only then serve the resource.
153
+ */
154
+ declare function hasPaymentPayload(req: Request): boolean;
155
+ /**
156
+ * Attach a facilitator's settlement result to a successful response.
157
+ *
158
+ * x402 defines `PAYMENT-RESPONSE` for this. MPP's public spec did not pin a
159
+ * settlement-confirmation header at the time of writing, so pass `header` to
160
+ * name whatever your provider expects rather than have the library guess.
161
+ */
162
+ declare function withSettlement(res: Response, settlement: unknown, opts?: {
163
+ header?: string;
164
+ }): Response;
165
+ /**
166
+ * Convenience: turn an {@link AgentDecision} straight into a response, or
167
+ * `null` when the request should simply be served.
168
+ *
169
+ * Returns 403 for `'block'`, a 402 challenge for `'charge'`, and `null` for
170
+ * `'allow'` and `'meter'` — metering is an accounting concern, not a gate, so
171
+ * the request still gets served while `trackVisit` records it.
172
+ */
173
+ declare function respondToDecision(decision: AgentDecision, opts: PaymentChallengeOptions): Response | null;
174
+
175
+ /**
176
+ * The paid-access gate: policy decides *whether* to charge, a gateway decides
177
+ * *how*. **EXPERIMENTAL** — see `payments.ts`. The classification and policy
178
+ * layers underneath are stable; the payment surface is not.
179
+ *
180
+ * The split matters. We own classification — telling a training crawl from a
181
+ * retrieval fetch, which is the part nobody else does and the part that makes
182
+ * charging sane. Settlement is somebody else's job: Stripe's MPP SDK, an x402
183
+ * facilitator, whatever comes next. A library that held money would inherit PCI
184
+ * scope and stop being something you drop into middleware.
185
+ *
186
+ * So gateways are injected, exactly like analytics adapters, and this module
187
+ * takes no dependency on Stripe or any chain.
188
+ */
189
+
190
+ /**
191
+ * Outcome of handing a request to a payment gateway.
192
+ *
193
+ * - `challenge` — respond with this. The client has not paid.
194
+ * - `paid` — settled; serve the resource. `receipt` decorates the response with
195
+ * whatever proof the protocol expects.
196
+ */
197
+ type GatewayResult = {
198
+ status: 'challenge';
199
+ response: Response;
200
+ } | {
201
+ status: 'paid';
202
+ receipt?: (res: Response) => Response;
203
+ };
204
+ interface PaymentGateway {
205
+ handle(req: Request): Promise<GatewayResult>;
206
+ }
207
+ /**
208
+ * Wrap Stripe's MPP SDK.
209
+ *
210
+ * `Mppx.compose(...)` returns a handler that either yields a 402 with a
211
+ * `.challenge` response, or a settled result with `.withReceipt(res)`. This
212
+ * adapts that shape without importing it — pass the composed handler in.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * const mppx = Mppx.create({ methods: [...], secretKey })
217
+ * const handler = Mppx.compose(
218
+ * mppx.tempo.charge({ amount: '0.01', recipient }),
219
+ * mppx.stripe.charge({ amount: '0.50', currency: 'usd' })
220
+ * )
221
+ * const gateway = mppxGateway(handler)
222
+ * ```
223
+ */
224
+ declare function mppxGateway(handler: (req: Request) => Promise<MppxResponse> | MppxResponse): PaymentGateway;
225
+ /** The subset of Stripe's MPP response we rely on. Structural, not imported. */
226
+ interface MppxResponse {
227
+ status: number;
228
+ challenge: Response;
229
+ withReceipt?: (res: Response) => Response;
230
+ }
231
+ interface X402GatewayOptions extends PaymentChallengeOptions {
232
+ /**
233
+ * Verify and settle a `PAYMENT-SIGNATURE` payload with your facilitator.
234
+ * Resolve truthy to serve the resource, falsy to re-challenge.
235
+ */
236
+ settle: (payload: string, req: Request) => Promise<boolean> | boolean;
237
+ /** Attach the facilitator's settlement result to the served response. */
238
+ receipt?: (res: Response) => Response;
239
+ }
240
+ /**
241
+ * Gateway using this library's own challenge builder plus a facilitator you
242
+ * supply. For x402, or for MPP if you are not using Stripe's SDK.
243
+ */
244
+ declare function x402Gateway(opts: X402GatewayOptions): PaymentGateway;
245
+ /** One unit of billable agent traffic. */
246
+ interface MeterRecord {
247
+ decision: AgentDecision;
248
+ /** Units consumed. One request is one unit unless you price by bytes or tokens. */
249
+ units: number;
250
+ path: string;
251
+ method: string;
252
+ }
253
+ /**
254
+ * Where billable usage goes.
255
+ *
256
+ * Metering is the model to ship first: it needs no crawler cooperation, works
257
+ * today, and produces the number you would negotiate a licence with. Charging
258
+ * per request is what the protocols define but not what a training sweep can
259
+ * actually do — no crawler in the wild retries a 402.
260
+ */
261
+ interface Meter {
262
+ record(entry: MeterRecord): Promise<void> | void;
263
+ }
264
+ interface PaymentGateOptions extends Omit<AgentPolicyOptions, 'verify'> {
265
+ gateway: PaymentGateway;
266
+ /**
267
+ * Sink for billable traffic. Called for every `'meter'` decision — serve the
268
+ * request, count it, bill out of band.
269
+ *
270
+ * Errors are swallowed: a metering failure must not turn into a failed
271
+ * response, for the same reason analytics failures do not.
272
+ */
273
+ meter?: Meter;
274
+ /**
275
+ * Identity verifier, sync or async. Unlike {@link agentPolicy}'s option this
276
+ * accepts a promise, because `paymentGate` is already async and can await it.
277
+ * That matters: `combinedVerifier()` and `webBotAuthVerifier()` are async by
278
+ * necessity — Web Bot Auth fetches the signer's key directory — so without
279
+ * this they could not be used with policy or payments at all.
280
+ */
281
+ verify?: (req: Request) => BotVerificationLike | Promise<BotVerificationLike>;
282
+ /**
283
+ * Called for every decision, paid or not — wire it to your metering so
284
+ * `'meter'` traffic is actually counted rather than merely allowed.
285
+ */
286
+ onDecision?: (decision: AgentDecision) => void;
287
+ }
288
+ /**
289
+ * Full gate: classify, decide, and either let the request through or return the
290
+ * response it should get instead.
291
+ *
292
+ * Returns `null` when the request should be served normally. That covers
293
+ * `'allow'`, `'meter'` (accounting, not a gate) and any request that has already
294
+ * paid — in which case `receipt` is handed back so you can decorate the response
295
+ * you were going to send anyway.
296
+ *
297
+ * @example
298
+ * ```ts
299
+ * const gate = await paymentGate(req, {
300
+ * onTraining: 'charge',
301
+ * verify: combinedVerifier(),
302
+ * gateway: mppxGateway(handler),
303
+ * onDecision: (d) => void trackVisit(req, { analytics, properties: { action: d.action } })
304
+ * })
305
+ * if (gate.response) return gate.response
306
+ * return gate.decorate(await serve(req))
307
+ * ```
308
+ */
309
+ declare function paymentGate(req: Request, opts: PaymentGateOptions): Promise<{
310
+ decision: AgentDecision;
311
+ /** Respond with this instead of serving, when set. */
312
+ response: Response | null;
313
+ /** Wrap the response you were going to send. Identity when nothing to add. */
314
+ decorate: (res: Response) => Response;
315
+ }>;
316
+
317
+ export { type GatewayResult as G, type Meter as M, type PaymentChallenge as P, type SubmittedPayment as S, type X402Challenge as X, type MeterRecord as a, type MppChallenge as b, type MppxResponse as c, type PaymentChallengeOptions as d, type PaymentGateOptions as e, type PaymentGateway as f, type PaymentProtocol as g, type PaymentRequirements as h, type X402GatewayOptions as i, hasPaymentPayload as j, paymentPayload as k, paymentRequired as l, mppxGateway as m, paymentGate as p, respondToDecision as r, withSettlement as w, x402Gateway as x };