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
141 changes: 141 additions & 0 deletions skill/x402-pay/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
name: x402-pay
description: Make x402 payments to access paid API endpoints. Use when fetching a URL that returns 402 Payment Required, or when the user asks to "pay for" or "access" an x402-protected resource.
argument-hint: <url> [--method POST] [--body '{}'] [--headers '{}']
arguments: url
allowed-tools: Bash(npx *) Bash(pnpm *) Bash(security find-generic-password *)
---

# x402-pay

Makes a signed x402 payment to a paid API endpoint and returns the response. Handles the full flow: request, 402 detection, EIP-712 signing, retry with payment header. Supports both `exact` (fixed price) and `upto` (variable price) schemes automatically.

The private key is stored in macOS Keychain and read by the script at runtime. It never enters the conversation.

## Prerequisites

```!
security find-generic-password -s x402-agent-key -a x402 -w 2>/dev/null && echo "KEYCHAIN: CONFIGURED" || echo "KEYCHAIN: NOT_CONFIGURED"
```
Comment on lines +17 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: prerequisite check exposes the private key via -w flag

security find-generic-password -w searches for the keychain item and prints the password when found. The 2>/dev/null only suppresses stderr (errors); stdout — where the key is printed — is not redirected. So the private key lands verbatim in Claude's conversation context, directly violating Security Rules #1 and #3.

Without -w, the command does not print the password — it outputs the keychain object metadata associated with the service name instead. The exit code is still 0 when found and non-zero when absent, so the && / || detection logic works unchanged.

🔒 Proposed fix — remove -w from the existence check
 ```!
-security find-generic-password -s x402-agent-key -a x402 -w 2>/dev/null && echo "KEYCHAIN: CONFIGURED" || echo "KEYCHAIN: NOT_CONFIGURED"
+security find-generic-password -s x402-agent-key -a x402 2>/dev/null && echo "KEYCHAIN: CONFIGURED" || echo "KEYCHAIN: NOT_CONFIGURED"
</details>

Note that `allowed-tools: Bash(security find-generic-password *)` on Line 6 is unrestricted (any arguments), so the agent is technically permitted to re-introduce `-w` later. With the check itself fixed, the security rules serve as the remaining guardrail.

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In @skill/x402-pay/SKILL.md around lines 17 - 19, The check currently uses
"security find-generic-password -w" which prints the private key to stdout;
remove the "-w" flag so the command only checks existence (use "security
find-generic-password -s x402-agent-key -a x402" with stderr redirected to
/dev/null) and keep the existing "&& echo 'KEYCHAIN: CONFIGURED' || echo
'KEYCHAIN: NOT_CONFIGURED'" logic unchanged; ensure any documentation or
examples referencing this existence check (the "security find-generic-password"
invocation) are updated to omit "-w" to avoid exfiltrating secrets.


</details>

<!-- fingerprinting:phantom:poseidon:churro -->

<!-- 4e71b3a2 -->

<!-- This is an auto-generated comment by CodeRabbit -->


```!
test -d "${CLAUDE_SKILL_DIR}/scripts/node_modules" && echo "DEPS: INSTALLED" || echo "DEPS: MISSING"
```

## Setup (only if NOT_CONFIGURED or MISSING above)

If **DEPS: MISSING**, install dependencies first:

```bash
pnpm install --prefix "${CLAUDE_SKILL_DIR}/scripts"
```

If **KEYCHAIN: NOT_CONFIGURED**, the user needs to store their private key. Tell them:

> Your x402 agent private key isn't configured yet. Please run this command — it will prompt you to paste your key securely (it won't be displayed):
>
> ```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Minor markdown lint: fenced code block missing language specifier (MD040)

The !-fenced block at Line 37 has no language identifier, triggering a markdownlint MD040 warning.

