NeuyRouter for Beginners

Updated Feb 10, 2026 - Build a working WETH > USDC quote + swap page (Ethereum)

This is an end-to-end tutorial that teaches you how to build a minimal "hello world" page using NeuyRouter. By the end you will have a working page you can run locally, connect MetaMask, get a quote with getV2Quote, and swap with dexV2Swap.

0. Goal

You will build a single-file HTML page that does:

  • Connect wallet (MetaMask)
  • Get a V2 quote: getV2Quote(WETH, USDC, amountIn)
  • Compute a slippage-protected minOut
  • Approve WETH (if needed)
  • Swap: dexV2Swap(route, amountIn, minOut, WETH, USDC)
Happy-path first: this tutorial is designed to avoid "first-run snags". It includes a full working file at the end so you can copy/paste and succeed immediately, then come back to learn each step.

1. Requirements

  • Wallet: MetaMask (or any wallet that injects window.ethereum)
  • Network: Ethereum mainnet
  • Funds: some ETH for gas + some WETH to swap
  • Serving: run from http://localhost or https (avoid file://)

2. Constants (Ethereum)

These are the only chain-specific parts in the demo:

Item Value Notes
Chain ID 0x1 Ethereum mainnet
NeuyRouter
0xD3E73c2563fFCE65401DfdEcf66b699D4ce41fB9
V3 beta 4 (Ethereum)
WETH
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
18 decimals
USDC
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
6 decimals

3. The two NeuyRouter calls

3.1 Quote: getV2Quote

This returns two values: the best amountOut and a route id. You must use that same route id for the swap.

const [amountOut, route] = await router.getV2Quote(WETH, USDC, amountIn);

3.2 Swap: dexV2Swap

This executes the swap using the chosen route and your slippage-protected minimum output.

await router.dexV2Swap(route, amountIn, minOut, WETH, USDC);

4. Slippage protection (minOut)

Never swap without a minimum output. This demo uses basis points (bps): (200 bps = 2%)

// minOut = amountOut * (10000 - bps) / 10000
function bpsMinOut(amountOutBn, bps) {
  const keep = ethers.BigNumber.from(10000 - Number(bps));
  return amountOutBn.mul(keep).div(10000);
}

5. ERC20 approval (WETH)

Because WETH is an ERC20 token, the router cannot move it unless you approve it first. This demo checks allowance, then approves only if needed.

async function ensureApprovalWeth(amountIn) {
  const weth = new ethers.Contract(WETH, erc20Abi, signer);
  const allowance = await weth.allowance(account, NEUY_ROUTER);
  if (allowance.gte(amountIn)) return;
  const tx = await weth.approve(NEUY_ROUTER, amountIn);
  await tx.wait();
}

6. Run locally (recommended)

The most reliable way to run browser Web3 demos is using localhost, not file://.

# In the folder with your HTML file:
python3 -m http.server 8080

# Then open:
http://localhost:8080/neuyrouter_ultrabasic_eth.html
Important: This demo swaps WETH > USDC. Make sure you hold WETH (not only ETH), and have ETH for gas.

7. Full copy/paste working file

