aifinpay-gate 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AiFinPay
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,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: aifinpay-gate
3
+ Version: 0.1.0
4
+ Summary: Accept AIFP-1 payments from AI agents in a Python API — ASGI (FastAPI, Starlette) and WSGI (Flask, Django) middleware. Port of @aifinpay/gate.
5
+ Author-email: "CoinSecurities (SECCO)" <contact@aifinpay.io>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://aifinpay.io
8
+ Project-URL: Repository, https://github.com/AiFinPay/sdk
9
+ Project-URL: Issues, https://github.com/AiFinPay/sdk/issues
10
+ Keywords: ai-agents,agent-payments,http-402,paywall,fastapi,flask,asgi,wsgi,middleware,aifinpay
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Framework :: Flask
16
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: PyNaCl>=1.5
21
+ Provides-Extra: redis
22
+ Requires-Dist: redis>=4.2; extra == "redis"
23
+ Dynamic: license-file
24
+
25
+ # aifinpay-gate (Python)
26
+
27
+ Accept payments from AI agents in a Python API. When an agent calls a paid
28
+ route without paying, the gate answers **HTTP 402** with everything it needs to
29
+ buy a batch of calls; the agent settles on-chain from its own wallet (you
30
+ receive 99%, AiFinPay 1%, non-custodial) and retries with an `AIFP-Receipt`.
31
+ The gate verifies that receipt **locally** — an Ed25519 signature against the
32
+ AiFinPay JWKS, no call to AiFinPay per request — and meters the batch.
33
+
34
+ A port of [`@aifinpay/gate`](https://www.npmjs.com/package/@aifinpay/gate): the
35
+ same 402 body, the same checks and the same refusal sentences, held to the Node
36
+ package by recorded fixtures (`tests/test_parity.py`).
37
+
38
+ ```bash
39
+ pip install aifinpay-gate # PyNaCl is the only dependency
40
+ pip install "aifinpay-gate[redis]" # shared counters across processes
41
+ ```
42
+
43
+ ## Register
44
+
45
+ ```bash
46
+ curl -s -X POST https://api.aifinpay.io/v1/merchants -H 'content-type: application/json' \
47
+ -d '{"name":"My API","pay_to":{"evm":"0xYourPayoutWallet"},"settlement_version":"1.4"}'
48
+ ```
49
+
50
+ Keep `merchant_id` and the one-time `merchant_secret`, then claim the merchant
51
+ at https://dash.aifinpay.io with the secret to see payments. One `pay_to.evm`
52
+ receives on every EVM network.
53
+
54
+ ## FastAPI / Starlette (ASGI)
55
+
56
+ ```python
57
+ from fastapi import FastAPI, Request
58
+ from aifinpay_gate import AifpGateMiddleware, Gate, Route
59
+
60
+ gate = Gate("mrch_…", routes=[
61
+ Route("/create", "premium", methods={"POST"}), # $0.005 per call
62
+ Route("/api/*"), # the section and everything under it
63
+ ])
64
+ app = FastAPI()
65
+ app.add_middleware(AifpGateMiddleware, gate=gate)
66
+
67
+ @app.post("/create")
68
+ async def create(request: Request):
69
+ paid = request.state.aifp # {"agent", "receipt_id", "used", "remaining", ...}
70
+ return {"ok": True, "remaining": paid["remaining"]}
71
+ ```
72
+
73
+ ## Flask / Django (WSGI)
74
+
75
+ ```python
76
+ from aifinpay_gate import AifpGateWSGI, Gate, Route
77
+
78
+ app.wsgi_app = AifpGateWSGI(app.wsgi_app, Gate("mrch_…", routes=[Route("/api/*")]))
79
+ # in a view: request.environ["aifp"]
80
+ ```
81
+
82
+ Both adapters also serve `/.well-known/x402.json` so agents discover the paid
83
+ routes before they hit one (`serve_discovery=False` to turn it off).
84
+
85
+ ## Any other framework
86
+
87
+ ```python
88
+ from aifinpay_gate import Gate, SimpleRequest
89
+
90
+ result = gate.decide(SimpleRequest(path, headers, method))
91
+ if result is None: # not a paid route
92
+ ...
93
+ elif not result.ok: # 402 / 403 / 503 — send result.status, result.headers, result.body as JSON
94
+ ...
95
+ else: # paid: add result.headers to your response; result.aifp is the metering context
96
+ ...
97
+ ```
98
+
99
+ ## Routes
100
+
101
+ `Route(pattern, tier="standard", weight=None, methods=None, paywall=True)`
102
+
103
+ - `pattern` is exact, or ends in `/*` — `/api/*` covers `/api` and everything
104
+ under `/api/`. The longest matching pattern wins. This is the hosted
105
+ gateway's matcher; wildcards in the middle (`/backtest/*/bid`) are refused.
106
+ - `tier`: `standard` $0.0005, `complex` $0.002, `premium` $0.005 per call
107
+ (weights 1, 4, 10 billing units). `weight` overrides the units per call.
108
+ - `paywall=False` serves the route free and meters nothing.
109
+ - Paths no route matches pass through untouched.
110
+
111
+ `Gate(merchant_id, resource="/api/search", tier="complex")` is the single-mount
112
+ form: every request handed to it is charged against that one resource.
113
+
114
+ ## Production: share the counters
115
+
116
+ The default `MemoryStore` meters per process. Under gunicorn/uvicorn with
117
+ several workers, or several pods, each gets a full copy of every batch — a
118
+ 200-unit batch serves up to 200 × workers calls. Use Redis:
119
+
120
+ ```python
121
+ import redis
122
+ from aifinpay_gate import Gate, RedisStore
123
+
124
+ gate = Gate("mrch_…", routes=[...], store=RedisStore(redis.Redis.from_url(REDIS_URL)))
125
+ ```
126
+
127
+ The store increments atomically and compares **after** the increment, so
128
+ concurrent requests can never overspend a batch; the counter's TTL is set once
129
+ and expires with the receipt.
130
+
131
+ ## Options
132
+
133
+ | Option | Default | |
134
+ |---|---|---|
135
+ | `should_charge` | everyone pays | `known_ai_agent` for content sites: human browsers read free and never see a 402; self-identifying AI crawlers and anything speaking AIFP pay. A predicate that raises charges. |
136
+ | `allow(ctx)` | — | Your veto after verification, before metering (a refused call costs nothing). Return `False` → 403. |
137
+ | `on_store_error` | `"closed"` | `"open"` serves un-metered calls when the store is down (`AIFP-Meter: degraded`). |
138
+ | `require_agent_match` | `False` | Refuse when `AIFP-Agent-Id` differs from the receipt subject (anti-accident, not anti-theft). |
139
+ | `replay` | `"auto"` | Single-use receipts get a one-shot nonce check. |
140
+ | `jwks` | fetched | Pin the key set and skip network I/O (redeploy on key rotation). |
141
+ | `on_event(e)` | — | `402` / `serve` / `403` / `meter_error` events for your metrics. |
142
+ | `refund_on_error` (adapters) | `False` | Give the units back when your handler answers 5xx. |
143
+
144
+ ## What it guarantees
145
+
146
+ - Fails **closed**: if the JWKS cannot be reached, paid routes answer 503, never free.
147
+ - Only EdDSA receipts from `https://api.aifinpay.io` with `aud` = your merchant id.
148
+ - Only quota receipts are spendable; per-call billing receipts are refused.
149
+ - An expired receipt gets a 402 (buy again), a wrong one a 403 with a reason.
150
+ - Stricter than the Node gate in two places: a receipt without a numeric `exp`,
151
+ or without a usable `unit_quota`, is refused.
152
+
153
+ Not included, on purpose: free allowances, daily caps and per-agent blocks live
154
+ in the AiFinPay dashboard, so there is one source of truth for each rule.
@@ -0,0 +1,130 @@
1
+ # aifinpay-gate (Python)
2
+
3
+ Accept payments from AI agents in a Python API. When an agent calls a paid
4
+ route without paying, the gate answers **HTTP 402** with everything it needs to
5
+ buy a batch of calls; the agent settles on-chain from its own wallet (you
6
+ receive 99%, AiFinPay 1%, non-custodial) and retries with an `AIFP-Receipt`.
7
+ The gate verifies that receipt **locally** — an Ed25519 signature against the
8
+ AiFinPay JWKS, no call to AiFinPay per request — and meters the batch.
9
+
10
+ A port of [`@aifinpay/gate`](https://www.npmjs.com/package/@aifinpay/gate): the
11
+ same 402 body, the same checks and the same refusal sentences, held to the Node
12
+ package by recorded fixtures (`tests/test_parity.py`).
13
+
14
+ ```bash
15
+ pip install aifinpay-gate # PyNaCl is the only dependency
16
+ pip install "aifinpay-gate[redis]" # shared counters across processes
17
+ ```
18
+
19
+ ## Register
20
+
21
+ ```bash
22
+ curl -s -X POST https://api.aifinpay.io/v1/merchants -H 'content-type: application/json' \
23
+ -d '{"name":"My API","pay_to":{"evm":"0xYourPayoutWallet"},"settlement_version":"1.4"}'
24
+ ```
25
+
26
+ Keep `merchant_id` and the one-time `merchant_secret`, then claim the merchant
27
+ at https://dash.aifinpay.io with the secret to see payments. One `pay_to.evm`
28
+ receives on every EVM network.
29
+
30
+ ## FastAPI / Starlette (ASGI)
31
+
32
+ ```python
33
+ from fastapi import FastAPI, Request
34
+ from aifinpay_gate import AifpGateMiddleware, Gate, Route
35
+
36
+ gate = Gate("mrch_…", routes=[
37
+ Route("/create", "premium", methods={"POST"}), # $0.005 per call
38
+ Route("/api/*"), # the section and everything under it
39
+ ])
40
+ app = FastAPI()
41
+ app.add_middleware(AifpGateMiddleware, gate=gate)
42
+
43
+ @app.post("/create")
44
+ async def create(request: Request):
45
+ paid = request.state.aifp # {"agent", "receipt_id", "used", "remaining", ...}
46
+ return {"ok": True, "remaining": paid["remaining"]}
47
+ ```
48
+
49
+ ## Flask / Django (WSGI)
50
+
51
+ ```python
52
+ from aifinpay_gate import AifpGateWSGI, Gate, Route
53
+
54
+ app.wsgi_app = AifpGateWSGI(app.wsgi_app, Gate("mrch_…", routes=[Route("/api/*")]))
55
+ # in a view: request.environ["aifp"]
56
+ ```
57
+
58
+ Both adapters also serve `/.well-known/x402.json` so agents discover the paid
59
+ routes before they hit one (`serve_discovery=False` to turn it off).
60
+
61
+ ## Any other framework
62
+
63
+ ```python
64
+ from aifinpay_gate import Gate, SimpleRequest
65
+
66
+ result = gate.decide(SimpleRequest(path, headers, method))
67
+ if result is None: # not a paid route
68
+ ...
69
+ elif not result.ok: # 402 / 403 / 503 — send result.status, result.headers, result.body as JSON
70
+ ...
71
+ else: # paid: add result.headers to your response; result.aifp is the metering context
72
+ ...
73
+ ```
74
+
75
+ ## Routes
76
+
77
+ `Route(pattern, tier="standard", weight=None, methods=None, paywall=True)`
78
+
79
+ - `pattern` is exact, or ends in `/*` — `/api/*` covers `/api` and everything
80
+ under `/api/`. The longest matching pattern wins. This is the hosted
81
+ gateway's matcher; wildcards in the middle (`/backtest/*/bid`) are refused.
82
+ - `tier`: `standard` $0.0005, `complex` $0.002, `premium` $0.005 per call
83
+ (weights 1, 4, 10 billing units). `weight` overrides the units per call.
84
+ - `paywall=False` serves the route free and meters nothing.
85
+ - Paths no route matches pass through untouched.
86
+
87
+ `Gate(merchant_id, resource="/api/search", tier="complex")` is the single-mount
88
+ form: every request handed to it is charged against that one resource.
89
+
90
+ ## Production: share the counters
91
+
92
+ The default `MemoryStore` meters per process. Under gunicorn/uvicorn with
93
+ several workers, or several pods, each gets a full copy of every batch — a
94
+ 200-unit batch serves up to 200 × workers calls. Use Redis:
95
+
96
+ ```python
97
+ import redis
98
+ from aifinpay_gate import Gate, RedisStore
99
+
100
+ gate = Gate("mrch_…", routes=[...], store=RedisStore(redis.Redis.from_url(REDIS_URL)))
101
+ ```
102
+
103
+ The store increments atomically and compares **after** the increment, so
104
+ concurrent requests can never overspend a batch; the counter's TTL is set once
105
+ and expires with the receipt.
106
+
107
+ ## Options
108
+
109
+ | Option | Default | |
110
+ |---|---|---|
111
+ | `should_charge` | everyone pays | `known_ai_agent` for content sites: human browsers read free and never see a 402; self-identifying AI crawlers and anything speaking AIFP pay. A predicate that raises charges. |
112
+ | `allow(ctx)` | — | Your veto after verification, before metering (a refused call costs nothing). Return `False` → 403. |
113
+ | `on_store_error` | `"closed"` | `"open"` serves un-metered calls when the store is down (`AIFP-Meter: degraded`). |
114
+ | `require_agent_match` | `False` | Refuse when `AIFP-Agent-Id` differs from the receipt subject (anti-accident, not anti-theft). |
115
+ | `replay` | `"auto"` | Single-use receipts get a one-shot nonce check. |
116
+ | `jwks` | fetched | Pin the key set and skip network I/O (redeploy on key rotation). |
117
+ | `on_event(e)` | — | `402` / `serve` / `403` / `meter_error` events for your metrics. |
118
+ | `refund_on_error` (adapters) | `False` | Give the units back when your handler answers 5xx. |
119
+
120
+ ## What it guarantees
121
+
122
+ - Fails **closed**: if the JWKS cannot be reached, paid routes answer 503, never free.
123
+ - Only EdDSA receipts from `https://api.aifinpay.io` with `aud` = your merchant id.
124
+ - Only quota receipts are spendable; per-call billing receipts are refused.
125
+ - An expired receipt gets a 402 (buy again), a wrong one a 403 with a reason.
126
+ - Stricter than the Node gate in two places: a receipt without a numeric `exp`,
127
+ or without a usable `unit_quota`, is refused.
128
+
129
+ Not included, on purpose: free allowances, daily caps and per-agent blocks live
130
+ in the AiFinPay dashboard, so there is one source of truth for each rule.
@@ -0,0 +1,36 @@
1
+ """AiFinPay gate for Python merchants — accept AIFP-1 payments from AI agents.
2
+
3
+ A port of ``@aifinpay/gate``: the same 402 challenge, the same local EdDSA
4
+ receipt verification against the AiFinPay JWKS, the same post-increment quota
5
+ metering. See README.md.
6
+ """
7
+
8
+ from .agents import AI_AGENT_UA_MARKERS, known_ai_agent
9
+ from .asgi import AifpGateMiddleware
10
+ from .challenge import build_challenge
11
+ from .core import (
12
+ DETAIL_QUOTA_EXHAUSTED,
13
+ DETAIL_RECEIPT_EXPIRED,
14
+ DETAIL_VERIFY_FAILED,
15
+ HEADER_QUOTA_REMAINING,
16
+ Gate,
17
+ GateResult,
18
+ Route,
19
+ SimpleRequest,
20
+ )
21
+ from .discovery import build_discovery_document
22
+ from .pricing import TIER_WEIGHTS, UNIT_PRICE_USD, min_requests_for_tier, unit_price_usd, weight_for_tier
23
+ from .scope import pattern_covers, scope_covers
24
+ from .stores import REDIS_INCRBY_SCRIPT, MemoryStore, RedisStore, StoreCapacityError
25
+ from .verify import Verifier
26
+ from .wsgi import AifpGateWSGI
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ "AI_AGENT_UA_MARKERS", "AifpGateMiddleware", "AifpGateWSGI", "DETAIL_QUOTA_EXHAUSTED", "DETAIL_RECEIPT_EXPIRED",
32
+ "DETAIL_VERIFY_FAILED", "Gate", "GateResult", "HEADER_QUOTA_REMAINING", "MemoryStore", "REDIS_INCRBY_SCRIPT",
33
+ "RedisStore", "Route", "SimpleRequest", "StoreCapacityError", "TIER_WEIGHTS", "UNIT_PRICE_USD", "Verifier",
34
+ "build_challenge", "build_discovery_document", "known_ai_agent", "min_requests_for_tier", "pattern_covers",
35
+ "scope_covers", "unit_price_usd", "weight_for_tier",
36
+ ]
@@ -0,0 +1,30 @@
1
+ """Who is an AI agent — the default ``should_charge`` for content sites.
2
+
3
+ A curated list of self-identifying crawlers plus one stronger signal: a
4
+ request already speaking AIFP (AIFP-Receipt or AIFP-Agent-Id) is an agent by
5
+ its own declaration. It is not stealth detection; a scraper with a browser
6
+ User-Agent walks past it, as it walks past Cloudflare's classifier. Extend it:
7
+
8
+ should_charge=lambda req: known_ai_agent(req) or my_signal(req)
9
+ """
10
+
11
+ AI_AGENT_UA_MARKERS = (
12
+ "gptbot", "oai-searchbot", "chatgpt-user",
13
+ "claudebot", "claude-web", "anthropic-ai",
14
+ "perplexitybot", "perplexity-user",
15
+ "ccbot", "bytespider", "google-extended", "applebot-extended",
16
+ "meta-externalagent", "facebookbot", "amazonbot",
17
+ "youbot", "diffbot", "timpibot", "omgilibot", "cohere-ai", "ai2bot", "mistralai",
18
+ "aifinpay-agent",
19
+ "headlesschrome", "phantomjs",
20
+ )
21
+
22
+
23
+ def known_ai_agent(req) -> bool:
24
+ # A paying agent must never be exempted by a human-looking User-Agent.
25
+ if req.header("AIFP-Receipt") or req.header("AIFP-Agent-Id"):
26
+ return True
27
+ ua = (req.header("user-agent") or "").lower()
28
+ if not ua:
29
+ return True # no browser sends none; scripts do
30
+ return any(marker in ua for marker in AI_AGENT_UA_MARKERS)
@@ -0,0 +1,87 @@
1
+ """ASGI middleware — FastAPI, Starlette, Quart, Litestar, any ASGI app.
2
+
3
+ from aifinpay_gate import Gate, Route, AifpGateMiddleware
4
+ gate = Gate("mrch_…", routes=[Route("/create", "premium", methods={"POST"})])
5
+ app.add_middleware(AifpGateMiddleware, gate=gate)
6
+
7
+ Paid calls reach the handler with the metering context in
8
+ ``request.state.aifp`` (Starlette) — i.e. ``scope["state"]["aifp"]``. A gate
9
+ decision is always a response, never an exception: a 402 routed through an
10
+ app's error handler would come out as a 500, and an agent cannot pay a 500.
11
+ """
12
+
13
+ import asyncio
14
+ import json
15
+ from typing import Any, Dict, Optional
16
+
17
+ from .core import Gate, GateResult
18
+ from .discovery import DISCOVERY_PATH, build_discovery_document
19
+
20
+
21
+ class _AsgiRequest:
22
+ def __init__(self, scope: Dict[str, Any]):
23
+ self.path = scope.get("path") or "/"
24
+ self.method = scope.get("method", "GET")
25
+ self._headers: Dict[str, str] = {}
26
+ for k, v in scope.get("headers") or []:
27
+ name = k.decode("latin-1").lower()
28
+ self._headers.setdefault(name, v.decode("latin-1"))
29
+
30
+ def header(self, name: str) -> Optional[str]:
31
+ return self._headers.get(name.lower())
32
+
33
+
34
+ async def _send_json(send, status: int, headers: Dict[str, str], body: Any) -> None:
35
+ raw = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode()
36
+ hdrs = [(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in headers.items() if k.lower() != "content-type"]
37
+ hdrs += [(b"content-type", b"application/json"), (b"content-length", str(len(raw)).encode())]
38
+ await send({"type": "http.response.start", "status": status, "headers": hdrs})
39
+ await send({"type": "http.response.body", "body": raw})
40
+
41
+
42
+ class AifpGateMiddleware:
43
+ def __init__(self, app, gate: Gate, serve_discovery: bool = True, refund_on_error: bool = False):
44
+ self.app = app
45
+ self.gate = gate
46
+ self.serve_discovery = serve_discovery
47
+ self.refund_on_error = refund_on_error
48
+ self._discovery = build_discovery_document(gate.merchant_id, gate.discovery_resources(),
49
+ gate.api_base or "https://api.aifinpay.io")
50
+
51
+ async def __call__(self, scope, receive, send):
52
+ if scope.get("type") != "http":
53
+ return await self.app(scope, receive, send)
54
+ req = _AsgiRequest(scope)
55
+ if self.serve_discovery and req.method == "GET" and req.path == DISCOVERY_PATH:
56
+ return await _send_json(send, 200, {"Cache-Control": "public, max-age=300"}, self._discovery)
57
+ # decide() may block on a JWKS fetch or a Redis round-trip; keep that
58
+ # off the event loop.
59
+ try:
60
+ result: Optional[GateResult] = await asyncio.get_running_loop().run_in_executor(
61
+ None, self.gate.decide, req
62
+ )
63
+ except Exception: # noqa: BLE001 — a bug in this package, not a payment decision
64
+ if self.gate.on_store_error == "open":
65
+ return await self.app(scope, receive, send)
66
+ return await _send_json(send, 503, {}, {"error": "AIFP-503-METER",
67
+ "detail": "payment gate unavailable — retry shortly"})
68
+ if result is None:
69
+ return await self.app(scope, receive, send)
70
+ if not result.ok:
71
+ return await _send_json(send, result.status, result.headers, result.body)
72
+
73
+ scope.setdefault("state", {})["aifp"] = result.aifp
74
+ extra = [(k.lower().encode("latin-1"), v.encode("latin-1")) for k, v in result.headers.items()]
75
+ status_seen = {}
76
+
77
+ async def send_with_headers(message):
78
+ if message["type"] == "http.response.start":
79
+ status_seen["status"] = message.get("status", 200)
80
+ message = {**message, "headers": list(message.get("headers") or []) + extra}
81
+ await send(message)
82
+
83
+ try:
84
+ await self.app(scope, receive, send_with_headers)
85
+ finally:
86
+ if self.refund_on_error and status_seen.get("status", 500) >= 500:
87
+ self.gate.refund(result.aifp)
@@ -0,0 +1,62 @@
1
+ """The 402 body, key for key what @aifinpay/gate and the hosted gateway emit.
2
+
3
+ It is the only documentation an agent gets before any relationship exists, so
4
+ it carries the whole purchase path. tests/test_parity.py compares it with the
5
+ Node package's output, byte for byte."""
6
+
7
+ from typing import Any, Dict, Optional
8
+
9
+ from .pricing import PROTOCOL_FEE_BPS, min_requests_for_tier, unit_price_usd
10
+
11
+ DEFAULT_API_BASE = "https://api.aifinpay.io"
12
+ DEFAULT_DETAIL = "Payment Required — prepay a batch of requests and retry with the AIFP-Receipt header"
13
+
14
+
15
+ def build_challenge(
16
+ merchant_id: str,
17
+ resource: str,
18
+ tier: str,
19
+ weight: int,
20
+ detail: Optional[str] = None,
21
+ scope: Optional[str] = None,
22
+ unit_price: Optional[str] = None,
23
+ min_requests: Optional[int] = None,
24
+ api_base: Optional[str] = None,
25
+ ) -> Dict[str, Any]:
26
+ # Absolute on purpose: this gate runs on the merchant's host, so a relative
27
+ # "/v1/quote" would send the agent to the merchant's own server.
28
+ api = (api_base or DEFAULT_API_BASE).rstrip("/")
29
+ scope = scope or "exact"
30
+ requests_ = min_requests if min_requests is not None else min_requests_for_tier(tier)
31
+ return {
32
+ "error": "AIFP-402",
33
+ "detail": detail or DEFAULT_DETAIL,
34
+ "protocol": "AIFP-1",
35
+ "documentation_url": "https://github.com/AiFinPay/sdk/blob/main/AGENT-FLOW.md",
36
+ "instructions_url": "https://raw.githubusercontent.com/AiFinPay/skill/main/agent/skills/aifinpay/SKILL.md",
37
+ "merchant_instructions_url":
38
+ "https://raw.githubusercontent.com/AiFinPay/skill/main/agent/skills/aifinpay-merchant/SKILL.md",
39
+ "merchant_id": merchant_id,
40
+ "resource": resource,
41
+ "tier": tier,
42
+ "unit_weight": weight,
43
+ "unit_price_usd": unit_price or unit_price_usd(tier),
44
+ "min_requests": requests_,
45
+ "scope": scope,
46
+ "protocol_fee_bps": PROTOCOL_FEE_BPS,
47
+ "no_minimum_fee": True,
48
+ "settlement_terms_from":
49
+ f"POST {api}/v1/quote — returns accepted_chains, accepted_assets, amount, order_id and expiry",
50
+ "how_to_pay": [
51
+ f'POST {api}/v1/quote {{"merchant_id":"{merchant_id}","resource":"{resource}","tier":"{tier}",'
52
+ f'"scope":"{scope}","requests":{requests_},"payer":"<your wallet address>"}} — payer is required; '
53
+ "raise requests to buy more than the minimum batch; check amount, scope, expiry and payer before paying",
54
+ "settle the quoted batch on-chain from your own wallet (order_id = quote_id)",
55
+ f"POST {api}/v1/pay {{quote_id, chain, asset, tx_ref, payment_authorization}} + Idempotency-Key header "
56
+ "-> quota receipt (wallet-signature-v1). Keep the quote until you hold the receipt; on a timeout retry "
57
+ "with the same key and tx_ref — never pay again",
58
+ "retry this request with header: AIFP-Receipt: <receipt JWT>",
59
+ ],
60
+ "no_wallet": "npx @aifinpay/mcp init — creates or selects a local wallet; read instructions_url for supported "
61
+ "clients and payment availability. Wallet setup alone does not enable settlement.",
62
+ }