Add x402-pay skill for public distribution via skill.sh#231
Add x402-pay skill for public distribution via skill.sh#231simantak-dabhade wants to merge 1 commit into
Conversation
Publishes the x402-pay Claude Code skill at skill/x402-pay/ so it can be discovered and installed by anyone via skill.sh, which indexes skills from public GitHub repos. The skill lets any Claude Code agent make x402 payments to access paid API endpoints — handles the full flow (request, 402, EIP-712 signing, retry) with the private key stored securely in macOS Keychain.
📝 WalkthroughWalkthroughThis PR adds the Changesx402-pay Skill
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Microsoft Presidio Analyzer (2.2.362)skill/x402-pay/SKILL.mdMicrosoft Presidio Analyzer failed to scan this file Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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 |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
1 similar comment
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with 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.
Inline comments:
In `@skill/x402-pay/scripts/setup.ts`:
- Line 136: The current call to walletClient.waitForTransactionReceipt({ hash })
can hang indefinitely; update the await to use a bounded wait (e.g., pass a
timeout option if the client supports one or wrap the call in a Promise.race
with a timeout promise) so it rejects after a reasonable period (e.g., 30–60s);
handle the rejection by returning/throwing a clear error or sentinel (e.g.,
null) from the function and ensure the calling code is updated to log or surface
that result so the caller can check on-chain status manually; focus changes
around waitForTransactionReceipt, walletClient, receipt and hash.
In `@skill/x402-pay/scripts/x402-fetch.ts`:
- Around line 203-211: The code converts balance (a bigint) to a JS Number
(balanceMicro) which can lose precision; change to use bigint arithmetic
instead: remove the Number(balance) conversion and compare balance (bigint)
directly to amountMicro (bigint) in the insufficiency check, and keep using
formatUnits(balance, 6) for human-readable logging and in the fatal metadata
(use formatUnits to render required values), referencing the existing symbols
balance, amountMicro, formatUnits, fatal, and account.address so the conditional
becomes a bigint comparison and no implicit Number cast occurs.
- Around line 87-89: The JSON.parse call that builds extraHeaders from
values.headers is unguarded and will surface a raw SyntaxError (ending up as an
UNEXPECTED error); wrap the parse in a try/catch around the
JSON.parse(values.headers) used to assign extraHeaders and, on parse failure,
throw/return a clear argument error (set errorType to "INVALID_ARGS" or throw a
CLI/argument error) with a message like "Invalid --headers JSON: <original
message>" so the error is reported as bad input rather than an unexpected
internal failure; update the handling path that currently relies on
main().catch() to preserve the new INVALID_ARGS error shape.
In `@skill/x402-pay/SKILL.md`:
- Around line 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).
- 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.
- Around line 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.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: b290200b-a705-4ffe-a401-c8fd4014c9c0
⛔ Files ignored due to path filters (1)
skill/x402-pay/scripts/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
skill/x402-pay/SKILL.mdskill/x402-pay/scripts/.gitignoreskill/x402-pay/scripts/package.jsonskill/x402-pay/scripts/setup.tsskill/x402-pay/scripts/tsconfig.jsonskill/x402-pay/scripts/x402-fetch.ts
| }); | ||
|
|
||
| log(`Approval tx submitted: ${hash}`); | ||
| const receipt = await walletClient.waitForTransactionReceipt({ hash }); |
There was a problem hiding this comment.
waitForTransactionReceipt will hang indefinitely if the transaction stalls
There is no timeout parameter. On Base mainnet a transaction can get stuck (replaced gas-price, nonce conflict, RPC hiccup), leaving the process blocked forever with no way for the agent to recover.
⏱️ Proposed fix — add a reasonable timeout
-const receipt = await walletClient.waitForTransactionReceipt({ hash });
+const receipt = await walletClient.waitForTransactionReceipt({ hash, timeout: 60_000 });Add a corresponding error path so the caller knows to check on-chain status manually if the timeout fires.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const receipt = await walletClient.waitForTransactionReceipt({ hash }); | |
| const receipt = await walletClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); |
🤖 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/scripts/setup.ts` at line 136, The current call to
walletClient.waitForTransactionReceipt({ hash }) can hang indefinitely; update
the await to use a bounded wait (e.g., pass a timeout option if the client
supports one or wrap the call in a Promise.race with a timeout promise) so it
rejects after a reasonable period (e.g., 30–60s); handle the rejection by
returning/throwing a clear error or sentinel (e.g., null) from the function and
ensure the calling code is updated to log or surface that result so the caller
can check on-chain status manually; focus changes around
waitForTransactionReceipt, walletClient, receipt and hash.
| const extraHeaders: Record<string, string> = values.headers | ||
| ? JSON.parse(values.headers) | ||
| : {}; |
There was a problem hiding this comment.
Unguarded JSON.parse for --headers produces a misleading UNEXPECTED error
If the user passes malformed JSON (e.g., --headers '{bad}'), the thrown SyntaxError propagates to the outer main().catch(), which produces { errorType: "UNEXPECTED" }. This is confusing because the problem is clearly bad CLI input, not an internal error.
🛡️ Proposed fix — catch and convert to INVALID_ARGS
-const extraHeaders: Record<string, string> = values.headers
- ? JSON.parse(values.headers)
- : {};
+let extraHeaders: Record<string, string> = {};
+if (values.headers) {
+ try {
+ extraHeaders = JSON.parse(values.headers);
+ } catch {
+ fatal("INVALID_ARGS", `--headers must be a valid JSON object, got: ${values.headers}`);
+ }
+}🤖 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/scripts/x402-fetch.ts` around lines 87 - 89, The JSON.parse
call that builds extraHeaders from values.headers is unguarded and will surface
a raw SyntaxError (ending up as an UNEXPECTED error); wrap the parse in a
try/catch around the JSON.parse(values.headers) used to assign extraHeaders and,
on parse failure, throw/return a clear argument error (set errorType to
"INVALID_ARGS" or throw a CLI/argument error) with a message like "Invalid
--headers JSON: <original message>" so the error is reported as bad input rather
than an unexpected internal failure; update the handling path that currently
relies on main().catch() to preserve the new INVALID_ARGS error shape.
| const balanceMicro = Number(balance); | ||
| log(`USDC balance: ${formatUnits(balance, 6)} USDC`); | ||
|
|
||
| if (balanceMicro < amountMicro) { | ||
| fatal("INSUFFICIENT_BALANCE", `USDC balance too low: have ${formatUnits(balance, 6)}, need ${amountUSD}`, { | ||
| walletAddress: account.address, | ||
| balanceUSDC: formatUnits(balance, 6), | ||
| requiredUSDC: amountUSD, | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Number(bigint) loses precision for large USDC balances and enables imprecise comparison
Converting balance (a bigint) to Number at Line 203 silently drops precision for values above Number.MAX_SAFE_INTEGER (≈9 × 10¹⁵ microunits ≈ 9 billion USDC). While this is an astronomically large balance for an agent wallet in practice, the comparison at Line 206 should use bigint arithmetic to stay correct by construction and avoid the implicit cast.
♻️ Proposed refactor — compare as bigint throughout
- const balanceMicro = Number(balance);
- log(`USDC balance: ${formatUnits(balance, 6)} USDC`);
-
- if (balanceMicro < amountMicro) {
- fatal("INSUFFICIENT_BALANCE", `USDC balance too low: have ${formatUnits(balance, 6)}, need ${amountUSD}`, {
+ log(`USDC balance: ${formatUnits(balance, 6)} USDC`);
+
+ if (balance < BigInt(amountMicro)) {
+ fatal("INSUFFICIENT_BALANCE", `USDC balance too low: have ${formatUnits(balance, 6)}, need ${amountUSD}`, {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const balanceMicro = Number(balance); | |
| log(`USDC balance: ${formatUnits(balance, 6)} USDC`); | |
| if (balanceMicro < amountMicro) { | |
| fatal("INSUFFICIENT_BALANCE", `USDC balance too low: have ${formatUnits(balance, 6)}, need ${amountUSD}`, { | |
| walletAddress: account.address, | |
| balanceUSDC: formatUnits(balance, 6), | |
| requiredUSDC: amountUSD, | |
| }); | |
| log(`USDC balance: ${formatUnits(balance, 6)} USDC`); | |
| if (balance < BigInt(amountMicro)) { | |
| fatal("INSUFFICIENT_BALANCE", `USDC balance too low: have ${formatUnits(balance, 6)}, need ${amountUSD}`, { | |
| walletAddress: account.address, | |
| balanceUSDC: formatUnits(balance, 6), | |
| requiredUSDC: amountUSD, | |
| }); |
🤖 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/scripts/x402-fetch.ts` around lines 203 - 211, The code
converts balance (a bigint) to a JS Number (balanceMicro) which can lose
precision; change to use bigint arithmetic instead: remove the Number(balance)
conversion and compare balance (bigint) directly to amountMicro (bigint) in the
insufficiency check, and keep using formatUnits(balance, 6) for human-readable
logging and in the fatal metadata (use formatUnits to render required values),
referencing the existing symbols balance, amountMicro, formatUnits, fatal, and
account.address so the conditional becomes a bigint comparison and no implicit
Number cast occurs.
| ```! | ||
| security find-generic-password -s x402-agent-key -a x402 -w 2>/dev/null && echo "KEYCHAIN: CONFIGURED" || echo "KEYCHAIN: NOT_CONFIGURED" | ||
| ``` |
There was a problem hiding this comment.
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 -->
|
|
||
| > 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): | ||
| > | ||
| > ``` |
There was a problem hiding this comment.
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.
| **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" } | ||
| } | ||
| ``` |
There was a problem hiding this comment.
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).
Summary
skill/x402-pay/so it can be discovered and installed by anyone via skill.shSKILL.mdfiles inskill/directories — placing it here makes it installable with a single commandWhat the skill does
Lets any Claude Code agent make x402 payments to access paid API endpoints. The agent invokes
/x402-pay <url>and gets structured JSON back — it never touches the private key.PAYMENT-SIGNATUREheaderFiles
skill/x402-pay/SKILL.mdscripts/x402-fetch.tsscripts/setup.tsscripts/package.json@x402/evm,viem,tsxTest plan
pnpm installinskill/x402-pay/scripts/installs cleanly/x402-paywith no Keychain key → triggers setup flow/x402-pay http://localhost:4001/premium→ completes exact payment/x402-pay http://localhost:4001/api/search?q=test→ completes upto payment🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation