viem and wagmi skill for Codex
Typed contract calls that simulate before they sign. Codex wires contract reads and writes with const ABIs so every argument is typed, simulates each write before the wallet opens, follows the transaction to its receipt, and turns custom errors into readable messages. It knows the wagmi v2 and v3 hook names, sets up cookie-based SSR for Next.js, and replaces ethers.js idioms with viem equivalents.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- viem-wagmi Installs to ~/.agents/skills/viem-wagmi/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install viem-wagmi 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 viem-wagmi Installs to .agents/skills/viem-wagmi/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/viem-wagmi.zipunzip -o viem-wagmi.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))) viem-wagmi 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 $viem-wagmi, 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
Builds Ethereum dapp frontends and scripts with viem 2.x and wagmi (v2 and v3) in React - typed ABIs with as const or the wagmi CLI, createConfig with transports and connectors, reads with useReadContract and multicall, writes through simulate then write then wait for receipt, chain switching, decoded revert errors, event watching and SSR-safe setup in Next.js. Use when wiring contract reads and writes into a React app, writing a viem script, fixing a wagmi hook, or upgrading ethers.js or wagmi v2 code.
Use it when
- A React or Next.js app reads or writes smart contracts
- You are writing a TypeScript script or backend that talks to an EVM chain
- Hooks break after a wagmi upgrade or hydration errors appear with SSR
- Users see raw hex errors instead of readable revert reasons
Use something else when
- The work is connect-wallet UX, signing prompts and session handling: use wallet-integration
- You need historical event data at scale: use thegraph-subgraphs
How to check the result
tsc passes with fully inferred contract types, and a local-chain run covers wrong chain, missing allowance, simulated revert, wallet rejection and a confirmed receipt.
The mistake it prevents
Calling writeContract without simulating first and treating the returned hash as success, so users see a confirmed button for a transaction that later reverts.
A dapp frontend is right when every contract call is fully typed from a const ABI, every write is simulated before the wallet prompt, the UI tracks a transaction until its receipt (not just the hash), revert reasons reach the user in plain words, and the app never assumes which chain the wallet is on.
Inspect first
- Read
AGENTS.mdandpackage.json: versions ofviem,wagmi,@tanstack/react-query, and any connector kit (RainbowKit, ConnectKit, Reown AppKit). Their majors must match each other; check the kit’s docs for the wagmi major it supports. - Find the existing config (
createConfig) and providers. Never create a second config. - Find where ABIs come from:
as constfiles,@wagmi/clioutput, or JSON imports. JSON imports lose type inference; convert them. - Note the framework: Next.js App Router needs the SSR setup below.
wagmi v2 to v3 names
| v2 | v3 |
|---|---|
useAccount | useConnection |
useAccountEffect | useConnectionEffect |
useSwitchAccount | useSwitchConnection |
const { writeContract } = useWriteContract() | const write = useWriteContract(); write.mutate(...) |
useConnect().connectors | useConnectors() |
useSwitchChain().chains | useChains() |
| Connector SDKs bundled | Connector SDKs are optional peer deps: install what you use |
Use the names that match the installed major. Do not upgrade majors as a side effect of another task.
Config
// wagmi.ts
import { createConfig, http, cookieStorage, createStorage } from "wagmi";
import { mainnet, base, sepolia } from "wagmi/chains";
import { injected, walletConnect } from "wagmi/connectors";
export const config = createConfig({
chains: [base, mainnet, sepolia],
connectors: [
injected(),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }),
],
transports: {
[base.id]: http(process.env.NEXT_PUBLIC_BASE_RPC),
[mainnet.id]: http(process.env.NEXT_PUBLIC_MAINNET_RPC),
[sepolia.id]: http(),
},
ssr: true,
storage: createStorage({ storage: cookieStorage }),
});
declare module "wagmi" {
interface Register { config: typeof config }
}
- Always pass your own RPC URLs.
http()with no URL uses a public endpoint that rate-limits. declare module "wagmi"registers the config so hooks infer chain IDs everywhere.- Wrap the app:
<WagmiProvider config={config}><QueryClientProvider client={queryClient}>. - Next.js App Router: read cookies on the server and pass
cookieToInitialState(config, (await headers()).get("cookie"))asinitialStatetoWagmiProviderin a client component to avoid hydration mismatches.
ABIs and types
export const vaultAbi = [
{ type: "function", name: "deposit", stateMutability: "nonpayable",
inputs: [{ name: "amount", type: "uint256" }], outputs: [] },
{ type: "function", name: "balanceOf", stateMutability: "view",
inputs: [{ name: "user", type: "address" }], outputs: [{ type: "uint256" }] },
{ type: "error", name: "InsufficientBalance",
inputs: [{ name: "requested", type: "uint256" }, { name: "available", type: "uint256" }] },
] as const;
Without as const, argument and return types collapse to unknown. For many contracts,
generate with @wagmi/cli (wagmi generate with the Foundry or Hardhat plugin) so ABIs stay
in sync with builds. Include custom error entries: viem can only decode errors in the ABI.
Reads
const { address } = useConnection(); // useAccount() on wagmi v2
const { data: balance, isLoading } = useReadContract({
address: VAULT, abi: vaultAbi, functionName: "balanceOf",
args: address ? [address] : undefined,
query: { enabled: Boolean(address) },
});
- Gate reads with
query.enabledinstead of passing placeholder args. - Batch several reads with
useReadContracts(uses multicall3 when the chain defines it). - After a write, refresh with
queryClient.invalidateQueries({ queryKey })using thequeryKeyreturned by the read hook, rather than polling. - Format units with
formatUnits(value, decimals)and readdecimalsfrom the token; never assume 18.
Writes: simulate, write, wait
const { data: sim, error: simError } = useSimulateContract({
address: VAULT, abi: vaultAbi, functionName: "deposit",
args: [parseUnits(amount || "0", decimals)],
query: { enabled: Number(amount) > 0 },
});
const write = useWriteContract();
const receipt = useWaitForTransactionReceipt({ hash: write.data });
// v3: write.mutate(sim!.request); v2: writeContract(sim!.request)
<button disabled={!sim || write.isPending} onClick={() => write.mutate(sim!.request)}>
Deposit
</button>
States to render: simulation error (explain why it would fail before the wallet opens),
waiting for signature, submitted (link to explorer with the hash), confirmed, reverted
(receipt.data?.status === "reverted"). A hash is not success.
ERC-20 flows need allowance first: read allowance, and if insufficient, run approve and
wait for its receipt before simulating the main call. Approve the exact amount by default;
offer unlimited only as an explicit user choice.
Plain viem (scripts, servers, tests)
import { createPublicClient, createWalletClient, http, parseUnits, BaseError,
ContractFunctionRevertedError } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
const publicClient = createPublicClient({ chain: base, transport: http(process.env.RPC_URL) });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: base, transport: http(process.env.RPC_URL) });
try {
const { request } = await publicClient.simulateContract({
account, address: VAULT, abi: vaultAbi, functionName: "deposit", args: [parseUnits("10", 6)],
});
const hash = await walletClient.writeContract(request);
const rcpt = await publicClient.waitForTransactionReceipt({ hash, confirmations: 2 });
if (rcpt.status !== "success") throw new Error(`reverted in ${hash}`);
} catch (err) {
if (err instanceof BaseError) {
const revert = err.walk((e) => e instanceof ContractFunctionRevertedError);
if (revert instanceof ContractFunctionRevertedError) {
console.error(revert.data?.errorName, revert.data?.args);
}
}
throw err;
}
Server-side keys come from a secret manager or env, never from the repo. Any script that sends mainnet transactions must print the target, function, args and chain, and wait for the user’s explicit go-ahead before sending.
Events and logs
- Historical:
publicClient.getContractEvents({ address, abi, eventName, fromBlock, toBlock }). RPC providers cap ranges (often 1k to 10k blocks); page through ranges. - Live:
useWatchContractEventorpublicClient.watchContractEvent; handle reorgs by treating unconfirmed logs as provisional. For dashboards and history, an indexer ($thegraph-subgraphs) beats client-side log scans.
Chains
- Read
chainIdfromuseConnection()/useAccount()and compare with the contract’s chain. OfferuseSwitchChain().switchChain({ chainId }); passchainIdexplicitly on reads and writes so a wrong-chain wallet cannot hit the wrong deployment. - Keep addresses in a
Record<chainId, Address>map, not scattered constants.
Migrating from ethers.js
| ethers v6 | viem |
|---|---|
new JsonRpcProvider(url) | createPublicClient({ transport: http(url) }) |
new Wallet(pk, provider) | createWalletClient({ account: privateKeyToAccount(pk) }) |
new Contract(addr, abi, signer) | getContract({ address, abi, client }) or direct actions |
contract.foo.staticCall() | simulateContract |
parseEther, formatUnits | same names from viem |
BigNumber / bigint | bigint only |
Gotchas
useWriteContractwithout a prior simulation opens the wallet for calls that will revert.- Mixing
numberandbigintthrows at runtime; keep amounts asbigintend to end. useWaitForTransactionReceiptdefaults to one confirmation; use more for high-value flows.- Do not store the wallet address in your own state; derive it from the connection hook.
- Fetching ENS names or avatars needs a mainnet transport in the config even on an L2 app.
Verification
tsc --noEmitpasses with noanyon contract calls.- Test on a local chain (anvil or Hardhat node) with an injected wallet, covering: not connected, wrong chain, insufficient allowance, revert during simulation, rejection in the wallet, success with receipt.
- Component tests mock the connector with wagmi’s
mockconnector rather than stubbing hooks.
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: "viem and wagmi"
short_description: "Typed contract calls that simulate before they sign"
default_prompt: "$viem-wagmi Add a deposit button that approves USDC then calls our vault with wagmi" 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.