A programmable stablecoin treasury is a company wallet that moves money only when a payout matches rules you control.
Your supplier payout starts with an invoice, an approved address, and an amount. Your policy service checks those facts. A Portal wallet signs the transaction, and Stable settles it in USDT0, the omnichain version of USDT that also pays for gas on Stable.
The wallet protects the signing material. Your code decides whether the payment should exist.
A programmable treasury turns your payment policy into a check that runs before the wallet signs.
Suppose an operations lead can pay approved suppliers up to $5,000. A controller must co-approve anything larger, and the company caps supplier payouts at $100,000 per day. You store those rules beside the invoice records they govern.
Portal gives the organization an MPC wallet, short for multi-party computation. MPC distributively generates cryptographic signing material so no stored share carries the whole signing authority. Stable gives that wallet a dollar balance whose transaction fees come from the same asset. Your finance team sees one USDT0 balance instead of a payment balance plus a separate gas token.
You need four components: a payout service, a policy service, a Portal wallet, and Stable settlement.

Create one Portal client for the treasury and keep its client signing share in your protected backend. Portal stores the other share. Use the sendAssets function from Portal to specify the transaction you want to execute. Portal signs and broadcasts it against Stable's RPC. Your policy service gets the final say.
Your spending rules run in Portal's pre-sign approval path, immediately before the wallet signs a transaction.
Enable Signature Approvals and point the webhook at your policy service. Portal sends the chain ID, client ID, and signing request. Return a 2xx status to allow it or 400 to deny it. Portal denies the request if your service does not answer within 30 seconds.
const decision = await policy.authorize({
invoiceId: "INV-2048",
supplier: "0x7a2e...91c4",
amount: 12_400,
approvals: ["ops-lead", "controller"],
idempotencyKey: "INV-2048:0x7a2e...91c4",
});
console.log(decision);{ allowed: true, rule: "two-approver-threshold", reservedDailyTotal: 47400 }Reserve the daily spend in the same database transaction that returns allowed: true. If $10,000 remains under the cap, two concurrent $7,000 requests must not both pass. Put a unique constraint on the idempotency key too. A retry should find the first payment, not create its twin.
You submit one USDT0 transfer after the policy service records an approved payment intent.
import { parseUnits, toHex } from "viem";
const response = await fetch(portalSignUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${portalClientApiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": invoice.id,
},
body: JSON.stringify({
share: treasurySigningShare,
method: "eth_sendTransaction",
params: {
from: treasury.address,
to: invoice.supplierAddress,
value: toHex(parseUnits(invoice.amountUsdt0, 18)),
data: "0x",
},
rpcUrl: stableRpcUrl,
chainId: "eip155:988",
}),
});
const { data: txHash } = await response.json();
console.log({ invoiceId: invoice.id, txHash });{ invoiceId: "INV-2048", txHash: "0x8f3a...2d41" }Keep invoice.amountUsdt0 as a decimal string so JavaScript never rounds the payment. Stable's mainnet configuration uses 18 decimals for native USDT0, which is why parseUnits receives 18. Stable charges the fee from that same balance.
Do not mark the invoice paid when you receive the hash. Store the hash, wait for a successful receipt, and then move the invoice from submitted to paid. A reverted transaction still has a perfectly real hash.
You keep a payout batch moving by separating submission lanes and reserving capacity for priority transfers.
Stable's independent nonce channels let one treasury wallet submit transactions on parallel sequences, so one stuck payout does not block every later one. The default channel preserves normal EVM behavior. Confirm that Portal supports Stable's custom transaction envelope before you use the additional channels through its signer.
Guaranteed Blockspace reserves per-lane gas capacity for priority traffic. It does not replace your retry logic. Underneath that, Stable's execution layer runs non-conflicting transactions in parallel, rechecks only affected senders after commit, and persists changes through an append-only log against a memory-mapped snapshot.
You delegate payment authority by giving a worker wallet an exact token allowance, then keeping recipient and invoice checks in the same policy path.
Portal's token delegation API builds ERC-20 approvals and lets a delegate call transferFrom. USDT0 supports those operations on Stable. Pass its contract address, not NATIVE. An allowance caps how much the delegate can move. It does not restrict the destination or attach the transfer to an invoice.
Approve the amount needed for the current payout window, inspect every delegated transfer, and revoke unused allowance. Stable's USDT0 balance can decrease through transferFrom without calling wallet code, so your reconciler must read the real onchain balance. Never treat your internal ledger as proof of solvency.
You should record the payment intent, policy decision, transaction hash, and final receipt as separate events.
That sequence gives finance an audit trail and gives engineering a precise place to resume after a crash. If the process dies after broadcast but before your database update, retry with the same Portal idempotency key and inspect the wallet nonce before creating a new payment.
Contract calls can use the same boundary. If you later move idle funds into a Morpho vault or claim Merkl rewards, decode the calldata and allow only the expected contract address, function selector, and amount. Arbitrary calldata turns a narrow treasury role into full wallet access.
Stable mainnet uses chain ID 988, Portal chain identifier eip155:988, and the RPC endpoint rpc.stable.xyz
Test the failures that move money twice: concurrent cap checks, webhook timeouts, duplicate invoices, reverted transactions, and a delegate spending its remaining allowance
With policy checks, signing controls, and reconciliation logic in place, you can build a programmable treasury that moves USDT0 on Stable according to clearly defined rules, recovers safely from failures, and scales supplier payouts without giving up control.
If you're building on Stable, find us in Discord and tell us what you're shipping.

