Payments
Every pattern is: check balance → confirm → send → wait for confirmation → show explorer link.
| Pattern | What happens |
|---|---|
| Pay | One OUSD transfer from buyer to creator. |
| Split | One payment, several sequential transfers by percentage. Show each with its own link; if one fails, show which shares are still owed. |
| Tip | Small fixed amount (e.g. 0.10 OUSD) after an action. |
| Provenance | The confirmed tx hash is the proof. Save buyer, price, time and hash; render a certificate linking to the explorer. |
Amounts
- Convert with on-chain decimals:
parseUnits("1.00", decimals). - Display 2 decimals. Default demo amounts ≤ 1 OUSD — this is real money.
Not enough OUSD
Show a "Get OUSD" button linking to joinopenstandard.com/integrate (Stripe / Bridge / Privy, Coinbase, Visa, Mastercard, or Coinbase / Kraken / Uniswap).
Wrong network
Refuse to send and show "Switch to <chain>". Never retry silently on another chain.
Reference code (generated)
These are the exact snippets every mega-prompt embeds.
Base
// src/lib/ousd.ts
import { base as CHAIN } from 'viem/chains';
export { CHAIN };
import { createPublicClient, http, erc20Abi, encodeFunctionData, parseUnits, formatUnits } from 'viem';
export const OUSD = '0xB2000000000000000000002fEb517dFeC7415344' as const;
export const pub = createPublicClient({ chain: CHAIN, transport: http('https://mainnet.base.org') });
let dec: number | undefined;
export async function ousdDecimals() { return dec ??= await pub.readContract({ address: OUSD, abi: erc20Abi, functionName: 'decimals' }); }
export async function ousdBalance(owner: `0x${string}`) {
const raw = await pub.readContract({ address: OUSD, abi: erc20Abi, functionName: 'balanceOf', args: [owner] });
return Number(formatUnits(raw, await ousdDecimals()));
}
export async function transferTx(to: `0x${string}`, amount: string) {
const value = parseUnits(amount, await ousdDecimals());
return { to: OUSD, value: 0n, chainId: CHAIN.id,
data: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [to, value] }) };
}
export const txUrl = (h: string) => 'https://basescan.org/tx/' + h;
export async function waitFor(hash: `0x${string}`) { return pub.waitForTransactionReceipt({ hash, timeout: 90_000 }); }
// src/components/privy-root.tsx (default export; loaded with React.lazy inside <ClientOnly>)
import { PrivyProvider } from '@privy-io/react-auth';
import { CHAIN } from '@/lib/ousd';
export default function PrivyRoot({ appId, children }: { appId: string; children: React.ReactNode }) {
return <PrivyProvider appId={appId} config={{ loginMethods: ['google'], defaultChain: CHAIN, supportedChains: [CHAIN],
embeddedWallets: { ethereum: { createOnLogin: 'users-without-wallets' } } }}>{children}</PrivyProvider>;
}
// Sending (inside a component under PrivyRoot)
// const { sendTransaction } = useSendTransaction(); // from '@privy-io/react-auth'
// const { hash } = await sendTransaction(await transferTx(to, '1.00'), { sponsor: true });
// await waitFor(hash); show txUrl(hash).
Ethereum
// src/lib/ousd.ts
import { mainnet as CHAIN } from 'viem/chains';
export { CHAIN };
import { createPublicClient, http, erc20Abi, encodeFunctionData, parseUnits, formatUnits } from 'viem';
export const OUSD = '0x9f6F3991D525015a6F8CaF062C83b62fD3AC4436' as const;
export const pub = createPublicClient({ chain: CHAIN, transport: http('https://ethereum-rpc.publicnode.com') });
let dec: number | undefined;
export async function ousdDecimals() { return dec ??= await pub.readContract({ address: OUSD, abi: erc20Abi, functionName: 'decimals' }); }
export async function ousdBalance(owner: `0x${string}`) {
const raw = await pub.readContract({ address: OUSD, abi: erc20Abi, functionName: 'balanceOf', args: [owner] });
return Number(formatUnits(raw, await ousdDecimals()));
}
export async function transferTx(to: `0x${string}`, amount: string) {
const value = parseUnits(amount, await ousdDecimals());
return { to: OUSD, value: 0n, chainId: CHAIN.id,
data: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [to, value] }) };
}
export const txUrl = (h: string) => 'https://etherscan.io/tx/' + h;
export async function waitFor(hash: `0x${string}`) { return pub.waitForTransactionReceipt({ hash, timeout: 90_000 }); }
// src/components/privy-root.tsx (default export; loaded with React.lazy inside <ClientOnly>)
import { PrivyProvider } from '@privy-io/react-auth';
import { CHAIN } from '@/lib/ousd';
export default function PrivyRoot({ appId, children }: { appId: string; children: React.ReactNode }) {
return <PrivyProvider appId={appId} config={{ loginMethods: ['google'], defaultChain: CHAIN, supportedChains: [CHAIN],
embeddedWallets: { ethereum: { createOnLogin: 'users-without-wallets' } } }}>{children}</PrivyProvider>;
}
// Sending (inside a component under PrivyRoot)
// const { sendTransaction } = useSendTransaction(); // from '@privy-io/react-auth'
// const { hash } = await sendTransaction(await transferTx(to, '1.00'), { sponsor: true });
// await waitFor(hash); show txUrl(hash).
Solana
// src/lib/ousd.ts
import { Connection, PublicKey, Transaction } from '@solana/web3.js';
import { getMint, getAssociatedTokenAddressSync, createAssociatedTokenAccountIdempotentInstruction,
createTransferCheckedInstruction, getAccount } from '@solana/spl-token';
export const MINT = new PublicKey('ousd2mJsPEckLHcSCDxyKD7NDGARZcfLbDZkKiatYHB');
export const conn = new Connection('https://api.mainnet-beta.solana.com', 'confirmed'); // swap for a proxied SOLANA_RPC_URL in production
let meta: { programId: PublicKey; decimals: number } | undefined;
export async function mintMeta() {
if (meta) return meta;
const info = await conn.getAccountInfo(MINT); const programId = info!.owner; // Token or Token-2022
const m = await getMint(conn, MINT, 'confirmed', programId);
return (meta = { programId, decimals: m.decimals });
}
export async function ousdBalance(owner: string) {
const { programId, decimals } = await mintMeta();
const ata = getAssociatedTokenAddressSync(MINT, new PublicKey(owner), false, programId);
try { const a = await getAccount(conn, ata, 'confirmed', programId); return Number(a.amount) / 10 ** decimals; }
catch { return 0; } // no token account yet
}
export async function transferTx(from: string, to: string, amount: number) {
const { programId, decimals } = await mintMeta();
const f = new PublicKey(from), t = new PublicKey(to);
const src = getAssociatedTokenAddressSync(MINT, f, false, programId);
const dst = getAssociatedTokenAddressSync(MINT, t, false, programId);
const tx = new Transaction().add(
createAssociatedTokenAccountIdempotentInstruction(f, dst, t, MINT, programId),
createTransferCheckedInstruction(src, MINT, dst, f, BigInt(Math.round(amount * 10 ** decimals)), decimals, [], programId));
tx.feePayer = f; tx.recentBlockhash = (await conn.getLatestBlockhash()).blockhash;
return tx.serialize({ requireAllSignatures: false });
}
export const txUrl = (sig: string) => 'https://solscan.io/tx/' + sig;
// src/components/privy-root.tsx (default export; loaded with React.lazy inside <ClientOnly>)
import { PrivyProvider } from '@privy-io/react-auth';
export default function PrivyRoot({ appId, children }: { appId: string; children: React.ReactNode }) {
return <PrivyProvider appId={appId} config={{ loginMethods: ['google'],
embeddedWallets: { solana: { createOnLogin: 'users-without-wallets' } } }}>{children}</PrivyProvider>;
}
// Sending: const { wallets } = useWallets() and const { signAndSendTransaction } = useSignAndSendTransaction()
// (both from '@privy-io/react-auth/solana'); const bytes = await transferTx(wallets[0].address, to, 1);
// const { signature } = await signAndSendTransaction({ transaction: bytes, wallet: wallets[0] });
// convert signature to base58 (bs58) if it is bytes, await conn.confirmTransaction, then show txUrl.
Tempo
// src/lib/ousd.ts
import { defineChain } from 'viem';
export const CHAIN = defineChain({ id: 4217, name: 'Tempo',
nativeCurrency: { name: 'USD', symbol: 'USD', decimals: 6 },
rpcUrls: { default: { http: ['https://rpc.tempo.xyz'] } } }); // confirm RPC at docs.tempo.xyz
import { createPublicClient, http, erc20Abi, encodeFunctionData, parseUnits, formatUnits } from 'viem';
export const OUSD = '0x20c0000000000000000000006a37DA5C996874BE' as const;
export const pub = createPublicClient({ chain: CHAIN, transport: http('https://rpc.tempo.xyz') });
let dec: number | undefined;
export async function ousdDecimals() { return dec ??= await pub.readContract({ address: OUSD, abi: erc20Abi, functionName: 'decimals' }); }
export async function ousdBalance(owner: `0x${string}`) {
const raw = await pub.readContract({ address: OUSD, abi: erc20Abi, functionName: 'balanceOf', args: [owner] });
return Number(formatUnits(raw, await ousdDecimals()));
}
export async function transferTx(to: `0x${string}`, amount: string) {
const value = parseUnits(amount, await ousdDecimals());
return { to: OUSD, value: 0n, chainId: CHAIN.id,
data: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [to, value] }) };
}
export const txUrl = (h: string) => 'https://explore.tempo.xyz/receipt/' + h;
export async function waitFor(hash: `0x${string}`) { return pub.waitForTransactionReceipt({ hash, timeout: 90_000 }); }
// src/components/privy-root.tsx (default export; loaded with React.lazy inside <ClientOnly>)
import { PrivyProvider } from '@privy-io/react-auth';
import { CHAIN } from '@/lib/ousd';
export default function PrivyRoot({ appId, children }: { appId: string; children: React.ReactNode }) {
return <PrivyProvider appId={appId} config={{ loginMethods: ['google'], defaultChain: CHAIN, supportedChains: [CHAIN],
embeddedWallets: { ethereum: { createOnLogin: 'users-without-wallets' } } }}>{children}</PrivyProvider>;
}
// Sending (inside a component under PrivyRoot)
// const { sendTransaction } = useSendTransaction(); // from '@privy-io/react-auth'
// const { hash } = await sendTransaction(await transferTx(to, '1.00'), { sponsor: true });
// await waitFor(hash); show txUrl(hash).