billmyagent 2.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,8 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: billmyagent
3
+ Version: 2.1.0
4
+ Summary: x402 payment SDK for billmyagent — auto-pays HTTP 402 challenges with an eth-account signer and gates your own endpoints (FastAPI/Flask)
5
+ Project-URL: Homepage, https://billmyagent.ai
6
+ Project-URL: Source, https://github.com/RealMaxPower/payment-processor
7
+ Author: billmyagent
8
+ License-Expression: Apache-2.0
9
+ Keywords: ai-agents,eip-3009,ethereum,payments,usdc,x402
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: eth-account>=0.13
12
+ Requires-Dist: requests>=2.31
13
+ Requires-Dist: x402[evm]<3,>=2.13
14
+ Provides-Extra: dev
15
+ Requires-Dist: mypy>=1.11; extra == 'dev'
16
+ Requires-Dist: pytest>=8.0; extra == 'dev'
17
+ Requires-Dist: responses>=0.25; extra == 'dev'
18
+ Requires-Dist: ruff>=0.6; extra == 'dev'
19
+ Requires-Dist: types-requests; extra == 'dev'
20
+ Provides-Extra: fastapi
21
+ Requires-Dist: fastapi>=0.110; extra == 'fastapi'
22
+ Requires-Dist: httpx>=0.27; extra == 'fastapi'
23
+ Provides-Extra: flask
24
+ Requires-Dist: flask>=3.0; extra == 'flask'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # billmyagent (Python)
28
+
29
+ x402 payment SDK for Python. Pay x402-gated APIs as a **server/agent** and gate
30
+ your **own endpoints** with a paywall. The Python counterpart of
31
+ [`@billmyagent/payments-core`](../core), built on the official
32
+ [`x402`](https://pypi.org/project/x402/) library + `eth-account`.
33
+
34
+ Unlike the browser-oriented JS SDK, the agent here holds its private key
35
+ directly — which is why this SDK matters for autonomous Python agents
36
+ (LangChain, CrewAI, FastAPI services).
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install billmyagent # buyer client
42
+ pip install 'billmyagent[fastapi]' # + FastAPI seller middleware
43
+ pip install 'billmyagent[flask]' # + Flask seller middleware
44
+ ```
45
+
46
+ ## Pay an x402-gated API (buyer / agent)
47
+
48
+ ```python
49
+ import os
50
+ from billmyagent import PaymentClient, create_signer
51
+
52
+ # The private key is held in-process and never leaves it.
53
+ signer = create_signer("base", os.environ["PRIVATE_KEY"])
54
+ client = PaymentClient(signer=signer)
55
+
56
+ # client.http is a requests.Session; any HTTP 402 it hits is paid and retried.
57
+ res = client.http.get("https://api.example.com/paid-resource")
58
+ print(res.json(), "paid:", bool(res.headers.get("x-payment-response")))
59
+ ```
60
+
61
+ ## Manage your merchant account (REST)
62
+
63
+ ```python
64
+ client = PaymentClient(api_key=os.environ["BMA_API_KEY"], auth_token=jwt)
65
+
66
+ client.get_payout_settings()
67
+ client.update_payout_settings({"base_address": "0x…"})
68
+ client.list_payments()
69
+ ```
70
+
71
+ ## Charge for your endpoints (seller)
72
+
73
+ FastAPI:
74
+
75
+ ```python
76
+ from fastapi import FastAPI
77
+ from billmyagent.middleware.fastapi import add_payment_middleware
78
+
79
+ app = FastAPI()
80
+ add_payment_middleware(
81
+ app,
82
+ pay_to="0xYourPayoutAddress",
83
+ routes={"GET /premium": "$0.01"},
84
+ network="base-sepolia", # base | base-sepolia | polygon
85
+ )
86
+
87
+ @app.get("/premium")
88
+ def premium():
89
+ return {"unlocked": True}
90
+ ```
91
+
92
+ Flask is identical via `billmyagent.middleware.flask.add_payment_middleware`.
93
+
94
+ > Mainnet (base/polygon) settlement needs a facilitator with CDP credentials —
95
+ > pass `facilitator_url=...`. Omit it to use the public x402.org testnet facilitator.
96
+
97
+ ## Supported networks
98
+
99
+ EVM only, `exact` scheme: **base**, **base-sepolia** (testing), **polygon**.
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ pip install -e '.[dev,fastapi,flask]'
105
+ pytest && ruff check . && mypy billmyagent
106
+ ```
107
+
108
+ ## License
109
+
110
+ Apache 2.0.
@@ -0,0 +1,84 @@
1
+ # billmyagent (Python)
2
+
3
+ x402 payment SDK for Python. Pay x402-gated APIs as a **server/agent** and gate
4
+ your **own endpoints** with a paywall. The Python counterpart of
5
+ [`@billmyagent/payments-core`](../core), built on the official
6
+ [`x402`](https://pypi.org/project/x402/) library + `eth-account`.
7
+
8
+ Unlike the browser-oriented JS SDK, the agent here holds its private key
9
+ directly — which is why this SDK matters for autonomous Python agents
10
+ (LangChain, CrewAI, FastAPI services).
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install billmyagent # buyer client
16
+ pip install 'billmyagent[fastapi]' # + FastAPI seller middleware
17
+ pip install 'billmyagent[flask]' # + Flask seller middleware
18
+ ```
19
+
20
+ ## Pay an x402-gated API (buyer / agent)
21
+
22
+ ```python
23
+ import os
24
+ from billmyagent import PaymentClient, create_signer
25
+
26
+ # The private key is held in-process and never leaves it.
27
+ signer = create_signer("base", os.environ["PRIVATE_KEY"])
28
+ client = PaymentClient(signer=signer)
29
+
30
+ # client.http is a requests.Session; any HTTP 402 it hits is paid and retried.
31
+ res = client.http.get("https://api.example.com/paid-resource")
32
+ print(res.json(), "paid:", bool(res.headers.get("x-payment-response")))
33
+ ```
34
+
35
+ ## Manage your merchant account (REST)
36
+
37
+ ```python
38
+ client = PaymentClient(api_key=os.environ["BMA_API_KEY"], auth_token=jwt)
39
+
40
+ client.get_payout_settings()
41
+ client.update_payout_settings({"base_address": "0x…"})
42
+ client.list_payments()
43
+ ```
44
+
45
+ ## Charge for your endpoints (seller)
46
+
47
+ FastAPI:
48
+
49
+ ```python
50
+ from fastapi import FastAPI
51
+ from billmyagent.middleware.fastapi import add_payment_middleware
52
+
53
+ app = FastAPI()
54
+ add_payment_middleware(
55
+ app,
56
+ pay_to="0xYourPayoutAddress",
57
+ routes={"GET /premium": "$0.01"},
58
+ network="base-sepolia", # base | base-sepolia | polygon
59
+ )
60
+
61
+ @app.get("/premium")
62
+ def premium():
63
+ return {"unlocked": True}
64
+ ```
65
+
66
+ Flask is identical via `billmyagent.middleware.flask.add_payment_middleware`.
67
+
68
+ > Mainnet (base/polygon) settlement needs a facilitator with CDP credentials —
69
+ > pass `facilitator_url=...`. Omit it to use the public x402.org testnet facilitator.
70
+
71
+ ## Supported networks
72
+
73
+ EVM only, `exact` scheme: **base**, **base-sepolia** (testing), **polygon**.
74
+
75
+ ## Development
76
+
77
+ ```bash
78
+ pip install -e '.[dev,fastapi,flask]'
79
+ pytest && ruff check . && mypy billmyagent
80
+ ```
81
+
82
+ ## License
83
+
84
+ Apache 2.0.
@@ -0,0 +1,17 @@
1
+ """billmyagent — x402 payment SDK for Python.
2
+
3
+ Pay x402-gated APIs as a server/agent, and gate your own endpoints.
4
+
5
+ from billmyagent import PaymentClient, create_signer
6
+
7
+ signer = create_signer("base", PRIVATE_KEY)
8
+ client = PaymentClient(signer=signer)
9
+ res = client.http.get("https://api.example.com/paid") # auto-pays any 402
10
+ """
11
+
12
+ from .client import PaymentClient, DEFAULT_BASE_URL
13
+ from .signer import create_signer, SUPPORTED_NETWORKS
14
+
15
+ __all__ = ["PaymentClient", "create_signer", "SUPPORTED_NETWORKS", "DEFAULT_BASE_URL", "__version__"]
16
+
17
+ __version__ = "2.1.0"
@@ -0,0 +1,131 @@
1
+ """billmyagent payment client (Python).
2
+
3
+ Mirrors ``@billmyagent/payments-core`` for the Python AI-agent audience. Two
4
+ independent, optional roles:
5
+
6
+ * **Pay x402-gated APIs** (buyer/agent): pass a ``signer``. Then ``client.http``
7
+ auto-pays any HTTP 402 it hits — ``client.http.get("https://api.example.com/paid")``.
8
+ * **Manage your merchant account / payments** (REST): pass ``api_key`` (and
9
+ ``auth_token`` for ``/merchant/*``) and call the methods below.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ import requests
17
+ from eth_account.signers.local import LocalAccount
18
+
19
+ from x402 import x402ClientSync
20
+ from x402.mechanisms.evm.exact import register_exact_evm_client
21
+ from x402.http.clients.requests import x402_requests
22
+
23
+ DEFAULT_BASE_URL = "https://api.billmyagent.ai/api/v1"
24
+
25
+
26
+ class PaymentClient:
27
+ """Client for the billmyagent x402 payment processor."""
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ signer: LocalAccount | None = None,
33
+ api_key: str | None = None,
34
+ base_url: str = DEFAULT_BASE_URL,
35
+ auth_token: str | None = None,
36
+ ) -> None:
37
+ """Create a client.
38
+
39
+ Args:
40
+ signer: eth_account signer (see :func:`billmyagent.create_signer`).
41
+ When provided, ``http`` auto-pays HTTP 402 challenges.
42
+ api_key: billmyagent API key, sent as ``X-API-Key`` on REST calls.
43
+ base_url: billmyagent API base URL.
44
+ auth_token: merchant JWT, required only for ``/merchant/*`` methods.
45
+ """
46
+ self.base_url = base_url.rstrip("/")
47
+ self._auth_token = auth_token
48
+
49
+ if signer is not None:
50
+ x402_client = x402ClientSync()
51
+ # Registers the exact-EVM scheme for all EVM networks (eip155:*),
52
+ # wrapping the LocalAccount so it can sign EIP-3009 authorizations.
53
+ # register_exact_evm_client wraps a LocalAccount internally
54
+ # (_wrap_if_local_account); the static type is the wrapped interface.
55
+ register_exact_evm_client(x402_client, signer) # type: ignore[arg-type]
56
+ #: requests.Session that automatically pays HTTP 402 challenges.
57
+ self.http: requests.Session = x402_requests(x402_client)
58
+ else:
59
+ self.http = requests.Session()
60
+
61
+ if api_key:
62
+ self.http.headers["X-API-Key"] = api_key
63
+ self.http.headers["Content-Type"] = "application/json"
64
+
65
+ # ── Payments (REST; X-API-Key) ──────────────────────────────────────────
66
+
67
+ def create_payment(self, request: dict[str, Any]) -> dict[str, Any]:
68
+ """Record a payment with a pre-built x402 payload/requirements pair."""
69
+ return self._json(self.http.post(f"{self.base_url}/payments", json=request))
70
+
71
+ def get_payment(self, payment_id: str) -> dict[str, Any]:
72
+ return self._json(self.http.get(f"{self.base_url}/payments/{payment_id}"))
73
+
74
+ def verify_payment(self, payment_id: str) -> dict[str, Any]:
75
+ return self._json(self.http.post(f"{self.base_url}/payments/{payment_id}/verify"))
76
+
77
+ def settle_payment(self, payment_id: str) -> dict[str, Any]:
78
+ return self._json(self.http.post(f"{self.base_url}/payments/{payment_id}/settle"))
79
+
80
+ def list_payments(self, limit: int = 20, offset: int = 0) -> dict[str, Any]:
81
+ return self._json(
82
+ self.http.get(f"{self.base_url}/payments", params={"limit": limit, "offset": offset})
83
+ )
84
+
85
+ # ── Merchant account management (JWT) ───────────────────────────────────
86
+
87
+ def get_payout_settings(self) -> dict[str, Any]:
88
+ """Get your payout addresses (per network) and current fee rate."""
89
+ return self._json(
90
+ self.http.get(f"{self.base_url}/merchant/payout-settings", headers=self._bearer())
91
+ )["settings"]
92
+
93
+ def update_payout_settings(self, addresses: dict[str, str]) -> dict[str, Any]:
94
+ """Set one or more payout wallet addresses. Only provided fields change."""
95
+ return self._json(
96
+ self.http.put(
97
+ f"{self.base_url}/merchant/payout-settings", json=addresses, headers=self._bearer()
98
+ )
99
+ )["settings"]
100
+
101
+ def get_fees(self) -> dict[str, Any]:
102
+ """Get your accrued platform-fee ledger plus a per-currency/status summary."""
103
+ body = self._json(self.http.get(f"{self.base_url}/merchant/fees", headers=self._bearer()))
104
+ return {"summary": body.get("summary"), "fees": body.get("fees")}
105
+
106
+ def list_invoices(self) -> list[dict[str, Any]]:
107
+ """List your fee invoices."""
108
+ return self._json(
109
+ self.http.get(f"{self.base_url}/merchant/invoices", headers=self._bearer())
110
+ )["invoices"]
111
+
112
+ def get_invoice(self, invoice_id: str) -> dict[str, Any]:
113
+ """Get an invoice plus, for open/overdue invoices, the x402 payment instruction."""
114
+ return self._json(
115
+ self.http.get(f"{self.base_url}/merchant/invoices/{invoice_id}", headers=self._bearer())
116
+ )
117
+
118
+ # ── internals ───────────────────────────────────────────────────────────
119
+
120
+ def _bearer(self) -> dict[str, str]:
121
+ if not self._auth_token:
122
+ raise ValueError(
123
+ "auth_token (JWT) is required for merchant account methods. "
124
+ "Set it in PaymentClient(...)."
125
+ )
126
+ return {"Authorization": f"Bearer {self._auth_token}"}
127
+
128
+ @staticmethod
129
+ def _json(response: requests.Response) -> Any:
130
+ response.raise_for_status()
131
+ return response.json()
@@ -0,0 +1,56 @@
1
+ """Seller-side x402 middleware for Python web frameworks.
2
+
3
+ Gate your own endpoints behind an x402 paywall. A request gets HTTP 402 with the
4
+ official payment requirements until a valid payment is presented; the facilitator
5
+ verifies + settles it and your payout address receives the funds directly.
6
+
7
+ These are thin wrappers over the official ``x402`` server middleware, pre-wired
8
+ to billmyagent conventions (simple network names, USD/USDC pricing). Import the
9
+ framework you use:
10
+
11
+ from billmyagent.middleware.fastapi import add_payment_middleware # FastAPI
12
+ from billmyagent.middleware.flask import add_payment_middleware # Flask
13
+ """
14
+
15
+ # Networks the x402 facilitator can settle, mapped to CAIP-2 chain ids.
16
+ NETWORK_TO_CAIP2 = {
17
+ "base": "eip155:8453",
18
+ "base-sepolia": "eip155:84532",
19
+ "polygon": "eip155:137",
20
+ }
21
+
22
+
23
+ def to_caip2(network: str) -> str:
24
+ """Translate a simple network name to its CAIP-2 id (raises on unknown)."""
25
+ try:
26
+ return NETWORK_TO_CAIP2[network]
27
+ except KeyError as exc:
28
+ raise ValueError(
29
+ f"Unsupported network {network!r}. Supported: {', '.join(NETWORK_TO_CAIP2)}"
30
+ ) from exc
31
+
32
+
33
+ def build_routes(pay_to: str, routes: dict[str, object], default_network: str) -> dict[str, object]:
34
+ """Build an x402 RoutesConfig from a simple ``{"GET /premium": "$0.01"}`` spec.
35
+
36
+ Each value may be a price string (uses ``default_network``) or a dict with
37
+ ``price`` and optional ``network`` keys.
38
+ """
39
+ config: dict[str, object] = {}
40
+ for route, spec in routes.items():
41
+ if isinstance(spec, str):
42
+ price, network = spec, default_network
43
+ elif isinstance(spec, dict):
44
+ price = str(spec.get("price"))
45
+ network = str(spec.get("network", default_network))
46
+ else:
47
+ raise TypeError(f"route spec for {route!r} must be a price string or dict")
48
+ config[route] = {
49
+ "accepts": {
50
+ "scheme": "exact",
51
+ "payTo": pay_to,
52
+ "price": price,
53
+ "network": to_caip2(network),
54
+ }
55
+ }
56
+ return config
@@ -0,0 +1,62 @@
1
+ """FastAPI x402 paywall middleware.
2
+
3
+ Example:
4
+ from fastapi import FastAPI
5
+ from billmyagent.middleware.fastapi import add_payment_middleware
6
+
7
+ app = FastAPI()
8
+ add_payment_middleware(
9
+ app,
10
+ pay_to="0xYourPayoutAddress",
11
+ routes={"GET /premium": "$0.01"},
12
+ network="base-sepolia",
13
+ )
14
+
15
+ @app.get("/premium")
16
+ def premium():
17
+ return {"unlocked": True}
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any
23
+
24
+ from x402 import x402ResourceServer
25
+ from x402.http import HTTPFacilitatorClient
26
+ from x402.mechanisms.evm.exact import register_exact_evm_server
27
+ from x402.http.middleware.fastapi import payment_middleware as _payment_middleware
28
+
29
+ from . import build_routes
30
+
31
+
32
+ def _build_server(facilitator_url: str | None) -> x402ResourceServer:
33
+ facilitator = HTTPFacilitatorClient({"url": facilitator_url}) if facilitator_url else HTTPFacilitatorClient()
34
+ server = x402ResourceServer(facilitator)
35
+ register_exact_evm_server(server)
36
+ return server
37
+
38
+
39
+ def add_payment_middleware(
40
+ app: Any,
41
+ *,
42
+ pay_to: str,
43
+ routes: dict[str, object],
44
+ network: str = "base-sepolia",
45
+ facilitator_url: str | None = None,
46
+ ) -> None:
47
+ """Register an x402 paywall on a FastAPI app.
48
+
49
+ Args:
50
+ app: the FastAPI application.
51
+ pay_to: payout address that receives payments.
52
+ routes: ``{"GET /premium": "$0.01"}`` (or per-route ``{"price","network"}`` dicts).
53
+ network: default network for routes without one (base / base-sepolia / polygon).
54
+ facilitator_url: a facilitator you control; omit for the public x402.org
55
+ testnet facilitator. Mainnet settlement needs CDP credentials.
56
+ """
57
+ server = _build_server(facilitator_url)
58
+ middleware = _payment_middleware(build_routes(pay_to, routes, network), server) # type: ignore[arg-type]
59
+
60
+ @app.middleware("http")
61
+ async def _x402(request: Any, call_next: Any) -> Any: # noqa: ANN401
62
+ return await middleware(request, call_next)
@@ -0,0 +1,56 @@
1
+ """Flask x402 paywall middleware.
2
+
3
+ Example:
4
+ from flask import Flask
5
+ from billmyagent.middleware.flask import add_payment_middleware
6
+
7
+ app = Flask(__name__)
8
+ add_payment_middleware(
9
+ app,
10
+ pay_to="0xYourPayoutAddress",
11
+ routes={"GET /premium": "$0.01"},
12
+ network="base-sepolia",
13
+ )
14
+
15
+ @app.get("/premium")
16
+ def premium():
17
+ return {"unlocked": True}
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any
23
+
24
+ from x402 import x402ResourceServerSync
25
+ from x402.http import HTTPFacilitatorClientSync
26
+ from x402.mechanisms.evm.exact import register_exact_evm_server
27
+ from x402.http.middleware.flask import payment_middleware as _payment_middleware
28
+
29
+ from . import build_routes
30
+
31
+
32
+ def _build_server(facilitator_url: str | None) -> x402ResourceServerSync:
33
+ facilitator = (
34
+ HTTPFacilitatorClientSync({"url": facilitator_url})
35
+ if facilitator_url
36
+ else HTTPFacilitatorClientSync()
37
+ )
38
+ server = x402ResourceServerSync(facilitator)
39
+ register_exact_evm_server(server)
40
+ return server
41
+
42
+
43
+ def add_payment_middleware(
44
+ app: Any,
45
+ *,
46
+ pay_to: str,
47
+ routes: dict[str, object],
48
+ network: str = "base-sepolia",
49
+ facilitator_url: str | None = None,
50
+ ) -> Any:
51
+ """Register an x402 paywall on a Flask app. Returns the PaymentMiddleware.
52
+
53
+ Args mirror the FastAPI variant. Uses the sync x402 server + facilitator.
54
+ """
55
+ server = _build_server(facilitator_url)
56
+ return _payment_middleware(app, build_routes(pay_to, routes, network), server) # type: ignore[arg-type]
@@ -0,0 +1,36 @@
1
+ """Signer helpers for the billmyagent x402 client.
2
+
3
+ A "signer" is an ``eth_account`` ``LocalAccount``. The x402 exact-EVM scheme
4
+ signs an EIP-3009 ``transferWithAuthorization`` with it; the facilitator settles
5
+ on-chain. On a server/agent the private key is held directly and never leaves
6
+ your process — which is the main reason this SDK exists (browsers use the JS SDK
7
+ with an injected wallet instead).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from eth_account import Account
13
+ from eth_account.signers.local import LocalAccount
14
+
15
+ # EVM networks the x402 facilitator can settle. Solana is intentionally
16
+ # unsupported; Ethereum L1 is not offered by the facilitator.
17
+ SUPPORTED_NETWORKS = ("base", "base-sepolia", "polygon")
18
+
19
+
20
+ def create_signer(network: str, private_key: str) -> LocalAccount:
21
+ """Build a signer (eth_account LocalAccount) from a private key.
22
+
23
+ Args:
24
+ network: Target network — one of ``base``, ``base-sepolia``, ``polygon``.
25
+ Advisory: the exact-EVM scheme pays whatever network the 402 challenge
26
+ asks for, but this guards against typos / unsupported chains.
27
+ private_key: 0x-prefixed private key. Use a dedicated, funded key.
28
+
29
+ Returns:
30
+ An ``eth_account`` ``LocalAccount`` usable as the client's ``signer``.
31
+ """
32
+ if network not in SUPPORTED_NETWORKS:
33
+ raise ValueError(
34
+ f"Unsupported network {network!r}. Supported: {', '.join(SUPPORTED_NETWORKS)}"
35
+ )
36
+ return Account.from_key(private_key)
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "billmyagent"
7
+ version = "2.1.0"
8
+ description = "x402 payment SDK for billmyagent — auto-pays HTTP 402 challenges with an eth-account signer and gates your own endpoints (FastAPI/Flask)"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "billmyagent" }]
13
+ keywords = ["x402", "payments", "ai-agents", "ethereum", "usdc", "eip-3009"]
14
+ dependencies = [
15
+ "x402[evm]>=2.13,<3",
16
+ "eth-account>=0.13",
17
+ "requests>=2.31",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://billmyagent.ai"
22
+ Source = "https://github.com/RealMaxPower/payment-processor"
23
+
24
+ [project.optional-dependencies]
25
+ fastapi = ["fastapi>=0.110", "httpx>=0.27"]
26
+ flask = ["flask>=3.0"]
27
+ dev = [
28
+ "pytest>=8.0",
29
+ "ruff>=0.6",
30
+ "mypy>=1.11",
31
+ "types-requests",
32
+ "responses>=0.25",
33
+ ]
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["billmyagent"]
37
+
38
+ [tool.ruff]
39
+ line-length = 100
40
+ target-version = "py310"
41
+
42
+ [tool.mypy]
43
+ python_version = "3.10"
44
+ ignore_missing_imports = true
@@ -0,0 +1,41 @@
1
+ import pytest
2
+ import responses
3
+
4
+ from billmyagent import PaymentClient
5
+
6
+ BASE = "https://api.test/api/v1"
7
+
8
+
9
+ @responses.activate
10
+ def test_create_payment_sends_api_key() -> None:
11
+ responses.add(responses.POST, f"{BASE}/payments", json={"id": "p1", "status": "pending"})
12
+ client = PaymentClient(api_key="sk_test", base_url=BASE)
13
+ out = client.create_payment({"amount": "1", "currency": "USDC"})
14
+ assert out["id"] == "p1"
15
+ assert responses.calls[0].request.headers["X-API-Key"] == "sk_test"
16
+
17
+
18
+ @responses.activate
19
+ def test_list_payments() -> None:
20
+ responses.add(responses.GET, f"{BASE}/payments", json={"data": [], "limit": 20, "offset": 0})
21
+ client = PaymentClient(api_key="sk_test", base_url=BASE)
22
+ assert client.list_payments()["limit"] == 20
23
+
24
+
25
+ def test_merchant_methods_require_auth_token() -> None:
26
+ client = PaymentClient(api_key="sk_test", base_url=BASE)
27
+ with pytest.raises(ValueError):
28
+ client.get_payout_settings()
29
+
30
+
31
+ @responses.activate
32
+ def test_get_payout_settings_sends_bearer() -> None:
33
+ responses.add(
34
+ responses.GET,
35
+ f"{BASE}/merchant/payout-settings",
36
+ json={"settings": {"base_address": "0xabc", "fee_bps": 100}},
37
+ )
38
+ client = PaymentClient(api_key="sk_test", base_url=BASE, auth_token="jwt123")
39
+ settings = client.get_payout_settings()
40
+ assert settings["base_address"] == "0xabc"
41
+ assert responses.calls[0].request.headers["Authorization"] == "Bearer jwt123"
@@ -0,0 +1,30 @@
1
+ import pytest
2
+
3
+ from billmyagent.middleware import build_routes, to_caip2
4
+
5
+
6
+ def test_to_caip2_maps_supported_networks() -> None:
7
+ assert to_caip2("base") == "eip155:8453"
8
+ assert to_caip2("base-sepolia") == "eip155:84532"
9
+ assert to_caip2("polygon") == "eip155:137"
10
+
11
+
12
+ def test_to_caip2_rejects_unsupported() -> None:
13
+ with pytest.raises(ValueError):
14
+ to_caip2("ethereum")
15
+
16
+
17
+ def test_build_routes_string_and_dict_specs() -> None:
18
+ routes = build_routes(
19
+ "0xPayout",
20
+ {"GET /a": "$0.01", "GET /b": {"price": "$1.00", "network": "polygon"}},
21
+ default_network="base",
22
+ )
23
+ a = routes["GET /a"]["accepts"] # type: ignore[index]
24
+ assert a["payTo"] == "0xPayout"
25
+ assert a["price"] == "$0.01"
26
+ assert a["network"] == "eip155:8453" # default network
27
+ assert a["scheme"] == "exact"
28
+
29
+ b = routes["GET /b"]["accepts"] # type: ignore[index]
30
+ assert b["network"] == "eip155:137" # per-route override
@@ -0,0 +1,17 @@
1
+ import pytest
2
+
3
+ from billmyagent import create_signer
4
+
5
+ # Well-known throwaway test key (never use a real key in tests).
6
+ TEST_KEY = "0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
7
+
8
+
9
+ def test_create_signer_returns_account() -> None:
10
+ account = create_signer("base", TEST_KEY)
11
+ assert account.address.startswith("0x")
12
+ assert len(account.address) == 42
13
+
14
+
15
+ def test_create_signer_rejects_unknown_network() -> None:
16
+ with pytest.raises(ValueError):
17
+ create_signer("ethereum", TEST_KEY)