Version: 1.0.0 Last Updated: February 4, 2025 PR: https://github.com/zkp2p/pay/pull/46
- Overview
- Skill Frontmatter
- Quick Start
- How It Works
- API Reference
- Code Examples
- Best Practices
- Troubleshooting
This Claude skill enables AI agents to accept fiat payments (Venmo, Zelle, Cash App, PayPal, Wise, Revolut) from humans and receive USDC on Base chain.
Key Features:
- Programmatic merchant registration (no browser required)
- Exact-fiat checkout sessions for predictable pricing
- Payment status polling or webhook notifications
- Zero-knowledge proof verification (handled by the platform)
Use Cases:
- Pay-per-task AI services
- Subscription billing
- Tips and donations
- Any agent that needs to charge users
---
name: zkp2p-pay-agent
description: Accept fiat payments from humans via ZKP2P Pay. Use when an agent needs to charge users money, create payment links, or accept Venmo/Zelle/CashApp payments in exchange for services.
allowed-tools: Bash(curl *), Read, Write
---No authentication required. Creates a new merchant and returns an API key.
curl -X POST https://api.pay.zkp2p.xyz/api/merchants \
-H "Content-Type: application/json" \
-d '{"name": "My AI Agent Service"}'Response:
{
"success": true,
"responseObject": {
"merchant": {
"id": "clxxxxxxxxxxxxxxxxxx",
"name": "My AI Agent Service",
"createdAt": "2025-02-04T10:00:00.000Z"
},
"apiKey": "a]1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4"
}
}IMPORTANT: Store the
apiKeysecurely - it is only returned once and cannot be retrieved later.
curl -X POST https://api.pay.zkp2p.xyz/api/checkout/sessions \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"merchantId": "YOUR_MERCHANT_ID",
"checkoutMode": "exact-fiat",
"fiatAmount": "10.00",
"fiatCurrency": "USD",
"destinationChainId": 8453,
"destinationToken": "USDC",
"recipientAddress": "0xYourWalletAddress",
"metadata": {"orderId": "task-123", "description": "AI assistance"}
}'Required Fields:
| Field | Value | Description |
|---|---|---|
merchantId |
Your merchant ID | From step 1 |
checkoutMode |
"exact-fiat" |
Fixed fiat amount |
fiatAmount |
"10.00" |
Amount in fiat |
fiatCurrency |
"USD" |
Currency code |
destinationChainId |
8453 |
Base chain |
destinationToken |
"USDC" |
Must be string "USDC", NOT contract address |
recipientAddress |
"0x..." |
Your EVM wallet to receive USDC |
Response:
{
"success": true,
"responseObject": {
"session": {
"id": "clxxxxxxxxxxxxxxxxxx",
"checkoutUrl": "https://pay.zkp2p.xyz/checkout/clxxxxxxxxxxxxxxxxxx",
"status": "pending",
"fiatAmount": "10.00",
"fiatCurrency": "USD",
"expiresAt": "2025-02-05T10:00:00.000Z"
}
}
}Provide the checkoutUrl to the user:
Please complete your $10.00 payment here: https://pay.zkp2p.xyz/checkout/clxxxxxxxxxxxxxxxxxx
The user will:
- Open the URL in their browser
- Select a payment platform (Venmo, Zelle, etc.)
- Send the fiat payment
- Generate a ZK proof via the PeerAuth browser extension
- Submit the proof
curl https://api.pay.zkp2p.xyz/api/checkout/sessions/SESSION_IDResponse (pending):
{
"success": true,
"responseObject": {
"session": {
"id": "clxxxxxxxxxxxxxxxxxx",
"status": "pending",
"fiatAmount": "10.00"
}
}
}Response (completed):
{
"success": true,
"responseObject": {
"session": {
"id": "clxxxxxxxxxxxxxxxxxx",
"status": "completed",
"fiatAmount": "10.00",
"cryptoAmount": "9.95",
"paymentPlatform": "venmo",
"transactionHash": "0xabc123...",
"completedAt": "2025-02-04T10:30:00.000Z"
}
}
}┌─────────────────────────────────────────────────────────────────────┐
│ PAYMENT FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Agent creates checkout session │
│ └─> API returns checkoutUrl │
│ │
│ 2. Agent sends checkoutUrl to human │
│ └─> Human opens in browser │
│ │
│ 3. Human selects payment platform │
│ └─> Venmo, Zelle, Cash App, PayPal, Wise, Revolut │
│ │
│ 4. Human sends fiat payment │
│ └─> To a verified ZKP2P recipient │
│ │
│ 5. Human generates ZK proof │
│ └─> PeerAuth browser extension proves payment │
│ └─> Without revealing sensitive transaction data │
│ │
│ 6. Platform verifies proof & fulfills on-chain │
│ └─> Agent receives confirmation (poll or webhook) │
│ │
│ 7. Merchant receives USDC on Base chain │
│ └─> At the specified recipientAddress │
│ │
└─────────────────────────────────────────────────────────────────────┘
| Status | Meaning | Action |
|---|---|---|
pending |
Awaiting payment | Continue polling |
awaiting_proof |
Fiat sent, generating ZK proof | Continue polling |
processing |
Proof verified, processing on-chain | Continue polling |
completed |
Payment successful, USDC received | Deliver service |
expired |
Session timed out (24h default) | Create new session |
failed |
Payment or proof failed | Handle error |
| Mode | Use Case | Required Fields |
|---|---|---|
exact-fiat |
Fixed USD price | fiatAmount, fiatCurrency |
exact-crypto |
Fixed USDC amount | cryptoAmount |
open |
User chooses amount (tips) | None |
Base URL: https://api.pay.zkp2p.xyz
Most endpoints require the X-API-Key header:
curl -H "X-API-Key: sk_live_xxxxx" https://api.pay.zkp2p.xyz/api/...All responses follow this structure:
{
"success": true,
"message": "Optional message",
"responseObject": { ... },
"statusCode": 200
}Error responses:
{
"success": false,
"message": "Error description",
"statusCode": 400
}Create a new merchant account.
Authentication: None required
Request:
curl -X POST https://api.pay.zkp2p.xyz/api/merchants \
-H "Content-Type: application/json" \
-d '{
"name": "My AI Agent",
"logoUrl": "https://example.com/logo.png"
}'| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Merchant display name |
logoUrl |
string | No | URL to logo image |
Response:
{
"success": true,
"responseObject": {
"merchant": {
"id": "clxxxxxxxxxxxxxxxxxx",
"name": "My AI Agent",
"logoUrl": "https://example.com/logo.png",
"createdAt": "2025-02-04T10:00:00.000Z"
},
"apiKey": "a1b2c3d4e5f6..."
}
}Create a payment session.
Authentication: Required (X-API-Key)
Request:
curl -X POST https://api.pay.zkp2p.xyz/api/checkout/sessions \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"merchantId": "clxxxxxxxxxxxxxxxxxx",
"checkoutMode": "exact-fiat",
"fiatAmount": "25.00",
"fiatCurrency": "USD",
"destinationChainId": 8453,
"destinationToken": "USDC",
"recipientAddress": "0xYourWalletAddress",
"metadata": {"orderId": "order_123"},
"expiresIn": 3600
}'| Field | Type | Required | Description |
|---|---|---|---|
merchantId |
string | Yes | Your merchant ID |
checkoutMode |
string | Yes | exact-fiat, exact-crypto, or open |
fiatAmount |
string | For exact-fiat | Amount (e.g., "25.00") |
fiatCurrency |
string | For exact-fiat | Currency code (e.g., "USD") |
cryptoAmount |
string | For exact-crypto | USDC amount |
destinationChainId |
number | Yes | 8453 for Base |
destinationToken |
string | Yes | "USDC" (string, NOT address) |
recipientAddress |
string | Yes* | EVM wallet for USDC (*required if no default) |
metadata |
object | No | Custom data (returned in webhooks) |
expiresIn |
number | No | Seconds until expiration (default: 86400) |
successUrl |
string | No | Redirect after success |
cancelUrl |
string | No | Redirect if cancelled |
Response:
{
"success": true,
"responseObject": {
"session": {
"id": "clxxxxxxxxxxxxxxxxxx",
"merchantId": "clxxxxxxxxxxxxxxxxxx",
"checkoutUrl": "https://pay.zkp2p.xyz/checkout/clxxxxxxxxxxxxxxxxxx",
"status": "pending",
"checkoutMode": "exact-fiat",
"fiatAmount": "25.00",
"fiatCurrency": "USD",
"expiresAt": "2025-02-04T11:00:00.000Z",
"createdAt": "2025-02-04T10:00:00.000Z"
}
}
}Get session status.
Authentication: None required (session IDs are unguessable)
Request:
curl https://api.pay.zkp2p.xyz/api/checkout/sessions/SESSION_IDResponse (completed):
{
"success": true,
"responseObject": {
"session": {
"id": "clxxxxxxxxxxxxxxxxxx",
"status": "completed",
"fiatAmount": "25.00",
"fiatCurrency": "USD",
"cryptoAmount": "24.87",
"paymentPlatform": "venmo",
"transactionHash": "0xabc123...",
"completedAt": "2025-02-04T10:30:00.000Z"
}
}
}Register a webhook URL.
Authentication: Required (X-API-Key)
Request:
curl -X POST https://api.pay.zkp2p.xyz/api/webhooks \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"merchantId": "clxxxxxxxxxxxxxxxxxx",
"url": "https://your-agent.example.com/webhooks/payment",
"events": ["session.completed", "session.failed"]
}'Event Types:
session.completed- Payment successfulsession.failed- Payment failedsession.expired- Session timed out
Webhook Payload:
{
"id": "evt_123456",
"event": "session.completed",
"createdAt": "2025-02-04T10:30:00.000Z",
"data": {
"sessionId": "clxxxxxxxxxxxxxxxxxx",
"merchantId": "clxxxxxxxxxxxxxxxxxx",
"status": "completed",
"fiatAmount": "25.00",
"cryptoAmount": "24.87",
"paymentPlatform": "venmo",
"transactionHash": "0xabc123...",
"metadata": {"orderId": "order_123"}
}
}| Endpoint | Limit |
|---|---|
| POST /api/merchants | 10/hour per IP |
| POST /api/checkout/sessions | 100/minute per API key |
| GET /api/checkout/sessions/:id | 300/minute per IP |
| Status | Code | Description |
|---|---|---|
| 400 | invalid_request |
Invalid parameters |
| 401 | unauthorized |
Invalid/missing API key |
| 404 | not_found |
Resource doesn't exist |
| 429 | rate_limited |
Too many requests |
| 500 | server_error |
Internal error |
import requests
import time
import os
class ZKP2PPayClient:
"""Client for ZKP2P Pay API."""
def __init__(self, merchant_id: str, api_key: str, recipient_address: str):
self.merchant_id = merchant_id
self.api_key = api_key
self.recipient_address = recipient_address
self.base_url = "https://api.pay.zkp2p.xyz"
def create_payment(
self,
amount: str,
description: str = None,
metadata: dict = None
) -> dict:
"""Create a payment session."""
response = requests.post(
f"{self.base_url}/api/checkout/sessions",
headers={
"Content-Type": "application/json",
"X-API-Key": self.api_key
},
json={
"merchantId": self.merchant_id,
"checkoutMode": "exact-fiat",
"fiatAmount": amount,
"fiatCurrency": "USD",
"destinationChainId": 8453,
"destinationToken": "USDC",
"recipientAddress": self.recipient_address,
"metadata": metadata or {"description": description}
}
)
data = response.json()
if not data["success"]:
raise Exception(f"Failed to create session: {data.get('message')}")
session = data["responseObject"]["session"]
return {
"session_id": session["id"],
"checkout_url": session["checkoutUrl"],
"expires_at": session["expiresAt"]
}
def get_status(self, session_id: str) -> dict:
"""Get session status."""
response = requests.get(
f"{self.base_url}/api/checkout/sessions/{session_id}"
)
data = response.json()
if not data["success"]:
raise Exception(f"Failed to get session: {data.get('message')}")
return data["responseObject"]["session"]
def wait_for_payment(
self,
session_id: str,
timeout_minutes: int = 30,
poll_interval: int = 30
) -> dict:
"""Poll until payment completes or times out."""
end_time = time.time() + (timeout_minutes * 60)
while time.time() < end_time:
session = self.get_status(session_id)
status = session["status"]
if status == "completed":
return {
"success": True,
"crypto_amount": session.get("cryptoAmount"),
"platform": session.get("paymentPlatform"),
"tx_hash": session.get("transactionHash")
}
elif status in ["failed", "expired"]:
return {"success": False, "status": status}
print(f"Status: {status}, waiting...")
time.sleep(poll_interval)
return {"success": False, "status": "timeout"}
# --- Setup (one-time) ---
def setup_merchant(name: str) -> tuple[str, str]:
"""Register as a merchant. Returns (merchant_id, api_key)."""
response = requests.post(
"https://api.pay.zkp2p.xyz/api/merchants",
json={"name": name}
)
data = response.json()
if not data["success"]:
raise Exception(f"Failed to create merchant: {data.get('message')}")
merchant = data["responseObject"]["merchant"]
api_key = data["responseObject"]["apiKey"]
print(f"Merchant ID: {merchant['id']}")
print(f"API Key: {api_key}")
print("SAVE THESE - API key is only shown once!")
return merchant["id"], api_key
# --- Usage Example ---
if __name__ == "__main__":
# Use environment variables or setup first
MERCHANT_ID = os.environ.get("ZKP2P_MERCHANT_ID")
API_KEY = os.environ.get("ZKP2P_API_KEY")
RECIPIENT = os.environ.get("ZKP2P_RECIPIENT_ADDRESS")
if not all([MERCHANT_ID, API_KEY, RECIPIENT]):
print("First time? Run setup:")
merchant_id, api_key = setup_merchant("My AI Agent")
print("\nSet environment variables:")
print(f"export ZKP2P_MERCHANT_ID={merchant_id}")
print(f"export ZKP2P_API_KEY={api_key}")
print("export ZKP2P_RECIPIENT_ADDRESS=0xYourWallet")
exit(0)
# Create client
client = ZKP2PPayClient(MERCHANT_ID, API_KEY, RECIPIENT)
# Create payment
payment = client.create_payment(
amount="5.00",
description="Code review service"
)
print(f"Pay here: {payment['checkout_url']}")
# Wait for payment
result = client.wait_for_payment(payment["session_id"], timeout_minutes=10)
if result["success"]:
print(f"Payment received! {result['crypto_amount']} USDC")
print(f"Transaction: {result['tx_hash']}")
else:
print(f"Payment {result['status']}")#!/bin/bash
# poll-payment.sh - Wait for payment completion
set -e
SESSION_ID="${1:?Usage: $0 <session_id>}"
BASE_URL="${ZKP2P_API_URL:-https://api.pay.zkp2p.xyz}"
MAX_ATTEMPTS=60 # 30 minutes at 30-second intervals
ATTEMPT=0
echo "Waiting for payment on session: $SESSION_ID"
echo ""
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
RESPONSE=$(curl -s "$BASE_URL/api/checkout/sessions/$SESSION_ID")
STATUS=$(echo "$RESPONSE" | jq -r '.responseObject.session.status')
case $STATUS in
"completed")
CRYPTO=$(echo "$RESPONSE" | jq -r '.responseObject.session.cryptoAmount')
PLATFORM=$(echo "$RESPONSE" | jq -r '.responseObject.session.paymentPlatform')
TX=$(echo "$RESPONSE" | jq -r '.responseObject.session.transactionHash')
echo "✓ Payment received!"
echo " Amount: $CRYPTO USDC"
echo " Platform: $PLATFORM"
echo " Transaction: $TX"
exit 0
;;
"failed")
echo "✗ Payment failed"
exit 1
;;
"expired")
echo "✗ Session expired"
exit 1
;;
*)
echo "Status: $STATUS - waiting... (attempt $((ATTEMPT+1))/$MAX_ATTEMPTS)"
;;
esac
sleep 30
((ATTEMPT++))
done
echo "✗ Timeout waiting for payment"
exit 1class PayPerTaskAgent:
"""Agent that charges per task."""
PRICING = {
"code_review": "5.00",
"bug_fix": "15.00",
"feature": "50.00",
"consultation": "25.00"
}
def __init__(self, payment_client: ZKP2PPayClient):
self.client = payment_client
self.pending_tasks = {}
def request_task(self, user_id: str, task_type: str, description: str) -> dict:
"""Create a task and request payment."""
price = self.PRICING.get(task_type, "10.00")
task_id = f"task_{user_id}_{int(time.time())}"
payment = self.client.create_payment(
amount=price,
metadata={
"task_id": task_id,
"task_type": task_type,
"description": description,
"user_id": user_id
}
)
self.pending_tasks[task_id] = {
"session_id": payment["session_id"],
"task_type": task_type,
"description": description
}
return {
"task_id": task_id,
"price": price,
"payment_url": payment["checkout_url"],
"message": f"Please pay ${price} for {task_type}: {payment['checkout_url']}"
}
def check_and_execute(self, task_id: str) -> dict:
"""Check if paid, then execute task."""
task = self.pending_tasks.get(task_id)
if not task:
return {"error": "Task not found"}
result = self.client.wait_for_payment(
task["session_id"],
timeout_minutes=5
)
if result["success"]:
# Execute the task here
output = self._execute_task(task)
del self.pending_tasks[task_id]
return {
"status": "completed",
"output": output,
"payment": result
}
else:
return {
"status": "unpaid",
"message": f"Payment {result['status']}. Please try again."
}
def _execute_task(self, task: dict) -> str:
"""Execute the actual task (implement your logic here)."""
return f"Completed {task['task_type']}: {task['description']}"
# Usage
agent = PayPerTaskAgent(client)
# User requests a task
request = agent.request_task(
user_id="user_123",
task_type="code_review",
description="Review my authentication module"
)
print(request["message"])
# After user pays...
result = agent.check_and_execute(request["task_id"])
if result["status"] == "completed":
print(result["output"])- Store API keys securely - Use environment variables or secrets manager
- Never log API keys - Mask in logs if needed
- Validate webhook signatures - Verify
X-Webhook-Signatureheader - Use HTTPS only - All API calls should be over TLS
- Handle all statuses - Don't just check for "completed"
- Implement retries - Use exponential backoff for network errors
- Set appropriate timeouts - Don't wait forever for payments
- Use metadata - Include task/order IDs for reconciliation
- Confirm before charging - Always show amount before creating session
- Provide clear instructions - Tell users what to expect
- Handle failures gracefully - Offer retry or alternative
- Send reminders - Sessions expire after 24 hours
| Error | Cause | Solution |
|---|---|---|
401 Unauthorized |
Invalid/missing API key | Check X-API-Key header |
Only USDC is supported |
Wrong destinationToken |
Use "USDC" (string), not contract address |
No default EVM wallet |
Missing recipientAddress |
Add recipientAddress to request |
Invalid fiatAmount |
Wrong format | Use string like "10.00", not number |
Session expired |
Took too long | Create a new session |
- Check response body - Error details are in
messagefield - Verify merchant exists - GET
/api/merchants/:id - Check session status - GET
/api/checkout/sessions/:id - Review API logs - Check your agent's logs for full response
#!/bin/bash
# test-integration.sh
BASE_URL="${ZKP2P_API_URL:-https://api.pay.zkp2p.xyz}"
echo "Testing ZKP2P Pay API..."
# Test 1: Create merchant
echo "1. Creating test merchant..."
RESPONSE=$(curl -s -X POST "$BASE_URL/api/merchants" \
-H "Content-Type: application/json" \
-d '{"name": "Test Agent"}')
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
if [ "$SUCCESS" = "true" ]; then
MERCHANT_ID=$(echo "$RESPONSE" | jq -r '.responseObject.merchant.id')
API_KEY=$(echo "$RESPONSE" | jq -r '.responseObject.apiKey')
echo " ✓ Merchant: $MERCHANT_ID"
echo " ✓ API Key: ${API_KEY:0:20}..."
else
echo " ✗ Failed: $(echo "$RESPONSE" | jq -r '.message')"
exit 1
fi
# Test 2: Create session
echo "2. Creating checkout session..."
SESSION=$(curl -s -X POST "$BASE_URL/api/checkout/sessions" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d "{
\"merchantId\": \"$MERCHANT_ID\",
\"checkoutMode\": \"exact-fiat\",
\"fiatAmount\": \"1.00\",
\"fiatCurrency\": \"USD\",
\"destinationChainId\": 8453,
\"destinationToken\": \"USDC\",
\"recipientAddress\": \"0x0000000000000000000000000000000000000001\"
}")
SESSION_SUCCESS=$(echo "$SESSION" | jq -r '.success')
if [ "$SESSION_SUCCESS" = "true" ]; then
SESSION_ID=$(echo "$SESSION" | jq -r '.responseObject.session.id')
CHECKOUT_URL=$(echo "$SESSION" | jq -r '.responseObject.session.checkoutUrl')
echo " ✓ Session: $SESSION_ID"
echo " ✓ URL: $CHECKOUT_URL"
else
echo " ✗ Failed: $(echo "$SESSION" | jq -r '.message')"
exit 1
fi
# Test 3: Get status
echo "3. Checking session status..."
STATUS=$(curl -s "$BASE_URL/api/checkout/sessions/$SESSION_ID" \
| jq -r '.responseObject.session.status')
echo " ✓ Status: $STATUS"
echo ""
echo "========================================="
echo "All tests passed!"
echo "========================================="- Documentation: https://docs.pay.zkp2p.xyz
- GitHub Issues: https://github.com/zkp2p/pay/issues
- API Status: https://status.pay.zkp2p.xyz
Generated for ClawdHub marketplace. Last updated: February 4, 2025.