Wallet Integration skill for Codex
Connect, switch and sign without confusing users. Codex builds connect flows that keep the app usable before connection, discover every installed wallet through EIP-6963, and give wrong-network, pending-request and rejected states their own UI. Sign-In with Ethereum is verified on the server with nonce, domain and expiry and accepts smart-account signatures, and typed-data prompts are named for humans.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- wallet-integration Installs to ~/.agents/skills/wallet-integration/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install wallet-integration The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project wallet-integration Installs to .agents/skills/wallet-integration/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/wallet-integration.zipunzip -o wallet-integration.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) wallet-integration PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $wallet-integration, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and implements wallet connection and signing UX for EVM dapps - EIP-6963 wallet discovery, connect modals with RainbowKit, ConnectKit or Reown AppKit, WalletConnect on mobile, wrong-network handling with wallet_switchEthereumChain, Sign-In with Ethereum (EIP-4361) sessions verified server-side, EIP-712 typed-data prompts users can read, EIP-5792 batched calls, and handling of rejection and pending-request errors. Use when building connect-wallet flows, adding SIWE login, fixing wallet detection or chain-switch bugs, or making signature requests clearer and safer.
Use it when
- A dapp needs a connect-wallet flow or a better one
- Users should log in with their wallet via SIWE
- Chain switching, wallet detection or pending-request bugs appear
- Signature prompts are unreadable or use eth_sign
Use something else when
- You need contract read and write hooks rather than connection UX: use viem-wagmi
- Users should transact without gas or with an embedded smart wallet: use account-abstraction
How to check the result
A manual matrix of an extension wallet, a WalletConnect mobile wallet and a smart-account wallet completes connect, switch, SIWE login and a typed-data signature, and each rejection path recovers.
The mistake it prevents
Verifying SIWE signatures only in the browser, or with ecrecover alone, so sessions can be forged and smart-account users cannot sign in.
Wallet UX is right when every installed wallet shows up without guesswork, the app works read-only before connecting, users always know which network and account they are on, every signature request is human-readable and justified, and a rejection or timeout leaves the UI in a clear, recoverable state.
Inspect first
- Read
AGENTS.md,package.jsonand the existing provider tree. Identify the stack: wagmi + a connect kit, a vendor SDK (embedded wallets), or rawwindow.ethereum. - List supported chains and where contract addresses live per chain.
- Find the auth model: no login, SIWE session, or a third-party auth provider.
- Check the connect kit’s supported wagmi major before upgrading anything; see
$viem-wagmifor hook names that differ between wagmi v2 and v3.
Choose the stack
| Need | Default |
|---|---|
| React app, standard wallets, good modal | wagmi + RainbowKit or ConnectKit |
| Many wallets incl. mobile, email/social login options | Reown AppKit (WalletConnect) |
| Users without wallets (consumer app) | Embedded wallet SDK or smart accounts: see $account-abstraction |
| Non-React or minimal | wagmi core or viem with EIP-6963 discovery |
Avoid reading window.ethereum directly: with several extensions installed they fight over
it. EIP-6963 announces each wallet separately; wagmi’s injected connector and the kits
already use it.
Getting a WalletConnect / Reown project ID requires the user to create one in the Reown dashboard; ask for it, put it in a public env var, and restrict allowed origins there.
Connection states to design
| State | UI |
|---|---|
| Not connected | Full read-only app; connect button; never block browsing on connection |
| Connecting | Modal shows which wallet is waiting; cancel returns to idle |
Request already pending (-32002) | Tell the user to open their wallet extension; do not re-fire the request |
| Connected, right chain | Short address (ENS name if any), network badge, disconnect |
| Connected, wrong chain | Banner with one-click switch; disable writes, keep reads working |
Wallet locked or disconnected (4900/4901) | Fall back to not-connected without losing form input |
Reconnect on reload only if the user connected before (connect kits handle this with storage); never auto-open the connect modal on first visit.
Network switching
import { useSwitchChain } from "wagmi";
const { switchChain, isPending, error } = useSwitchChain();
// onClick:
switchChain({ chainId: base.id });
- Under the hood:
wallet_switchEthereumChain; error4902means the wallet does not know the chain and wagmi falls back towallet_addEthereumChainusing the chain definition. - Some wallets (many mobile and WalletConnect sessions) do not allow programmatic switching; show manual instructions as a fallback.
- Pass
chainIdexplicitly on every read and write so a wallet on another chain cannot send a transaction to the wrong deployment.
Sign-In with Ethereum (EIP-4361)
Flow: server issues a nonce, client builds and signs the message, server verifies and creates a session. Never verify only on the client.
// server: GET /api/siwe/nonce
import { generateSiweNonce } from "viem/siwe";
const nonce = generateSiweNonce(); // store in the session, single use, short TTL
// client
import { createSiweMessage } from "viem/siwe";
const message = createSiweMessage({
address, chainId, domain: window.location.host, nonce,
uri: window.location.origin, version: "1",
statement: "Sign in to Example. This does not cost gas.",
issuedAt: new Date(), expirationTime: new Date(Date.now() + 10 * 60_000),
});
const signature = await signMessageAsync({ message });
// server: POST /api/siwe/verify
const ok = await publicClient.verifySiweMessage({ message, signature, nonce: session.nonce, domain: req.host });
publicClient.verifySiweMessagechecks the signature with ERC-1271 and ERC-6492 too, so smart-account users can sign in. Plainecrecoverrejects them.- Validate domain, nonce (then delete it), chain ID and expiry on the server.
- Session cookie:
HttpOnly,Secure,SameSite=Lax; end the session on account change (accountsChanged) as well as on disconnect. - A SIWE session proves wallet control at login time. Authorize later actions server-side against that session, not against an address sent by the client.
Signature prompts users can read
| Method | Use | Never |
|---|---|---|
personal_sign (signMessage) | SIWE, simple consent text | Hex blobs the user cannot read |
eth_signTypedData_v4 (signTypedData) | Orders, permits, off-chain votes | Domains without chainId and verifyingContract |
eth_sign | - | Never request it; it signs arbitrary hashes and wallets warn or block it |
await signTypedDataAsync({
domain: { name: "Example Exchange", version: "1", chainId: 8453, verifyingContract: EXCHANGE },
types: { Order: [
{ name: "sell", type: "address" }, { name: "amount", type: "uint256" },
{ name: "minReceive", type: "uint256" }, { name: "deadline", type: "uint256" },
{ name: "nonce", type: "uint256" },
] },
primaryType: "Order",
message: { sell: USDC, amount: 100_000_000n, minReceive: 0n, deadline, nonce },
});
Field names appear in the wallet; name them for humans (minReceive, not p2). Before the
prompt, show in your own UI what the signature authorizes, for how long, and that it is free
or costs gas.
Batched calls (EIP-5792)
wallet_getCapabilities tells you whether the wallet supports atomic batches and paymaster
sponsorship per chain. If it does, send approve + action as one wallet_sendCalls (wagmi
useSendCalls) and track with wallet_getCallsStatus. If not, fall back to sequential
transactions with clear “step 1 of 2” progress. Never assume support.
Error handling
| Code / case | Meaning | Response |
|---|---|---|
4001 | User rejected | Quiet inline message, keep inputs, allow retry. Not an error toast |
-32002 | Request already pending in wallet | Ask the user to open the wallet |
4100 | Not authorized for this account/method | Reconnect |
4902 | Unknown chain | Offer add-chain |
| Insufficient funds for gas | Balance too low | Show required amount and network |
| Simulation revert | Call would fail | Explain the decoded reason before the wallet opens |
Map viem errors with err.walk() to UserRejectedRequestError and friends rather than
string-matching messages that differ by wallet.
Security rules
- Show the full destination address (checksummed) and amount before any value transfer; never auto-submit a transaction after connect.
- Request exact token approvals by default; unlimited only as an explicit user choice with a warning, and link to revoking.
- Never ask for seed phrases or private keys, and never build flows where your server holds user keys unless that is the explicit product (custodial) with its own security review.
- Pin and verify domains in SIWE; phishing sites replay messages for other domains.
- Do not log full signatures, SIWE messages with nonces, or session tokens.
Testing
- Use wagmi’s
mockconnector in component tests for connected, wrong-chain and rejected states. - Manual matrix before release: one desktop extension, one mobile wallet via WalletConnect, one smart-account wallet (SIWE and permit via 1271), two extensions installed at once.
- E2E with a headless wallet harness (for example Synpress-style setups) if the team already has one; otherwise script the mock connector flows.
Done when
- App is usable read-only; connect modal lists all EIP-6963 wallets
- Wrong-chain, pending-request and rejection states each have UI
- SIWE verified server-side with nonce, domain and expiry; smart accounts can log in
- Every signature prompt is typed data or readable text with an in-app explanation
- Batching uses EIP-5792 when supported and falls back cleanly
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Wallet Integration"
short_description: "Connect, switch and sign without confusing users"
default_prompt: "$wallet-integration Add a connect-wallet button with RainbowKit and handle the wrong network" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.