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.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- viem-wagmi

Installs to ~/.agents/skills/viem-wagmi/ for every project on this machine.

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

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.

~/.agents/skills/viem-wagmi/SKILL.md

--- name: viem-wagmi description: 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… license: MIT ---

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

  1. Read AGENTS.md and package.json: versions of viem, 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.
  2. Find the existing config (createConfig) and providers. Never create a second config.
  3. Find where ABIs come from: as const files, @wagmi/cli output, or JSON imports. JSON imports lose type inference; convert them.
  4. Note the framework: Next.js App Router needs the SSR setup below.

wagmi v2 to v3 names

v2v3
useAccountuseConnection
useAccountEffectuseConnectionEffect
useSwitchAccountuseSwitchConnection
const { writeContract } = useWriteContract()const write = useWriteContract(); write.mutate(...)
useConnect().connectorsuseConnectors()
useSwitchChain().chainsuseChains()
Connector SDKs bundledConnector 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")) as initialState to WagmiProvider in 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.enabled instead 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 the queryKey returned by the read hook, rather than polling.
  • Format units with formatUnits(value, decimals) and read decimals from 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: useWatchContractEvent or publicClient.watchContractEvent; handle reorgs by treating unconfirmed logs as provisional. For dashboards and history, an indexer ($thegraph-subgraphs) beats client-side log scans.

Chains

  • Read chainId from useConnection()/useAccount() and compare with the contract’s chain. Offer useSwitchChain().switchChain({ chainId }); pass chainId explicitly 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 v6viem
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, formatUnitssame names from viem
BigNumber / bigintbigint only

Gotchas

  • useWriteContract without a prior simulation opens the wallet for calls that will revert.
  • Mixing number and bigint throws at runtime; keep amounts as bigint end to end.
  • useWaitForTransactionReceipt defaults 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 --noEmit passes with no any on 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 mock connector rather than stubbing hooks.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem