Add CrowPay tools — payment service for AI agents#1112
Add CrowPay tools — payment service for AI agents#1112streacy wants to merge 2 commits intoMervinPraison:mainfrom
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a robust payment system for PraisonAI agents by integrating CrowPay. This allows agents to seamlessly handle financial transactions for external services, enhancing their autonomy while maintaining strict control through configurable spending limits and human oversight. The system is designed to be secure and efficient, providing a critical capability for agents interacting with paid APIs and services. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
Review Summary by QodoAdd CrowPay payment tools for autonomous agent payments
WalkthroughsDescription• Add CrowPay payment tools module for AI agent wallet management • Implement x402 (USDC on Base) and credit card payment authorization • Provide polling mechanism for human approval workflows • Enable settlement reporting for on-chain transactions Diagramflowchart LR
Agent["Agent"] -->|crowpay_setup| Wallet["Create Wallet"]
Agent -->|crowpay_authorize| X402["Authorize x402 Payment"]
Agent -->|crowpay_authorize_card| Card["Authorize Card Payment"]
X402 -->|pending| Poll["Poll Status"]
Card -->|pending| Poll
Poll -->|approved| Settle["Settle Transaction"]
Settle -->|confirm| Done["Complete"]
File Changes1. src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py
|
Code Review by Qodo
1.
|
📝 WalkthroughWalkthroughAdds a new CrowPay integration module implementing five HTTP-based utilities (wallet setup, authorize, card authorize, poll status, settle) and registers those tools in the tools package; uses JSON payloads, urllib.request, and a module logger. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a new set of tools for AI agents to interact with the CrowPay payment service. However, significant security concerns exist regarding credential handling and unsanitized user inputs. Specifically, API keys are passed as direct arguments, exposing sensitive secrets, and unsanitized input in URL construction could lead to parameter injection. Beyond security, improvements are needed for robustness, including centralizing module imports for maintainability and fixing a bug in HTTPError handling for empty or invalid JSON responses. Addressing these will make the new tools more reliable and secure.
|
|
||
|
|
||
| def crowpay_authorize( | ||
| api_key: str, |
There was a problem hiding this comment.
The api_key is passed as a direct argument to the crowpay_authorize function. In the context of AI agents, this is a significant security risk as it requires the LLM to have the sensitive API key in its context to call the tool. This exposes the key to potential leakage via prompt injection or inclusion in conversation history and logs. It is highly recommended to retrieve the API key from a secure source, such as an environment variable (e.g., os.environ.get('CROWPAY_API_KEY')), inside the function instead of taking it as an argument.
|
|
||
|
|
||
| def crowpay_authorize_card( | ||
| api_key: str, |
There was a problem hiding this comment.
The api_key is passed as a direct argument to the crowpay_authorize_card function. Similar to other tools in this module, this design exposes sensitive credentials to the AI agent's context, increasing the risk of accidental disclosure or exploitation via prompt injection. Credentials should be managed securely via environment variables or a dedicated secret manager within the tool's implementation.
| return {"error": str(e)} | ||
|
|
||
|
|
||
| def crowpay_poll_status(api_key: str, approval_id: str) -> Dict: |
There was a problem hiding this comment.
The api_key is passed as a direct argument to the crowpay_poll_status function. This pattern of passing secrets as tool arguments is insecure for AI agent workflows. Please refactor the tool to use environment variables for authentication to prevent the API key from being exposed in the LLM's prompt or conversation history.
| return {"error": str(e)} | ||
|
|
||
|
|
||
| def crowpay_settle(api_key: str, transaction_id: str, tx_hash: str) -> Dict: |
There was a problem hiding this comment.
| except HTTPError as e: | ||
| resp_body = e.read().decode() if e.read else "{}" | ||
| result = json.loads(resp_body) | ||
| result["_status_code"] = e.code | ||
| return result |
There was a problem hiding this comment.
The error handling for HTTPError has a potential bug. The condition if e.read will always be true because e.read is a method object, not its result. This means the else branch is unreachable. Furthermore, if the error response has an empty body, e.read().decode() will be an empty string, causing json.loads() to raise a JSONDecodeError. This exception is not handled here and will be caught by the generic except Exception, masking the original HTTPError. It's safer to wrap the JSON parsing in a try...except block to handle empty or invalid JSON responses gracefully.
| except HTTPError as e: | |
| resp_body = e.read().decode() if e.read else "{}" | |
| result = json.loads(resp_body) | |
| result["_status_code"] = e.code | |
| return result | |
| except HTTPError as e: | |
| try: | |
| result = json.loads(e.read().decode()) | |
| except (json.JSONDecodeError, UnicodeDecodeError): | |
| result = {} | |
| result["_status_code"] = e.code | |
| return result |
| except HTTPError as e: | ||
| resp_body = e.read().decode() if e.read else "{}" | ||
| result = json.loads(resp_body) | ||
| result["_status_code"] = e.code | ||
| return result |
There was a problem hiding this comment.
The error handling for HTTPError has a potential bug. The condition if e.read will always be true because e.read is a method object, not its result. This means the else branch is unreachable. Furthermore, if the error response has an empty body, e.read().decode() will be an empty string, causing json.loads() to raise a JSONDecodeError. This exception is not handled here and will be caught by the generic except Exception, masking the original HTTPError. It's safer to wrap the JSON parsing in a try...except block to handle empty or invalid JSON responses gracefully.
| except HTTPError as e: | |
| resp_body = e.read().decode() if e.read else "{}" | |
| result = json.loads(resp_body) | |
| result["_status_code"] = e.code | |
| return result | |
| except HTTPError as e: | |
| try: | |
| result = json.loads(e.read().decode()) | |
| except (json.JSONDecodeError, UnicodeDecodeError): | |
| result = {} | |
| result["_status_code"] = e.code | |
| return result |
| try: | ||
| from urllib.request import urlopen, Request | ||
|
|
||
| url = f"{CROWPAY_BASE_URL}/authorize/status?id={approval_id}" |
There was a problem hiding this comment.
The approval_id is directly embedded into the URL query string without proper URL encoding. This is a parameter injection vulnerability, as special characters (e.g., &, =, ?, #, or spaces) could lead to a malformed URL or broken requests. To prevent this, ensure the approval_id is safely encoded using urllib.parse.quote before being appended to the URL.
| url = f"{CROWPAY_BASE_URL}/authorize/status?id={approval_id}" | |
| url = f"{CROWPAY_BASE_URL}/authorize/status?id={quote(approval_id)}" |
| from typing import Dict, Optional | ||
| import logging | ||
| import json |
There was a problem hiding this comment.
For better code organization, readability, and to avoid repetition, it's standard Python practice (PEP 8) to place all imports at the top of the file. The urllib imports are currently repeated within each function. They should be moved here. This also makes the module's dependencies clear at a glance. After this change, please remove the local urllib imports from all functions in this file.
| from typing import Dict, Optional | |
| import logging | |
| import json | |
| from typing import Dict, Optional | |
| import logging | |
| import json | |
| from urllib.request import urlopen, Request | |
| from urllib.error import HTTPError | |
| from urllib.parse import quote |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py (1)
120-123: Validateamount_centsbefore making external calls.Fail fast on non-positive values to avoid unnecessary remote errors and ambiguous downstream handling.
💡 Suggested guard
def crowpay_authorize_card( @@ ) -> Dict: @@ """ try: + if amount_cents <= 0: + return {"error": "amount_cents must be greater than 0"} + from urllib.request import urlopen, Request from urllib.error import HTTPError🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py` around lines 120 - 123, The function crowpay_authorize_card currently doesn't validate amount_cents before making external calls; add an early guard at the start of crowpay_authorize_card that ensures amount_cents is an int (or castable) and strictly > 0 and raise a clear ValueError (or similar) with a descriptive message if the check fails so the function fails fast and avoids remote errors or ambiguous downstream behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py`:
- Around line 198-199: The poll URL currently interpolates approval_id directly;
update the url construction to URL-encode the query parameter using
urllib.parse.urlencode so reserved characters are escaped (e.g., build query =
urlencode({"id": approval_id}) and set url =
f"{CROWPAY_BASE_URL}/authorize/status?{query}"), then create the Request as
before with Request(url, headers={"X-API-Key": api_key}); reference symbols:
approval_id, CROWPAY_BASE_URL, Request, api_key, and url.
- Around line 3-5: Add the missing CrowPay tool registrations to the lazy-load
mapping: update TOOL_MAPPINGS to include keys for crowpay_setup,
crowpay_authorize, crowpay_authorize_card, crowpay_poll_status, and
crowpay_settle mapping to their import targets so the import path used in the
docstring (from praisonaiagents.tools import ...) resolves; ensure the keys
exactly match the function names and the values point to the module path that
exports those functions so the lazy loader can import them without raising
AttributeError.
- Around line 110-114: Create a helper function named _parse_http_error(e) that
reads and decodes the HTTPError body, attempts json.loads(resp_body) inside a
try/except JSONDecodeError and falls back to an empty dict on failure, then sets
result["_status_code"] = e.code (and returns result); replace the two HTTPError
handlers that currently do resp_body = e.read().decode() / result =
json.loads(resp_body) / result["_status_code"] = e.code / return result with
calls to return _parse_http_error(e) so the status code is always preserved even
when JSON decoding fails.
---
Nitpick comments:
In `@src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py`:
- Around line 120-123: The function crowpay_authorize_card currently doesn't
validate amount_cents before making external calls; add an early guard at the
start of crowpay_authorize_card that ensures amount_cents is an int (or
castable) and strictly > 0 and raise a clear ValueError (or similar) with a
descriptive message if the check fails so the function fails fast and avoids
remote errors or ambiguous downstream behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b621f6b7-9e64-487d-a682-6c95453004a5
📒 Files selected for processing (1)
src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py
…, register tools - Support CROWPAY_API_KEY env var to avoid exposing keys in agent context - Add _parse_http_error helper to safely handle non-JSON/empty error bodies - URL-encode approval_id in poll URL using urlencode - Move imports to module level per PEP 8 - Register all crowpay tools in TOOL_MAPPINGS for framework discovery - Use logger.exception for proper traceback logging
|
Addressed all review feedback:
Thanks for all the thorough reviews! |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py (2)
210-219: Unify HTTP error handling in poll/settle with authorize flows.
crowpay_poll_statusandcrowpay_settlecurrently collapse HTTP failures to generic exception strings. Reusing_parse_http_errorkeeps status/body details consistent across all CrowPay endpoints.Proposed fix
def crowpay_poll_status(approval_id: str, api_key: Optional[str] = None) -> Dict: @@ - with urlopen(req, timeout=15) as resp: - return json.loads(resp.read().decode()) + try: + with urlopen(req, timeout=15) as resp: + return json.loads(resp.read().decode()) + except HTTPError as e: + return _parse_http_error(e) except Exception as e: logger.exception(f"CrowPay poll failed: {e}") return {"error": str(e)} @@ def crowpay_settle(transaction_id: str, tx_hash: str, api_key: Optional[str] = None) -> Dict: @@ - with urlopen(req, timeout=15) as resp: - return json.loads(resp.read().decode()) + try: + with urlopen(req, timeout=15) as resp: + return json.loads(resp.read().decode()) + except HTTPError as e: + return _parse_http_error(e) except Exception as e: logger.exception(f"CrowPay settle failed: {e}") return {"error": str(e)}Also applies to: 233-250
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py` around lines 210 - 219, The poll/settle handlers (crowpay_poll_status and crowpay_settle) currently catch all exceptions and return only str(e); update their exception handling to mirror the authorize flow by detecting HTTPError (from urllib.error) and passing the exception into _parse_http_error to return structured status/body info, while still logging other unexpected exceptions with logger.exception and returning {"error": str(e)}; locate the try/except blocks around _get_api_key, url construction using CROWPAY_BASE_URL and urlopen(req, timeout=15) and replace the generic except with an HTTPError branch that calls _parse_http_error(e) and a fallback Exception branch.
139-147: Validate card payment inputs before calling CrowPay.
amount_cents,merchant, andreasonare accepted as-is. Guarding obvious invalid inputs early prevents avoidable network calls and gives clearer tool feedback.Proposed fix
def crowpay_authorize_card( amount_cents: int, merchant: str, reason: str, @@ ) -> Dict: @@ try: + if amount_cents <= 0: + return {"error": "amount_cents must be greater than 0"} + if not merchant.strip(): + return {"error": "merchant is required"} + if not reason.strip(): + return {"error": "reason is required"} + key = _get_api_key(api_key)Also applies to: 167-173
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py` around lines 139 - 147, Validate inputs at the start of crowpay_authorize_card (and the similar crowpay_capture_card function) and fail fast with clear errors: ensure amount_cents is an int > 0, merchant is a non-empty string, reason is a non-empty string, and api_key (if required) is provided; also validate currency is a 3-letter string if you want stricter checks. Put these checks at the top of the functions (before any network calls) and raise ValueError (or return a standardized error dict) with concise descriptive messages referencing the invalid field so callers get immediate, clear feedback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py`:
- Around line 46-54: _parsed_http_error currently assumes json.loads returns a
dict and blindly sets payload["_status_code"], which fails for non-object JSON
(arrays, strings). Update _parse_http_error to test whether the parsed payload
is a mapping (e.g., isinstance(payload, dict)); if it is, set
payload["_status_code"] = e.code, otherwise wrap the parsed value into an object
such as {"body": payload, "_status_code": e.code} (or use a key like "payload")
so the status code is always preserved and the original data is not lost; keep
the existing fallback for JSON decode errors and the e.fp/raw handling.
---
Nitpick comments:
In `@src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py`:
- Around line 210-219: The poll/settle handlers (crowpay_poll_status and
crowpay_settle) currently catch all exceptions and return only str(e); update
their exception handling to mirror the authorize flow by detecting HTTPError
(from urllib.error) and passing the exception into _parse_http_error to return
structured status/body info, while still logging other unexpected exceptions
with logger.exception and returning {"error": str(e)}; locate the try/except
blocks around _get_api_key, url construction using CROWPAY_BASE_URL and
urlopen(req, timeout=15) and replace the generic except with an HTTPError branch
that calls _parse_http_error(e) and a fallback Exception branch.
- Around line 139-147: Validate inputs at the start of crowpay_authorize_card
(and the similar crowpay_capture_card function) and fail fast with clear errors:
ensure amount_cents is an int > 0, merchant is a non-empty string, reason is a
non-empty string, and api_key (if required) is provided; also validate currency
is a 3-letter string if you want stricter checks. Put these checks at the top of
the functions (before any network calls) and raise ValueError (or return a
standardized error dict) with concise descriptive messages referencing the
invalid field so callers get immediate, clear feedback.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 971abe8c-737f-4990-9ae2-1a25243a291d
📒 Files selected for processing (2)
src/praisonai-agents/praisonaiagents/tools/__init__.pysrc/praisonai-agents/praisonaiagents/tools/crowpay_tools.py
| def _parse_http_error(e: HTTPError) -> Dict: | ||
| """Safely parse HTTP error responses, preserving status code.""" | ||
| raw = e.read().decode("utf-8", errors="replace") if e.fp else "" | ||
| try: | ||
| payload = json.loads(raw) if raw else {} | ||
| except json.JSONDecodeError: | ||
| payload = {"error": raw or str(e)} | ||
| payload["_status_code"] = e.code | ||
| return payload |
There was a problem hiding this comment.
Harden _parse_http_error against non-object JSON bodies.
If the server returns valid JSON that is not an object (e.g., a string/array), setting payload["_status_code"] raises and drops the original HTTP context.
Proposed fix
def _parse_http_error(e: HTTPError) -> Dict:
"""Safely parse HTTP error responses, preserving status code."""
raw = e.read().decode("utf-8", errors="replace") if e.fp else ""
try:
- payload = json.loads(raw) if raw else {}
+ payload = json.loads(raw) if raw else {}
except json.JSONDecodeError:
payload = {"error": raw or str(e)}
+ if not isinstance(payload, dict):
+ payload = {"error": raw or str(payload)}
payload["_status_code"] = e.code
return payload🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai-agents/praisonaiagents/tools/crowpay_tools.py` around lines 46
- 54, _parsed_http_error currently assumes json.loads returns a dict and blindly
sets payload["_status_code"], which fails for non-object JSON (arrays, strings).
Update _parse_http_error to test whether the parsed payload is a mapping (e.g.,
isinstance(payload, dict)); if it is, set payload["_status_code"] = e.code,
otherwise wrap the parsed value into an object such as {"body": payload,
"_status_code": e.code} (or use a key like "payload") so the status code is
always preserved and the original data is not lost; keep the existing fallback
for JSON decode errors and the e.fp/raw handling.
|
@streacy Can you please add tools to https://github.com/MervinPraison/PraisonAI-Tools Thank you |
|
Thanks @MervinPraison! Created the tools in PraisonAI-Tools as requested: |
What this adds
A new
crowpay_tools.pymodule insrc/praisonai-agents/praisonaiagents/tools/that gives PraisonAI agents the ability to pay for APIs and services autonomously — within spending rules set by the wallet owner.Functions
crowpay_setup(network)— Create a new agent wallet and API keycrowpay_authorize(api_key, payment_required, merchant, reason)— Authorize an x402 payment (USDC on Base)crowpay_authorize_card(api_key, amount_cents, merchant, reason)— Authorize a credit card paymentcrowpay_poll_status(api_key, approval_id)— Poll for human approval statuscrowpay_settle(api_key, transaction_id, tx_hash)— Report x402 settlementHow it works
CrowPay provides managed wallets for AI agents with built-in spending rules, human approval workflows, and audit trails. Supports:
No wallet private keys are ever exposed to the agent. Default rules: auto-approve under $5, human approval above, $50 daily limit.
Usage
Uses only stdlib (
urllib,json) — no additional dependencies.Why this is useful for PraisonAI
Agents interacting with paid services need a way to handle payments without exposing private keys or requiring manual intervention for every transaction. CrowPay provides this with configurable spending rules and a full audit trail. Pairs naturally with Nightmarket for accessing paid APIs.
Summary by CodeRabbit