What is this?
Whether you're a collector or you want to make money buying and selling Chonks Traits, bidding on TraitIndexes allows you to offer money to the thousands in the Chonks community.
Create USDC or WETH Offers for a Trait (like Sumo bottoms) or an exact Chonk. Any eligible seller can accept your Offer later, swapping their Chonk or Trait for your money.
Selling instead of buying? Build a Selling Bot covers listing and canceling your own Traits directly on the marketplace contract.
Read below for more.
What is a Trait Index?
Chonk Traits are organized by TraitIndex. A Rainbow Hair Trait has a TraitIndex of 2034. However, there are 879 Rainbow Hair tokens, each with their own Token ID.
Each time we put out a new Trait, it gets its own TraitIndex before any are collected.
A Trait (like Brown Shoes) can have many token IDs but will always have exactly one TraitIndex shared among all the tokens.
What You Need
- A dedicated bidder wallet EOA and private key
- One Chonk owned by that EOA. Accepted Traits go into this Chonk's Backpack
- WETH or USDC on Base
- A configured list of Trait Index targets (you can find a Trait's TraitIndex by clicking any Trait on /traits)
- Optionally, exact Chonk IDs to target
- viem for signing typed data and sending optional approval transactions
Example Bot
Start with our example Bun bot: chonksxyz/traitindex-offer-bot. Clone it, set your bidder private key and target config, then run one Offer per target and currency.
Safety
- Please use a dedicated EOA. Do not use your main wallet
- Never paste a private key into ChatGPT, a browser, or a hosted tool
- Run your agent or bot locally or in infrastructure you control
- Put the private key in an environment variable or secrets manager like 1Password.
privateKeyToAccountexpects a0x-prefixed 32-byte hex key - The wallet must own the receiving Chonk and hold enough WETH or USDC on Base
- Offers are not escrowed. If your wallet balance or Permit2 allowance drops, later seller accepts can fail
Shared Response Types
Dates are returned as ISO strings. Amounts are returned as raw smallest-unit strings unless a field is explicitly named
human.type Address = string;
type Hex = string;
type BidHash = string;
type TypedData = {
domain: Record<string, unknown>;
types: Record<string, Array<{ name: string; type: string }>>;
primaryType: string;
message: Record<string, unknown>;
};
type StoredTraitIndexBid = {
id: number;
bidHash: BidHash;
bidderEOA: Address;
receivingChonkId: number;
traitIndex: number;
traitType: number;
currency: Address;
currencyDecimals: number;
amount: string;
deadline: string;
salt: string;
permit2Nonce: string;
permit2Deadline: string;
signature: Hex;
eip712TypedData: TypedData | null;
status: "active" | "settling" | "canceled" | "settled" | "expired";
replacedByBidHash: BidHash | null;
settledTraitId: number | null;
settlementTxHash: Hex | null;
createdAt: string;
updatedAt: string;
canceledAt: string | null;
settledAt: string | null;
};
type PublicTraitIndexBid = Omit<
StoredTraitIndexBid,
"salt" | "permit2Nonce" | "permit2Deadline" | "signature" | "eip712TypedData"
>;
type ApiErrorResponse = {
error: string;
code: string;
details?: unknown;
issues?: unknown[];
};
Build An Offer
Build returns canonical typed data and a
createRequest. It does not
create the Offer.| Param | Required | Values |
|---|---|---|
bidderEOA |
Yes | Bidder wallet address. This EOA signs the offer and owns the receiving Chonk. |
receivingChonkId |
Yes | Chonk ID owned by bidderEOA. Accepted Traits go into this Chonk's Backpack. |
traitIndex |
Yes | Target Trait Index number, such as 57. |
currency |
Yes | usdc or weth, case-insensitive. |
amount |
Yes | Human-readable amount string, such as "25.50" for USDC or "0.01" for WETH. |
expirationDays |
No | 1, 3, or 7. Defaults to 7. |
curl -s https://www.chonks.xyz/api/trait-index-bids/build \
-X POST \
-H "Content-Type: application/json" \
-d '{
"bidderEOA": "0xYourBidderWallet",
"receivingChonkId": 12345,
"traitIndex": 57,
"currency": "usdc",
"amount": "25.50",
"expirationDays": 7
}'
currency is usdc or weth, case-insensitive. amount is a human-readable string. For USDC, "5" means 5 USDC and "0.5" means 50 cents.Build preflight is guidance, not a write. Low balance and missing allowance do not make build fail. Create will hard-fail if you ignore those flags.
If
preflight.approval.required is true, submit preflight.approval.transaction with the bidder EOA before signing. This approves the request currency, USDC or WETH, to Permit2. USDC and WETH approvals are separate, so a bot that offers both currencies needs to approve both tokens. The returned approval uses a max allowance for convenience. You may send a lower allowance yourself if your wallet policy requires it.The marketplace permission is the
typedData signature. It authorizes preflight.approval.marketplaceSpender to spend the offered amount through Permit2 for this exact Offer. On Base, that TraitIndex marketplace is 0x10DDD4C86f085C911E8c9000cA9c060EA261c4E3. This is also returned as typedData.message.spender. Token approval alone is not enough. Do not skip signing typedData.If
preflight.balance.hasEnoughBalance is false, fund the bidder EOA before signing.If the request is replacing an active Offer and the amount is too low, build returns
409 REPLACEMENT_TOO_LOW with details.minimumAmount and details.minimumHumanAmount. Rebuild with at least details.minimumHumanAmount.Treat
typedData and createRequest as opaque bot inputs. Sign the returned typedData exactly, and POST the returned createRequest back unchanged with the signature.Response:
type BuildOfferResponse = {
bidHash: BidHash;
traitIndex: number;
traitType: number;
receivingChonkId: number;
currency: {
symbol: "USDC" | "WETH";
address: Address;
decimals: number;
};
amount: {
human: string;
raw: string;
};
deadline: number;
expirationDays: 1 | 3 | 7;
salt: string;
marketplaceSpender: Address;
typedData: TypedData;
createRequest: {
bidHash: BidHash;
bidderEOA: Address;
receivingChonkId: number;
traitIndex: number;
traitType: number;
currency: Address;
currencyDecimals: number;
amount: string;
deadline: number;
expirationDays: 1 | 3 | 7;
salt: string;
permit2Nonce: string;
permit2Deadline: number;
typedData: TypedData;
};
preflight: {
balance: {
balance: string;
requiredAmount: string;
hasEnoughBalance: boolean;
};
approval: {
required: boolean;
isSufficient: boolean;
allowance: string;
minimumAmount: string;
recommendedAmount: string;
spender: Address;
permit2Spender: Address;
marketplaceSpender: Address;
function: "approve(address,uint256)";
transaction: null | {
chainId: 8453;
to: Address;
value: "0";
data: Hex;
};
};
replacement: {
isReplacement: boolean;
previousBidHash: BidHash | null;
minimumAmount: string | null;
minimumHumanAmount: string | null;
};
};
};
Create The Offer
Sign the returned
typedData with the bidder EOA. Then POST the returned createRequest plus signature.import { createWalletClient, http, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
const rawPrivateKey = process.env.BIDDER_PRIVATE_KEY;
if (!rawPrivateKey) throw new Error("Missing BIDDER_PRIVATE_KEY");
const privateKey = (
rawPrivateKey.startsWith("0x") ? rawPrivateKey : `0x${rawPrivateKey}`
) as Hex;
const account = privateKeyToAccount(privateKey);
const wallet = createWalletClient({ account, chain: base, transport: http() });
async function readJson(response: Response) {
const body = await response.json();
if (!response.ok) throw body;
return body;
}
const build = await fetch("https://www.chonks.xyz/api/trait-index-bids/build", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
bidderEOA: account.address,
receivingChonkId: 12345,
traitIndex: 57,
currency: "usdc",
amount: "25.50",
expirationDays: 7,
}),
}).then(readJson);
if (!build.preflight.balance.hasEnoughBalance) {
throw new Error("Fund the bidder EOA before signing this Offer.");
}
if (build.preflight.approval.required) {
await wallet.sendTransaction({
to: build.preflight.approval.transaction.to,
data: build.preflight.approval.transaction.data,
value: 0n,
});
}
const signature = await wallet.signTypedData(build.typedData);
const created = await fetch("https://www.chonks.xyz/api/trait-index-bids", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...build.createRequest,
signature,
}),
}).then(readJson);
console.log(created.bid.id, created.bid.bidHash);
Response:
type CreateOfferResponse = {
bid: StoredTraitIndexBid;
};
Store both
bid.id and bid.bidHash. You need both to cancel.List Active Offers
List active Offers for one Trait Index and Trait category. Use the
traitType returned by build.| Param | Required | Values |
|---|---|---|
traitIndex |
Yes | Trait Index number, such as 57. |
traitType |
Yes | Trait type number returned by build. |
currency |
No | usdc or weth. Only one currency is allowed. |
limit |
No | 1 to 250. Defaults to 100. |
offset |
No | 0 or higher. Defaults to 0. |
curl -s "https://www.chonks.xyz/api/trait-index-bids?traitIndex=57&traitType=1"
Filter to one currency:
curl -s "https://www.chonks.xyz/api/trait-index-bids?traitIndex=57&traitType=1¤cy=usdc"
If no currency is provided, WETH and USDC Offers are returned together, sorted highest to lowest by estimated USD value using the current ETH price. If ETH price lookup fails, the API falls back to deterministic token ordering.
Response:
type ListActiveOffersResponse = {
bids: PublicTraitIndexBid[];
limit: number;
offset: number;
};
Cancel An Offer
Build the cancel message as a three-line string with
\n line breaks. Do not add a leading or trailing blank line.const bidId = 123;
const bidHash = "0xabc...";
const cancelMessage = [
"Cancel Chonks TraitIndex Offer",
"Bid id: " + bidId,
"Bid hash: " + bidHash,
].join("\n");
const signature = await wallet.signMessage({
account,
message: cancelMessage,
});
const canceled = await fetch("https://www.chonks.xyz/api/trait-index-bids/cancel", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: bidId,
bidHash,
signature,
}),
}).then(readJson);
Response:
type CancelOfferResponse = {
bid: StoredTraitIndexBid;
signedMessage: string;
};
The API checks that the bid ID and bid hash match, the signature recovers the bidder EOA, and that EOA still owns the receiving Chonk.
Error Recovery
VALIDATION_FAILED: Fix the request shape. Amounts must be strings. Currency isusdcorwethfor build.TRAIT_INDEX_NOT_FOUND: Check the target Trait Index.CHONK_NOT_FOUND: Check the exact Chonk ID.SELF_OWNED_CHONK: Exact Chonk Offers cannot target a Chonk already owned by the bidder.BIDDER_DOES_NOT_CURRENTLY_OWN_THE_RECEIVING_CHONK: Use a receiving Chonk owned by the bidder EOA.INSUFFICIENT_BALANCE: Build reports this aspreflight.balance.hasEnoughBalance = false. Create returns409if ignored. Fund the bidder EOA, then build again.INSUFFICIENT_ALLOWANCE: Build reports this aspreflight.approval.required = true. Create returns409if ignored, withdetails.permit2Spenderanddetails.marketplaceSpender. Submit the approval transaction from build, wait for confirmation, then create again.REPLACEMENT_TOO_LOW: Increase the amount to at leastdetails.minimumHumanAmount, then build again.BID_HASH_MISMATCH: Use the exactcreateRequestreturned by build and do not edit signed fields.BID_NOT_ACTIVE: The Offer was already canceled, expired, settled, or replaced.SETTLEMENT_CONTRACT_NOT_CONFIGURED: Local validation is missingTRAIT_INDEX_BIDS_SETTLEMENT_CONTRACT. Set it to0x10DDD4C86f085C911E8c9000cA9c060EA261c4E3and restart the dev server.
Bot Loop
For each configured Trait Index or exact Chonk target:
- Build the Offer.
- If
build.preflight.approval.requiredistrue, submit the returned token approval. A USDC Offer approves USDC tobuild.preflight.approval.permit2Spender. A WETH Offer approves WETH tobuild.preflight.approval.permit2Spender. If the bot offers both currencies, approve both. - Sign
typedData. This is the marketplace permission forbuild.preflight.approval.marketplaceSpender. TraitIndex Offers use0x10DDD4C86f085C911E8c9000cA9c060EA261c4E3; exact Chonk Offers use0x8927B8B87DDDa4b6f60aC1510CEE4C6b24e9DE49. - Create the Offer.
- Store the id and bidHash from the Offer's return data in your database so you can programmatically cancel it later if you choose.
- Rebuild the Offer when replacing, renewing, or changing the
amount. - Cancel by
id + bidHashwhen the bot should stop offering.
After a seller accepts an Offer, refresh your wallet's balances before renewing Other offers. This is recommended operating hygiene, not an API requirement.
Exact Chonk Offers
Exact Chonk Offers target one
chonkId. The bidder EOA offers WETH or USDC for that Chonk, and seller acceptance later transfers that Chonk to the bidder.The build/create flow is the same as TraitIndex Offers: call build, sign the returned
typedData, then POST createRequest plus signature. Exact Chonk typed data uses the SignedChonkBid witness from ChonksMarketV2UUPS.Build:
curl -s https://www.chonks.xyz/api/chonk-bids/build \
-X POST \
-H "Content-Type: application/json" \
-d '{
"bidderEOA": "0xYourBidderWallet",
"chonkId": 12345,
"currency": "usdc",
"amount": "25.50",
"expirationDays": 7
}'
Create at
POST /api/chonk-bids, list active Offers at GET /api/chonk-bids?chonkId=12345, and filter with currency=usdc or currency=weth.Cancel by signing:
Cancel Chonks exact Chonk Offer
Bid id: <id>
Bid hash: <bidHash>
Then POST
{ id, bidHash, signature } to /api/chonk-bids/cancel.Exact Chonk Offers spend through the Chonks marketplace contract,
0x8927B8B87DDDa4b6f60aC1510CEE4C6b24e9DE49. This is returned as typedData.message.spender and preflight.approval.marketplaceSpender.Replacement behavior matches TraitIndex Offers: the same bidder, currency, and exact target can have one active Offer. A replacement must be at least 5% higher than the active Offer.