agent-framework-botsrated 0.1.0__py3-none-any.whl

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,28 @@
1
+ """BotsRated pre-payment checks for Microsoft Agent Framework (Python)."""
2
+
3
+ from agent_framework_botsrated._middleware import (
4
+ DECISION_METADATA_KEY,
5
+ BotsRatedPaymentBlocked,
6
+ BotsRatedPaymentMiddleware,
7
+ PaymentQuote,
8
+ QuoteExtractor,
9
+ botsrated_check_payment_tool,
10
+ decision_to_dict,
11
+ default_quote_extractor,
12
+ )
13
+
14
+ BOTSRATED_MCP_URL = "https://botsrated.com/mcp"
15
+ """BotsRated's remote MCP server (streamable HTTP, no auth). Free tools: preflight, coverage, sample, history."""
16
+
17
+ __all__ = [
18
+ "BOTSRATED_MCP_URL",
19
+ "DECISION_METADATA_KEY",
20
+ "BotsRatedPaymentBlocked",
21
+ "BotsRatedPaymentMiddleware",
22
+ "PaymentQuote",
23
+ "QuoteExtractor",
24
+ "botsrated_check_payment_tool",
25
+ "decision_to_dict",
26
+ "default_quote_extractor",
27
+ ]
28
+ __version__ = "0.1.0"
@@ -0,0 +1,168 @@
1
+ """BotsRated pre-payment check for Microsoft Agent Framework function tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from collections.abc import Awaitable, Callable, Collection, Mapping
8
+ from dataclasses import dataclass
9
+ from typing import Annotated, Any, Literal
10
+
11
+ from agent_framework import FunctionInvocationContext, FunctionMiddleware, FunctionTool, MiddlewareFailure, tool
12
+ from botsrated import BotsRatedClient, Decision, PrePaymentGuard, Reason
13
+ from pydantic import BaseModel, Field
14
+
15
+ DECISION_METADATA_KEY = "botsrated_decision"
16
+ """Key of the ``botsrated.Decision`` stored in ``FunctionInvocationContext.metadata`` for checked calls."""
17
+
18
+ DEFAULT_NETWORK = "eip155:8453"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class PaymentQuote:
23
+ """The x402 payment option a payment tool is about to pay."""
24
+
25
+ url: str
26
+ pay_to: str
27
+ amount: str
28
+ network: str = DEFAULT_NETWORK
29
+ scheme: str = "exact"
30
+
31
+ def requirements(self) -> dict[str, str]:
32
+ return {"scheme": self.scheme, "network": self.network, "payTo": self.pay_to, "amount": self.amount}
33
+
34
+
35
+ QuoteExtractor = Callable[[Mapping[str, Any]], PaymentQuote]
36
+ """Reads the payment quote from a payment tool's arguments. Raise any exception if it can't."""
37
+
38
+
39
+ def _first(args: Mapping[str, Any], *names: str) -> Any:
40
+ for name in names:
41
+ value = args.get(name)
42
+ if value not in (None, ""):
43
+ return value
44
+ return None
45
+
46
+
47
+ def default_quote_extractor(args: Mapping[str, Any]) -> PaymentQuote:
48
+ """Read ``url``, ``pay_to`` (or ``payTo``), ``amount`` and optional ``network``/``scheme`` arguments."""
49
+ url = _first(args, "url", "resource_url", "resource")
50
+ pay_to = _first(args, "pay_to", "payTo")
51
+ amount = _first(args, "amount", "amount_atomic", "max_amount_required", "maxAmountRequired")
52
+ if not isinstance(url, str) or not isinstance(pay_to, str) or amount is None:
53
+ missing = [n for n, v in (("url", url), ("pay_to", pay_to), ("amount", amount)) if v is None]
54
+ raise ValueError(f"payment arguments are missing or not text: {', '.join(missing) or 'url/pay_to'}")
55
+ if isinstance(amount, bool) or not str(amount).isdigit():
56
+ raise ValueError("amount must be an integer number of atomic units")
57
+ network = _first(args, "network") or DEFAULT_NETWORK
58
+ scheme = _first(args, "scheme") or "exact"
59
+ return PaymentQuote(url=url, pay_to=pay_to, amount=str(amount), network=str(network), scheme=str(scheme))
60
+
61
+
62
+ class BotsRatedPaymentBlocked(MiddlewareFailure):
63
+ """Raised for a blocked payment when the middleware uses ``on_block="raise"``. It ends the run."""
64
+
65
+ def __init__(self, decision: Decision) -> None:
66
+ super().__init__(decision.summary())
67
+ self.decision = decision
68
+
69
+
70
+ def _as_mapping(arguments: BaseModel | Mapping[str, Any]) -> Mapping[str, Any]:
71
+ if isinstance(arguments, BaseModel):
72
+ return arguments.model_dump(mode="json")
73
+ return arguments
74
+
75
+
76
+ class BotsRatedPaymentMiddleware(FunctionMiddleware):
77
+ """Function middleware that checks an x402 payment with BotsRated before a payment tool runs.
78
+
79
+ Only the functions named in ``payment_tools`` are checked; every other call passes through
80
+ untouched. For a payment tool, the middleware reads the quote (URL, payTo, amount, network)
81
+ from the call's arguments and asks BotsRated's free preflight (no key, no wallet) whether
82
+ that payTo was observed for the seller. A blocked call never reaches ``call_next``: the
83
+ reason becomes the call's result, so the model reads it, or with ``on_block="raise"`` the
84
+ run ends with ``BotsRatedPaymentBlocked``.
85
+
86
+ Default policy (from ``botsrated.GuardPolicy``): a payTo that BotsRated has not seen for the
87
+ host is blocked; an uncovered host or stale data is allowed; a price above the observed one
88
+ is a warning. If BotsRated can't be reached, the payment is allowed (``fail_open=True`` on
89
+ the guard). If the quote can't be read from the arguments, the call is blocked.
90
+
91
+ Args:
92
+ payment_tools: Names of the payment functions to check, or a mapping from name to a
93
+ ``QuoteExtractor`` (``None`` uses ``default_quote_extractor``).
94
+ guard: A ``botsrated.PrePaymentGuard``. Defaults to a free-only guard (nothing is paid).
95
+ on_block: ``"result"`` (default) returns the reason as the call's result; ``"raise"`` ends the run.
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ payment_tools: Collection[str] | Mapping[str, QuoteExtractor | None],
101
+ *,
102
+ guard: PrePaymentGuard | None = None,
103
+ on_block: Literal["result", "raise"] = "result",
104
+ ) -> None:
105
+ if isinstance(payment_tools, str):
106
+ payment_tools = [payment_tools]
107
+ if isinstance(payment_tools, Mapping):
108
+ self.extractors: dict[str, QuoteExtractor] = {
109
+ name: extractor or default_quote_extractor for name, extractor in payment_tools.items()
110
+ }
111
+ else:
112
+ self.extractors = {name: default_quote_extractor for name in payment_tools}
113
+ if not self.extractors:
114
+ raise ValueError("payment_tools must name at least one payment function")
115
+ self.guard = guard or PrePaymentGuard(BotsRatedClient())
116
+ self.on_block = on_block
117
+
118
+ async def process(
119
+ self,
120
+ context: FunctionInvocationContext,
121
+ call_next: Callable[[], Awaitable[None]],
122
+ ) -> None:
123
+ extractor = self.extractors.get(context.function.name)
124
+ if extractor is None:
125
+ await call_next()
126
+ return
127
+ try:
128
+ quote = extractor(_as_mapping(context.arguments))
129
+ except Exception as error: # noqa: BLE001 - any unreadable quote fails closed
130
+ decision = Decision("block", None, [Reason("quote_unreadable", str(error), "block")])
131
+ else:
132
+ decision = await asyncio.to_thread(self.guard.decide, quote.url, quote.requirements())
133
+ context.metadata[DECISION_METADATA_KEY] = decision
134
+ if self.guard.on_decision:
135
+ self.guard.on_decision(decision)
136
+ if decision.action == "allow":
137
+ await call_next()
138
+ return
139
+ if self.on_block == "raise":
140
+ raise BotsRatedPaymentBlocked(decision)
141
+ context.result = f"Payment tool '{context.function.name}' was not executed. {decision.summary()}"
142
+
143
+
144
+ def decision_to_dict(decision: Decision) -> dict[str, Any]:
145
+ return {
146
+ "action": decision.action,
147
+ "host": decision.host,
148
+ "reasons": [{"code": r.code, "detail": r.detail, "effect": r.effect} for r in decision.reasons],
149
+ "receipt_url": decision.facts.receipt_url if decision.facts else None,
150
+ }
151
+
152
+
153
+ def botsrated_check_payment_tool(guard: PrePaymentGuard | None = None) -> FunctionTool:
154
+ """A function tool the model can call to check an x402 payment option before paying it (free preflight)."""
155
+ checker = guard or PrePaymentGuard(BotsRatedClient())
156
+
157
+ async def botsrated_check_payment(
158
+ url: Annotated[str, Field(description="The seller URL you are about to pay.")],
159
+ pay_to: Annotated[str, Field(description="The payTo address from the seller's 402 payment option.")],
160
+ amount: Annotated[str, Field(description="The amount in atomic units from that option.")],
161
+ network: Annotated[str, Field(description="The CAIP-2 network of that option.")] = DEFAULT_NETWORK,
162
+ ) -> str:
163
+ """Check an x402 payment option against BotsRated's measurements BEFORE paying it."""
164
+ quote = PaymentQuote(url=url, pay_to=pay_to, amount=amount, network=network)
165
+ decision = await asyncio.to_thread(checker.decide, quote.url, quote.requirements())
166
+ return json.dumps(decision_to_dict(decision))
167
+
168
+ return tool(botsrated_check_payment)
File without changes
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-framework-botsrated
3
+ Version: 0.1.0
4
+ Summary: BotsRated pre-payment check for Microsoft Agent Framework: function middleware that checks an x402 payTo with the free BotsRated preflight before a payment tool runs.
5
+ Project-URL: Homepage, https://botsrated.com
6
+ Project-URL: Repository, https://github.com/botsrated/botsrated
7
+ Project-URL: Documentation, https://github.com/botsrated/botsrated/blob/main/integrations/microsoft-agent-framework/README.md
8
+ Project-URL: Issues, https://github.com/botsrated/botsrated/issues
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent-framework,base,botsrated,microsoft-agent-framework,middleware,payments,usdc,x402
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: agent-framework-core<2,>=1.22
14
+ Requires-Dist: botsrated<0.2,>=0.1.2
15
+ Provides-Extra: mcp
16
+ Requires-Dist: mcp<3,>=1.24; extra == 'mcp'
17
+ Requires-Dist: tenacity<10,>=9.1.4; extra == 'mcp'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # agent-framework-botsrated: BotsRated pre-payment check for Microsoft Agent Framework (Python)
21
+
22
+ Function middleware for [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) that checks an
23
+ x402 payment with [BotsRated](https://botsrated.com) **before a payment tool runs**. BotsRated probes x402 sellers on
24
+ Base and records which `payTo` wallet each one actually quoted. The middleware asks BotsRated's free preflight
25
+ (no API key, no wallet) whether the `payTo` your agent is about to pay was observed for that seller, and stops the call
26
+ if it wasn't.
27
+
28
+ Maintained by BotsRated, not by Microsoft. Issues: https://github.com/botsrated/botsrated/issues/new
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install "agent-framework-botsrated @ git+https://github.com/botsrated/botsrated#subdirectory=integrations/microsoft-agent-framework"
34
+ # with the hosted MCP server example:
35
+ pip install "agent-framework-botsrated[mcp] @ git+https://github.com/botsrated/botsrated#subdirectory=integrations/microsoft-agent-framework"
36
+ ```
37
+
38
+ Requires Python 3.10+, `agent-framework-core>=1.22,<2` and [`botsrated`](https://pypi.org/project/botsrated/) 0.1.2+.
39
+ Tested with agent-framework-core 1.22.0 (with mcp 1.30) and 1.23.0 (with mcp 2.3). agent-framework-core 1.22 can't
40
+ initialize MCP sessions with mcp 2.x, so pin `mcp<2` there if you use the hosted MCP server.
41
+
42
+ ## Middleware: check payments before they run
43
+
44
+ ```python
45
+ from agent_framework import Agent
46
+ from agent_framework_botsrated import BotsRatedPaymentMiddleware
47
+
48
+
49
+ def pay_x402(url: str, pay_to: str, amount: str, network: str = "eip155:8453") -> str:
50
+ """Pay an x402 seller."""
51
+ ... # your payment code
52
+
53
+
54
+ agent = Agent(
55
+ client=chat_client,
56
+ tools=[pay_x402],
57
+ middleware=[BotsRatedPaymentMiddleware(["pay_x402"])],
58
+ )
59
+ ```
60
+
61
+ - Only the functions you name are checked. Every other function call passes through untouched.
62
+ - The quote is read from the call's arguments: `url`, `pay_to` (or `payTo`), `amount` (atomic units) and optional
63
+ `network` (CAIP-2, default `eip155:8453`). For other argument names pass an extractor:
64
+ `BotsRatedPaymentMiddleware({"buy": lambda a: PaymentQuote(url=a["endpoint"], pay_to=a["recipient"], amount=str(a["atomic"]))})`.
65
+ - A blocked call never reaches `call_next`. The reason (with a link to the BotsRated probe receipt) becomes the call's
66
+ result, so the model reads it. With `on_block="raise"` the run ends with `BotsRatedPaymentBlocked` (a
67
+ `MiddlewareFailure`).
68
+ - The `botsrated.Decision` is stored in `context.metadata["botsrated_decision"]`; `guard.on_decision` is also called.
69
+
70
+ Default policy (from `botsrated.GuardPolicy`, configurable through `guard=PrePaymentGuard(policy=...)`):
71
+
72
+ | Situation | Result |
73
+ |---|---|
74
+ | `payTo` never observed by BotsRated for that host (same network) | **blocked** (`payto_mismatch`) |
75
+ | Quote can't be read from the arguments | **blocked** (`quote_unreadable`) |
76
+ | Price above the last observed price on the same route | allowed with a warning |
77
+ | Host not covered, or data older than 24 h | allowed (info) |
78
+ | BotsRated unreachable | allowed (`fail_open=True`); set `PrePaymentGuard(fail_open=False)` to block |
79
+
80
+ The middleware checks what the payment tool was asked to pay. If your tool fetches the 402 itself and signs it, also
81
+ register `botsrated.PrePaymentGuard().x402_hook` on your `x402Client`'s `on_before_payment_creation` so the check
82
+ runs on the exact quote being signed.
83
+
84
+ ## Tool: let the model check a payment option
85
+
86
+ ```python
87
+ from agent_framework_botsrated import botsrated_check_payment_tool
88
+
89
+ agent = Agent(client=chat_client, tools=[botsrated_check_payment_tool(), pay_x402])
90
+ ```
91
+
92
+ `botsrated_check_payment(url, pay_to, amount, network)` returns JSON with `action` (`allow`/`block`), `reasons` and
93
+ `receipt_url`. It uses the free preflight only.
94
+
95
+ ## Hosted MCP server (no package needed)
96
+
97
+ BotsRated also runs a remote MCP server (streamable HTTP, no auth). Its free tools are `preflight`, `coverage`,
98
+ `sample` and `history`:
99
+
100
+ ```python
101
+ from agent_framework import MCPStreamableHTTPTool
102
+
103
+ botsrated_mcp = MCPStreamableHTTPTool(
104
+ name="botsrated",
105
+ url="https://botsrated.com/mcp",
106
+ allowed_tools=["preflight", "coverage", "sample", "history"],
107
+ )
108
+ async with botsrated_mcp:
109
+ result = await Agent(client=chat_client, tools=[botsrated_mcp]).run("Is blockrun.ai a known x402 seller?")
110
+ ```
111
+
112
+ The paid lookups (`check` $0.001, `report` $0.002, `best` $0.01 USDC on Base; the first 100 checks and 10 reports per
113
+ client per day are free) are not used by this package unless you pass a paid-enabled `BotsRatedClient` to the guard.
114
+
115
+ ## Run the example and tests
116
+
117
+ ```bash
118
+ pip install -e ".[mcp]" pytest pytest-asyncio
119
+ python examples/quickstart.py # scripted model, real free preflight, stub payment
120
+ pytest -m "not live" # offline
121
+ pytest -m live # real free BotsRated preflight + hosted MCP server; nothing is paid
122
+ ```
123
+
124
+ `examples/quickstart.py` uses a scripted chat client instead of a model, so it runs without an API key. One payment
125
+ call uses an unknown `payTo` and is blocked; the other uses the `payTo` BotsRated observed for blockrun.ai and runs.
126
+
127
+ BotsRated results are measurements, not endorsements. MIT licensed.
@@ -0,0 +1,7 @@
1
+ agent_framework_botsrated/__init__.py,sha256=JS6gucnzE3OTWNgM5pwkGB3RqsS-7_HMLTkCR8szvic,793
2
+ agent_framework_botsrated/_middleware.py,sha256=2DttyywIlrOBb7IWFY50h-b7j1M7rIHBtgqtFNkBNR0,7740
3
+ agent_framework_botsrated/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ agent_framework_botsrated-0.1.0.dist-info/METADATA,sha256=wSCGL6rUANn8Y93roKoMajbx3bhG28P2v0yhk8QEhw4,6147
5
+ agent_framework_botsrated-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ agent_framework_botsrated-0.1.0.dist-info/licenses/LICENSE,sha256=3mkSwjwOJ_YSnozCObgbawNbWffdiM2Z04kunzx7yBg,1066
7
+ agent_framework_botsrated-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BotsRated
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.