forgemesh-pay-per-crawl 0.1.2__tar.gz
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.
- forgemesh_pay_per_crawl-0.1.2/LICENSE +21 -0
- forgemesh_pay_per_crawl-0.1.2/PKG-INFO +157 -0
- forgemesh_pay_per_crawl-0.1.2/README.md +121 -0
- forgemesh_pay_per_crawl-0.1.2/pyproject.toml +47 -0
- forgemesh_pay_per_crawl-0.1.2/setup.cfg +4 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/__init__.py +3 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/asgi.py +64 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/__init__.py +42 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/catalog.py +97 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/classifier.py +143 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/config.py +100 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/discovery.py +131 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/gate.py +233 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/http.py +35 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/keccak.py +74 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/locks.py +77 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/pro.py +223 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/core/x402.py +351 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/django.py +73 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/py.typed +0 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl/wsgi.py +59 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl.egg-info/PKG-INFO +157 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl.egg-info/SOURCES.txt +31 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl.egg-info/dependency_links.txt +1 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl.egg-info/requires.txt +12 -0
- forgemesh_pay_per_crawl-0.1.2/src/forgemesh_pay_per_crawl.egg-info/top_level.txt +1 -0
- forgemesh_pay_per_crawl-0.1.2/tests/test_adapters.py +109 -0
- forgemesh_pay_per_crawl-0.1.2/tests/test_classifier.py +86 -0
- forgemesh_pay_per_crawl-0.1.2/tests/test_gate.py +242 -0
- forgemesh_pay_per_crawl-0.1.2/tests/test_integration.py +138 -0
- forgemesh_pay_per_crawl-0.1.2/tests/test_keccak.py +28 -0
- forgemesh_pay_per_crawl-0.1.2/tests/test_locks_pro.py +99 -0
- forgemesh_pay_per_crawl-0.1.2/tests/test_x402.py +171 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ForgeMesh Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forgemesh-pay-per-crawl
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Charge AI crawlers and bots per page with x402 USDC micropayments on Base. ASGI, WSGI and Django middleware; you keep 100%.
|
|
5
|
+
Author-email: ForgeMesh Labs <hello@forgemesh.io>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://forgemesh.io
|
|
8
|
+
Project-URL: Repository, https://github.com/forgemeshlabs/pay-per-crawl-py
|
|
9
|
+
Project-URL: Issues, https://github.com/forgemeshlabs/pay-per-crawl-py/issues
|
|
10
|
+
Project-URL: PHP-WordPress-edition, https://github.com/forgemeshlabs/forgemesh-pay-per-crawl
|
|
11
|
+
Keywords: x402,mpp,pay-per-crawl,402,ai-crawlers,usdc,base,fastapi,flask,django,asgi,wsgi
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pycryptodome>=3.20
|
|
26
|
+
Provides-Extra: httpx
|
|
27
|
+
Requires-Dist: httpx>=0.27; extra == "httpx"
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
30
|
+
Requires-Dist: httpx>=0.27; extra == "test"
|
|
31
|
+
Requires-Dist: fastapi>=0.110; extra == "test"
|
|
32
|
+
Requires-Dist: uvicorn>=0.29; extra == "test"
|
|
33
|
+
Requires-Dist: flask>=3; extra == "test"
|
|
34
|
+
Requires-Dist: django>=4.2; extra == "test"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# ForgeMesh Pay-per-Crawl for Python
|
|
38
|
+
|
|
39
|
+
Charge AI crawlers and bots per page with [x402](https://x402.org) USDC micropayments on Base, paid straight to your wallet. Humans and verified search engines read free. Middleware for **FastAPI / Starlette (ASGI)**, **Flask and any WSGI app**, and **Django**, on one framework-free core.
|
|
40
|
+
|
|
41
|
+
This is the Python edition of the [WordPress plugin](https://github.com/forgemeshlabs/forgemesh-pay-per-crawl); behaviour is identical.
|
|
42
|
+
|
|
43
|
+
- **Humans see nothing different.** Browsers get the page as always.
|
|
44
|
+
- **Verified search engines stay free.** Googlebot, Bingbot, DuckDuckBot, Applebot and others are checked by reverse DNS, so SEO is untouched. A crawler that only *claims* to be Googlebot is treated as a script.
|
|
45
|
+
- **AI crawlers, SEO crawlers and scripts get `402 Payment Required`** with an x402 challenge (v2 `PAYMENT-REQUIRED` header + v1 JSON body) and an MPP `WWW-Authenticate: Payment` challenge. They pay USDC on Base and retry; the page is served only after the payment settles on-chain.
|
|
46
|
+
- **You keep 100%.** The payer's USDC goes directly to your wallet. No custody, no account, no API key, no revenue share.
|
|
47
|
+
- **Discovery built in.** Serves `/.well-known/x402.json`, `/llms.txt` and `/agents.txt`.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
pip install forgemesh-pay-per-crawl
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Python 3.10+. One dependency (`pycryptodome`, for keccak256). `httpx` is used for facilitator calls if installed, otherwise the standard library. Start with `mode="monitor"` (records what bots would have paid, never blocks), then switch to `mode="gate"`.
|
|
56
|
+
|
|
57
|
+
## FastAPI / Starlette
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from forgemesh_pay_per_crawl import Config
|
|
61
|
+
from forgemesh_pay_per_crawl.asgi import PayPerCrawlMiddleware
|
|
62
|
+
|
|
63
|
+
app.add_middleware(PayPerCrawlMiddleware,
|
|
64
|
+
config=Config(pay_to="0xYourWalletOnBase", mode="gate", price_usd=0.01, base_url="https://example.com"))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Flask (and any WSGI app)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from forgemesh_pay_per_crawl import Config
|
|
71
|
+
from forgemesh_pay_per_crawl.wsgi import PayPerCrawlWSGI
|
|
72
|
+
|
|
73
|
+
app.wsgi_app = PayPerCrawlWSGI(app.wsgi_app,
|
|
74
|
+
Config(pay_to="0xYourWalletOnBase", mode="gate", price_usd=0.01, base_url="https://example.com"))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Django
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# settings.py
|
|
81
|
+
MIDDLEWARE = ["forgemesh_pay_per_crawl.django.PayPerCrawlMiddleware", *MIDDLEWARE] # near the top
|
|
82
|
+
PAY_PER_CRAWL = {"pay_to": "0xYourWalletOnBase", "mode": "gate", "price_usd": 0.01, "base_url": "https://example.com"}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Settings
|
|
86
|
+
|
|
87
|
+
All are `Config(...)` keyword arguments (or keys of `PAY_PER_CRAWL`).
|
|
88
|
+
|
|
89
|
+
| Setting | Default | Meaning |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `pay_to` | required in gate mode | Your `0x` wallet on Base. |
|
|
92
|
+
| `mode` | `"monitor"` | `"monitor"` records only; `"gate"` answers 402. |
|
|
93
|
+
| `price_usd` | `0.01` | Price per page fetch, paid in USDC. |
|
|
94
|
+
| `facilitator_url` | PayAI (`https://facilitator.payai.network`, no key) | Any x402 facilitator that supports Base. `facilitator_auth` sets an `Authorization` header if yours needs one. |
|
|
95
|
+
| `class_policy` | search/social/monitor free; seo/ai/automation/bot paid | Per class: `"free"`, `"paid"` or `"block"`. |
|
|
96
|
+
| `bot_overrides` | `{}` | Per bot name, e.g. `{"GPTBot": "free", "Bytespider": "block"}`. |
|
|
97
|
+
| `allow_paths` | `[]` | Path prefixes that are never gated. `/robots.txt`, `/sitemap*`, `/llms.txt`, `/agents.txt`, `/.well-known/`, `/favicon.ico` always are. |
|
|
98
|
+
| `base_url` | from the request | Your public origin. Set it behind a proxy or load balancer. |
|
|
99
|
+
| `trust_proxy_header` | `""` | Header with the real client IP (`"CF-Connecting-IP"`, `"X-Forwarded-For"`) so search-engine verification sees the right address. |
|
|
100
|
+
| `mpp_secret` | random per process | HMAC key for MPP challenge ids. Set a stable value if you run more than one worker. |
|
|
101
|
+
| `gated_methods` | `("GET", "HEAD")` | HEAD is gated like GET (402 with no body). |
|
|
102
|
+
| `site_name`, `site_description`, `pages` | | Used in the discovery files; `pages` is a list (or callable returning one) of `(title, url)` for `llms.txt`. |
|
|
103
|
+
| `on_event` | `None` | Callback receiving a dict for every classified request (status, bot, class, rail, tx hash, payer). |
|
|
104
|
+
|
|
105
|
+
Bot classes: `human`, `search`, `social`, `monitor`, `seo`, `ai`, `automation`, `bot`. The catalog covers 85 crawlers and tools; see `core/catalog.py`.
|
|
106
|
+
|
|
107
|
+
Multiple worker processes: pass `lock=FileNonceLock("/var/tmp/fmppc")` to `Gate(...)` (and use `PayPerCrawlMiddleware(app, gate=gate)`) so a payment authorization cannot be replayed on another worker, and set a stable `mpp_secret`. The default lock is in-process memory.
|
|
108
|
+
|
|
109
|
+
### Behind Cloudflare
|
|
110
|
+
|
|
111
|
+
If your site is proxied by Cloudflare, set **AI Crawl Control** (and any Bot Fight Mode / AI Labyrinth / "Block AI bots" rule) to **Allow** for the crawlers you want to charge. Cloudflare answers those requests at the edge before they ever reach your app, so the gate would never see them. Set `trust_proxy_header="CF-Connecting-IP"` and `base_url` to your public origin.
|
|
112
|
+
|
|
113
|
+
## How a payment works
|
|
114
|
+
|
|
115
|
+
1. A bot requests a page and gets `402` with the price, asset (USDC on Base) and your wallet, in the `PAYMENT-REQUIRED` header (x402 v2), the JSON body (x402 v1, with `invalidReason` when a payment was rejected) and `WWW-Authenticate: Payment` (MPP).
|
|
116
|
+
2. The bot signs a USDC transfer authorization (EIP-3009) and retries with it in `PAYMENT-SIGNATURE` (v2), `X-PAYMENT` (v1) or `Authorization: Payment` (MPP).
|
|
117
|
+
3. The middleware checks recipient, amount and expiry locally, locks the authorization nonce against replay, then posts it to the **facilitator** (`/verify`, then `/settle`). The facilitator verifies and submits it on-chain; it never holds funds. v1 payments are forwarded in v1 shape.
|
|
118
|
+
4. The page is served only when a transaction hash comes back, with the receipt in `PAYMENT-RESPONSE`, `X-PAYMENT-RESPONSE` and (MPP) `Payment-Receipt`.
|
|
119
|
+
|
|
120
|
+
The package never contacts ForgeMesh. Its only outbound requests are to the facilitator you chose, and only when a payment is presented. (The optional Pro reporter below is the one exception, and is off without a key.)
|
|
121
|
+
|
|
122
|
+
## Money flow
|
|
123
|
+
|
|
124
|
+
The payer's USDC moves wallet to wallet on Base, from the payer to the `pay_to` address you configured. There is no ForgeMesh account, no custody, no settlement service and no revenue share: the site owner keeps 100%. The facilitator (PayAI by default) charges the seller no fee at the time of writing; check your facilitator's terms.
|
|
125
|
+
|
|
126
|
+
## Using the pieces directly
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from forgemesh_pay_per_crawl import Classifier, Config, Gate, Request, X402, build_challenge
|
|
130
|
+
|
|
131
|
+
Classifier().classify("Mozilla/5.0 (compatible; GPTBot/1.2)").cls # "ai"
|
|
132
|
+
gate = Gate(Config(pay_to="0x...", mode="gate"))
|
|
133
|
+
decision = gate.handle(Request(method="GET", path="/x", headers={"user-agent": "curl/8"}, host="example.com"))
|
|
134
|
+
decision.response # a Response (status, headers, body) to send, or None to serve the page
|
|
135
|
+
decision.headers # headers to add to the page you serve (payment receipt, Link)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`Gate.handle` is blocking (it can call the facilitator); in async code run it in a thread, as the ASGI middleware does. The facilitator transport is injectable (`Gate(config, transport=fn)` with `fn(url, body, headers) -> dict`).
|
|
139
|
+
|
|
140
|
+
## Pro (optional)
|
|
141
|
+
|
|
142
|
+
With a ForgeMesh Pay-per-Crawl Pro license (`pro_key="fmppc_..."`) the package reports daily bot rollups and settlements to the hosted dashboard at proxy.forgemesh.io and pulls the crawler-intel feed into the classifier. Nothing is sent without a key. The rollups are held in memory per process, so with several workers each reports its own counts.
|
|
143
|
+
|
|
144
|
+
## Verified
|
|
145
|
+
|
|
146
|
+
- Unit and integration tests: `pytest` (keccak vectors incl. block-boundary cases, classifier with rDNS, 402 shapes, MPP challenge/credential round trip, facilitator flow, replay, HEAD, file lock, Pro sync, all three adapters, and an integration matrix against live uvicorn/FastAPI and Flask servers with a mock facilitator).
|
|
147
|
+
- Real settlements on Base mainnet through PayAI, October 2026, from the FastAPI example gated at $0.01: x402 v1 ([tx](https://basescan.org/tx/0x49f0f05b3509545e5134aa6cf091ff7eab4e4f1e1b9aef9bf28280c231e2a945)) and x402 v2 ([tx](https://basescan.org/tx/0x15d95ad90c9530e4816016f65681ddd4a79730373634c991c57afa2b88a8b44b)).
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
python -m venv .venv && .venv/bin/pip install -e ".[test]"
|
|
153
|
+
.venv/bin/pytest
|
|
154
|
+
python -m build
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
MIT. Made by [ForgeMesh Labs](https://forgemesh.io) (hello@forgemesh.io).
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# ForgeMesh Pay-per-Crawl for Python
|
|
2
|
+
|
|
3
|
+
Charge AI crawlers and bots per page with [x402](https://x402.org) USDC micropayments on Base, paid straight to your wallet. Humans and verified search engines read free. Middleware for **FastAPI / Starlette (ASGI)**, **Flask and any WSGI app**, and **Django**, on one framework-free core.
|
|
4
|
+
|
|
5
|
+
This is the Python edition of the [WordPress plugin](https://github.com/forgemeshlabs/forgemesh-pay-per-crawl); behaviour is identical.
|
|
6
|
+
|
|
7
|
+
- **Humans see nothing different.** Browsers get the page as always.
|
|
8
|
+
- **Verified search engines stay free.** Googlebot, Bingbot, DuckDuckBot, Applebot and others are checked by reverse DNS, so SEO is untouched. A crawler that only *claims* to be Googlebot is treated as a script.
|
|
9
|
+
- **AI crawlers, SEO crawlers and scripts get `402 Payment Required`** with an x402 challenge (v2 `PAYMENT-REQUIRED` header + v1 JSON body) and an MPP `WWW-Authenticate: Payment` challenge. They pay USDC on Base and retry; the page is served only after the payment settles on-chain.
|
|
10
|
+
- **You keep 100%.** The payer's USDC goes directly to your wallet. No custody, no account, no API key, no revenue share.
|
|
11
|
+
- **Discovery built in.** Serves `/.well-known/x402.json`, `/llms.txt` and `/agents.txt`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pip install forgemesh-pay-per-crawl
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python 3.10+. One dependency (`pycryptodome`, for keccak256). `httpx` is used for facilitator calls if installed, otherwise the standard library. Start with `mode="monitor"` (records what bots would have paid, never blocks), then switch to `mode="gate"`.
|
|
20
|
+
|
|
21
|
+
## FastAPI / Starlette
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from forgemesh_pay_per_crawl import Config
|
|
25
|
+
from forgemesh_pay_per_crawl.asgi import PayPerCrawlMiddleware
|
|
26
|
+
|
|
27
|
+
app.add_middleware(PayPerCrawlMiddleware,
|
|
28
|
+
config=Config(pay_to="0xYourWalletOnBase", mode="gate", price_usd=0.01, base_url="https://example.com"))
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Flask (and any WSGI app)
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from forgemesh_pay_per_crawl import Config
|
|
35
|
+
from forgemesh_pay_per_crawl.wsgi import PayPerCrawlWSGI
|
|
36
|
+
|
|
37
|
+
app.wsgi_app = PayPerCrawlWSGI(app.wsgi_app,
|
|
38
|
+
Config(pay_to="0xYourWalletOnBase", mode="gate", price_usd=0.01, base_url="https://example.com"))
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Django
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# settings.py
|
|
45
|
+
MIDDLEWARE = ["forgemesh_pay_per_crawl.django.PayPerCrawlMiddleware", *MIDDLEWARE] # near the top
|
|
46
|
+
PAY_PER_CRAWL = {"pay_to": "0xYourWalletOnBase", "mode": "gate", "price_usd": 0.01, "base_url": "https://example.com"}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Settings
|
|
50
|
+
|
|
51
|
+
All are `Config(...)` keyword arguments (or keys of `PAY_PER_CRAWL`).
|
|
52
|
+
|
|
53
|
+
| Setting | Default | Meaning |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `pay_to` | required in gate mode | Your `0x` wallet on Base. |
|
|
56
|
+
| `mode` | `"monitor"` | `"monitor"` records only; `"gate"` answers 402. |
|
|
57
|
+
| `price_usd` | `0.01` | Price per page fetch, paid in USDC. |
|
|
58
|
+
| `facilitator_url` | PayAI (`https://facilitator.payai.network`, no key) | Any x402 facilitator that supports Base. `facilitator_auth` sets an `Authorization` header if yours needs one. |
|
|
59
|
+
| `class_policy` | search/social/monitor free; seo/ai/automation/bot paid | Per class: `"free"`, `"paid"` or `"block"`. |
|
|
60
|
+
| `bot_overrides` | `{}` | Per bot name, e.g. `{"GPTBot": "free", "Bytespider": "block"}`. |
|
|
61
|
+
| `allow_paths` | `[]` | Path prefixes that are never gated. `/robots.txt`, `/sitemap*`, `/llms.txt`, `/agents.txt`, `/.well-known/`, `/favicon.ico` always are. |
|
|
62
|
+
| `base_url` | from the request | Your public origin. Set it behind a proxy or load balancer. |
|
|
63
|
+
| `trust_proxy_header` | `""` | Header with the real client IP (`"CF-Connecting-IP"`, `"X-Forwarded-For"`) so search-engine verification sees the right address. |
|
|
64
|
+
| `mpp_secret` | random per process | HMAC key for MPP challenge ids. Set a stable value if you run more than one worker. |
|
|
65
|
+
| `gated_methods` | `("GET", "HEAD")` | HEAD is gated like GET (402 with no body). |
|
|
66
|
+
| `site_name`, `site_description`, `pages` | | Used in the discovery files; `pages` is a list (or callable returning one) of `(title, url)` for `llms.txt`. |
|
|
67
|
+
| `on_event` | `None` | Callback receiving a dict for every classified request (status, bot, class, rail, tx hash, payer). |
|
|
68
|
+
|
|
69
|
+
Bot classes: `human`, `search`, `social`, `monitor`, `seo`, `ai`, `automation`, `bot`. The catalog covers 85 crawlers and tools; see `core/catalog.py`.
|
|
70
|
+
|
|
71
|
+
Multiple worker processes: pass `lock=FileNonceLock("/var/tmp/fmppc")` to `Gate(...)` (and use `PayPerCrawlMiddleware(app, gate=gate)`) so a payment authorization cannot be replayed on another worker, and set a stable `mpp_secret`. The default lock is in-process memory.
|
|
72
|
+
|
|
73
|
+
### Behind Cloudflare
|
|
74
|
+
|
|
75
|
+
If your site is proxied by Cloudflare, set **AI Crawl Control** (and any Bot Fight Mode / AI Labyrinth / "Block AI bots" rule) to **Allow** for the crawlers you want to charge. Cloudflare answers those requests at the edge before they ever reach your app, so the gate would never see them. Set `trust_proxy_header="CF-Connecting-IP"` and `base_url` to your public origin.
|
|
76
|
+
|
|
77
|
+
## How a payment works
|
|
78
|
+
|
|
79
|
+
1. A bot requests a page and gets `402` with the price, asset (USDC on Base) and your wallet, in the `PAYMENT-REQUIRED` header (x402 v2), the JSON body (x402 v1, with `invalidReason` when a payment was rejected) and `WWW-Authenticate: Payment` (MPP).
|
|
80
|
+
2. The bot signs a USDC transfer authorization (EIP-3009) and retries with it in `PAYMENT-SIGNATURE` (v2), `X-PAYMENT` (v1) or `Authorization: Payment` (MPP).
|
|
81
|
+
3. The middleware checks recipient, amount and expiry locally, locks the authorization nonce against replay, then posts it to the **facilitator** (`/verify`, then `/settle`). The facilitator verifies and submits it on-chain; it never holds funds. v1 payments are forwarded in v1 shape.
|
|
82
|
+
4. The page is served only when a transaction hash comes back, with the receipt in `PAYMENT-RESPONSE`, `X-PAYMENT-RESPONSE` and (MPP) `Payment-Receipt`.
|
|
83
|
+
|
|
84
|
+
The package never contacts ForgeMesh. Its only outbound requests are to the facilitator you chose, and only when a payment is presented. (The optional Pro reporter below is the one exception, and is off without a key.)
|
|
85
|
+
|
|
86
|
+
## Money flow
|
|
87
|
+
|
|
88
|
+
The payer's USDC moves wallet to wallet on Base, from the payer to the `pay_to` address you configured. There is no ForgeMesh account, no custody, no settlement service and no revenue share: the site owner keeps 100%. The facilitator (PayAI by default) charges the seller no fee at the time of writing; check your facilitator's terms.
|
|
89
|
+
|
|
90
|
+
## Using the pieces directly
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from forgemesh_pay_per_crawl import Classifier, Config, Gate, Request, X402, build_challenge
|
|
94
|
+
|
|
95
|
+
Classifier().classify("Mozilla/5.0 (compatible; GPTBot/1.2)").cls # "ai"
|
|
96
|
+
gate = Gate(Config(pay_to="0x...", mode="gate"))
|
|
97
|
+
decision = gate.handle(Request(method="GET", path="/x", headers={"user-agent": "curl/8"}, host="example.com"))
|
|
98
|
+
decision.response # a Response (status, headers, body) to send, or None to serve the page
|
|
99
|
+
decision.headers # headers to add to the page you serve (payment receipt, Link)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`Gate.handle` is blocking (it can call the facilitator); in async code run it in a thread, as the ASGI middleware does. The facilitator transport is injectable (`Gate(config, transport=fn)` with `fn(url, body, headers) -> dict`).
|
|
103
|
+
|
|
104
|
+
## Pro (optional)
|
|
105
|
+
|
|
106
|
+
With a ForgeMesh Pay-per-Crawl Pro license (`pro_key="fmppc_..."`) the package reports daily bot rollups and settlements to the hosted dashboard at proxy.forgemesh.io and pulls the crawler-intel feed into the classifier. Nothing is sent without a key. The rollups are held in memory per process, so with several workers each reports its own counts.
|
|
107
|
+
|
|
108
|
+
## Verified
|
|
109
|
+
|
|
110
|
+
- Unit and integration tests: `pytest` (keccak vectors incl. block-boundary cases, classifier with rDNS, 402 shapes, MPP challenge/credential round trip, facilitator flow, replay, HEAD, file lock, Pro sync, all three adapters, and an integration matrix against live uvicorn/FastAPI and Flask servers with a mock facilitator).
|
|
111
|
+
- Real settlements on Base mainnet through PayAI, October 2026, from the FastAPI example gated at $0.01: x402 v1 ([tx](https://basescan.org/tx/0x49f0f05b3509545e5134aa6cf091ff7eab4e4f1e1b9aef9bf28280c231e2a945)) and x402 v2 ([tx](https://basescan.org/tx/0x15d95ad90c9530e4816016f65681ddd4a79730373634c991c57afa2b88a8b44b)).
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
python -m venv .venv && .venv/bin/pip install -e ".[test]"
|
|
117
|
+
.venv/bin/pytest
|
|
118
|
+
python -m build
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
MIT. Made by [ForgeMesh Labs](https://forgemesh.io) (hello@forgemesh.io).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "forgemesh-pay-per-crawl"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Charge AI crawlers and bots per page with x402 USDC micropayments on Base. ASGI, WSGI and Django middleware; you keep 100%."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "ForgeMesh Labs", email = "hello@forgemesh.io" }]
|
|
14
|
+
keywords = ["x402", "mpp", "pay-per-crawl", "402", "ai-crawlers", "usdc", "base", "fastapi", "flask", "django", "asgi", "wsgi"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware",
|
|
24
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["pycryptodome>=3.20"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
httpx = ["httpx>=0.27"]
|
|
31
|
+
test = ["pytest>=8", "httpx>=0.27", "fastapi>=0.110", "uvicorn>=0.29", "flask>=3", "django>=4.2"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://forgemesh.io"
|
|
35
|
+
Repository = "https://github.com/forgemeshlabs/pay-per-crawl-py"
|
|
36
|
+
Issues = "https://github.com/forgemeshlabs/pay-per-crawl-py/issues"
|
|
37
|
+
PHP-WordPress-edition = "https://github.com/forgemeshlabs/forgemesh-pay-per-crawl"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.package-data]
|
|
43
|
+
forgemesh_pay_per_crawl = ["py.typed"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
pythonpath = ["src", "."]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""ASGI middleware for FastAPI, Starlette and any ASGI app.
|
|
2
|
+
|
|
3
|
+
app.add_middleware(PayPerCrawlMiddleware, config=Config(pay_to="0x...", mode="gate"))
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from .core import Config, Gate, Request, Response
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _request(scope) -> Request:
|
|
14
|
+
headers: dict[str, str] = {}
|
|
15
|
+
for k, v in scope.get("headers", []):
|
|
16
|
+
name = k.decode("latin-1").lower()
|
|
17
|
+
val = v.decode("latin-1")
|
|
18
|
+
headers[name] = f"{headers[name]}, {val}" if name in headers else val
|
|
19
|
+
client = scope.get("client") or ("", 0)
|
|
20
|
+
host = headers.get("host") or ""
|
|
21
|
+
if not host:
|
|
22
|
+
server = scope.get("server") or ("", None)
|
|
23
|
+
host = server[0] + (f":{server[1]}" if server[1] else "")
|
|
24
|
+
return Request(
|
|
25
|
+
method=scope.get("method", "GET"),
|
|
26
|
+
path=(scope.get("raw_path") or scope.get("path", "/").encode()).decode("latin-1").split("?")[0] or "/",
|
|
27
|
+
query=scope.get("query_string", b"").decode("latin-1"),
|
|
28
|
+
scheme=scope.get("scheme", "http"),
|
|
29
|
+
host=host,
|
|
30
|
+
headers=headers,
|
|
31
|
+
ip=client[0] or "",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PayPerCrawlMiddleware:
|
|
36
|
+
def __init__(self, app, config: Optional[Config] = None, gate: Optional[Gate] = None, **settings):
|
|
37
|
+
self.app = app
|
|
38
|
+
self.gate = gate or Gate(config or Config(**settings))
|
|
39
|
+
|
|
40
|
+
async def __call__(self, scope, receive, send):
|
|
41
|
+
if scope["type"] != "http":
|
|
42
|
+
await self.app(scope, receive, send)
|
|
43
|
+
return
|
|
44
|
+
decision = await asyncio.to_thread(self.gate.handle, _request(scope))
|
|
45
|
+
if decision.response is not None:
|
|
46
|
+
r: Response = decision.response
|
|
47
|
+
await send({"type": "http.response.start", "status": r.status,
|
|
48
|
+
"headers": [(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in r.headers]})
|
|
49
|
+
await send({"type": "http.response.body", "body": r.body})
|
|
50
|
+
return
|
|
51
|
+
if not decision.headers:
|
|
52
|
+
await self.app(scope, receive, send)
|
|
53
|
+
return
|
|
54
|
+
extra = [(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in decision.headers]
|
|
55
|
+
names = {k for k, _ in extra}
|
|
56
|
+
|
|
57
|
+
async def send_with_headers(message):
|
|
58
|
+
if message["type"] == "http.response.start":
|
|
59
|
+
# Ours win over same-named headers from the app (receipts, no-store).
|
|
60
|
+
kept = [(k, v) for k, v in message.get("headers", []) if k.lower() not in names or k.lower() == b"link"]
|
|
61
|
+
message = {**message, "headers": kept + extra}
|
|
62
|
+
await send(message)
|
|
63
|
+
|
|
64
|
+
await self.app(scope, receive, send_with_headers)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Framework-free core of ForgeMesh Pay-per-Crawl."""
|
|
2
|
+
from .classifier import CLASSES, Classification, Classifier, SystemResolver
|
|
3
|
+
from .config import DEFAULT_CLASS_POLICY, Config
|
|
4
|
+
from .discovery import PATHS as DISCOVERY_PATHS
|
|
5
|
+
from .discovery import Discovery
|
|
6
|
+
from .gate import Gate
|
|
7
|
+
from .http import Decision, Request, Response
|
|
8
|
+
from .keccak import keccak256, keccak256_hex
|
|
9
|
+
from .locks import FileNonceLock, MemoryNonceLock
|
|
10
|
+
from .pro import ProReporter, Stats
|
|
11
|
+
from .x402 import TransportError, X402, default_transport, usd_to_units
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CLASSES", "Classification", "Classifier", "SystemResolver", "Config", "DEFAULT_CLASS_POLICY", "Discovery",
|
|
15
|
+
"DISCOVERY_PATHS", "Gate", "Decision", "Request", "Response", "keccak256", "keccak256_hex", "FileNonceLock",
|
|
16
|
+
"MemoryNonceLock", "ProReporter", "Stats", "TransportError", "X402", "default_transport", "usd_to_units",
|
|
17
|
+
"classify", "build_challenge", "extract_payment", "precheck", "verify_and_settle",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Function-style entry points over the classes, for people who want the pieces.
|
|
22
|
+
def classify(ua, headers=None, ip="", classifier=None):
|
|
23
|
+
return (classifier or Classifier()).classify(ua, headers, ip)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_challenge(x402: X402, resource_url, description=""):
|
|
27
|
+
"""(PAYMENT-REQUIRED value, v1 body dict, WWW-Authenticate value) for one resource."""
|
|
28
|
+
from .x402 import b64_encode_json
|
|
29
|
+
return (b64_encode_json(x402.envelope_v2(resource_url, description)),
|
|
30
|
+
x402.body_v1(resource_url, description), x402.mpp_challenge(description))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_payment(x402, headers, resource_url):
|
|
34
|
+
return x402.extract_payment(headers, resource_url)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def precheck(x402, payment, resource_url=""):
|
|
38
|
+
return x402.precheck(payment, resource_url)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def verify_and_settle(x402, payment, resource_url):
|
|
42
|
+
return x402.verify_and_settle(payment, resource_url)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Generated from the PHP reference catalog; edit by hand from here on.
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
# name, class, match needles, rDNS suffixes that prove the operator (search engines).
|
|
5
|
+
CATALOG: list[dict] = [
|
|
6
|
+
# Search engines (free by default; verified by rDNS).
|
|
7
|
+
{"name": 'Googlebot', "class": 'search', "match": ['googlebot'], "verify": ['.googlebot.com', '.google.com']},
|
|
8
|
+
{"name": 'Google-InspectionTool', "class": 'search', "match": ['google-inspectiontool'], "verify": ['.googlebot.com', '.google.com']},
|
|
9
|
+
{"name": 'GoogleOther', "class": 'ai', "match": ['googleother'], "verify": ['.googlebot.com', '.google.com']},
|
|
10
|
+
{"name": 'Bingbot', "class": 'search', "match": ['bingbot'], "verify": ['.search.msn.com']},
|
|
11
|
+
{"name": 'DuckDuckBot', "class": 'search', "match": ['duckduckbot'], "verify": []},
|
|
12
|
+
{"name": 'Applebot', "class": 'search', "match": ['applebot'], "verify": ['.applebot.apple.com']},
|
|
13
|
+
{"name": 'YandexBot', "class": 'search', "match": ['yandexbot', 'yandex.com/bots'], "verify": ['.yandex.ru', '.yandex.net', '.yandex.com']},
|
|
14
|
+
{"name": 'Baiduspider', "class": 'search', "match": ['baiduspider'], "verify": ['.baidu.com', '.baidu.jp']},
|
|
15
|
+
{"name": 'Yahoo Slurp', "class": 'search', "match": ['yahoo! slurp'], "verify": ['.crawl.yahoo.net']},
|
|
16
|
+
{"name": 'SeznamBot', "class": 'search', "match": ['seznambot'], "verify": ['.seznam.cz']},
|
|
17
|
+
{"name": 'Qwantbot', "class": 'search', "match": ['qwantbot', 'qwantify'], "verify": []},
|
|
18
|
+
# AI crawlers and assistants (paid by default).
|
|
19
|
+
{"name": 'GPTBot', "class": 'ai', "match": ['gptbot']},
|
|
20
|
+
{"name": 'ChatGPT-User', "class": 'ai', "match": ['chatgpt-user']},
|
|
21
|
+
{"name": 'OAI-SearchBot', "class": 'ai', "match": ['oai-searchbot']},
|
|
22
|
+
{"name": 'ClaudeBot', "class": 'ai', "match": ['claudebot', 'anthropic-ai']},
|
|
23
|
+
{"name": 'Claude-User', "class": 'ai', "match": ['claude-user']},
|
|
24
|
+
{"name": 'Claude-SearchBot', "class": 'ai', "match": ['claude-searchbot']},
|
|
25
|
+
{"name": 'PerplexityBot', "class": 'ai', "match": ['perplexitybot']},
|
|
26
|
+
{"name": 'Perplexity-User', "class": 'ai', "match": ['perplexity-user']},
|
|
27
|
+
{"name": 'Google-Extended', "class": 'ai', "match": ['google-extended']},
|
|
28
|
+
{"name": 'Gemini-Deep-Research', "class": 'ai', "match": ['gemini-deep-research']},
|
|
29
|
+
{"name": 'Amazonbot', "class": 'ai', "match": ['amazonbot']},
|
|
30
|
+
{"name": 'Applebot-Extended', "class": 'ai', "match": ['applebot-extended']},
|
|
31
|
+
{"name": 'Meta-ExternalAgent', "class": 'ai', "match": ['meta-externalagent', 'meta-externalfetcher', 'facebookbot']},
|
|
32
|
+
{"name": 'Bytespider', "class": 'ai', "match": ['bytespider', 'tiktokspider']},
|
|
33
|
+
{"name": 'CCBot', "class": 'ai', "match": ['ccbot']},
|
|
34
|
+
{"name": 'cohere-ai', "class": 'ai', "match": ['cohere-ai', 'cohere-training-data-crawler']},
|
|
35
|
+
{"name": 'MistralAI-User', "class": 'ai', "match": ['mistralai-user']},
|
|
36
|
+
{"name": 'DuckAssistBot', "class": 'ai', "match": ['duckassistbot']},
|
|
37
|
+
{"name": 'YouBot', "class": 'ai', "match": ['youbot']},
|
|
38
|
+
{"name": 'Diffbot', "class": 'ai', "match": ['diffbot']},
|
|
39
|
+
{"name": 'ImagesiftBot', "class": 'ai', "match": ['imagesiftbot']},
|
|
40
|
+
{"name": 'Omgili', "class": 'ai', "match": ['omgili', 'webzio']},
|
|
41
|
+
{"name": 'AI2Bot', "class": 'ai', "match": ['ai2bot']},
|
|
42
|
+
{"name": 'PetalBot', "class": 'ai', "match": ['petalbot']},
|
|
43
|
+
{"name": 'Timpibot', "class": 'ai', "match": ['timpibot']},
|
|
44
|
+
{"name": 'iAskBot', "class": 'ai', "match": ['iaskspider', 'iaskbot']},
|
|
45
|
+
{"name": 'Kagibot', "class": 'ai', "match": ['kagibot']},
|
|
46
|
+
{"name": 'DeepSeekBot', "class": 'ai', "match": ['deepseekbot']},
|
|
47
|
+
{"name": 'Grok', "class": 'ai', "match": ['xai-grok', 'grokbot']},
|
|
48
|
+
{"name": 'Brave Search', "class": 'ai', "match": ['bravebot']},
|
|
49
|
+
{"name": 'Firecrawl', "class": 'ai', "match": ['firecrawl']},
|
|
50
|
+
{"name": 'Jina Reader', "class": 'ai', "match": ['jina', 'readerbot']},
|
|
51
|
+
{"name": 'Exa', "class": 'ai', "match": ['exabot', 'exa.ai']},
|
|
52
|
+
{"name": 'Tavily', "class": 'ai', "match": ['tavily']},
|
|
53
|
+
{"name": 'Browserbase', "class": 'ai', "match": ['browserbase']},
|
|
54
|
+
# SEO crawlers (paid by default: commercial, high volume).
|
|
55
|
+
{"name": 'AhrefsBot', "class": 'seo', "match": ['ahrefsbot']},
|
|
56
|
+
{"name": 'SemrushBot', "class": 'seo', "match": ['semrushbot']},
|
|
57
|
+
{"name": 'MJ12bot', "class": 'seo', "match": ['mj12bot']},
|
|
58
|
+
{"name": 'DotBot', "class": 'seo', "match": ['dotbot']},
|
|
59
|
+
{"name": 'DataForSeoBot', "class": 'seo', "match": ['dataforseobot']},
|
|
60
|
+
{"name": 'BLEXBot', "class": 'seo', "match": ['blexbot']},
|
|
61
|
+
{"name": 'Screaming Frog', "class": 'seo', "match": ['screaming frog']},
|
|
62
|
+
{"name": 'SiteAuditBot', "class": 'seo', "match": ['siteauditbot']},
|
|
63
|
+
# Social link previews (free by default: humans see these).
|
|
64
|
+
{"name": 'Facebook preview', "class": 'social', "match": ['facebookexternalhit']},
|
|
65
|
+
{"name": 'Twitterbot', "class": 'social', "match": ['twitterbot']},
|
|
66
|
+
{"name": 'LinkedInBot', "class": 'social', "match": ['linkedinbot']},
|
|
67
|
+
{"name": 'Slackbot', "class": 'social', "match": ['slackbot', 'slack-imgproxy']},
|
|
68
|
+
{"name": 'Discordbot', "class": 'social', "match": ['discordbot']},
|
|
69
|
+
{"name": 'TelegramBot', "class": 'social', "match": ['telegrambot']},
|
|
70
|
+
{"name": 'WhatsApp', "class": 'social', "match": ['whatsapp']},
|
|
71
|
+
{"name": 'Pinterestbot', "class": 'social', "match": ['pinterestbot', 'pinterest/']},
|
|
72
|
+
{"name": 'Redditbot', "class": 'social', "match": ['redditbot']},
|
|
73
|
+
{"name": 'Mastodon', "class": 'social', "match": ['mastodon']},
|
|
74
|
+
{"name": 'Bluesky', "class": 'social', "match": ['bluesky cardyb']},
|
|
75
|
+
{"name": 'iMessage preview', "class": 'social', "match": ['facebot twitterbot']},
|
|
76
|
+
# Monitors and feed readers (free by default).
|
|
77
|
+
{"name": 'UptimeRobot', "class": 'monitor', "match": ['uptimerobot']},
|
|
78
|
+
{"name": 'Pingdom', "class": 'monitor', "match": ['pingdom']},
|
|
79
|
+
{"name": 'StatusCake', "class": 'monitor', "match": ['statuscake']},
|
|
80
|
+
{"name": 'Better Uptime', "class": 'monitor', "match": ['betteruptime', 'better uptime']},
|
|
81
|
+
{"name": 'Feed reader', "class": 'monitor', "match": ['feedly', 'feedbin', 'inoreader', 'newsblur', 'feedfetcher']},
|
|
82
|
+
{"name": 'Internet Archive', "class": 'monitor', "match": ['archive.org_bot', 'ia_archiver']},
|
|
83
|
+
{"name": 'WordPress', "class": 'monitor', "match": ['wordpress/']},
|
|
84
|
+
# Generic automation (paid by default).
|
|
85
|
+
{"name": 'curl', "class": 'automation', "match": ['curl/']},
|
|
86
|
+
{"name": 'wget', "class": 'automation', "match": ['wget/']},
|
|
87
|
+
{"name": 'python-requests', "class": 'automation', "match": ['python-requests', 'python-urllib', 'aiohttp', 'httpx/']},
|
|
88
|
+
{"name": 'Scrapy', "class": 'automation', "match": ['scrapy']},
|
|
89
|
+
{"name": 'Go-http-client', "class": 'automation', "match": ['go-http-client']},
|
|
90
|
+
{"name": 'Node fetch', "class": 'automation', "match": ['node-fetch', 'undici', 'axios/', 'got (']},
|
|
91
|
+
{"name": 'Java HTTP', "class": 'automation', "match": ['java/', 'okhttp', 'apache-httpclient']},
|
|
92
|
+
{"name": 'Headless browser', "class": 'automation', "match": ['headlesschrome', 'phantomjs', 'puppeteer', 'playwright', 'selenium']},
|
|
93
|
+
{"name": 'HTTPie', "class": 'automation', "match": ['httpie']},
|
|
94
|
+
{"name": 'PostmanRuntime', "class": 'automation', "match": ['postmanruntime']},
|
|
95
|
+
{"name": 'libwww', "class": 'automation', "match": ['libwww', 'lwp-trivial']},
|
|
96
|
+
{"name": 'x402 client', "class": 'automation', "match": ['x402', 'mpp/']},
|
|
97
|
+
]
|