Install
openclaw skills install portal-walletMPC-secured crypto wallet via Portal. Use when users ask to check balances, send tokens, sign transactions, or swap tokens. Supports Monad, Ethereum, Solana, Bitcoin, Polygon, Base, and more. Private keys are never exposed — signing uses MPC threshold cryptography.
openclaw skills install portal-walletPortal provides MPC-based wallets. The private key is split between your share (stored in env vars) and Portal's server — neither party can sign alone.
This skill controls real cryptocurrency funds. Follow these rules strictly:
0x and be 42 hex characters. Solana addresses must be valid base58. Bitcoin addresses must match the expected format for the network. If an address looks wrong, flag it.personal_sign and eth_signTypedData_v4 can authorize token approvals, permits (EIP-2612), and gasless orders. Always decode and display the message contents to the user. Never sign typed data containing approval, permit, or transfer operations without explicit consent.PORTAL_CLIENT_API_KEY — Client API key for authenticationPORTAL_SECP256K1_SHARE — MPC share for EVM and Bitcoin signingPORTAL_ED25519_SHARE — MPC share for Solana signinghttps://api.portalhq.iohttps://mpc-client.portalhq.iocurl -s 'https://api.portalhq.io/api/v3/clients/me' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" | jq .
curl -s 'https://api.portalhq.io/api/v3/clients/me/chains/eip155:143/assets' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" | jq .
Replace eip155:1 with the desired chain ID (see Supported Chains below).
For EVM chains:
curl -s 'https://api.portalhq.io/api/v3/clients/me/transactions?chainId=eip155:143' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" | jq .
For Solana:
curl -s 'https://api.portalhq.io/api/v3/clients/me/chains/solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/transactions' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" | jq .
All POST examples below use jq to construct the JSON body and pipe it to curl --data @-. This is the recommended pattern — jq --arg handles escaping for all values (addresses, amounts, token names) so you don't have to worry about quotes or special characters breaking the JSON.
The simplest way to send crypto. Portal handles transaction building and broadcasting.
EVM chains (example uses Monad — swap monad / 143 for any other EVM chain from the Supported Chains table):
jq -n \
--arg share "$PORTAL_SECP256K1_SHARE" \
--arg to "<recipient-address>" \
--arg amount "<amount>" \
'{share: $share, chain: "monad", to: $to, token: "NATIVE", amount: $amount, rpcUrl: "https://api.portalhq.io/rpc/v1/eip155/143"}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/assets/send' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
Solana:
jq -n \
--arg share "$PORTAL_ED25519_SHARE" \
--arg to "<recipient-address>" \
--arg amount "<amount>" \
'{share: $share, chain: "solana-devnet", to: $to, token: "NATIVE", amount: $amount, rpcUrl: "https://api.portalhq.io/rpc/v1/solana/EtWTRABZaYq6iMfeYKouRu166VU2xqa1"}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/assets/send' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
Bitcoin:
jq -n \
--arg share "$PORTAL_SECP256K1_SHARE" \
--arg to "<recipient-address>" \
--arg amount "<amount>" \
'{share: $share, chain: "bitcoin-segwit-testnet", to: $to, token: "NATIVE", amount: $amount}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/assets/send' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
Use the token contract address, or shortcuts "USDC" / "USDT":
jq -n \
--arg share "$PORTAL_SECP256K1_SHARE" \
--arg to "<recipient-address>" \
--arg token "USDC" \
--arg amount "10" \
'{share: $share, chain: "monad", to: $to, token: $token, amount: $amount, rpcUrl: "https://api.portalhq.io/rpc/v1/eip155/143"}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/assets/send' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
For full control over transaction parameters.
Note: params must be a stringified JSON object. Build it with jq -c first, then pass as a string arg.
TX_PARAMS=$(jq -cn \
--arg from "<sender>" \
--arg to "<recipient>" \
--arg value "<wei-hex>" \
'{from: $from, to: $to, value: $value, data: ""}')
jq -n \
--arg share "$PORTAL_SECP256K1_SHARE" \
--arg params "$TX_PARAMS" \
'{share: $share, method: "eth_sendTransaction", params: $params, rpcUrl: "https://api.portalhq.io/rpc/v1/eip155/143", chainId: "eip155:143"}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/sign' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
jq -n \
--arg share "$PORTAL_SECP256K1_SHARE" \
--arg params "<hex-encoded-message>" \
'{share: $share, method: "personal_sign", params: $params, chainId: "eip155:143"}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/sign' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
jq -n \
--arg share "$PORTAL_SECP256K1_SHARE" \
--arg params "<stringified-typed-data-json>" \
'{share: $share, method: "eth_signTypedData_v4", params: $params, chainId: "eip155:143"}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/sign' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
jq -n \
--arg share "$PORTAL_ED25519_SHARE" \
--arg params "<base64-serialized-transaction>" \
'{share: $share, method: "sol_signAndConfirmTransaction", params: $params, chainId: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/sign' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
For signing arbitrary hashes without transaction building:
jq -n \
--arg share "$PORTAL_SECP256K1_SHARE" \
--arg params "<hex-digest-without-0x>" \
'{share: $share, params: $params}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/raw/sign/SECP256K1' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
jq -n \
--arg buyToken "<token-address-or-symbol>" \
--arg sellToken "<token-address-or-symbol>" \
--arg sellAmount "<amount-in-smallest-unit>" \
--arg chainId "eip155:<chain-id-number>" \
'{buyToken: $buyToken, sellToken: $sellToken, sellAmount: $sellAmount, chainId: $chainId}' \
| curl -s -X POST 'https://api.portalhq.io/api/v3/clients/me/integrations/0x/swap/quote' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
-H 'Content-Type: application/json' \
--data @- | jq .
jq -n \
--arg buyToken "<token-address-or-symbol>" \
--arg sellToken "<token-address-or-symbol>" \
--arg sellAmount "<amount-in-smallest-unit>" \
--arg chainId "eip155:<chain-id-number>" \
'{buyToken: $buyToken, sellToken: $sellToken, sellAmount: $sellAmount, chainId: $chainId}' \
| curl -s -X POST 'https://api.portalhq.io/api/v3/clients/me/integrations/0x/swap/price' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
-H 'Content-Type: application/json' \
--data @- | jq .
Simulate or validate a transaction before signing:
jq -n \
--arg to "<recipient>" \
--arg value "<wei-hex>" \
--arg data "<calldata>" \
'{to: $to, value: $value, data: $data, chainId: "eip155:143"}' \
| curl -s -X POST 'https://api.portalhq.io/api/v3/clients/me/evaluate-transaction' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
-H 'Content-Type: application/json' \
--data @- | jq .
| Chain | chain param | Chain ID | RPC path |
|---|---|---|---|
| Monad | monad | eip155:143 | eip155/143 |
| Monad Testnet | monad-testnet | eip155:10143 | eip155/10143 |
| Ethereum | ethereum | eip155:1 | eip155/1 |
| Sepolia | sepolia | eip155:11155111 | eip155/11155111 |
| Polygon | polygon | eip155:137 | eip155/137 |
| Base | base | eip155:8453 | eip155/8453 |
| Arbitrum | arbitrum | eip155:42161 | eip155/42161 |
| Optimism | optimism | eip155:10 | eip155/10 |
| Network | chain param | Chain ID |
|---|---|---|
| Mainnet | solana | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
| Devnet | solana-devnet | solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 |
| Network | chain param |
|---|---|
| Mainnet | bitcoin-segwit |
| Testnet | bitcoin-segwit-testnet |
Portal provides RPC via: https://api.portalhq.io/rpc/v1/<namespace>/<chain-id-number>
If the wallet was created with Account Abstraction enabled (isAccountAbstracted: true), gas is sponsored by default — the user does NOT need to hold native tokens to pay for gas. The sponsorGas field controls this:
true: Gas is sponsored (user pays nothing for gas)false: User pays gas from their own native token balanceExample with gas sponsorship explicitly disabled:
jq -n \
--arg share "$PORTAL_SECP256K1_SHARE" \
--arg to "<recipient>" \
'{share: $share, chain: "monad", to: $to, token: "USDC", amount: "10", rpcUrl: "https://api.portalhq.io/rpc/v1/eip155/143", sponsorGas: false}' \
| curl -s -X POST 'https://mpc-client.portalhq.io/v1/assets/send' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PORTAL_CLIENT_API_KEY" \
--data @- | jq .
If the wallet is NOT Account Abstraction enabled, the user must hold native tokens (ETH, SOL, etc.) on the relevant chain to cover gas fees. Gas sponsorship is not available for non-AA wallets.
When an API call returns an error, interpret it and give the user a clear, helpful message. Common errors:
Insufficient funds / balance too low: Tell the user they don't have enough funds and need to top up their wallet. Suggest they send tokens to their wallet address (use the "Get Wallet Details" endpoint to show their address). If on testnet, suggest using Portal's faucet endpoint.
Invalid chain or unsupported chain: Tell the user which chains are supported (see the Supported Chains table). If they asked for a chain not in the table, let them know it's not available yet.
Invalid address format:
Tell the user the address they provided doesn't look right. EVM addresses start with 0x and are 42 characters. Solana addresses are base58 encoded. Bitcoin addresses vary by format.
Authentication errors (401/403): Tell the user their Portal credentials may be expired or misconfigured. They should check their PORTAL_CLIENT_API_KEY environment variable.
Rate limiting (429): Tell the user to wait a moment and try again. Portal has rate limits on API calls.
RPC errors / network issues: Tell the user the blockchain network might be congested or the RPC endpoint is having issues. Suggest trying again in a few moments.
Share parsing errors: Tell the user their MPC share may be corrupted or incorrectly configured. They should verify the PORTAL_SECP256K1_SHARE or PORTAL_ED25519_SHARE environment variable contains the full base64-encoded share from wallet generation.
General guidelines:
params field in sign requests must be a JSON string (stringified), not a JSON object.nonce field manually to avoid conflicts.