Diffusal

Testnet Guide

Getting started on Monad testnet with collateral, authentication, and your first trade

This guide walks through everything needed to start trading on the Diffusal Monad testnet: connecting to the API, obtaining testnet collateral, and executing your first RFQ trade.

Testnet Environment

ResourceURL
REST APIhttps://api.testnet.diffusal.xyz
WebSocketwss://api.testnet.diffusal.xyz
Health checkGET https://api.testnet.diffusal.xyz/api/health

The testnet runs on Monad with approximately 0.4-second block times. All contract addresses are published in the Contract Addresses page.


1. Prerequisites

  • A wallet with a private key (MetaMask, or any EIP-1193 wallet)
  • Monad testnet ETH for gas (use the Monad faucet)
  • Node.js 18+ and the viem library for programmatic access

2. Get Testnet Collateral

Diffusal uses TestnetUSDT as collateral (6 decimals, ERC-20). The contract has a built-in faucet that dispenses 100,000 USDT per claim with a 24-hour cooldown.

Via Contract Call

Call the faucet() function on the TestnetUSDT contract directly from your wallet:

import { createWalletClient, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { monad } from "viem/chains";

const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const client = createWalletClient({
  account,
  chain: monad,
  transport: http(),
});

// Call the faucet — check Contract Addresses page for current address
const TESTNET_USDT = "0x..."; // See /docs/contracts/addresses
const hash = await client.writeContract({
  address: TESTNET_USDT as `0x${string}`,
  abi: parseAbi(["function faucet() external"]),
  functionName: "faucet",
});
console.log("Faucet claim tx:", hash);

The faucet is rate-limited to one claim per 24 hours per address. If the cooldown is active, the transaction will revert with FaucetCooldownActive.

3. Deposit Collateral

Before trading, deposit USDT into a Diffusal portfolio:

import { parseAbi, parseUnits } from "viem";

// 1. Approve the CollateralVault to spend your USDT
const VAULT = "0x..."; // DiffusalCollateralVault — see /docs/contracts/addresses
const amount = parseUnits("10000", 6); // 10,000 USDT

await client.writeContract({
  address: TESTNET_USDT as `0x${string}`,
  abi: parseAbi(["function approve(address spender, uint256 amount) external returns (bool)"]),
  functionName: "approve",
  args: [VAULT as `0x${string}`, amount],
});

// 2. Create a portfolio (if you don't have one)
const PORTFOLIO_MANAGER = "0x..."; // See /docs/contracts/addresses
await client.writeContract({
  address: PORTFOLIO_MANAGER as `0x${string}`,
  abi: parseAbi(["function createPortfolio() external returns (uint256)"]),
  functionName: "createPortfolio",
});

// 3. Deposit into portfolio ID 1
await client.writeContract({
  address: VAULT as `0x${string}`,
  abi: parseAbi(["function depositToPortfolio(uint256 portfolioId, uint256 amount) external"]),
  functionName: "depositToPortfolio",
  args: [1n, amount],
});

4. Authenticate

Complete the SIWE authentication flow to access trading endpoints. See the Authentication page for details, or use the inline example from the Quick Start page.

const API = "https://api.testnet.diffusal.xyz";

// 1. Get nonce
const { nonce } = await fetch(`${API}/api/auth/siwe/nonce`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ walletAddress: account.address, chainId: 10143 }),
}).then((r) => r.json());

// 2. Sign SIWE message (see Quick Start for full message format)
// 3. Verify and get token
const { token } = await fetch(`${API}/api/auth/siwe/verify`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    message: siweMessage,
    signature,
    walletAddress: account.address,
    chainId: 10143,
  }),
}).then((r) => r.json());

5. First Trade: RFQ Flow

The RFQ (Request for Quote) flow is the simplest way to execute a trade. You request quotes from market makers, receive signed quotes, and fill on-chain.

Step 1: Check Available Markets

const markets = await fetch(`${API}/api/markets/pairs`).then((r) => r.json());
console.log("Available pairs:", markets);

Step 2: Request a Quote

const quoteRes = await fetch(`${API}/api/rfq/request`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    taker: account.address,
    takerPortfolioId: 1,
    pairId: "0x...", // Use /api/helpers/pair-id to compute
    strike: "82000000000000000000000", // 82,000 in WAD (1e18)
    expiry: Math.floor(Date.now() / 1000) + 7 * 86400, // 7 days
    isCall: true,
    size: "1000000000000000000", // 1 contract in WAD
    intent: "open",
  }),
});
const auction = await quoteRes.json();
console.log("RFQ auction created:", auction.requestId);

Step 3: Listen for Quotes

Subscribe to the RFQ taker quote channel on the private WebSocket to receive market maker responses:

const ws = new WebSocket("wss://api.testnet.diffusal.xyz/ws/private");

ws.addEventListener("open", () => {
  // Authenticate first
  ws.send(JSON.stringify({ method: "AUTH", params: { token }, id: 1 }));
});

ws.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data as string);

  if (msg.status === "authenticated") {
    // Subscribe to quotes for your specific request
    ws.send(
      JSON.stringify({
        method: "SUBSCRIBE",
        params: [`rfq.quotes.${auction.requestId}`],
        id: 2,
      }),
    );
  }

  if (msg.stream?.startsWith("rfq.quotes.")) {
    console.log("Quote received:", msg.data);
    // msg.data contains the signed EIP-712 quote ready for on-chain fill
  }
});

Step 4: Fill the Quote On-Chain

Once you receive a signed quote, fill it on-chain via the RFQ contract:

// Use the fill endpoint to build calldata
const fillRes = await fetch(`${API}/api/rfq/fill`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${token}`,
  },
  body: JSON.stringify({
    requestId: auction.requestId,
    mmSignature: quote.signature, // from the WS quote message
  }),
});
const fillData = await fillRes.json();

// Submit the transaction on-chain
const txHash = await client.sendTransaction({
  to: fillData.relay.to as `0x${string}`,
  data: fillData.relay.calldata as `0x${string}`,
});
console.log("Fill tx:", txHash);

RFQ quotes expire after 30 seconds -- fill immediately after receiving a quote.


Troubleshooting

IssueCauseFix
FaucetCooldownActiveClaimed within last 24 hoursWait for the cooldown to expire
Invalid or expired nonceNonce reused or expiredRequest a fresh nonce from /api/auth/siwe/nonce
NotOperator revertCalling SeriesRegistry directlySeries creation must go through operator contracts (OrderBook or RFQ)
Transaction shows gasUsed = gasLimitNormal Monad behavior on revertsCheck receipt.status -- Monad reports gasUsed = gasLimit on all reverts
Quote expiredTook too long to fillRFQ quotes expire in 30 seconds; fill immediately

See Also

On this page