This guide is part of the V2 cutover series. For the Python migration see py_clob_client Reference. For the full V1→V2 playbook covering Python, TypeScript, and Rust together see Migrating Your Polymarket Bot to CLOB V2. For TS-specific errors after the port see py-clob-client-v2 Errors (the TS SDK shares most of the post-cutover error patterns).

Last verified: May 2026 against @polymarket/[email protected].


Why the ethers → viem switch

Polymarket made this call before CLOB V2 even shipped. The reasons are the same ones the broader EVM ecosystem has been migrating for:

  • Bundle size. viem is tree-shakable; you import only what you use. ethers v5 in particular is monolithic.
  • Type safety. viem’s types are strict around addresses (0x${string}), data (Hex), and BigInts. Fewer “string vs hex” / “BigNumber vs bigint” footguns.
  • Performance. viem’s signing and encoding paths are measurably faster, especially for high-throughput bots.
  • API surface. viem splits read-side (PublicClient) from write-side (WalletClient) — which maps cleanly onto how the CLOB client uses the signer (only for signing, never for RPC reads).

The trade-off is that you can’t just swap the import — viem’s mental model is different enough that you’ll touch the wallet-setup code, anywhere you used provider.getBalance() (now publicClient.getBalance()), and any helper that returned an ethers BigNumber (now a JS bigint).

If your bot was a thin wrapper around ClobClient, the port is about 30 minutes. If your bot has a lot of incidental ethers usage (own RPC reads, custom contract calls), budget more.


The conceptual shift in one diagram

In ethers, you had one combined object that did everything — a Wallet connected to a Provider, which signed and made RPC calls. In viem, those are two separate objects:

  • An Account holds the private key and can sign.
  • A WalletClient wraps an Account + a Transport and exposes write-side methods (signMessage, signTypedData, sendTransaction).
  • A separate PublicClient wraps a Transport and exposes read-side methods (getBalance, readContract, getBlockNumber).

The Polymarket V2 ClobClient only needs the WalletClient — it has its own HTTP layer for talking to the CLOB and doesn’t read from chain itself. If your code reads on-chain state (balances, allowances, contract storage), you’ll build a separate PublicClient and use it there.

ethers v5/v6                       viem
─────────────────────              ──────────────────────────────────
const provider = new JsonRpc…      const account = privateKeyToAccount(pk)
const wallet   = new Wallet(pk,    const walletClient = createWalletClient({
                              prov)                       account,
                                                          chain: polygon,
                                                          transport: http(),
                                                        })
                                   const publicClient = createPublicClient({
                                                          chain: polygon,
                                                          transport: http(),
                                                        })
↑ signer + provider in one         ↑ account → signs; walletClient → writes;
                                     publicClient → reads

Install

# V1 (still installs, won't talk to V2 backend)
npm install @polymarket/clob-client ethers

# V2 — replace both
npm uninstall @polymarket/clob-client ethers
npm install @polymarket/clob-client-v2 viem

If you have other parts of your codebase that still need ethers (e.g., a different DeFi integration), you can keep ethers installed alongside viem — they don’t conflict. Just don’t pass an ethers Signer to the V2 ClobClient; the constructor expects a viem WalletClient.


The full side-by-side

Wallet / signer construction

// V1 — ethers v5
import { ethers } from "ethers";

const provider = new ethers.providers.JsonRpcProvider(
  process.env.POLYGON_RPC,  // optional — defaults to localhost
);
const signer = new ethers.Wallet(process.env.PK!, provider);
// V2 — viem
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { polygon } from "viem/chains";

const account = privateKeyToAccount(process.env.PK as `0x${string}`);
const walletClient = createWalletClient({
  account,
  chain: polygon,           // viem's `polygon` chain object
  transport: http(),        // defaults to the chain's public RPC; pass URL to override
});

Two notes that catch ethers veterans:

  • The private key has to be typed as `0x${string}`. viem is strict; pass a plain string and TypeScript complains. Either cast with as 0x${string} or use `.startsWith("0x") ? pk : `0x${pk} to guarantee the prefix.
  • http() with no argument uses the chain’s default public RPC. For production bots you almost certainly want to pass your own RPC URL: transport: http(process.env.POLYGON_RPC). The defaults rate-limit aggressively.

ClobClient construction

// V1 — positional args, ethers Signer
import { ClobClient } from "@polymarket/clob-client";

