Build a P2P payment app on Stable. Create wallets, check balances, send and receive USDT0, and query transaction history.
This guide walks through building a P2P payment application on Stable. The app handles the full payment lifecycle: the sender transfers USDT0 directly, the receiver detects the incoming payment in real time, and both can query their own transaction history. Same architecture as any wallet or payment interface, whether a mobile app, web checkout, or backend service.No middleware, no intermediary. For the conceptual overview, see P2P payments.
A wallet is a key pair derived from a seed phrase. Generate one for a new user and return the phrase so they can back it up. A returning user restores their wallet from the same phrase.
// wallet.tsimport { ethers } from "ethers";import { provider } from "./config";/** Create a new wallet for a new user. */export function createWallet() { const wallet = ethers.Wallet.createRandom(provider); return { wallet, address: wallet.address, seedPhrase: wallet.mnemonic!.phrase, // display to user for backup };}/** Restore a wallet from a seed phrase (returning user). */export function restoreWallet(seedPhrase: string) { const wallet = ethers.Wallet.fromPhrase(seedPhrase, provider); return { wallet, address: wallet.address };}if (import.meta.url === `file://${process.argv[1]}`) { const { address, seedPhrase } = createWallet(); console.log("Address: ", address); console.log("Seed phrase:", seedPhrase);}
npx tsx wallet.ts
Address: 0xAlice...1234Seed phrase: liberty shoot ... (12 words)
The sender signs and submits a transfer directly. On Stable, USDT0 is the native asset, so a simple value transfer is the cheapest path (21,000 gas). This is the same code path as “Send” in any payment app.
The receiver listens for incoming Transfer events. This is equivalent to push notifications in a traditional payment app. On Stable, single-slot finality means the receiver sees a payment almost instantly.
// receive.tsimport { ethers } from "ethers";import { STABLE_WS, USDT0_ADDRESS } from "./config";const wsProvider = new ethers.WebSocketProvider(STABLE_WS);const usdt0 = new ethers.Contract( USDT0_ADDRESS, ["event Transfer(address indexed from, address indexed to, uint256 value)"], wsProvider);export function watchIncomingPayments(address: string) { const filter = usdt0.filters.Transfer(null, address); usdt0.on(filter, (from: string, to: string, value: bigint, event: any) => { console.log("Payment received:"); console.log(" from: ", from); console.log(" amount:", ethers.formatUnits(value, 6), "USDT0"); console.log(" tx: ", event.log.transactionHash); }); console.log("Watching for incoming payments to", address);}if (import.meta.url === `file://${process.argv[1]}`) { watchIncomingPayments(process.argv[2]);}
npx tsx receive.ts 0xBob...5678
Watching for incoming payments to 0xBob...5678Payment received: from: 0xAlice...1234 amount: 0.001 USDT0 tx: 0x8f3a...2d41
Native transfers (value transfers) also emit a Transfer event on the USDT0 ERC-20 contract because USDT0 is both the native asset and an ERC-20 token on Stable. A single event listener covers both transfer methods.
sent 0.001 USDT0 0xBob...5678 0x8f3a...2d41received 0.01 USDT0 0xFaucet... 0x22b1...3f09
Scanning wide block ranges (millions of blocks) can time out and exceed RPC rate limits. For production, use the Stablescan Etherscan-compatible API for paginated history queries — every transaction is already indexed.