From x402fhe
Direct-token operations on Zama-encrypted cUSDC for x402fhe — read wallet/network info, check public and encrypted balances (own or via prior delegation), wrap plain USDC into confidential cUSDC, and send encrypted payments through the X402PaymentVerifier relay. Use when the user asks to pay, send, transfer, wrap, deposit, top up, check balance, decrypt balance, see wallet info, or inspect contract addresses for x402fhe confidential payments. Do NOT use for redeeming cUSDC back to USDC, escrow job funding, agent identity, reputation feedback, balance delegation setup, or HTTP-402 demo orchestration — those are handled by sibling skills (fhe-payment-unwrap, fhe-escrow, fhe-agent-identity, fhe-delegation, x402-demo-orchestrators).
How this skill is triggered — by the user, by Claude, or both
Slash command
/x402fhe:fhe-payment-basicsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Four OpenClaw commands for direct cUSDC operations: `info`, `balance`, `wrap`, `pay`. Each script lives in `packages/openclaw-skill/scripts/` and is invoked via pnpm + tsx from anywhere in the monorepo. Each emits a single JSON line to stdout and never throws to the shell — parse it and branch on `ok`, not on bash exit code.
Four OpenClaw commands for direct cUSDC operations: info, balance, wrap, pay. Each script lives in packages/openclaw-skill/scripts/ and is invoked via pnpm + tsx from anywhere in the monorepo. Each emits a single JSON line to stdout and never throws to the shell — parse it and branch on ok, not on bash exit code.
paypay.ts returns a nonce field. That nonce is the ONLY proof-of-payment binding for downstream give-feedback, and it is NOT recoverable from chain state.
The pay command returns this exact shape (from scripts/pay.ts lines 43-49):
{
"ok": true,
"action": "pay",
"to": "0xAbCdEf0123456789aBcDeF0123456789AbCdEf01",
"amount": "0.5",
"txHash": "0x...",
"nonce": "0x<64 hex chars — 32 bytes>"
}
The nonce is generated client-side at line 21: const nonce = ethers.hexlify(ethers.randomBytes(32));. It is fresh-random per invocation, embedded into the confidentialTransferAndCall calldata, and consumed by X402PaymentVerifier.onConfidentialTransferReceived for replay protection. The verifier marks the nonce as used but does not store the raw bytes anywhere queryable — there is no Nonce(bytes32) event, no public mapping keyed on the nonce that you can iterate, and no way to derive it from the txHash. Once the JSON line scrolls out of the chat, the nonce is gone forever.
It matters because give-feedback.ts (lines 33-39) derives the proof-of-payment as:
const proofOfPayment = ethers.keccak256(
ethers.solidityPacked(['bytes32', 'address'], [args.nonce, await verifier.getAddress()]),
);
If the user later asks you to rate the recipient of a payment, the only field needed from the prior pay invocation is the nonce. No nonce, no feedback — permanently.
Rules:
nonce verbatim in your reply when pay succeeds, even if the user did not explicitly ask for it. Format it inside a code span: `nonce: 0x<64 hex>`. Do NOT paraphrase, summarize, abbreviate with …, or wrap it across lines. The user must be able to copy-paste the full 32-byte value.give-feedback --nonce. It cannot be recovered from the chain."pay result.pay reply. This is a discipline rule, not a stylistic preference.Rationalizations that do NOT override these rules:
| Rationalization | Why it is wrong |
|---|---|
| "The user did not ask for a nonce" | They cannot ask for what they do not know exists. The first mention of the nonce is from you, in this reply. |
| "It is just an implementation detail" | It is the only input to proofOfPayment for give-feedback. There is no fallback derivation. |
| "I have the txHash, that is enough" | The txHash proves a transfer happened. It does NOT give give-feedback what it needs — proofOfPayment = keccak256(nonce ‖ verifierAddress) cannot be reconstructed from a tx hash. |
| "I am in a hurry, the user is queued" | A 64-character hex string costs one line. Skipping it loses the ability to rate forever. The user is not actually faster if they have to redo the payment because they cannot rate. |
| "I will surface it later if feedback comes up" | By "later" the JSON line from pay is scrolled off, your context window is compacted, or the agent has been swapped. The nonce must live in the chat transcript, not in your head. |
| "I will grep the bash output buffer when I need it" | Bash output buffers are not persistent across turns or sessions. The stdout JSON exists for exactly one turn. |
| "It looks like internal protocol state" | The burnHandle in fhe-payment-unwrap looks the same way and is treated identically. Treat the nonce as a user-owned claim ticket, not as internal state. |
Red-flag self-check: if your drafted reply to the user contains txHash but does NOT contain the literal nonce hex inside a code span, delete the draft and rewrite. No exceptions.
These rules apply to every command in this skill:
{"ok": true, "action": "<name>", ...} or {"ok": false, "error": "..."}. The canonical success check is parsed.ok === true. The action field tells you which command ran (info, balance, wrap, pay); it is informational, not a second success signal. Bash exit code is unreliable — a script that returns {"ok": false, ...} still exits 0.wrap, pay). Both submit signed transactions. A retry is a second on-chain transaction, a second gas spend, and a second nonce on pay. If the first call returned an error, surface it to the user and wait for instructions. (Read-only info and balance are safe to re-run.)pay returned ok: false after the tx was already submitted (rare but possible mid-flight), the txHash may still be in the error message — surface whatever you have. Do not "clean up" by retrying."execution reverted" into your own words — the exact text is often the only diagnostic clue (a hex revert reason, a contract-specific message, a node-layer error code).Every command in this skill MUST be invoked through the workspace filter, exactly like this:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/<command>.ts --flag value
The plugin-local package.json has only "test": "vitest run" — there is no "pay" or "info" script. Do not look for one and do not invent one.
Forbidden invocation forms (will fail in confusing ways):
node scripts/pay.ts --to ... — node cannot run TypeScript directly.npx tsx scripts/pay.ts --to ... — does not resolve workspace dependencies; _wallet.ts will fail to import its workspace packages.ts-node scripts/pay.ts --to ... — same problem, plus ESM/CJS friction.tsx scripts/pay.ts --to ... from any cwd outside packages/openclaw-skill/ — same problem.pnpm pay --to ... — there is no such pnpm script.Why: the scripts share a _wallet.ts singleton that imports from @x402fhe/core and other workspace packages. Those imports only resolve through the pnpm workspace package graph, which pnpm --filter ... exec activates. Any other launcher will hit a module resolution error before the script even reaches its first line.
You need one wallet mode plus an RPC URL set in env. See ../../references/wallet-setup.md for the three modes (user, dfns, ledger-bridge), ../../references/env-vars.md for the full variable list, and ../../references/output-schema.md for the canonical ok/fail wrapper shape. Minimum working dev setup:
export WALLET_MODE=user
export USER_PRIVATE_KEY=0x... # 64 hex chars
export RPC_URL=https://sepolia.infura.io/v3/YOUR_KEY
When RPC_URL points to Sepolia (chain ID 11155111), SEPOLIA_ADDRESSES are auto-selected — do not override CUSDC_ADDRESS, VERIFIER_ADDRESS, etc. unless you are targeting a non-default deployment.
WARNING — zero-address fallback footgun: if any contract address env var (VERIFIER_ADDRESS, CUSDC_ADDRESS, etc.) is missing AND the chain auto-detect does not provide it, _wallet.ts falls back to '0x' + '00'.repeat(20) (the zero address) and silently constructs a Contract proxy pointing at it. The script appears healthy until the actual transaction hits Sepolia, at which point you get a cryptic "no code at address" error or an empty-data revert. Always run info first to print all five contract addresses and visually confirm none of them are 0x0000000000000000000000000000000000000000. This single check prevents the nastiest debugging time-sink in the whole skill.
(Maintainer note: getContracts() returns a Proxy that lazily constructs each contract on first property access — the type annotation lists all six, but only the ones you destructure actually get built. This is invisible to users but worth knowing if you read the source and wonder why some contracts seem to "not exist" until touched.)
When to use: the user asks about wallet address, current network, ETH balance, contract addresses, or "what is configured", or you need to sanity-check the env before a write operation. ALSO use this preemptively before your first wrap or pay in a session to confirm the contract addresses are non-zero (see the WARNING above).
Invocation:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/info.ts
No flags. No required args.
Required env: RPC_URL, one wallet mode.
Returns:
{
"ok": true,
"action": "info",
"network": "Ethereum Sepolia",
"chainId": 11155111,
"walletType": "user",
"walletAddress": "0x...",
"ethBalance": "12345678900000000",
"contracts": {
"cUSDC": "0x...",
"verifier": "0x...",
"identity": "0x...",
"reputation": "0x...",
"escrow": "0x..."
},
"scheme": "fhe-confidential-v1"
}
Common errors:
Missing USER_PRIVATE_KEY (or Missing DFNS_*) — wallet env vars not set. See ../../references/wallet-setup.md.could not detect network — bad RPC_URL, rate-limited, or endpoint down. Verify the URL and try a different provider.0x0000000000000000000000000000000000000000 — the env var for that contract is missing and the zero-address fallback kicked in. Set the env var explicitly or fix RPC_URL to a recognized chain._wallet.ts init crashed before returning JSON. Check env vars and pnpm install.When to use: the user asks "how much cUSDC do I have", "what is my encrypted balance", "do I have enough to pay X", or "what does Alice's balance look like". Read-only. Also use this implicitly before any pay to confirm sufficient encrypted funds.
Invocation (own balance, public + encrypted handle only):
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/balance.ts
Invocation (own balance, decrypt encrypted via Zama KMS):
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/balance.ts --decrypt true
--decrypt true requires a live @zama-fhe/sdk install (not the config-only stub). On stub mode the call returns the response with a delegationError field explaining the SDK is unavailable.
Invocation (read another address via prior delegation):
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/balance.ts --of 0xAlice...
--of is a delegated read. The target address must have previously run grant-view --delegate <yourAddress> --contract <cUSDCAddress> (see fhe-delegation), and propagation typically takes 1-2 minutes. Delegation errors are non-fatal — the script returns the public balance plus a delegationError field describing what to do.
Required env: RPC_URL, one wallet mode.
Optional flags:
--decrypt true — decrypt own encrypted balance via Zama KMS. Live SDK only.--of 0xAddr — read a different address's balance (requires prior delegation from that address).Returns:
{
"ok": true,
"action": "balance",
"walletAddress": "0x...",
"publicBalance": "1500000",
"publicBalanceUSDC": "1.500000",
"hasEncryptedBalance": true,
"encryptedBalanceHandle": "0x...",
"decryptedBalance": "0.500000",
"decryptedBalanceRaw": "500000"
}
decryptedBalance and decryptedBalanceRaw are present only when --decrypt true succeeded against own balance, or when --of succeeded against a delegated read. viewingAs is added on --of calls. delegationError is added on --of calls where decryption could not complete (no delegation, expired, not yet propagated, cooldown).
Common errors:
--of must be a valid Ethereum address — typo in the address. Use 0x + exactly 40 hex chars.delegationError: "No delegation — ask the owner to run grant-view --delegate ..." — non-fatal. The public balance is still returned. Tell the user to ask the target to grant view access.delegationError: "Delegation not yet propagated — wait 1-2 minutes after grant-view" — non-fatal. Same as above; wait briefly.delegationError: "Decrypt requires @zama-fhe/sdk (not available in stub mode)" — install the live SDK to enable --decrypt true.could not detect network — bad RPC_URL.When to use: the user has plain USDC (ERC-20) and wants to convert it into confidential cUSDC (ERC-7984) so they can later send it via pay. This is a prerequisite for any FHE payment when the user only holds plain USDC.
Invocation:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/wrap.ts --amount 10
--amount is a human-readable decimal string in USDC units. parseAmount converts it to 6-decimal micro-USDC internally (see "Amount handling" below for the truncation rule).
Two on-chain transactions, in order:
usdc.approve(cUSDCAddress, rawAmount) — gas hardcoded at 100k.cUSDC.wrap(walletAddress, rawAmount) — gas hardcoded at 500k.Both must succeed; the script awaits the second receipt before returning. The returned txHash is the wrap tx (the second one), not the approve tx.
Required env: RPC_URL, one wallet mode, enough plain USDC balance, enough ETH for two transactions (~600k gas total).
Returns:
{
"ok": true,
"action": "wrap",
"amount": "10",
"txHash": "0x...",
"blockNumber": 12345678
}
Common errors:
--amount is required — forgot the flag.Invalid amount "..." — non-numeric input. Use 10, not ten.execution reverted on approve — possible USDC balance insufficient for the amount, or USDC contract not deployed at the configured address.execution reverted on wrap — usually the approve did not propagate or the cUSDC contract is not at the expected address. Run info to verify all contract addresses.insufficient funds for gas — need more Sepolia ETH. The wallet address from info is where to send it.When to use: the user asks to pay, send, transfer, or remit some amount of cUSDC to a specific Ethereum address with confidentiality. The recipient address is passed as --to, the amount as --amount (decimal cUSDC).
Invocation:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/pay.ts --to 0xAbCdEf0123456789aBcDeF0123456789AbCdEf01 --amount 0.5
The verifier relay pattern (read this before being confused by the bash command).
The script does NOT call cUSDC.confidentialTransfer(recipient, amount) directly. Instead, it calls cUSDC.confidentialTransferAndCall(verifierAddress, encAmount, proof, data) where data = abi.encode(['address','bytes32','uint64'], [recipient, nonce, rawAmount]). The recipient is buried inside the encoded data blob, NOT passed as the first argument. This is intentional and correct — it is the canonical x402fhe verifier relay pattern.
What happens on-chain:
cUSDC receives the encrypted amount and runs the FHE.fromExternal proof verification.cUSDC transfers the encrypted amount to the verifier address (so the verifier momentarily holds the cUSDC).cUSDC calls back into X402PaymentVerifier.onConfidentialTransferReceived(operator, from, encAmount, data).data to extract (recipient, nonce, rawAmount), marks the nonce used (replay protection), stores the encrypted amount handle in paymentAmounts[nonce] for later facilitator decryption, grants persistent FHE ACL access to the recipient via FHE.allow(amount, recipient), then forwards the cUSDC to the real recipient via confidentialTransfer(recipient, amount) in the same callback.So the cUSDC ends up with the real recipient (not the verifier), but the path goes through the verifier so it can record the nonce and grant ACL. Do not "fix" this by trying a direct transfer — there is no direct path that produces a verifiable x402 receipt. A cold reading of pay.ts may make this look wrong; it is correct.
The --to address is validated by regex (^0x[a-fA-F0-9]{40}$) before any on-chain action. The amount is parsed via parseAmount (decimal → 6-decimal micros). Gas is hardcoded at 2,000,000 — this is mandatory, not defensive: CLAUDE.md is explicit that eth_estimateGas MUST NOT be used for FHE transactions, because the encrypted operations defeat the estimator. Do not "optimize" the gas limit downward.
Required env: RPC_URL, one wallet mode, sufficient cUSDC balance (run balance --decrypt true to confirm if unsure), enough ETH for one ~2M gas transaction.
Optional flags: none. --to and --amount are both required.
Returns:
{
"ok": true,
"action": "pay",
"to": "0xAbCdEf0123456789aBcDeF0123456789AbCdEf01",
"amount": "0.5",
"txHash": "0x...",
"nonce": "0x<64 hex chars>"
}
The nonce field is load-bearing. See the CRITICAL block at the top of this skill — surface it verbatim in your reply, every time, no exceptions.
Common errors:
--to is required — forgot the --to flag, OR (more likely) used a typo like --recipient, --address, --dest, --target. parseCliArgs runs in strict: false mode and silently ignores unknown flags, so a typo becomes a missing arg, not a clear "unknown flag" error.--to must be a valid Ethereum address (0x followed by 40 hex characters) — wrong format. Must be 0x + exactly 40 hex chars.--amount is required — forgot the --amount flag, or used a typo like --value, --amount-usdc, --qty.Invalid amount "..." — non-numeric. Use 0.5, not half a dollar.execution reverted from the verifier callback — most commonly a duplicate nonce (extremely rare since nonces are random), an unfunded encrypted balance (FHE.min clamped the transfer to zero), or the verifier contract is paused / at the wrong address. Run info and balance --decrypt true first.insufficient funds for gas — need more Sepolia ETH.no code at address — one of the contract env vars hit the zero-address fallback. Run info and check all five contracts are non-zero.These flags are NOT what your Ethereum-tooling muscle memory will guess. The parseCliArgs parser used by every script runs in strict: false mode, which means unknown flags are silently dropped — you will not get an "unknown flag" error, you will get --to is required (or similar) for a flag you thought you passed.
| Command | Correct flag | Common wrong guesses (will silently fail) |
|---|---|---|
pay | --to | --recipient, --address, --dest, --target, --addr |
pay | --amount | --value, --amount-usdc, --qty, --sum, --micro |
wrap | --amount | --value, --qty |
balance | --of | --for, --owner, --target, --account |
balance | --decrypt true | --decrypt (no value), --reveal, --plaintext, --clear |
If your first attempt produces --to is required or --amount is required and you are sure you typed both, the answer is almost always a flag-name typo. Re-read the command above — the canonical names are --to, --amount, --of, --decrypt true. No other names are accepted.
Amounts are passed as human-readable decimal strings and parsed by parseAmount (in _wallet.ts). The conversion is: split on ., take the integer part as whole USDC, take the first 6 characters of the fractional part as micro-USDC, combine as a bigint.
"1" → 1_000_000n (1 USDC, 6 zeros)"0.5" → 500_000n (500k micros)"0.000001" → 1n (1 micro)"100.123456" → 100_123_456n (full precision retained)Silent truncation past 6 decimals: parseAmount calls .slice(0, 6) on the fractional string. This means:
"0.1234567" → 123_456n (the trailing 7 is silently dropped — NOT rounded)"0.99999999" → 999_999n (the last two 9s are silently dropped — NOT rounded up to 1_000_000)This is fine in practice for USDC (six decimals is the canonical precision), but if a user types an amount with seven or more decimal digits, you should:
Recommendation: always pass amounts with at most 6 decimal places to avoid surprise truncation. If the user gives you "0.1234567", surface "I will round to 0.123456 (6-decimal cUSDC precision) — confirm?" before running pay or wrap.
User: "Send 0.5 cUSDC to 0xAbCdEf0123456789aBcDeF0123456789AbCdEf01."
Run:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/pay.ts --to 0xAbCdEf0123456789aBcDeF0123456789AbCdEf01 --amount 0.5
Parse the JSON. If ok: true and action: "pay", reply:
Sent 0.5 cUSDC to
0xAbCdEf0123456789aBcDeF0123456789AbCdEf01via the X402PaymentVerifier relay. Tx:0x<txHash>.Save this nonce — it is the only way to submit feedback for this payment via
give-feedback --nonce, and it cannot be recovered from the chain:nonce: 0x<full 64-hex value, no truncation>
Both txHash AND nonce are surfaced verbatim. The nonce is in a code block, full hex, no … truncation. The user can copy-paste both into a notebook.
User: "I have 10 USDC. Send 5 to 0xBob... encrypted."
Step 1, run wrap:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/wrap.ts --amount 10
Parse. If ok: true and action: "wrap", reply with the wrap txHash. Then step 2:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/pay.ts --to 0xBob... --amount 5
Parse. Surface both the wrap txHash AND the pay txHash AND (per the CRITICAL block) the pay nonce. The user now has 5 cUSDC remaining wrapped in their wallet (10 wrapped − 5 sent) and can run balance --decrypt true to confirm.
User: "Pay 100 cUSDC to 0xAlice...."
Step 1, sanity-check:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/balance.ts --decrypt true
If the response shows decryptedBalance < 100, do NOT proceed with pay. Reply: "Your decrypted cUSDC balance is <X>, less than the requested 100. You need to wrap an additional <100 - X> USDC first via wrap --amount <100 - X>, or send a smaller amount." Wait for the user to decide.
If decryptedBalance >= 100 (or --decrypt is unavailable in stub mode but hasEncryptedBalance: true and the user confirms they have enough), proceed with pay --to 0xAlice... --amount 100. Surface txHash and nonce per Example 1.
If delegationError: "Decrypt requires @zama-fhe/sdk..." blocks the decrypt, fall back to telling the user "I cannot read your encrypted balance in stub mode. Confirm you have at least 100 cUSDC and I will proceed."
| Error text (regex / substring) | Cause | Remediation |
|---|---|---|
--to is required | Forgot --to, OR typo (--recipient / --address / --dest / --target) | Use exactly --to — parseCliArgs silently drops unknown flags |
--amount is required | Forgot --amount, OR typo (--value / --qty / --amount-usdc) | Use exactly --amount |
--to must be a valid Ethereum address | Bad format | Must be 0x + exactly 40 hex chars |
--of must be a valid Ethereum address | Bad format on balance --of | Same — 0x + 40 hex |
Invalid amount "..." | Non-numeric ("five", "0.5e2", "$0.50") | Use a plain decimal: 0.5, 100, 0.000001 |
Insufficient confidential balance / execution reverted on pay | Not enough cUSDC, OR FHE clamped to zero | Run wrap --amount <X> first; verify with balance --decrypt true |
execution reverted from RPC with no decoded reason on pay | Likely revert inside X402PaymentVerifier.onConfidentialTransferReceived — duplicate nonce, paused contract, or wrong env vars | Check info output (all 5 contract addresses non-zero), confirm the verifier is not paused, retry only after fixing the root cause |
no code at address / contract calls returning empty data | One of the contract env vars hit the zero-address fallback | Run info and verify EVERY contract address in contracts.{cUSDC,verifier,identity,reputation,escrow} is non-zero. Set the missing env var or fix RPC_URL to a recognized chain |
Decrypt requires @zama-fhe/sdk (not available in stub mode) | Stub mode SDK on balance --decrypt true | Install @zama-fhe/sdk. See ../../references/wallet-setup.md for live SDK setup |
delegationError: "No delegation — ask the owner..." | balance --of against an address that has not granted view | Ask the target to run grant-view --delegate <yourAddress> (sibling skill fhe-delegation) |
delegationError: "Delegation not yet propagated" | Just-granted delegation, propagation lag | Wait 1-2 minutes and retry balance --of |
Missing USER_PRIVATE_KEY / Missing DFNS_* | Wallet env vars not set | See ../../references/wallet-setup.md |
could not detect network | Bad RPC_URL, rate-limited, or endpoint down | Check the URL and quota |
insufficient funds for gas | Wallet has no Sepolia ETH | Fund the address shown by info |
| Bash exit ≠ 0 with stack trace, NOT a JSON line | Script crashed before returning JSON — usually _wallet.ts init error | Check env vars, verify RPC_URL is reachable, confirm pnpm install was run from repo root |
This skill covers ONLY info, balance, wrap, and pay. Do not use it for:
unwrap / finalize-unwrap (redeeming cUSDC back to USDC) — see fhe-payment-unwrapcreate-job / fund-job / submit / complete-job (encrypted escrow lifecycle) — see fhe-escrowregister-agent / identity / metadata (ERC-8004 NFT identity) — see fhe-agent-identitygrant-view / revoke-view / view-as (delegation setup, not delegated reads) — see fhe-delegationresearch-and-visualize / review-and-rate / HTTP-402 paid-request orchestration — see x402-demo-orchestratorsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub zama-ai/confidential-agentic-payment-stack --plugin x402fhe