Save the following as neuyrouter_ultrabasic_eth.html, then run it using the steps above. This is the exact working beginner demo.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>NeuyRouter Ultra-Basic (Ethereum) - WETH > USDC (V2 Quote + V2 Swap)</title>
  <style>
    body { font-family: Arial, Helvetica, sans-serif; background:#fff; color:#111; max-width:760px; margin:32px auto; padding:16px; }
    h1 { font-size:20px; margin:0 0 14px; }
    .box { border:1px solid #ddd; border-radius:10px; padding:14px; }
    label { display:block; font-weight:700; margin:12px 0 6px; color:#333; }
    input, button { width:100%; padding:11px; border-radius:8px; border:1px solid #ccc; font-size:16px; box-sizing:border-box; }
    button { background:#2563eb; color:#fff; border:none; cursor:pointer; font-weight:700; }
    button:disabled { opacity:0.6; cursor:not-allowed; }
    .row { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
    .muted { color:#666; font-size:13px; margin-top:6px; }
    #status { margin-top:12px; padding:12px; border-radius:8px; background:#f3f4f6; white-space:pre-wrap; line-height:1.35; }
    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
  </style>
</head>
<body>
  <h1>NeuyRouter Ultra-Basic (Ethereum) - WETH > USDC</h1>

  <div class="box">
    <button id="connectBtn">Connect Wallet (Ethereum)</button>

    <div id="app" style="display:none;">
      <div class="row">
        <div>
          <label>Amount In (WETH)</label>
          <input id="amountInHuman" value="0.01" />
          <div class="muted">Human units (we'll convert using 18 decimals).</div>
        </div>

        <div>
          <label>Slippage (bps)</label>
          <input id="slippageBps" value="200" />
          <div class="muted">200 = 2%</div>
        </div>
      </div>

      <div class="row" style="margin-top:10px;">
        <button id="quoteBtn">Get V2 Quote</button>
        <button id="swapBtn">Approve (if needed) + V2 Swap</button>
      </div>

      <div id="status" class="mono">Ready.</div>
    </div>
  </div>

  <!-- ethers v5 -->
  <script src="https://finance.neuy.io/ethers.umd.min.js" type="application/javascript"></script>

  <script>
    const CHAIN_ID_HEX = "0x1";
    const NEUY_ROUTER = "0xD3E73c2563fFCE65401DfdEcf66b699D4ce41fB9";
    const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
    const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

    const routerAbi = [
      "function getV2Quote(address tkIn, address tkOut, uint256 amountIn) external view returns (uint256,uint256)",
      "function dexV2Swap(uint256 route, uint256 amountIn, uint256 amountOut, address tkIn, address tkOut) external returns (uint256)"
    ];

    const erc20Abi = [
      "function approve(address spender, uint256 amount) external returns (bool)",
      "function allowance(address owner, address spender) external view returns (uint256)"
    ];

    const connectBtn = document.getElementById("connectBtn");
    const app = document.getElementById("app");
    const quoteBtn = document.getElementById("quoteBtn");
    const swapBtn = document.getElementById("swapBtn");
    const statusEl = document.getElementById("status");
    const amountInHumanEl = document.getElementById("amountInHuman");
    const slippageBpsEl = document.getElementById("slippageBps");

    let provider, signer, account, router;
    let lastQuote = null;

    function setStatus(msg, isError=false) {
      statusEl.textContent = msg;
      statusEl.style.background = isError ? "#fee2e2" : "#f3f4f6";
      statusEl.style.color = isError ? "#7f1d1d" : "#111";
    }

    async function ensureEthereumMainnet() {
      const cid = await window.ethereum.request({ method: "eth_chainId" });
      if ((cid || "").toLowerCase() === CHAIN_ID_HEX) return;
      await window.ethereum.request({
        method: "wallet_switchEthereumChain",
        params: [{ chainId: CHAIN_ID_HEX }]
      });
    }

    function bpsMinOut(amountOutBn, bps) {
      const keep = ethers.BigNumber.from(10000 - Number(bps));
      return amountOutBn.mul(keep).div(10000);
    }

    async function ensureApprovalWeth(amountIn) {
      const weth = new ethers.Contract(WETH, erc20Abi, signer);
      const allowance = await weth.allowance(account, NEUY_ROUTER);
      if (allowance.gte(amountIn)) return;
      setStatus("Approving WETH...");
      const tx = await weth.approve(NEUY_ROUTER, amountIn);
      await tx.wait();
    }

    async function connect() {
      if (!window.ethereum) {
        setStatus("No wallet found (window.ethereum missing). Use MetaMask.", true);
        return;
      }
      if (typeof ethers === "undefined") {
        setStatus("ethers v5 did not load.", true);
        return;
      }

      setStatus("Connecting...");
      await ensureEthereumMainnet();
      await window.ethereum.request({ method: "eth_requestAccounts" });

      provider = new ethers.providers.Web3Provider(window.ethereum, "any");
      signer = provider.getSigner();
      account = await signer.getAddress();
      router = new ethers.Contract(NEUY_ROUTER, routerAbi, signer);

      connectBtn.style.display = "none";
      app.style.display = "block";

      setStatus(
        "Connected ✅\n" +
        `Account: ${account}\n` +
        `Router:  ${NEUY_ROUTER}\n` +
        `Pair:    WETH > USDC`
      );
    }

    async function getQuote() {
      try {
        quoteBtn.disabled = true;
        await ensureEthereumMainnet();

        const amountInHuman = (amountInHumanEl.value || "").trim();
        const slippageBps = Number((slippageBpsEl.value || "0").trim());

        if (!amountInHuman || Number(amountInHuman) <= 0) {
          setStatus("Enter a valid WETH amount.", true);
          return;
        }
        if (!Number.isFinite(slippageBps) || slippageBps < 0 || slippageBps > 5000) {
          setStatus("Slippage bps must be between 0 and 5000.", true);
          return;
        }

        const amountIn = ethers.utils.parseUnits(amountInHuman, 18);

        setStatus("Quoting via getV2Quote...");
        const [amountOut, route] = await router.getV2Quote(WETH, USDC, amountIn);
        const minOut = bpsMinOut(amountOut, slippageBps);

        lastQuote = { amountIn, amountOut, route, minOut, slippageBps };

        setStatus(
          "Quote OK ✅\n" +
          `amountIn (WETH): ${amountInHuman}\n` +
          `amountOut (USDC): ${ethers.utils.formatUnits(amountOut, 6)}\n` +
          `route: ${route.toString()}\n` +
          `minOut (@${slippageBps} bps): ${ethers.utils.formatUnits(minOut, 6)}`
        );
      } catch (e) {
        setStatus("Quote failed ❌\n" + (e?.reason || e?.message || String(e)), true);
      } finally {
        quoteBtn.disabled = false;
      }
    }

    async function doSwap() {
      try {
        swapBtn.disabled = true;
        await ensureEthereumMainnet();

        if (!lastQuote) {
          setStatus("No quote yet. Click "Get V2 Quote" first.", true);
          return;
        }

        const { amountIn, minOut, route } = lastQuote;

        setStatus("Checking/setting WETH approval...");
        await ensureApprovalWeth(amountIn);

        setStatus("Sending dexV2Swap...");
        const tx = await router.dexV2Swap(route, amountIn, minOut, WETH, USDC);
        setStatus("Swap sent ✅\n" + tx.hash + "\nWaiting confirmation...");

        const receipt = await tx.wait();
        if (receipt.status === 1) setStatus("Swap success ✅\n" + tx.hash);
        else setStatus("Swap reverted ❌\n" + tx.hash, true);

      } catch (e) {
        setStatus("Swap failed ❌\n" + (e?.reason || e?.message || String(e)), true);
      } finally {
        swapBtn.disabled = false;
      }
    }

    connectBtn.addEventListener("click", connect);
    quoteBtn.addEventListener("click", getQuote);
    swapBtn.addEventListener("click", doSwap);
  </script>
</body>
</html>

8. Troubleshooting

  • Wallet not detected: don't use file://. Use http://localhost or https.
  • Wrong network: switch MetaMask to Ethereum mainnet.
  • Swap fails: ensure you have ETH for gas and WETH to sell.
  • Approval succeeds but swap fails: re-quote, then swap again; increase slippage bps slightly (e.g. 200 > 300).

9. Next steps (V3 + Split)

Once you're comfortable with this flow, the upgrade path is simple:

  • Swap getV2Quote > getV3Quote
  • Swap dexV2Swap > dexV3Swap
  • Try getSplitV2Quote + dexSplitV2Swap

The core mental model stays the same: quote > slippage guard > approve > swap.

Reference: NeuyRouter Dev Docs