const client = new ClobClient(
  "https://clob.polymarket.com",
  137,                       // chainId — positional
  signer,                    // ethers Signer
  creds,                     // ApiKeyCreds
  2,                         // signatureType
  "0x...",                   // funderAddress
  false,                     // useServerTime
  builderConfig,
  undefined,                 // getSigner
  false,                     // retryOnError
  60_000,                    // tickSizeTtlMs (REMOVED in V2)
  false,                     // throwOnError
);
// V2 — options object, viem WalletClient
import {
  ApiKeyCreds, Chain, ClobClient, SignatureTypeV2,
} from "@polymarket/clob-client-v2";

const client = new ClobClient({
  host:           "https://clob.polymarket.com",
  chain:          Chain.POLYGON,        // renamed from chainId; use the enum
  signer:         walletClient,         // viem WalletClient, not ethers Signer
  creds,                                // same shape: { key, secret, passphrase }
  signatureType:  SignatureTypeV2.POLY_1271,
  funderAddress:  process.env.FUNDER,   // for sig types 1/2/3
  // tickSizeTtlMs   ← REMOVED, no replacement
  // geoBlockToken   ← REMOVED, no replacement
  // throwOnError flag still exists
});

The changes you’ll touch every time:

  • chainIdchain. The new Chain enum has Chain.POLYGON (137) and Chain.AMOY (80002 — testnet). You can also just pass the chain ID number.
  • Constructor is an options object. The order of fields doesn’t matter; missing fields default sensibly.
  • signer must be a viem WalletClient. Passing an ethers Signer won’t even compile.
  • tickSizeTtlMs is gone — the V2 client caches tick sizes internally with sane defaults you can’t tune.
  • geoBlockToken is gone — geo-blocking is now backend-only.

Deriving / loading API credentials

// V1
const creds = await client.createOrDeriveApiCreds();
client.setApiCreds(creds);  // legacy mutate-in-place pattern
// V2 — two-step is idiomatic
const l1 = new ClobClient({ host, chain: Chain.POLYGON, signer: walletClient });
const creds = await l1.createOrDeriveApiKey();          // ← method renamed

const client = new ClobClient({                          // L2 trading client
  host, chain: Chain.POLYGON, signer: walletClient, creds,
});

Only difference worth flagging: the method is createOrDeriveApiKey(), not createOrDeriveApiCreds(). The shape of creds is unchanged ({ key, secret, passphrase }).

Placing a limit order

// V1
import { Side } from "@polymarket/clob-client";

const resp = await client.createAndPostOrder(
  { tokenID, price: 0.50, size: 10, side: Side.BUY },
  { tickSize: "0.01", negRisk: false },
  // OrderType.GTC default
);
// V2 — same shape, but the third arg is the order type and the options shape gained fields
import { OrderType, Side } from "@polymarket/clob-client-v2";

const resp = await client.createAndPostOrder(
  { tokenID, price: 0.50, size: 10, side: Side.BUY },
  { tickSize: "0.01" },         // negRisk is auto-detected when omitted
  OrderType.GTC,                // explicit — default GTC, but be explicit for clarity
);

Three fields that used to be settable on OrderArgs and no longer are (the V2 backend removed them from the signed order):

  • feeRateBps — fees are set by the protocol at match time.
  • nonce — replaced by timestamp (ms) for per-address uniqueness, handled by the SDK.
  • taker — was unused in practice; removed.

If your V1 code set these, delete them. If you want fee-aware fill amounts on market buys, V2 adds an optional userUSDCBalance field on MarketOrderArgs so the SDK can compute the post-fee fill.

Placing a market order

// V1
import { Side, OrderType } from "@polymarket/clob-client";

const resp = await client.createAndPostMarketOrder(
  { tokenID, amount: 25, side: Side.BUY, orderType: OrderType.FOK },
  { tickSize: "0.01" },
);
// V2
const resp = await client.createAndPostMarketOrder(
  { tokenID, amount: 25, side: Side.BUY, orderType: OrderType.FOK },
  { tickSize: "0.01" },
  OrderType.FOK,                // explicit — must match orderType above
);

The doubled orderType (once on MarketOrderArgs, once as the third arg) is intentional — the args field is for the order builder, the third arg is what gets sent in the POST body. Keep them aligned or the SDK throws.

