apay 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.
apay/__init__.py ADDED
@@ -0,0 +1,44 @@
1
+ """
2
+ A-Pay (AgentPay) Protocol
3
+ Universal Micropayment & Economic Layer for Autonomous AI Agents.
4
+ """
5
+
6
+ from .client import APayClient, APaySession
7
+ from .integrations import (
8
+ APayCrewTool,
9
+ APayRequestsWrapper,
10
+ APayTool,
11
+ create_apay_tool,
12
+ create_crew_tool,
13
+ )
14
+ from .proxy import create_proxy_app
15
+ from .relayer import ClaimJournal, ClaimRecord, SettlementRelayer
16
+ from .wallet import (
17
+ DEFAULT_CHAIN_ID,
18
+ DEFAULT_EIP712_NAME,
19
+ DEFAULT_EIP712_VERSION,
20
+ DEFAULT_ROUTER_ADDRESS,
21
+ AgentWallet,
22
+ get_eip712_domain,
23
+ )
24
+
25
+ __all__ = [
26
+ "APayClient",
27
+ "APayCrewTool",
28
+ "APayRequestsWrapper",
29
+ "APaySession",
30
+ "APayTool",
31
+ "AgentWallet",
32
+ "ClaimJournal",
33
+ "ClaimRecord",
34
+ "DEFAULT_CHAIN_ID",
35
+ "DEFAULT_EIP712_NAME",
36
+ "DEFAULT_EIP712_VERSION",
37
+ "DEFAULT_ROUTER_ADDRESS",
38
+ "SettlementRelayer",
39
+ "create_apay_tool",
40
+ "create_crew_tool",
41
+ "create_proxy_app",
42
+ "get_eip712_domain",
43
+ ]
44
+ __version__ = "0.1.0"
apay/cli.py ADDED
@@ -0,0 +1,216 @@
1
+ """A-Pay CLI: 1-Command Tooling for Website Owners & AI Agent Builders."""
2
+
3
+ import time
4
+
5
+ import click
6
+ import httpx
7
+ import uvicorn
8
+
9
+ from .client import APaySession
10
+ from .proxy import create_proxy_app
11
+ from .relayer import ClaimJournal, SettlementRelayer
12
+ from .wallet import AgentWallet
13
+
14
+
15
+ @click.group()
16
+ @click.version_option("0.1.0", prog_name="apay")
17
+ def main() -> None:
18
+ """⚡ A-Pay: Universal HTTP 402 Micropayment Rail for AI Agents & APIs."""
19
+ pass
20
+
21
+
22
+ @main.command()
23
+ @click.option(
24
+ "--target", "-t", required=True, help="Upstream API/website URL to protect (e.g. http://localhost:8000)"
25
+ )
26
+ @click.option("--price", "-p", required=True, type=float, help="Price per request in USDC (e.g. 0.001)")
27
+ @click.option("--wallet", "-w", required=True, help="Receiver wallet address to collect funds (0x...)")
28
+ @click.option("--port", default=8080, type=int, help="Port to run the A-Pay Gateway on (default: 8080)")
29
+ @click.option("--host", default="0.0.0.0", help="Host to bind gateway (default: 0.0.0.0)")
30
+ @click.option("--network", default="base", help="Settlement network (default: base)")
31
+ def protect(target: str, price: float, wallet: str, port: int, host: str, network: str) -> None:
32
+ """🛡️ Protect ANY website or API with an instant 1-command HTTP 402 Paywall."""
33
+ click.echo(click.style("==================================================", fg="cyan", bold=True))
34
+ click.echo(click.style("⚡ A-PAY GATEWAY IS ACTIVE", fg="green", bold=True))
35
+ click.echo(click.style("==================================================", fg="cyan", bold=True))
36
+ click.echo(f" Target URL : {target}")
37
+ click.echo(f" Paywall Price : ${price} USDC / request")
38
+ click.echo(f" Receiver Wallet : {wallet}")
39
+ click.echo(f" Settlement Rail : {network}")
40
+ click.echo(f" Gateway Running : http://{host}:{port}")
41
+ click.echo(click.style("\nIncoming AI bots will be challenged with HTTP 402 unless paid.\n", fg="yellow"))
42
+
43
+ app = create_proxy_app(target_url=target, receiver_wallet=wallet, price_usdc=price, network=network)
44
+ uvicorn.run(app, host=host, port=port, log_level="info")
45
+
46
+
47
+ @main.group()
48
+ def wallet() -> None:
49
+ """🔐 Manage Agent Wallets & Cryptographic Keys."""
50
+ pass
51
+
52
+
53
+ @wallet.command("new")
54
+ @click.option("--max-call", default=0.05, type=float, help="Max spend limit per API call in USDC")
55
+ @click.option("--daily-budget", default=5.0, type=float, help="Max total daily budget in USDC")
56
+ def wallet_new(max_call: float, daily_budget: float) -> None:
57
+ """Generate a new cryptographic Agent Wallet."""
58
+ w = AgentWallet.create(max_per_call_usdc=max_call, daily_budget_usdc=daily_budget)
59
+ click.echo(click.style("\n✨ New Agent Wallet Generated Successfully!\n", fg="green", bold=True))
60
+ click.echo(f" Address : {click.style(w.address, fg='cyan', bold=True)}")
61
+ click.echo(f" Private Key : {click.style(w.private_key, fg='red')}")
62
+ click.echo(f" Max Per Call : ${w.max_per_call_usdc} USDC")
63
+ click.echo(f" Daily Budget : ${w.daily_budget_usdc} USDC")
64
+ click.echo(
65
+ click.style("\n⚠️ Keep the private key safe. Pass it to APaySession(private_key=...)\n", fg="yellow")
66
+ )
67
+
68
+
69
+ @main.command()
70
+ @click.option("--url", "-u", required=True, help="URL of the protected API endpoint")
71
+ @click.option("--key", "-k", required=True, help="Agent's private key (0x...)")
72
+ @click.option("--budget", "-b", default=5.0, type=float, help="Daily budget limit in USDC")
73
+ def pay(url: str, key: str, budget: float) -> None:
74
+ """🤖 Make a test auto-settling request to an A-Pay protected URL."""
75
+ click.echo(f"Contacting {url} with agent wallet...")
76
+ agent_wallet = AgentWallet(private_key=key, daily_budget_usdc=budget)
77
+ session = APaySession(agent_wallet)
78
+ try:
79
+ resp = session.get(url)
80
+ click.echo(click.style(f"\n✅ Status: {resp.status_code}", fg="green", bold=True))
81
+ click.echo(f"X-APay-Settled: {resp.headers.get('X-APay-Settled')}")
82
+ click.echo(f"Response Body : {resp.text[:300]}")
83
+ except Exception as e:
84
+ click.echo(click.style(f"❌ Error: {e!s}", fg="red"))
85
+ finally:
86
+ session.close()
87
+
88
+
89
+ @main.group()
90
+ def claims() -> None:
91
+ """📊 Inspect & manage offline verified micropayment claims."""
92
+ pass
93
+
94
+
95
+ @claims.command("list")
96
+ def claims_list() -> None:
97
+ """List all collected claims waiting for on-chain settlement."""
98
+ journal = ClaimJournal()
99
+ all_claims = journal.load_all()
100
+ unsettled = [c for c in all_claims if not c.settled]
101
+ total_unsettled_usdc = sum(c.price_usdc for c in unsettled)
102
+
103
+ click.echo(click.style("\n📜 A-Pay Claims Ledger Summary:\n", fg="cyan", bold=True))
104
+ click.echo(f" Total Claims Recorded : {len(all_claims)}")
105
+ click.echo(f" Unsettled Claims : {len(unsettled)}")
106
+ click.echo(f" Pending Gross Value : ${total_unsettled_usdc:.4f} USDC")
107
+
108
+ if unsettled:
109
+ click.echo("\n Recent Pending Claims:")
110
+ for c in unsettled[-5:]:
111
+ click.echo(
112
+ f" • [{c.id}] Payer: {c.payer[:10]}... | Amount: ${c.price_usdc:.4f} USDC | Nonce: {c.nonce[:8]}..."
113
+ )
114
+ click.echo(
115
+ click.style("\nRun `apay settle --dry-run` to preview batch settlement breakdown.\n", fg="yellow")
116
+ )
117
+ else:
118
+ click.echo(click.style("\n✨ All claims are settled or ledger is clean.\n", fg="green"))
119
+
120
+
121
+ @claims.command("clear")
122
+ def claims_clear() -> None:
123
+ """Clear claims journal."""
124
+ journal = ClaimJournal()
125
+ journal.clear()
126
+ click.echo(click.style("✅ Claims ledger cleared successfully.", fg="green"))
127
+
128
+
129
+ @main.command()
130
+ @click.option("--key", "-k", help="Merchant / Relayer private key to sign on-chain settlement")
131
+ @click.option("--dry-run", is_flag=True, help="Simulate batch settlement without broadcasting on-chain")
132
+ def settle(key: str | None, dry_run: bool) -> None:
133
+ """⚡ Batch settle pending micropayment claims onto Base L2 APayRouter."""
134
+ relayer = SettlementRelayer()
135
+ unsettled = relayer.journal.get_unsettled()
136
+
137
+ if not unsettled:
138
+ click.echo(click.style("\n✨ No unsettled claims found in ledger.", fg="yellow"))
139
+ return
140
+
141
+ simulation = relayer.simulate_batch(unsettled)
142
+
143
+ click.echo(click.style("\n==================================================", fg="cyan", bold=True))
144
+ click.echo(click.style("⚡ A-PAY BATCH SETTLEMENT ENGINE", fg="green", bold=True))
145
+ click.echo(click.style("==================================================", fg="cyan", bold=True))
146
+ click.echo(f" Settlement Rail : {simulation['settlement_rail']}")
147
+ click.echo(f" Target Claims Count : {simulation['claims_count']}")
148
+ click.echo(f" Gross Volume : ${simulation['gross_volume_usdc']:.4f} USDC")
149
+ click.echo(f" Protocol Fee (1.0%) : ${simulation['protocol_fee_usdc']:.6f} USDC")
150
+ click.echo(f" Net Merchant Payout : ${simulation['net_merchant_usdc']:.4f} USDC")
151
+ click.echo(f" Router Contract : {simulation['router_contract']}")
152
+
153
+ if dry_run or not key:
154
+ click.echo(
155
+ click.style(
156
+ "\nℹ️ Dry-run mode completed. Pass `--key 0x...` to broadcast live to Base Sepolia.\n",
157
+ fg="yellow",
158
+ )
159
+ )
160
+ return
161
+
162
+ click.echo("\n🚀 Broadcasting batch settlement to Base L2...")
163
+ result = relayer.execute_settle(private_key=key, dry_run=False)
164
+ click.echo(
165
+ click.style(f"✅ Batch Settled Successfully! Status: {result['status']}", fg="green", bold=True)
166
+ )
167
+ click.echo(f" Transaction Hash : {result['tx_hash']}")
168
+ click.echo(f" Basescan Explorer : {result['explorer_url']}\n")
169
+
170
+
171
+ @main.command()
172
+ def verify() -> None:
173
+ """🔬 Multi-dimensional system, cryptographic benchmark & RPC health check."""
174
+ click.echo(
175
+ click.style("\n⚡ Running A-Pay Multi-Dimensional System Health Check...\n", fg="cyan", bold=True)
176
+ )
177
+
178
+ # 1. Benchmark Cryptographic Signing
179
+ w = AgentWallet.create()
180
+ t0 = time.perf_counter()
181
+ iterations = 50
182
+ for i in range(iterations):
183
+ sig = w.sign_challenge(f"APAY:0x5eC0:1000:{i}")
184
+ AgentWallet.verify_signature(f"APAY:0x5eC0:1000:{i}", sig)
185
+ avg_latency_ms = ((time.perf_counter() - t0) / iterations) * 1000.0
186
+
187
+ click.echo(f" 🔐 Cryptographic Sign & Verify Latency : {avg_latency_ms:.2f} ms / op (Target: < 20ms)")
188
+ if avg_latency_ms < 20.0:
189
+ click.echo(click.style(" -> PASS: Ultra-low latency machine-to-machine verified.", fg="green"))
190
+ else:
191
+ click.echo(click.style(" -> WARN: Higher than expected latency.", fg="yellow"))
192
+
193
+ # 2. Check RPC Node Connectivity
194
+ rpc_url = "https://sepolia.base.org"
195
+ try:
196
+ with httpx.Client(timeout=5.0) as client:
197
+ resp = client.post(
198
+ rpc_url, json={"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1}
199
+ )
200
+ if resp.status_code == 200 and "result" in resp.json():
201
+ block_hex = resp.json()["result"]
202
+ block_num = int(block_hex, 16)
203
+ click.echo(f" 🌐 Base Sepolia RPC Connectivity : Connected (Block #{block_num:,})")
204
+ click.echo(click.style(" -> PASS: Live on-chain rail active.", fg="green"))
205
+ else:
206
+ click.echo(click.style(" -> WARN: RPC node returned non-standard response.", fg="yellow"))
207
+ except Exception as err:
208
+ click.echo(click.style(f" 🌐 Base Sepolia RPC Error : {err!s}", fg="yellow"))
209
+
210
+ click.echo(
211
+ click.style("\n✅ A-Pay Protocol Core is operating at 100% capacity.\n", fg="green", bold=True)
212
+ )
213
+
214
+
215
+ if __name__ == "__main__":
216
+ main()
apay/client.py ADDED
@@ -0,0 +1,146 @@
1
+ """Async & Sync HTTP Clients with Automatic HTTP 402 Micropayment Negotiation."""
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from .wallet import DEFAULT_CHAIN_ID, DEFAULT_ROUTER_ADDRESS, AgentWallet
8
+
9
+
10
+ class APayClient:
11
+ """Asynchronous HTTP Client for AI Agents with auto-settling 402 handling."""
12
+
13
+ def __init__(self, wallet: AgentWallet, timeout: float = 30.0, use_eip712: bool = True) -> None:
14
+ self.wallet = wallet
15
+ self.client = httpx.AsyncClient(timeout=timeout)
16
+ self.use_eip712 = use_eip712
17
+
18
+ async def get(self, url: str, **kwargs: Any) -> httpx.Response:
19
+ return await self._request("GET", url, **kwargs)
20
+
21
+ async def post(self, url: str, **kwargs: Any) -> httpx.Response:
22
+ return await self._request("POST", url, **kwargs)
23
+
24
+ async def _request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
25
+ # 1. Initial attempt
26
+ response = await self.client.request(method, url, **kwargs)
27
+
28
+ # 2. Intercept HTTP 402 Payment Required
29
+ if response.status_code == 402:
30
+ return await self._handle_402_and_retry(method, url, response, **kwargs)
31
+
32
+ return response
33
+
34
+ async def _handle_402_and_retry(
35
+ self, method: str, url: str, initial_response: httpx.Response, **kwargs: Any
36
+ ) -> httpx.Response:
37
+ headers = initial_response.headers
38
+ raw_amount = headers.get("X-APay-Amount", "0")
39
+ try:
40
+ price = float(raw_amount) / 1_000_000.0 # 6 decimals for USDC
41
+ except (ValueError, TypeError):
42
+ return initial_response
43
+ receiver = headers.get("X-APay-Receiver")
44
+ nonce = headers.get("X-APay-Nonce")
45
+
46
+ if not receiver or not nonce:
47
+ # Not a standard APay 402 response, return original response
48
+ return initial_response
49
+
50
+ # Check local wallet budget guardrails
51
+ can_pay, reason = self.wallet.can_spend(price)
52
+ if not can_pay:
53
+ raise PermissionError(f"[A-Pay Guardrail Triggered] Cannot pay ${price} USDC: {reason}")
54
+
55
+ scheme = headers.get("X-APay-Scheme", "eip712" if self.use_eip712 else "legacy")
56
+ chain_id = int(headers.get("X-APay-Chain-Id", str(DEFAULT_CHAIN_ID)))
57
+ router = headers.get("X-APay-Router", DEFAULT_ROUTER_ADDRESS)
58
+
59
+ if self.use_eip712 and scheme == "eip712":
60
+ signature = self.wallet.sign_typed_payment(
61
+ receiver=receiver,
62
+ amount_base_units=raw_amount,
63
+ nonce=nonce,
64
+ chain_id=chain_id,
65
+ router_address=router,
66
+ )
67
+ auth_header = f"APay eip712:{signature}:{self.wallet.address}:{nonce}:{raw_amount}"
68
+ else:
69
+ payload = f"APAY:{receiver}:{raw_amount}:{nonce}"
70
+ signature = self.wallet.sign_challenge(payload)
71
+ auth_header = f"APay sig:{signature}:{self.wallet.address}:{nonce}:{raw_amount}"
72
+
73
+ req_headers = dict(kwargs.get("headers") or {})
74
+ req_headers["Authorization"] = auth_header
75
+ kwargs["headers"] = req_headers
76
+
77
+ self.wallet.record_spend(price)
78
+ return await self.client.request(method, url, **kwargs)
79
+
80
+ async def aclose(self) -> None:
81
+ await self.client.aclose()
82
+
83
+
84
+ class APaySession:
85
+ """Synchronous HTTP Session for simple Python scripts, LangChain, and CrewAI."""
86
+
87
+ def __init__(self, wallet: AgentWallet, timeout: float = 30.0, use_eip712: bool = True) -> None:
88
+ self.wallet = wallet
89
+ self.client = httpx.Client(timeout=timeout)
90
+ self.use_eip712 = use_eip712
91
+
92
+ def get(self, url: str, **kwargs: Any) -> httpx.Response:
93
+ return self._request("GET", url, **kwargs)
94
+
95
+ def post(self, url: str, **kwargs: Any) -> httpx.Response:
96
+ return self._request("POST", url, **kwargs)
97
+
98
+ def _request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
99
+ response = self.client.request(method, url, **kwargs)
100
+
101
+ if response.status_code == 402:
102
+ headers = response.headers
103
+ raw_amount = headers.get("X-APay-Amount", "0")
104
+ try:
105
+ price = float(raw_amount) / 1_000_000.0
106
+ except (ValueError, TypeError):
107
+ return response
108
+ receiver = headers.get("X-APay-Receiver")
109
+ nonce = headers.get("X-APay-Nonce")
110
+
111
+ if not receiver or not nonce:
112
+ return response
113
+
114
+ can_pay, reason = self.wallet.can_spend(price)
115
+ if not can_pay:
116
+ raise PermissionError(f"[A-Pay Guardrail Triggered] Cannot pay ${price} USDC: {reason}")
117
+
118
+ scheme = headers.get("X-APay-Scheme", "eip712" if self.use_eip712 else "legacy")
119
+ chain_id = int(headers.get("X-APay-Chain-Id", str(DEFAULT_CHAIN_ID)))
120
+ router = headers.get("X-APay-Router", DEFAULT_ROUTER_ADDRESS)
121
+
122
+ if self.use_eip712 and scheme == "eip712":
123
+ signature = self.wallet.sign_typed_payment(
124
+ receiver=receiver,
125
+ amount_base_units=raw_amount,
126
+ nonce=nonce,
127
+ chain_id=chain_id,
128
+ router_address=router,
129
+ )
130
+ auth_header = f"APay eip712:{signature}:{self.wallet.address}:{nonce}:{raw_amount}"
131
+ else:
132
+ payload = f"APAY:{receiver}:{raw_amount}:{nonce}"
133
+ signature = self.wallet.sign_challenge(payload)
134
+ auth_header = f"APay sig:{signature}:{self.wallet.address}:{nonce}:{raw_amount}"
135
+
136
+ req_headers = dict(kwargs.get("headers") or {})
137
+ req_headers["Authorization"] = auth_header
138
+ kwargs["headers"] = req_headers
139
+
140
+ self.wallet.record_spend(price)
141
+ return self.client.request(method, url, **kwargs)
142
+
143
+ return response
144
+
145
+ def close(self) -> None:
146
+ self.client.close()
@@ -0,0 +1,15 @@
1
+ """A-Pay Agent Framework Integrations.
2
+
3
+ Provides integrations with LangChain, CrewAI, and other autonomous agent ecosystems.
4
+ """
5
+
6
+ from .crewai import APayCrewTool, create_crew_tool
7
+ from .langchain import APayRequestsWrapper, APayTool, create_apay_tool
8
+
9
+ __all__ = [
10
+ "APayCrewTool",
11
+ "APayRequestsWrapper",
12
+ "APayTool",
13
+ "create_apay_tool",
14
+ "create_crew_tool",
15
+ ]
@@ -0,0 +1,92 @@
1
+ """CrewAI Integration for A-Pay Protocol.
2
+
3
+ Enables CrewAI autonomous agents to seamlessly execute HTTP 402 micropayments
4
+ when accessing restricted data feeds or services.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from apay.client import APayClient, APaySession
12
+ from apay.wallet import AgentWallet
13
+
14
+ try:
15
+ from crewai.tools import BaseTool as CrewAIBaseTool
16
+ except ImportError: # pragma: no cover
17
+ CrewAIBaseTool = None # type: ignore[assignment, misc]
18
+
19
+
20
+ class APayCrewToolInput(BaseModel):
21
+ """Input arguments schema for CrewAI tool execution."""
22
+
23
+ query: str = Field(default="", description="Input or parameters passed to the paid API endpoint")
24
+
25
+
26
+ class _BaseCrewToolImpl(BaseModel):
27
+ name: str
28
+ description: str
29
+ target_url: str
30
+ wallet: AgentWallet
31
+ method: str = "GET"
32
+ use_eip712: bool = True
33
+ model_config = ConfigDict(arbitrary_types_allowed=True)
34
+
35
+ def _run(self, *args: Any, **kwargs: Any) -> str:
36
+ """Synchronously call API with auto-negotiating HTTP 402 payments."""
37
+ query = str(kwargs.get("query", args[0] if args else ""))
38
+ session = APaySession(wallet=self.wallet, use_eip712=self.use_eip712)
39
+ try:
40
+ if self.method.upper() == "GET":
41
+ params = {"q": query} if query else {}
42
+ resp = session.get(self.target_url, params=params)
43
+ else:
44
+ payload = {"query": query} if query else {}
45
+ resp = session.post(self.target_url, json=payload)
46
+ return resp.text
47
+ finally:
48
+ session.close()
49
+
50
+ async def _arun(self, *args: Any, **kwargs: Any) -> str:
51
+ """Asynchronously call API with auto-negotiating HTTP 402 payments."""
52
+ query = str(kwargs.get("query", args[0] if args else ""))
53
+ client = APayClient(wallet=self.wallet, use_eip712=self.use_eip712)
54
+ try:
55
+ if self.method.upper() == "GET":
56
+ params = {"q": query} if query else {}
57
+ resp = await client.get(self.target_url, params=params)
58
+ else:
59
+ payload = {"query": query} if query else {}
60
+ resp = await client.post(self.target_url, json=payload)
61
+ return resp.text
62
+ finally:
63
+ await client.aclose()
64
+
65
+
66
+ if CrewAIBaseTool is not None: # pragma: no cover
67
+
68
+ class APayCrewTool(_BaseCrewToolImpl, CrewAIBaseTool): # type: ignore[misc]
69
+ args_schema: type[BaseModel] = APayCrewToolInput
70
+ else:
71
+
72
+ class APayCrewTool(_BaseCrewToolImpl): # type: ignore[no-redef]
73
+ args_schema: type[BaseModel] = APayCrewToolInput
74
+
75
+
76
+ def create_crew_tool(
77
+ name: str,
78
+ description: str,
79
+ target_url: str,
80
+ wallet: AgentWallet,
81
+ method: str = "GET",
82
+ use_eip712: bool = True,
83
+ ) -> APayCrewTool:
84
+ """Factory helper to equip a CrewAI Agent with an A-Pay micropayment tool."""
85
+ return APayCrewTool(
86
+ name=name,
87
+ description=description,
88
+ target_url=target_url,
89
+ wallet=wallet,
90
+ method=method,
91
+ use_eip712=use_eip712,
92
+ )
@@ -0,0 +1,137 @@
1
+ """LangChain Integration for A-Pay Protocol.
2
+
3
+ Provides drop-in tools and requests wrappers enabling LangChain AI agents
4
+ to autonomously navigate HTTP 402 payment-gated APIs using AgentWallet.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from apay.client import APayClient, APaySession
12
+ from apay.wallet import AgentWallet
13
+
14
+ try:
15
+ from langchain_core.tools import BaseTool as LangchainBaseTool
16
+ except ImportError: # pragma: no cover
17
+ LangchainBaseTool = None # type: ignore[assignment, misc]
18
+
19
+
20
+ class APayRequestsWrapper:
21
+ """Drop-in HTTP client wrapper for LangChain with automated HTTP 402 negotiation."""
22
+
23
+ def __init__(self, wallet: AgentWallet, use_eip712: bool = True, timeout: float = 30.0) -> None:
24
+ self.wallet = wallet
25
+ self.session = APaySession(wallet=wallet, timeout=timeout, use_eip712=use_eip712)
26
+ self.async_client = APayClient(wallet=wallet, timeout=timeout, use_eip712=use_eip712)
27
+
28
+ def get(self, url: str, **kwargs: Any) -> str:
29
+ """Synchronously execute GET request, auto-negotiating HTTP 402 if challenged."""
30
+ resp = self.session.get(url, **kwargs)
31
+ return resp.text
32
+
33
+ def post(self, url: str, **kwargs: Any) -> str:
34
+ """Synchronously execute POST request, auto-negotiating HTTP 402 if challenged."""
35
+ resp = self.session.post(url, **kwargs)
36
+ return resp.text
37
+
38
+ async def aget(self, url: str, **kwargs: Any) -> str:
39
+ """Asynchronously execute GET request, auto-negotiating HTTP 402 if challenged."""
40
+ resp = await self.async_client.get(url, **kwargs)
41
+ return resp.text
42
+
43
+ async def apost(self, url: str, **kwargs: Any) -> str:
44
+ """Asynchronously execute POST request, auto-negotiating HTTP 402 if challenged."""
45
+ resp = await self.async_client.post(url, **kwargs)
46
+ return resp.text
47
+
48
+ def close(self) -> None:
49
+ self.session.close()
50
+
51
+ async def aclose(self) -> None:
52
+ await self.async_client.aclose()
53
+
54
+
55
+ class APayToolInput(BaseModel):
56
+ """Input arguments for invoking an A-Pay gated tool."""
57
+
58
+ query: str = Field(default="", description="Query string or instructions to pass to the endpoint")
59
+
60
+
61
+ class _BaseToolImpl(BaseModel):
62
+ name: str
63
+ description: str
64
+ target_url: str
65
+ wallet: AgentWallet
66
+ method: str = "GET"
67
+ use_eip712: bool = True
68
+ model_config = ConfigDict(arbitrary_types_allowed=True)
69
+
70
+ def _execute(self, query: str = "") -> str:
71
+ session = APaySession(wallet=self.wallet, use_eip712=self.use_eip712)
72
+ try:
73
+ url = self.target_url
74
+ if self.method.upper() == "GET":
75
+ params = {"q": query} if query else {}
76
+ resp = session.get(url, params=params)
77
+ else:
78
+ payload = {"query": query} if query else {}
79
+ resp = session.post(url, json=payload)
80
+ return resp.text
81
+ finally:
82
+ session.close()
83
+
84
+ async def _aexecute(self, query: str = "") -> str:
85
+ client = APayClient(wallet=self.wallet, use_eip712=self.use_eip712)
86
+ try:
87
+ url = self.target_url
88
+ if self.method.upper() == "GET":
89
+ params = {"q": query} if query else {}
90
+ resp = await client.get(url, params=params)
91
+ else:
92
+ payload = {"query": query} if query else {}
93
+ resp = await client.post(url, json=payload)
94
+ return resp.text
95
+ finally:
96
+ await client.aclose()
97
+
98
+ def run(self, tool_input: str | dict[str, Any] = "") -> str:
99
+ q = tool_input if isinstance(tool_input, str) else str(tool_input.get("query", ""))
100
+ return self._execute(query=q)
101
+
102
+ def _run(self, *args: Any, **kwargs: Any) -> str:
103
+ q = str(kwargs.get("query", args[0] if args else ""))
104
+ return self._execute(query=q)
105
+
106
+ async def _arun(self, *args: Any, **kwargs: Any) -> str:
107
+ q = str(kwargs.get("query", args[0] if args else ""))
108
+ return await self._aexecute(query=q)
109
+
110
+
111
+ if LangchainBaseTool is not None: # pragma: no cover
112
+
113
+ class APayTool(_BaseToolImpl, LangchainBaseTool): # type: ignore[misc]
114
+ args_schema: type[BaseModel] = APayToolInput
115
+ else:
116
+
117
+ class APayTool(_BaseToolImpl): # type: ignore[no-redef]
118
+ args_schema: type[BaseModel] = APayToolInput
119
+
120
+
121
+ def create_apay_tool(
122
+ name: str,
123
+ description: str,
124
+ target_url: str,
125
+ wallet: AgentWallet,
126
+ method: str = "GET",
127
+ use_eip712: bool = True,
128
+ ) -> APayTool:
129
+ """Convenience factory to create a LangChain-compatible tool for an HTTP 402 API."""
130
+ return APayTool(
131
+ name=name,
132
+ description=description,
133
+ target_url=target_url,
134
+ wallet=wallet,
135
+ method=method,
136
+ use_eip712=use_eip712,
137
+ )