Quick start
Connect to LitVM, pin the deployment, and keep all transaction decisions grounded in live RPC state.
npm install viemimport { createPublicClient, defineChain, http } from "viem";
export const litvmTestnet = defineChain({
id: 4441,
name: "LitVM LiteForge",
nativeCurrency: { name: "zkLTC", symbol: "zkLTC", decimals: 18 },
rpcUrls: {
default: { http: ["https://liteforge.rpc.caldera.xyz/infra-partner-http"] },
},
blockExplorers: {
default: { name: "LiteForge Explorer", url: "https://liteforge.explorer.caldera.xyz" },
},
});
export const publicClient = createPublicClient({
chain: litvmTestnet,
transport: http(),
});
export const contracts = {
pixel: "0x33A32b9b2BEe864f9e42BFa39cA7BDC72f655988",
pixelMarketplace: "0x13337cadA78d53C90E3c0EcE44C17c467C1a86F4",
nusd: "0x5317e21aba902c6c7087a84457bc02fFe99604d1",
pump: "0x4a0eaf310e3659aa9b360fd44e90208c31dbe0e2",
fiRouter: "0x7b17035A4aC3A32f2A18a16e2F43A5f8C66275b0",
fiFactory: "0xe33fE815c2e12DC83b69397CeD12b09849Fa9C0D",
wzkLtc: "0xE93d4373CE1eDA3df6c3Ab7ed3ab07A07aA5939F",
nbtc: "0x0CBc1e968db77885DCa648D7bD0e80fCc94cB9Cf",
neth: "0xD504bB9430d94ccFF87e12e94fd6C0074D0E8aCb",
} as const;Network and contracts
These public addresses are the checked LitVM testnet deployment used by the current web application.
Core ecosystem
0x33A32b9b2BEe864f9e42BFa39cA7BDC72f6559880x13337cadA78d53C90E3c0EcE44C17c467C1a86F40x5317e21aba902c6c7087a84457bc02fFe99604d10x4a0eaf310e3659aa9b360fd44e90208c31dbe0e20x2112Cea76b76817626cA58B205b3dC5560F058570xFi
0x7b17035A4aC3A32f2A18a16e2F43A5f8C66275b00xe33fE815c2e12DC83b69397CeD12b09849Fa9C0D0xE93d4373CE1eDA3df6c3Ab7ed3ab07A07aA5939F0x36F425fddc59d281c6ddEaDAc34B32E6f039EB130x7CB638F8e10f1bd200A3c5C3fD014C3FD97BA9140x0CBc1e968db77885DCa648D7bD0e80fCc94cB9Cf0xD504bB9430d94ccFF87e12e94fd6C0074D0E8aCbCanonical 0xFi pools
0x8dd79c3966c8392b08b609FAEce029c3329f9E9E0x548db4D485127d2196393FE93DB1298fCDA241260xC7aCe4e674e2a64c5daC0c09E97BCFCb17BD2CDBEcosystem 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 native zkLTC marketplace.
Pixel runs, artwork name, 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.
Fixed-price listings and escrowed offers settle in zkLTC. Sales apply a 0.4% platform fee and a 1% creator royalty.
import { parseAbi } from "viem";
export const pixelAbi = parseAbi([
"function checkOriginal(string pixelData, uint256 grid) view returns (bool)",
"function mint(string artName, uint256 grid, string pixelData) returns (uint256 tokenId)",
"function tokenData(uint256 tokenId) view returns (string artName, uint256 gridSize, string pixelData, address creator, uint256 mintedAt, bytes32 artworkHash)",
"function tokenURI(uint256 tokenId) view returns (string)",
]);
// Supported grids: 8, 16, 32, 64.
// pixelData is packed horizontal runs: x, y, count, r, g, b.
const isOriginal = await publicClient.readContract({
address: contracts.pixel,
abi: pixelAbi,
functionName: "checkOriginal",
args: [pixelData, 32n],
});0xPump
A NUSD launchpad with reserved metadata, virtual constant-product pricing, explicit lifecycle states, and protected graduation.
reserveMarket(contentHash)Pay the nonrefundable creation reservation.createMarket(...)Create the fixed-supply token and curve.quoteBuy / buyQuote and execute NUSD input.quoteSell / sellQuote and execute token input.0xFi
The liquidity and risk layer for swaps, pools, NUSD staking, LP gauges, fixed-rate NUSD lending, borrowing, and DIA-priced synthetic assets.
| AMM path | LP fee | Router charge | Nominal total |
|---|---|---|---|
| 1 pool | 0.5% | 0.1% | 0.6% |
| 2 pools | 1.0% | 0.2% | 1.2% |
| 3 pools | 1.5% | 0.2% | 1.7% |
getAmountsOut already includes LP fees, router charges, and price impact. Do not subtract them twice.
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.
Integrate 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 |
|---|---|---|
| zkLTC to NUSD | NUSD | quoteMint + mintAtOracle |
| NUSD to zkLTC | NUSD | quoteRedeem + redeemAtOracle |
| ERC-20 to ERC-20 | Router | swapExactTokensForTokens |
| zkLTC to ERC-20 | Router | swapExactNativeForTokens |
| ERC-20 to zkLTC | Router | swapExactTokensForNative |
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.import { parseAbi, type Address, type PublicClient, type WalletClient } from "viem";
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)",
]);
const erc20Abi = parseAbi([
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
]);
type Quote = { path: Address[]; amountOut: bigint };
export async function swapErc20(params: {
publicClient: PublicClient;
walletClient: WalletClient;
account: Address;
tokenIn: Address;
tokenOut: Address;
amountIn: bigint;
slippageBps?: bigint;
}) {
const {
publicClient,
walletClient,
account,
tokenIn,
tokenOut,
amountIn,
slippageBps = 50n,
} = params;
if (await walletClient.getChainId() !== 4441) {
throw new Error("Switch wallet to LitVM chain 4441");
}
if (amountIn <= 0n || slippageBps < 0n || slippageBps >= 10_000n) {
throw new Error("Invalid swap parameters");
}
const paths: Address[][] = [[tokenIn, tokenOut]];
const usesNusd =
tokenIn.toLowerCase() === contracts.nusd.toLowerCase() ||
tokenOut.toLowerCase() === contracts.nusd.toLowerCase();
if (!usesNusd) paths.push([tokenIn, contracts.nusd, tokenOut]);
const quotes = await Promise.all(paths.map(async (path): Promise<Quote | undefined> => {
try {
const amounts = await publicClient.readContract({
address: contracts.fiRouter,
abi: routerAbi,
functionName: "getAmountsOut",
args: [amountIn, path],
});
return { path, amountOut: amounts.at(-1)! };
} catch {
return undefined;
}
}));
const best = quotes
.filter((quote): quote is Quote => Boolean(quote))
.sort((a, b) => a.amountOut === b.amountOut ? 0 : a.amountOut > b.amountOut ? -1 : 1)[0];
if (!best) throw new Error("No executable 0xFi route");
const amountOutMin = best.amountOut * (10_000n - slippageBps) / 10_000n;
const allowance = await publicClient.readContract({
address: tokenIn,
abi: erc20Abi,
functionName: "allowance",
args: [account, contracts.fiRouter],
});
if (allowance < amountIn) {
const approval = await publicClient.simulateContract({
account,
address: tokenIn,
abi: erc20Abi,
functionName: "approve",
args: [contracts.fiRouter, amountIn],
});
const approvalHash = await walletClient.writeContract(approval.request);
const approvalReceipt = await publicClient.waitForTransactionReceipt({ hash: approvalHash });
if (approvalReceipt.status !== "success") throw new Error("Approval reverted");
}
const deadline = BigInt(Math.floor(Date.now() / 1_000) + 20 * 60);
const simulation = await publicClient.simulateContract({
account,
address: contracts.fiRouter,
abi: routerAbi,
functionName: "swapExactTokensForTokens",
args: [amountIn, amountOutMin, best.path, account, deadline],
});
const hash = await walletClient.writeContract(simulation.request);
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("0xFi swap reverted");
return { hash, receipt, path: best.path, quotedOut: best.amountOut, amountOutMin };
}Native zkLTC and NUSD
The exact zkLTC/NUSD route uses OracleNUSD. It does not use an AMM pool and still requires minimum-output protection.
const nusdAbi = parseAbi([
"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)",
]);
const minimumOut = (quote: bigint, slippageBps = 50n) =>
quote * (10_000n - slippageBps) / 10_000n;
// Native zkLTC to NUSD
const quotedNusd = await publicClient.readContract({
address: contracts.nusd,
abi: nusdAbi,
functionName: "quoteMint",
args: [zkLtcIn],
});
const mint = await publicClient.simulateContract({
account,
address: contracts.nusd,
abi: nusdAbi,
functionName: "mintAtOracle",
args: [minimumOut(quotedNusd), account],
value: zkLtcIn,
});
// NUSD to native zkLTC. No ERC-20 approval is required.
const quotedZkLtc = await publicClient.readContract({
address: contracts.nusd,
abi: nusdAbi,
functionName: "quoteRedeem",
args: [nusdIn],
});
const redeem = await publicClient.simulateContract({
account,
address: contracts.nusd,
abi: nusdAbi,
functionName: "redeemAtOracle",
args: [nusdIn, minimumOut(quotedZkLtc), account],
});Vibe-code command
Paste this specification into a coding agent to generate a complete testnet swap integration without losing protocol constraints.
Build a complete 0xFi swap integration for an existing Next.js TypeScript app.
Target network
- Network: LitVM LiteForge Testnet
- Chain ID: 4441
- Native asset: zkLTC, 18 decimals
- RPC: https://liteforge.rpc.caldera.xyz/infra-partner-http
- Explorer: https://liteforge.explorer.caldera.xyz
Pinned contracts
- NUSD / OracleNUSD: 0x5317e21aba902c6c7087a84457bc02fFe99604d1
- WzkLTC: 0xE93d4373CE1eDA3df6c3Ab7ed3ab07A07aA5939F
- 0xFi Factory: 0xe33fE815c2e12DC83b69397CeD12b09849Fa9C0D
- 0xFi Router: 0x7b17035A4aC3A32f2A18a16e2F43A5f8C66275b0
Implementation requirements
- Use viem and the wallet layer already installed by the app. Do not add a server-side signer.
- Create typed chain config, minimal ABIs, quote service, transaction service, React hook, and accessible swap UI.
- Accept bigint amounts internally. Read token decimals, balance, and allowance over RPC.
- For ERC-20 routes, probe [tokenIn, tokenOut] and [tokenIn, NUSD, tokenOut]. Choose the successful quote with the largest final output.
- Use getAmountsOut for executable AMM quotes. It already includes LP fees, protocol fees, route surcharge, and price impact.
- Use swapExactTokensForTokens for ERC-20 pairs.
- Use swapExactNativeForTokens when input is native zkLTC. The path must start with WzkLTC and the transaction must send value.
- Use swapExactTokensForNative when output is native zkLTC. The path must end with WzkLTC.
- For the exact zkLTC/NUSD route, use quoteMint plus mintAtOracle, or quoteRedeem plus redeemAtOracle. Do not route this exact pair through the AMM.
- Request exact ERC-20 approval only when allowance is insufficient. Wait for the approval receipt before the swap.
- Apply user-controlled slippage in basis points. Never use zero minimum output.
- Use a short deadline, simulate every write, wait for the final receipt, and reject reverted receipts.
- Verify chain ID 4441 before every write. Keep enough zkLTC for gas.
- Show route, fee summary, quoted output, minimum received, price impact, recipient, pending state, transaction hash, and actionable errors.
- Requote after token, amount, account, chain, or block-relevant state changes.
- Use Goldsky only for history and charts. Never use indexed reserves for an executable quote.
- Reject unsupported fee-on-transfer tokens and repeated or zero-address paths.
Required delivery
- Return complete files with no placeholders or omitted sections.
- Include loading, empty, wrong-network, approval, signing, confirming, success, and error states.
- Include unit tests for route selection, slippage math, native path rules, allowance branching, and failed simulation.
- Mark the integration as testnet-only. Do not describe it as audited or mainnet-ready.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_cmr0mev6548fr01xtd92rc135/subgraphs/marketplace/1.0.1/gnhttps://api.goldsky.com/api/public/project_cmqmpust19i8v01t595z8hpq4/subgraphs/zeroxpump-testnet/staging/gnhttps://api.goldsky.com/api/public/project_cms8vgtcn6a6z01r5fo87d6im/subgraphs/zeroxfi-testnet/staging/gnconst INDEXERS = {
pixelMarketplace: "https://api.goldsky.com/api/public/project_cmr0mev6548fr01xtd92rc135/subgraphs/marketplace/1.0.1/gn",
pump: "https://api.goldsky.com/api/public/project_cmqmpust19i8v01t595z8hpq4/subgraphs/zeroxpump-testnet/staging/gn",
fi: "https://api.goldsky.com/api/public/project_cms8vgtcn6a6z01r5fo87d6im/subgraphs/zeroxfi-testnet/staging/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.