Reading market data

Most read methods are unchanged in name and arg order:

const book      = await client.getOrderBook(tokenID);
const midpoint  = await client.getMidpoint(tokenID);
const price     = await client.getPrice(tokenID, Side.BUY);
const tickSize  = await client.getTickSize(tokenID);
const negRisk   = await client.getNegRisk(tokenID);
const marketInfo = await client.getClobMarketInfo(conditionID);   // new in V2

The shape of getOrderBook’s return changed: it’s now the raw API response (a plain object with bids: [{price, size}, ...], asks: [...], market, asset_id, hash). In TypeScript this is less disruptive than in Python because you were probably typing it anyway; just confirm your interface matches the new shape.

Cancelling and managing orders

// Unchanged signatures, only the import path differs
await client.cancel({ orderID });
await client.cancelOrders([orderID1, orderID2]);
await client.cancelAll();
await client.cancelMarketOrders({ market: conditionID });   // new
const open = await client.getOpenOrders();

Error handling: ApiError and throwOnError

V1 returned { error, status, data } on failures. V2 keeps that as the default for backwards compatibility but adds a throwOnError: true constructor flag that makes the client raise typed ApiErrors instead:

import { ApiError, ClobClient } from "@polymarket/clob-client-v2";

const client = new ClobClient({
  host, chain: Chain.POLYGON, signer: walletClient, creds,
  throwOnError: true,                  // ← opt in to typed errors
});

try {
  const book = await client.getOrderBook(tokenID);
} catch (e) {
  if (e instanceof ApiError) {
    console.log(e.message);  // "No orderbook exists for the requested token id"
    console.log(e.status);   // 404
    console.log(e.data);     // full error response object
  }
}

Highly recommended for new code. Silent failure paths in { error, status } returns are a class of bugs you don’t need to bring with you.


A full bot, ported

A minimal “find a market → check the book → place a $5 limit a couple cents under the ask” bot, in both versions.

V1 (ethers — no longer works on production)

import { ethers } from "ethers";
import { ClobClient, Side, OrderType } from "@polymarket/clob-client";
import axios from "axios";

const provider = new ethers.providers.JsonRpcProvider(process.env.POLYGON_RPC);
const signer   = new ethers.Wallet(process.env.PK!, provider);

const client = new ClobClient(
  "https://clob.polymarket.com",
  137,
  signer,
  /* creds */ undefined,
  2,
  process.env.FUNDER,
);
client.setApiCreds(await client.createOrDeriveApiCreds());

const markets = (
  await axios.get("https://gamma-api.polymarket.com/markets", {
    params: { closed: false, limit: 5, order: "volume", ascending: false },
  })
).data;
const target  = markets[0];
const tokenID = JSON.parse(target.clobTokenIds)[0];

const book    = await client.getOrderBook(tokenID);
const bestAsk = book.asks?.[0]?.price ? Number(book.asks[0].price) : undefined;

if (bestAsk && bestAsk > 0.05) {
  const myPrice = Math.round((bestAsk - 0.02) * 100) / 100;
  const resp = await client.createAndPostOrder(
    { tokenID, price: myPrice, size: 10, side: Side.BUY },
    { tickSize: "0.01" },
  );
  console.log(resp);
}

V2 (viem — working)

import {
  ApiKeyCreds, ApiError, Chain, ClobClient,
  OrderType, Side, SignatureTypeV2,
} from "@polymarket/clob-client-v2";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { polygon } from "viem/chains";
import axios from "axios";

const account      = privateKeyToAccount(process.env.PK as `0x${string}`);
const walletClient = createWalletClient({
  account, chain: polygon, transport: http(process.env.POLYGON_RPC),
});

// Step 1: derive creds with an L1-only client
const l1 = new ClobClient({
  host:  "https://clob.polymarket.com",
  chain: Chain.POLYGON,
  signer: walletClient,
});
const creds: ApiKeyCreds = await l1.createOrDeriveApiKey();

// Step 2: build the trading client with creds + funder + sig type
const client = new ClobClient({
  host:           "https://clob.polymarket.com",
  chain:          Chain.POLYGON,
  signer:         walletClient,
  creds,
  signatureType:  SignatureTypeV2.POLY_1271,   // or .GNOSIS_SAFE / .POLY_PROXY / .EOA
  funderAddress:  process.env.FUNDER,
  throwOnError:   true,                         // ← typed errors
});

