Quick start
Connect to LitVM, pin the deployment, and keep all transaction decisions grounded in live RPC state.
npm install viemimport { createPublicClient, defineChain, fallback, http } from "viem";
export const litvmTestnet = defineChain({
id: 4441,
name: "LitVM LiteForge",
nativeCurrency: { name: "zkLTC", symbol: "zkLTC", decimals: 18 },
rpcUrls: {
default: { http: ["https://rpc.testnet.liteforge.zeeve.net"] },
},
blockExplorers: {
default: { name: "LiteForge Explorer", url: "https://explorer.testnet.liteforge.zeeve.net" },
},
});
// Reads: primary endpoint, then the public fallback.
export const publicClient = createPublicClient({
chain: litvmTestnet,
transport: fallback(litvmTestnet.rpcUrls.default.http.map((url) => http(url, { timeout: 15_000 })), {
retryCount: 1,
}),
pollingInterval: 1_000,
});
// For reading only. In code, load addresses from a copy of
// apps/web/features/fi/config/testnet.generated.json, not from this page.
export const contracts = {
pixel: "0xd83cb7acef921f98b6b983cbb712a583869da9eb",
pixelClassic: "0x33A32b9b2BEe864f9e42BFa39cA7BDC72f655988",
pixelMarketplace: "0xd219EfFe076B68934e212313C8d468b994d6e52E",
nusd: "0x69320A82ACCeC1b8A5EF59fd02b217e7ea4aEa19",
pump: "0x0239baEc3c695A6EA893Ad90a7608112e0580526",
fiRouter: "0x1cda4793122a223A09be54ED5f664ba222eEe042",
fiFactory: "0xc6B675d701a168789A5cc7Dd04Ef9e531886114c",
wzkLtc: "0xE93d4373CE1eDA3df6c3Ab7ed3ab07A07aA5939F",
nbtc: "0xAA612e178ff9c4Ea044eF507670C7502D58CbA46",
neth: "0x3f3B4F9DfE1AE3CDaa89788810557194c662a8Ef",
} as const;Network and contracts
These public addresses are the checked LitVM testnet deployment used by the current web application.
Core ecosystem
0xd83cb7acef921f98b6b983cbb712a583869da9eb0x33A32b9b2BEe864f9e42BFa39cA7BDC72f6559880xd219EfFe076B68934e212313C8d468b994d6e52E0x69320A82ACCeC1b8A5EF59fd02b217e7ea4aEa190xf9CCF611b7c4e642852D3d1a7A6C3883985A29620xAea1Ef82Dbe800BB3683009630813457D990a9B00x59589d9a5840D2CEF7A6e4A8cB55C258cc73E4830x0239baEc3c695A6EA893Ad90a7608112e05805260x935ec3B5ED5ef330895C87139e0a905B009187a50xdf151Ab408d099cB7Edf3B44d284011767C2475A0x6345cAFFFc5e87668508873Dd586662e5fE145a50xFi
0x1cda4793122a223A09be54ED5f664ba222eEe0420xc6B675d701a168789A5cc7Dd04Ef9e531886114c0xE93d4373CE1eDA3df6c3Ab7ed3ab07A07aA5939F0xB300Faa8AE7a36eFa17e028dE325dA0c121109040x6Cc96c044321b3205Ac108107Fc6d2d47e0B3E440x094343B925941D00424E8440C572231D6b93a7510xF909924f8BD003673d97667EA150173f87FaFA1B0xAA612e178ff9c4Ea044eF507670C7502D58CbA460x3f3B4F9DfE1AE3CDaa89788810557194c662a8EfCanonical 0xFi pools
0x7a9295742b237802571E80148C2CEc629C5D6E670x1D7e66D1301154146467dC7E3fc2916aa1a8Bc4c0x1890bb89CB57551575ccc84D0079d6A8A245285BEcosystem architecture
Each product is useful on its own. Together they form a path from creation to markets, liquidity, and persistent digital ownership.
0xPixel
A fully onchain pixel-art studio, ERC-721 collection, provenance registry, gallery, and NUSD NFT marketplace.
Packed pixel runs, artwork name, description, grid size, creator, mint time, and artwork hash live in contract storage.
The contract generates SVG and base64 JSON metadata. The collectible does not depend on an offchain image host.
The registry hashes packed pixel data with the grid size and rejects an artwork that was already registered.
Listings and escrowed offers settle in NUSD. Each sale burns 0.1% of the price and pays 0.4% to the artwork's creator; there is no platform fee. Sale proceeds and refunds are reusable credits. zkLTC only pays gas. Other marketplaces read a 1% creator royalty through ERC-2981.
import { parseAbi } from "viem";
export const pixelAbi = parseAbi([
"function checkOriginalPacked(bytes pixels, uint256 grid) view returns (bool)",
"function mintPacked(string artName, string description, uint256 grid, bytes pixels) returns (uint256)",
"function tokenPackedData(uint256 tokenId) view returns (string artName, string description, uint256 grid, bytes pixels, address creator, uint256 mintedAt, bytes32 artworkHash)",
"function tokenURI(uint256 tokenId) view returns (string)",
]);
// Grids: 8, 16, 32, 64, 128, 256.
// pixels: 6 bytes per horizontal run (x, y, width - 1, r, g, b), at most 4,096 runs.
const isOriginal = await publicClient.readContract({
address: contracts.pixel,
abi: pixelAbi,
functionName: "checkOriginalPacked",
args: [pixels, 32n],
});0xPump
A NUSD launchpad with reserved metadata, virtual constant-product pricing, explicit lifecycle states, and protected graduation.
reserveMarket(contentHash)Pay the nonrefundable creation fee.createMarket(...)Create the fixed-supply token and curve.quoteBuy / buyQuote and execute NUSD input.quoteSell / sellQuote and execute token input.NUSD
The dollar every 0xNothing product settles in: minted against native zkLTC at the DIA price, and staked for a share of protocol fees.
quoteMint / mintAtOracleNative zkLTC in, NUSD out.quoteRedeem / redeemAtOracleNUSD in, native zkLTC out. No approval needed.0xFi
The liquidity and risk layer for swaps, pools, LP farms, pooled NUSD lending and borrowing, DIA-priced synthetic assets, and a planned RWA market.
| Swap | LP fee | Router fee | Nominal total |
|---|---|---|---|
| 1 hop, TOKEN ↔ NUSD or WzkLTC | 0.5% | 0.1% | 0.6% |
| 2 hops, one end NUSD or WzkLTC | 1.0% | 0.1% | 1.1% |
| TOKEN → NUSD or WzkLTC → TOKEN | 1.0% | 0.3% | 1.3% |
| 3 hops (maximum) | 1.5% | 0.1% or 0.3% | 1.6% or 1.8% |
getAmountsOut already returns the net amount after LP fees, the router fee, and price impact. Do not subtract them twice. The router fee is charged once per route: 10 bps when the first or last token is NUSD or WzkLTC, otherwise 30 bps.
0xUniverse
A persistent game world where Web3 finance and GameFi connect virtual property, creator economies, and compliant RWA representations.
Characters, land, items, art, permissions, and provenance remain portable assets controlled by users.
Markets, crafting, quests, resource sinks, liquidity, lending, and settlement become parts of one world economy.
Digital assets can connect to compliant real-world representations through attestations, oracles, custody, and jurisdiction-aware access.
0xPixel supplies culture, 0xPump forms communities, and 0xFi supplies settlement and financial rails.
Developer guides
One guide per product in the repository, checked against the deployed manifests and ABIs. Each pairs with a prompts file for coding agents.
| Product | Flows covered | Guide | Agent prompts |
|---|---|---|---|
| 0xFi | Swap, liquidity, LP farms, NUSD lending and borrowing, nBTC and nETH synths, oracle prices | 0xfi.md | prompts/0xfi.md |
| 0xPump | Read a market, quote, buy, sell, create a token, graduate, distribute fees | 0xpump.md | prompts/0xpump.md |
| NUSD | Mint, redeem, oracle failure and emergency exit, stake, claim or restake, withdraw | nusd.md | prompts/nusd.md |
| 0xPixel | Mint artwork, read tokens and metadata, list, buy, offers, credits | 0xpixel.md | prompts/0xpixel.md |
Start with the README: network, manifest and ABI files, a shared viem setup, the write pattern, and the rules every integration follows. Integrate with the contracts over JSON-RPC and the public Goldsky subgraphs. The web app's own HTTP routes and WebSocket plumbing are internal and are not a public API.
Read the developer guide READMEIntegrate 0xFi swaps
Choose the correct execution surface, quote against current state, simulate the exact request, then ask the wallet to sign.
| User action | Contract | Method |
|---|---|---|
| Quote any route | ZeroXFiRouterV2 | getAmountsOut(amountIn, path) |
| ERC-20 to ERC-20 | ZeroXFiRouterV2 | swapExactTokensForTokens |
| zkLTC to ERC-20 | ZeroXFiRouterV2 | swapExactNativeForTokens |
| ERC-20 to zkLTC | ZeroXFiRouterV2 | swapExactTokensForNative |
| zkLTC to NUSD | OracleNUSDv2 | quoteMint + mintAtOracle |
| NUSD to zkLTC | OracleNUSDv2 | quoteRedeem + redeemAtOracle |
Path begins with WzkLTCSend the exact input as transaction value. No ERC-20 approval.Path ends with WzkLTCApprove the ERC-20 input. The router unwraps before transfer.Key rules
// fi/swap.ts
import { parseAbi, type Address, type WalletClient } from "viem";
import { publicClient } from "../chain";
import { assertChain, ensureAllowance, minOut, send } from "../tx";
import { FI } from "./addresses";
import { fiErrorsAbi } from "./errors";
const routerAbi = [
...parseAbi([
"function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)",
"function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[] amounts)",
"function swapExactNativeForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable returns (uint256[] amounts)",
"function swapExactTokensForNative(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[] amounts)",
]),
...fiErrorsAbi,
] as const;
const eq = (x: Address, y: Address) => x.toLowerCase() === y.toLowerCase();
export const deadlineIn = (minutes = 20) => BigInt(Math.floor(Date.now() / 1000) + minutes * 60);
export type Side = Address | "native";
export async function quoteBestRoute(tokenIn: Side, tokenOut: Side, amountIn: bigint) {
const a = tokenIn === "native" ? FI.wzkLtc : tokenIn;
const b = tokenOut === "native" ? FI.wzkLtc : tokenOut;
if (eq(a, b)) throw new Error("Same token: wrap or unwrap with WzkLTC deposit/withdraw instead");
const candidates: Address[][] = [[a, b]];
for (const base of [FI.nusd, FI.wzkLtc]) {
if (!eq(a, base) && !eq(b, base)) candidates.push([a, base, b]);
}
const quotes = await Promise.all(
candidates.map(async (path) => {
try {
const amounts = await publicClient.readContract({
address: FI.router, abi: routerAbi, functionName: "getAmountsOut", args: [amountIn, path],
});
return { path, out: amounts[amounts.length - 1] };
} catch {
return undefined; // PairNotFound, InvalidPath, InsufficientLiquidity
}
}),
);
let best: { path: Address[]; out: bigint } | undefined;
for (const q of quotes) if (q && (!best || q.out > best.out)) best = q;
if (!best || best.out === 0n) throw new Error("No executable 0xFi route");
return best;
}
export async function swap(
walletClient: WalletClient,
account: Address,
tokenIn: Side,
tokenOut: Side,
amountIn: bigint,
slippageBps = 50n,
) {
if (amountIn <= 0n) throw new Error("Amount must be positive");
if (slippageBps < 0n || slippageBps >= 10_000n) throw new Error("Slippage out of range");
await assertChain(walletClient);
if ((await publicClient.getChainId()) !== 4441) throw new Error("RPC is not chain 4441");
const nativeNusd =
(tokenIn === "native" && tokenOut !== "native" && eq(tokenOut, FI.nusd)) ||
(tokenOut === "native" && tokenIn !== "native" && eq(tokenIn, FI.nusd));
if (nativeNusd) throw new Error("Use NUSD mintAtOracle / redeemAtOracle for zkLTC <-> NUSD");
// Fresh quote right before sending.
const best = await quoteBestRoute(tokenIn, tokenOut, amountIn);
const amountOutMin = minOut(best.out, slippageBps);
if (amountOutMin === 0n) throw new Error("Minimum output rounds to zero");
if (tokenIn === "native") {
return send(walletClient, {
account, address: FI.router, abi: routerAbi, functionName: "swapExactNativeForTokens",
args: [amountOutMin, best.path, account, deadlineIn()], value: amountIn,
});
}
await ensureAllowance(walletClient, account, tokenIn, FI.router, amountIn);
return send(walletClient, {
account, address: FI.router, abi: routerAbi,
functionName: tokenOut === "native" ? "swapExactTokensForNative" : "swapExactTokensForTokens",
args: [amountIn, amountOutMin, best.path, account, deadlineIn()],
});
}The snippet uses chain.ts and tx.ts from the README and fi/addresses.ts from the 0xFi guide. For exact zkLTC and NUSD conversion, see NUSD integration.
Integrate 0xPump
Read the market over RPC, quote on the curve, then buy or sell with a minimum output and a deadline.
| User action | Contract | Method |
|---|---|---|
| Read a market | ZeroXPumpV2 | markets, marketParams, status, curveProgressBps |
| Quote | ZeroXPumpV2 | quoteBuy / quoteSell |
| Buy, NUSD to token | ZeroXPumpV2 | buy(token, maxNusdIn, minTokenOut, deadline) |
| Sell, token to NUSD | ZeroXPumpV2 | sell(token, tokenIn, minNusdOut, deadline) |
| Create a token | ZeroXPumpV2 | reserveMarket(contentHash), then createMarket(...) |
| Graduate a READY market | PumpGraduationController | graduateReady(token) |
| Distribute protocol fees | ZeroXPumpV2 | distributeFees() |
Key rules
// pump/buy.ts
import { erc20Abi, type Address, type WalletClient } from "viem";
import { publicClient } from "../chain";
import { assertChain, ensureAllowance, minOut, send } from "../tx";
import { NUSD, PUMP } from "./addresses";
import { pumpAbi } from "./abi";
const nowSec = () => BigInt(Math.floor(Date.now() / 1000));
export async function buyOnCurve(
walletClient: WalletClient,
account: Address,
token: Address,
maxNusdIn: bigint, // parseUnits(input, 18)
slippageBps = 100n, // 1%; the web app offers 50 / 100 / 200
) {
await assertChain(walletClient);
if (maxNusdIn <= 0n) throw new Error("Enter an amount");
const [market, paused] = await Promise.all([
publicClient.readContract({ address: PUMP, abi: pumpAbi, functionName: "markets", args: [token] }),
publicClient.readContract({ address: PUMP, abi: pumpAbi, functionName: "paused" }),
]);
if (paused) throw new Error("0xPump is paused; buys are disabled");
if (market[7] !== 1) throw new Error("Buys are open only while the market is TRADING");
const [tokenOut, , userNusdAmount, feeNusd, readyAfter] = await publicClient.readContract({
address: PUMP, abi: pumpAbi, functionName: "quoteBuy", args: [token, maxNusdIn],
});
if (tokenOut === 0n || userNusdAmount === 0n) throw new Error("Amount too small");
const minTokenOut = minOut(tokenOut, slippageBps);
const balance = await publicClient.readContract({
address: NUSD, abi: erc20Abi, functionName: "balanceOf", args: [account],
});
if (balance < userNusdAmount) throw new Error("Not enough NUSD");
await ensureAllowance(walletClient, account, NUSD, PUMP, userNusdAmount);
const deadline = nowSec() + 20n * 60n;
const { hash, result, receipt } = await send(walletClient, {
account, address: PUMP, abi: pumpAbi, functionName: "buy",
args: [token, userNusdAmount, minTokenOut, deadline],
});
return { hash, receipt, simulated: result, feeNusd, readyAfter };
}Integrate NUSD
One contract, OracleNUSDv2, is the ERC-20, the mint and redeem desk, and the zkLTC reserve. RewardStaking runs Earn.
| User action | Contract | Method |
|---|---|---|
| Mint, zkLTC to NUSD | OracleNUSDv2 | quoteMint + mintAtOracle with value |
| Redeem, NUSD to zkLTC | OracleNUSDv2 | quoteRedeem + redeemAtOracle |
| Report an oracle failure | OracleNUSDv2 | reportOracleFailure() |
| Emergency exit | OracleNUSDv2 | emergencyRedeem(amountNusd, minCollateralOutWei, recipient) |
| Stake | RewardStaking | stake(amount, lockDuration) |
| Claim or restake | RewardStaking | claim(recipient) / claimAndStake(lockDuration) |
| Withdraw a matured position | RewardStaking | withdraw(positionId) |
Key rules
import type { Address, WalletClient } from "viem";
import { publicClient } from "./chain";
import { assertChain, confirm, minOut } from "./tx";
import { NUSD, nusdAbi, oracleRouterAbi } from "./nusd";
export async function mintNusd(
walletClient: WalletClient,
account: Address,
collateralWei: bigint, // parseEther("1") for 1 zkLTC
slippageBps = 50n,
) {
if (collateralWei <= 0n) throw new Error("Amount must be greater than 0");
await assertChain(walletClient);
const [paused, oracle, balance] = await Promise.all([
publicClient.readContract({ address: NUSD, abi: nusdAbi, functionName: "mintPaused" }),
publicClient.readContract({ address: NUSD, abi: nusdAbi, functionName: "oracle" }),
publicClient.getBalance({ address: account }),
]);
if (paused) throw new Error("Minting is paused");
if (balance < collateralWei) throw new Error("Not enough zkLTC (keep some for gas)");
const fresh = await publicClient.readContract({ address: oracle, abi: oracleRouterAbi, functionName: "isFresh" });
if (!fresh) throw new Error("Oracle price is stale; minting is unavailable");
const quote = await publicClient.readContract({
address: NUSD, abi: nusdAbi, functionName: "quoteMint", args: [collateralWei],
});
if (quote === 0n) throw new Error("Amount too small");
const minNusdOut = minOut(quote, slippageBps);
const { request, result } = await publicClient.simulateContract({
account, address: NUSD, abi: nusdAbi, functionName: "mintAtOracle",
args: [minNusdOut, account], value: collateralWei,
});
await assertChain(walletClient);
const hash = await walletClient.writeContract(request);
const receipt = await confirm(hash); // throws unless status === "success"
return { hash, receipt, expectedNusd: result };
}Integrate 0xPixel
Mint original artwork to 0xPixel V2, read it back from the contract, and trade it on the NUSD NFT marketplace.
| User action | Contract | Method |
|---|---|---|
| Mint an artwork | ZeroXPixelV2 | checkOriginalPacked, then mintPacked |
| Read tokens | ZeroXPixelV2 | tokenPackedData, tokenURI, tokensOfOwner, royaltyInfo |
| List or change a price | NusdNftMarketplaceV2 | list / updateListingPrice |
| Buy a listing | NusdNftMarketplaceV2 | buy(listingId, expectedPrice, maxWalletPayment) |
| Make an offer | NusdNftMarketplaceV2 | makeOffer(collection, tokenId, price, expiresAt, maxWalletPayment) |
| Accept an offer | NusdNftMarketplaceV2 | acceptOffer(offerId, minimumSellerProceeds) |
| Cancel, expire, withdraw | NusdNftMarketplaceV2 | cancelListing, cancelOffer, expireOffer, withdrawNusd |
Key rules
import { publicClient, connectedAccount, MARKET } from "./liteforge";
import { send } from "./send";
import { marketAbi, checkMarket, ensureNusdAllowance, walletPayment } from "./market";
export async function buyListing(listingId: bigint) {
const account = await connectedAccount();
await checkMarket(true);
const [, , price, seller, active] = await publicClient.readContract({
address: MARKET, abi: marketAbi, functionName: "listings", args: [listingId],
});
if (!active) throw new Error("Listing is no longer active");
if (seller.toLowerCase() === account.toLowerCase()) throw new Error("You cannot buy your own listing");
const credit = await publicClient.readContract({ address: MARKET, abi: marketAbi, functionName: "credits", args: [account] });
const pay = walletPayment(price, credit);
await ensureNusdAllowance(account, pay); // also checks wallet balance
return send({ account, address: MARKET, abi: marketAbi, functionName: "buy", args: [listingId, price, pay] });
}Vibe-code prompts
Paste one prompt into a coding agent such as Claude Code, Cursor, or Codex, in your own project with this repository available to read. Each prompt names the network, the manifest and ABI files to load, and the rules to follow.
0xFi: swap widget
Copied from prompts/0xfi.md, 1. Swap widget. The same file has prompts for the other flows.
You are adding a 0xFi swap widget to my app.
STACK
- Next.js (App Router) + TypeScript (strict) + viem 2 + wagmi 2. <my wallet connector, e.g. RainbowKit / ConnectKit / injected>.
- All token amounts are bigint in base units. Never use Number, parseFloat, toFixed or `* 10 **` on amounts. Parse with parseUnits, format with formatUnits, and read decimals() live for any token that is not NUSD/WzkLTC (both 18).
CHAIN
- LitVM LiteForge testnet: id 4441, native currency zkLTC (18 decimals).
- RPC HTTP: https://rpc.testnet.liteforge.zeeve.net.
- RPC WS: wss://rpc.testnet.liteforge.zeeve.net/ws.
- Explorer: https://explorer.testnet.liteforge.zeeve.net.
- Source: 0xNothing-zkLTC-Testnet/apps/web/lib/publicConfig.ts:6-9. Define the chain with viem defineChain and a fallback() transport.
ADDRESSES (load, do not hard-code)
- Copy 0xNothing-zkLTC-Testnet/apps/web/features/fi/config/testnet.generated.json into the project as a JSON module and pass every address through getAddress(). Keys used here:
- dexRouter -> ZeroXFiRouterV2
- dexFactory -> ZeroXFiFactoryV2
- nusd -> NUSD (base asset, 18 decimals)
- wzkltc -> WzkLTC (base asset, 18 decimals; wrapped native zkLTC)
- Do NOT use 0xFi/contracts/deployments/latest.json: it is the retired v1 suite.
- On load, assert publicClient.getChainId() === 4441.
ABI (copy these fragments; they match apps/web/features/fi/lib/abis/dex.ts:13-92 and 0xFi/contracts/src/amm/ZeroXFiRouterV2.sol)
Router:
function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)
function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[] amounts)
function swapExactNativeForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable returns (uint256[] amounts)
function swapExactTokensForNative(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[] amounts)
function LP_FEE_BPS() view returns (uint256)
function DIRECT_FEE_BPS() view returns (uint256)
function CROSS_FEE_BPS() view returns (uint256)
error DeadlineExpired()
error FeeOnTransferUnsupported()
error InsufficientAmount()
error InsufficientLiquidity()
error InsufficientOutputAmount()
error InvalidPath()
error InvalidRecipient()
error PairNotFound()
error UnauthorizedNativeSender()
Factory:
function swapsPaused() view returns (bool)
error SwapsPaused()
error Forbidden()
ERC-20:
function decimals() view returns (uint8)
function symbol() view returns (string)
function balanceOf(address) view returns (uint256)
function allowance(address owner, address spender) view returns (uint256)
function approve(address spender, uint256 amount) returns (bool)
FACTS YOU MUST RESPECT
- Every pool contains NUSD or WzkLTC. A path has 2 to 4 entries (max 3 hops), no zero address, no duplicates; each hop's pair must exist.
- getAmountsOut(...)[last] is the amount delivered AFTER all fees. Fees: 50 bps LP per hop, plus ONE router fee per route: 10 bps if path[0] or path[last] is NUSD or WzkLTC, otherwise 30 bps. No other surcharge. (ZeroXFiRouterV2.sol:45-48,337,359-365)
- Native zkLTC in: swapExactNativeForTokens with value = amountIn, path[0] must be WzkLTC, no approval.
- Native zkLTC out: swapExactTokensForNative, path[last] must be WzkLTC, approve the ERC-20 input to the router.
- ERC-20 in: approve the ROUTER for exactly amountIn. Fee-on-transfer tokens are rejected (FeeOnTransferUnsupported).
- Never send zkLTC to the router directly (UnauthorizedNativeSender).
- WzkLTC <-> native zkLTC is wrap/unwrap (WzkLTC.deposit() payable / withdraw(uint256)), not a swap.
- Exact zkLTC <-> NUSD is NOT an AMM trade: route it to NUSD mintAtOracle / redeemAtOracle (see docs/developers/nusd.md). You may leave it out of this widget and link to the NUSD panel.
FLOW
1. Require a connected account. If the wallet chain id !== 4441, show "Switch to LiteForge" (switchChain, addChain if unknown) and disable the action.
2. Read factory.swapsPaused(); if true, show "Swaps paused" and disable.
3. Map "native" to WzkLTC for path building. Build candidates: [in, out]; [in, NUSD, out] if neither end is NUSD; [in, WzkLTC, out] if neither end is WzkLTC.
4. Debounce input (~300 ms). Quote each candidate with getAmountsOut; a revert (PairNotFound, InvalidPath, InsufficientLiquidity) just drops that candidate. Pick the largest last amount. If none: "No route".
5. minOut = quoted * (10000n - slippageBps) / 10000n. slippageBps default 50n, user-editable 1..500. Reject minOut === 0n.
6. deadline = BigInt(Math.floor(Date.now() / 1000) + 20 * 60).
7. If the input is an ERC-20: read allowance(account, router). If < amountIn, simulate approve(router, amountIn), write it, wait for the receipt, require status "success", then continue.
8. Re-quote the chosen path right before sending and recompute minOut.
9. simulateContract with the exact account, args and value (value only for the native-in function). Show the decoded error name on revert and send nothing.
10. Check chain id 4441 again, writeContract(request), waitForTransactionReceipt({ hash }). Anything but receipt.status === "success" is a failure.
11. Refresh balances and the quote.
UI STATES
not connected | wrong network | swaps paused | loading quote | no route | quote ready (show route, expected out, minimum received, fee breakdown: LP 0.5% per hop + router 0.1% or 0.3%, slippage) | insufficient balance (leave gas when paying native) | needs approval | approving | awaiting signature | pending (link https://explorer.testnet.liteforge.zeeve.net/tx/<hash>) | success | reverted / error with the decoded error name. Disable the button while a transaction is in flight (no double submit).
DO NOT
- Do not call any /api/* or /0xFi/api/* route of the 0xNothing web app.
- Do not use the subgraph to size a transaction or compute minOut.
- Do not use an unlimited approval.
- Do not add a private key, mnemonic or API key anywhere.
ACCEPTANCE CHECKLIST (verify each and list the file:line that satisfies it)
[ ] Addresses come from the copied testnet.generated.json through getAddress; nothing is hard-coded; publicClient chain id 4441 is asserted.
[ ] Candidates include [in,out], [in,NUSD,out] and [in,WzkLTC,out]; failed quotes are skipped, not fatal.
[ ] Every write: chain check, fresh quote, non-zero minOut, deadline, allowance branch with receipt wait, simulateContract with the same account/args/value, writeContract, receipt status check.
[ ] Native in uses value and no approval; native out ends the path in WzkLTC.
[ ] All amounts are bigint; a grep for Number(, parseFloat, toFixed on amounts finds nothing.
[ ] Errors show the decoded custom error name.
[ ] A unit test covers minOut math (0 and 10000 bps bounds), best-route selection with a rejected candidate, and the native/ERC-20 function choice.
Cite the source file and line for every address, function signature and fee value you used.0xPump: buy and sell widget
Copied from prompts/0xpump.md, 2. Buy and sell widget. The same file has prompts for the other flows.
You are building a 0xPump buy/sell panel in a Next.js + TypeScript app with viem and wagmi. 0xPump (ZeroXPumpV2) sells a token along a NUSD bonding curve on the LitVM LiteForge testnet.
Network: viem defineChain with id 4441, name "LitVM LiteForge", native currency zkLTC (18 decimals), HTTP RPC https://rpc.testnet.liteforge.zeeve.net (apps/web/lib/publicConfig.ts:6), explorer https://explorer.testnet.liteforge.zeeve.net.
Addresses: copy apps/web/features/fi/config/testnet.generated.json and read `pump` (the launchpad and the ERC-20 spender) and `nusd` (NUSD, 18 decimals). Use getAddress. No other hard-coded addresses.
ABI: copy from docs/developers/0xpump.md section "ABI": markets, marketParams, paused, quoteBuy, quoteSell, buy, sell, the TokenTraded/TokenReadyForGraduation/TokenCurveReopened events, all ZeroXPumpV2 custom errors (ContractPaused, DeadlineExpired, MarketNotFound, MarketNotTrading, InvalidAmount, SlippageExceeded, AssetTransferMismatch, ...), and pumpTokenAbi (balanceOf, allowance, approve, decimals, plus ERC20InsufficientBalance/ERC20InsufficientAllowance/ERC20InvalidAddress). For NUSD use viem's erc20Abi. Exact signatures:
- quoteBuy(address token,uint256 maxNusdIn) view returns (uint256 tokenOut,uint256 curveNusdAmount,uint256 userNusdAmount,uint256 feeNusd,bool readyAfter)
- quoteSell(address token,uint256 tokenIn) view returns (uint256 curveNusdAmount,uint256 userNusdAmount,uint256 feeNusd)
- buy(address token,uint256 maxNusdIn,uint256 minTokenOut,uint256 deadline) returns (uint256 tokenOut,uint256 userNusdAmount)
- sell(address token,uint256 tokenIn,uint256 minNusdOut,uint256 deadline) returns (uint256 userNusdAmount)
Neither function is payable.
Rules for BUY (NUSD -> token):
1. Require the wallet chain id to be 4441; offer switchChain (and addChain if unknown) otherwise. Re-check right before every write.
2. Read markets(token) (lifecycle at index 7) and paused(). Buy only when lifecycle === 1 (TRADING) and not paused.
3. Parse the input with parseUnits(input, 18). Read quoteBuy(token, amount) fresh (debounce ~300 ms while typing, and again right before sending). Reject tokenOut == 0.
4. Show: tokens out (tokenOut), NUSD actually spent (userNusdAmount, which can be less than the input near the graduation target: say so), fee (feeNusd), and a "this buy completes the curve" note when readyAfter is true.
5. minTokenOut = tokenOut * (10000 - slippageBps) / 10000 with bigint math. Slippage choices 50 / 100 / 200 bps, default 100.
6. Check NUSD balanceOf >= userNusdAmount. If NUSD allowance(account, pump) < userNusdAmount: simulate and send approve(pump, userNusdAmount) (exact amount, not unlimited), wait for the receipt, require status "success".
7. deadline = BigInt(Math.floor(Date.now()/1000)) + 1200n (20 minutes).
8. simulateContract buy(token, userNusdAmount, minTokenOut, deadline) with the connected account, then writeContract(request), then waitForTransactionReceipt and require status "success".
Rules for SELL (token -> NUSD):
1. Same chain check. Sell when lifecycle is 1 (TRADING) or 2 (READY). Selling works while paused. A sell from READY reopens the curve: tell the user.
2. quoteSell(token, tokenIn) fresh; output = userNusdAmount, fee = feeNusd; reject userNusdAmount == 0.
3. minNusdOut from userNusdAmount with the same slippage math.
4. Check token balanceOf >= tokenIn; if token allowance(account, pump) < tokenIn approve exactly tokenIn to pump and wait for a successful receipt.
5. simulate sell(token, tokenIn, minNusdOut, deadline), write, wait, check status.
After any confirmed trade: re-read market, balances and allowance over RPC. Never use subgraph data or event payloads to size a trade.
Per-market fee: read marketParams(token).tradeFeeBps; do not hard-code 10 bps.
Errors: decode viem ContractFunctionRevertedError and map names to messages: ContractPaused, DeadlineExpired (re-quote), MarketNotTrading (refresh status; market became READY or GRADUATED), InvalidAmount, SlippageExceeded (re-quote, show the new price), AssetTransferMismatch, ERC-20 balance/allowance errors, user rejection (UserRejectedRequestError: return to idle, no error banner).
UI states: disconnected, wrong network, market not tradable (READY: only sell; GRADUATED: link to 0xFi pool markets(token).pool), quoting, insufficient balance, needs approval, approving (pending hash with explorer link), ready to submit, simulating, awaiting signature, pending, confirmed (hash + amounts), failed (decoded reason). Disable the submit button while any step is in flight; block double submits.
Acceptance checklist (report file:line for each):
- [ ] Chain id 4441 checked before approve and before buy/sell.
- [ ] Addresses only from the copied manifest via getAddress.
- [ ] Quote is re-read right before sending; minimum output is never 0.
- [ ] Buy passes the quoted userNusdAmount as maxNusdIn and approves exactly that.
- [ ] Deadline argument present on both buy and sell.
- [ ] Approval receipt awaited before the dependent call.
- [ ] simulateContract uses the same account and args as writeContract.
- [ ] Receipt status checked; failures show the decoded error name.
- [ ] All amounts bigint; no Number/parseFloat/toFixed on amounts.
- [ ] No /api/* calls, no secrets.
- [ ] Unit test for the minOut function (0, 50, 100, 200, 10000 bps) and a build that passes.NUSD: mint and redeem panel
Copied from prompts/nusd.md, 1. Mint and redeem panel. The same file has prompts for the other flows.
You are adding an NUSD mint and redeem panel to my app.
STACK
- Next.js (App Router) + TypeScript (strict) + viem 2 + wagmi 2. <my wallet connector, e.g. RainbowKit / ConnectKit / injected>.
- All token amounts are bigint in base units (18 decimals). Never use Number, parseFloat, toFixed or `* 10 **` on amounts. Parse with parseUnits/parseEther, format with formatUnits.
CHAIN
- LitVM LiteForge testnet: id 4441, native currency zkLTC (18 decimals).
- RPC HTTP: https://rpc.testnet.liteforge.zeeve.net.
- RPC WS: wss://rpc.testnet.liteforge.zeeve.net/ws.
- Source: 0xNothing-zkLTC-Testnet/apps/web/lib/publicConfig.ts:6-8. Define the chain with viem defineChain and a fallback() transport.
ADDRESSES (load, do not hard-code)
- Read 0xNothing-zkLTC-Testnet/contracts/deployments/v3-core.json at build time (copy it into the project as a JSON module). Assert chainId === 4441. Use keys:
- nusd -> NUSD token and mint/redeem contract (OracleNUSDv2)
- ltcOracle -> LTC/USD OracleRouter
- On load, read nusd.oracle() and fail closed if it differs from ltcOracle.
ABI (copy these fragments; they match apps/web/features/pump/abis.ts:68-96 and the Solidity in contracts/src/nusd/OracleNUSDv2.sol and contracts/src/nusd/DIAOracleAdapter.sol)
NUSD:
function balanceOf(address account) view returns (uint256)
function totalSupply() view returns (uint256)
function oracle() view returns (address)
function mintPaused() view returns (bool)
function backingRatioWad() view returns (uint256)
function supplyCeilingNusd() view returns (uint256)
function quoteMint(uint256 collateralWei) view returns (uint256 amountNusd)
function mintAtOracle(uint256 minNusdOut, address recipient) payable returns (uint256 amountNusd)
function quoteRedeem(uint256 amountNusd) view returns (uint256 collateralOutWei)
function redeemAtOracle(uint256 amountNusd, uint256 minCollateralOutWei, address recipient) returns (uint256 collateralOutWei)
event MintedAtOracle(address indexed payer, address indexed recipient, uint256 collateralInWei, uint256 amountNusd)
event RedeemedAtOracle(address indexed account, address indexed recipient, uint256 amountNusd, uint256 collateralOutWei)
error MintPaused()
error InvalidAmount()
error InvalidRecipient()
error SlippageExceeded(uint256 minimumAmount, uint256 actualAmount)
error SupplyCeilingExceeded(uint256 requestedAmountNusd, uint256 availableAmountNusd)
error ERC20InsufficientBalance(address account, uint256 balance, uint256 requiredAmount)
error StaleOraclePrice(uint256 updatedAt, uint256 currentTimestamp)
error InvalidOracleAnswer()
error IncompleteOracleRound()
error FutureOracleTimestamp()
OracleRouter:
function readPriceWad() view returns (uint256 priceWad, uint256 updatedAt, uint80 roundId)
function isFresh() view returns (bool)
FACTS YOU MUST RESPECT
- Mint pays native zkLTC as `value` to mintAtOracle. No ERC-20 approval. NEVER send a plain zkLTC transfer to the NUSD address: receive() books it as a reserve donation and mints nothing.
- Redeem needs NO approval: redeemAtOracle burns from msg.sender.
- Redeem pays min(oracle price, pro-rata share of the reserve). Backing can be below 1 (it was ~0.91 on 2026-11-05 and moves with the LTC price), so show the quoted zkLTC, never "1 NUSD = 1 USD guaranteed".
- Admins cannot pause redeem, but quoteRedeem and redeemAtOracle REVERT while the DIA price is stale (older than 90 minutes). Catch that revert (StaleOraclePrice or the other oracle errors) and show "Price feed is stale - use the emergency exit" with a link to the emergency panel.
- Mint can be paused (mintPaused()). Mint also reverts on a stale price and above the supply ceiling.
- Prices are WAD (1e18 = 1 USD). Fees are 0 today but read them from the quote, do not assume.
FLOW: MINT
1. Require a connected account. If chainId !== 4441, show a "Switch to LiteForge" button (switchChain, then addChain if unknown) and disable the action.
2. Read mintPaused(), oracle(), router.isFresh(), the account's zkLTC balance. Block with a clear message if paused or not fresh, or if amount > balance (leave gas).
3. Debounce input (~300 ms), then read quoteMint(amountWei). Re-read the quote right before sending.
4. minNusdOut = quote * (10000n - slippageBps) / 10000n, slippageBps default 50n (0.5%), user-editable 1..500. Never 0.
5. simulateContract({ account, address: nusd, abi, functionName: "mintAtOracle", args: [minNusdOut, account], value: amountWei }).
6. Check the chain id again, writeContract(request), then waitForTransactionReceipt({ hash }). Treat anything but receipt.status === "success" as failure.
7. Decode MintedAtOracle from the receipt, show the minted amount, refresh balances.
FLOW: REDEEM
1. Same chain guard. Read NUSD balanceOf(account); block if amount > balance.
2. Read quoteRedeem(amountNusd). If it reverts with an oracle error, switch to the stale-price state. If it returns 0, show "amount too small".
3. minCollateralOutWei = quote * (10000n - slippageBps) / 10000n.
4. simulateContract redeemAtOracle(amountNusd, minCollateralOutWei, account) with the exact account. No value, no approval.
5. writeContract, waitForTransactionReceipt, require status "success", decode RedeemedAtOracle, refresh.
UI STATES
not connected | wrong network | loading quote | quote ready (show rate, min received, slippage, backing ratio) | mint paused | price stale | insufficient balance | above supply ceiling (show available from SupplyCeilingExceeded) | awaiting signature | pending (link https://explorer.testnet.liteforge.zeeve.net/tx/<hash>) | success | reverted / error with the decoded error name. Disable the button while a transaction is in flight (no double submit).
DO NOT
- Do not call any /api/* or /0xFi/api/* route of the 0xNothing web app.
- Do not use a subgraph to size a transaction.
- Do not add a private key, mnemonic or API key anywhere.
ACCEPTANCE CHECKLIST (verify each and list the file:line that satisfies it)
[ ] Addresses come from v3-core.json; chainId and nusd.oracle() === ltcOracle are asserted.
[ ] Every write is preceded by a chain-id 4441 check, a fresh quote, a non-zero min-out, and simulateContract with the same account/args/value.
[ ] Mint uses mintAtOracle with value; there is no plain zkLTC transfer anywhere.
[ ] Redeem has no approve call.
[ ] Stale-oracle revert on redeem is caught and routed to the emergency exit.
[ ] receipt.status === "success" is required before showing success.
[ ] All amounts are bigint; a grep for Number(, parseFloat, toFixed on amounts finds nothing.
[ ] Errors show the decoded custom error name.
[ ] A unit test covers minOut math (including 0 and 10000 bps bounds) and the stale-oracle branch with a mocked revert.
Cite the source file and line for every address, function signature and decimals value you used.0xPixel: mint studio
Copied from prompts/0xpixel.md, 1. Mint studio. The same file has prompts for the other flows.
Build a "0xPixel mint studio" page in my Next.js (App Router) + TypeScript app using viem and wagmi. Users draw pixel art and mint it to the 0xPixel V2 NFT contract on LitVM LiteForge testnet.
CHAIN
- Define the chain with viem defineChain: id 4441, name "LitVM LiteForge", native currency zkLTC (18 decimals).
- RPC http: "https://rpc.testnet.liteforge.zeeve.net". WebSocket: "wss://rpc.testnet.liteforge.zeeve.net/ws". Explorer: "https://explorer.testnet.liteforge.zeeve.net". Multicall3: 0xca11bde05977b3631167028862be2a173976ca11. These come from 0xNothing-zkLTC-Testnet/apps/web/lib/publicConfig.ts.
ADDRESSES
- Load the collection address from 0xNothing-zkLTC-Testnet/contracts/deployments/v3-core.json key "pixelV2" (check "chainId" is 4441). Copy that JSON into my repo as config/0xnothing/v3-core.json and import it. Do not hard-code the address in components.
ABI (copy exactly; source 0xNothing-zkLTC-Testnet/contracts/src/0xpixel/ZeroXPixelV2.sol)
- function mintPacked(string artName, string description, uint256 grid, bytes pixels) returns (uint256)
- function checkOriginalPacked(bytes pixels, uint256 grid) view returns (bool)
- event Minted(address indexed creator, uint256 indexed tokenId, string name)
- errors: InvalidName(), InvalidDescription(), InvalidUTF8(), InvalidGrid(), InvalidPixels(), PixelOutOfBounds(), NonCanonicalPixels(), ArtworkAlreadyExists(), ReentrantCall()
ENCODING (the contract rejects anything else)
- Grid sizes: 8, 16, 32, 64, 128, 256. Cells are "#rrggbb" or transparent.
- pixels = raw bytes, 6 per horizontal run: [x, y, countMinusOne, r, g, b]. Width = countMinusOne + 1.
- Row-major: rows by y ascending, runs by x ascending, no overlap. Adjacent cells of the same colour in a row MUST be merged into one run. Transparent cells produce no bytes.
- At least 1 run, at most 4,096 runs (24,576 bytes). Count runs live and block minting above the limit.
- Write packPixels(rows, grid): Hex as a pure function with unit tests: merging, transparency gaps, a full 256-wide row (countMinusOne = 255), empty canvas throws, 4,097 runs throws.
FLOW for "Mint"
1. Require a connected wallet. If chainId !== 4441, show "Switch to LiteForge" and call switchChain; do not continue until it is 4441.
2. Validate with TextEncoder: trimmed name 1-64 bytes, description 0-1,024 bytes.
3. Pack. Read checkOriginalPacked(pixels, BigInt(grid)) fresh (debounce while drawing, and again right before signing). false => "Already minted". A revert => encoding bug, show it.
4. simulateContract mintPacked with the connected account and no value field (mintPacked is nonpayable, so the mint sends 0 zkLTC; viem's types reject `value` on nonpayable functions). No token approvals exist for this flow.
5. estimateContractGas, gas = est + est/5n + 100_000n; read latest block and refuse if gas > 90% of block.gasLimit ("Too detailed for one block").
6. writeContract with the simulated request and that gas. waitForTransactionReceipt. Require receipt.status === "success". Parse the Minted log for tokenId (bigint) and link to the explorer tx.
7. Decode reverts with viem's ContractFunctionRevertedError and map each error name to a plain message.
UI STATES
- Canvas editor with grid-size picker, colour picker, eraser (transparent), clear; live run counter "N / 4,096".
- Checking originality, original, already minted, encoding error.
- Wrong network, not connected, simulating, waiting for wallet signature, pending (tx hash link), success (token id + link), failed (decoded error), user rejected.
- Disable the Mint button while any request is in flight; prevent double submit.
ACCEPTANCE CHECKLIST
- [ ] No address literal in components; it comes from the copied v3-core.json.
- [ ] Every amount, grid argument and token id is a bigint at the contract boundary.
- [ ] packPixels unit tests pass, including merge and 256-wide-row cases.
- [ ] Chain id 4441 is checked before simulate and before write.
- [ ] Originality is re-read right before signing; simulate runs with the exact account and no value field (0 zkLTC).
- [ ] Receipt status is checked; success is only shown after status === "success".
- [ ] All 9 contract errors decode to readable messages.
- [ ] No private keys, API keys or .env secrets anywhere; typecheck and lint pass.Indexed data
Goldsky powers discovery, history, charts, and activity. RPC remains authoritative for balances, quotes, allowances, and transaction preflight.
https://api.goldsky.com/api/public/project_cmqmpust19i8v01t595z8hpq4/subgraphs/pixel-nusd-market/1.1.0/gnListings, offers, activity, and market totals, plus mints from both collections from block 61141428. Re-read a listing over RPC before buying.
https://api.goldsky.com/api/public/project_cmqmpust19i8v01t595z8hpq4/subgraphs/zeroxpump-testnet/1.0.0/gnMarkets, trades, candles, holders, and totals. Check _meta.block.number for lag.
https://api.goldsky.com/api/public/project_cmqmpust19i8v01t595z8hpq4/subgraphs/zeroxfi-testnet/1.0.0/gnPools, swaps, liquidity events, candles, and gauge and lending activity. The mappings match NUSD against the retired v1 address, so check NUSD-denominated volume fields against RPC; if they read 0, derive volume from the token amounts.
No public subgraphRead mint, redeem, and RewardStaking events with getLogs from block 61141328, in bounded block ranges.
const INDEXERS = {
pixelMarket: "https://api.goldsky.com/api/public/project_cmqmpust19i8v01t595z8hpq4/subgraphs/pixel-nusd-market/1.1.0/gn",
pump: "https://api.goldsky.com/api/public/project_cmqmpust19i8v01t595z8hpq4/subgraphs/zeroxpump-testnet/1.0.0/gn",
fi: "https://api.goldsky.com/api/public/project_cmqmpust19i8v01t595z8hpq4/subgraphs/zeroxfi-testnet/1.0.0/gn",
} as const;
export async function queryIndexer<T>(endpoint: string, query: string, variables = {}) {
const response = await fetch(endpoint, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ query, variables }),
});
if (!response.ok) throw new Error("Indexer HTTP " + response.status);
const payload = await response.json();
if (payload.errors?.length) throw new Error(payload.errors[0].message);
return payload.data as T;
}Testnet safety
This deployment is for integration testing. A successful transaction or build does not represent a security audit.