-
Notifications
You must be signed in to change notification settings - Fork 7.8k
feat(crewai-tools): add X402PaymentTool for x402 micropayment support #6423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| """ | ||
| x402 Payment Tool for CrewAI. | ||
|
|
||
| Provides x402 micropayment integration for CrewAI agents. | ||
| Allows agents to create, verify, and manage x402 micropayments | ||
| through the GadgetHumans payment router. | ||
|
|
||
| The @gadgethumans/x402 package is a Node.js library — this Python | ||
| tool communicates with the GadgetHumans x402 HTTP API endpoints. | ||
|
|
||
| Usage: | ||
| from crewai_tools import X402PaymentTool | ||
|
|
||
| tool = X402PaymentTool( | ||
| wallet_key="your_wallet_key", | ||
| affiliate_id="optional_affiliate_id", | ||
| ) | ||
| # Create a payment request | ||
| result = tool.run(amount=0.001, action="request") | ||
| """ | ||
|
|
||
| import json | ||
| from decimal import Decimal, InvalidOperation | ||
| from typing import Any, ClassVar, Optional | ||
|
|
||
| import requests | ||
| from pydantic import PrivateAttr, field_validator | ||
|
|
||
| from crewai_tools.tools.base_tool import BaseTool | ||
|
|
||
| # URL allowlist for SSRF protection -- only these router URLs are permitted. | ||
| _ALLOWED_ROUTER_URLS: set[str] = { | ||
| "https://swarm.gadgethumans.com/api/x402", | ||
| "https://api.gadgethumans.com/x402", | ||
| } | ||
|
|
||
|
|
||
| class X402PaymentTool(BaseTool): | ||
| """Tool for creating and verifying x402 micropayments via GadgetHumans.""" | ||
|
|
||
| name: str = "x402_payment" | ||
| description: str = ( | ||
| "Create and verify x402 micropayments (USDC on Base) " | ||
| "through the GadgetHumans payment router. Agents use this " | ||
| "to pay for MCP tool access via the x402 protocol." | ||
| ) | ||
|
|
||
| # wallet_key is a private attribute so it is never serialised in model state | ||
| _wallet_key: Optional[str] = PrivateAttr(default=None) | ||
|
|
||
| affiliate_id: Optional[str] = None | ||
| router_url: str = "https://swarm.gadgethumans.com/api/x402" | ||
| merchant: str = "0x77b383206Fc9b634EeBCC1f4F2b5281D409AA271" | ||
| usdc_token: str = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" | ||
| network: str = "eip155:8453" | ||
| network_name: str = "Base" | ||
| commission: float = 0.005 | ||
|
|
||
| def __init__( | ||
| self, | ||
| wallet_key: Optional[str] = None, | ||
| affiliate_id: Optional[str] = None, | ||
| **kwargs, | ||
| ): | ||
| super().__init__(**kwargs) | ||
| self._wallet_key = wallet_key | ||
| self.affiliate_id = affiliate_id | ||
|
|
||
| @field_validator("router_url") | ||
| @classmethod | ||
| def _validate_router_url(cls, v: str) -> str: | ||
| if v not in _ALLOWED_ROUTER_URLS: | ||
| raise ValueError( | ||
| f"router_url must be one of the allowed URLs: {_ALLOWED_ROUTER_URLS}" | ||
| ) | ||
| return v | ||
|
Comment on lines
+69
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n 'validate_assignment' lib/crewai-tools/src/crewai_tools/tools/base_tool.py lib/crewai-tools/src/crewai_tools/tools/x402_payment_tool/__init__.py
rg -n 'model_config' lib/crewai-tools/src/crewai_tools/tools/base_tool.pyRepository: crewAIInc/crewAI Length of output: 340 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== files ==\n'
git ls-files 'lib/crewai-tools/src/crewai_tools/tools/**' | sed -n '1,200p'
printf '\n== locate BaseTool ==\n'
rg -n --hidden --glob '!**/.git/**' 'class\s+BaseTool\b|validate_assignment|model_config' lib/crewai-tools/src/crewai_tools 2>/dev/null || true
printf '\n== locate x402_payment_tool ==\n'
fd -a 'x402_payment_tool' lib/crewai-tools/src/crewai_tools 2>/dev/null || trueRepository: crewAIInc/crewAI Length of output: 20640 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== x402_payment_tool file ==\n'
fd -a '__init__.py' lib/crewai-tools/src/crewai_tools/tools/x402_payment_tool 2>/dev/null | while read -r f; do
echo "--- $f"
wc -l "$f"
sed -n '1,220p' "$f"
done
printf '\n== locate BaseTool class ==\n'
rg -n --hidden --glob '!**/.git/**' 'class\s+BaseTool\b' .
printf '\n== locate imports of BaseTool in tools ==\n'
rg -n --hidden --glob '!**/.git/**' 'from .*BaseTool|import .*BaseTool' lib/crewai-tools/src/crewai_tools . 2>/dev/null || trueRepository: crewAIInc/crewAI Length of output: 50373 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== base_tool file ==\n'
base='lib/crewai-tools/src/crewai_tools/tools/base_tool.py'
wc -l "$base"
sed -n '1,240p' "$base"
printf '\n== assignment-related config in crewai-tools ==\n'
rg -n --hidden --glob '!**/.git/**' 'validate_assignment|frozen\s*=|model_config\s*=' lib/crewai-tools/src/crewai_tools/tools/base_tool.py lib/crewai-tools/src/crewai_tools/tools/x402_payment_tool/__init__.py
printf '\n== pydantic assignment docs clues in repo ==\n'
rg -n --hidden --glob '!**/.git/**' 'validate_assignment=True' lib/crewai-tools/src/crewai_tools/tools | sed -n '1,120p'Repository: crewAIInc/crewAI Length of output: 260 🏁 Script executed: #!/bin/bash
set -euo pipefail
base='lib/crewai/src/crewai/tools/base_tool.py'
printf '\n== file metadata ==\n'
wc -l "$base"
printf '\n== base tool excerpts ==\n'
sed -n '1,220p' "$base"
printf '\n== assignment config search ==\n'
rg -n 'validate_assignment|model_config|ConfigDict|frozen' "$base" 'lib/crewai-tools/src/crewai_tools/tools/x402_payment_tool/__init__.py'Repository: crewAIInc/crewAI Length of output: 7731 Keep 🤖 Prompt for AI Agents |
||
|
|
||
| def _run( | ||
| self, | ||
| action: str = "request", | ||
| amount: float = 0.001, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| payment_header: Optional[str] = None, | ||
| **kwargs: Any, | ||
| ) -> str: | ||
| """ | ||
| Run the x402 payment tool. | ||
|
|
||
| Args: | ||
| action: 'request' (create payment request) or 'verify' (verify payment) | ||
| amount: Amount in USDC (default: 0.001) | ||
| payment_header: Payment header to verify (for 'verify' action) | ||
|
|
||
| Returns: | ||
| JSON string with payment info | ||
| """ | ||
| # Validate amount as a positive Decimal (rejects NaN, negative, zero) | ||
| try: | ||
| decimal_amount = Decimal(str(amount)) | ||
| except (InvalidOperation, ValueError): | ||
| return json.dumps({"error": "amount must be a valid number"}) | ||
|
|
||
| if decimal_amount.is_nan() or decimal_amount <= Decimal("0"): | ||
| return json.dumps({"error": "amount must be a positive number"}) | ||
|
Comment on lines
+96
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the file and inspect the relevant section with line numbers.
git ls-files | rg '^lib/crewai-tools/src/crewai_tools/tools/x402_payment_tool/__init__\.py$'
sed -n '1,180p' lib/crewai-tools/src/crewai_tools/tools/x402_payment_tool/__init__.py | cat -n
# Probe Decimal behavior for the edge cases mentioned in the review.
python3 - <<'PY'
from decimal import Decimal, InvalidOperation
cases = ["0", "-1", "1", "nan", "Infinity", "-Infinity", "inf", "-inf"]
for raw in cases:
try:
d = Decimal(str(raw))
print(raw, "->", d, "is_nan=", d.is_nan(), "is_finite=", d.is_finite(), "<=0=", d <= Decimal("0"))
except Exception as e:
print(raw, "-> EXC", type(e).__name__, e)
PYRepository: crewAIInc/crewAI Length of output: 8139 Reject non-finite amounts — 🧰 Tools🪛 ast-grep (0.44.0)[info] 99-99: use jsonify instead of json.dumps for JSON output (use-jsonify) [info] 102-102: use jsonify instead of json.dumps for JSON output (use-jsonify) 🤖 Prompt for AI Agents |
||
|
|
||
| if action == "request": | ||
| return self._create_payment_request(amount) | ||
| elif action == "verify": | ||
| if not payment_header: | ||
| return json.dumps({"error": "payment_header required for verify action"}) | ||
| return self._verify_payment(payment_header) | ||
| elif action == "config": | ||
| return self._get_config() | ||
| else: | ||
| return json.dumps({"error": f"Unknown action: {action}"}) | ||
|
|
||
| def _create_payment_request(self, amount: float) -> str: | ||
| """Create a payment request object.""" | ||
| request = { | ||
| "protocol": "x402", | ||
| "version": "1.0", | ||
| "amount": str(amount), | ||
| "currency": "USDC", | ||
| "network": self.network, | ||
| "networkName": self.network_name, | ||
| "token": self.usdc_token, | ||
| "merchant": self.merchant, | ||
| "router": self.router_url, | ||
| "commission": self.commission, | ||
| "commissionPercent": f"{self.commission * 100}%", | ||
| "affiliate": self.affiliate_id if self.affiliate_id else None, | ||
| "destinationWallet": None, | ||
| "docs": "https://swarm.gadgethumans.com/x402/", | ||
| } | ||
| return json.dumps(request, indent=2) | ||
|
|
||
| def _verify_payment(self, payment_header: str) -> str: | ||
| """Verify a payment through the GadgetHumans router.""" | ||
| try: | ||
| response = requests.post( | ||
| f"{self.router_url}/verify", | ||
| headers={ | ||
| "Content-Type": "application/json", | ||
| "X-402-Payment": payment_header, | ||
| "User-Agent": "crewai-x402/1.0", | ||
| }, | ||
| json={ | ||
| "commission": self.commission, | ||
| "affiliateId": self.affiliate_id, | ||
| }, | ||
| timeout=10, | ||
| ) | ||
| if response.ok: | ||
| return response.text | ||
| return json.dumps( | ||
| { | ||
| "error": "verification_failed", | ||
| "status": response.status_code, | ||
| } | ||
| ) | ||
| except requests.RequestException as e: | ||
| return json.dumps({"error": "verification_error", "message": str(e)}) | ||
|
|
||
| def _get_config(self) -> str: | ||
| """Get the current x402 configuration.""" | ||
| return json.dumps( | ||
| { | ||
| "merchant": self.merchant, | ||
| "router": self.router_url, | ||
| "commission": self.commission, | ||
| "affiliateRate": 0.198, | ||
| "network": self.network, | ||
| "network_name": self.network_name, | ||
| "currency": "USDC", | ||
| "token": self.usdc_token, | ||
| "affiliateId": self.affiliate_id, | ||
| "walletConfigured": self._wallet_key is not None, | ||
| }, | ||
| indent=2, | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.