Docs/WDK/Migration Guide

>_ WDK / HOW-TO GUIDE

MIGRATION
GUIDE.

Move from a USDC-only EIP-3009 integration to WDK + USDT0 routing with zero downtime. New fields are additive — existing clients continue to work unchanged throughout the migration.

⌘KCommand-palette first navJump:QuickstartAPIErrorsMigrationSecurity

Backwards compatibility guarantee

  • All existing /api/v1/facilitator/settle requests with USDC continue to work — no changes required.
  • The new quoteId, routeId, and client fields are optional in the settle endpoint for existing callers.
  • You can migrate incrementally — add USDT0 support while keeping USDC as a fallback.

What Changes

The migration adds two things to your settlement flow: a quote step before settlement, and WDK signer support for USDT0 tokens. Here is the before/after comparison.

Before (USDC only)
POST /api/v1/facilitator/settle
{
  "paymentPayload": {
    "x402Version": 2,
    "scheme": "exact",
    "network": "eip155:8453",
    "payload": {
      "signature": "0x...",
      "authorization": {
        "from": "0x...",
        "to": "0xTreasury",
        "value": "1000000",
        "validAfter": "...",
        "validBefore": "...",
        "nonce": "0x..."
      }
    }
  },
  "paymentRequirements": { ... }
}
After (WDK + USDT0)
// Step 1: Quote (NEW)
POST /api/v1/liquidity/quote
{ "sourceAssets": ["USDT0", "USDC"], ... }
→ { "quoteId": "q_123", "routes": [...] }

// Step 2: Settle (EXTENDED)
POST /api/v1/router/settle
{
  "quoteId": "q_123",   // NEW
  "routeId": "r_fast",  // NEW
  "client": {           // NEW
    "type": "wdk",
    "version": "1.0.0"
  },
  "authType": "eip3009",
  "amount": "1.00",
  "asset": "USDT0",     // NEW (was USDC)
  "payment": {
    "scheme": "exact",
    "authorization": { ... },
    "signature": "0x..."
  }
}
0

Baseline Audit

Before changing any code, inventory what you have. This tells you the scope of work and gives you a baseline to measure against after migration.

Checklist

  • Find all callers using /api/v1/facilitator/settle — these are your migration targets.
  • Check whether any caller hardcodes "asset": "USDC" or USDC decimal math (6 decimals).
  • Note current success rate, average latency, and failure modes for the existing settle flow.
  • Identify which chains your users have wallets on (Base, Arbitrum, Ethereum).
  • Confirm you have a WDK-compatible wallet for USDT0 signing, or a plan to get one.
1

Add the Quote Step

Add the quote call before your settle call. This is additive — your existing settle call does not change yet. At this stage, just pass USDC in sourceAssets and use the returned quoteId and routeId in your settle request.

typescript — phase 1: add quote call
// BEFORE (no quote step):
async function settle(amount: string, authorization: Authorization, signature: string) {
  return fetch('/api/v1/facilitator/settle', {
    method: 'POST',
    body: JSON.stringify({ paymentPayload: { ... } }),
  });
}

// AFTER PHASE 1 (add quote, keep USDC):
async function settle(amount: string, authorization: Authorization, signature: string) {
  // New: get a quote first
  const quote = await fetch('/api/v1/liquidity/quote', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}` },
    body: JSON.stringify({
      invoiceId: crypto.randomUUID(),
      walletAddress: authorization.from,
      sourceAssets: ['USDC'],   // Still USDC only for now
    }),
  }).then(r => r.json());

  const route = quote.routes[0];

  // Extended: include quoteId + routeId, keep existing authorization
  return fetch('/api/v1/router/settle', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}` },
    body: JSON.stringify({
      quoteId: quote.quoteId,    // NEW
      routeId: route.routeId,   // NEW
      client: { type: 'direct', version: '1.0.0' },  // NEW
      authType: 'eip3009',
      amount,
      asset: 'USDC',            // Still USDC
      payment: { scheme: 'exact', authorization, signature },
    }),
  });
}

Deploy this, monitor your success rate, and confirm nothing regressed. The route selection for USDC should return results immediately — no new failures expected.

2

Add WDK Signer + USDT0

Add the WDK signer adapter and expand sourceAssets to include USDT0. The quote response selects the best available token — if the user has USDT0 on Arbitrum, it'll be preferred over USDC on Base for most routes.

typescript — phase 2: add WDK signer
import { WdkSigner } from '@tether/wdk';

async function settle(amount: string, walletAddress: string, wdkSigner: WdkSigner) {
  // Step 1: Quote with USDT0 preference
  const quote = await fetch('/api/v1/liquidity/quote', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}` },
    body: JSON.stringify({
      invoiceId: crypto.randomUUID(),
      walletAddress,
      sourceAssets: ['USDT0', 'USDC'],  // USDT0 preferred, USDC fallback
    }),
  }).then(r => r.json());

  const route = quote.routes[0];
  const isWdk = route.sourceAsset === 'USDT0';

  const now = Math.floor(Date.now() / 1000);
  const nonce = '0x' + crypto.getRandomValues(new Uint8Array(32))
    .reduce((hex, b) => hex + b.toString(16).padStart(2, '0'), '');

  const authorization = {
    from: walletAddress,
    to: P402_TREASURY,
    value: String(Math.round(parseFloat(amount) * 1_000_000)),  // 6 decimals
    validAfter: String(now - 60),
    validBefore: String(now + 3600),
    nonce,
  };

  // Sign: WDK for USDT0, standard EIP-712 for USDC
  const signature = isWdk
    ? await wdkSigner.signTransferAuthorization({
        token: USDT0_CONTRACT[route.sourceChain],
        chainId: parseInt(route.sourceChain.split(':')[1]),
        authorization,
      })
    : await standardEip712Sign(authorization);  // Your existing signing logic

  // Step 2: Settle
  return fetch('/api/v1/router/settle', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}` },
    body: JSON.stringify({
      quoteId: quote.quoteId,
      routeId: route.routeId,
      client: { type: isWdk ? 'wdk' : 'direct', version: '1.0.0' },
      authType: 'eip3009',
      amount,
      asset: route.sourceAsset,
      payment: { scheme: 'exact', authorization, signature },
    }),
  }).then(r => r.json());
}
3

Update Your UI

If you have a payment UI, update it to show the selected route and token. The quote response contains everything you need: selected asset, chain, fee, and estimated latency.

UI changes to make

  • 1.Replace a token-only selector (e.g. "Pay with USDC") with a route card showing token + chain + estimated fee.
  • 2.Show the selected route from the quote before asking the user to sign.
  • 3.Display the receipt txHash and receipt_id after settlement for user reference.
  • 4.Handle P402_QUOTE_EXPIRED gracefully: re-fetch the quote and present the new options.

Testing Your Migration

After each phase, verify these signals before moving to the next phase:

CheckHow
Settlement success rate unchangedCompare 7-day success rate before and after deploy.
No double-spend incidentsMonitor for P402_REPLAY_DETECTED errors — any replay means a nonce bug.
Quote responses include routesAssert quote.routes.length > 0 before attempting settlement.
Signature verification passesP402_AUTH_INVALID errors indicate a signing mismatch — check chainId and contract address.
Receipt_id returned on successLog receipt_id for each settlement; absence means the response schema changed.