From x402fhe
ERC-8183 encrypted job escrow on x402fhe — create a job (client + provider + evaluator + expiry + description), attach an encrypted cUSDC budget and fund it, and later approve or reject the submitted deliverable through the AgenticCommerceProtocol contract. Use when the user asks to create a job, open an escrow, lock a budget, attach funds to a job, escrow cUSDC for an evaluator, hire a provider, approve or reject a delivered job, release payout, refund a client, or otherwise run any step of the ERC-8183 job lifecycle. Do NOT use for direct token transfers, wrap, balance checks, redeeming cUSDC, agent identity, reputation feedback, delegated balance reads, or HTTP-402 orchestration — those are handled by sibling skills (fhe-payment-basics, fhe-payment-unwrap, fhe-agent-identity, fhe-delegation, x402-demo-orchestrators).
How this skill is triggered — by the user, by Claude, or both
Slash command
/x402fhe:fhe-escrowThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Three OpenClaw commands for the ERC-8183 encrypted job escrow on `AgenticCommerceProtocol`: `create-job`, `fund-job`, and `complete-job`. Budgets are euint64 (FHE-encrypted) — only the client, the escrow, and the payment token ever see the ciphertext handle. Each command lives in `packages/openclaw-skill/scripts/` and is invoked via pnpm + tsx from anywhere in the monorepo. Each emits a single ...
Three OpenClaw commands for the ERC-8183 encrypted job escrow on AgenticCommerceProtocol: create-job, fund-job, and complete-job. Budgets are euint64 (FHE-encrypted) — only the client, the escrow, and the payment token ever see the ciphertext handle. Each command 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.
fund-job.ts encrypts the budget for the ESCROW contract, NOT for cUSDC. This is counter-intuitive and it is the single most common cold-agent mistake on this skill.
The exact call inside scripts/fund-job.ts (lines 22-26):
const encrypted = await session.encrypt(
rawAmount,
escrowAddress, // contractAddress — where FHE.fromExternal runs
clientAddress, // userAddress — msg.sender when calling setBudget
);
The Zama fhEVM protocol rule: session.encrypt(amount, contractAddress, userAddress) binds the ZK proof to (contractAddress, userAddress). The proof is verified by the contract whose function accepts the ciphertext via FHE.fromExternal(encAmount, inputProof).
Trace it through AgenticCommerceProtocol.sol:
fund-job.ts calls escrow.setBudget(jobId, encHandle, inputProof, '0x').setBudget (line 222) runs euint64 amount = FHE.fromExternal(encAmount, inputProof); — this is where proof verification happens, inside the escrow contract, with msg.sender == client.escrow.fund(jobId, '0x'), does the escrow call paymentToken.confidentialTransferFrom(msg.sender, address(this), requestedAmount) — and at that point it passes the already-verified euint64 handle, NOT the original externalEuint64 + inputProof. cUSDC never runs FHE.fromExternal on this handle.So the proof must be bound to (escrow, client). If you encrypt for (cUSDC, client), FHE.fromExternal inside setBudget rejects the proof and reverts with an opaque "InvalidCiphertext" or "execution reverted" error. See ../../references/fhe-context-cheatsheet.md for the full rule table.
Mock FHE in Hardhat tests accepts any contractAddress. Only live Sepolia verification catches a contractAddress mismatch. Do not assume a green unit-test suite means the encryption context is correct.
| Rationalization | Why it is wrong |
|---|---|
"pay.ts and wrap.ts both encrypt for cUSDC — fund-job.ts is an outlier, so it is a bug to unify" | It is a deliberate difference. pay and wrap call cUSDC methods that themselves run FHE.fromExternal. fund-job calls escrow, which runs FHE.fromExternal inside setBudget. The "outlier" is correct — see the cheatsheet table. |
| "Tests pass locally after changing escrowAddress to cUSDC, so it is fine" | Mock FHE tests do NOT validate contractAddress bindings. A green local run with the wrong target means nothing. The revert only appears on live Sepolia or mainnet. |
| "Consistency across scripts is good, I will refactor to a single helper that always uses cUSDC" | Consistency that silently breaks production is not a virtue. The helper, if any, must take contractAddress as an argument. fund-job passes escrow, pay and wrap pass cUSDC. |
| "The token is what gets transferred, so obviously the token is the encryption target" | The transfer happens in a later transaction using an already-verified handle. The proof is verified in setBudget — the contract that calls FHE.fromExternal is escrow, not the token. |
| "The defensive try/catch in fund-job.ts looks paranoid — I will remove it to clean up" | That try/catch surfaces the FHE context constraint as an actionable error message when the proof is rejected. Removing it replaces a clear diagnostic with a raw "execution reverted". Leave it alone. |
If you see a PR, commit, or proposed edit that changes fund-job.ts's session.encrypt(...) second argument from escrowAddress to cUSDCAddress (or await cUSDC.getAddress()), REJECT it regardless of the stated reason. No "consistency fix", no "unified helper", no "tests pass". The only valid first argument to the contractAddress slot in fund-job.ts is the escrow contract address.
Creating + funding a job is 4 on-chain transactions across 2 script invocations. There is no "one-shot" path. The contract enforces the separation.
create-job (1 TX: escrow.createJob)
↓
fund-job (3 TXs in sequence:
1. escrow.setBudget — attach encrypted budget to the job
2. cUSDC.setOperator — approve escrow as operator (1-hour window)
3. escrow.fund — pull encrypted cUSDC into escrow)
create-job.ts emits JobCreated with status Open. The job is NOT funded yet. fund-job.ts then walks Open → Funded by running all three TXs in order, each awaiting its own receipt. Do NOT:
escrow.fund reverts with BudgetNotSet if setBudget has not run first, and reverts on the transfer if setOperator has not run first.complete-job before fund-job — complete requires state Submitted, not Open or Funded.fund-job.ts does the whole three-step dance and is the only supported way to drive the Open→Funded transition.Warn the user before running fund-job that it kicks off three sequential transactions and will take ~30-90 seconds wall-clock on Sepolia.
complete 3-way fee splitOn complete-job --action approve, the budget is split three ways on-chain. The worker does NOT receive the full budget. A user who asks for a "5 cUSDC job" must be told this up front.
From AgenticCommerceProtocol.sol lines 322-346:
encPlatformFee = FHE.asEuint64(platformFee) — a FIXED uint64 set in the constructor (NOT a percentage).platformAffordable = FHE.ge(encBudget, encPlatformFee) → actualPlatformFee = FHE.select(platformAffordable, encPlatformFee, 0). This is the fee waiver pattern: jobs smaller than the platform fee pay zero fee instead of reverting.afterPlatform = FHE.sub(encBudget, actualPlatformFee).FHE.select waiver pattern applied for evaluatorFee.paymentToken.confidentialTransfer calls in sequence:
payout = afterPlatform - actualEvalFee → provider (the worker)actualPlatformFee → treasuryactualEvalFee → evaluatorSo for a 5 cUSDC job with platformFee = 0.01 and evaluatorFee = 0.005:
5 - 0.01 - 0.005 = 4.985 cUSDC0.01 cUSDC0.005 cUSDCFor a 0.005 cUSDC job with the same fees:
Before reporting expected payouts to the user, read platformFee and evaluatorFee from the contract (e.g., via a one-off cast/ethers call on the escrow address from the info output — they are public state vars). Never assume zero fees and never report the raw budget as the worker's take-home.
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. Bash exit code is unreliable — a script that returns {"ok": false, ...} still exits 0.create-job, fund-job, and complete-job all submit signed transactions. A blind retry on fund-job in particular can double-create setOperator approvals or, worse, attempt a second fund on an already-funded job and revert with InvalidStatus while burning gas. If the first call returned an error, surface it to the user and wait for instructions.fund-job.ts runs three TXs and returns only the final fund tx hash — if it fails on step 2 (setOperator) or step 3 (fund), the step-1 setBudget has already landed on-chain and the job now has budgetSet = true. Surface whatever transaction hash is in the error message and tell the user the job is in a partial state.BudgetNotSet, InvalidStatus, Unauthorized, HookNotWhitelisted, and InvalidExpiry are the primary diagnostic signal.fund-job.ts runs three sequential transactions and awaits the receipt on the third. On Sepolia, expect 30-90 seconds of wall-clock time in a single invocation. Tell the user before running: "This will take ~30-90 seconds and will submit three transactions (setBudget, setOperator, fund)." Do not interrupt and re-run — see the error handling rules.
The script is named fund-job.ts but it runs setBudget + setOperator + fund under the hood. Each step can fail independently. Know the failure modes:
| Step | Fails with | Partial state after failure |
|---|---|---|
setBudget | execution reverted — most likely the encrypted budget proof was not accepted. ... (surfaced verbatim from the defensive wrapper in lines 37-45) | Job still Open, no budget set — safe to re-run after fixing the root cause |
setOperator | execution reverted on cUSDC | Job has budgetSet = true but no operator approval — fund in the next step will revert on the transfer; re-running the whole script resets the operator and proceeds |
fund | BudgetNotSet, OperatorNotApproved, or a transfer revert | Job still Open with budget set; re-running the script re-approves the operator and retries the transfer |
Only the FINAL fund tx hash is returned in the txHash field. The setBudget and setOperator hashes are discarded — if a caller needs them for receipts, they must modify the script. The setOperator approval is hardcoded to a 1-hour expiry window (line 48: Math.floor(Date.now() / 1000) + 3600) and is not configurable via a flag.
FHE.min clamping silent underfundIf the client does not have enough cUSDC, the fund step silently funds the job with whatever balance is available — it does NOT revert.
The root cause is at AgenticCommerceProtocol.sol lines 260-265: fund captures the ACTUAL transferred amount from confidentialTransferFrom and stores it as the new encBudget. ERC-7984's confidentialTransferFrom uses FHE.min(balance, amount) clamping internally, so a client requesting to fund 5 cUSDC with only 2 cUSDC in their wallet will end up with a job funded at 2 cUSDC — and neither the JSON response nor any event will make this visible to the caller. The job simply funds at the clamped value.
Before running fund-job, always verify the client has sufficient cUSDC. Run balance --decrypt true from the sibling fhe-payment-basics skill (or wrap --amount <X> first if they only hold plain USDC). If you cannot decrypt the balance (stub mode), ask the user to confirm they have enough — do not proceed silently.
You need one wallet mode plus an RPC URL 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 — including the AgenticCommerceProtocol escrow address. On other networks you MUST set ESCROW_ADDRESS explicitly or _wallet.ts will fall back to the zero address and every call will fail with "no code at address".
Always run info from the sibling fhe-payment-basics skill first to verify contracts.escrow is non-zero. The same zero-address fallback footgun documented there applies here: a missing ESCROW_ADDRESS silently constructs a Contract proxy pointing at 0x000...000 and only fails when a real transaction hits the network.
All commands in this skill run from anywhere in the monorepo via the workspace filter:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/<name>.ts [--flag value]
Do NOT use node scripts/..., npx tsx, or ts-node — the _wallet.ts singleton resolves imports via pnpm workspace paths and fails otherwise. See fhe-payment-basics for the full reasoning.
The escrow enum is JobStatus { Open, Funded, Submitted, Completed, Rejected, Expired }. Transitions:
| From | Transition | To | Who | OpenClaw command |
|---|---|---|---|---|
| — | createJob | Open | client | create-job |
Open | setBudget + fund | Funded | client | fund-job (runs both) |
Funded | submit | Submitted | provider | NO OpenClaw script — provider must call escrow.submit(jobId, deliverable, '0x') directly |
Submitted | complete | Completed | evaluator | complete-job --action approve |
Open / Funded | reject | Rejected | client | complete-job --action reject |
Funded / Submitted | reject | Rejected | evaluator | complete-job --action reject |
Funded / Submitted | claimRefund (after block.timestamp >= expiredAt) | Expired | client | NO OpenClaw script — call escrow.claimRefund(jobId) directly |
Gap to know about: there is no OpenClaw script for submit or claimRefund. If the user asks you to "submit a deliverable for job X" or "claim an expired job refund", you must either tell them to use an ethers/cast call against the escrow contract directly, or ask an engineer to add the script. Do NOT try to misuse complete-job or any other command — they will revert with InvalidStatus or Unauthorized.
When to use: the user asks to create a new job, open a new escrow, start a new work order for a provider/evaluator pair, or set up a hiring arrangement with a deadline and description. This is always step 1. It does NOT attach a budget or move any cUSDC — that comes in step 2 (fund-job).
Invocation:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/create-job.ts \
--provider 0xAbCd...0001 \
--evaluator 0xAbCd...0002 \
--expiry 24 \
--description "Research and draft executive summary"
Required flags:
--provider — the worker's Ethereum address (0x + 40 hex chars). Validated against /^0x[a-fA-F0-9]{40}$/.--evaluator — the address that will approve or reject the deliverable. Same regex. Cannot equal msg.sender (contract reverts with SelfDealing).--expiry — expiry in HOURS from now, as a number string. Internally: expiredAt = Math.floor(Date.now()/1000) + parseInt(expiry) * 3600. Must be strictly in the future (contract reverts with InvalidExpiry if expiredAt <= block.timestamp).--description — free-form human-readable string. Stored on-chain.Optional flags:
--hook — an IACPHook contract address for lifecycle callbacks. Defaults to ethers.ZeroAddress (no hook). If set, it must be a valid Ethereum address AND whitelisted via escrow.whitelistedHooks[hook] == true, or the contract reverts with HookNotWhitelisted.Address validation is strict. The placeholder strings "0xAlice" / "0xBob" that user queries sometimes contain are NOT valid — create-job.ts rejects them immediately with --provider must be a valid Ethereum address (0x followed by 40 hex characters). If the user passes placeholder names, ask them for real 40-hex addresses before running.
Required env: RPC_URL, one wallet mode, enough ETH for one ~300k-gas transaction.
Returns:
{
"ok": true,
"action": "create_job",
"jobId": "1",
"txHash": "0x..."
}
The jobId is parsed from the JobCreated event in the receipt logs; if parsing fails it falls back to "1" (so always cross-check against the next _nextJobId if you care about the exact ID). Surface both jobId and txHash to the user verbatim — the jobId is the only input to fund-job and complete-job.
Common errors:
--provider must be a valid Ethereum address / --evaluator must be a valid Ethereum address — placeholder like 0xAlice or typo. Use 0x + exactly 40 hex chars.--expiry is required (hours as a number) — forgot the flag, or passed a non-numeric value.execution reverted with InvalidEvaluator — evaluator is address(0).execution reverted with SelfDealing — evaluator equals the caller.execution reverted with InvalidExpiry — the computed expiredAt is in the past (pass a positive number of hours).execution reverted with HookNotWhitelisted — the --hook address is not on whitelistedHooks. Use the zero address (omit --hook) or ask the owner to whitelist it.When to use: the user has just created a job (or already has an Open jobId) and now wants to attach an encrypted budget and lock cUSDC into the escrow. ALSO use this when the user says "fund the job" or "lock the budget" — the script handles both setBudget and the actual fund step internally. Only works on jobs in the Open state (contract reverts with InvalidStatus otherwise).
WARNING: this command runs THREE sequential on-chain transactions (setBudget → setOperator → fund) and takes 30-90 seconds. Only the final fund tx hash is returned. See "IMPORTANT — fund-job is silently overloaded" above.
Invocation:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/fund-job.ts --jobId 1 --amount 5
Required flags:
--jobId — the numeric job ID returned by create-job.--amount — human-readable decimal cUSDC ("5", "0.5", "100.123456"). Parsed by parseAmount into 6-decimal micro-USDC internally. Truncates silently past 6 decimals — see fhe-payment-basics "Amount handling" for the exact rule.Required env: RPC_URL, one wallet mode, sufficient cUSDC balance (see the FHE.min clamping warning above), and enough ETH for three transactions (~4.5M gas total).
Before running: confirm the client has >= amount cUSDC by running balance --decrypt true (from fhe-payment-basics). Confirm the job is in state Open via escrow.getJobStatus(jobId) if you are uncertain. Warn the user about the 30-90 second duration and the three transactions.
Returns:
{
"ok": true,
"action": "fund_job",
"jobId": "1",
"txHash": "0x..."
}
The txHash is the FINAL fund transaction hash only. The setBudget and setOperator hashes are discarded — if you need them for receipts, modify the script. On success, the job state transitions Open → Funded.
Common errors:
--jobId is required / --amount is required — missing flag.setBudget reverted — most likely the encrypted budget proof was not accepted. Verify that session.encrypt() was called with escrowAddress (0x...) as the contractAddress argument; encrypting for cUSDC or any other contract will cause FHE.fromExternal to reject the proof. Original error: ... — this is the defensive wrapper in fund-job.ts lines 37-45. See the FHE context CRITICAL block at the top of this file. Never "fix" by changing escrowAddress to cUSDCAddress. The fix is to check that the script was not edited — if the source file is unchanged, the error means something else (corrupted session, stub mode mismatch, wrong ESCROW_ADDRESS).execution reverted with BudgetNotSet — should not happen via this script since setBudget runs first, but if you see it, the step-1 tx silently reverted; re-run the whole script.execution reverted with InvalidStatus — the job is not in Open state (already funded, rejected, or expired). Check escrow.getJobStatus(jobId).execution reverted with Unauthorized — msg.sender != client. Only the client who called createJob can fund the job. Check the wallet address via info.fund in the trace — often either setOperator expired (1-hour window — rare in a 90-second script) or the client has insufficient cUSDC (FHE.min clamped to zero after balance check). Run balance --decrypt true to confirm and wrap --amount <X> to top up if needed.When to use: the user is the evaluator (or the client on an unsubmitted job) and wants to either approve a submitted deliverable — triggering the 3-way payout — or reject it and refund the client. This is the ONLY script for evaluator actions. The underlying contract functions are complete (approve path) and reject (reject path), switched by the --action flag.
Invocation (approve — release payout via 3-way split):
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/complete-job.ts --jobId 1 --action approve
Invocation (reject — refund client, no payout):
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/complete-job.ts --jobId 1 --action reject --reason "quality"
Required flags:
--jobId — numeric job ID.--action — must be exactly approve or reject. Any other value fails validation before any tx.Optional flags:
--reason — human-readable reason string. Encoded via ethers.encodeBytes32String(reason.slice(0, 31)) → bytes32, so anything past 31 characters is silently truncated. Defaults to ethers.ZeroHash if omitted.State requirements (enforced by the contract):
approve requires status == Submitted AND msg.sender == evaluator. Reverts with InvalidStatus or Unauthorized otherwise.reject:
status is Open or Funded.status is Funded or Submitted.InvalidStatus or Unauthorized otherwise.Funded or Submitted (no refund path from Open, since no cUSDC was pulled yet).Required env: RPC_URL, one wallet mode, enough ETH for one ~1.5-2M-gas transaction.
Returns: (approve path)
{
"ok": true,
"action": "approve",
"jobId": "1",
"txHash": "0x..."
}
Returns: (reject path)
{
"ok": true,
"action": "reject",
"jobId": "1",
"txHash": "0x..."
}
The action field echoes the user's --action value verbatim. On approve, payouts have already settled on-chain in this single TX — the provider, treasury, and evaluator all receive their split (see CRITICAL block on the 3-way fee split).
Common errors:
--action is required (must be "approve" or "reject") — missing flag.--action must be "approve" or "reject" — passed something else (accept, yes, done).execution reverted with Unauthorized — caller is neither the evaluator (for approve) nor an authorized rejector (client or evaluator, depending on state).execution reverted with InvalidStatus — job is in the wrong state for the action. See the state machine above. Approve requires Submitted, which in turn requires the provider to have called escrow.submit(jobId, deliverable, '0x') directly (no OpenClaw script for submit).User: "Create an escrow job for worker 0xAbCd...0001, evaluator 0xAbCd...0002, budget 5 cUSDC, expiring in 24 hours. Then fund it."
Pre-flight: First, verify the env and confirm the client has enough cUSDC. Both pre-flight commands live in the sibling fhe-payment-basics skill — switch skill context and run info (to verify contracts.escrow is non-zero) followed by balance --decrypt true (to verify decryptedBalance >= 5). If the client has only plain USDC, run wrap --amount 5 first (also in fhe-payment-basics).
Step 1, create:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/create-job.ts \
--provider 0xAbCd...0001 \
--evaluator 0xAbCd...0002 \
--expiry 24 \
--description "Research and draft executive summary"
Parse. If ok: true and action: "create_job", capture the jobId (say "7") and txHash.
Step 2, fund (warn the user this runs THREE transactions and takes ~30-90 seconds):
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/fund-job.ts --jobId 7 --amount 5
Parse. If ok: true and action: "fund_job", surface the fund txHash.
Reply to user (remembering to mention the 3-way split so the worker expectation is correct):
Created job 7 (tx
0xcreate...) and funded it with 5 cUSDC (tx0xfund..., which also ran setBudget and setOperator under the hood). The job is now in stateFunded. Next: the provider0xAbCd...0001should submit a deliverable viaescrow.submit(7, <deliverableHash>, '0x')(there is no OpenClaw script for submit yet). Once submitted, the evaluator0xAbCd...0002can approve withcomplete-job --jobId 7 --action approveor reject withcomplete-job --jobId 7 --action reject --reason "<reason>".Heads up on payouts: on approve, the 5 cUSDC will split three ways — provider gets
5 - platformFee - evaluatorFee, treasury gets the platform fee, evaluator gets the evaluator fee. The fees are fixeduint64amounts set on the escrow contract (not percentages). ReadplatformFeeandevaluatorFeefrom the escrow contract if you need the exact take-home figures before committing.
User: "Cancel job 7 and refund me."
Check first that the user is the client on job 7 and the status is Funded (you may need to read from the contract or getJob(7)). Then:
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/complete-job.ts --jobId 7 --action reject --reason "cancelled by client"
Parse. If ok: true and action: "reject", reply: "Job 7 rejected in tx 0x.... The escrowed cUSDC has been refunded to your wallet via confidentialTransfer. You can verify with balance --decrypt true." Note that the --reason is truncated to 31 chars and encoded as bytes32.
User: "I reviewed the deliverable for job 7 and it looks good — approve and release payment."
First confirm the user is the evaluator on job 7 and the status is Submitted (if the provider has not yet called submit, the approve will revert with InvalidStatus and you must tell the user to ask the provider to submit first — there is no OpenClaw script for it).
pnpm --filter @x402fhe/openclaw-skill exec tsx scripts/complete-job.ts --jobId 7 --action approve --reason "approved"
Parse. If ok: true and action: "approve", reply with the txHash and remind the user that the budget just split 3 ways: provider → payout, treasury → platform fee, evaluator (you) → evaluator fee. The FHE.select guard waives each fee if the remaining amount cannot cover it, so jobs smaller than the combined fees produce a zero payout to the provider.
| Error text (regex / substring) | Cause | Remediation |
|---|---|---|
setBudget reverted — most likely the encrypted budget proof was not accepted | session.encrypt was called with the wrong contractAddress (usually cUSDC instead of escrow) | The script already uses escrow correctly — if you see this error and the source is unchanged, suspect wrong ESCROW_ADDRESS env var or a session stub mismatch. See CRITICAL block and ../../references/fhe-context-cheatsheet.md. Never "fix" by editing fund-job.ts to encrypt for cUSDC. |
BudgetNotSet | escrow.fund called before setBudget — should not happen via fund-job.ts but can appear on a retry | Re-run fund-job from scratch; do NOT call escrow.fund directly |
InvalidStatus on fund | Job is not in Open state (already funded, rejected, or expired) | Check escrow.getJobStatus(jobId); if already Funded, nothing to do; if Rejected or Expired, the refund has already happened |
InvalidStatus on complete --action approve | Job is not in Submitted state — usually the provider has not called submit yet | Ask the provider to run escrow.submit(jobId, deliverableHash, '0x') directly (no OpenClaw script exists); then retry approve |
InvalidStatus on complete --action reject | State does not match the allowed rejector | Client may reject Open or Funded; evaluator may reject Funded or Submitted. Check current state. |
Unauthorized | Wrong wallet — only client can fund or client-reject, only evaluator can approve or evaluator-reject | Run info to confirm the active wallet address; switch wallets if needed |
InvalidExpiry on create-job | expiredAt <= block.timestamp — expiry hours was 0 or negative | Pass a positive integer number of hours in --expiry |
InvalidEvaluator / SelfDealing on create-job | Evaluator is zero address or equals the caller | Pass a non-zero evaluator different from the calling wallet |
HookNotWhitelisted on create-job | --hook address is not on escrow.whitelistedHooks | Omit --hook (defaults to zero address = no hook), or ask the escrow owner to whitelist the hook contract |
--provider must be a valid Ethereum address / --evaluator must be... | Placeholder string like 0xAlice, or bad format | Use 0x + exactly 40 hex chars — escrow addresses are real Ethereum addresses |
OperatorNotApproved or bare transfer revert inside fund step | cUSDC.setOperator expired (1-hour window) between step 2 and step 3 — rare in a single script run | Re-run the whole fund-job script; the operator approval is re-submitted at the start of step 2 |
| Worker received less than the full budget on approve | 3-way fee split (platform fee + evaluator fee, both fixed uint64) | Read platformFee and evaluatorFee from the escrow contract and explain the split to the user — the worker's take-home is budget - platformFee - evaluatorFee, or the budget itself if either fee is waived via FHE.select for small jobs |
| Job funded at a smaller amount than requested | Silent FHE.min(balance, requested) clamping in confidentialTransferFrom because the client balance was too low | Always run balance --decrypt true before fund-job and wrap more if needed — the contract silently under-funds rather than reverts |
no code at address / empty data on any escrow call | ESCROW_ADDRESS env var missing and zero-address fallback kicked in | Run info (from fhe-payment-basics) and confirm contracts.escrow is non-zero; set ESCROW_ADDRESS explicitly if on a non-Sepolia chain |
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 URL and quota |
insufficient funds for gas | Wallet has no Sepolia ETH | Fund the wallet 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; run info first to validate wiring |
This skill covers ONLY create-job, fund-job, and complete-job. Do not use it for:
info / balance / wrap / pay — see fhe-payment-basicsunwrap / finalize-unwrap (redeeming cUSDC back to USDC) — see fhe-payment-unwrapregister-agent / identity / metadata (ERC-8004 NFT identity) — see fhe-agent-identitygrant-view / revoke-view / view-as (delegation setup for reads) — see fhe-delegationresearch-and-visualize / review-and-rate / HTTP-402 paid-request orchestration — see x402-demo-orchestratorsnpx claudepluginhub zama-ai/confidential-agentic-payment-stack --plugin x402fheProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.