const markets = (
  await axios.get("https://gamma-api.polymarket.com/markets", {
    params: { closed: false, limit: 5, order: "volume", ascending: false },
  })
).data;
const target  = markets[0];
const tokenID = JSON.parse(target.clobTokenIds)[0] as string;

const book    = await client.getOrderBook(tokenID);
const bestAsk = book.asks?.[0]?.price ? Number(book.asks[0].price) : undefined;

if (bestAsk && bestAsk > 0.05) {
  const myPrice = Math.round((bestAsk - 0.02) * 100) / 100;
  try {
    const resp = await client.createAndPostOrder(
      { tokenID, price: myPrice, size: 10, side: Side.BUY },
      { tickSize: "0.01" },
      OrderType.GTC,
    );
    console.log(resp);
  } catch (e) {
    if (e instanceof ApiError) {
      console.error(`CLOB ${e.status}: ${e.message}`);
    } else {
      throw e;
    }
  }
}

The diff in one sentence: two viem objects (account, walletClient) replace the ethers provider + signer, the ClobClient constructor takes an options bag instead of positional args, createOrDeriveApiKey() replaces createOrDeriveApiCreds() + setApiCreds(), the third arg to createAndPostOrder is the order type, and throwOnError: true plus ApiError gives you typed exceptions.


Viem-specific gotchas

A short field-report of things that bit our TS port:

  • account vs walletClient. Some viem methods take an Account directly (signMessage), others take a WalletClient. The Polymarket SDK takes a WalletClient. If you see Type 'PrivateKeyAccount' is not assignable to type 'WalletClient', you’re passing the wrong one — wrap it with createWalletClient.
  • Chain selection. viem/chains exports polygon (the chain object) and a few related ones. Don’t confuse that with the SDK’s Chain enum (Chain.POLYGON = 137). You use polygon when constructing the viem WalletClient; you pass Chain.POLYGON (or just 137) to the ClobClient constructor’s chain field.
  • Default http() transport. Without an argument it uses the chain’s default public RPC. Public RPCs are rate-limited and unreliable for high-throughput bots. Always pass your own RPC URL: http(process.env.POLYGON_RPC).
  • BigInts everywhere. viem returns bigint for amounts, balances, gas, etc. — not ethers’ BigNumber. .toNumber() doesn’t exist; use Number(x) (for small enough values) or arithmetic with BigInt literals. The CLOB SDK abstracts this for you on the trading side, but if you do your own viem reads, expect bigints.
  • Private key formatting. privateKeyToAccount expects `0x${string}`. If your env var is “abc123…” without the 0x prefix, prepend it: `0x${pk}` as `0x${string}`.
  • Account abstraction / smart-contract wallets. If you’re signing through an EIP-1271-validated smart-contract wallet (Safe, deposit wallet), viem’s flow is the same — but you’ll wire signatureType: SignatureTypeV2.POLY_1271 and funderAddress on the ClobClient, and your viem signer is still the EOA that controls the smart wallet. See POLY_1271 & Smart-Contract Wallets for the full story.

What didn’t change

To avoid over-correcting:

  • The CLOB host URL: https://clob.polymarket.com.
  • The Gamma API and Data API.
  • WebSocket URLs and payloads.
  • Existing API key / secret / passphrase — ClobAuthDomain (L1/L2 API auth) stayed at version "1". You do not need to regenerate creds.
  • Order types: GTC, FOK, FAK; post-only flag (still GTC-only).
  • The shape of getOrderBook’s return is similar (bids, asks, market, asset_id, hash) — just typed cleanly in V2.

Changelog

DateChange
May 25, 2026Initial publication. Verified against @polymarket/[email protected] README and the V2 migration docs.

Official Resources

AgentBets Guides

Where This Fits in the Agent Betting Stack

This is Layer 3 (Trading) infrastructure — the execution layer your TS-based agent depends on. The ethers→viem swap doesn’t change strategy or signal generation (Layer 4); it changes how the bot puts orders on the wire. If you’re rebuilding from scratch on the V2 SDK, the Trading Bot Quickstart is V2-native end-to-end and avoids ethers entirely.


This guide is maintained by AgentBets.ai. Hit a viem-specific gotcha we missed? Let us know on Twitter.

Not financial advice. Built for builders.