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
| Resource | URL |
|---|---|
| REST API | https://api.testnet.diffusal.xyz |
| WebSocket | wss://api.testnet.diffusal.xyz |
| Health check | GET 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
viemlibrary 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
| Issue | Cause | Fix |
|---|---|---|
FaucetCooldownActive | Claimed within last 24 hours | Wait for the cooldown to expire |
Invalid or expired nonce | Nonce reused or expired | Request a fresh nonce from /api/auth/siwe/nonce |
NotOperator revert | Calling SeriesRegistry directly | Series creation must go through operator contracts (OrderBook or RFQ) |
| Transaction shows gasUsed = gasLimit | Normal Monad behavior on reverts | Check receipt.status -- Monad reports gasUsed = gasLimit on all reverts |
| Quote expired | Took too long to fill | RFQ quotes expire in 30 seconds; fill immediately |
See Also
- Quick Start -- full API integration sequence
- Authentication -- SIWE session details
- On-Chain Guide -- contract interaction patterns
- Error Codes -- API error reference