Add Nightmarket tools — API marketplace for AI agents#1111
Add Nightmarket tools — API marketplace for AI agents#1111streacy 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 significant enhancement to PraisonAI agents by integrating the Nightmarket API marketplace. This integration provides agents with the ability to programmatically discover and utilize a wide array of third-party API services. By leveraging the x402 payment protocol, agents can autonomously handle per-call payments, eliminating the need for manual API key management and fostering a more dynamic and self-sufficient operational model for accessing external functionalities. 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
Activity
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 Nightmarket API marketplace tools for AI agents
WalkthroughsDescription• Adds Nightmarket API marketplace integration for AI agents • Enables agents to discover and call paid third-party services • Implements x402 payment protocol for on-chain USDC settlements • Uses only stdlib dependencies (urllib, json) Diagramflowchart LR
Agent["AI Agent"] -->|search| Search["nightmarket_search()"]
Search -->|query marketplace| NM["Nightmarket API"]
NM -->|returns services| Agent
Agent -->|get details| Details["nightmarket_service_details()"]
Details -->|fetch endpoint| NM
NM -->|service info| Agent
Agent -->|call service| Call["nightmarket_call()"]
Call -->|x402 request| NM
NM -->|402 payment required| CrowPay["CrowPay"]
CrowPay -->|payment signature| Call
Call -->|retry with signature| NM
NM -->|API response| Agent
File Changes1. src/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py
|
📝 WalkthroughWalkthroughAdds a new Nightmarket API client module with search, service-details, and service-invocation functions; and registers those functions in the tools mapping. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant Agent as nightmarket_call
participant API as Nightmarket API
participant Payment as Payment System
Client->>Agent: nightmarket_call(endpoint_id, method, params, body, headers)
Agent->>API: HTTP request (with safe headers, optional body/params)
API-->>Agent: 402 Payment Required (payment details)
Agent-->>Client: Return payment-needed structure (endpoint, amount, currency, instructions)
Client->>Payment: Process payment (external)
Payment-->>Client: payment_signature
Client->>Agent: nightmarket_call(..., payment_signature)
Agent->>API: HTTP request with payment signature header
API-->>Agent: 200 OK (service response)
Agent-->>Client: Decoded JSON response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 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 |
Code Review by Qodo
1.
|
There was a problem hiding this comment.
Code Review
This pull request introduces useful tools for interacting with the Nightmarket API marketplace, with a clean implementation using standard libraries. However, significant security concerns exist regarding user-supplied input, which could be influenced by prompt injection. Specifically, the endpoint_id parameter is susceptible to path traversal on the remote API, and the headers parameter in nightmarket_call allows for arbitrary header injection, potentially leading to SSRF. Sanitizing these inputs is crucial for improving security. Beyond security, several code-related issues need addressing: urllib imports should be at the module level, there's a potential bug in error handling for 402 responses that could lead to a JSONDecodeError, and the nightmarket_call function doesn't handle GET parameters as implied by its docstring. A critical omission is that the new tools are not registered in src/praisonai-agents/praisonaiagents/tools/__init__.py within the TOOL_MAPPINGS, preventing their discovery by the agent framework.
| def nightmarket_call( | ||
| endpoint_id: str, | ||
| method: str = "GET", | ||
| body: Optional[Dict] = None, | ||
| headers: Optional[Dict] = None, | ||
| payment_signature: Optional[str] = None, | ||
| ) -> Dict: |
There was a problem hiding this comment.
The module's docstring provides an example of calling this function with a params argument for GET request parameters: nightmarket_call("abc123", method="GET", params={"city": "NYC"}). However, the function signature does not include a params argument, and there's no logic to handle it. This is a significant omission that limits the tool's functionality.
To fix this, you should:
- Add
params: Optional[Dict] = Noneto the function signature. - Update the function's docstring to describe the
paramsargument. - Add logic within the function to
urlencodetheparamsand append them to the URL for GET requests, similar to how it's done innightmarket_search.
| return result | ||
| except HTTPError as e: | ||
| if e.code == 402: | ||
| payment_info = json.loads(e.read().decode()) if e.read else {} |
There was a problem hiding this comment.
This line has a potential bug. The condition if e.read will always be true because e.read is a method object. If the response body from e.read() is empty, json.loads('') will raise a json.decoder.JSONDecodeError. The code should first read the response body into a variable and then attempt to parse it only if it's not empty.
| payment_info = json.loads(e.read().decode()) if e.read else {} | |
| payment_info = json.loads(body.decode()) if (body := e.read()) else {} |
| try: | ||
| from urllib.request import urlopen, Request | ||
|
|
||
| url = f"{NIGHTMARKET_BASE_URL}/marketplace/{endpoint_id}" |
There was a problem hiding this comment.
The endpoint_id parameter is directly concatenated into the URL without validation or sanitization. This allows an attacker (e.g., via prompt injection) to manipulate the URL path and access unintended endpoints on the nightmarket.ai domain. For example, providing ../../other-endpoint would result in a request to https://nightmarket.ai/api/other-endpoint instead of the intended marketplace endpoint.
| from urllib.request import urlopen, Request | ||
| from urllib.error import HTTPError | ||
|
|
||
| url = f"{NIGHTMARKET_BASE_URL}/x402/{endpoint_id}" |
There was a problem hiding this comment.
The endpoint_id parameter is directly concatenated into the URL without validation or sanitization. This allows an attacker (e.g., via prompt injection) to manipulate the URL path and access unintended endpoints on the nightmarket.ai domain. For example, providing ../../other-endpoint would result in a request to https://nightmarket.ai/api/other-endpoint instead of the intended x402 endpoint.
| url = f"{NIGHTMARKET_BASE_URL}/x402/{endpoint_id}" | ||
| req_headers = {"Accept": "application/json"} | ||
| if headers: | ||
| req_headers.update(headers) |
There was a problem hiding this comment.
The nightmarket_call function accepts an arbitrary dictionary of headers and applies them to the outgoing request. This allows an attacker to inject sensitive headers, such as Host, which can be used to perform SSRF attacks if the request passes through a proxy, or to bypass security controls on the target server. It is recommended to restrict the allowed headers to a safe subset or block sensitive headers like Host, Content-Length, and Connection.
| from urllib.request import urlopen, Request | ||
| from urllib.parse import urlencode |
There was a problem hiding this comment.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py (1)
49-49: Consider centralizing timeout configuration for consistency.Timeouts are hardcoded to two values (15s/30s). Exposing a module-level timeout constant (or function arg) makes behavior tunable and consistent with other configurable HTTP paths.
Also applies to: 70-70, 115-115
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py` at line 49, Replace the hardcoded timeouts used in urlopen(...) calls with a single module-level constant (e.g. DEFAULT_HTTP_TIMEOUT) and optionally allow an override via a function argument; locate the urlopen(req, timeout=15) occurrences in nightmarket_tools.py (the calls at the lines shown plus the ones at 70 and 115) and change them to use the constant (or the passed-in timeout parameter) so all HTTP timeouts are configurable and consistent across the module.
🤖 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/nightmarket_tools.py`:
- Around line 77-113: The nightmarket_call function signature and implementation
lack support for a params argument even though examples/docstring reference it;
add an optional params: Dict = None parameter to nightmarket_call and, inside
the function (before creating Request), if params is provided build a query
string using urllib.parse.urlencode (handle list values appropriately or use
doseq=True) and append it to the base URL (only for methods where a query string
is valid, e.g., GET/DELETE), ensuring body is not used for GET requests; also
update req creation to use the new URL and ensure the docstring mentions params
in the args list.
- Around line 29-83: Add the Nightmarket tool functions to the TOOL_MAPPINGS
registry: import nightmarket_search, nightmarket_service_details, and
nightmarket_call and add entries mapping stable keys (e.g. "nightmarket_search",
"nightmarket_service_details", "nightmarket_call") to the corresponding
callables in the TOOL_MAPPINGS dict so the framework can discover them; ensure
the import uses the module where those functions are defined and follow the same
entry structure as existing tools (name -> function reference and any metadata
if required).
- Around line 118-121: In the except HTTPError as e block, avoid calling
e.read() twice and protect json.loads from invalid or empty bodies: read the
response once into a variable (e.g., body = e.read()), check if body is
non-empty, then try to json.loads(body.decode()) inside a try/except (falling
back to {} on decode errors or empty body); keep retrieving the PAYMENT-REQUIRED
header via e.headers.get("PAYMENT-REQUIRED", "") and assign results to
payment_info and payment_header accordingly.
---
Nitpick comments:
In `@src/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py`:
- Line 49: Replace the hardcoded timeouts used in urlopen(...) calls with a
single module-level constant (e.g. DEFAULT_HTTP_TIMEOUT) and optionally allow an
override via a function argument; locate the urlopen(req, timeout=15)
occurrences in nightmarket_tools.py (the calls at the lines shown plus the ones
at 70 and 115) and change them to use the constant (or the passed-in timeout
parameter) so all HTTP timeouts are configurable and consistent across the
module.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 68c8e898-773f-4c6c-ba71-ac7e8cc6b175
📒 Files selected for processing (1)
src/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py
…ster tools - Add params parameter to nightmarket_call for query string support - Fix e.read bug (was checking method object, now reads body once) - Add endpoint_id validation to prevent path traversal - Filter headers to safe subset to prevent header injection - Move imports to module level per PEP 8 - Register nightmarket tools in TOOL_MAPPINGS for framework discovery - Use logger.exception for proper traceback logging
|
Addressed all review feedback:
Thanks for the thorough reviews! |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py (1)
90-132:⚠️ Potential issue | 🟠 MajorEnforce allowed HTTP methods and reject incompatible body usage.
Line 132 currently accepts any method string, and a provided
bodyis silently ignored unless the method is POST/PUT/PATCH. This can cause surprising behavior for paid calls.Proposed fix
def nightmarket_call( endpoint_id: str, method: str = "GET", params: Optional[Dict] = None, @@ ) -> Dict: @@ try: + allowed_methods = {"GET", "POST", "PUT", "PATCH", "DELETE"} + method_u = method.upper() + if method_u not in allowed_methods: + raise ValueError(f"Unsupported method: {method_u}") + if body is not None and method_u not in {"POST", "PUT", "PATCH"}: + raise ValueError(f"Body is not allowed for method: {method_u}") + safe_id = _validate_endpoint_id(endpoint_id) url = f"{NIGHTMARKET_BASE_URL}/x402/{safe_id}" @@ - if body and method.upper() in ("POST", "PUT", "PATCH"): + if body and method_u in ("POST", "PUT", "PATCH"): data = json.dumps(body).encode() req_headers["Content-Type"] = "application/json" - req = Request(url, data=data, headers=req_headers, method=method.upper()) + req = Request(url, data=data, headers=req_headers, method=method_u)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py` around lines 90 - 132, The nightmarket_call function currently accepts arbitrary HTTP methods and silently ignores a provided body unless method is POST/PUT/PATCH; update nightmarket_call to (1) normalize method to upper() and validate it against an allowlist (e.g., {"GET","POST","PUT","PATCH","DELETE"}) and raise a ValueError for disallowed methods, and (2) if a body is supplied but the normalized method is not one that supports a body (POST/PUT/PATCH), raise a ValueError indicating incompatible body usage; keep the rest of the request construction (req_headers, Content-Type, Request(...)) the same so the check occurs before Request creation.
🤖 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/nightmarket_tools.py`:
- Around line 137-153: The outer exception handler is collapsing non-402
HTTPError cases into a generic exception; after the HTTPError except that
handles 402, change the branch that currently does `raise` to instead build and
return a structured response for other HTTP errors: read e.read() into raw_body
(safely decode/JSON-decode like the 402 branch), capture relevant headers
(e.headers) and status (e.code), and return a dict such as {"status": e.code,
"message": f"HTTP error {e.code}", "body": parsed_body, "headers":
dict(e.headers)} so callers get status/body/headers instead of a generic error;
update the HTTPError handler where variables `e`, `raw_body`, `payment_info`,
and `payment_header` are used to implement this.
---
Duplicate comments:
In `@src/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py`:
- Around line 90-132: The nightmarket_call function currently accepts arbitrary
HTTP methods and silently ignores a provided body unless method is
POST/PUT/PATCH; update nightmarket_call to (1) normalize method to upper() and
validate it against an allowlist (e.g., {"GET","POST","PUT","PATCH","DELETE"})
and raise a ValueError for disallowed methods, and (2) if a body is supplied but
the normalized method is not one that supports a body (POST/PUT/PATCH), raise a
ValueError indicating incompatible body usage; keep the rest of the request
construction (req_headers, Content-Type, Request(...)) the same so the check
occurs before Request creation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cb24ce47-a44d-42de-aac1-50ed68cb12a2
📒 Files selected for processing (2)
src/praisonai-agents/praisonaiagents/tools/__init__.pysrc/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py
| except HTTPError as e: | ||
| if e.code == 402: | ||
| raw_body = e.read() | ||
| try: | ||
| payment_info = json.loads(raw_body.decode()) if raw_body else {} | ||
| except (json.JSONDecodeError, UnicodeDecodeError): | ||
| payment_info = {"raw_body": raw_body.decode(errors="replace")} if raw_body else {} | ||
| payment_header = e.headers.get("PAYMENT-REQUIRED", "") | ||
| return { | ||
| "status": 402, | ||
| "message": "Payment required. Use CrowPay to authorize payment, then retry with payment_signature.", | ||
| "payment_required": payment_info, | ||
| "payment_header": payment_header, | ||
| } | ||
| raise | ||
| except Exception as e: | ||
| logger.exception(f"Nightmarket call failed: {e}") |
There was a problem hiding this comment.
Return structured non-402 HTTP errors instead of collapsing to generic error.
After Line 151 re-raises, the outer handler at Line 152 strips status/body context into {"error": ...}. That makes 404/429/500 handling harder for callers.
Proposed fix
except HTTPError as e:
if e.code == 402:
raw_body = e.read()
try:
payment_info = json.loads(raw_body.decode()) if raw_body else {}
except (json.JSONDecodeError, UnicodeDecodeError):
payment_info = {"raw_body": raw_body.decode(errors="replace")} if raw_body else {}
payment_header = e.headers.get("PAYMENT-REQUIRED", "")
return {
"status": 402,
"message": "Payment required. Use CrowPay to authorize payment, then retry with payment_signature.",
"payment_required": payment_info,
"payment_header": payment_header,
}
- raise
+ raw_body = e.read()
+ try:
+ error_body = json.loads(raw_body.decode()) if raw_body else {}
+ except (json.JSONDecodeError, UnicodeDecodeError):
+ error_body = {"raw_body": raw_body.decode(errors="replace")} if raw_body else {}
+ return {
+ "status": e.code,
+ "error": str(e),
+ "response": error_body,
+ }🧰 Tools
🪛 Ruff (0.15.2)
[warning] 153-153: Redundant exception object included in logging.exception call
(TRY401)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai-agents/praisonaiagents/tools/nightmarket_tools.py` around lines
137 - 153, The outer exception handler is collapsing non-402 HTTPError cases
into a generic exception; after the HTTPError except that handles 402, change
the branch that currently does `raise` to instead build and return a structured
response for other HTTP errors: read e.read() into raw_body (safely
decode/JSON-decode like the 402 branch), capture relevant headers (e.headers)
and status (e.code), and return a dict such as {"status": e.code, "message":
f"HTTP error {e.code}", "body": parsed_body, "headers": dict(e.headers)} so
callers get status/body/headers instead of a generic error; update the HTTPError
handler where variables `e`, `raw_body`, `payment_info`, and `payment_header`
are used to implement this.
What this adds
A new
nightmarket_tools.pymodule insrc/praisonai-agents/praisonaiagents/tools/that gives PraisonAI agents the ability to discover and call paid third-party API services through the Nightmarket marketplace.Functions
nightmarket_search(query, sort)— Search the marketplace for available API servicesnightmarket_service_details(endpoint_id)— Get full details including request/response examplesnightmarket_call(endpoint_id, method, body, headers, payment_signature)— Call a service with optional x402 payment proofHow it works
Nightmarket is an API marketplace where AI agents find and pay for third-party services (data enrichment, analytics, AI models, content generation, etc.). Every call settles on-chain in USDC on Base using the x402 payment protocol — no API keys or subscriptions needed.
The tool uses only stdlib (
urllib,json) with no additional dependencies.Usage
Why this is useful for PraisonAI
PraisonAI agents can use these tools to autonomously access external APIs without configuring API keys for each service. The agent searches the marketplace, pays per-call in USDC, and gets the response. Pairs with CrowPay for automatic payment handling.
Summary by CodeRabbit