Install
openclaw skills install ceaser-sendFully automated private ETH transfer via Ceaser Protocol on Base L2 using the ceaser-mcp MCP tools. This skill uses the ceaser-mcp npm package for ALL operat...
openclaw skills install ceaser-sendYou are a skill that executes a complete private ETH transfer on Base L2 (chain ID 8453) using the Ceaser privacy protocol. You orchestrate the full flow: generate an ephemeral hot wallet with a BIP-39 mnemonic (shown to the user once for recovery), wait for user funding, Shield (deposit) into the privacy pool with automatic TX signing, extract the on-chain leafIndex, update the local note, Unshield (withdraw) to the recipient address, and refund remaining ETH to the user.
Network: Base L2 (chain ID 8453)
Contract: 0x278652aA8383cBa29b68165926d0534e52BcD368
Facilitator: https://ceaser.org
Protocol Fee: 0.25% (25 bps) per operation
Valid Denominations: 0.001, 0.01, 0.1, 1, 10, 100 ETH
Proof System: Noir circuits compiled to UltraHonk proofs (no trusted setup)
This skill uses the ceaser-mcp npm package for shield, unshield, and note management operations. All ceaser tool calls use CLI subcommands:
npx -y ceaser-mcp <subcommand> [args]
Alternatively, if mcporter is installed with the ceaser MCP server configured (see {baseDir}/mcporter.json), you may use mcporter call ceaser.TOOL_NAME as an equivalent method. CLI is the primary and recommended approach.
Exactly ONE manual step is required: the user must send ETH to a generated hot wallet address. All other steps (proof generation, TX signing, broadcasting, leafIndex extraction, unshield, refund) are fully automated.
Auto-signing mode creates observable on-chain links that reduce privacy compared to manual signing (the /ceaser skill). Specifically:
shieldETH() on the contract. H is now linked to the shield deposit.Recommendation: For maximum privacy, use the /ceaser skill (manual signing via MetaMask). The user signs the shield transaction directly from their wallet, with no intermediate hot wallet, no funding link, and no refund link. Use /ceaser-send (this skill) only when the user explicitly requests automated signing or cannot interact with a wallet UI.
Before executing this skill, verify:
node {baseDir}/helpers/wallet-ops.js --help)Execute ALL of these checks BEFORE starting the flow. Abort if any check fails.
Run:
curl -s "https://ceaser.org/status" | jq .
Verify:
circuitBreaker.tripped is falseindexer.synced is trueIf the facilitator is down or circuit breaker is tripped, inform the user and abort.
Run:
curl -s "https://ceaser.org/api/ceaser/denominations" | jq .
Verify:
If the amount is not a valid denomination, show the user the valid options and ask them to choose.
Run:
curl -s "https://ceaser.org/api/ceaser/fees/AMOUNT_WEI" | jq .
Replace AMOUNT_WEI with the amount in wei (e.g., 1000000000000000 for 0.001 ETH).
Present to the user:
Store internally: amountWei and feeWei from the response (needed for funding calculation in Wallet Generation Phase).
Ask the user to confirm they want to proceed.
Validate the recipient address format: must match /^0x[0-9a-fA-F]{40}$/.
If invalid, inform the user and ask for a correct Ethereum address.
Run:
npx -y ceaser-mcp notes
Check for existing unspent notes:
Run:
node {baseDir}/helpers/wallet-ops.js --help
Verify: valid JSON output listing available commands (generate, balance, sign-and-send, refund).
If this check fails: inform the user and abort. Message: "Helper script not available. Please run npm install in the skill directory."
Based on Pre-Flight Check 5:
Path A -- Use Existing Note: If the user wants to use an existing unspent note with a valid leafIndex, skip directly to the Unshield Phase.
Path B -- Update Existing Note: If a note has leafIndex=null and the user has the TX hash, skip to the TX Confirmation and leafIndex Extraction phase.
Path C -- Full Shield Flow: No suitable note exists. Execute the complete Wallet -> Fund -> Shield -> Auto-Sign -> Confirm -> Update -> Unshield -> Refund flow.
Only execute for Path C (Full Shield Flow).
Run:
node {baseDir}/helpers/wallet-ops.js generate
Store internally (in your working context):
mnemonic -- The 12-word BIP-39 recovery phrase.address -- The hot wallet address to show the user.IMPORTANT: Show the mnemonic to the user EXACTLY ONCE with a clear security warning:
RECOVERY MNEMONIC (save this securely):
word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12SAVE THIS MNEMONIC SECURELY. It controls the hot wallet funds. If this session breaks after funding, import these 12 words into MetaMask (or any BIP-39 wallet) to recover your ETH. Do NOT share this mnemonic. Anyone with these words can access the hot wallet.
After showing the mnemonic once, do NOT repeat it in summaries, follow-up responses, or any subsequent messages.
Use the fee data from Pre-Flight Check 3:
amountWei + feeWei from ceaser_get_fees responseamountWei + feeWei) + 500000000000000Convert total funding to ETH for display. Round UP to a human-friendly value if needed.
Before showing the funding instructions, ask the user:
"Where should I send any remaining ETH after the operation? Please provide your wallet address for the refund."
Store the refund address internally. If the user does not provide one, attempt to detect it from the incoming funding transaction later. If detection fails, ask again after the shield completes.
Note: If the session breaks after funding, the user can also recover hot wallet funds by importing the mnemonic into MetaMask.
Present to the user:
Hot Wallet Address:
HOT_WALLET_ADDRESSSend exactly:
FUNDING_AMOUNTETH (or slightly more)Network: Base Mainnet (Chain ID 8453)
Send EXACTLY the specified amount or slightly more. Sending less will cause the shield to fail. Any excess ETH will be automatically refunded after completion.
If you lose the mnemonic, you cannot recover funds from this wallet. The mnemonic will NOT be shown again. In case of session interruption, use the mnemonic to recover funds via MetaMask.
Only execute for Path C, after showing funding instructions.
Repeatedly execute:
node {baseDir}/helpers/wallet-ops.js balance --address HOT_WALLET_ADDRESS
balanceWei >= required funding amount (in wei)The user can say "cancel" or "abbrechen" at any time during the funding wait.
To refund on cancel:
CEASER_HOT_MNEMONIC="MNEMONIC_PHRASE" node {baseDir}/helpers/wallet-ops.js refund --recipient REFUND_ADDRESS --rpc https://mainnet.base.org
HOT_WALLET_ADDRESS..."If balance > 0 but less than required:
"Received: X ETH. Required: Y ETH. Please send an additional Z ETH."
Continue polling.
When balance >= required amount:
"Funding received. Proceeding with Shield operation."
If the user provided excess ETH:
"Received more ETH than required. Excess will be refunded after completion."
Run:
npx -y ceaser-mcp shield USER_AMOUNT
Replace USER_AMOUNT with the ETH denomination (e.g., 0.1).
note.id -- Internal ID for later referencenote.commitment -- bytes32 commitment hashnote.leafIndex -- Will be null (expected)unsignedTx.to -- Must be 0x278652aA8383cBa29b68165926d0534e52BcD368unsignedTx.data -- ABI-encoded calldataunsignedTx.value -- Amount + fee in weiunsignedTx.chainId -- Must be 8453backup -- Base64-encoded backup string (SECURITY CRITICAL)instructions -- Array of guidance messagesVerify after receiving the response:
unsignedTx.value from shield response matches (amountWei + feeWei) from ceaser_get_feesunsignedTx.chainId == 8453unsignedTx.to == 0x278652aA8383cBa29b68165926d0534e52BcD368SECURITY CRITICAL:
SAVE THIS BACKUP STRING SECURELY. It contains your private ZK secrets (secret + nullifier). Anyone with this string can withdraw your shielded ETH. Store it offline. Do not share it.
Remember these values for later steps (keep in your working context, do not output again):
note.idnote.commitmentbackup (needed for leafIndex update)pendingTxFile (path to the saved unsigned TX -- auto-detected by sign-and-send, no need to pass manually)If ceaser_shield_eth fails (proof error, facilitator down, etc.):
This replaces the previous manual signing phase. The agent signs and broadcasts the Shield TX automatically using the mnemonic-derived hot wallet.
The ceaser-mcp shield command automatically saves the unsigned transaction to ~/.ceaser-mcp/pending-tx.json. The sign-and-send helper auto-detects this file, so you do NOT need to pass the transaction data as a CLI argument.
Run:
CEASER_HOT_MNEMONIC="MNEMONIC_PHRASE" node {baseDir}/helpers/wallet-ops.js sign-and-send --rpc https://mainnet.base.org
The helper automatically reads the unsigned TX from ~/.ceaser-mcp/pending-tx.json and deletes the file after a successful send.
Where:
MNEMONIC_PHRASE: The 12-word mnemonic from Wallet Generation Phase (passed via environment variable, NEVER as CLI argument)Alternative: You can also specify the file explicitly or pass the JSON directly:
# Explicit file path
CEASER_HOT_MNEMONIC="..." node {baseDir}/helpers/wallet-ops.js sign-and-send --unsigned-tx-file ~/.ceaser-mcp/pending-tx.json --rpc https://mainnet.base.org
# Legacy: pass JSON directly (NOT recommended -- the data field is 4000-9000 chars)
CEASER_HOT_MNEMONIC="..." node {baseDir}/helpers/wallet-ops.js sign-and-send --unsigned-tx 'JSON_STRING' --rpc https://mainnet.base.org
IMPORTANT: Do NOT pass the unsignedTx JSON as a CLI argument unless absolutely necessary. The data field contains 4000-9000 characters of hex-encoded ZK proof, which can cause issues with shell argument handling. Always prefer the automatic file-based approach.
Security notes:
CEASER_HOT_MNEMONIC), NOT as a CLI argumentSet exec timeout: 60 seconds (covers gas estimation + signing + broadcasting + 1 block confirmation).
status == 1): Extract txHash, proceed to TX Confirmation phase.txHash if available and proceed to manual confirmation check.Using the txHash from the automatic sign-and-send operation, extract the leafIndex from the on-chain Shield event.
Note: The helper script already waits for 1 block confirmation. In most cases, the receipt is already available. The steps below serve as verification and leafIndex extraction.
Use Bash to query the Base Mainnet RPC:
curl -s -X POST https://mainnet.base.org \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getTransactionReceipt","params":["TX_HASH"],"id":1}' | jq '.result'
Replace TX_HASH with the actual hash.
status == "0x1": Success. Continue to Step 3.status == "0x0": TX failed on-chain. Inform the user: "Transaction reverted. Possible causes: insufficient funds, gas limit too low, or contract error." Attempt refund of remaining hot wallet balance.status == null or no result: TX still pending. Wait 10 seconds and retry. Maximum 5 retries. If still pending, inform the user and provide the TX hash for manual checking.Search the logs array for an entry matching:
address == 0x278652aa8383cba29b68165926d0534e52bcd368 (case-insensitive)topics[0] == 0x39b0d8da40fd574f8fb61ef14d4f466fb3bceb268547c27680755e9b08fd8677 (Shield event signature)If no matching log found: "No Shield event found in this transaction. This may not be a Shield transaction. Please verify the TX hash." Abort.
The Shield event structure:
topics[0]: Event signature hashtopics[1]: commitment (indexed, bytes32)topics[2]: assetId (indexed, uint256)data: ABI-encoded (uint32 leafIndex, uint256 timestamp)Extract leafIndex from data:
# data is hex string like 0x000000000000000000000000000000000000000000000000000000000000000600000000...
# First 32 bytes (64 hex chars after 0x) = leafIndex (uint32 zero-padded)
LEAF_INDEX_HEX=$(echo "DATA_FIELD" | cut -c3-66)
LEAF_INDEX=$(node -e "console.log(parseInt('0x$LEAF_INDEX_HEX', 16))")
Extract topics[1] from the log and compare against the stored note.commitment.
CRITICAL: If the commitment does not match, WARN the user: "The Shield event commitment does not match the generated note. This TX hash may belong to a different shield operation. Aborting leafIndex extraction to prevent data corruption."
The extracted leafIndex must be a non-negative integer. If it is NaN or negative, abort with an error.
If the TX contains multiple Shield events (unusual but possible in batch operations), use the commitment match from Step 5 to identify the correct event.
The shield tool stores the note with leafIndex=null. The unshield tool requires leafIndex != null. No MCP tool exists for updating the leafIndex directly. This workaround uses Bash to manipulate the backup string and notes.json.
ceaser_shield_eth saves note with leafIndex=null (TX not yet sent at proof time)ceaser_unshield requires leafIndex != null (needed for Merkle proof)ceaser_import_note rejects duplicates by commitment (including spent notes)echo "BACKUP_STRING" | base64 -d
This yields a JSON object with fields: s (secret), n (nullifier), a (amount), am (amountWei), c (commitment), i (leafIndex -- currently null), ai (assetId), as (assetSym), ad (contractAddress).
UPDATED_JSON=$(echo "BACKUP_STRING" | base64 -d | jq --argjson idx LEAF_INDEX '.i = $idx')
Replace LEAF_INDEX with the extracted decimal value. This sets the i field from null to the actual leafIndex.
UPDATED_BACKUP=$(echo "$UPDATED_JSON" | base64 -w 0)
CRITICAL: Only remove the specific entry matching the commitment. Do NOT modify other notes.
COMMITMENT="NOTE_COMMITMENT_VALUE"
jq --arg c "$COMMITMENT" '[.[] | select(.commitment != $c)]' ~/.ceaser-mcp/notes.json > ~/.ceaser-mcp/notes.json.tmp && mv ~/.ceaser-mcp/notes.json.tmp ~/.ceaser-mcp/notes.json && chmod 600 ~/.ceaser-mcp/notes.json
IMPORTANT: Always restore file permissions to 0600 after modification.
Run:
npx -y ceaser-mcp import "$UPDATED_BACKUP"
Run:
npx -y ceaser-mcp notes
Verify:
leafIndex is the correct value (NOT null, NOT 0 unless actually the first leaf)spent is falseNumber(null) returns 0 in JavaScript. ALWAYS set the leafIndex in the backup string BEFORE importing. Never import with i=null.chmod 600 ~/.ceaser-mcp/notes.json.Before calling unshield, verify:
npx -y ceaser-mcp notes)leafIndex != nullspent == falseInform the user: "Generating burn proof and submitting to facilitator. This may take 15-60 seconds depending on tree size and hardware."
Run:
npx -y ceaser-mcp unshield NOTE_ID RECIPIENT_ADDRESS
Replace NOTE_ID and RECIPIENT_ADDRESS with actual values.
success: truetxHash: Facilitator settlement TX hashrecipient: Target addressgrossAmount: Gross amount in weifeeWei: Fee in weinetAmount: Net amount in weinoteId: The used note ID"Note does not have a leaf index": leafIndex update failed. Check notes.json."Note has already been spent": Double-spend attempt. Note was already unshielded."Unshield proof generation failed": Proof generation error. May retry.The unshield is GASLESS -- the facilitator pays all gas costs.
Only execute for Path C, after successful Unshield. Refunds remaining ETH from the hot wallet to the user.
Run:
CEASER_HOT_MNEMONIC="MNEMONIC_PHRASE" node {baseDir}/helpers/wallet-ops.js refund --recipient REFUND_ADDRESS --rpc https://mainnet.base.org
Where:
MNEMONIC_PHRASE: The 12-word mnemonic (from Wallet Generation Phase)REFUND_ADDRESS: The user's refund address (from Step 3 of Wallet Generation Phase, or detected from funding TX)refunded: true: Show amount and TX hash to user. "Refunded X ETH to your address."refunded: false (balance too low for gas + L1 fee): Inform user: "Remaining balance is too small to cover gas + L1 data fee for a refund transfer. Import the mnemonic into MetaMask to recover manually."The refund is a best-effort operation. Refund failure does NOT affect the main operation (Shield + Unshield already completed successfully).
Present to the user:
netAmount from wei to ETH (human-readable)feeWei from wei to ETHhttps://basescan.org/tx/SHIELD_TX_HASHhttps://basescan.org/tx/UNSHIELD_TX_HASHPresent:
Present:
| Error | Cause | Data Loss? | Action |
|---|---|---|---|
| Hot wallet generation fails | Node.js crypto issue | None | Retry |
| Insufficient funding | User sent too little ETH | ETH in hot wallet | Inform user, wait for more ETH |
| Funding timeout (10 min) | User did not fund | None (no ETH sent) | Retry flow |
| Funding timeout with balance | User sent partial amount | ETH in hot wallet | Auto-refund, retry flow |
| User cancels funding | User choice | None or ETH in hot wallet | Auto-refund if balance > 0 |
| Shield proof fails after funding | Facilitator down, circuit error | ETH in hot wallet | Retry (2x), then auto-refund |
| Sign-and-send fails | Gas, nonce, RPC issue | ETH in hot wallet | Retry or auto-refund |
| TX reverts (status=0x0) | Insufficient funds, gas, revert | ETH partially in hot wallet | Auto-refund remainder |
| TX pending too long | Network congestion | ETH spent on TX | Provide TX hash, check later |
| No Shield event in TX | Wrong TX hash | None | Verify TX hash |
| Commitment mismatch | Wrong TX for this note | None | Verify TX hash matches shield operation |
| notes.json manipulation fails | Permissions, syntax | Note still in backup | Manual recovery with backup string |
| Unshield proof fails | Facilitator down, indexer desync | None (note unspent) | Retry later |
| ceaser.org unreachable | Server down | None | Check status, retry later |
| Invalid mnemonic in env var | Env var corrupted or wrong format | None | Re-run generate, use fresh mnemonic |
| Refund fails | Gas too high for remainder | Small dust in hot wallet | Inform user |
Recovery guarantee: The backup string is ALWAYS the ultimate fallback. As long as the user has it, shielded ETH can be recovered.
If the session aborts during the flow:
| Phase | State | Loss Risk | Recovery |
|---|---|---|---|
| Before funding | No ETH sent | None | Restart flow |
| After funding, before Shield TX | ETH in hot wallet | Recoverable via mnemonic | User imports mnemonic into MetaMask to access hot wallet |
| After Shield TX, before Unshield | Note in notes.json, backup with user | None | Resume with Path A or B using backup |
| After Unshield, before refund | Main operation complete | Small dust in hot wallet | User can recover dust via mnemonic if desired |
Risk minimization: The agent executes the Shield TX as quickly as possible after funding is confirmed. The window between funding and Shield TX is minimized. Additionally, the user has the mnemonic phrase as a safety net. Even if the session breaks after funding, the user can recover funds by importing the mnemonic into any BIP-39 compatible wallet (e.g., MetaMask).
ceaser-mcp stores notes in a shared notes.json file -- parallel writes may conflict