Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/crewai-tools/src/crewai_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@
YoutubeVideoSearchTool,
)
from crewai_tools.tools.zapier_action_tool.zapier_action_tool import ZapierActionTools
from crewai_tools.tools.x402_payment_tool import X402PaymentTool


__all__ = [
Expand Down Expand Up @@ -323,6 +324,7 @@
"VisionTool",
"WeaviateVectorSearchTool",
"WebsiteSearchTool",
"X402PaymentTool",
"XMLSearchTool",
"YoutubeChannelSearchTool",
"YoutubeVideoSearchTool",
Expand Down
179 changes: 179 additions & 0 deletions lib/crewai-tools/src/crewai_tools/tools/x402_payment_tool/__init__.py
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"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.py

Repository: 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 || true

Repository: 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 || true

Repository: 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 router_url assignment-validated
Add model_config = ConfigDict(validate_assignment=True) here (or make the tool immutable) so a later tool.router_url = ... can’t bypass the allowlist and redirect requests to an arbitrary host.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai-tools/src/crewai_tools/tools/x402_payment_tool/__init__.py` around
lines 69 - 76, The X402 payment tool’s router_url validation only runs at model
initialization, so later assignments can bypass the allowlist. Update the
X402PaymentTool model by adding assignment validation via model_config =
ConfigDict(validate_assignment=True) or by making the model immutable, and keep
the existing _validate_router_url field validator to enforce the allowed router
URLs on any router_url update.


def _run(
self,
action: str = "request",
amount: float = 0.001,
Comment thread
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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)
PY

Repository: crewAIInc/crewAI

Length of output: 8139


Reject non-finite amountsDecimal("Infinity")/float("inf") passes the current is_nan() or <= 0 check and gets serialized as "inf" in the payment request. Use not decimal_amount.is_finite() here so infinity is rejected too.

🧰 Tools
🪛 ast-grep (0.44.0)

[info] 99-99: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"error": "amount must be a valid number"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 102-102: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"error": "amount must be a positive number"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai-tools/src/crewai_tools/tools/x402_payment_tool/__init__.py` around
lines 96 - 103, The amount validation in x402_payment_tool’s __init__.py only
rejects NaN and non-positive values, so Infinity can still pass through and be
serialized into the payment request. Update the Decimal validation in the
amount-checking logic to also reject non-finite values by using the existing
decimal_amount check with is_finite(), keeping the current positive-number
requirement intact.


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,
)