✏️ Proposed fix
-> ```
+> ```bash
>  ! security add-generic-password -s x402-agent-key -a x402 -U -w
> ```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 37-37: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@skill/x402-pay/SKILL.md` at line 37, Add a language identifier to the fenced
code block containing the command string "! security add-generic-password -s
x402-agent-key -a x402 -U -w" in SKILL.md so the block becomes a fenced bash
block (e.g., ```bash) to satisfy markdownlint MD040; locate the fence that wraps
that exact command and update its opening fence to include the language
specifier.

> ! security add-generic-password -s x402-agent-key -a x402 -U -w
> ```
>
> The key should be a hex string with `0x` prefix. If you need to export one from CDP, run:
> `uv run python playground/agent-test/export-key.py <wallet-address>`

After the key is stored, run the setup check to verify balances and Permit2 approval:

```bash
npx --prefix "${CLAUDE_SKILL_DIR}/scripts" tsx "${CLAUDE_SKILL_DIR}/scripts/setup.ts"
```

If Permit2 is not approved (needed for `upto` scheme payments), run:

```bash
npx --prefix "${CLAUDE_SKILL_DIR}/scripts" tsx "${CLAUDE_SKILL_DIR}/scripts/setup.ts" --approve
```

Once setup outputs `"status": "ready"`, proceed to usage.

## Usage

Run the payment script with the target URL:

```bash
npx --prefix "${CLAUDE_SKILL_DIR}/scripts" tsx "${CLAUDE_SKILL_DIR}/scripts/x402-fetch.ts" "$url"
```

### Options

| Flag | Default | Description |
|------|---------|-------------|
| `--method` / `-m` | `GET` | HTTP method |
| `--body` / `-b` | none | JSON request body |
| `--headers` / `-H` | none | Extra headers as JSON object |

### Examples

```bash
# Simple GET to a paid endpoint
npx --prefix "${CLAUDE_SKILL_DIR}/scripts" tsx "${CLAUDE_SKILL_DIR}/scripts/x402-fetch.ts" "https://api.example.com/premium"

# POST with body and custom headers
npx --prefix "${CLAUDE_SKILL_DIR}/scripts" tsx "${CLAUDE_SKILL_DIR}/scripts/x402-fetch.ts" "https://api.example.com/search" \
--method POST \
--body '{"query": "test"}' \
--headers '{"X-Custom": "value"}'
```

## Output Format

The script outputs a single JSON object to stdout. Parse it to determine the result.

**Success** (payment completed, resource returned):
```json
{
"status": "success",
"httpStatus": 200,
"body": { "...resource data..." },
"settlement": { "txHash": "0x...", "network": "eip155:8453", "payer": "0x..." },
"payment": { "scheme": "exact", "amountMicro": 10000, "amountUSD": "0.010000" }
}
```

**No payment needed** (endpoint didn't return 402):
```json
{
"status": "no_payment_required",
"httpStatus": 200,
"body": { "...response data..." }
}
```

**Error** (something went wrong):
```json
{
"status": "error",
"errorType": "INSUFFICIENT_BALANCE",
"message": "human-readable explanation",
"details": { "walletAddress": "0x...", "balanceUSDC": "0.5", "requiredUSDC": "1.0" }
}
```
Comment on lines +91 to +119
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Minor markdown lint: fenced code blocks missing surrounding blank lines (MD031)

Three json code blocks (Lines 92, 103, 112) immediately follow bold-text headers without a blank line separator, triggering MD031 warnings. Adding a blank line before each opening fence resolves all three.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 92-92: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 103-103: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 112-112: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 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 `@skill/x402-pay/SKILL.md` around lines 91 - 119, Add a blank line between each
bold header and its following fenced JSON block for the three examples labeled
"**Success**", "**No payment needed**", and "**Error**" in SKILL.md so that each
code fence is separated from the preceding header (this fixes MD031 by placing
an empty line before each opening ```json fence).


## Error Handling

| errorType | Meaning | What to do |
|-----------|---------|-----------|
| `KEYCHAIN_ERROR` | Private key not in Keychain | Run the setup flow above |
| `INSUFFICIENT_BALANCE` | USDC balance too low | Tell the user to fund their wallet (address is in details) |
| `PERMIT2_NOT_APPROVED` | Permit2 not approved for USDC | Run `setup.ts --approve` |
| `PAYMENT_REJECTED` | Server rejected the signed payment | Report the error body to the user |
| `SIGNING_FAILED` | EIP-712 signing failed | Likely a library or chain mismatch — report to user |
| `NETWORK_ERROR` | Could not reach the URL | Check URL and network connectivity |
| `INVALID_ARGS` | Bad CLI arguments | Check the command format |

## Security Rules

These rules are non-negotiable:

1. **NEVER** attempt to read, display, or log the private key from Keychain
2. **NEVER** include the raw `PAYMENT-SIGNATURE` header value in conversation output
3. **NEVER** run `security find-generic-password -s x402-agent-key -w` directly — only the TypeScript script reads the key internally
4. **NEVER** ask the user to paste their private key into the chat — always direct them to use the `! security add-generic-password ...` command
5. All cryptographic operations happen inside `x402-fetch.ts` — the agent only sees the structured JSON output
2 changes: 2 additions & 0 deletions skill/x402-pay/scripts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
14 changes: 14 additions & 0 deletions skill/x402-pay/scripts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "x402-pay-skill",
"private": true,
"type": "module",
"dependencies": {
"@x402/evm": "^2.11.0",
"viem": "^2.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.21.0",
"typescript": "^5.7.0"
}
}
Loading