# Accounts guides
Source: https://docs.stable.xyz/en/explanation/accounts-index
All account guides, concepts, and references on one page: wallet creation, EIP-7702 delegation, agent wallets, and the related API surface.
Every guide, concept, and reference under the Accounts tab, grouped by what you're trying to do.
## Set up a wallet
Generate a new key pair or restore from a seed phrase using ethers.js or the Tether WDK.
Self-custodied wallets for AI agents — how they differ from user wallets.
## Delegate the account (EIP-7702)
What EIP-7702 enables on Stable and the security model.
Apply EIP-7702 to batch payments, spending limits, and session keys.
## Reference
Type-4 transaction format and the authorization list.
Apply an EIP-7702 delegate to a subscription payment flow (cross-listed).
# Accounts on Stable
Source: https://docs.stable.xyz/en/explanation/accounts-overview
Wallets, EIP-7702 delegation, session keys, and spending limits for user and agent accounts on Stable.
An account on Stable is a standard Ethereum EOA that can optionally execute smart contract logic through [EIP-7702 delegation](/en/explanation/eip-7702). Users keep one address and one private key across wallets, batch payments, recurring subscriptions, and session keys. Agents run the same account model without any custodial middleware.
## What you can build
* **Wallets** from a seed phrase, with native USDT0 balance queries and signed transactions.
* **Batched payments**: execute multiple transfers in one atomic transaction via a delegated EOA.
* **Spending limits**: enforce per-transaction or per-day caps on the EOA itself through delegate logic.
* **Session keys**: grant a scoped, time-bound, budget-bound key to a dApp so users don't re-sign every action.
* **Agent wallets**: fund an AI agent with a self-custodied key and let it pay for x402 services autonomously. See [Agentic wallets](/en/reference/agentic-wallets) for providers and integration patterns.
## How Stable differs
* **One address for everything.** No account migration to unlock smart contract features. EIP-7702 delegates code *onto* the existing EOA.
* **USDT0-only gas.** Users don't need a separate native token. A new account funds with USDT0 and can transact immediately.
* **Multi-function delegate pattern.** A single delegate can combine batch, spending limits, session keys, and subscriptions so one delegation covers every feature you ship.
## Start here
Generate or restore a wallet with ethers.js or the Tether WDK.
Apply batch payments, spending limits, and session keys to an existing EOA.
## Next recommended
Jump to the full list of account guides and references.
Why delegation works without account migration.
Apply the Accounts model to a recurring payment flow.
# AI and agents guides
Source: https://docs.stable.xyz/en/explanation/ai-agents-index
All agent guides, concepts, and references: MCP setup, paid MCP tools, agent wallets, and agentic facilitators.
Every guide, concept, and reference under the AI/Agents tab, grouped by what you're trying to do.
## Equip an AI editor
Install Docs MCP, Runtime MCP, agent skills, and paste the Stable context block.
Self-custodied key via WDK — the foundation for agent payments.
## Monetise and consume services
Price any HTTP endpoint per request in USDT0 with x402 middleware.
Wrap x402-paid APIs as MCP tools so an AI client calls and pays for them.
## Reference
Settlement services for agent-to-agent commerce on Stable.
Wallet specs for autonomous agent use.
## Foundation concepts
The HTTP protocol agents use to pay per request.
The signed-authorization standard x402 settles through.
# AI and agents on Stable
Source: https://docs.stable.xyz/en/explanation/ai-agents-overview
Wire MCP servers, agent skills, and x402-paid tools into AI clients so agents can query, simulate, and pay for services on Stable autonomously.
Stable treats AI agents as first-class accounts. An agent holds a self-custodied wallet, looks up docs and chain state through MCP servers, and pays for services per request through x402 — no card on file, no API key rotation, no human in the loop for routine spend. This tab covers how to equip an agent to build on Stable and how to monetise APIs for agent consumption.
## What you can build
* **AI-assisted development** against Stable's Docs MCP and Runtime MCP servers inside Cursor, Claude Code, or any MCP-compatible client.
* **Agent skills** that combine docs lookup, simulation, and on-chain execution into one workflow (for example, "send 100 USDT0 to three addresses").
* **Paid MCP tools**: wrap x402 APIs as MCP tools so an AI client calls them through prompts and pays per call.
* **Agent-payable APIs**: price an HTTP endpoint per request in USDT0 so any agent with a wallet can consume it without a sign-up.
* **Agent wallets** funded with USDT0 only, no custodial middleware.
## How Stable differs
* **Single-asset agent wallets.** USDT0 is the gas token and the payment token, so an agent doesn't juggle ETH + USDT separately.
* **Sub-second settlement.** A paid API call settles inside the HTTP lifecycle (\~700 ms block time).
* **x402 on ERC-3009.** The facilitator submits the on-chain call; the agent only needs to sign an authorization.
## Start here
Wire Docs MCP and Runtime MCP into your AI editor and paste the Stable context block.
Expose x402-paid APIs as MCP tools an agent can call through natural-language prompts.
## Next recommended
Full list of agent guides, facilitator references, and agent wallet specs.
Settlement services for agent-to-agent commerce on Stable.
The x402 server that MCP tools wrap.
# Autobahn
Source: https://docs.stable.xyz/en/explanation/autobahn
Autobahn DAG-based BFT consensus protocol balancing low latency and fault tolerance for StableBFT.
## Tradeoffs in BFT: latency vs. robustness
Modern Byzantine Fault Tolerant (BFT) consensus protocols typically operate under the partial synchrony model. This model assumes that the network eventually stabilizes and message delays remain bounded. While practical for protocol design, real-world deployments rarely enjoy long periods of uninterrupted stability. Instead, systems frequently experience periods of synchrony followed by short disruptions such as latency spikes, node outages, or adversarial conditions. These transient disruptions are referred to as **“blips”**.
Under such conditions, existing consensus protocols are forced to **choose between low latency in stable network conditions and robustness in the presence of faults.**
* **Traditional view-based BFT protocols**, such as PBFT and HotStuff, are optimized for responsiveness during good intervals when the network is stable. However, they suffer from degraded performance when a blip occurs. This degradation, known as a hangover, can persist even after the network has recovered, as backlogged requests accumulate and delay subsequent transactions.
* **DAG-based BFT protocols**, such as [Narwhal & Tusk](https://arxiv.org/pdf/2105.11827)/[Bullshark](https://arxiv.org/pdf/2201.05677), decouple data dissemination (DAG) from consensus (BFT) and propagate transactions asynchronously across replicas. This design enables high throughput and allows the system to continue making progress during network disruptions. However, these protocols tend to incur high latency even during good intervals due to the complexity of their asynchronous ordering mechanisms.
[**Autobahn**](https://arxiv.org/pdf/2401.10369) introduces a new approach that bridges these two design philosophies. It combines the high throughput and blip tolerance of DAG-based protocols with the low latency performance of traditional view-based consensus. At the core of Autobahn is a highly parallel data dissemination layer that continuously propagates proposals at network speed, regardless of consensus progress. On top of this layer, Autobahn runs a low-latency, partially synchronous consensus protocol that commits proposals by referencing lightweight snapshots of the data layer.
A defining feature of Autobahn is its ability to recover from blips without performance degradation. This property, referred to as **seamlessness**, ensures that the system resumes full throughput and low latency immediately after the network stabilizes. No costly reprocessing of backlogged transactions is required. By cleanly separating data availability from ordering and avoiding protocol-induced synchronization delays, Autobahn offers a robust yet responsive foundation for blockchain consensus in real-world conditions.
## Autobahn architecture overview
Autobahn is architected around a clear separation of responsibilities between its two core layers: a **data dissemination layer** and a **consensus layer**. This decoupling is inspired by the design of DAG-based systems like Narwhal, but Autobahn enhances this structure to support seamlessness and lower latency.
The data dissemination layer is responsible for broadcasting client transactions in a scalable, asynchronous manner. It allows each replica to maintain its own lane of transaction batches, which can be propagated and certified independently of the consensus state. These lanes grow continuously, even when the consensus process stalls, ensuring that the system remains responsive to clients at all times.
On top of this, Autobahn runs a partially synchronous consensus layer based on a PBFT-style protocol. However, instead of reaching agreement on individual batches of transactions, consensus is reached on "tip cuts,” which are compact summaries of the latest state of all data lanes. This design allows Autobahn to commit arbitrarily large amounts of data in a single step, minimizing the impact of blips.
HotStuff tightly couples data and consensus, causing stalls when a leader fails. Bullshark incurs high commit latencies due to DAG traversal and data synchronization. Autobahn provides a smoother and faster consensus experience, inheriting the parallelism of DAGs while avoiding their latency pitfalls.
## Data dissemination layer: lanes and cars
In Autobahn, each replica proposes transactions in its own independently advancing chain called a **lane**. Each data proposal in a lane is bundled with a set of acknowledgments from other replicas, forming what the authors call a "**car**" (short for Certification of Available Request). These cars act as proof of availability (PoA), ensuring that at least one correct replica holds the data and can retransmit it if needed.
Cars are chained together by including a reference to the previous car in each new proposal. This structure guarantees that validating the tip of a lane implies the availability of the entire lane history. This transitive proof of availability is key to Autobahn's instant referencing. The consensus layer can refer to a tip cut (a vector of current lane heads) and know that all prior data is retrievable without performing DAG traversal or additional synchronization.
Unlike typical DAG protocols, Autobahn avoids the costly reliable broadcast steps that enforce global availability and non-equivocation. Instead, it uses minimal coordination and trusts that at least one honest replica per PoA holds the data. This enables high throughput and low tail latency even under varying load or partial failures. The data layer continues progressing independently of consensus, ensuring responsiveness during blips.
## Consensus layer: low-latency agreement
The consensus layer in Autobahn builds upon classic PBFT principles but introduces key optimizations to reduce latency and support seamless recovery. Each consensus slot targets the commitment of a "**tip cut**" that captures the latest certified proposal from every replica's lane. The consensus leader proposes this cut using a two-phase commit process: Prepare and Confirm.
During the Prepare phase, replicas vote on the proposed tip cut. If the leader receives enough votes quickly (a full quorum), it can enter the Fast Path and commit immediately with only 3 message delays. If not, it proceeds to the Confirm phase, collecting another quorum of acknowledgments before finalizing the commit in 6 message delays.
A key innovation is the decoupling of data synchronization from consensus voting. Replicas are allowed to vote based on the certified tips alone, even if they haven’t received the full proposal data yet. This is safe because the PoA ensures retrievability. Synchronization happens in parallel and finishes before the execution stage, avoiding protocol stalls. In the event of leader failure or timeout, view changes are triggered using timeout certificates, and new leaders can resume progress efficiently.
## Key properties of Autobahn
Autobahn satisfies the standard **safety** and **liveness** guarantees expected from BFT protocols. Safety ensures that no two correct replicas commit different blocks for the same slot. Liveness guarantees progress after global stabilization time (GST) as long as a correct leader is eventually selected.
More importantly, Autobahn achieves **seamlessness**. It avoids protocol-induced hangovers by allowing the consensus layer to commit arbitrarily large data backlogs in constant time. Even after a blip, as soon as synchrony returns, all data proposals that were successfully disseminated can be committed immediately. This enables Autobahn to operate smoothly in environments with intermittent faults, outperforming traditional BFT protocols in both recovery time and system responsiveness.
In addition, the protocol **scales horizontally**. Each replica contributes to the system's throughput via its own lane, and consensus cuts grow naturally with the number of participants. This makes Autobahn suitable for large-scale deployments requiring both high performance and robustness.
## Low latency meets high resilience
Autobahn was evaluated against leading BFT protocols, particularly Bullshark and HotStuff, under both ideal and fault-injected conditions. The results demonstrate that Autobahn achieves the best of both worlds: it matches Bullshark’s throughput, processing over 230,000 transactions per second, while reducing its latency by more than 50%.
Under good network conditions, Autobahn commits transactions with just 3 to 6 message delays, compared to Bullshark’s 12. This translates to commit latencies as low as 280ms in practice, versus over 590ms for Bullshark. Unlike HotStuff, which suffers from long hangovers after blips due to backlog processing delays, Autobahn commits its entire backlog in a single step as soon as the network stabilizes.
In scenarios involving leader failures or partial network partitions, Autobahn demonstrates seamless recovery. It continues disseminating data during faults and quickly commits accumulated proposals once consensus resumes. These performance advantages make Autobahn a compelling choice for blockchain platforms seeking to combine low-latency responsiveness with high throughput and fault tolerance.
## Further reading
For more technical deep-dives and details, refer to:
* [Autobahn: Seamless high speed BFT](https://arxiv.org/pdf/2401.10369)
## Next recommended
Return to StableBFT, the consensus implementation that Autobahn evolves.
Use Stable's single-slot finality when building against the RPC.
# Bank module
Source: https://docs.stable.xyz/en/explanation/bank-module
The bank precompile exposes ERC-20-compatible token transfers plus mint, burn, and authorization methods backed by the SDK x/bank module.
The `x/bank` module in Stable's SDK handles token balances, transfers, and supply. Its EVM surface (the **bank precompile**) wraps this module and adds ERC-20 semantics plus an authorization layer for privileged mint/burn operations. Contracts that need to move tokens on Stable call the precompile directly without deploying their own token implementation.
## What it exposes
The bank precompile provides standard ERC-20 methods:
* `transfer`, `balanceOf`, `totalSupply`
* `approve`, `transferFrom`, `allowance`, `revoke`
These work from any caller. No registration required.
It also provides privileged methods:
* `mint`: mints new tokens and transfers them to an account.
* `burn`: destroys tokens held by an account.
* `multiTransfer`: moves tokens from one sender to many recipients in a single call.
Mint and burn require the caller contract to be registered on the `x/precompile` allowlist via a governance proposal. Governance-token minting is blocked outright. This keeps supply inflation gated to authorized contracts only.
## When to use it
* A DeFi contract needs to move STABLE or USDT0 on behalf of users: call `transfer` or `transferFrom` directly on the precompile.
* A protocol contract mints or burns tokens based on business logic: register through governance first, then call `mint` / `burn`.
* A payments contract needs one-to-many disbursement: call `multiTransfer` in a single transaction instead of looping transfers.
## Where to find the ABI
The full method signatures, event payloads, and authorization flow are in the [Bank precompile reference](/en/reference/bank-module-api).
## Next recommended
Call `transfer`, `approve`, `mint`, `burn`, and read events.
Return to the full list of precompile-exposed modules.
Understand the dual-role asset model the bank module manages.
# Overview
Source: https://docs.stable.xyz/en/explanation/build-overview
Ship on Stable: wallets and accounts, payments, smart contracts, and AI agents. Start with the quick start or jump straight to the capability you need.
This tab is the how-to side of Stable. Every capability area has a landing page, a short start sequence, and a full guide index. If you've already read the [Learn tab](/en/explanation/learn-overview), this is where you translate those concepts into working code.
New to Stable? Run the [Quick start](/en/tutorial/quick-start) first. It takes five minutes and sends a testnet transaction so the rest of the tab has something to plug into.
## Capability areas
Create wallets, delegate EOAs with EIP-7702, and scope session keys for users and agents.
Send USDT0, build P2P and subscription flows, settle invoices with ERC-3009, and price APIs with x402.
Deploy, verify, and index Solidity contracts, and call Bank / Distribution / Staking precompiles.
Wire MCP servers into AI clients and expose x402-paid tools agents can call through prompts.
## Start here
Connect to testnet, fund a wallet, and send your first USDT0 transaction.
Native and ERC-20 transfers on the same balance, with TypeScript examples.
Scaffold Foundry, configure Stable, and deploy the Counter contract.
## Next recommended
Mainnet and testnet chain IDs, RPC endpoints, and block explorers.
Network information, gas waiver service, and ecosystem providers for going to production.
What changes when you port Ethereum code to Stable: gas token, finality, priority fees.
# Confidential transfer
Source: https://docs.stable.xyz/en/explanation/confidential-transfer
Confidential transfer mechanism for privacy-preserving USDT transactions with regulatory compliance on Stable.
**Confidential Transfer** is a privacy layer on Stable that shields the **amount** of a USDT0 transfer while keeping sender and recipient addresses publicly visible. The shielded amount is readable only by the transacting parties and authorized regulatory auditors, using zero-knowledge (ZK) cryptography to prove validity without revealing the value. The feature is under development; this page describes the target model.
## The problem it solves
Standard on-chain transfers are fully transparent; anyone can read the sender, recipient, and amount. For business payments, that transparency is a data-leakage problem:
* A retailer paying suppliers on-chain exposes order volumes and wholesale pricing to any observer.
* A treasury moving funds between accounts advertises its position sizes.
* A payroll run publishes salary data to the entire network.
Full opacity (Monero-style) would solve this but breaks compliance: regulators and auditors can't verify the transaction. Selective confidentiality (amounts hidden, parties auditable) is the model Stable targets.
## What stays visible, what doesn't
| Field | Visible on-chain | Shielded |
| :----------------- | :--------------- | :------- |
| Sender address | ✓ | |
| Recipient address | ✓ | |
| Transfer amount | | ✓ |
| Auxiliary metadata | | ✓ |
The shielded amount is encrypted. Valid proofs attest that the transfer is balance-consistent (no inflation, no negative amounts) without revealing the value itself. Only the sender, recipient, and authorized regulatory auditors can decrypt the shielded value.
## How it fits the compliance model
Two properties make the design auditable:
* **Deterministic auditor access.** Regulatory auditors hold keys that decrypt shielded amounts for transactions in their jurisdiction. Business privacy is preserved against random observers; compliance scrutiny is not.
* **Standard address transparency.** AML/KYC tooling that operates on address-level flows (sanction checks, source-of-funds analysis) works against the same public address graph as any transparent chain.
## When to use it
Confidential Transfer fits any flow where the amount is commercially sensitive but the counterparties are appropriately public:
* Supplier and invoice payments where order sizes reveal pricing.
* Treasury operations where position sizes reveal strategy.
* Payroll where individual salaries shouldn't be indexable by competitors.
* Large OTC settlements where price discovery against orderbook tape is a risk.
For flows that need address-level privacy as well (e.g. whistleblower donations), Confidential Transfer alone isn't sufficient. Those use cases need additional address-obscuring primitives that Stable doesn't provide.
## Status
Confidential Transfer is in development. See [Roadmap](/en/explanation/technical-roadmap) for timing. The mechanism will ship as a dedicated transfer path alongside standard USDT0 transfers; existing applications that don't opt in are unaffected.
## Next recommended
Understand the asset model confidential transfers shield.
See where confidentiality fits in the end-to-end payment lifecycle.
Track when confidential transfer ships.
# Consensus
Source: https://docs.stable.xyz/en/explanation/consensus
StableBFT consensus protocol design, DAG-based upgrade roadmap, and 200,000 TPS throughput benchmarks.
## PoS consensus with StableBFT
Stable Blockchain leverages **StableBFT**, a customized PoS consensus protocol built on CometBFT, to deliver high throughput, low latency, and strong reliability. StableBFT provides deterministic finality (blocks are final on inclusion, without forks) and Byzantine fault tolerance up to 1/3 of validators failing or acting maliciously.
To further optimize consensus performance, Stable plans to implement the following improvements in the near future:
* **Decoupled Transaction and Consensus Gossiping**: Separating the transaction gossiping layer from the consensus gossiping layer prevents network congestion on the transaction side from interfering with consensus communications.
* **Direct Transaction Broadcasting to the Block Proposer**: In the current model, transactions propagate through peer-to-peer gossiping among nodes, creating high gossip traffic across the network. Stable aims to improve efficiency by enabling transactions to broadcast directly to the block proposer.
## Future roadmap: DAG-based consensus
To significantly accelerate consensus, Stable intends to upgrade its protocol to a DAG-based design capable of delivering up to 5x speed improvements.
Traditional view-based BFT protocols like PBFT and HotStuff are optimized for low latency under stable network conditions. However, they degrade significantly during disruptions, often experiencing long recovery delays after temporary faults.
First-generation DAG-based engines like Narwhal and Tusk demonstrate that decoupling data dissemination from consensus ordering can eliminate single-proposer bottlenecks and also improve robustness under network instability. However, their architecture is not directly compatible with systems like CometBFT, as they diverge from conventional height-based block semantics and mempool designs.
[Autobahn](/en/explanation/autobahn) offers a PBFT-on-DAG architecture that integrates more naturally with Stable’s consensus layer, while delivering low latency under normal conditions and fast recovery in the face of network faults. The Stable team maintains a close relationship with the authors of the Autobahn paper and will leverage Autobahn’s architecture to maximize the performance of StableBFT.
StableBFT, built atop Autobahn, will enable:
* Parallel proposal processing by eliminating the single-leader limitation.
* Faster finality by separating data propagation from final ordering.
* Enhanced resilience against network adversities through robust BFT mechanisms.
This advanced consensus design supports much higher throughput based on the internal proof-of-concept, which has demonstrated over 200,000 TPS (Consensus only) in controlled environments.
## Next recommended
Read the protocol paper that underpins StableBFT's DAG-based upgrade path.
See how blocks move from consensus into parallel execution.
Apply Stable's single-slot finality when building against the RPC.
# Contracts guides
Source: https://docs.stable.xyz/en/explanation/contracts-index
All contract guides, concepts, and references: deploy flow, system modules, JSON-RPC compatibility, and Ethereum differences.
Every guide, concept, and reference under the Contracts tab, grouped by what you're trying to do.
## Build and ship a contract
Scaffold a Foundry project and deploy Counter to Stable testnet.
Upload source to Stablescan so users can read and call your contract.
Build a live event stream with ethers.js, plus historical backfill.
## Call system modules
Call Bank, Distribution, and Staking precompiles from Solidity or ethers.js.
Subscribe to the UnbondingCompleted event emitted via the StableSystem precompile.
## Reference
Precompile addresses and per-module ABI pointers.
Supported `eth_*`, `net_*`, `web3_*`, and `debug_*` methods.
## Foundation concepts
Dual-role balance, reconciliation events, and contract design rules.
Gas token, finality, priority tips, and EVM compatibility.
# Contracts on Stable
Source: https://docs.stable.xyz/en/explanation/contracts-overview
Deploy, verify, and index smart contracts on Stable. Full EVM compatibility plus precompiles for Bank, Distribution, and Staking modules.
Stable is fully EVM-compatible. Solidity, Vyper, Hardhat, Foundry, ethers.js, and viem work unchanged. Existing contracts deploy as-is once you point tooling at Stable's RPC. On top of the standard EVM, Stable exposes protocol-level modules (Bank, Distribution, Staking) as precompiled contracts at fixed addresses, so your Solidity can call into staking and reward distribution without re-implementing them.
## What you can build
* **Standard application contracts** (ERC-20, ERC-721, escrows, AMMs) with any EVM toolchain.
* **Verified, indexed contracts** on Stablescan with live event streams through ethers.js.
* **Protocol-integrated contracts** that call Bank / Distribution / Staking precompiles from Solidity.
* **System-transaction listeners** that watch for protocol-emitted events (for example, unbonding completions) through standard `eth_getLogs`.
## How Stable differs
* **USDT0 is the gas token.** `maxPriorityFeePerGas` must be `0`. The `value` field in native transfers carries USDT0, not ETH. See [Work with USDT0 as gas](/en/how-to/work-with-usdt-gas).
* **USDT0 has a dual role.** Contracts that hold native USDT0 can have their balance changed by ERC-20 `transferFrom` or `permit` — never mirror native balance in a `uint256`. See [USDT0 behavior on Stable](/en/explanation/usdt0-behavior).
* **Precompile addresses are fixed across testnet and mainnet.** Burn them into your contract as constants.
## Start here
Scaffold Foundry, configure Stable, and deploy Counter.
Upload source to Stablescan with forge verify-contract.
Subscribe to events with ethers.js and backfill historical logs.
## Next recommended
Full list of contract guides, precompile references, and system module ABIs.
Call Bank / Distribution / Staking from Solidity and ethers.js.
Which `eth_*` and `debug_*` methods Stable supports.
# Core concepts
Source: https://docs.stable.xyz/en/explanation/core-concepts
Understand Stable's four core concepts before you build: USDT0 as gas, guaranteed blockspace, transfer aggregation, and EVM compatibility.
Four concepts are enough to start building. Each section defines the concept, shows it, and links to the full reference.
## USDT0 as gas
You pay transaction fees in USDT0, the same asset you're already holding and transacting in. There's no second token to fund or manage.
USDT0 is both the native gas asset (18 decimals, read via `address(x).balance`) and an ERC-20 token (6 decimals, read via `USDT0.balanceOf(x)`). Both interfaces operate on the same underlying balance, and the protocol reconciles the 12-digit precision gap automatically.
```solidity theme={"dark"}
// Both read the same balance:
uint256 native = address(user).balance; // 18 decimals
uint256 erc20 = IERC20(USDT0).balanceOf(user); // 6 decimals
```
Balance reconciliation emits extra `Transfer` events at the reserve address `0x6D11e1A6BdCC974ebE1cA73CC2c1Ea3fDE624370`. Indexers that replay `Transfer` events must filter transfers to and from this address, or they will silently double-count balances.
Read more: [USDT0 as gas](/en/explanation/usdt-as-gas-token) · [USDT0 behavior on Stable](/en/explanation/usdt0-behavior).
## Guaranteed blockspace
Stable reserves a portion of each block's capacity for pre-allocated enterprise workloads. Reserved traffic settles with predictable latency and cost even when general traffic is congested; it doesn't compete in the fee market.
This behavior is transparent at the caller level. You submit transactions the normal way; allocations are applied at the protocol level for enrolled accounts.
Read more: [Guaranteed blockspace](/en/explanation/guaranteed-blockspace).
## USDT transfer aggregator
High-volume USDT0 transfers are batched and verified in parallel using a MapReduce-inspired pipeline. Per-account failures are isolated, so one bad transfer doesn't abort the batch.
The caller-side transfer API is unchanged. You submit transfers the normal way and gain throughput without code changes.
Read more: [USDT transfer aggregator](/en/explanation/usdt-transfer-aggregator).
## EVM compatibility
Standard EVM tooling works unchanged. At the EVM level, three behaviors differ from Ethereum (USDT0 as gas, covered above, is the fourth).
**Single-slot finality.** A transaction is final once included in a block. Blocks are produced roughly every 0.7 seconds.
**No priority tips.** `maxPriorityFeePerGas` is always ignored. The effective gas price is the base fee set by the protocol.
```typescript theme={"dark"}
import { ethers } from "ethers";
const block = await provider.getBlock("latest");
const baseFee = block.baseFeePerGas;
const tx = await wallet.sendTransaction({
to: "0xRecipientAddress",
value: ethers.parseEther("0.1"),
maxFeePerGas: baseFee * 2n, // 2x base fee as safety margin
maxPriorityFeePerGas: 0n, // always 0 on Stable
});
await tx.wait();
console.log("Included at gas price:", tx.gasPrice?.toString());
```
```text theme={"dark"}
Included at gas price: 1000000000
```
**Dual-role USDT0, porting risks.** Contracts ported from Ethereum should not mirror native balance, should reject `address(0)` transfers, and should not rely on `EXTCODEHASH` for address-reuse detection.
Porting a contract that mirrors native balance in an internal variable is unsafe on Stable. An external `USDT0.transferFrom` call can drain the contract's native balance without invoking any contract code. Always solvency-check with `address(this).balance` at the moment of transfer.
Read more: [Differences from Ethereum](/en/explanation/ethereum-comparison) · [Contracts on Stable](/en/explanation/contracts-overview) · [USDT0 migration checklist](/en/explanation/usdt0-behavior).
## Confidential transfer (planned)
Stable has a planned feature for zero-knowledge transfers that hide amounts while staying auditable for authorized parties. It is not yet live.
Read more: [Confidential transfer](/en/explanation/confidential-transfer).
## Next recommended
Connect to testnet and send a first transaction.
Port a contract to Stable without hitting dual-role gotchas.
Construct transactions correctly on Stable's fee model.
Validate an integration before shipping to mainnet.
# Overview
Source: https://docs.stable.xyz/en/explanation/core-optimization-overview
Introduction to Stable's full-stack optimization covering consensus, execution, database, and RPC layers.
## Full stack core optimization
The lifecycle of a blockchain transaction, from submission to finalized result, passes through multiple tightly connected stages. A transaction is first submitted through the **RPC** interface, added to the **mempool**, packaged into a block, validated through **consensus**, executed by the **state machine**, and finally written to persistent storage in the **database**. Only after completing this full pipeline does the user receive a confirmed result.
Improving just one stage in isolation is not enough. Any inefficiency in the pipeline can impact the overall performance of the system. This is why Stable focuses on optimizing the blockchain stack from top to bottom.
The following pages describe how Stable upgrades each layer of its architecture (Consensus, Execution, Database, and RPC) to ensure reliable and high-performance transaction processing.
## Next recommended
Learn how StableBFT extends CometBFT for high throughput and low latency.
See how Stable EVM runs transactions in parallel with Block-STM and Optimistic Block Processing.
Understand how decoupled state commitment and memory-mapped storage remove the disk I/O bottleneck.
Understand the split-path RPC architecture separating reads from writes.
# Distribution module
Source: https://docs.stable.xyz/en/explanation/distribution-module
The distribution precompile surfaces staking rewards (withdraw addresses, reward queries, and commission management) to EVM contracts.
The `x/distribution` module handles staking-reward accrual and withdrawal for delegators and validators. Its precompile bridges this behavior into the EVM so a Solidity contract can claim rewards, set withdraw addresses, and query outstanding rewards without interacting with the Cosmos SDK directly.
## What it exposes
* **Set withdraw address**: a delegator designates which address receives their rewards. By default, rewards go to the delegator's own address; setting a withdraw address routes them elsewhere (useful for contract-managed staking).
* **Withdraw delegator rewards**: claims all outstanding rewards from a single validator in one call.
* **Withdraw validator commission**: a validator claims their accumulated commission from delegators' rewards.
* **Query methods**: read reward balances, commission rates, and community-pool state without a transaction.
## Authorization semantics
The precompile checks that the caller is the delegator (or validator) whose state is being modified. You cannot claim someone else's rewards or change their withdraw address.
## When to use it
* A vault or staking aggregator claims rewards on a schedule: call `withdrawDelegatorRewards` directly.
* A DAO routes staking rewards to a treasury address: set a withdraw address once, then rewards flow automatically.
* A front-end displays current reward balances: use the query methods (no transaction needed).
## Where to find the ABI
Full method signatures, input/output types, and emitted events are in the [Distribution precompile reference](/en/reference/distribution-module-api).
## Next recommended
Call `withdrawDelegatorRewards`, set withdraw addresses, and read reward balances.
See how delegation (the source of these rewards) works.
Learn how unbonding completions reach the EVM as events.
# EIP-7702
Source: https://docs.stable.xyz/en/explanation/eip-7702
Batch payments, spending limits, and session keys for EOAs, with no new account or wallet migration required.
Stable supports **EIP-7702**, which allows an EOA to **set its account code to an existing smart contract**. The EOA executes that contract's logic while keeping its original address and private key. The delegation is persistent until the EOA explicitly changes or clears it.
For a full specification, see [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702).
## What EIP-7702 enables on Stable
EIP-7702 lets existing EOAs execute smart contract logic without account migration. In Stable's USDT-centric payment environment, this supports patterns such as:
* **Batch payments**: multiple calls (e.g., paying several recipients in a payroll run) execute in a single atomic transaction.
* **Spending limits**: a delegate contract enforces daily caps or per-transaction limits on the EOA.
* **Session keys**: an EOA grants a dApp scoped, time-limited transaction permissions without exposing the owner's private key.
**Ready to implement?** See the [Account Abstraction (EIP-7702) implementation guide](/en/reference/eip-7702-api) for contract templates, authorization signing, and transaction submission.
## How it works
EIP-7702 introduces a new transaction type (`0x04`) that carries an `authorizationList`. Each authorization designates a smart contract whose code the EOA will execute for that transaction. The flow is:
1. **Choose or deploy a delegate contract**: a standard Solidity contract that implements the logic you want the EOA to run. You can use an existing deployed contract or deploy your own. Use an audited contract whenever possible.
2. **Sign an authorization**: the EOA owner signs a message designating the delegate contract.
3. **Submit an EIP-7702 transaction**: the transaction includes the authorization, and the EOA runs the delegate's code during execution.
After submission, the EOA's account code is set to the delegate. Subsequent transactions to the EOA execute the delegate's logic until the owner clears or replaces the delegation.
## What doesn't change
* **No new account needed**: users keep their existing EOA address and private key. There is no migration step.
* **Existing keys still sign**: the EOA's private key signs the authorization and any follow-on transactions. EIP-7702 does not introduce a new signing scheme.
* **Standard EVM execution**: the delegate runs as regular contract code. Tooling that debugs or traces contract execution works unchanged.
## Security considerations
* **Delegate access is total.** The delegate contract has full execution authority over the EOA for the duration of the delegation. Treat delegate selection as a trust decision: a malicious delegate can drain assets.
* **Delegation persists.** It does not expire at the end of a single transaction. The owner must explicitly clear or replace the delegation when they no longer want it.
* **Gas costs are slightly higher** due to authorization processing, but this is offset when the delegate batches multiple calls. On Stable, where the base fee is 1 gwei and gas is denominated in USDT0, the additional authorization overhead stays well under a cent, comparable to a standard ERC-20 transfer in cost.
## Next recommended
Implement batch payments, spending limits, and session keys against a delegate contract.
Understand the gas model that EIP-7702 transactions run on.
Compare delegation to gas-waived flows where an application pays the user's gas instead.
# Settle with signed authorizations
Source: https://docs.stable.xyz/en/explanation/erc-3009
ERC-3009 allows a token holder to authorize a transfer by signing a message, without calling the contract directly. The settlement mechanism behind x402 payments on Stable.
ERC-3009 lets a token holder authorize a transfer by signing a message. Anyone can then submit that signed authorization to execute the transfer on-chain. The sender never needs to call the contract directly.
This is the settlement mechanism behind [x402](/en/explanation/x402) payments on Stable.
## What problem does it solve?
### The allowance problem
The traditional ERC-20 pattern for third-party transfers is `approve` + `transferFrom`. The sender first calls `approve` to grant a spending allowance, then the third party calls `transferFrom` to move funds. This has well-known issues:
* **Two transactions required**: The sender must send an on-chain `approve` transaction before any transfer can happen. This costs gas and adds latency.
* **Infinite allowance risk**: To avoid repeated approval transactions, many applications request unlimited spending permission, creating a significant security risk.
ERC-3009 takes a different approach. Instead of granting an allowance, the sender signs a one-time authorization for a specific transfer. No separate approval step, no lingering spending permissions.
### The sequential nonce problem
ERC-2612 (`permit`) also enables signed authorizations, but it uses sequential nonces. Multiple permits carry ordering dependencies: if nonce 5 is not consumed, nonce 6 can never execute.
ERC-3009 solves this with **unique nonces**. Each authorization uses a 32-byte value instead of a sequential counter. Multiple authorizations can be created and submitted independently, in any order, without depending on each other.
### Comparison
| **Property** | **ERC-20** (`approve`) | **ERC-2612** (`permit`) | **ERC-3009** |
| :------------------------ | :----------------------------- | :-------------------------------- | :------------------------------ |
| On-chain steps | 2 (`approve` + `transferFrom`) | 1 (`transferFrom`) | 1 (`transferWithAuthorization`) |
| Uses allowance model | Required (on-chain tx) | Yes (sets allowance via `permit`) | Not required (signature) |
| Nonce model | Sequential | Sequential | Unique |
| Concurrent authorizations | No | No | Yes |
## How it works
### transferWithAuthorization
The sender signs an EIP-712 typed data message containing the transfer details. Anyone can then call `transferWithAuthorization` on the token contract with that signed message. The contract verifies the signature, checks the validity window, executes the transfer, and marks the nonce as used.
The signed authorization contains:
* `from`: address of the sender (the signer)
* `to`: address of the recipient
* `value`: transfer amount
* `validAfter`: earliest time this authorization can be executed (Unix timestamp)
* `validBefore`: latest time this authorization can be executed (Unix timestamp)
* `nonce`: 32-byte value ensuring uniqueness
The time window (`validAfter`/`validBefore`) gives the sender precise control over when the transfer can happen. An authorization can be scheduled for the future, given a deadline, or both. If the window expires before submission, the authorization becomes invalid and the funds stay with the sender.
### receiveWithAuthorization
This function works identically to `transferWithAuthorization`, with one additional check: **the caller must be the recipient**. This prevents front-running attacks where a third party observes a pending authorization and submits it first to manipulate transaction ordering.
This is useful in payment scenarios where the recipient (a merchant or service provider) should be the one to initiate settlement.
### cancelAuthorization
The sender can revoke an unused authorization before it is executed. The sender signs an EIP-712 cancellation message, and the contract marks the nonce as used without executing the transfer. The original authorization can no longer be submitted.
## Built-in safety properties
* **One-time use**: Each unique nonce can only be used once. Resubmitting the same signed authorization reverts.
* **Time-bound**: The `validAfter`/`validBefore` window ensures authorizations do not remain valid indefinitely.
* **Self-contained**: One signature authorizes one specific transfer to one specific recipient for one specific amount. No lingering permissions.
* **Non-custodial**: The submitter never holds the sender's funds. The transfer moves directly from sender to recipient within the contract.
## ERC-3009 on Stable
USDT0 on Stable natively implements ERC-3009. Any application can use `transferWithAuthorization` without deploying additional contracts or relay infrastructure.
### Single-asset settlement
On Ethereum, even with ERC-3009, the submitter needs ETH to pay gas for calling `transferWithAuthorization`. The transfer itself is in USDT, but execution depends on a separate native asset.
On Stable, USDT0 serves as both the payment token and the gas token. The entire payment lifecycle, from authorization to on-chain settlement, runs on a single stablecoin. No separate native asset is needed at any step.
This property is what makes ERC-3009 on Stable a strong foundation for higher-level payment protocols. [x402](/en/explanation/x402) leverages this directly, using ERC-3009 as its on-chain settlement mechanism within standard HTTP communication.
## Key takeaways
* ERC-3009 lets a token holder authorize a transfer by signing a message. Anyone can submit that signed authorization to execute the transfer.
* It replaces the ERC-20 allowance model with one-time-use, self-contained authorizations. No `approve` step, no lingering permissions, no double-spend risk.
* Unique nonces allow multiple authorizations to be created and submitted concurrently, in any order.
* USDT0 on Stable natively supports ERC-3009, and because settlement can be completed using USDT0 alone, it provides a practical foundation for x402.
**See also:**
* [USDT as Gas](/en/explanation/usdt-as-gas-token)
* [USDT0 Behavior on Stable](/en/explanation/usdt0-behavior)
* [x402 (HTTP-Native Payments)](/en/explanation/x402)
# Ethereum comparison
Source: https://docs.stable.xyz/en/explanation/ethereum-comparison
Stable is fully EVM-compatible. What stays the same, what changes, and what to watch out for when porting from Ethereum.
Stable is fully EVM-compatible, so most Ethereum tools, libraries, and contract patterns work without modification. The sections below walk through what stays the same and what changes when you move from Ethereum to Stable.
## What stays the same
Stable maintains full compatibility with the Ethereum development ecosystem:
| **Area** | **Compatibility** |
| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Languages | Solidity, Vyper |
| Tooling | Hardhat, Foundry |
| Libraries | ethers.js, web3.js |
| Contract patterns | All standard EVM conventions (ERC-20, ERC-721, ERC-1155, proxies, etc.) |
| RPC interface | Most `eth_*` methods supported (`eth_call`, `eth_sendRawTransaction`, `eth_getBalance`, `eth_getLogs`, `eth_estimateGas`, etc.). For the full list, see [JSON-RPC API](/en/reference/json-rpc-api) |
Existing smart contracts, deployment scripts, and frontend integrations target Stable by changing the RPC endpoint and chain ID.
## What is different
Four behaviors differ from Ethereum.
### 1. Single-slot finality
Ethereum requires multiple block confirmations before a transaction is considered final. Stable provides single-slot finality: a transaction is final once included in a block.
For developers, this means:
* Once a transaction appears in a confirmed block, its state changes are final and irreversible.
* Applications can safely rely on block inclusion as confirmation of settlement.
Even with deterministic finality, applications handling financially sensitive flows should:
* Verify transaction success via RPC or emitted events before proceeding with dependent actions (e.g., unlocks, redemptions).
* Implement retry and reconciliation logic for automation and batch operations to handle transient submission or RPC errors.
### 2. Gas token: USDT0
On Stable, transaction fees are paid in USDT0, not a volatile native token. This provides USDT-denominated, predictable low gas costs.
* Users need USDT0 in their wallet to submit transactions.
* The `value` field in transactions still works for sending USDT0, similar to how ETH is sent on Ethereum.
* See [USDT as gas](/en/explanation/usdt-as-gas-token) for details.
### 3. No priority tips
Stable uses a single-component gas model. There is no tip-based transaction ordering.
* `maxPriorityFeePerGas` is ignored (always 0).
* Transaction ordering is not influenced by fee bidding.
* Wallets should hide or disable the priority tip input field.
* See [Gas pricing](/en/explanation/gas-pricing) for details.
### 4. USDT0 dual-role behavior
USDT0 functions as both the native gas token and an ERC-20 token. This introduces behavioral differences around balance semantics, allowance safety, and certain opcode assumptions. For the full details, see [USDT0 behavior on Stable](/en/explanation/usdt0-behavior).
## Quick comparison
| **Parameter** | **Stable** | **Ethereum** |
| :------------------------------------ | :----------------- | :------------------------ |
| Gas token | USDT0 | ETH |
| Finality | Single-slot | Multi-block confirmations |
| Block time | \~0.7 seconds | \~12 seconds |
| Priority tip (`maxPriorityFeePerGas`) | Ignored (always 0) | Used for ordering |
| EIP-1559 transaction format | Supported | Supported |
| EVM compatibility | Full | N/A |
## Next recommended
Understand the asset model that replaces ETH for gas.
Review the single-component fee model in detail.
Audit contracts for dual-role asset semantics, allowance safety, and `EXTCODEHASH` behavior.
# Compatibility with the Ethereum ecosystem
Source: https://docs.stable.xyz/en/explanation/ethereum-compatibility
Stable's full EVM compatibility with Solidity, Hardhat, Foundry, ethers.js, and standard JSON-RPC interfaces.
Stable is fully compatible with the Ethereum Virtual Machine (EVM), allowing developers to use familiar tools, libraries, and contract patterns without modification.
This ensures seamless migration of existing applications and straightforward onboarding for teams already building in the Ethereum ecosystem.
**Key compatibility features**
* **Languages**: Supports Solidity and Vyper for smart contract development.
* **Tooling**: Works out of the box with standard frameworks such as Hardhat and Foundry.
* **Libraries**: Fully compatible with ethers.js, web3.js, and other common JSON-RPC clients.
* **Contract patterns**: Adheres to standard EVM conventions, including ERC-20 approvals, event emission, and access-control mechanisms.
* **RPC interface**: Exposes the same JSON-RPC methods used by Ethereum networks, so your existing integrations and indexers function without code changes.
# Execution
Source: https://docs.stable.xyz/en/explanation/execution
Stable EVM parallel execution using Block-STM, Optimistic Block Processing, and future StableVM++ improvements.
## Stable EVM
**Stable EVM** is Stable's Ethereum-compatible execution layer. Existing Ethereum tools and wallets like MetaMask interact with Stable unchanged. Stable EVM combines the EVM's developer experience with the modular infrastructure of the Stable SDK.
To bridge the gap between the Stable EVM and the Stable SDK, Stable EVM introduces a set of **precompiles**. These precompiles expose native Stable SDK module functionality to EVM smart contracts, enabling them to call into the core chain logic securely and atomically. Smart contracts can then perform privileged operations such as token transfers, staking, or governance participation.
## Future roadmap 1: Optimistic parallel execution
Historically, blockchain systems have relied on sequential execution, where each transaction is processed one after another to ensure deterministic state across all nodes. While this design guarantees consistency, it severely limits throughput and scalability, especially as modern blockchains aim to support tens of thousands of transactions per second.
To overcome this constraint, Stable is adopting **Block-STM**, a proven parallel execution engine that enables **Optimistic Parallel Execution (OPE)**. This allows transactions to be executed in parallel while preserving determinism, significantly enhancing performance.
### How Block-STM works
Block-STM uses an optimistic concurrency control mechanism: transactions are first executed in parallel under the assumption that they won’t conflict. Then, during a validation phase, any conflicts are detected and handled through re-execution. The process relies on the following five key techniques:
**1. Multi-version memory structure**
Block-STM stores multiple versions of each memory key:
* Each transaction reads the latest version committed by prior transactions.
* During execution, both reads and writes are versioned.
* Later, during validation, these versions are checked for consistency to detect conflicts.
**2. Read-Set / Write-Set based validation**
* During execution, each transaction logs the keys and versions it reads in a Read-Set.
* At the end of execution, it records its Write-Set into the multi-version memory.
* During validation, if another transaction has modified any key in the Read-Set, the transaction is marked as conflicting. It is then aborted and re-executed with an incremented incarnation number.
**3. Fast conflict detection with ESTIMATE markers**
* When a transaction fails, its Write-Set is marked with an ESTIMATE flag.
* If another transaction reads an ESTIMATE-marked value, it immediately halts and waits for re-execution (triggered by a `READ_ERROR`).
* This helps reduce overhead by quickly identifying dependencies without re-executing the full transaction set.
**4. Preset transaction order**
* All transactions within a block are executed according to a preset, deterministic order.
* Validation and commit stages also follow this same order.
* This ensures that even with parallel execution, all nodes reach the same final state.
**5. Collaborative scheduler**
* A Collaborative Scheduler distributes tasks between execution and validation workers in a thread-safe manner.
* It prioritizes lower-index transactions to accelerate early commits and minimize re-execution.
* The scheduler manages transaction incarnations for repeated attempts until they commit successfully.
### Key benefits of Block-STM
* **Parallelism Without Locks**: By leveraging MVCC (Multi-Version Concurrency Control), Block-STM allows multiple transactions to read and write concurrently without the need for mutex locks. Conflicts are only checked after execution, allowing maximum throughput during the initial processing phase.
* **Minimal Overhead via ESTIMATE Markers**: Failed transactions flag their Write-Sets with ESTIMATE markers, signaling dependent transactions to pause early, avoiding wasted execution. This results in faster convergence on valid execution paths.
* **Efficient Scheduling and Prioritized Commits**: Using the Collaborative Scheduler, the system minimizes retries by committing lower-index transactions first. This improves overall throughput and shortens execution cycles.
* **Determinism and Consensus Compatibility**: Because every transaction adheres to a fixed order, even re-executed transactions ultimately commit in the same sequence. This ensures safe and deterministic state agreement across all nodes, preserving consensus integrity even in a parallelized environment.
### OPE on Stable
Stable will incorporate **Optimistic Parallel Execution (OPE)** as a core feature of its execution layer, in conjunction with **Optimistic Block Processing (OBP)**. Please note that OPE and OBP are complementary but fundamentally different strategies.
### About OBP
* OBP is not about parallelism, but about execution timing.
* During the `ProcessProposal` stage, Stable pre-executes blocks while they are being gossiped to other nodes.
* The resulting state is cached in memory and reused during `FinalizeBlock`, saving time and reducing duplicate computation.
By combining OPE and OBP, Stable can minimize both execution latency and resource contention, delivering superior performance under high transaction load.
### Expected performance gains
Internal benchmarks suggest that with **Block-STM-based OPE** and **StableDB** integration, Stable can achieve **at least 2x throughput improvements** in end-to-end transaction processing.
## Future roadmap 2: StableVM++
While efforts like Optimistic Parallel Execution (OPE) and Optimistic Block Processing (OBP) focus on optimizing *how multiple transactions are executed concurrently*, there’s another vital performance lever: **how efficiently each individual transaction is processed**.
Stable is currently exploring alternative EVM implementations to boost execution speed. Among the candidates, **EVMONE**, a high-performance EVM written in C++, stands out as a strong contender to replace the existing Go-based EVM. This switch is projected to deliver up to a **6x increase in EVM execution performance** based on theoretical benchmarks.
## Next recommended
See how decoupled state commitment feeds execution without blocking on disk I/O.
Understand the split-path RPC that surfaces execution results to clients.
Port existing contracts using standard EVM tooling against Stable.
# Finality rules & compatibility guarantees
Source: https://docs.stable.xyz/en/explanation/finality
Stable's single-slot finality rules and developer guidelines for confirming transaction settlement.
Stable processes transactions within an EVM-based execution environment. When a block includes a transaction, the chain applies its effects to state and makes them immediately visible to applications, contracts, and indexers.
### Execution confirmation
A transaction is considered **confirmed** once:
* It is successfully included in a produced block
* State changes (balances, storage, events) can be observed through RPC
During the public testnet phase:
* Treat confirmed state as valid for application logic
* Use monitoring systems to track block continuity
### Settlement considerations
Stable provides single-slot finality, meaning transactions are finalized as soon as they are included in a valid block.
**For developers, this ensures:**
* Once a transaction appears in a confirmed block, its state changes are final and irreversible.
* Applications can safely rely on block inclusion as confirmation of settlement.
**Even with deterministic finality, applications handling financially sensitive flows should:**
* Verify transaction success via RPC or emitted events before proceeding with dependent actions (e.g., unlocks, redemptions).
* Implement retry and reconciliation logic for automation and batch operations to handle transient submission or RPC errors.
### Compatibility commitments
Stable intends to maintain a consistent execution surface for developers throughout testnet growth phases.
**Current commitments:**
* Stable will maintain published system module interfaces and execution behavior unless explicitly noted
* Any potentially disruptive changes will be:
* Announced in advance
* Documented in the Release & Change Log
* Accompanied by migration instructions when necessary
Future updates will introduce:
* A formal compatibility policy
* Change-level classification for developer-facing features
* Clear handling guidance for version transitions
# Flow of Funds
Source: https://docs.stable.xyz/en/explanation/flow-of-funds
End-to-end lifecycle of USDT on Stable, from on-ramp through on-chain transfer to off-ramp settlement.
Stable is the first blockchain purpose-built for stablecoin payments. The network is optimized for high-throughput, low-latency stablecoin transactions, delivering P2P payments and merchant acceptance with immediate settlement in USDT. Application-layer gas sponsorship and waivers allow providers to offer a zero-fee experience for end users, providing the feel of a mainstream payments network while abstracting away the complexity of blockchain systems.
This page describes the complete lifecycle of funds on Stable: how USDT enters the network, moves between participants, and exits back to fiat rails.
## 1. Customer deposit (on-ramp)
A user brings money into the network through one of three primary channels:
* **Crypto transfer**: Any major cryptocurrency is bridged or converted to USDT0 on Stable. USDT0 is the omnichain standard for USDT and the primary form factor on the network.
* **Fiat on-ramp**: Card, ACH, or local payment method converts fiat to USDT0, delivered directly into the user's wallet.
* **CEX withdrawal**: The user withdraws USDT from a supporting centralized exchange, selecting Stable as the destination network. The exchange settles directly into the user's wallet.
In all cases the end state is the same: the user's wallet holds USDT (as USDT0) directly on Stable.
## 2. P2P / merchant transfer (on-chain pay-in)
Once funds are on Stable, the customer sends USDT directly to another user or merchant. Key properties of on-chain transfers:
* **Instant settlement**: transfers settle on-chain immediately.
* **Non-custodial**: in the case of a non-custodial wallet, no PSP or intermediary ever touches user balances between source and destination.
* **Single asset**: because USDT is both the gas and settlement asset, there are no extra tokens in the flow and no hidden spreads.
* **Zero-gas option**: gas waivers allow end users to move funds without needing to manage blockchain fees. See [Gas Waiver](/en/reference/gas-waiver-api) for details.
## 3. User / merchant balance
Merchants receive USDT in their Stable wallet under their own direct control. Funds are held on-chain under the user or merchant's custody. These wallets can be created and managed by a payments provider on behalf of the user.
## 4. Merchant withdrawal (off-ramp / payout)
When a merchant or user requests off-chain fiat settlement:
1. The provider initiates a conversion (USDT → fiat) via banking or payout rails.
2. Funds are credited to the merchant's chosen account.
The provider re-enters the flow only to cash merchants out, not during intra-ecosystem transfers. Day-to-day P2P flows require no intermediation; providers participate only at deposit (USDT transfer to a merchant's account) or withdrawal (USDT → fiat).
## Cross-asset trades
Stable also supports scenarios where the payer holds a non-USDT cryptocurrency.
### User trades into another cryptocurrency
A user may hold or trade into another cryptocurrency (e.g., BTC or ETH) through an integrated exchange, broker, or on-chain DEX. At the point of payment the system automatically converts the selected cryptocurrency into USDT, which is then transmitted to the merchant's Stable wallet. All on-chain settlement continues to occur in USDT regardless of the user's preferred asset.
### Merchant acceptance of cryptocurrency payments
Merchants do not need to accept or manage multiple cryptocurrencies directly. They are always credited with USDT in their Stable wallet, preserving a single settlement currency across the network. This design minimizes FX exposure for merchants and simplifies reconciliation and reporting.
### Provider's role in conversion
The conversion logic (e.g., BTC → USDT) may be handled by an exchange partner, liquidity provider, or the payments provider's own treasury. The merchant remains insulated from volatility or liquidity risks; they only ever receive USDT.
## Next recommended
Understand how USDT0 serves as both native gas and ERC-20 balance on Stable.
See how USDT0 moves onto Stable from other chains via OFT Mesh or Legacy Mesh.
Submit a USDT0 transfer on testnet using standard EVM tooling.
# Gas pricing
Source: https://docs.stable.xyz/en/explanation/gas-pricing
Stable uses a single-component gas fee model with no priority tips, delivering predictable costs denominated in USDT0.
Stable uses a simplified, single-component gas fee model designed to remove fee volatility and deliver predictable, low transaction costs. Transaction ordering is not influenced by tip bidding. The effective gas price is determined solely by the protocol's base fee.
## Why this model
Three properties fall out of the single-component design:
* **Predictable costs**: fees are based purely on base execution cost. Tip auctions don't introduce variance.
* **USDT-denominated pricing**: gas is priced in USDT0, so a developer or user reasoning about cost in dollar terms doesn't have to account for native-token price fluctuations.
* **Extremely low fees**: at a base fee of 1 gwei, a native USDT0 transfer (21,000 gas) costs approximately **0.0000021 USDT0**. Even complex contract interactions stay well under a cent.
## How it compares to Ethereum
| **Parameter** | **Stable** | **Ethereum** |
| :------------------------------------ | :----------------- | :---------------- |
| Gas token | USDT0 | ETH |
| Base fee | Yes | Yes |
| Priority tip (`maxPriorityFeePerGas`) | Ignored (always 0) | Used for ordering |
| EIP-1559 transaction format | Supported | Supported |
Stable accepts EIP-1559 (Type 2) transactions, but `maxPriorityFeePerGas` is always ignored. Transaction ordering is not influenced by tip bidding.
## Implications
* **Wallets** should hide or disable priority-tip input fields. Displaying them may confuse users since the value has no effect.
* **Analytics dashboards** should not track priority fees. They will always be zero.
* **Transaction-construction tooling** should set `maxPriorityFeePerGas` to `0` explicitly, then compute `maxFeePerGas` from the latest block's base fee with a safety margin.
## Next recommended
Construct transactions, estimate gas, and configure tooling against Stable's fee model.
See how USDT0 serves as both native gas and ERC-20 balance.
Review every behavior difference you'll encounter porting from Ethereum.
# Gas waiver
Source: https://docs.stable.xyz/en/explanation/gas-waiver
Gas Waiver lets applications cover gas on behalf of users. Governance-approved waivers submit wrapper transactions that execute the user's signed payload at zero gas price.
Governance-approved addresses (called **waivers**) submit a wrapper transaction that carries the user's signed payload and executes it at `gasPrice = 0`. The user holds no USDT0 and pays no gas. Stable operates one such waiver as a hosted service; partners can also register their own waiver addresses through validator governance.
## How it works
Gas Waiver uses a wrapper-transaction pattern:
1. **The user signs an `InnerTx`** with `gasPrice = 0`. The user's signature is preserved end-to-end; the waiver cannot modify the payload without invalidating it.
2. **A waiver wraps the `InnerTx` into a `WrapperTx`** sent to a protocol marker address (`0x000000000000000000000000000000000000f333`) with `value = 0`, `gasPrice = 0`, and the signed `InnerTx` as its data payload.
3. **Validators detect the marker**, check the waiver's authorization and policy constraints, and execute the inner transaction under the user's identity (`from`, `nonce`, call semantics).
Gas accounting is handled inside the waiver mechanism. The user pays nothing; the wrapper pays nothing; validators absorb the cost against the per-waiver policy.
## Authorization and policy
Waivers are controlled by validator governance, not application logic. Governance provides:
* **Reviewable registration**: every waiver address is registered on-chain and visible in state.
* **Revocation**: validators can remove a misbehaving waiver at any time.
* **Scoped access via `AllowedTarget`**: each waiver is bound to a specific set of target contracts and method selectors. The protocol rejects any wrapper whose inner `to` address and method selector fall outside that scope.
A valid wrapper transaction satisfies all of the following:
* `WrapperTx.to == 0x000000000000000000000000000000000000f333` (the marker address).
* `WrapperTx.from` is a waiver registered on-chain via governance.
* `WrapperTx.gasPrice == 0` and `InnerTx.gasPrice == 0`.
* `WrapperTx.value == 0`.
* `InnerTx.to` and the extracted method selector are permitted by the waiver's `AllowedTarget` policy.
If any condition fails, validators reject the wrapper without executing the inner transaction.
## Security model
* **User signature integrity**: the user signs the `InnerTx`. The waiver cannot mutate the payload without invalidating the signature. Partners are still responsible for ensuring the user signs only the intended payload.
* **On-chain authorization**: authorization lives on-chain. Only governance-registered waiver addresses can produce a valid wrapper submission, regardless of where the request originates.
* **Service-availability boundary**: when partners route through Stable's hosted Waiver Server, submission availability depends on the service. The protocol-level authorization guarantees are unaffected.
## When to use Gas Waiver
Gas Waiver fits any flow where the end user shouldn't have to hold USDT0 for gas:
* Consumer apps onboarding users who have no stablecoin balance yet.
* Agent-driven flows where the agent's wallet funds the gas.
* Enterprise payment rails where the operator absorbs network costs.
For flows where the user does hold USDT0 but wants to bundle multiple calls into one signed transaction, see [EIP-7702 delegation](/en/reference/eip-7702-api) instead.
## Next recommended
Integrate the hosted Waiver Server API with API-key submission and NDJSON responses.
Read the full protocol spec: marker routing, wrapper format, governance controls.
Understand the gas token that the waiver covers.
# Guaranteed blockspace
Source: https://docs.stable.xyz/en/explanation/guaranteed-blockspace
Guaranteed blockspace allocation giving enterprise partners reserved throughput capacity on the Stable network.
**Guaranteed Blockspace** is a dedicated blockspace-allocation model that reserves a fixed share of every block's capacity for enrolled enterprise partners, regardless of broader network conditions. Transactions routed through the guaranteed path execute with predictable latency and cost. Payroll, settlement, and supplier payments don't compete with public mempool traffic.
**Planned.** Guaranteed Blockspace is a forward-looking roadmap item. See [Roadmap](/en/explanation/technical-roadmap) for timing.
## Why this matters
General-purpose chains weren't designed for fee predictability under load:
* **Ethereum**: on May 1, 2022, the Yuga Labs "Otherside" NFT mint pushed peak gas above 8,000 gwei and burned over \$200M in fees, breaking any workload that required deterministic cost.
* **Low-fee networks** like Solana and Base attract MEV and arbitrage spam, so legitimate transactions compete with bot traffic for inclusion.
Enterprise payment flows can't tolerate this variance. Guaranteed Blockspace addresses it directly.
## How the guarantee works
The guarantee is enforced at three layers:
* **Guaranteed mempool**: validators pull guaranteed transactions from a dedicated mempool, isolated from public traffic.
* **Validator-level reservation**: each validator reserves a predefined portion of every block's gas capacity for the guaranteed lane. Deterministic inclusion falls out of this.
* **Dedicated RPC nodes**: the Guaranteed Blockspace API routes transactions through isolated RPC endpoints, so submission latency doesn't spike with public RPC load.
The result, for an enrolled partner:
* **Exclusive routing path**: submissions don't compete with public mempool traffic.
* **Guaranteed inclusion**: capacity is reserved in every block regardless of network congestion.
* **No decentralization trade-off**: validator openness and network participation are preserved; the guarantee lives alongside the public lane, not above it.
* **Reliable on-chain performance** for business-critical operations, even under load.
## Next recommended
See the payment pattern that depends on guaranteed blockspace: timed DvP settlement cycles with deterministic inclusion.
Understand the asset that flows through guaranteed blockspace.
Review how STABLE staking underpins validator blockspace guarantees.
# High performance RPC
Source: https://docs.stable.xyz/en/explanation/high-performance-rpc
Stable's split-path RPC architecture separating reads from writes for high-throughput, low-latency access.
In the pursuit of a high-performance blockchain, it's not enough to only optimize consensus or block production. The RPC layer is a critical component of the end-to-end user experience because it is the interface between the blockchain and its users. Stable proposes a new RPC-dedicated architecture to overcome the limitations of traditional RPC design.
## Why high-performance RPC matters
### The user's gateway to the blockchain
The **Remote Procedure Call (RPC)** interface is the primary way users interact with the blockchain:
* Wallets use RPC to broadcast transactions.
* dApps query state via RPC to render UI with on-chain data, to prepare and simulate transactions, fetch logs and events, etc.
* Explorers, indexers, and bots all rely on RPC for real-time data.
Even if the blockchain can process transactions at lightning speed and produce blocks rapidly, none of it matters if users experience latency and delays due to a slow RPC. In practice, RPC is often the bottleneck in the overall user experience.
Stable’s roadmap toward a high-performance chain explicitly includes **RPC optimization** as a first-class priority.
## The problem with traditional RPC architecture
### Monolithic design and resource contention
Traditionally, an RPC node is simply a repurposed full node with additional RPC endpoints exposed. This means:
* Syncing the chain and serving RPC requests occur on the same instance.
* To scale RPC, teams must spin up entire new full nodes, triggering resource-heavy operations like state sync and consensus setup.
* Consensus, execution, and RPC all share the same CPU, memory, and disk. During periods of high transaction load, a busy component **starves the others**, degrading RPC performance.
In addition, traditional RPC architecture treats read-heavy and write-heavy operations identically. Even though read queries (e.g., `eth_getBalance`) vastly outnumber write transactions, there is no differentiation in how they are handled. This design is inherently inefficient and non-scalable.
## The Stable RPC architecture
Stable introduces a split-path RPC architecture that separates reads from writes and optimizes each independently.
### Core principles
* Separate the RPC into efficient lightweight RPC nodes based on functionality.
* Use lightweight RPCs as edge nodes to enhance scalability.
* Optimize the data path of function-specific RPCs to reduce latency, offering more direct access or management through more efficient data structures
### Performance gains
Internal benchmarks of the new read RPC path demonstrate:
* Supports throughput of over 10,000 RPS, with end-to-end latency under 100ms in the same environment.
* Linear scalability of edge nodes, with no need for full state sync or consensus overhead.
Stable’s new RPC architecture results in a significantly smoother and faster user experience, even during high traffic events.
## Future work
### Optimizing EVM view calls
One exciting area of ongoing research is dedicated support for EVM view operations (`eth_call`):
* These do not require transaction commitment or state updates.
* Execution can happen on lightweight stateless environments using only the current state snapshot.
* A specialized RPC node could be designed specifically for these operations, delivering even faster response times and reducing load on primary full nodes.
### Integration of indexer directly to the node
By integrating an indexer directly into the node, it becomes possible to serve the fastest possible data to dApps.
* Typical architectures: Node → RPC → Indexer (e.g., The Graph) → Storage → dApp
* Proposed Architecture: Node with Indexer → DB → dApp
* This architecture enables much faster data delivery as the indexer is natively integrated into the node, removing the network communication steps.
## Next recommended
Use the `eth_*` methods Stable exposes for contract reads, transaction submission, and log filtering.
See how execution feeds state to the RPC layer.
Review the storage layer the RPC reads query.
# Overview
Source: https://docs.stable.xyz/en/explanation/integrate-overview
Everything you need to connect to Stable, send USDT0, and build payment flows on a USDT-native Layer 1.
Stable is an EVM-compatible Layer 1 where USDT0 is the native gas token. Most Ethereum tools, libraries, and contract patterns work without modification. You connect by pointing your RPC to Stable and switching the chain ID.
This section covers what you need to integrate.
## Connect and fund
Mainnet and testnet chain IDs, RPC endpoints, block explorers.
Get testnet USDT0 via the faucet or bridge from Sepolia.
## Build with USDT0
Native and ERC-20 transfers with TypeScript examples.
Dual-role balance reconciliation, contract design requirements, migration checklist.
Single-slot finality, USDT0 gas, no priority tips.
Gas Waiver integration via the Waiver Server API.
## Payments
Transfer With Authorization: the on-chain settlement primitive.
HTTP-native payments with no accounts or API keys.
Native and ERC-3009 delegated transfers.
## Ecosystem
Providers and infrastructure already live on Stable: bridges, oracles, RPCs, wallets, custody, and more. Browse the [Ecosystem](/en/reference/bridges) section for the full list.
# Key features
Source: https://docs.stable.xyz/en/explanation/key-features
Stable's headline specs (single-slot finality, USDT0 as gas, full EVM compatibility) and the USDT-specific features built on top.
Stable is a delegated Proof-of-Stake Layer 1 with single-slot finality, full EVM compatibility, and USDT0 as the native gas token. The features below are the ones that shape day-to-day integration. Each links to the page that covers it in depth.
## Protocol-level features
| Feature | What it means |
| :------------------------- | :--------------------------------------------------------------------------------------------------- |
| **Single-slot finality** | A transaction is final once it's in a block. No multi-block confirmation wait. |
| **Full EVM compatibility** | Solidity, Vyper, Foundry, Hardhat, ethers, viem, and `eth_*` RPC methods work unchanged. |
| **USDT0 as gas** | One asset serves as both native balance and ERC-20. No separate gas token to hold. |
| **Cross-chain bridging** | USDT0 moves onto Stable from Ethereum, Arbitrum, HyperEVM, Tron, and other chains via LayerZero OFT. |
## USDT-specific features
USDT0 serves as both the native gas token and an ERC-20 token on the same balance.
Governance-authorized waivers submit wrapper transactions that execute at zero gas price on the user's behalf.
Enterprise partners secure reserved capacity in every block for payment flows.
High-volume USDT0 transfers batch into parallelized, fault-tolerant settlement bundles.
Zero-knowledge cryptography shields transfer amounts while keeping parties auditable.
For which upgrades are live today and which are on the roadmap, see [Roadmap](/en/explanation/technical-roadmap).
## Next recommended
Identify what stays the same and what changes when you port from Ethereum to Stable.
Trace USDT from on-ramp through on-chain transfer to off-ramp settlement.
Walk through the consensus, execution, database, and RPC layers that deliver these features.
# Overview
Source: https://docs.stable.xyz/en/explanation/learn-overview
Concepts, architecture, and use case narratives for Stable. What it is, how it differs from Ethereum, and the mental models behind USDT0 as gas.
This tab holds the concepts, architecture, and narrative context for Stable. It's the background reading that sits behind every capability tab — why USDT0 is the gas token, how single-slot finality works, how payments, confidential transfers, and guaranteed blockspace fit together, and what's on the roadmap.
Looking to ship something specific? Jump to the capability tab instead: [Accounts](/en/explanation/accounts-overview), [Payments](/en/explanation/payments-overview), [Contracts](/en/explanation/contracts-overview), [AI/Agents](/en/explanation/ai-agents-overview), or [Infrastructure](/en/explanation/infrastructure-overview). Or run the [Quick start](/en/tutorial/quick-start) to send a testnet transaction in five minutes.
## Foundation
What Stable is and how to read this documentation.
Headline specs: single-slot finality, USDT0 as gas, full EVM compatibility.
What stays the same and what changes when you port from Ethereum.
USDT0 dual role, guaranteed blockspace, transfer aggregator, finality.
## USDT0 behavior
Dual-role balance, reconciliation events, and contract design rules.
Why Stable uses USDT0 to pay for gas and what that means for fees.
How USDT moves end-to-end across Stable.
Every USDT0-specific feature with links to each.
## Architecture
Consensus, execution, database, and RPC layers in one page.
The performance work behind sub-second finality.
Single-slot finality, reorg behavior, and what "confirmed" means.
Base-fee-only model priced in USDT0.
## Use case narratives
Why Stable fits P2P, subscriptions, invoices, and pay-per-call.
Batched and scheduled payroll runs on Stable.
Letting applications cover gas for their users.
Upcoming confidential payment flows.
## Next recommended
Send a first transaction on testnet before diving deeper.
What's shipped and what's coming.
Common answers about chain IDs, RPC endpoints, and onboarding.
# Overview
Source: https://docs.stable.xyz/en/explanation/overview
Connect to Stable, confirm the chain ID, and see what changes from Ethereum before you build.
Stable is a Layer 1 where USDT0 is the native gas token, and standard EVM tooling (Solidity, Foundry, Hardhat, ethers, viem, and the `eth_*` JSON-RPC methods) works unchanged. Point your RPC at Stable and confirm the chain ID:
```bash theme={"dark"}
cast chain-id --rpc-url https://rpc.stable.xyz
```
```text theme={"dark"}
988
```
For the full list of endpoints (mainnet and testnet), see [Connect](/en/reference/connect).
## What to read next
If you haven't sent a transaction on Stable yet, start with [Quick start](/en/tutorial/quick-start) for a five-minute walkthrough on testnet. Then pick the path that matches what you're building:
* Wallets, delegation, and agent accounts → [Accounts](/en/explanation/accounts-overview).
* Moving USDT0 or building payment flows → [Payments](/en/explanation/payments-overview).
* Deploying smart contracts → [Contracts](/en/explanation/contracts-overview).
* Wiring AI editors or building agent-paid services → [AI/Agents](/en/explanation/ai-agents-overview).
* Running a full or archive node, ecosystem providers, or covering gas → [Infrastructure](/en/explanation/infrastructure-overview).
Before you ship, [Core concepts](/en/explanation/core-concepts) covers four behaviors that differ from Ethereum (USDT0 dual role, guaranteed blockspace, transfer aggregator, EVM finality). [Production readiness](/en/how-to/production-readiness) is the mainnet-readiness checklist.
## Next recommended
Connect to testnet, fund a wallet with USDT0, and send a transaction in under five minutes.
The four concepts (USDT0 dual role, guaranteed blockspace, transfer aggregator, EVM finality) you need before you build.
What stays the same and what changes when you port from Ethereum.
Validate an integration before shipping to mainnet.
# Use cases overview
Source: https://docs.stable.xyz/en/explanation/payment-use-cases-overview
Payment patterns on Stable: P2P transfers, subscription billing, invoice settlement, pay-per-call APIs.
Stable supports multiple payment patterns, from simple wallet-to-wallet transfers to agent-driven service payments. The use cases below cover the patterns that are production-ready today. For patterns on the horizon (guaranteed settlement, confidential payments, agent-to-agent commerce), see [Upcoming use cases](/en/explanation/upcoming-use-cases).
## Live use cases
Wallet-to-wallet USDT0 transfers. Sub-second settlement, zero gas via Gas Waiver.
Pull-based recurring billing via EIP-7702. Subscriber authorizes once, provider collects each cycle.
B2B invoice payment with deterministic nonces. On-chain settlement links automatically to the invoice.
Per-request HTTP payments via x402 middleware. No accounts, no API keys, no billing cycles.
## Shared foundations
Most patterns build on the same two protocols:
* **[ERC-3009](/en/explanation/erc-3009)**: signed authorizations for delegated settlement. Used by invoices, pay-per-call, and P2P application-initiated transfers.
* **[x402](/en/explanation/x402)**: HTTP-native payments over standard headers. Used by pay-per-call APIs and MCP-driven payment flows.
* **[EIP-7702](/en/explanation/eip-7702)**: EOA delegation for recurring authorization. Used by subscription billing.
## Next recommended
Start with the core settlement standard.
Preview agent-to-agent commerce, guaranteed settlement, and confidential payments.
# Payments guides
Source: https://docs.stable.xyz/en/explanation/payments-index
All payment guides, concepts, and references: Send USDT0, P2P, subscriptions, invoices, pay-per-call, bridging, and zero-gas.
Every guide, concept, and reference under the Payments tab, grouped by what you're trying to do.
## Send and transfer
Native and ERC-20 transfers on the same balance.
Transfer USDT0 with the fee covered by a Gas Waiver.
Construct transactions correctly: priority tip 0, `value` in USDT0.
Bridge from Ethereum Sepolia using LayerZero OFT.
## Build a payment flow
Wallet + send + receive + history in one app.
Pull-based recurring billing via EIP-7702.
ERC-3009 with deterministic nonces for invoice settlement.
Monetise HTTP endpoints with x402 middleware.
## Protocols and references
Transfer With Authorization: the signed-settlement primitive.
Server responds 402, client signs, facilitator settles on-chain.
Model overview and comparison to traditional rails.
Pull-based billing model and trade-offs.
Deterministic-nonce settlement model.
x402 pricing and endpoint discovery model.
Guaranteed settlement, confidential payments, agent-to-agent.
# Payments on Stable
Source: https://docs.stable.xyz/en/explanation/payments-overview
Build payment flows on Stable: P2P transfers, subscriptions, invoices, pay-per-call APIs, zero-gas flows, and bridging.
Stable is built around payments. USDT0 is the native asset and the gas token, so settlement and fees share one balance. Single-slot finality means a transfer clears in under a second. ERC-3009, EIP-7702, and x402 are native primitives, not workarounds — you can settle with a signature, pull from a delegated account, or charge per HTTP request without running a billing stack.
## What you can build
* **P2P transfers** — native USDT0 sends with 21k gas and sub-second finality.
* **Subscriptions** — pull-based recurring billing with EIP-7702 delegation.
* **Invoice settlement** — ERC-3009 `transferWithAuthorization` with deterministic nonces for exact reconciliation.
* **Pay-per-call APIs** — x402 middleware for per-request USDT0 payments; no API keys, no sign-ups.
* **Zero-gas UX** — application-sponsored transactions via the Gas Waiver service.
* **Cross-chain USDT0** — LayerZero OFT bridging from Ethereum and other networks.
## How Stable differs
* **One asset for everything**: the sender doesn't hold a separate gas token.
* **Native ERC-3009**: USDT0 implements `transferWithAuthorization` directly, so payments settle with a signature and no approve step.
* **Deterministic finality**: a block is final the moment it's committed. No confirmation waits.
* **Native x402**: the facilitator pays no gas through the Gas Waiver, so per-request settlement costs stay below a cent.
## Start here
Native and ERC-20 transfers on the same balance.
Transfer USDT0 with the fee covered by a Gas Waiver.
Build a wallet + send + receive + history app from scratch.
Price HTTP endpoints per request with x402.
## Payment primitives
Transfer With Authorization: the settlement standard behind invoices and x402.
Server responds 402, client signs ERC-3009, facilitator settles on-chain.
## Next recommended
Every guide, concept, and reference under the Payments tab.
Pull-based recurring billing via EIP-7702.
ERC-3009 with deterministic nonces for exact reconciliation.
# Storage (StableDB)
Source: https://docs.stable.xyz/en/explanation/stable-db
StableDB architecture using decoupled state commitment, MemDB, and mmap to eliminate disk I/O bottlenecks.
One of the main bottlenecks in end-to-end blockchain performance is **Disk I/O**. Specifically, committing and storing state data after block execution creates the key bottleneck. Stable tackles this problem with architectural innovations such as `MemDB`, `VersionDB`, and memory-mapped storage (`mmap`) to dramatically improve throughput.
## Why disk I/O is a bottleneck
### State transition and persistence
Every time a block of transactions is executed, the blockchain transitions from one state to the next. This process has two fundamental stages:
1. **State Commitment**: The new application state is committed after transaction execution.
2. **State Storage**: The committed state is persisted to disk for long-term access and historical verification.
In conventional architectures, state storage is **tightly coupled** with state commitment. This means that:
* The node must wait for the new state to be fully stored on disk before proceeding with the next block's execution.
* The state data is written in random disk locations that are not mapped to fixed addresses. This leads to high latency when retrieving state data during execution of subsequent transactions.
Even if consensus and execution layers are heavily optimized, this serialized dependency on slow disk operations caps the achievable performance of the entire system.
## Optimizing DB operations for higher throughput
To overcome these limitations, Stable proposes a two-fold architectural enhancement focused on **decoupling state operations** and **introducing memory-mapped DB optimizations**.
### 1. Decoupling state commitment and storage
The first step is to decouple the state commitment from its storage:
* After committing a new state, the node immediately proceeds to execute the next block.
* The actual persistence of state to disk occurs asynchronously in the background.
This separation allows execution to happen instantly and leapfrog the latency of slow disk writes, thereby eliminating blocking dependencies and eventually improving end-to-end performance.
### 2. Introducing `MemDB` and `VersionDB` via `mmap`
Stable enhances this with a dual-database model powered by `mmap` (memory-mapped file access):
* **MemDB (Memory DB)**:
* Stores recent and active states that are frequently accessed.
* Uses fixed address mapping via `mmap`, enabling fast and deterministic lookups.
* Ideal for most transaction workloads which target recently modified state.
* **VersionDB (Historical DB)**:
* Stores older, historical states on disk.
* Optimized for archival and long-range queries, not for high-frequency access.
This design ensures that **hot data is served from fast, memory-resident structures**, while cold data is offloaded to slower, persistent storage. By combining `mmap` access with smart state tiering, Stable can significantly reduce the DB read/write latency during block execution.
## Expected gains and precedents
This architectural optimization is not just theoretical. It is already being implemented by high-performance blockchains such as Sei and Cronos. Both have adopted similar decoupled architectures with memory-mapped DBs and have observed **up to 2x increases in overall TPS**.
Stable also anticipates comparable gains, as the architecture will no longer be bottlenecked by the storage layer. Instead, consensus and execution performance can scale without being throttled by disk operations.
## Further reading
For more technical deep-dives and implementation details, refer to:
* [ADR-065: Cosmos Store V2 Architecture](https://docs.cosmos.network/main/build/architecture/adr-065-store-v2)
* [MemIAVL: A Practical Guide](https://hackmd.io/@yihuang/rkeCvy5xh)
* [Cronos MemIAVL Node Configuration](https://docs.cronos.org/for-node-hosts/running-nodes/memiavl)
* [Sei’s DB Design Approach](https://4pillars.io/ko/articles/sei-db)
## Next recommended
See how the RPC layer exposes state reads without contending with writes.
Understand how execution writes into the storage layer covered here.
Review the consensus layer that orders blocks before they reach storage.
# Staking module
Source: https://docs.stable.xyz/en/explanation/staking-module
The staking precompile exposes delegation, undelegation, and validator management to EVM contracts, with authorization checks that enforce caller identity.
The `x/staking` module controls validator participation and delegation on Stable. Its precompile makes these operations callable from Solidity, so a contract can delegate STABLE, undelegate after the unbonding period, redelegate between validators, or query validator state without leaving the EVM.
## What it exposes
* **Create validator**: register a new validator with description, commission rate, and initial self-delegation.
* **Edit validator**: update validator metadata and commission parameters.
* **Delegate**: stake STABLE with a validator.
* **Undelegate**: begin unbonding from a validator (the tokens become available after the unbonding period).
* **Redelegate**: move stake between validators without unbonding.
* **Cancel unbonding delegation**: reverse an in-progress unbonding before the period completes.
* **Query methods**: read validator sets, delegation records, unbonding records, and parameters.
## Authorization semantics
The precompile performs two checks:
1. The bond denom (staking token) must be registered at chain initialization. On Stable this is the STABLE token.
2. The caller must match the validator or delegator whose state is being modified. You cannot delegate on someone else's behalf by calling the precompile directly.
## Unbonding completions
When an unbonding period finishes, the tokens become liquid, but the SDK handles this quietly and the EVM doesn't see a direct event. Stable's [system transaction](/en/explanation/system-transactions) mechanism bridges this: the protocol emits an `UnbondingCompleted` event through the `StableSystem` precompile once the unbonding clears, so dApps can subscribe via standard EVM logs.
## When to use it
* A staking protocol manages delegation from a vault contract: call `delegate` and `undelegate` as users deposit and withdraw.
* A governance dashboard needs a live validator set: use the query methods.
* A restaking or liquid-staking product tracks unbonding completions: subscribe to the `UnbondingCompleted` event (see [Tracking unbonding completions](/en/how-to/track-unbonding) once that guide ships).
## Where to find the ABI
Full method signatures, struct definitions, and emitted events are in the [Staking precompile reference](/en/reference/staking-module-api).
## Next recommended
Call `delegate`, `undelegate`, `redelegate`, and read validator state.
Learn how unbonding completions reach the EVM as events.
Withdraw rewards earned from the delegations managed here.
# System modules
Source: https://docs.stable.xyz/en/explanation/system-modules-overview
Stable exposes Cosmos-SDK protocol modules to the EVM through precompiled contracts, so dApps can call staking, distribution, and bank operations without rewriting them.
Stable's core protocol behavior lives in SDK modules: `x/bank`, `x/distribution`, `x/staking`. To make this behavior accessible from the EVM, Stable exposes each module as a **precompiled contract** at a fixed address. Contracts written in Solidity call the precompile directly, and the EVM routes the call into the native SDK handler. Precompiles are implemented at the protocol level, making them significantly more gas-efficient than an equivalent Solidity re-implementation.
## The three modules
| Module | Precompile address | Purpose |
| :--------------------------------------------------------- | :--------------------- | :--------------------------------------------------------------------------------------------- |
| [Bank](/en/explanation/bank-module) | `0x0000…1003` (STABLE) | Token transfers, balance accounting, allowance management, mint/burn for authorized contracts. |
| [Distribution](/en/explanation/distribution-module) | `0x0000…0801` | Staking-reward claims, reward queries, withdraw-address management. |
| [Staking](/en/explanation/staking-module) | `0x0000…0800` | Delegation, undelegation, redelegation, validator queries. |
| [System transactions](/en/explanation/system-transactions) | `0x0000…9999` | Protocol-emitted EVM events for SDK-layer operations (e.g. unbonding completions). |
Each page above explains what the module does, when to use it, and where to find its ABI.
## Why precompiles, not Solidity
Two reasons:
* **Gas efficiency.** A precompile runs in the protocol's native execution path. An equivalent Solidity contract would re-implement the same logic with significantly higher gas cost.
* **Single source of truth.** Staking, distribution, and token supply are protocol-level state. Exposing them through precompiles avoids maintaining a duplicate Solidity implementation that could drift from the SDK.
## Authorization
Some precompile methods (`mint`, `burn`, protocol-level staking operations) require caller authorization. The `x/precompile` module maintains an on-chain whitelist, and calls from unregistered contracts revert. This keeps privileged operations governance-gated without blocking general EVM use of read/transfer methods.
## Next recommended
Understand token transfers, allowances, and the mint/burn authorization model.
See how delegation and validator management reach the EVM.
Learn how protocol-level events like unbonding completions surface as EVM logs.
# System transactions
Source: https://docs.stable.xyz/en/explanation/system-transactions
System transactions bridge Cosmos-SDK events to EVM logs. Protocol-level operations like unbonding completions surface as standard Transfer-style events dApps can subscribe to.
EVM applications subscribe to on-chain activity through standard interfaces like `eth_getLogs`. But some of the most important operations on Stable (staking unbonding completions, for example) happen inside SDK modules that don't naturally emit EVM events. **System transactions** close this visibility gap: the protocol itself submits EVM transactions that emit events for SDK-layer operations, making them indexable through the same log stream dApps already use.
## Why this matters
Consider tracking when a user's tokens finish unbonding. Without system transactions, a dApp would need to either:
* Run a separate indexer that watches for SDK events and stores them in its own database. Operational overhead plus a new failure point.
* Poll a REST endpoint periodically. 5–10 second latency, higher RPC load, two client stacks (web3 + REST) to maintain.
System transactions give dApps real-time event notifications through the same WebSocket connections they already use for EVM logs. No separate indexer. No REST polling.
## How the flow works
```
1. Protocol event: An SDK-layer operation completes (e.g. staking unbonding).
2. Detection: The x/stable EndBlocker detects the event and queues it in state.
3. System TX: In the next block's PrepareProposal, the protocol generates a
system transaction calling the StableSystem precompile.
4. EVM emission: The precompile processes the queued entries and emits standard
EVM events — dApps see them through eth_getLogs and subscriptions.
```
The system transaction is created by validators during block proposal, not by users. It lands at the front of the block, before any user transactions.
## The StableSystem precompile
Events flow through the `StableSystem` precompile at `0x0000000000000000000000000000000000009999`. Today it emits one event (`UnbondingCompleted`) for staking unbondings. The protocol is designed to extend this to other SDK operations (validator commission changes, governance execution) under the same pattern.
## Security model
Two properties keep the event stream trustworthy:
* **Protocol-only sender.** System transactions use `0x0000000000000000000000000000000000000001` as their sender. The EVM state-transition rules only allow transactions to the `StableSystem` precompile from this address to skip signature verification. Users cannot forge events or call restricted precompile functions from their own transactions.
* **Deterministic emission.** Every honest validator produces the same system transaction for the same protocol events. There's no additional trust assumption beyond standard consensus.
## Batch processing
To bound block size, each block processes at most 100 unbonding completions. At Stable's \~700 ms block time, that's roughly 9,000 completions per minute, well above typical staking activity. If a burst exceeds the per-block limit, completions queue in FIFO order and drain over subsequent blocks.
There's a one-block (\~700 ms) delay between the SDK event and the EVM emission, which is negligible relative to the 7-day unbonding period itself.
## Where to find the ABI
The `StableSystem` interface, event signatures, and sender-authorization rules are in the [System transactions reference](/en/reference/system-transactions-api).
## Next recommended
Review the `IStableSystem` interface, gas accounting, and authorization rules.
See the SDK operations that surface as system-transaction events.
Return to the precompile-exposed module list.
# Tech overview
Source: https://docs.stable.xyz/en/explanation/tech-overview
How Stable's four-layer stack (consensus, execution, storage, and RPC) is tuned end-to-end for stablecoin payment throughput.
**What's live today:** StableBFT consensus, Stable EVM (full EVM compatibility), StableDB, and the split-path RPC layer are all production-ready. Autobahn (DAG-based consensus) and StableVM++ (optimistic parallel execution) are roadmap items. See the [Roadmap](/en/explanation/technical-roadmap) for timelines.
You can deploy Solidity or Vyper contracts on Stable today using Hardhat, Foundry, or any standard EVM tooling, and your contracts work without modification. What changes: gas is paid in USDT0, transactions reach single-slot finality, and every layer of the stack is tuned for stablecoin throughput.
## StableBFT
**Status: Live**
Stable Blockchain leverages **StableBFT**, a customized PoS consensus protocol built on CometBFT, for high throughput, low latency, and strong reliability across the network.
**Planned:** DAG-based **Autobahn** consensus with decoupled data dissemination. See the [Roadmap](/en/explanation/technical-roadmap#phase-3-full-stack-optimized-layer-for-usdt).
## Stable EVM
**Status: Live**
**Stable EVM** is Stable's Ethereum-compatible execution layer. Standard Ethereum tools and wallets interact with the chain unchanged. A set of **precompiles** bridges Stable EVM to the Stable SDK, letting EVM smart contracts call into core chain logic atomically.
**Planned:** **StableVM++** with optimistic parallel execution (Block-STM). See the [Roadmap](/en/explanation/technical-roadmap#phase-3-full-stack-optimized-layer-for-usdt).
## StableDB
**Status: Live**
Stable fixes a major blockchain bottleneck: slow disk storage after each block. It separates state commitment from storage so blocks process without delay. `MemDB` and `VersionDB`, powered by `mmap`, keep recent data in memory while older data is stored efficiently, boosting overall throughput.
## High performance RPC
**Status: Live**
A slow RPC layer ruins the user experience even on a fast chain. Stable addresses this with a **split-path architecture** that separates operations by function, deploying lightweight, specialized RPC nodes for faster response times.
**Planned:** RPC nodes optimized for EVM view calls, plus a node-integrated indexer. See the [Roadmap](/en/explanation/technical-roadmap#phase-3-full-stack-optimized-layer-for-usdt).
## Next recommended
Walk through each layer of the stack (consensus, execution, database, RPC) and how they fit together.
Go deeper into StableBFT and how it extends CometBFT for high throughput.
See which performance upgrades are shipping next and when.
# Roadmap
Source: https://docs.stable.xyz/en/explanation/technical-roadmap
Stable's phased optimization roadmap: what's live today, what's shipping next, and what comes after.
Stable optimizes every layer of the transaction pipeline (consensus, execution, storage, RPC, and USDT-specific flows) across three phases. This page marks what's shipped, what's in progress, and what's still ahead.
## Phase 1: Foundational layer for USDT
Status: **Live on mainnet.**
### StableBFT: Live
A customized PoS consensus protocol built on CometBFT. Delivers deterministic finality and Byzantine fault tolerance up to one-third of validators. See [Consensus](/en/explanation/consensus) for the current implementation.
### USDT as native gas: Live
USDT0 is the native asset for gas payment and value transfer, and simultaneously supports the ERC-20 surface (`approve`, `transfer`, `transferFrom`, `permit`). See [USDT as gas](/en/explanation/usdt-as-gas-token).
### Stable Pay & Stable Name: In progress
Stable Pay is a Web2.5 UX wallet experience designed to simplify onboarding for new users while remaining compatible with existing Web3 wallets. Stable Name is a user-friendly aliasing system that replaces raw EVM addresses with human-readable identifiers for sending and receiving tokens.
## Phase 2: Experience layer for USDT
Status: **Mixed.** State DB optimization is live; the remaining items are in development.
### State DB optimization: Live
Stable decouples state commitment from state storage. Validators commit the latest state in memory while historical state is deferred to disk. `MemDB` and `VersionDB` powered by `mmap` handle real-time commitment without blocking on disk I/O. See [Storage (StableDB)](/en/explanation/stable-db).
### Optimistic parallel execution: Planned
Real-world telemetry shows 60–80% of transactions interact with disjoint state and can safely execute in parallel. Stable will execute transactions optimistically under the no-conflict assumption, with rollback and sequential re-execution on detected conflicts. This preserves correctness while improving throughput.
### USDT Transfer Aggregator: Planned
Aggregating mechanism that groups USDT0 transfers and processes them collectively, reducing per-transaction overhead and improving overall throughput. See [USDT transfer aggregator](/en/explanation/usdt-transfer-aggregator).
### Guaranteed blockspace: Planned
Reserved block capacity for enterprise partners, enforced through validator-level reservations and dedicated RPC endpoints. Delivers predictable latency for mission-critical payment flows even under network congestion. See [Guaranteed blockspace](/en/explanation/guaranteed-blockspace).
## Phase 3: Full-stack optimized layer for USDT
Status: **Planned.**
### StableBFT on Autobahn
DAG-based BFT consensus that integrates naturally with Stable's CometBFT-based consensus layer. See [StableBFT](/en/explanation/consensus) for the current protocol and [Autobahn](/en/explanation/autobahn) for the target architecture. Internal proof-of-concept has demonstrated over 200,000 TPS (consensus only) in controlled environments.
### StableVM++
High-performance execution engine that replaces the Go-based EVM with a C++ implementation. Projected to deliver up to a 6x improvement in EVM execution speed.
### High performance RPC
A full RPC stack covering node-level enhancements (real-time chain state processing), node-integrated indexing (low-latency application APIs), scalable pub/sub over WebSocket, and a hybrid load balancer that routes by operation type. See [High performance RPC](/en/explanation/high-performance-rpc) for the current split-path architecture.
## Next recommended
Walk through the current state of the stack the roadmap evolves.
Review the economic model that funds validator incentives across the roadmap.
Return to the architecture summary.
# Upcoming use cases
Source: https://docs.stable.xyz/en/explanation/upcoming-use-cases
Payment patterns on the horizon for Stable: guaranteed settlement, confidential payments, and agent-to-agent commerce.
Stable is building toward payment patterns that go beyond simple transfers and API billing. The cases below cover time-guaranteed settlement, privacy-preserving payments, and autonomous agent commerce. Some are functional today in early form; others depend on Stable features currently in development.
## Guaranteed settlement
Reliable payment settlement backed by reserved block capacity, ensuring transaction inclusion regardless of network conditions.
### Concept
Some payments are part of a timed settlement cycle, not a standalone transfer. In these flows, settlement must complete before the cycle closes so the next state transition can proceed as scheduled. If the required payments are delayed past that window, the cycle may fail, roll to the next window, or require manual recovery.
Stable's [Guaranteed Blockspace](/en/explanation/guaranteed-blockspace) addresses this by reserving execution capacity for qualifying payment flows. The goal is not simply faster settlement, but operationally reliable completion within exact timing constraints.
### Expected scenario
A tokenized asset platform runs scheduled DvP (Delivery versus Payment) settlement every few minutes. Cash legs are submitted as a batch, and securities are released only if the full payment batch is included before the current settlement cycle closes. Under normal conditions, this clears immediately. During burst traffic, partial inclusion would create a failed or rolled settlement cycle. With Guaranteed Settlement, the platform reserves capacity for the payment batch so the cycle can close deterministically.
### What enables it
Guaranteed blockspace turns on-chain payment into a schedulable operation. Settlement cycles can be designed with hard timing assumptions, batch payments can be committed atomically within a single window, and upstream systems can treat block inclusion as a dependency rather than a hope.
## Confidential payments
Privacy-preserving USDT0 transfers where selected transaction details are shielded from public observers while remaining verifiable by the transacting parties and authorized auditors.
### Concept
Standard on-chain transfers are fully transparent; anyone can see the sender, recipient, and amount. For business payments, this transparency can expose commercially sensitive information to anyone monitoring the chain.
Stable is developing [Confidential Transfer](/en/explanation/confidential-transfer), a privacy layer using zero-knowledge cryptography that enables selective confidentiality for on-chain transactions. The shielded values are accessible only to the involved parties and authorized regulatory auditors.
### Expected scenario
A large retailer settles inventory procurement with multiple suppliers on-chain. On a transparent chain, competitors can monitor these transactions to reverse-engineer supplier relationships, order volumes, and wholesale pricing. With confidential transfers, the commercially sensitive details are shielded while the on-chain record still serves as a verifiable settlement receipt for both parties and authorized auditors.
## Agent-to-agent payment
Payments initiated and settled between AI agents autonomously, without human approval or intervention in the transaction loop.
### Concept
As AI agents take on more operational tasks, they will need to procure services from other agents. In current workflows, this requires a human in the loop to approve each purchase, select a vendor, or verify that the counterparty is trustworthy. Agent-to-agent payment removes that bottleneck by letting agents find, evaluate, and pay for services autonomously within a single transaction loop.
This pattern depends on several emerging protocols working together: agent discovery and trust ([ERC-8004](https://eips.ethereum.org/EIPS/eip-8004)), secure communication ([XMTP](https://xmtp.org)), and a payment rail that can settle in real time ([x402](/en/explanation/x402)).
### Expected flow
1. **Discover**: the buyer agent queries an ERC-8004 Identity Registry to find agents that offer the required capability (e.g., image generation). The registry returns matching agent identities with associated metadata.
2. **Verify**: the buyer checks the ERC-8004 registries for each candidate. Identity, reputation scores, and validation proofs determine which providers are trustworthy enough to transact with.
3. **Negotiate**: the buyer sends task parameters to the selected provider over XMTP. The two agents agree on price, deadline, and deliverable format through encrypted messaging.
4. **Pay**: the buyer calls the provider's HTTP endpoint. The provider responds with 402. The buyer signs an ERC-3009 authorization and retries with the payment header. The facilitator settles the payment on Stable, and the provider returns the result.
5. **Rate**: after delivery, the buyer posts feedback to the ERC-8004 Reputation Registry, updating the provider's score for future interactions.
### What enables it
Agent-to-agent payment turns service procurement into a fully programmable loop. Agents can compare providers, switch vendors, and settle payments in real time without human scheduling or approval queues. This makes it possible to build autonomous supply chains where agents continuously source, pay for, and deliver services at machine speed, scaling commerce beyond what manual coordination can support.
## Next recommended
Review the protocol-level mechanism behind guaranteed settlement.
See the privacy model Stable is building.
Understand the settlement protocol behind agent-to-agent flows.
# USDT as gas
Source: https://docs.stable.xyz/en/explanation/usdt-as-gas-token
How USDT0 functions as Stable's native gas token, replacing volatile assets for predictable transaction fees.
**You pay fees in USDT0. No second token, no wrapping, no ETH-equivalent to keep topped up.** USDT0 serves as both the native gas token and an ERC-20 token on the same balance. The same asset that moves as payment also pays for the transaction that moves it. Fees are denominated in dollars, not a volatile native token.
This design comes with behavioral differences from Ethereum that affect balance semantics, allowance safety, and certain opcode assumptions. If you're porting a contract from Ethereum, see [USDT0 behavior on Stable](/en/explanation/usdt0-behavior) for the migration checklist before deploying.
## Abstract
Stable is an EVM-compatible blockchain that uses USDT0 as its native gas token. USDT0 simultaneously functions as the native asset for gas payment and value transfer, and as an ERC-20 token supporting `approve`, `transfer`, `transferFrom`, and `permit`.
This document specifies Stable's USDT0 gas mechanism, describes the resulting behavioral differences, and defines required and recommended development patterns for smart contracts deployed on Stable.
## Version note
With Stable v1.2.0, USDT0 becomes the native gas token on Stable, replacing gUSDT. As part of this transition:
* gUSDT is being sunset.
* Existing gUSDT balances are automatically converted to USDT0.
* Users and applications no longer need wrapping and unwrapping flows to pay fees or move value.
After v1.2.0, USDT0 serves as both:
* the network fee asset (gas), and
* a standard ERC20 token with `approve`, `permit`, `transfer`, and `transferFrom`.
## Network addresses
USDT0 token contract addresses:
* Testnet: [0x78cf24370174180738c5b8e352b6d14c83a6c9a9](https://testnet.stablescan.xyz/token/0x78cf24370174180738c5b8e352b6d14c83a6c9a9)
* Mainnet: [0x779ded0c9e1022225f8e0630b35a9b54be713736](https://stablescan.xyz/token/0x779ded0c9e1022225f8e0630b35a9b54be713736)
## Terminology
* **Stable**: An EVM-compatible blockchain where USDT0 is the native gas token.
* **USDT0**: An omnichain version of USDT that functions both as:
* the native asset used for gas and value transfers, and
* an ERC20 token with allowance and permit semantics.
* **Native balance**: The balance returned by `address(x).balance`, denominated in USDT0.
* **Gas fee**: The transaction fee paid in USDT0, calculated under an EIP-1559-style fee market.
## What is USDT0?
USDT0 is an omnichain representation of USDT using LayerZero’s Omnichain Fungible Token (OFT) standard. USDT0 is pegged 1:1 with USDT and is designed to move across multiple blockchains without requiring traditional bridge workflows or wrapped representations.
When transferring USDT0 across chains, the token is locked on some source chains (depending on the chain’s native USDT support) or burned. It is then minted on the destination chain via LayerZero’s cross-chain messaging. This preserves a 1:1 peg while consolidating liquidity into a single interoperable asset rather than fragmented chain-local pools.
For users, this enables faster onboarding, reduced operational complexity, and improved liquidity mobility.
## USDT0 and Stable
USDT0 is the core asset that powers Stable’s onchain economics and day-to-day usage. Because the same asset is used for both paying fees and transferring value, Stable reduces friction for:
* **Everyday users**: Simpler onboarding and fewer token concepts
* **Developers**: Simpler fee and value flows
* **Enterprises**: Simplified accounting and treasury operations
Stable can also access deep USDT liquidity from day one by enabling users to onboard USDT0 from other networks via LayerZero.
## Assumptions and prerequisites
For the content below, you are expected to understand:
* Solidity execution semantics and native value transfers
* ERC20 allowance mechanics and permit flows
* Standard smart contract security patterns, including Checks-Effects-Interactions
## 1. Gas and fee model
### 1.1 Overview
Stable denominates all transaction fees in USDT0. Gas pricing follows an EIP-1559-style model with a dynamically adjusting base fee.
The transaction fee is defined as:
```
fee = gasUsed × baseFee
```
Transactions may specify `maxFeePerGas` using standard EIP-1559 parameters.
*Note: Stable does not support priority tips. Do not set `maxPriorityFeePerGas`, or the tip amount will be lost.*
### 1.2 Transaction submission
Clients should fetch the latest base fee from the most recent block and include a safety margin when computing `maxFeePerGas`.
Example (illustrative):
```javascript theme={"dark"}
const block = await provider.getBlock("latest");
const baseFee = block.baseFeePerGas;
const maxPriorityFeePerGas = 1n;
const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas;
```
### 1.3 Acquiring USDT0
Accounts obtain USDT0 by:
* Bridging USDT0 from other supported chains
* Receiving transfers from other accounts on Stable
## 2. How Stable enables USDT0 as the gas token
Stable charges gas in USDT0 using a pre-charge and refund settlement model.
### Example transaction
Alice sends 100 USDT0 to Bob.
### 2.1 Ante-handler phase
During transaction validation in `MonoEVMAnteHandler`:
1. Alice’s USDT0 balance is read.
2. The protocol verifies Alice can cover:
* the transaction value (100 USDT0), and
* the maximum possible gas fee (`gasWanted × fee`).
3. The maximum gas fee is transferred upfront:
* `alice → fee_collector` in USDT0.
### 2.2 Execution phase
During `ApplyTransaction`:
1. The EVM executes the transaction.
2. Actual gas consumption is recorded.
3. The value transfer is applied:
* `alice → bob` transfers 100 USDT0.
### 2.3 Settlement phase
After execution:
1. The protocol computes the unused portion of the pre-charged fee:
```
refund = (gasWanted − gasUsed) × baseFee
```
2. The unused fee is refunded:
* `fee_collector → alice` in USDT0.
## 3. Balance semantics and behavioral differences
### 3.1 Native balance mutability
On Ethereum, a contract’s native balance typically changes only as a result of contract execution.
On Stable, a contract’s native USDT0 balance may also change due to ERC20 allowance-based operations, including `transferFrom` and `permit`. These operations can reduce a contract’s native balance without invoking any contract code.
As a result, the following assumption is invalid on Stable:
* A contract’s native balance can only decrease if the contract is called.
## 4. Contract design requirements
### 4.1 Prohibited pattern: mirrored balance accounting
Contracts must not rely on internal variables to mirror native balance.
Example of an unsafe pattern:
```solidity theme={"dark"}
uint256 public deposited;
function deposit() external payable {
deposited += msg.value;
}
```
Such variables can diverge from the actual native balance if USDT0 is drained through allowance-based transfers.
### 4.2 Required pattern: real-balance solvency checks
All native value transfers must verify solvency using `address(this).balance` immediately before transfer.
Example:
```solidity theme={"dark"}
require(address(this).balance >= amount, "insufficient balance");
```
Withdrawals must follow Checks-Effects-Interactions ordering:
```solidity theme={"dark"}
uint256 amount = credit[msg.sender];
credit[msg.sender] = 0;
require(address(this).balance >= amount);
payable(msg.sender).call{value: amount}("");
```
### 4.3 State progression must be balance-independent
Protocol logic that depends on progression, milestones, or completion conditions must track these explicitly using non-balance state variables, such as counters or epochs.
Native balances must be used only for solvency verification at the moment of payout.
### 4.4 Allowance exposure
Contracts that custody user funds should not grant USDT0 allowances to external addresses.
If allowances are unavoidable, contracts should:
* Approve only exact amounts
* Reset allowances immediately after use
* Treat residual drain risk as a known limitation
## 5. Address state assumptions
### 5.1 EXTCODEHASH
Contracts must not rely on `EXTCODEHASH(addr) == 0x0` to infer that an address has never been used.
Any notion of address usage must be tracked explicitly within contract state.
Example:
```solidity theme={"dark"}
mapping(address => bool) public used;
```
## 6. Zero address handling
On Stable:
* Native USDT0 transfers to `address(0)` revert.
* ERC20 USDT0 transfers to `address(0)` also revert.
There is no supported mechanism for burning USDT0 by transferring to the zero address.
Contracts must:
* Explicitly reject `address(0)` as a recipient
* Redesign any logic that assumes zero-address burns
* Use explicit sink contracts if irreversible loss semantics are required
## 7. Testing requirements
Test suites for Stable deployments should include:
* Allowance-based drain scenarios (`approve` + `transferFrom`)
* Solvency enforcement using real native balance
* Address usage logic without reliance on `EXTCODEHASH`
* Explicit failure cases for zero-address transfers
## 8. Migration checklist
When porting contracts from Ethereum to Stable:
* Remove internal native balance mirrors
* Replace all solvency checks with `address(this).balance`
* Remove all native or ERC20 transfers to `address(0)`
* Audit all USDT0 approvals
* Add tests covering permit and allowance-based flows
## 9. Summary
Stable’s use of USDT0 as a gas token provides predictable fees and unified value accounting while changing core assumptions about native balance behavior.
Correct contract design on Stable requires:
* Treating USDT0 as a dual-role asset
* Enforcing solvency against real balances
* Avoiding allowance-based drain paths
* Eliminating reliance on Ethereum-specific balance and address assumptions
## FAQ
**We’re using USDT0 as the wrapped native token today. After this upgrade, which token should be treated as the wrapped native?**
USDT0 becomes both the native token and an ERC-20 token after the upgrade. You should use USDT0 directly, and wrapping or unwrapping is no longer required.
**What happens to the original USDT0 contract address (`0x779Ded0c9e1022225f8E0630b35a9b54bE713736`)?**
Nothing changes. The same address remains valid and continues to represent USDT0.
**After the upgrade, is the native token address `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` (instead of `0x0000000000000000000000000000000000001000`)?**
Yes. After the upgrade, the native token identifier/address is `0x779Ded0c9e1022225f8E0630b35a9b54bE713736`.
**What about `0x0000000000000000000000000000000000001000`? Is it still used as the token address for gUSDT, and should we keep it on our side?**
No. You can remove it. It will not be used after the upgrade.
**For DEX calldata, will protocols stop using `0x0000000000000000000000000000000000001000` as the “native token” identifier and use `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` instead?**
Correct. After the upgrade, DEXs should use `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` as the native token identifier.
## Next recommended
Submit a USDT0 transfer on testnet using standard EVM tooling.
Build transactions against Stable's single-component fee model.
Audit contracts for dual-role asset semantics, balance reconciliation, and `EXTCODEHASH` behavior.
# Overview
Source: https://docs.stable.xyz/en/explanation/usdt-features-overview
How Stable's five USDT-specific features work together to make stablecoin payment flows practical at scale.
Stable's USDT-specific features aren't a menu of independent options. They compose. Each one removes a specific friction that shows up when stablecoin payments move from demos to production. This page explains why the five features exist together.
## The friction stack
Most stablecoin payment architecture on general-purpose chains runs into the same stack of problems:
1. **Users have to hold a second token** (ETH, SOL) to pay gas for a transaction that moves stablecoins. An onboarding step that bleeds conversion.
2. **Even with a second token, the user has to cover gas.** This breaks the "send a dollar for a dollar" mental model that merchants and payment apps need.
3. **Transaction costs fluctuate** with network activity. Payroll, treasury ops, and batch settlements can't plan cost or inclusion.
4. **Per-transaction limits cap throughput.** High-volume USDT flows hit chain-wide contention and degrade alongside unrelated activity.
5. **Every transaction is publicly observable.** Supplier payments, salary runs, and treasury moves leak commercially sensitive data.
## How the features compose
Stable addresses each friction with a dedicated mechanism:
| Friction | Mechanism | Page |
| :-------------------------- | :-------------------------------------------- | :------------------------------------------------------------------- |
| Separate gas token | USDT0 is the native gas token | [USDT as gas](/en/explanation/usdt-as-gas-token) |
| User pays gas at all | Governance-authorized waivers cover gas | [Gas waiver](/en/explanation/gas-waiver) |
| Cost and inclusion variance | Reserved block capacity for enrolled partners | [Guaranteed blockspace](/en/explanation/guaranteed-blockspace) |
| Throughput ceiling | Parallelized USDT0 transfer batching | [USDT transfer aggregator](/en/explanation/usdt-transfer-aggregator) |
| Public amount visibility | Selective privacy via ZK cryptography | [Confidential transfer](/en/explanation/confidential-transfer) |
Any one of these helps. Taken together, they make Stable a chain where a payments team can model cost, latency, and privacy the same way they would on a traditional card network, with the settlement finality of a blockchain.
For the full set of Stable's differences from a general-purpose EVM chain, see [Ethereum comparison](/en/explanation/ethereum-comparison). For a worked example of USDT moving end-to-end through these features, see [Flow of funds](/en/explanation/flow-of-funds).
## Next recommended
Understand the asset that replaces ETH for gas and payment at once.
Trace USDT from on-ramp through on-chain transfer to off-ramp settlement.
See how USDT0 moves onto Stable from other chains.
# USDT transfer aggregator
Source: https://docs.stable.xyz/en/explanation/usdt-transfer-aggregator
USDT Transfer Aggregator batching high-volume USDT0 transfers into parallelized, fault-tolerant settlement.
The **USDT Transfer Aggregator** bundles USDT0 transfers into parallelized, fault-tolerant batches instead of processing each transfer sequentially. It isolates USDT0 throughput from the rest of the execution pipeline so high-volume stablecoin activity doesn't crowd out other transactions.
**Planned.** The aggregator is a forward-looking roadmap item. The content below describes the target design. See [Roadmap](/en/explanation/technical-roadmap) for timing.
## Why it exists
Two constraints pull against each other:
* Traditional ERC-20 transfers are processed sequentially. Under high load, that's a bottleneck.
* Simply giving USDT0 priority would crowd out other transactions and degrade general chain performance.
The aggregator resolves this by pulling USDT0 transfers into a dedicated parallel pipeline, leaving the main execution path untouched for everything else.
## Parallel aggregation and verification
At the heart of the transfer aggregation system is a parallelizable aggregation and verification pipeline, inspired by the `MapReduce` computational model. Instead of processing each transfer in order, the system performs bundle-level computation, aggregating inputs and outputs across accounts before executing balance updates.
### Key steps
1. **Aggregate Account Diffs**
* Each transfer is mapped to a sender and recipient.
* A diff journal is generated for each account representing the net token movement:
* Negative values for total debits (send).
* Positive values for total credits (receive).
2. **Balance Verification**
* The system ensures global balance invariants: total input equals total output.
* Each account's net change is verified independently in parallel to confirm sufficient funds.
* Accounts without sufficient balance are flagged without halting the bundle.
3. **MapReduce Model for Parallelism**
* **Map Phase**: Compute the net delta for each account based on all incoming/outgoing transfers.
* **Reduce Phase**: Aggregate these deltas to determine the final state update.
## Technical highlights
### Parallel computation model
* Leverages parallelism in precompiled contracts to check balances and compute diffs concurrently.
* Greatly reduces execution time compared to traditional, sequential ERC20 processing.
### Dependency analysis
* Identifies overlapping transfers (e.g., multiple sends from the same account).
* Pre-flags high-risk transfers (e.g., likely insufficient funds) to minimize cascading failures.
### Modular failure handling
* Transfers are isolated at the account level, so only problematic accounts are affected.
* Non-conflicting transfers execute and finalize normally.
### Selective failure handling
Traditional transfer handling is all-or-nothing within a block. Stable’s aggregation model introduces granular, per-account failure isolation:
* If an account’s `current balance + net diff < 0`, the system marks only that account’s transfers as failed.
* Transfers involving other accounts proceed as normal.
* This selective rollback mechanism ensures that invalid or malicious transfers do not compromise the integrity of an entire bundle.
## Proposer-driven or reputation-based sorting
To further optimize execution and avoid state conflicts, Stable incorporates pre-processing ordering mechanisms for aggregated transfers:
* **Reputation-Based Sorting**: Senders with strong histories or proven reliability are prioritized, reducing risk of failures and reordering.
* **Proposer-Based Ordering**: Transactions may be sorted by a trusted proposer node that structures the bundle to minimize conflicts and maximize throughput.
* **Bundled Transfer Prioritization**: Aggregated USDT transfers are prioritized before general transactions, reducing dependency collisions and unlocking cleaner execution windows.
Stable's USDT Transfer Aggregator is a targeted optimization that maximizes throughput for USDT0 transfers without degrading general transaction processing. By combining parallel execution, modular failure handling, and smart ordering strategies, Stable offers a scalable foundation for stablecoin-driven economies. Fast, frequent, and frictionless token transfers are the norm.
## Next recommended
See the payment patterns that benefit most from aggregated throughput: P2P, subscriptions, pay-per-call.
See the parallel execution engine the aggregator builds on.
Understand the asset model the aggregator moves.
# USDT0 behavior on Stable
Source: https://docs.stable.xyz/en/explanation/usdt0-behavior
USDT0 operates as both the native asset and an ERC-20 token on Stable. Audit contracts for balance reconciliation, allowance safety, and opcode behavior.
**If you're porting a contract from Ethereum, read this page before deploying.** USDT0 on Stable is both the native gas token and an ERC-20 token on the same balance. Four Ethereum-assumed behaviors break as a result: a contract's native balance can change without a call into the contract, `EXTCODEHASH` can oscillate between zero and empty hash, zero-address transfers revert, and a single logical transfer can emit multiple `Transfer` events from fractional-balance reconciliation.
This page walks through each case and gives safe contract patterns. If you only read one section, read the [Migration checklist](#migration-checklist). It's the port-your-Ethereum-contract-here summary.
## Dual-role overview
USDT0 on Stable is both the native gas token and an ERC-20 token. This dual-role model affects balance behavior, contract design, and event handling. The sections below walk through every case where the dual role changes expected behavior.
For background on why USDT0 operates this way, see [USDT as gas](/en/explanation/usdt-as-gas-token). To experience the behavior through real transfers, see [Send your first USDT0](/en/tutorial/send-usdt0).
## Balance reconciliation
USDT0 uses 18 decimals as the native asset and 6 decimals as an ERC-20 token. Native transfers and ERC-20 transfers operate on the same underlying balance, but the 12-digit precision gap means the system must reconcile fractional amounts when a transfer involves sub-integer precision.
```
before
0.000001 USDT0 (ERC-20) + 0.000000000000000000 USDT0 (internal)
// address(account).balance = 0.000001000000000000
// USDT0.balanceOf(account) = 0.000001
if transfer 0.0000001 USDT0 to another account
after
0.000000 USDT0 (ERC-20) + 0.000000900000000000 USDT0 (internal)
// address(account).balance = 0.000000900000000000
// USDT0.balanceOf(account) = 0.000000
```
This can cause `address(account).balance` and `USDT0.balanceOf(account)` to differ by up to 0.000001 USDT0.
## Event handling
Each reconciliation transfer emits an additional `Transfer` event. A single logical USDT0 transfer can produce up to two extra `Transfer` events depending on how the sender's and receiver's fractional balances are affected:
* **Sender adjustment**: If the sender's fractional balance is insufficient, 0.000001 USDT0 is moved from the sender to the reserve address. This emits an extra `Transfer` event.
* **Receiver adjustment**: If the receiver's fractional balance overflows, 0.000001 USDT0 is moved from the reserve address to the receiver. This emits an extra `Transfer` event.
* **Both adjustments**: If both conditions occur in the same transfer, the reserve is bypassed. The sender transfers 0.000001 USDT0 directly to the receiver as part of the main transfer. No extra event is emitted.
These auxiliary events involve the reserve address `0x6D11e1A6BdCC974ebE1cA73CC2c1Ea3fDE624370`. Indexers and off-chain services that track USDT0 balances by replaying `Transfer` events must filter or account for transfers to and from this address.
## Contract design requirements
### Native balance mutability
On Ethereum, a contract's native balance typically changes only as a result of contract execution. On Stable, a contract's native USDT0 balance may also change due to ERC-20 allowance-based operations, including `transferFrom` and `permit`. These operations can reduce a contract's native balance without invoking any contract code.
As a result, the following assumption is invalid on Stable:
> A contract's native balance can only decrease if the contract is called.
### Do not mirror native balance
On Ethereum, it is common to track deposits with an internal variable. On Stable, this is unsafe because ERC-20 `transferFrom` can drain the native balance externally.
```solidity theme={"dark"}
// UNSAFE on Stable
uint256 public deposited;
function deposit() external payable {
deposited += msg.value;
}
```
### Always check real balance before transfers
All native value transfers must verify solvency using `address(this).balance` just before transfer, not internal accounting variables:
```solidity theme={"dark"}
// SAFE
function withdraw() external {
uint256 amount = credit[msg.sender];
credit[msg.sender] = 0;
require(address(this).balance >= amount, "insufficient balance");
payable(msg.sender).call{value: amount}("");
}
```
### State progression must be balance-independent
Protocol logic that depends on progression, milestones, or completion conditions must track these explicitly using non-balance state variables, such as counters or epochs. Native balances should be used only for solvency verification at the moment of payout.
### No zero-address transfers
On Stable, both native and ERC-20 transfers to `address(0)` revert.
```solidity theme={"dark"}
// REVERT on Stable
payable(address(0)).call{value: amount}("")
USDT0.transfer(address(0), amount);
```
Any contract logic that sends native USDT0 should validate the recipient and explicitly reject `address(0)` before the transfer call:
```solidity theme={"dark"}
// SAFE
require(recipient != address(0), "zero address recipient");
payable(recipient).call{value: amount}("");
```
If a contract uses zero-address transfers as a burn mechanism, it must be redesigned. Use explicit sink contracts if irreversible loss semantics are required.
### EXTCODEHASH behavior
On Ethereum, the `EXTCODEHASH` opcode returns:
* **Zero hash** (`0x0000...`): if an address has never been used (nonce=0, balance=0, no code).
* **Empty hash** (`0xc5d2…a470`, the Keccak-256 hash of empty code): if an address exists but has no code.
On Ethereum, once an address transitions from zero hash to empty hash, it cannot return to zero hash. On Stable, because USDT0 supports `permit()`-based approvals, an address can create approvals without sending a transaction. Combined with `transferFrom()`, this allows native balance changes without nonce increment, potentially allowing `EXTCODEHASH` to oscillate between zero hash and empty hash.
```solidity theme={"dark"}
// UNSAFE on Stable
function isUnusedAddress(address addr) public view returns (bool) {
bytes32 codeHash;
assembly {
codeHash := extcodehash(addr)
}
return codeHash == bytes32(0);
}
```
Use explicit tracking instead:
```solidity theme={"dark"}
// SAFE
contract SafeAddressTracker {
mapping(address => bool) public hasBeenUsed;
function markAsUsed(address addr) internal {
hasBeenUsed[addr] = true;
}
function isUnused(address addr) public view returns (bool) {
return !hasBeenUsed[addr];
}
}
```
## Testing requirements
Test suites for Stable deployments should include:
* Allowance-based drain scenarios (`approve` + `transferFrom`)
* Solvency enforcement using real native balance
* Address usage logic without reliance on `EXTCODEHASH`
* Explicit failure cases for zero-address transfers
## Migration checklist
When porting contracts from Ethereum to Stable:
* Remove internal native balance mirrors
* Replace all solvency checks with `address(this).balance`
* Remove all native or ERC-20 transfers to `address(0)`
* Audit all USDT0 approvals
* Add tests covering `permit` and allowance-based flows
* Verify off-chain indexers handle auxiliary `Transfer` events from fractional balance reconciliation
## Key takeaways
Correct contract design on Stable requires:
* Treating USDT0 as a dual-role asset
* Enforcing solvency against real balances
* Avoiding allowance-based drain paths
* Eliminating reliance on Ethereum-specific balance and address assumptions
Off-chain services and indexers should:
* Account for auxiliary `Transfer` events from fractional balance reconciliation
* Use direct balance queries instead of event-based balance reconstruction
## Next recommended
Understand why USDT0 operates as both the native asset and an ERC-20 token.
Submit a USDT0 transfer on testnet via native and ERC-20 paths.
Review every behavior difference when porting from Ethereum.
# Bridging to Stable
Source: https://docs.stable.xyz/en/explanation/usdt0-bridging
Cross-chain bridging mechanics for USDT0 (OFT Mesh) and native USDT (Legacy Mesh) into Stable.
USDT reaches Stable via one of two bridge paths, depending on what form it takes on the source chain. Both paths deliver USDT0 into the user's wallet on Stable.
**Two paths, one outcome:**
* **OFT Mesh**: source chain already has USDT0. Burn on source, mint on Stable. 1:1 across chains. Examples: Arbitrum, Ethereum, Ink, Bera, MegaETH.
* **Legacy Mesh**: source chain has native USDT only. Routes through Arbitrum as hub. 0.03% fee on the transferred amount. Examples: Tron, TON.
The sections below describe each path in detail.
## USDT0 OFT Mesh vs Legacy Mesh
Stable participates in two complementary cross-chain transfer networks.
### OFT Mesh
Any chain that supports USDT0 can participate in the OFT Mesh. Within the OFT Mesh, USDT0 cross-chain transfers maintain a 1:1 value ratio. When a transfer occurs, the USDT0 tokens on the source chain are burned and an equivalent amount is minted on the destination chain. Current OFT Mesh participants include Arbitrum, Ethereum, Ink, Bera, MegaETH, and Stable.
### Legacy Mesh
Any chain with native USDT (rather than USDT0) can route through the Legacy Mesh. The Legacy Mesh follows a hub-and-spoke architecture with Arbitrum serving as the central hub for USDT0. This model leverages a USDT0 liquidity pool on Arbitrum. The USDT0 team charges a 0.03% fee on the transferred amount. Current Legacy Mesh participants include Tron and TON.
Ethereum and Arbitrum participate in both meshes: users on these chains can bridge via the OFT path (burn/mint USDT0) or the Legacy path (lock native USDT through the Arbitrum hub).
***
## Path 1: Bridging USDT0 to Stable (OFT-supported chains)
This path applies when the user already holds USDT0 on an OFT-supported source chain such as Arbitrum or Ink.
### Actors
| Name | On-chain? | Responsible party |
| --------------------- | --------- | --------------------------- |
| User | N/A | User |
| USDT0 OUpgradable | ✅ | Smart contract by USDT0 |
| LayerZero Endpoint V2 | ✅ | Smart contract by LayerZero |
| MessageLib Registry | ✅ | Smart contract by LayerZero |
| Executor | ❌ | LayerZero Labs |
| USDT0 DVN | ❌ | USDT0 |
| LayerZero DVN | ❌ | LayerZero Labs |
### Flow diagram
### Detailed steps
#### 1. Initiate transfer (on-chain, source chain)
The user calls the `lzSend` method on the **USDT0 OUpgradable** contract on the source chain. The transaction includes the message payload, destination LayerZero endpoint and contract address, and configuration parameters such as gas limits and fees.
#### 2. Packet creation (on-chain, source chain)
The source LayerZero Endpoint packages the OApp's message, encodes it using the designated source MessageLib contract, and emits it to the Security Stack (DVNs) and Executor, completing the send transaction.
#### 3. Message verification (off-chain, DVNs)
Decentralized Verifier Networks (DVNs) independently verify the message using their own methods. Only DVNs authorized by the OApp can perform verification. For USDT0 bridging, both the **LayerZero DVN** and the **USDT0 DVN** must approve the message.
#### 4. Mark as verifiable (on-chain, Stable)
Once both DVNs verify the message, the destination MessageLib contract marks it as verifiable.
#### 5. Verification commitment (off-chain, Executor)
The Executor commits the verified message to the destination LayerZero Endpoint, preparing it for execution.
#### 6. Packet validation (on-chain, Stable)
The destination LayerZero Endpoint confirms that the Executor-delivered packet matches the one verified by the DVNs.
#### 7. Message execution (off-chain, Executor)
The Executor invokes `lzReceive` on the destination chain, triggering message processing by the USDT0 OUpgradable contract on Stable.
#### 8. Completion (on-chain, Stable)
The USDT0 OUpgradable contract on Stable processes the verified message, completing the cross-chain transfer. USDT0 is minted to the user's address.
***
## Path 2: Bridging native USDT to Stable (Legacy Mesh)
This path applies when the user holds native USDT on a Legacy Mesh chain such as Tron. The transfer routes through Arbitrum as an intermediary hub before arriving on Stable.
### Actors
| Name | On-chain? | Responsible party |
| -------------------------- | --------- | --------------------------- |
| User | N/A | User |
| USDT Pool | ✅ | Smart contract by USDT0 |
| USDT0 Pool | ✅ | Smart contract by USDT0 |
| MultiHopComposer | ✅ | Smart contract by LayerZero |
| USDT0 OUpgradable | ✅ | Smart contract by USDT0 |
| LayerZero Endpoint | ✅ | Smart contract by LayerZero |
| MessageLib Registry | ✅ | Smart contract by LayerZero |
| USDT0 Legacy Mesh Operator | ❌ | USDT0 |
| Executor | ❌ | LayerZero Labs |
| USDT0 DVN | ❌ | USDT0 |
| LayerZero DVN | ❌ | LayerZero Labs |
### Flow diagram
### Detailed steps
#### 1. Initiate transfer (on-chain, Tron)
The user initiates the bridge transaction and sends native USDT to the **USDT Pool** contract on Tron. The USDT is locked in the pool. The USDT Pool contract then sends a message to the LayerZero Endpoint contract on Tron.
#### 2. Send message to the Legacy Mesh (off-chain)
The LayerZero Endpoint contract emits the message to the **USDT0 Legacy Mesh Operator**, which verifies the message.
#### 3. Initiate MultiHop transfer (on-chain, Arbitrum)
The USDT0 Legacy Mesh Operator calls the `lzCompose()` method on the LayerZero **MultiHopComposer** contract on Arbitrum. Without additional user interaction, the MultiHopComposer contract carries out the USDT0 mint-and-burn bridge transfer from Arbitrum to Stable.
The MultiHopComposer contract is completely permissionless and has no `owner()` to ensure immutability.
#### 4. Transfer USDT0 to Stable (on-chain and off-chain)
The remaining steps follow the exact same path as [bridging USDT0 to Stable](#path-1--bridging-usdt0-to-stable-oft-supported-chains) (steps 1–8 above). The USDT0 OUpgradable contract on Arbitrum sends via LayerZero, DVNs verify, and USDT0 is minted on Stable.
### Things to note
* USDT0 liquidity on Arbitrum is managed by the USDT0 team.
* The Legacy Mesh incurs a 0.03% fee on the transferred amount.
* The user does not need to interact with Arbitrum directly; the MultiHop flow is automatic.
## Next recommended
See the end-to-end lifecycle of USDT from on-ramp through settlement.
Bridge Test USDT from Sepolia to Stable testnet using the LayerZero OFT Adapter.
Understand what the asset does once it lands on Stable.
# Payments & transfers
Source: https://docs.stable.xyz/en/explanation/use-case-payments
How Stable supports P2P payments and merchant settlement with a single-asset model, zero-fee UX, and immediate finality.
P2P payments and merchant settlement built around one asset that moves the money and pays for the transaction.
## The problem
On general-purpose chains, users must hold a separate gas token (ETH, SOL) just to move stablecoins. That breaks the "send a dollar, receive a dollar" mental model and bleeds conversion at onboarding, where a payer who only has USDT can't even submit the transfer.
## How Stable addresses it
* **USDT0 is both the gas token and the payment asset.** A user only ever needs one asset to send or receive. See [USDT as gas](/en/explanation/usdt-as-gas-token).
* **Gas waiver lets applications cover gas on behalf of users**, enabling a zero-fee UX without the user touching a second token. See [Gas waiver](/en/explanation/gas-waiver).
* **Single-slot finality means settlement is immediate.** Once a transfer is in a block, it's final. See [Ethereum comparison](/en/explanation/ethereum-comparison).
## Next recommended
Understand the asset that replaces ETH for gas and payment at once.
See how applications cover user gas through governance-approved waiver addresses.
Review what changes (finality, gas token, priority tips) when moving from Ethereum.
# Payroll & mass payouts
Source: https://docs.stable.xyz/en/explanation/use-case-payroll
How Stable handles high-volume disbursements with batched settlement, reserved blockspace, and shielded amounts.
Paying employees, contractors, and suppliers at scale, with predictable throughput, predictable cost, and privacy for sensitive amounts.
## The problem
High-volume stablecoin disbursements hit per-transaction throughput limits on shared chains. Costs fluctuate with network congestion, so a payroll run that cleared cheaply yesterday can spike today. On top of that, salary and supplier amounts are publicly visible to anyone watching the chain, which leaks commercially sensitive data.
## How Stable addresses it
* **The USDT transfer aggregator batches high-volume transfers into parallelized settlement bundles**, so a single run isn't bottlenecked by per-transaction overhead. See [USDT transfer aggregator](/en/explanation/usdt-transfer-aggregator).
* **Guaranteed blockspace gives enrolled partners reserved capacity in every block**, so inclusion and cost stay predictable regardless of what else the network is doing. See [Guaranteed blockspace](/en/explanation/guaranteed-blockspace).
* **Confidential transfer shields amounts using zero-knowledge cryptography**, so payroll and supplier runs don't publish sensitive numbers on-chain. See [Confidential transfer](/en/explanation/confidential-transfer).
## Next recommended
Understand how high-volume USDT0 transfers batch into parallelized settlement bundles.
See how enrolled partners secure reserved capacity in every block.
Review how ZK cryptography shields transfer amounts while keeping parties auditable.
# Private transfers
Source: https://docs.stable.xyz/en/explanation/use-case-private
How Stable shields commercially sensitive transfer amounts using zero-knowledge cryptography while keeping parties auditable.
Treasury operations, supplier payments, and salary runs where the amount is commercially sensitive and shouldn't be published to the world.
## The problem
All standard EVM transfers are publicly observable. A payroll run or supplier settlement leaks business-critical data on-chain: who paid whom, how much, and how often. Competitors, counterparties, and anyone scraping the network can reconstruct payroll bands, vendor pricing, and treasury moves without asking.
## How Stable addresses it
* **Confidential transfer uses zero-knowledge cryptography to shield transfer amounts** while keeping the parties auditable for compliance, so sensitive numbers stay private without sacrificing the audit trail. See [Confidential transfer](/en/explanation/confidential-transfer).
* **Flow of funds shows where confidential transfer sits** in the full USDT lifecycle from on-ramp to off-ramp. See [Flow of funds](/en/explanation/flow-of-funds).
## Next recommended
Review how ZK cryptography shields transfer amounts while keeping parties auditable.
Trace USDT from on-ramp through on-chain transfer to off-ramp settlement.
# Sponsored and gasless experiences
Source: https://docs.stable.xyz/en/explanation/use-case-sponsored
How Stable lets applications cover gas on behalf of users entirely, so onboarding doesn't require acquiring a gas token.
Apps that want to remove gas from the user experience entirely, so a first-time user can sign in and transact without acquiring a second asset first.
## The problem
Requiring users to acquire a gas token before using an app creates an onboarding cliff that kills conversion for consumer-facing products. A new user who shows up with only USDT (or nothing at all) can't submit a transaction, and pushing them to a separate exchange to buy gas is where most of them drop off.
## How Stable addresses it
* **Gas waiver: governance-approved waiver addresses submit wrapper transactions that execute at zero gas price on the user's behalf**, so the app covers gas end-to-end and the user sees a free action. See [Gas waiver](/en/explanation/gas-waiver).
* **EIP-7702 session keys let a dApp hold scoped, time-limited permissions**, so it can submit transactions on the user's behalf without the user signing each one. See [EIP-7702](/en/explanation/eip-7702).
## Next recommended
See how governance-approved waivers submit wrapper transactions at zero gas price.
Understand how EOAs can delegate scoped, time-limited permissions to a dApp.
# Monetize HTTP endpoints
Source: https://docs.stable.xyz/en/explanation/x402
x402 is a payment protocol built on HTTP. A server asks for payment, a client signs it, and a facilitator settles it on-chain. No accounts, no API keys, no subscriptions.
x402 is a payment protocol built on HTTP. A server responds with `402 Payment Required` and payment details, a client signs an [ERC-3009](/en/explanation/erc-3009) authorization, and a facilitator settles it on-chain. The entire exchange happens over standard HTTP headers. The client only needs a wallet: no sign-up, no API keys, no card registration.
This applies to any scenario where a client pays a server for a resource or service: API access, digital content, merchant checkout, or agent-to-agent payments.
## What problem does it solve?
Paying for a service on the internet today requires user intervention at every step: sign up for an account, sign in, register a payment method. This model does not scale to:
* Services too small to justify the infrastructure cost
* Transactions too cheap for card network fees
* Autonomous agents (AI, bots, IoT devices) that cannot perform sign-up flows
With x402, a client only needs a wallet to pay.
| **Aspect** | **Traditional billing** | **With x402** |
| :-------------------- | :------------------------------- | :--------------------- |
| Account required | Yes | No |
| API key required | Yes | No |
| Minimum viable price | \~\$0.30 (card processing floor) | \~\$0.001 (on-chain) |
| Settlement time | Days (card networks) | Sub-second (on Stable) |
| PCI compliance needed | Yes | No |
## How it works
### The three roles
**Client** is whoever needs the resource: a web app, a backend service, a CLI tool, or an AI agent. The client only needs a wallet (a private key that can sign ERC-3009 authorizations).
**Server** is whoever provides the resource. The server defines what costs how much by attaching x402 middleware to its endpoints.
**Facilitator** is the settlement service. It receives the signed payment from the server, verifies it, submits the on-chain transaction, and returns the result. The facilitator never holds the client's funds. The transfer moves directly from client to server within the token contract.
On Stable, [Semantic Pay](https://x402.semanticpay.io) operates a public facilitator.
### The payment flow
1. **Client requests a resource.** The client sends a normal HTTP request (GET, POST, etc.) to the server.
2. **Server responds with 402.** The server returns HTTP `402 Payment Required` along with a `PAYMENT-REQUIRED` header containing all the information the client needs: how much to pay, which token, which network, and where to send the funds.
3. **Client signs and resubmits.** The client reads the payment requirements, signs an ERC-3009 authorization for the specified amount, and resubmits the original request with a `PAYMENT-SIGNATURE` header containing the signed authorization.
4. **Facilitator verifies and settles.** The server forwards the signed payment to its facilitator. The facilitator validates the signature, submits the `transferWithAuthorization` call on-chain, and once confirmed, the server returns the requested resource along with a `PAYMENT-RESPONSE` header containing the settlement receipt.
### The three headers
All payment information travels through standard HTTP headers, encoded in Base64:
| **Header** | **Direction** | **Contents** |
| :------------------ | :--------------- | :--------------------------------------------------------------------------- |
| `PAYMENT-REQUIRED` | Server to client | Payment scheme, token address, amount, recipient address, network identifier |
| `PAYMENT-SIGNATURE` | Client to server | Signed ERC-3009 authorization proving the client has authorized the transfer |
| `PAYMENT-RESPONSE` | Server to client | Settlement result including transaction hash and confirmation status |
This design works with any HTTP client, any programming language, and any infrastructure that supports custom headers.
## x402 on Stable
The x402 protocol defines how payment works over HTTP. Stable provides the settlement environment that makes it practical for production use.
### Sub-second finality
Stable's consensus provides sub-second block finality (\~700 milliseconds), allowing x402 facilitators to verify and settle transactions in real time. This is critical for high-frequency automated interactions where AI agents or IoT devices may execute many small payments in rapid succession.
### Single-asset settlement
On Stable, USDT0 is both the native gas token and the payment token. The entire x402 payment lifecycle runs on USDT0 alone. The client holds only USDT0, and the facilitator submits transactions using the same token it settles. For AI agents using x402, this means an agent wallet needs to manage only one asset.
### Micro-pricing
Prices are denominated in USDT0 atomic units (6 decimals): a cost parameter of `"1000"` translates to exactly \$0.001. This precision allows x402 servers to set prices at fractions of a cent.
### Gas waiver integration
The [Gas Waiver](/en/how-to/integrate-gas-waiver) eliminates transaction costs entirely. The x402 facilitator can use the Gas Waiver infrastructure to submit `transferWithAuthorization` calls without charging gas to either the buyer or the seller. This means x402 micropayments on Stable carry no overhead beyond the payment amount itself.
## Infrastructure
### Semantic Pay
[Semantic Pay](https://x402.semanticpay.io) provides a public x402 facilitator for Stable. It handles signature verification, on-chain submission, and confirmation tracking. Developers integrating x402 on Stable can point their middleware to this endpoint without running their own settlement infrastructure.
**Facilitator endpoint:** `https://x402.semanticpay.io`
### WDK (Wallet Development Kit)
For AI agents to participate in x402 autonomously, they need wallets they can control programmatically. Tether's open-source WDK provides this:
* **Self-custody**: WDK enables AI agents to generate and store private keys locally without relying on centralized API infrastructure.
* **x402 compatibility**: The WDK's `WalletAccountEvm` instance natively satisfies the client signer interface required by the x402 SDK, allowing agents to automatically intercept 402 HTTP responses, sign ERC-3009 authorizations, and resubmit requests.
**See also:**
* [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009): the on-chain settlement standard that x402 uses
* [Payment use cases](/en/explanation/payment-use-cases-overview): P2P, subscriptions, invoices, and API billing patterns
* [Gas Waiver](/en/how-to/integrate-gas-waiver): zero-cost transaction submission
# Account abstraction with EIP-7702
Source: https://docs.stable.xyz/en/how-to/account-abstraction
Implement batch payments, spending limits, and session keys on an existing EOA by delegating to an EIP-7702 smart contract.
This guide walks through applying EIP-7702 to an EOA and using the delegation for three patterns: batch payments, spending limits, and session keys. The EOA keeps its address and private key throughout.
**Concept:** For what EIP-7702 enables on Stable and the security considerations, see [EIP-7702](/en/explanation/eip-7702).
## Prerequisites
* Understanding of EOA vs. smart contract accounts (EOAs have no code by default).
* Familiarity with EVM transaction types ([EIP-2718](https://eips.ethereum.org/EIPS/eip-2718)).
## Overview
EIP-7702 introduces a new transaction type (`0x04`) that carries an `authorizationList`. Each authorization designates a smart contract whose code the EOA will execute for that transaction. The flow is:
1. **Choose or deploy a delegate contract**: a standard Solidity contract implementing the logic you want EOAs to use. You can use a deployed contract or deploy your own. Use an audited contract whenever possible.
2. **Sign an authorization**: the EOA owner signs a message authorizing the delegate contract.
3. **Submit an EIP-7702 transaction**: the transaction includes the authorization, and the EOA runs the delegate's code during execution.
## Use case: batch transactions
The steps below walk through this flow using `Multicall3` as the delegate contract. `Multicall3` is a widely deployed utility contract that aggregates multiple calls into a single transaction. By designating `Multicall3` as the EIP-7702 delegate, an EOA can batch arbitrary contract interactions (token transfers, approvals, contract reads, or any combination) into one atomic transaction. Batch payments are one example: instead of sending ten separate transactions for a payroll run, the EOA executes them all at once.
### Step 1: Use Multicall3 as a delegate contract
`Multicall3` is deployed at `0xcA11bde05977b3631167028862bE2a173976CA11` on Stable. Since it's already deployed and widely used, you don't need to deploy your own delegate. Signing an EIP-7702 authorization grants the delegate full execution authority over your EOA.
```solidity theme={"dark"}
// Multicall3 interface (relevant functions only)
interface IMulticall3 {
struct Call3 {
address target;
bool allowFailure;
bytes callData;
}
struct Result {
bool success;
bytes returnData;
}
/// @notice Aggregate calls, allowing each to succeed or fail independently
function aggregate3(Call3[] calldata calls)
external payable returns (Result[] memory returnData);
}
```
### Step 2: Sign an authorization
The EOA owner signs an authorization that designates the delegate contract. This authorization is included in the EIP-7702 transaction.
```javascript theme={"dark"}
// config.ts
import { ethers } from "ethers";
export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const STABLE_TESTNET_CHAIN_ID = 2201;
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
export const DELEGATE_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
```
```javascript theme={"dark"}
// signAuthorization.ts
import { ethers } from "ethers";
import { DELEGATE_ADDRESS, STABLE_TESTNET_CHAIN_ID, provider, wallet } from "./config";
export async function signAuthorization() {
const authorization = {
chainId: STABLE_TESTNET_CHAIN_ID,
address: DELEGATE_ADDRESS,
nonce: await provider.getTransactionCount(wallet.address),
};
return wallet.signAuthorization(authorization);
}
```
### Step 3: Submit an EIP-7702 transaction
Combine the authorization with a call to `Multicall3.aggregate3`. This example batches three USDT0 transfers in a single transaction.
```javascript theme={"dark"}
import { ethers } from "ethers";
import { wallet, USDT0_ADDRESS } from "./config";
import { signAuthorization } from "./signAuthorization";
const usdt0Interface = new ethers.Interface([
"function transfer(address to, uint256 amount)",
]);
const batchInterface = new ethers.Interface([
"function aggregate3((address target, bool allowFailure, bytes callData)[] calls) returns ((bool success, bytes returnData)[])",
]);
async function main() {
const recipients = [
{ to: "0xAlice...", amount: ethers.parseUnits("100", 6) },
{ to: "0xBob...", amount: ethers.parseUnits("200", 6) },
{ to: "0xCarol...", amount: ethers.parseUnits("150", 6) },
];
const batchData = batchInterface.encodeFunctionData("aggregate3", [
recipients.map(({ to, amount }) => ({
target: USDT0_ADDRESS,
allowFailure: false,
callData: usdt0Interface.encodeFunctionData("transfer", [to, amount]),
})),
]);
const signedAuth = await signAuthorization();
const tx = await wallet.sendTransaction({
type: 4, // EIP-7702 transaction type
to: wallet.address, // call is directed at the EOA itself
data: batchData, // aggregate3 call to execute
authorizationList: [signedAuth],
maxPriorityFeePerGas: 0n,
});
const receipt = await tx.wait(1);
console.log("Batch transactions executed in tx:", receipt.hash);
}
```
```text theme={"dark"}
Batch transactions executed in tx: 0x...
```
The EOA executes all three calls in a single atomic transaction via `Multicall3.aggregate3`. The delegation persists until explicitly changed or cleared. While this example shows batch payments, the same pattern works for any combination of contract calls.
## Use case: spending limits
A delegate contract can enforce per-transaction or daily caps on an EOA without account migration.
```solidity theme={"dark"}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SpendingLimitExecutor
/// @notice Delegate contract that enforces daily spending caps
contract SpendingLimitExecutor {
mapping(address => uint256) public dailyLimit;
mapping(address => uint256) public spentToday;
mapping(address => uint256) public lastResetDay;
function setDailyLimit(uint256 limit) external {
dailyLimit[msg.sender] = limit;
}
function executeWithLimit(
address target,
uint256 value,
bytes calldata data
) external payable {
uint256 today = block.timestamp / 1 days;
if (today > lastResetDay[msg.sender]) {
spentToday[msg.sender] = 0;
lastResetDay[msg.sender] = today;
}
spentToday[msg.sender] += value;
require(
spentToday[msg.sender] <= dailyLimit[msg.sender],
"daily limit exceeded"
);
(bool success,) = target.call{value: value}(data);
require(success, "call failed");
}
}
```
## Use case: session keys
Session keys allow a dApp to execute transactions on behalf of an EOA within scoped permissions: a time window and an allowed set of target contracts. This is useful for dApps where frequent on-chain interactions would otherwise require repeated wallet approvals.
```solidity theme={"dark"}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SessionKeyExecutor
/// @notice Delegate contract that grants scoped, time-limited access to a session key
contract SessionKeyExecutor {
struct Session {
address key;
uint256 validUntil;
uint256 spendingLimit;
uint256 spent;
}
mapping(address => Session) public sessions;
mapping(address => mapping(address => bool)) public allowedTargets;
/// @notice Register a session key with scoped permissions
function startSession(
address key,
uint256 validUntil,
uint256 spendingLimit,
address[] calldata targets
) external {
sessions[msg.sender] = Session({
key: key,
validUntil: validUntil,
spendingLimit: spendingLimit,
spent: 0
});
for (uint256 i = 0; i < targets.length; i++) {
allowedTargets[msg.sender][targets[i]] = true;
}
}
/// @notice Execute a call using the session key
function executeAsSessionKey(
address owner,
address target,
uint256 value,
bytes calldata data
) external {
Session storage session = sessions[owner];
require(msg.sender == session.key, "not session key");
require(block.timestamp <= session.validUntil, "session expired");
require(allowedTargets[owner][target], "target not allowed");
uint256 beforeBalance = owner.balance;
(bool success,) = target.call{value: value}(data);
require(success, "call failed");
session.spent += owner.balance - beforeBalance;
require(session.spent <= session.spendingLimit, "budget exceeded");
}
/// @notice Revoke the active session
function revokeSession() external {
delete sessions[msg.sender];
}
}
```
## Important considerations
* **Persistent delegation**: the delegation persists until the EOA explicitly changes or clears it. It is not limited to a single transaction.
* **Gas costs**: EIP-7702 transactions have slightly higher base gas due to authorization processing, offset when the delegate batches multiple calls.
* **Use audited delegates**: a malicious delegate contract can drain the EOA's assets. Only delegate to contracts that have been audited.
## Key takeaways
* EIP-7702 lets EOAs execute smart contract logic without migrating to a new account type.
* On Stable, EIP-7702 enables batch payments, spending limits, and scoped session keys on existing EOAs.
* The delegation persists until explicitly changed. Always use an audited delegate contract.
## Next recommended
Apply EIP-7702 to recurring subscription payments with a SubscriptionManager.
Understand the delegation model before you ship it.
Look up the `0x04` transaction format and authorization fields.
# Learn P2P payments
Source: https://docs.stable.xyz/en/how-to/build-p2p-payments
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](/en/reference/p2p-payments).
## What you'll build
Five scripts forming a minimal payment app:
* `wallet.ts` — create or restore a wallet.
* `getBalance.ts` — query the current USDT0 balance.
* `send.ts` — send USDT0 to another address.
* `receive.ts` — watch for incoming payments in real time.
* `history.ts` — query past Transfer events for an address.
### Demo
```text theme={"dark"}
step 1. Alice creates wallet → address: 0xAlice...
step 2. Alice's balance: 0.01 USDT0
step 3. Alice sends 0.001 USDT0 to Bob
tx: 0x8f3a...2d41
gas fee: 0.000021 USDT0
Alice balance: 0.008979 USDT0
step 4. Bob receives payment (real-time event)
from: 0xAlice...
amount: 0.001 USDT0
tx: 0x8f3a...2d41
```
## Prerequisites
* Node.js 20 or later.
* A private key with testnet USDT0 (see [Quick start](/en/tutorial/quick-start) to fund a wallet).
## Project setup
```bash theme={"dark"}
mkdir stable-p2p && cd stable-p2p
npm init -y && npm install ethers dotenv
```
```text theme={"dark"}
added 2 packages, audited 3 packages in 1s
```
Create `config.ts` shared by every script:
```typescript theme={"dark"}
// config.ts
import { ethers } from "ethers";
import "dotenv/config";
export const STABLE_RPC = "https://rpc.testnet.stable.xyz";
export const STABLE_WS = "wss://rpc.testnet.stable.xyz";
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
export const provider = new ethers.JsonRpcProvider(STABLE_RPC);
```
## 1. Create or restore a wallet
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.
```typescript theme={"dark"}
// wallet.ts
import { 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);
}
```
```bash theme={"dark"}
npx tsx wallet.ts
```
```text theme={"dark"}
Address: 0xAlice...1234
Seed phrase: liberty shoot ... (12 words)
```
## 2. Check the balance
USDT0 is the native asset on Stable, so balance queries work exactly like ETH on Ethereum. Native balance is 18 decimals, use `formatEther` for display.
```typescript theme={"dark"}
// getBalance.ts
import { ethers } from "ethers";
import { provider } from "./config";
export async function getBalance(address: string) {
const balance = await provider.getBalance(address);
return ethers.formatEther(balance); // 18 decimals
}
if (import.meta.url === `file://${process.argv[1]}`) {
const address = process.argv[2];
const balance = await getBalance(address);
console.log("Balance:", balance, "USDT0");
}
```
```bash theme={"dark"}
npx tsx getBalance.ts 0xAlice...1234
```
```text theme={"dark"}
Balance: 0.01 USDT0
```
## 3. Send a payment
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.
```typescript theme={"dark"}
// send.ts
import { ethers } from "ethers";
import { provider } from "./config";
export async function sendPayment(
senderKey: string,
recipient: string,
amount: string // e.g. "0.001" for 0.001 USDT0
) {
const wallet = new ethers.Wallet(senderKey, provider);
const block = await provider.getBlock("latest");
const baseFee = block!.baseFeePerGas!;
const tx = await wallet.sendTransaction({
to: recipient,
value: ethers.parseEther(amount),
maxFeePerGas: baseFee * 2n,
maxPriorityFeePerGas: 0n, // always 0 on Stable
});
console.log("Payment sent:", tx.hash);
const receipt = await tx.wait(1);
if (receipt!.status === 1) console.log("Payment settled");
return tx.hash;
}
if (import.meta.url === `file://${process.argv[1]}`) {
const [, , recipient, amount] = process.argv;
await sendPayment(process.env.PRIVATE_KEY!, recipient, amount);
}
```
```bash theme={"dark"}
npx tsx send.ts 0xBob...5678 0.001
```
```text theme={"dark"}
Payment sent: 0x8f3a...2d41
Payment settled
```
## 4. Receive payments in real time
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.
```typescript theme={"dark"}
// receive.ts
import { 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]);
}
```
```bash theme={"dark"}
npx tsx receive.ts 0xBob...5678
```
```text theme={"dark"}
Watching for incoming payments to 0xBob...5678
Payment 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.
## 5. Query transaction history
Query past `Transfer` events to build a transaction history view, like a bank statement or transaction list in any payment app.
```typescript theme={"dark"}
// history.ts
import { ethers } from "ethers";
import { provider, USDT0_ADDRESS } from "./config";
const usdt0 = new ethers.Contract(
USDT0_ADDRESS,
["event Transfer(address indexed from, address indexed to, uint256 value)"],
provider
);
export async function getTransactionHistory(address: string, fromBlock?: number) {
if (fromBlock === undefined) {
const latest = await provider.getBlockNumber();
fromBlock = Math.max(0, latest - 10_000);
}
const [sentEvents, receivedEvents] = await Promise.all([
usdt0.queryFilter(usdt0.filters.Transfer(address, null), fromBlock),
usdt0.queryFilter(usdt0.filters.Transfer(null, address), fromBlock),
]);
return [
...sentEvents.map((e: any) => ({
type: "sent" as const,
counterparty: e.args[1],
amount: ethers.formatUnits(e.args[2], 6),
txHash: e.transactionHash,
block: e.blockNumber,
})),
...receivedEvents.map((e: any) => ({
type: "received" as const,
counterparty: e.args[0],
amount: ethers.formatUnits(e.args[2], 6),
txHash: e.transactionHash,
block: e.blockNumber,
})),
].sort((a, b) => b.block - a.block);
}
if (import.meta.url === `file://${process.argv[1]}`) {
const history = await getTransactionHistory(process.argv[2]);
for (const tx of history) {
console.log(`${tx.type} ${tx.amount} USDT0 ${tx.counterparty} ${tx.txHash}`);
}
}
```
```bash theme={"dark"}
npx tsx history.ts 0xAlice...1234
```
```text theme={"dark"}
sent 0.001 USDT0 0xBob...5678 0x8f3a...2d41
received 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](https://stablescan.xyz) for paginated history queries — every transaction is already indexed.
## Next recommended
Pull-based recurring subscriptions with EIP-7702 delegation.
Settle invoices with ERC-3009 and deterministic nonces.
Reference the basic native vs. ERC-20 transfer flow.
# Build a pay-per-call API
Source: https://docs.stable.xyz/en/how-to/build-pay-per-call
Monetize HTTP endpoints with per-request USDT0 payments using x402 on Stable. Server setup, client integration, and spending controls.
This guide walks through monetizing an API endpoint with x402. The server adds a payment handler, the client pays per request, and settlement happens within the HTTP lifecycle.
**Concept:** For the x402 protocol and why it fits Stable, see [x402](/en/explanation/x402). For the high-level use case model, see [Pay-per-call APIs](/en/reference/pay-per-call).
The Semantic facilitator currently operates on mainnet only. The examples in this guide use Stable mainnet. Use small amounts when testing.
## What you'll build
A paid HTTP API where the server responds with `402 Payment Required`, the client pays per request, and the facilitator settles USDT0 on-chain within the HTTP lifecycle.
### Demo
```text theme={"dark"}
step 1. Client: GET /weather (no payment)
Server: 402 Payment Required
PAYMENT-REQUIRED: { amount: "1000", asset: USDT0, network: eip155:988 }
step 2. Client signs ERC-3009 authorization
step 3. Client: GET /weather + PAYMENT-SIGNATURE header
Server: forwards to facilitator → transferWithAuthorization settles on-chain
(~700ms block confirmation)
Server: 200 OK { weather: "sunny", temperature: 70 }
PAYMENT-SETTLE-RESPONSE: { txHash: "0x8f3a...", paid: "0.001 USDT0" }
step 4. Verify settlement on Stablescan
https://stablescan.xyz/tx/0x8f3a...
```
## Overview
**Seller (server):**
```typescript theme={"dark"}
// --- Server ---
app.use(paymentMiddleware({
"GET /weather": {
price: { amount: "1000", asset: USDT0 },
payTo: sellerAddress,
},
"POST /inference": {
price: { amount: "50000", asset: USDT0 },
payTo: sellerAddress,
},
}, resourceServer));
// Routes not listed in the config are not gated.
```
**Buyer (client):**
```typescript theme={"dark"}
// --- Client ---
account = new WalletAccountEvm(seedPhrase, { provider: RPC });
client = new x402Client();
fetchWithPayment = wrapFetchWithPayment(fetch, client);
weatherResponse = fetchWithPayment("https://api.example.com/weather");
inferenceResponse = fetchWithPayment("https://api.example.com/inference", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "Hello" }),
});
// For each paid request:
// 1. Initial request returns 402 with PAYMENT-REQUIRED header
// 2. Client signs ERC-3009 authorization with wallet
// 3. Client retries with PAYMENT-SIGNATURE header
// 4. Facilitator settles on-chain, server returns the response
```
## Seller: set up paid endpoints
The seller adds x402 middleware to define which routes require payment. When a request arrives without payment, the middleware responds with `402 Payment Required` and the payment terms. When a valid payment header is present, the middleware forwards it to a facilitator that verifies the signature and settles the payment on-chain. The seller only configures the price and the receiving address; the facilitator handles verification and settlement.
```bash theme={"dark"}
npm install express @x402/express @x402/evm @x402/core
```
### Pricing
Each route specifies the payment amount in USDT0 base units (6 decimals), the network, and the address to receive funds. For example, `"1000"` equals `$0.001` and `"50000"` equals `$0.05`.
```typescript theme={"dark"}
price: {
amount: "1000", // base units (6 decimals)
asset: USDT0_STABLE, // USDT0 contract address
extra: { name: "USDT0", version: "1", decimals: 6 }, // EIP-712 domain info
}
```
The `extra` fields (`name`, `version`, `decimals`) are used by the buyer's client for EIP-712 signature construction and must match the on-chain USDT0 contract.
### Route configuration
Routes are mapped using the `METHOD /path` format. Each route specifies the accepted payment scheme, network, price, and the address to receive funds (`payTo`). The `description` and `mimeType` fields help buyers and AI agents discover what the endpoint provides. Routes not listed in the config are not gated and behave like normal Express routes.
```typescript theme={"dark"}
// server.ts
import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
const PAY_TO = process.env.PAY_TO_ADDRESS as `0x${string}`;
const FACILITATOR_URL = "https://x402.semanticpay.io/";
const STABLE_NETWORK = "eip155:988"; // Stable Mainnet CAIP-2 ID
const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736";
const facilitatorClient = new HTTPFacilitatorClient({ url: FACILITATOR_URL });
const resourceServer = new x402ResourceServer(facilitatorClient)
.register(STABLE_NETWORK, new ExactEvmScheme());
const app = express();
app.use(
paymentMiddleware(
{
// Example 1: Configure a paid GET route
"GET /weather": {
accepts: [
{
scheme: "exact",
network: STABLE_NETWORK,
price: {
amount: "1000", // $0.001
asset: USDT0_STABLE,
extra: { name: "USDT0", version: "1", decimals: 6 },
},
payTo: PAY_TO,
},
],
description: "Weather data",
mimeType: "application/json",
},
// Example 2: Configure a paid POST route
"POST /inference": {
accepts: [
{
scheme: "exact",
network: STABLE_NETWORK,
price: {
amount: "50000", // $0.05
asset: USDT0_STABLE,
extra: { name: "USDT0", version: "1", decimals: 6 },
},
payTo: PAY_TO,
},
],
description: "AI inference endpoint",
mimeType: "application/json",
},
},
resourceServer,
),
);
app.get("/weather", (req, res) => {
res.json({ weather: "sunny", temperature: 70 });
});
app.post("/inference", (req, res) => {
const { prompt } = req.body;
res.json({ result: `Inference result for: ${prompt}` });
});
// Not listed in the config, so no payment required.
app.get("/health", (req, res) => {
res.json({ status: "ok", payTo: PAY_TO });
});
const PORT = process.env.PORT || 4021;
app.listen(PORT, () => {
console.log(`Server listening at http://localhost:${PORT}`);
console.log(`GET /health - free`);
console.log(`GET /weather - $0.001 per request`);
console.log(`POST /inference - $0.05 per request`);
});
```
x402 also provides middleware for Hono (`@x402/hono`) and Next.js (`@x402/next`). The pattern is the same: create a facilitator client, register the EVM scheme, and apply middleware.
## Buyer: make paid requests
The buyer accesses paid endpoints without going through manual payment flows. The buyer does not pay gas. The facilitator settles on-chain, and the buyer only pays the exact amount specified in the payment requirements.
```bash theme={"dark"}
npm install @x402/fetch @x402/evm @tetherto/wdk-wallet-evm
```
### Create a wallet and check balance
```typescript theme={"dark"}
// client.ts
import WalletManagerEvm from "@tetherto/wdk-wallet-evm";
const account = await new WalletManagerEvm(process.env.SEED_PHRASE!, {
provider: "https://rpc.stable.xyz",
}).getAccount(0);
console.log("Buyer address:", account.address);
// USDT0 uses 6 decimals. A balance of 1000000 equals 1.00 USDT0.
const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736";
const balance = await account.getTokenBalance(USDT0_STABLE);
console.log("USDT0 balance:", Number(balance) / 1e6, "USDT0");
```
### Connect to x402 and make a paid request
`WalletAccountEvm` satisfies the signer interface that x402 expects, so it can be registered directly as the signer for the x402 client. Once registered, requests sent through the x402-enabled client handle 402 payment flows automatically.
```typescript theme={"dark"}
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
const client = new x402Client();
registerExactEvmScheme(client, { signer: account });
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const response = await fetchWithPayment("http://localhost:4021/weather");
const data = await response.json();
console.log("Response:", data);
```
Under the hood, `fetchWithPayment` intercepts the 402 response, parses the payment requirements (amount, token, network, recipient), signs an ERC-3009 `transferWithAuthorization` with the WDK wallet, and retries the request with the `PAYMENT-SIGNATURE` header.
If you prefer Axios, use `@x402/axios` with `wrapAxiosWithPayment` for the same automatic payment handling.
## Test the payment flow
Start the server and verify both the paid and free routes.
This test flow runs on Stable mainnet. Each successful paid request settles a real USDT0 payment through the hosted facilitator. Use a dedicated wallet and small amounts only.
### 1. Confirm the 402 response
```bash theme={"dark"}
curl -i http://localhost:4021/weather
```
The response should be `402 Payment Required` with a `PAYMENT-REQUIRED` header containing the price, asset, and network.
### 2. Run the client
```bash theme={"dark"}
npx tsx client.ts
```
The client handles the full cycle: receives the 402, signs the authorization, retries with payment, and prints the response.
### 3. Read the receipt
After a successful paid request, the buyer can read the `PAYMENT-SETTLE-RESPONSE` header from the server response and parse the settlement receipt.
```typescript theme={"dark"}
// (continued) client.ts
import { x402HTTPClient } from "@x402/fetch";
const httpClient = new x402HTTPClient(client);
const receipt = httpClient.getPaymentSettleResponse(
(name) => response.headers.get(name),
);
console.log("Payment receipt:", JSON.stringify(receipt, null, 2));
```
## Test without the live facilitator
Because the Semantic facilitator is mainnet-only, you can't point your server at a testnet facilitator today. To iterate on server logic, route handlers, and middleware behavior without settling real payments, stub the facilitator client.
```typescript theme={"dark"}
// server.test.ts
import { x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
// Stub facilitator: accepts any signature, returns a fake settlement.
const stubFacilitatorClient = {
verify: async () => ({ isValid: true, payer: "0xMockPayer" }),
settle: async () => ({
success: true,
txHash: "0xMOCK000000000000000000000000000000000000000000000000000000000001",
networkId: "eip155:988",
}),
};
export const testResourceServer = new x402ResourceServer(stubFacilitatorClient as any)
.register("eip155:988", new ExactEvmScheme());
```
Run unit tests against the stub to validate:
* 402 responses include the correct `PAYMENT-REQUIRED` payload.
* Requests with a valid `PAYMENT-SIGNATURE` header reach the handler.
* Requests with a missing or malformed header get rejected before the handler runs.
When you're ready to exercise real settlement, swap back to `HTTPFacilitatorClient` and run on mainnet with small amounts.
Stubbed settlement only verifies middleware behavior. It doesn't prove your route handler is idempotent under real network latency or concurrent payments. Always finish with a live mainnet test against small amounts before shipping.
## Advanced: lifecycle hooks
x402 provides hooks to intercept and customize payment processing at key points in the flow. For example, the server can run logic before verification (e.g., checking API keys or subscriber status) to bypass payment for authorized requests, and the client can enforce spending limits before signing.
For the full hook reference and examples, see [x402 Lifecycle Hooks](https://x402.semanticpay.io/docs/hooks).
## Next recommended
Understand the protocol and where it fits.
Review the settlement standard x402 uses.
Wrap this API as an MCP tool so AI clients can call it through prompts.
# Create a wallet
Source: https://docs.stable.xyz/en/how-to/create-wallet
Generate a new Stable wallet or restore one from a seed phrase using ethers.js or the Tether WDK.
A Stable wallet is an Ethereum-standard key pair. Any wallet library that produces an EVM account works on Stable without modification. This guide shows two paths: ethers.js for most applications, and Tether's [WDK (Wallet Development Kit)](https://github.com/tetherto/wdk) for integrations that want a turnkey self-custody layer for agents and payments.
No registration, no Stable-specific account setup. A wallet can immediately receive USDT0 from the [testnet faucet](/en/how-to/fund-testnet-wallet) or a mainnet transfer.
## Prerequisites
* Node.js 20 or later.
## Option 1: ethers.js
Install the library and generate a key pair.
```bash theme={"dark"}
npm install ethers
```
```typescript theme={"dark"}
// wallet.ts
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz");
/** 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, // show to the user once 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);
}
```
```bash theme={"dark"}
npx tsx wallet.ts
```
```text theme={"dark"}
Address: 0xAlice...1234
Seed phrase: liberty shoot ... (12 words)
```
Never log or store the seed phrase in plain text in production. Encrypt it at rest, or use a secrets manager. `ethers.Wallet.createRandom` returns the phrase once per call — if you lose it, funds are unrecoverable.
## Option 2: Tether WDK
The WDK wraps key derivation, signing, and transaction submission into a single interface. It's the right choice when you want self-custody without re-implementing common account flows, and it integrates directly with [x402](/en/how-to/build-pay-per-call) for agent payments.
```bash theme={"dark"}
npm install @tetherto/wdk @tetherto/wdk-wallet-evm
```
```typescript theme={"dark"}
// wallet-wdk.ts
import WDK from "@tetherto/wdk";
import WalletManagerEvm from "@tetherto/wdk-wallet-evm";
function initWdk(seedPhrase: string) {
return new WDK(seedPhrase)
.registerWallet("stable", WalletManagerEvm, {
provider: "https://rpc.testnet.stable.xyz",
});
}
/** Create a new wallet for a new user. */
export async function createWallet() {
const seedPhrase = WDK.getRandomSeedPhrase();
const wdk = initWdk(seedPhrase);
const account = await wdk.getAccount("stable", 0);
return {
account,
address: await account.getAddress(),
seedPhrase, // show to the user once for backup
};
}
/** Restore a wallet from a seed phrase (returning user). */
export async function restoreWallet(seedPhrase: string) {
const wdk = initWdk(seedPhrase);
const account = await wdk.getAccount("stable", 0);
return { account, address: await account.getAddress() };
}
```
```bash theme={"dark"}
npx tsx wallet-wdk.ts
```
```text theme={"dark"}
Address: 0xAlice...1234
Seed phrase: liberty shoot ... (12 words)
```
## Fund the wallet
Before the wallet can transact, it needs USDT0 for gas. On testnet, request from the faucet:
```bash theme={"dark"}
open https://faucet.stable.xyz
```
Paste the address and select the button to receive 1 testnet USDT0 (enough for thousands of native transfers). For mainnet, send USDT0 from any supported exchange or bridge; see [Bridging to Stable](/en/explanation/usdt0-bridging).
## Check the balance
Native USDT0 uses 18 decimals. The native balance is the one gas is paid from.
```typescript theme={"dark"}
// balance.ts
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz");
const balance = await provider.getBalance("0xYourAddress");
console.log("Balance:", ethers.formatEther(balance), "USDT0");
```
```bash theme={"dark"}
npx tsx balance.ts
```
```text theme={"dark"}
Balance: 1.0 USDT0
```
## Next recommended
Add batch payments, spending limits, and session keys to this wallet.
Native and ERC-20 transfers on the same balance.
Faucet and Sepolia bridge options for larger test balances.
# Develop with AI
Source: https://docs.stable.xyz/en/how-to/develop-with-ai
Set up MCP servers, agent skills, and plain-text docs so AI editors and coding agents can build on Stable directly.
Stable provides MCP servers, agent skills, and plain-text documentation files so AI editors and coding agents can work with Stable directly. This page covers how to wire each piece into your workflow, a copy-pasteable context block for non-MCP AI tools, and starter prompts for common tasks.
## MCP servers
Stable runs two MCP servers. **Docs MCP** searches this docs site for concepts, guides, code snippets, and contract references. **Runtime MCP** interacts with the Stable chain for balance queries, transaction simulation, and execution.
Both servers can be added to any MCP-compatible client.
### Cursor
Open your MCP configuration file and add:
```json theme={"dark"}
{
"mcpServers": {
"stable-docs": {
"url": "https://docs.stable.xyz/mcp"
},
"stable-runtime": {
"url": "https://runtime.stable.xyz/mcp"
}
}
}
```
Restart Cursor. Verify by asking: "How do I send USDT0 on Stable?"
### Claude Code
```bash theme={"dark"}
claude mcp add stable-docs https://docs.stable.xyz/mcp
claude mcp add stable-runtime https://runtime.stable.xyz/mcp
```
Verify by asking: "Search Stable docs for Gas Waiver integration steps."
## Agent skills
Agent skills are predefined workflows that combine Docs MCP and Runtime MCP. When you ask an AI to perform a task like "send 100 USDT0 to three addresses," the skill handles the full sequence: look up the relevant docs, resolve addresses and parameters, check balances, simulate the transaction, and execute after approval.
Skills are available as a Claude Code plugin.
### Install
```bash theme={"dark"}
claude plugin add stable-xyz/agent-skills
```
Or install from the Claude Code marketplace.
For the full skill definitions and source, see the [agent-skills repository](https://github.com/stable-xyz/agent-skills).
## Plain-text docs
For AI tools that do not support MCP, Stable documentation is available as static text files.
| **File** | **URL** | **Content** |
| :-------------- | :----------------------------------------------------------------------------- | :-------------------------------------- |
| `llms.txt` | [https://docs.stable.xyz/llms.txt](https://docs.stable.xyz/llms.txt) | Page index with titles and descriptions |
| `llms-full.txt` | [https://docs.stable.xyz/llms-full.txt](https://docs.stable.xyz/llms-full.txt) | Full documentation in a single file |
These files are static snapshots. For the most current content, use Docs MCP.
### Cursor
1. Go to **Settings > Features > Docs**.
2. Select **Add** and enter `https://docs.stable.xyz/llms-full.txt`.
3. Reference in chat with `@Stable`.
### Other tools
Download `llms-full.txt` and include it in your project context or system prompt.
## Stable context block
Paste this at the top of any AI chat or system prompt. It gives the model everything it needs to generate correct Stable code on the first attempt.
```markdown theme={"dark"}
# Stable chain context
Stable is a Layer 1 where USDT0 is the native gas token. Fully EVM-compatible.
All standard EVM tools (Hardhat, Foundry, ethers.js, viem) work unchanged once
you adjust three gas fields (see Behavioral differences below).
## Network
| Field | Mainnet | Testnet |
| :-------------- | :--------------------------------------- | :----------------------------------------- |
| Chain ID | 988 | 2201 |
| RPC | https://rpc.stable.xyz | https://rpc.testnet.stable.xyz |
| Explorer | https://stablescan.xyz | https://testnet.stablescan.xyz |
| Currency symbol | USDT0 | USDT0 |
## USDT0 contract addresses
- Mainnet: 0x779ded0c9e1022225f8e0630b35a9b54be713736
- Testnet: 0x78cf24370174180738c5b8e352b6d14c83a6c9a9
## Behavioral differences from Ethereum
1. **Gas token is USDT0, not ETH.** The `value` field in native transfers
carries USDT0. Fees are denominated in USDT0.
2. **`maxPriorityFeePerGas` is always 0.** No tip-based ordering. Set it
explicitly to `0n` or validators will reject or ignore tip components.
3. **USDT0 has a dual role**: native asset (18 decimals) AND ERC-20 (6 decimals)
on the same balance. `address(x).balance` reports 18-decimal wei;
`USDT0.balanceOf(x)` reports 6-decimal units. Values may differ by up to
0.000001 USDT0 due to fractional reconciliation. Never mirror native
balance in an internal variable; always query at payout time.
4. **Transfer events are emitted for native transfers too.** A single Transfer
event listener on the USDT0 ERC-20 contract covers both transfer paths.
5. **Single-slot finality (~700ms).** Once a block is committed, it cannot
be reorged. No need to wait multiple confirmations.
6. **Gas Waiver** lets applications cover gas: user signs with `gasPrice = 0`,
a governance-registered waiver wraps and submits. Contracts must be on
the waiver's AllowedTarget policy.
7. **EIP-7702** is supported for delegating an EOA to a contract (type-4 tx).
8. **Precompile addresses**: Bank `0x...1003`, Distribution `0x...0801`,
Staking `0x...0800`, StableSystem `0x...9999`.
## Common mistakes to avoid
- Copying Ethereum priority-fee constants (2 gwei tips, etc.) — has no effect
on Stable and can be rejected by wallets.
- Using `ethers.parseUnits(x, 18)` for ERC-20 USDT0 amounts. ERC-20 uses 6
decimals; native transfers use 18.
- Mirroring native balance in a `uint256 deposited` variable — USDT0
allowance-based operations (transferFrom, permit) can reduce a contract's
native balance without invoking its code.
- Sending native or ERC-20 USDT0 to `address(0)` — both revert on Stable.
- Assuming `EXTCODEHASH == 0` means an address is unused. On Stable,
permit-based approvals can change state without incrementing nonce.
- Writing `value: ethers.parseEther(amount, "ether")` and expecting ETH
semantics. That transfer sends USDT0.
```
## Starter prompts
Copy any of these into your AI editor after loading the context block above.
### Deploy a contract
```text theme={"dark"}
Use Foundry to scaffold a project called `stable-escrow`. Write a minimal
Escrow contract in Solidity ^0.8.24 with deposit() and withdraw(amount)
functions that transfer USDT0 natively. Use address(this).balance for
solvency checks (never mirror the balance in a uint256). Reject
address(0) recipients. Then produce a deployment command using
`forge create` pointed at Stable testnet (RPC https://rpc.testnet.stable.xyz,
chain ID 2201).
```
### Send USDT0
```text theme={"dark"}
Write a TypeScript script using ethers v6 that sends 0.001 USDT0 natively
from the wallet loaded from PRIVATE_KEY. Use base-fee-only EIP-1559 gas
(maxPriorityFeePerGas = 0n, maxFeePerGas = 2 * baseFeePerGas). Target
Stable testnet. Log the tx hash and a Stablescan explorer URL.
```
### Set up EIP-7702 delegation
```text theme={"dark"}
Write a TypeScript script using ethers v6 that:
1. Signs an EIP-7702 authorization delegating my EOA to Multicall3 at
0xcA11bde05977b3631167028862bE2a173976CA11 on Stable testnet
(chain ID 2201).
2. Sends a type-4 transaction with authorizationList: [signedAuth],
to: wallet.address (self-call), and data that invokes aggregate3()
to batch three USDT0 transfers (100, 200, 150 USDT0 with 6 decimals).
3. Use maxPriorityFeePerGas: 0n.
```
### Build a subscription contract
```text theme={"dark"}
Write a SubscriptionManager Solidity contract for EIP-7702 delegation on
Stable. It runs on a subscriber's EOA. Expose:
- subscribe(bytes32 subId, address provider, uint256 amount, uint256 interval)
callable only when msg.sender == address(this) (subscriber on their own EOA).
- collect(bytes32 subId) callable only by the registered provider, only
when block.timestamp >= nextChargeAt; advances nextChargeAt by interval
and transfers USDT0 to the provider. Use IERC20 USDT0 at the testnet
address 0x78cf24370174180738c5b8e352b6d14c83a6c9a9.
- cancelSubscription(bytes32 subId) callable only by the subscriber.
Emit events for SubscriptionCreated, SubscriptionCollected, SubscriptionCancelled.
```
### Build an x402 pay-per-call API
```text theme={"dark"}
Write an Express server in TypeScript that exposes GET /weather priced
at $0.001 USDT0 (amount: "1000", 6 decimals) using @x402/express,
@x402/evm/exact/server, and HTTPFacilitatorClient pointed at
https://x402.semanticpay.io/. Use Stable mainnet (CAIP-2 eip155:988,
USDT0 at 0x779Ded0c9e1022225f8E0630b35a9b54bE713736). The handler should
return { weather: "sunny", temperature: 70 }. Read PAY_TO_ADDRESS from
env. Print the configured routes on startup.
```
## Next recommended
Wrap a paid API as an MCP tool so AI clients can call and pay for it.
Pair the AI context with a first-transaction run in five minutes.
Deep-dive on the gas and USDT0 semantics in the context block.
# Index contract events
Source: https://docs.stable.xyz/en/how-to/index-contract
Subscribe to smart contract events on Stable with ethers.js watchContractEvent and build a live event stream.
Indexing turns on-chain events into data your application can react to: balance updates, transaction history, UI notifications. This guide shows how to subscribe to events from a deployed Stable contract using ethers.js and how to backfill historical events so you don't miss any emitted while your service was offline.
## Prerequisites
* A deployed contract on Stable testnet or mainnet. If you need one, see [Deploy](/en/tutorial/smart-contract) and [Verify](/en/how-to/verify-contract).
* Node.js 20 or later.
* The contract address and the ABI of the events you want to index.
## 1. Install and configure
```bash theme={"dark"}
npm install ethers
```
```typescript theme={"dark"}
// config.ts
import { ethers } from "ethers";
export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const STABLE_TESTNET_WS = "wss://rpc.testnet.stable.xyz";
export const CONTRACT_ADDRESS = "0xDeployedContractAddress";
// Minimal ABI: only the events you want to index.
export const CONTRACT_ABI = [
"event NumberUpdated(address indexed caller, uint256 oldValue, uint256 newValue)",
];
```
## 2. Subscribe to live events
Use a WebSocket provider so you receive events as soon as validators finalize each block. WebSocket avoids polling overhead and keeps notification latency close to block time (\~0.7 seconds on Stable).
```typescript theme={"dark"}
// watchLive.ts
import { ethers } from "ethers";
import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config";
const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS);
const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);
contract.on("NumberUpdated", (caller, oldValue, newValue, event) => {
console.log("NumberUpdated:");
console.log(" caller: ", caller);
console.log(" oldValue: ", oldValue.toString());
console.log(" newValue: ", newValue.toString());
console.log(" tx: ", event.log.transactionHash);
console.log(" block: ", event.log.blockNumber);
});
console.log("Listening for NumberUpdated events...");
```
```bash theme={"dark"}
npx tsx watchLive.ts
```
```text theme={"dark"}
Listening for NumberUpdated events...
NumberUpdated:
caller: 0x1234...abcd
oldValue: 0
newValue: 42
tx: 0x8f3a...2d41
block: 1284371
```
Events arrive in real time as callers invoke your contract.
## 3. Backfill historical events
When a service starts, you usually need to catch up on events emitted while it was offline. Use `queryFilter` with a block range.
```typescript theme={"dark"}
// backfill.ts
import { ethers } from "ethers";
import { STABLE_TESTNET_RPC, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config";
const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);
const latest = await provider.getBlockNumber();
const fromBlock = Math.max(0, latest - 10_000); // last ~10k blocks
const events = await contract.queryFilter(
contract.filters.NumberUpdated(),
fromBlock,
latest
);
for (const event of events) {
console.log(
`[block ${event.blockNumber}]`,
event.args.caller,
"set number to",
event.args.newValue.toString()
);
}
console.log(`Backfilled ${events.length} events from block ${fromBlock} to ${latest}`);
```
```bash theme={"dark"}
npx tsx backfill.ts
```
```text theme={"dark"}
[block 1282351] 0x1234...abcd set number to 10
[block 1283092] 0xef01...2345 set number to 25
[block 1284371] 0x1234...abcd set number to 42
Backfilled 3 events from block 1282351 to 1284371
```
Wide block ranges (millions of blocks) can exceed RPC rate limits and time out. For production indexers, paginate by 10k-block windows or use [Stablescan's Etherscan-compatible API](/en/how-to/build-p2p-payments#transaction-history) for indexed historical queries.
## 4. Filter events by indexed arguments
Events with `indexed` parameters (like `caller` above) can be filtered server-side. Pass the filter value instead of reading every event and filtering in your app.
```typescript theme={"dark"}
// watchUser.ts
import { ethers } from "ethers";
import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config";
const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS);
const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);
const userAddress = "0x1234...abcd";
const filter = contract.filters.NumberUpdated(userAddress);
contract.on(filter, (caller, oldValue, newValue, event) => {
console.log(`${caller} set number to ${newValue.toString()}`);
});
console.log(`Watching NumberUpdated for ${userAddress}...`);
```
```bash theme={"dark"}
npx tsx watchUser.ts
```
```text theme={"dark"}
Watching NumberUpdated for 0x1234...abcd...
0x1234...abcd set number to 42
```
## Handle connection drops
WebSocket connections can drop. For production indexers, implement reconnection logic so you don't miss events.
```typescript theme={"dark"}
// resilientWatch.ts
import { ethers } from "ethers";
import { STABLE_TESTNET_WS, CONTRACT_ADDRESS, CONTRACT_ABI } from "./config";
let reconnectAttempts = 0;
const MAX_RECONNECT = 5;
function setupWatcher() {
const provider = new ethers.WebSocketProvider(STABLE_TESTNET_WS);
const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);
contract.on("NumberUpdated", (caller, oldValue, newValue) => {
console.log(`${caller} set number to ${newValue.toString()}`);
});
provider.websocket.onerror = (err: any) => {
console.error("Provider error:", err);
if (reconnectAttempts < MAX_RECONNECT) {
reconnectAttempts++;
setTimeout(setupWatcher, 5000);
}
};
}
setupWatcher();
```
## Next recommended
Index system transaction events (unbonding completions) emitted by the protocol.
Apply indexing to USDT0 Transfer events and build a payment history view.
See which `eth_getLogs` and related methods Stable supports.
# Enable gas-free transactions
Source: https://docs.stable.xyz/en/how-to/integrate-gas-waiver
Integrate the Stable Gas Waiver to enable gas-free transactions by submitting signed InnerTx payloads to the Waiver Server API.
Gas Waiver enables gas-free transactions on Stable. With Gas Waiver, applications cover gas fees on behalf of users, so users can interact with contracts without holding USDT0 for gas.
This guide covers integrating via the Waiver Server API.
**Concept:** For what Gas Waiver is, why it exists, and how governance-authorized waivers work, see [Gas waiver](/en/explanation/gas-waiver). For the full protocol specification (wrapper transaction format, marker address, execution semantics, security model), see [Gas waiver protocol](/en/reference/gas-waiver-api).
## Prerequisites
* An API key for the Waiver Server, issued by the Stable team
* Target contract address must be registered in the waiver's `AllowedTarget` policy
## Waiver Server
**Base URLs:**
* Mainnet: TBD
* Testnet: `https://waiver.testnet.stable.xyz`
**Authorization:** `Bearer `
## Overview
The integration flow has three steps:
1. **Build an InnerTx**: the user signs a transaction with `gasPrice = 0`.
2. **Submit to Waiver Server**: submit the signed transaction to the Waiver Server API.
3. **Handle the response**: the waiver server wraps and broadcasts the transaction. Process the streamed results and surface the transaction hash to the user.
## Step 1: create the user's InnerTx
The user signs a standard transaction with `gasPrice = 0`. The `to` address and method selector must be permitted by the waiver's `AllowedTarget` policy.
```typescript theme={"dark"}
// config.ts
export const CONFIG = {
RPC_URL: "https://rpc.testnet.stable.xyz",
CHAIN_ID: 2201, // 988 for mainnet
WAIVER_SERVER: "https://waiver.testnet.stable.xyz",
USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
};
```
```typescript theme={"dark"}
import { ethers } from "ethers";
import { CONFIG } from "./config";
const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL);
const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [
"function transfer(address to, uint256 amount) returns (bool)"
], provider);
const callData = usdt0.interface.encodeFunctionData("transfer", [
recipientAddress,
ethers.parseUnits("0.01", 18)
]);
const gasEstimate = await provider.estimateGas({
from: userWallet.address,
to: CONFIG.USDT0_ADDRESS,
data: callData,
});
const nonce = await provider.getTransactionCount(userWallet.address);
const innerTx = {
to: CONFIG.USDT0_ADDRESS,
data: callData,
value: 0,
gasPrice: 0,
gasLimit: gasEstimate,
nonce: nonce,
chainId: CONFIG.CHAIN_ID,
};
const signedInnerTx = await userWallet.signTransaction(innerTx);
```
`gasPrice` must be `0`. If it is non-zero, the waiver server rejects the transaction.
## Step 2: submit to the Waiver Server
```typescript theme={"dark"}
import { CONFIG } from "./config";
const API_KEY = process.env.WAIVER_API_KEY;
const response = await fetch(`${CONFIG.WAIVER_SERVER}/v1/submit`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_KEY}`,
},
body: JSON.stringify({
transactions: [signedInnerTx],
}),
});
```
### Batch submissions
You can submit multiple signed transactions in a single request:
```typescript theme={"dark"}
body: JSON.stringify({
transactions: [signedTx1, signedTx2, signedTx3],
})
```
Each result line includes an `index` field corresponding to the transaction's position in the array.
## Step 3: handle the response
The response is streamed as NDJSON (newline-delimited JSON). Each line corresponds to one submitted transaction.
```typescript theme={"dark"}
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const lines = decoder.decode(value).trim().split("\n");
for (const line of lines) {
const result = JSON.parse(line);
if (result.success) {
console.log(`tx ${result.index} confirmed: ${result.txHash}`);
} else {
console.error(`tx ${result.index} failed: ${result.error.message}`);
}
}
}
```
**Success response:**
```json theme={"dark"}
{"index": 0, "id": "abc123", "success": true, "txHash": "0x..."}
```
**Failure response:**
```json theme={"dark"}
{"index": 1, "id": "def456", "success": false, "error": {"code": "VALIDATION_FAILED", "message": "invalid signature"}}
```
## Error codes
| **Code** | **Description** |
| :-------------------- | :------------------------------------------------------------------------- |
| `PARSE_ERROR` | Failed to parse transaction |
| `INVALID_REQUEST` | Malformed request body |
| `BATCH_SIZE_EXCEEDED` | Batch size exceeds allowed maximum |
| `VALIDATION_FAILED` | Transaction validation failed (e.g., invalid signature, disallowed target) |
| `BROADCAST_FAILED` | Failed to broadcast to chain |
| `RATE_LIMITED` | Rate limit exceeded |
| `QUEUE_FULL` | Server queue at capacity |
| `TIMEOUT` | Request timed out |
## API reference
### GET `/v1/health`
Health check endpoint. Authentication: none.
### POST `/v1/submit`
Submit a batch of signed inner transactions. Authentication: required (Bearer).
**Request body:**
```json theme={"dark"}
{
"transactions": ["0x", "0x"]
}
```
Response is streamed as NDJSON. Each line corresponds to a submitted transaction index.
### GET `/v1/submit`
WebSocket interface for streaming submissions. Authentication: required (Bearer).
## Key takeaways
* Gas Waiver is a server-side integration: your backend submits signed user transactions to the Waiver Server. Users never interact with the Waiver Server directly.
* The user always signs the InnerTx, preserving signature integrity. The waiver cannot modify the user's transaction.
* The target contract must be on the waiver's `AllowedTarget` list.
## Next recommended
See the demo-focused flow and how to verify zero gas on a receipt.
Run your own waiver without the hosted API.
Full wrapper transaction spec and governance model.
# Paying with invoice
Source: https://docs.stable.xyz/en/how-to/pay-with-invoice
Settle invoices on Stable using ERC-3009 with deterministic nonces derived from invoice metadata. Reconcile through on-chain events.
This guide walks through settling an invoice on-chain using [ERC-3009](/en/explanation/erc-3009) with a deterministic nonce derived from invoice metadata. The nonce links each payment to its invoice and prevents double payment.
**Concept:** For the invoice settlement model and comparison to traditional B2B invoicing, see [Invoice settlement](/en/reference/invoices).
## What you'll build
A full invoice lifecycle: the buyer signs an ERC-3009 authorization off-chain, the vendor submits it on-chain, and reconciliation matches the resulting `AuthorizationUsed` event back to the invoice by deterministic nonce.
### Demo
```text theme={"dark"}
step 1. Invoice issued
number: INV-2026-001234
amount: 5000 USDT0
dueDate: 2026-04-30
step 2. Buyer signs authorization (off-chain, no gas)
nonce: 0xa1b2...c3d4 (from invoice metadata)
signature: 0xf0e9...1234
step 3. Vendor submits transferWithAuthorization
tx: 0x8f3a...2d41
amount: 5000 USDT0 transferred to vendor
step 4. Reconciliation
AuthorizationUsed(nonce=0xa1b2...) → invoice INV-2026-001234
Transfer event verified for correct amount and parties
ERP: marked PAID at block 1284371
```
## Overview
**Buyer:**
```
─── Buyer ───────────────────────────────────────────
nonce = getInvoiceNonce(invoice)
authorization = { from: buyer, to: vendor, value: amount, nonce, ... }
signature = signTypedData(authorization)
// Option A: Buyer submits the transaction directly.
usdt0.transferWithAuthorization(authorization, signature)
// Option B: Buyer sends {authorization, signature} to the vendor.
// The vendor (or a facilitator) submits on the buyer's behalf.
```
**Vendor:**
```
─── Vendor ──────────────────────────────────────────
// If Option B: submit transferWithAuthorization using the buyer's signature
// Reconcile via AuthorizationUsed event
on AuthorizationUsed(authorizer, nonce):
invoice = nonceToInvoice.get(nonce)
transferLog = receipt.logs.find(Transfer matching invoice.buyer, invoice.vendor, invoice.amount)
if transferLog:
erpSystem.markPaid(invoice.id, txHash, settledAt)
```
## Configuration
```typescript theme={"dark"}
// config.ts
import { ethers } from "ethers";
export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const CHAIN_ID = 2201;
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
export const EIP712_DOMAIN = {
name: "USDT0",
version: "1",
chainId: CHAIN_ID,
verifyingContract: USDT0_ADDRESS,
};
export const TRANSFER_WITH_AUTHORIZATION_TYPE = {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
};
export interface Invoice {
number: string; // e.g. "INV-2026-001234"
vendor: string; // vendor wallet address
buyer: string; // buyer wallet address
amount: bigint; // amount in USDT0 atomic units (6 decimals)
dueDate: number; // Unix timestamp
}
```
## Step 1: Generate a deterministic nonce
Both the buyer and the vendor can independently compute the same nonce from invoice metadata. No external registry is needed.
```typescript theme={"dark"}
// nonce.ts
import { ethers } from "ethers";
import { Invoice } from "./config";
export function getInvoiceNonce(invoice: Invoice): string {
return ethers.solidityPackedKeccak256(
["string", "address", "address", "uint256", "uint256"],
[
invoice.number,
invoice.vendor,
invoice.buyer,
invoice.amount,
invoice.dueDate,
]
);
}
// Example
const invoice: Invoice = {
number: "INV-2026-001234",
vendor: "0xVendorAddress",
buyer: "0xBuyerAddress",
amount: ethers.parseUnits("5000", 6), // 5,000 USDT0
dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000),
};
const nonce = getInvoiceNonce(invoice);
// Same input always produces the same nonce.
// This nonce is consumed on-chain upon payment, preventing double payment.
```
## Step 2: Sign the authorization (buyer)
The buyer signs an ERC-3009 `transferWithAuthorization` using the deterministic nonce from Step 1.
```typescript theme={"dark"}
// sign-invoice.ts
import { ethers } from "ethers";
import {
provider,
EIP712_DOMAIN,
TRANSFER_WITH_AUTHORIZATION_TYPE,
Invoice,
} from "./config";
import { getInvoiceNonce } from "./nonce";
const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider);
async function signInvoiceAuthorization(invoice: Invoice) {
const nonce = getInvoiceNonce(invoice);
const gracePeriod = 30 * 24 * 60 * 60; // 30 days after due date
const authorization = {
from: invoice.buyer,
to: invoice.vendor,
value: invoice.amount,
validAfter: 0,
validBefore: invoice.dueDate + gracePeriod,
nonce,
};
const signature = await buyerWallet.signTypedData(
EIP712_DOMAIN,
TRANSFER_WITH_AUTHORIZATION_TYPE,
authorization
);
return { authorization, signature };
}
```
## Step 3: Submit the transaction
Two options depending on who submits.
### Option A: Buyer submits
The buyer submits the `transferWithAuthorization` transaction directly and pays gas. Use this when the buyer controls when and how the payment is executed, for example when the buyer's accounting system needs the tx hash tied to an internal approval flow.
```typescript theme={"dark"}
// pay.ts
import { ethers } from "ethers";
import { provider, USDT0_ADDRESS } from "./config";
const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider);
const usdt0 = new ethers.Contract(
USDT0_ADDRESS,
[
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)",
],
buyerWallet,
);
async function payInvoice(
authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string },
signature: string,
) {
const { v, r, s } = ethers.Signature.from(signature);
const tx = await usdt0.transferWithAuthorization(
authorization.from,
authorization.to,
authorization.value,
authorization.validAfter,
authorization.validBefore,
authorization.nonce,
v, r, s,
);
const receipt = await tx.wait(1);
console.log("Invoice paid, tx:", receipt.hash);
// The nonce is now consumed; the same invoice cannot be paid twice.
return { txHash: receipt.hash, blockNumber: receipt.blockNumber };
}
```
### Option B: Vendor submits
The buyer sends `{authorization, signature}` to the vendor through API, email, or any channel. The vendor (or a facilitator) submits the transaction on the buyer's behalf, so the buyer does not need to manage gas. Use this when the vendor needs synchronous confirmation within the same request flow.
```typescript theme={"dark"}
// settle.ts
import { ethers } from "ethers";
import { provider, USDT0_ADDRESS } from "./config";
const vendorWallet = new ethers.Wallet(process.env.VENDOR_KEY!, provider);
const usdt0 = new ethers.Contract(
USDT0_ADDRESS,
[
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)",
],
vendorWallet,
);
async function settleInvoice(
authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string },
signature: string,
) {
const { v, r, s } = ethers.Signature.from(signature);
const tx = await usdt0.transferWithAuthorization(
authorization.from,
authorization.to,
authorization.value,
authorization.validAfter,
authorization.validBefore,
authorization.nonce,
v, r, s,
);
const receipt = await tx.wait(1);
console.log("Invoice settled, tx:", receipt.hash);
return { txHash: receipt.hash, blockNumber: receipt.blockNumber };
}
```
## Step 4: Reconcile via on-chain events (vendor)
Regardless of who submitted the transaction, every invoice payment emits an `AuthorizationUsed` event carrying the deterministic nonce. The vendor listens for this event and matches it to a pending invoice by nonce. Because the nonce is derived from invoice metadata, matching is exact.
Matching by nonce identifies which invoice was paid, but the vendor should also verify the `Transfer` event in the same transaction to confirm that the correct amount was sent to the correct recipient. The code below includes this verification.
```typescript theme={"dark"}
// reconcile.ts
import { ethers } from "ethers";
import { provider, USDT0_ADDRESS, Invoice } from "./config";
import { getInvoiceNonce } from "./nonce";
const usdt0 = new ethers.Contract(
USDT0_ADDRESS,
[
"event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
],
provider,
);
// Build a lookup map: nonce -> invoice.
// In production, this comes from your invoice database.
const invoices: Invoice[] = [
{
number: "INV-2026-001234",
vendor: "0xVendorAddress",
buyer: "0xBuyerAddress",
amount: ethers.parseUnits("5000", 6),
dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000),
},
];
const nonceToInvoice = new Map();
for (const inv of invoices) {
nonceToInvoice.set(getInvoiceNonce(inv), inv);
}
usdt0.on("AuthorizationUsed", async (authorizer: string, nonce: string, event: any) => {
const invoice = nonceToInvoice.get(nonce);
if (!invoice) return; // not one of our invoices
const receipt = await event.getTransactionReceipt();
const transferLog = receipt.logs
.map((log: any) => {
try { return usdt0.interface.parseLog(log); } catch { return null; }
})
.find(
(parsed: any) =>
parsed?.name === "Transfer" &&
parsed.args[0].toLowerCase() === invoice.buyer.toLowerCase() &&
parsed.args[1].toLowerCase() === invoice.vendor.toLowerCase() &&
parsed.args[2] === invoice.amount
);
if (!transferLog) {
console.error("No matching Transfer event for invoice:", invoice.number);
return;
}
// All checks passed
console.log(`Invoice ${invoice.number} PAID`);
console.log(" tx:", receipt.hash);
console.log(" settled at block:", receipt.blockNumber);
// In production: update your ERP/accounting system here
// erpSystem.markPaid(invoice.number, receipt.hash, receipt.blockNumber);
});
console.log("Listening for invoice settlements...");
```
```bash theme={"dark"}
npx tsx reconcile.ts
```
```text theme={"dark"}
Listening for invoice settlements...
Invoice INV-2026-001234 PAID
tx: 0x8f3a...2d41
settled at block: 1284371
```
## Handle failed payments
A submitted `transferWithAuthorization` can revert for several reasons. Detect and surface each one to the vendor or buyer so the invoice can be retried or closed.
| **Revert reason** | **Cause** | **Recovery** |
| :----------------------------------------------- | :------------------------------------------------------------------------ | :------------------------------------------------------------------ |
| `FiatTokenV2: invalid signature` | Signature doesn't match the authorization fields. | Ask buyer to re-sign with unchanged invoice data. |
| `FiatTokenV2: authorization is used or canceled` | Nonce was already consumed (double-submission) or the buyer cancelled it. | Mark the invoice as already-paid; look up the original tx by nonce. |
| `FiatTokenV2: authorization is not yet valid` | Submitted before `validAfter`. | Wait until `validAfter` or issue a new authorization. |
| `FiatTokenV2: authorization is expired` | Submitted after `validBefore`. | Issue a new authorization with an extended window. |
| `FiatTokenV2: transfer amount exceeds balance` | Buyer's USDT0 balance is insufficient. | Notify buyer to fund their wallet, then retry the same signature. |
Catch reverts and classify them before retrying.
```typescript theme={"dark"}
// retry.ts
import { ethers } from "ethers";
async function submitWithRetry(
submit: () => Promise,
): Promise {
try {
const tx = await submit();
const receipt = await tx.wait(1);
return receipt!.hash;
} catch (err: any) {
const reason = err?.info?.error?.message || err?.reason || err?.message || "";
if (reason.includes("authorization is used or canceled")) {
// Lookup the original tx by AuthorizationUsed event; mark invoice paid.
throw new Error("ALREADY_PAID");
}
if (reason.includes("authorization is expired")) {
throw new Error("AUTHORIZATION_EXPIRED");
}
if (reason.includes("invalid signature")) {
throw new Error("INVALID_SIGNATURE");
}
if (reason.includes("transfer amount exceeds balance")) {
throw new Error("INSUFFICIENT_BALANCE");
}
throw err;
}
}
```
Never retry a failed submission without classifying the error. Blind retries on a reverted transferWithAuthorization can pass validation after the buyer tops up their balance, which may not match the buyer's latest intent.
## Next recommended
Understand the deterministic-nonce reconciliation model.
Review the signed-authorization standard behind this flow.
Combine with Gas Waiver to eliminate gas from the settlement path.
# Paying with MCP server
Source: https://docs.stable.xyz/en/how-to/pay-with-mcp
Connect x402-enabled APIs to AI clients through an MCP server. Users pay for API calls through prompts without managing wallets or payment flows directly.
This guide shows how to bridge x402-enabled APIs to [MCP](https://modelcontextprotocol.io) tools so AI clients can call and pay for them through natural-language prompts. It builds on the server from [Build a pay-per-call API](/en/how-to/build-pay-per-call).
## What you'll build
An MCP server that wraps x402-paid endpoints as tools. The AI client types a natural-language prompt, each tool call triggers a paid x402 request, and settlement is visible on Stablescan. The user never sees a wallet prompt.
### Demo
```text theme={"dark"}
step 1. User in Claude: "Pull financials for ACME Corp and assess credit risk."
step 2. Client calls get_company_financials("ACME")
→ MCP handler: fetchWithPayment("/financials?ticker=ACME")
→ 402 Payment Required → sign ERC-3009 → retry
→ Facilitator settles $0.01 USDT0 on-chain
→ tx: 0x8f3a...aaaa
→ 200 OK { revenue, debt_ratio, cash_flow }
step 3. Client calls assess_credit_risk(financials)
→ MCP handler: fetchWithPayment("/credit-risk", POST)
→ Facilitator settles $0.05 USDT0 on-chain
→ tx: 0x9bc4...bbbb
→ 200 OK { score: 72, rating: "moderate" }
step 4. Claude responds:
"ACME Corp has a credit risk score of 72 (moderate). Revenue is stable
but debt-to-equity ratio is elevated at 1.8x..."
```
Both `tx` values are visible on [https://stablescan.xyz](https://stablescan.xyz).
**Agent wallet funding**: The MCP server signs payments with a seed phrase you control. Fund that wallet with USDT0 on mainnet before starting the server. A balance of at least `$0.10` covers several paid calls; `$1.00` is plenty for extended testing. Top up as needed using a standard USDT0 transfer to the wallet's address.
## Overview
**MCP Server:**
```typescript theme={"dark"}
// --- MCP Server ---
// Bridge x402-enabled APIs to MCP tools
tools = {
"get_company_financials": {
handler: (ticker) =>
fetchWithPayment("https://api.example.com/financials?ticker=" + ticker),
},
"assess_credit_risk": {
handler: (financials) =>
fetchWithPayment("https://api.example.com/credit-risk", {
method: "POST",
body: JSON.stringify({ financials }),
}),
},
}
```
**User (via AI client):**
```
─── AI Client ───────────────────────────────────────
User: "Pull financials for ACME Corp and assess their credit risk."
Client calls get_company_financials tool
→ MCP server sends x402 paid request
→ Facilitator settles USDT0 on-chain
→ API returns financial data
Client calls assess_credit_risk tool with the result
→ MCP server sends x402 paid request
→ Facilitator settles USDT0 on-chain
→ API returns risk assessment
→ Client responds with the combined result
```
## Prerequisites
* A running x402 server (see [Build a pay-per-call API](/en/how-to/build-pay-per-call)).
* An MCP-compatible AI client (Claude Desktop, Claude Code, etc.).
## Step 1: Create the MCP server
The MCP server acts as a bridge between AI clients and x402-enabled APIs. Each tool makes a paid request using the x402 client SDK and returns the result.
```bash theme={"dark"}
npm install @modelcontextprotocol/sdk @x402/fetch @x402/evm @tetherto/wdk-wallet-evm
```
```typescript theme={"dark"}
// mcp-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import WalletManagerEvm from "@tetherto/wdk-wallet-evm";
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { z } from "zod";
// --- Wallet and x402 client ---
const account = await new WalletManagerEvm(process.env.SEED_PHRASE!, {
provider: "https://rpc.stable.xyz",
}).getAccount(0);
const client = new x402Client();
registerExactEvmScheme(client, { signer: account });
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
// --- x402 API base URL ---
const API_BASE = process.env.API_BASE || "http://localhost:4021";
// --- MCP server ---
const server = new McpServer({
name: "x402-payments",
version: "1.0.0",
});
server.tool(
"get_company_financials",
"Get company financial data by ticker (paid endpoint, $0.01 per call)",
{ ticker: z.string().describe("Company ticker symbol (e.g. ACME)") },
async ({ ticker }) => {
const response = await fetchWithPayment(`${API_BASE}/financials?ticker=${ticker}`);
const data = await response.json();
return { content: [{ type: "text", text: JSON.stringify(data) }] };
},
);
server.tool(
"assess_credit_risk",
"Assess credit risk from financial data (paid endpoint, $0.05 per call)",
{ financials: z.string().describe("JSON string of company financial data") },
async ({ financials }) => {
const response = await fetchWithPayment(`${API_BASE}/credit-risk`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: financials,
});
const data = await response.json();
return { content: [{ type: "text", text: JSON.stringify(data) }] };
},
);
server.tool(
"check_balance",
"Check the USDT0 balance of the payment wallet",
{},
async () => {
const USDT0_STABLE = "0x779Ded0c9e1022225f8E0630b35a9b54bE713736";
const balance = await account.getTokenBalance(USDT0_STABLE);
const formatted = (Number(balance) / 1e6).toFixed(2);
return {
content: [{ type: "text", text: `Wallet balance: ${formatted} USDT0` }],
};
},
);
// --- Start ---
const transport = new StdioServerTransport();
await server.connect(transport);
```
Each tool handler calls `fetchWithPayment`, which handles the full x402 payment cycle automatically. The AI client only sees the tool name, description, and parameters.
## Step 2: Configure your AI client
Add the MCP server to your AI client's configuration.
**Claude Desktop** (`claude_desktop_config.json`):
```json theme={"dark"}
{
"mcpServers": {
"x402-payments": {
"command": "npx",
"args": ["tsx", "/path/to/mcp-server.ts"],
"env": {
"SEED_PHRASE": "your seed phrase here",
"API_BASE": "https://api.example.com"
}
}
}
}
```
**Claude Code:**
```bash theme={"dark"}
claude mcp add x402-payments -- npx tsx /path/to/mcp-server.ts
```
After configuration, restart your AI client. The tools should appear in the available tool list.
The seed phrase in the MCP configuration controls real funds. Store it securely using your OS keychain or a secrets manager rather than in plain-text config files.
## Step 3: Type the prompt and use it
Once configured, the AI client can call paid APIs through the user's prompt:
**User:** "Pull financials for ACME Corp and assess their credit risk."
1. Client calls `get_company_financials("ACME")`: \$0.01 paid via x402. Returns revenue, debt ratio, cash flow, etc.
2. Client calls `assess_credit_risk(financials)`: \$0.05 paid via x402. Returns risk score, rating, key factors.
3. Client responds: "ACME Corp has a credit risk score of 72 (moderate). Revenue is stable but debt-to-equity ratio is elevated at 1.8x..."
Individual tools also work on their own:
* "Pull financials for ACME Corp" calls `get_company_financials` (\$0.01).
* "Assess credit risk for this data" calls `assess_credit_risk` (\$0.05).
* "How much USDT0 do I have left?" calls `check_balance`.
The user does not interact with wallets, signatures, or payment flows. The MCP server handles payment for each tool call transparently.
## Spending controls
To prevent unexpected spending, consider adding controls to the MCP server.
```typescript theme={"dark"}
const MAX_PER_CALL = 100_000; // $0.10 in base units
const MAX_PER_SESSION = 5_000_000; // $5.00 in base units
let sessionSpent = 0n;
function checkSpendingLimit(amount: bigint) {
if (amount > BigInt(MAX_PER_CALL)) {
throw new Error(`Amount exceeds per-call limit of $${MAX_PER_CALL / 1e6}`);
}
if (sessionSpent + amount > BigInt(MAX_PER_SESSION)) {
throw new Error(`Session spending limit of $${MAX_PER_SESSION / 1e6} reached`);
}
sessionSpent += amount;
}
```
These limits run server-side. The AI client cannot modify or bypass them.
## Next recommended
Set up the x402 server this MCP server bridges.
Review the settlement protocol behind these payments.
Wire Stable's Docs and Runtime MCP servers into the same AI client.
# Self-hosted gas waiver
Source: https://docs.stable.xyz/en/how-to/self-hosted-gas-waiver
Run your own gas waiver on Stable. Register a waiver address through governance, construct wrapper transactions, and broadcast them directly without the hosted Waiver Server API.
Self-hosted Gas Waiver lets you operate your own waiver infrastructure instead of using the hosted Waiver Server API. You register a waiver address through on-chain governance, then broadcast wrapper transactions directly to the network.
This guide covers registering a waiver address, collecting signed user transactions, constructing wrapper transactions, and broadcasting them.
**Concept:** For what Gas Waiver is and why it exists, see [Gas waiver](/en/explanation/gas-waiver). For the full protocol specification (wrapper transaction mechanism, authorization, policy checks, execution semantics, security model), see [Gas waiver protocol](/en/reference/gas-waiver-api).
For the hosted Waiver Server API integration path, see [Enable gas-free transactions](/en/how-to/integrate-gas-waiver).
## Prerequisites
* A waiver address registered on-chain via validator governance.
* `AllowedTarget` policy configured for your target contracts.
## Overview
The self-hosted flow:
1. **Collect a signed InnerTx** from the user with `gasPrice = 0`.
2. **Construct a WrapperTx**: RLP-encode the InnerTx and wrap it in a transaction sent to the marker address.
3. **Broadcast** the WrapperTx via `eth_sendRawTransaction`.
## Step 1: Collect the user's InnerTx
The user signs a transaction with `gasPrice = 0`. The `to` address and method selector must match your waiver's `AllowedTarget` policy.
```typescript theme={"dark"}
// config.ts
export const CONFIG = {
RPC_URL: "https://rpc.testnet.stable.xyz",
CHAIN_ID: 2201, // 988 for mainnet
MARKER_ADDRESS: "0x000000000000000000000000000000000000f333",
USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
};
```
```typescript theme={"dark"}
// collectInnerTx.ts
import { ethers } from "ethers";
import { CONFIG } from "./config";
const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL);
const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [
"function transfer(address to, uint256 amount) returns (bool)"
], provider);
const callData = usdt0.interface.encodeFunctionData("transfer", [
recipientAddress,
ethers.parseUnits("0.01", 18)
]);
const gasEstimate = await provider.estimateGas({
from: userWallet.address,
to: CONFIG.USDT0_ADDRESS,
data: callData,
});
const nonce = await provider.getTransactionCount(userWallet.address);
const innerTx = {
to: CONFIG.USDT0_ADDRESS,
data: callData,
value: 0,
gasPrice: 0,
gasLimit: gasEstimate,
nonce: nonce,
chainId: CONFIG.CHAIN_ID,
};
const signedInnerTx = await userWallet.signTransaction(innerTx);
```
## Step 2: Construct the WrapperTx
RLP-encode the signed InnerTx and wrap it in a transaction to the marker address. The `gasLimit` must cover both the inner execution and the wrapping overhead.
```typescript theme={"dark"}
// constructWrapper.ts
import { ethers } from "ethers";
import { CONFIG } from "./config";
const innerTxBytes = ethers.decodeRlp(signedInnerTx);
const rlpEncoded = ethers.encodeRlp(innerTxBytes);
const waiverNonce = await provider.getTransactionCount(waiverWallet.address);
const wrapperTx = {
to: CONFIG.MARKER_ADDRESS,
data: rlpEncoded,
value: 0,
gasPrice: 0,
gasLimit: (gasEstimate * 12n / 10n) * 2n, // ~2x inner gas for overhead
nonce: waiverNonce,
chainId: CONFIG.CHAIN_ID,
};
const signedWrapperTx = await waiverWallet.signTransaction(wrapperTx);
```
Both `InnerTx.gasPrice` and `WrapperTx.gasPrice` must be `0`. `WrapperTx.value` must also be `0`. If any of these conditions are not met, validators will reject the transaction.
## Step 3: Broadcast
Submit the signed WrapperTx via standard JSON-RPC.
```typescript theme={"dark"}
// broadcast.ts
const txHash = await provider.send("eth_sendRawTransaction", [signedWrapperTx]);
console.log("Wrapper tx broadcast:", txHash);
const receipt = await provider.waitForTransaction(txHash);
console.log("Confirmed:", receipt.status === 1);
```
```text theme={"dark"}
Wrapper tx broadcast: 0x...
Confirmed: true
```
## Key takeaways
* Self-hosted waiver requires a waiver address registered through on-chain validator governance.
* The WrapperTx is sent to the marker address (`0x...f333`) with the RLP-encoded InnerTx as data.
* Both InnerTx and WrapperTx must have `gasPrice = 0` and `value = 0`.
## Next recommended
Understand the mechanism before you run your own.
Reference the full protocol spec for marker routing, authorization, and execution semantics.
Use the hosted Waiver Server API instead of self-hosting.
# Subscribe and collect
Source: https://docs.stable.xyz/en/how-to/subscribe-and-collect
Build pull-based subscription payments on Stable using EIP-7702 delegation. Set up subscriber authorization and automate billing-cycle collection.
This guide walks through building a subscription payment system where the subscriber authorizes once and the service provider collects each billing cycle automatically via EIP-7702 account abstraction.
**Concept:** For the subscription model, trade-offs, and comparison to card-on-file billing, see [Subscription billing](/en/reference/subscriptions).
## What you'll build
A full subscription lifecycle: the subscriber delegates and subscribes once, the provider collects on schedule (second cycle shown to prove repeat behavior), and the subscriber cancels.
### Demo
```text theme={"dark"}
step 1. Subscriber delegates EOA to SubscriptionManager (EIP-7702)
tx: 0x7702...aaaa
step 2. Subscriber registers subscription (10 USDT0 / 30 days)
subscriptionId: 0xabc...
nextChargeAt: 2026-05-23T12:00:00Z
step 3. Provider calls collect() on day 30
collected: 10 USDT0
gas cost: ~0.000050 USDT0
nextChargeAt: 2026-06-22T12:00:00Z
step 4. Provider calls collect() on day 60
collected: 10 USDT0
gas cost: ~0.000050 USDT0
nextChargeAt: 2026-07-22T12:00:00Z
step 5. Subscriber cancels
subscription: inactive
```
## Overview
**Subscriber:**
```
─── Subscriber ───────────────────────────────────────
// One-time setup: delegate EOA to the subscription contract
signAuthorization(delegateContract)
sendTransaction({ type: 4, authorizationList: [signedAuth] })
// Subscribe: set billing terms on own EOA
sendTransaction({ to: self, data: subscribe(subscriptionId, provider, amount, interval) })
// Cancel: revoke billing access at any time
sendTransaction({ to: self, data: cancelSubscription(subscriptionId) })
```
**Service provider:**
```
─── Service Provider ────────────────────────────────
// Each billing cycle: collect payment from subscriber's EOA
// The delegate contract verifies caller, billing schedule, and amount
sendTransaction({ to: subscriberEOA, data: collect(subscriptionId) })
// Automate with a cron job matching the billing interval
// The contract reverts if called before the interval has elapsed
```
## Delegate contract
Subscription billing works by delegating the subscriber's EOA to a contract that enforces billing terms. Through EIP-7702, the subscriber's account temporarily gains contract logic, allowing a service provider to collect payments at each billing cycle without requiring the subscriber to sign every time.
You can use an existing deployed contract or deploy your own. The example below is a minimal `SubscriptionManager` contract that supports three operations:
* `subscribe`: register billing terms for a `subscriptionId`.
* `collect`: provider pulls the next scheduled payment for that `subscriptionId`.
* `cancelSubscription`: subscriber revokes a specific subscription.
```solidity theme={"dark"}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @title SubscriptionManager (example)
/// @notice Delegate contract for EIP-7702 subscription billing.
/// Runs on the subscriber's EOA via delegation.
contract SubscriptionManager {
struct Subscription {
address provider;
uint256 amount;
uint256 interval;
uint256 nextChargeAt;
bool active;
}
// Keyed by subscriptionId.
// Storage is already per subscriber EOA under delegation.
mapping(bytes32 => Subscription) public subscriptions;
IERC20 public immutable usdt0;
event SubscriptionCreated(
bytes32 indexed subscriptionId,
address indexed provider,
uint256 amount,
uint256 interval,
uint256 nextChargeAt
);
event SubscriptionCollected(
bytes32 indexed subscriptionId,
address indexed provider,
uint256 amount,
uint256 collectedAt
);
event SubscriptionCancelled(bytes32 indexed subscriptionId);
constructor(address _usdt0) {
usdt0 = IERC20(_usdt0);
}
/// @notice Register a subscription. Called by the subscriber on their own EOA.
function subscribe(
bytes32 subscriptionId,
address provider,
uint256 amount,
uint256 interval
) external {
require(msg.sender == address(this), "subscriber only");
require(provider != address(0), "invalid provider");
require(amount > 0, "invalid amount");
require(interval > 0, "invalid interval");
require(!subscriptions[subscriptionId].active, "already exists");
uint256 nextChargeAt = block.timestamp + interval;
subscriptions[subscriptionId] = Subscription({
provider: provider,
amount: amount,
interval: interval,
nextChargeAt: nextChargeAt,
active: true
});
emit SubscriptionCreated(subscriptionId, provider, amount, interval, nextChargeAt);
}
/// @notice Collect a payment for a specific subscription. Called by the service provider.
function collect(bytes32 subscriptionId) external {
Subscription storage sub = subscriptions[subscriptionId];
require(sub.active, "not active");
require(msg.sender == sub.provider, "not provider");
require(block.timestamp >= sub.nextChargeAt, "too early");
sub.nextChargeAt += sub.interval;
require(usdt0.transfer(sub.provider, sub.amount), "transfer failed");
emit SubscriptionCollected(subscriptionId, sub.provider, sub.amount, block.timestamp);
}
/// @notice Cancel a specific subscription. Called by the subscriber.
function cancelSubscription(bytes32 subscriptionId) external {
require(msg.sender == address(this), "subscriber only");
require(subscriptions[subscriptionId].active, "not active");
delete subscriptions[subscriptionId];
emit SubscriptionCancelled(subscriptionId);
}
}
```
This contract is provided as a reference implementation for testing purposes. A delegate contract has full execution authority over the subscriber's EOA, so in production, use an audited and verified contract. For more context on EIP-7702 delegation and security, see [EIP-7702](/en/explanation/eip-7702).
## Configuration
```typescript theme={"dark"}
// config.ts
import { ethers } from "ethers";
export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const CHAIN_ID = 2201;
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
export const SUBSCRIPTION_MANAGER = "0xYourDeployedSubscriptionManager";
export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
export const subscriberWallet = new ethers.Wallet(process.env.SUBSCRIBER_KEY!, provider);
```
## Step 1: Delegate the subscriber's EOA (EIP-7702)
The subscriber signs an EIP-7702 authorization to delegate their EOA to the `SubscriptionManager`. After this, the subscriber's EOA executes the delegate contract's logic.
```typescript theme={"dark"}
// delegate.ts
import { subscriberWallet, provider, CHAIN_ID, SUBSCRIPTION_MANAGER } from "./config";
const authorization = {
chainId: CHAIN_ID,
address: SUBSCRIPTION_MANAGER,
nonce: await provider.getTransactionCount(subscriberWallet.address),
};
const signedAuth = await subscriberWallet.signAuthorization(authorization);
const tx = await subscriberWallet.sendTransaction({
type: 4,
to: subscriberWallet.address,
authorizationList: [signedAuth],
maxPriorityFeePerGas: 0n,
});
const receipt = await tx.wait(1);
console.log("Delegation tx:", receipt.hash);
```
```bash theme={"dark"}
npx tsx delegate.ts
```
```text theme={"dark"}
Delegation tx: 0x7702...aaaa
```
## Step 2: Register a subscription (subscriber)
The subscriber calls `subscribe()` on their own EOA. Since the EOA is delegated, this executes `SubscriptionManager.subscribe`.
```typescript theme={"dark"}
// subscribe.ts
import { ethers } from "ethers";
import { subscriberWallet } from "./config";
const subscriptionManager = new ethers.Interface([
"function subscribe(bytes32 subscriptionId, address provider, uint256 amount, uint256 interval)",
]);
const serviceProvider = "0xServiceProviderAddress";
const monthlyAmount = ethers.parseUnits("10", 6); // 10 USDT0
const interval = 30 * 24 * 60 * 60; // 30 days in seconds
// Derive a unique subscriptionId from provider + plan name + local nonce
const subscriptionId = ethers.solidityPackedKeccak256(
["address", "string", "uint256"],
[serviceProvider, "pro-monthly", 1]
);
const tx = await subscriberWallet.sendTransaction({
to: subscriberWallet.address, // call self (delegate code executes)
data: subscriptionManager.encodeFunctionData("subscribe", [
subscriptionId,
serviceProvider,
monthlyAmount,
interval,
]),
maxPriorityFeePerGas: 0n,
});
const receipt = await tx.wait(1);
console.log("Subscription registered, tx:", receipt.hash);
console.log("Subscription ID:", subscriptionId);
```
```bash theme={"dark"}
npx tsx subscribe.ts
```
```text theme={"dark"}
Subscription registered, tx: 0xabcd...1234
Subscription ID: 0xfedc...9876
```
## Step 3: Collect a payment (service provider)
Each billing cycle, the service provider calls `collect(subscriptionId)` on the subscriber's EOA. The delegate logic verifies the caller, billing schedule, and amount before transferring USDT0.
```typescript theme={"dark"}
// collect.ts
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz");
const providerWallet = new ethers.Wallet(process.env.PROVIDER_KEY!, provider);
const subscriptionManager = new ethers.Interface([
"function collect(bytes32 subscriptionId)",
]);
const subscriberEOA = "0xSubscriberEOAAddress";
const subscriptionId = "0xYourSubscriptionId";
const tx = await providerWallet.sendTransaction({
to: subscriberEOA, // subscriber's EOA (runs delegate code)
data: subscriptionManager.encodeFunctionData("collect", [subscriptionId]),
maxPriorityFeePerGas: 0n,
});
const receipt = await tx.wait(1);
console.log("Payment collected, tx:", receipt.hash);
console.log("Gas used:", receipt.gasUsed.toString());
// In production, run this on a cron schedule matching the billing interval.
// The delegate contract will revert if called before the interval has elapsed.
```
```bash theme={"dark"}
npx tsx collect.ts
```
```text theme={"dark"}
Payment collected, tx: 0x8f3a...2d41
Gas used: 52000
```
A `collect()` call costs roughly **50k-55k gas** on Stable (21k base + 7702 delegation overhead + ERC-20 `transfer`). At a 1 gwei base fee, that's approximately `0.000050 USDT0` per billing cycle paid by the provider.
## Step 4: Cancel a subscription (subscriber)
The subscriber calls `cancelSubscription(subscriptionId)` on their own EOA to revoke billing access for that specific subscription.
```typescript theme={"dark"}
// cancel.ts
import { ethers } from "ethers";
import { subscriberWallet } from "./config";
const subscriptionManager = new ethers.Interface([
"function cancelSubscription(bytes32 subscriptionId)",
]);
const subscriptionId = "0xYourSubscriptionId";
const tx = await subscriberWallet.sendTransaction({
to: subscriberWallet.address,
data: subscriptionManager.encodeFunctionData("cancelSubscription", [subscriptionId]),
maxPriorityFeePerGas: 0n,
});
const receipt = await tx.wait(1);
console.log("Subscription cancelled, tx:", receipt.hash);
```
```bash theme={"dark"}
npx tsx cancel.ts
```
```text theme={"dark"}
Subscription cancelled, tx: 0xdef0...5678
```
## Security model
The subscriber is authorizing the delegate contract to pull funds from their EOA. Understand exactly what that authorization covers and how to limit exposure.
**What the subscriber is authorizing.** By delegating to `SubscriptionManager`, the subscriber grants the contract's logic full execution authority over their EOA. The delegate can only transfer funds under the conditions coded into it: caller is the registered provider, the interval has elapsed, the amount matches the stored subscription. It cannot transfer to other addresses or bypass the interval check, because the contract code doesn't allow those actions.
**Failure modes to mitigate.**
* **Malicious delegate upgrade**: if the `SubscriptionManager` is a proxy whose implementation can be changed by an admin, the authorization effectively trusts that admin. Delegate only to immutable contracts or proxies with transparent, time-locked upgrades.
* **Provider compromise**: if the provider's key leaks, an attacker can collect early payments up to the per-cycle amount. Subscribers should set a `spendingLimit` per subscription and monitor for unauthorized `SubscriptionCollected` events.
* **Delegation replacement**: subscribing again with a different delegate wipes the subscription state. Use a modular delegate that supports multiple functions (subscription, batch payments, spending limits) under a single delegation, rather than one delegate per feature.
* **Replayable signatures**: all signatures use EIP-7702 nonces tied to the subscriber's EOA, so they can't replay across chains or across delegations.
**Recommended guardrails.**
* Audit the delegate contract before production use.
* Keep per-subscription amounts small relative to the subscriber's balance.
* Monitor `SubscriptionCreated` / `SubscriptionCollected` events and surface them to the subscriber.
* Offer the subscriber a clear "cancel" UI that calls `cancelSubscription(subscriptionId)` on their own EOA.
## Important considerations
* **Persistent delegation**: the EIP-7702 delegation persists until the subscriber explicitly changes or clears it. No re-delegation needed each billing cycle.
* **Single delegation per EOA**: if the subscriber later delegates to a different contract, the subscription delegate logic is replaced and collection fails. Use a modular delegate contract that supports multiple functions (subscriptions, batch payments, spending limits, session keys) under a single delegation.
* **Schedule behavior**: this example advances `nextChargeAt` by one interval on each successful collection. If more than one billing period has elapsed, repeated `collect()` calls can catch up one period at a time. Extend the logic if your product requires a different policy.
* **Use audited delegates**: only delegate to contracts that have been audited.
## Next recommended
Understand the pull-based billing model.
See how batch payments, spending limits, and session keys combine under one delegation.
Review the delegation model that makes this possible.
# Tracking unbonding completions
Source: https://docs.stable.xyz/en/how-to/track-unbonding
Subscribe to UnbondingCompleted events emitted by the StableSystem precompile through system transactions. Filter by user or validator, query historical events.
When an unbonding period completes, the protocol emits an `UnbondingCompleted` event through the `StableSystem` precompile (`0x0000000000000000000000000000000000009999`) via a system transaction. This lets dApps notify users and update balances in real time without running custom indexers or polling REST endpoints.
**Concept:** For how system transactions bridge SDK-layer events to the EVM and why it matters, see [System transactions](/en/explanation/system-transactions).
## Prerequisites
* Understanding of [System transactions](/en/explanation/system-transactions).
* Familiarity with [Staking](/en/explanation/staking-module), specifically `undelegate` and the unbonding process.
* Experience with contract event subscription and filtering using a standard web3 library (e.g. [ethers.js](https://docs.ethers.org/) v6).
## Overview
* **Set up the contract instance**: create a contract instance for the StableSystem precompile.
* **Handle events in your application**: subscribe to real-time events or query historical data depending on your application logic.
* **Handle connection issues**: implement reconnection logic for persistent WebSocket subscriptions.
## Step 1: Set up the contract instance
Create a contract instance for the `StableSystem` precompile using the `UnbondingCompleted` event ABI.
```typescript theme={"dark"}
// config.ts
import { ethers } from "ethers";
export const STABLE_SYSTEM_ADDRESS =
"0x0000000000000000000000000000000000009999";
export const STABLE_SYSTEM_ABI = [
"event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)",
];
export const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz");
export const stableSystem = new ethers.Contract(
STABLE_SYSTEM_ADDRESS,
STABLE_SYSTEM_ABI,
provider
);
```
## Step 2: Handle events in your application
Subscribe to real-time events, query historical data, or both depending on your application logic.
### Real-time subscription
Subscribe to `UnbondingCompleted` events for real-time notifications when any unbonding completes. Useful for triggering balance updates, sending notifications, or refreshing dashboard statistics.
```typescript theme={"dark"}
// subscribeBasic.ts
import { stableSystem } from "./config";
stableSystem.on("UnbondingCompleted", (delegator, validator, amount, event) => {
console.log("Unbonding completed:");
console.log(" Delegator:", delegator);
console.log(" Validator:", validator);
console.log(" Amount:", ethers.formatEther(amount), "tokens");
console.log(" Block:", event.log.blockNumber);
console.log(" Tx Hash:", event.log.transactionHash);
});
```
### Filter by user
To only receive events for a particular delegator address, use the indexed event parameters to create a filter.
```typescript theme={"dark"}
// subscribeByUser.ts
import { ethers } from "ethers";
import { stableSystem } from "./config";
const userAddress = "0xabcd...";
const filter = stableSystem.filters.UnbondingCompleted(userAddress);
stableSystem.on(filter, (delegator, validator, amount, event) => {
refreshUserBalance(userAddress);
showNotification(
`Your unbonding of ${ethers.formatEther(amount)} tokens completed!`
);
});
```
### Filter by validator
```typescript theme={"dark"}
// subscribeByValidator.ts
import { stableSystem } from "./config";
const validatorAddress = "0x1234...";
const validatorFilter = stableSystem.filters.UnbondingCompleted(
null,
validatorAddress
);
stableSystem.on(validatorFilter, (delegator, validator, amount) => {
updateValidatorStats(validator, amount);
});
```
### Historical query
If your dApp needs to show a history of past unbonding completions, query historical events using event filters with block ranges.
```typescript theme={"dark"}
// queryHistory.ts
import { ethers } from "ethers";
import { provider, stableSystem } from "./config";
async function getUnbondingHistory(
userAddress: string,
fromBlock: number,
toBlock: number
) {
const filter = stableSystem.filters.UnbondingCompleted(userAddress);
const events = await stableSystem.queryFilter(filter, fromBlock, toBlock);
return events.map((event) => ({
delegator: event.args.delegator,
validator: event.args.validator,
amount: ethers.formatEther(event.args.amount),
blockNumber: event.blockNumber,
txHash: event.transactionHash,
}));
}
const currentBlock = await provider.getBlockNumber();
const history = await getUnbondingHistory(
"0xabcd...",
currentBlock - 1000,
currentBlock
);
```
## Step 3: Handle connection issues
Event subscriptions rely on persistent WebSocket connections. Implement reconnection logic for production dApps.
```typescript theme={"dark"}
// subscribeWithReconnection.ts
import { ethers } from "ethers";
import { STABLE_SYSTEM_ADDRESS, STABLE_SYSTEM_ABI } from "./config";
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
function handleUnbonding(delegator: string, validator: string, amount: bigint) {
console.log("Unbonding completed:", { delegator, validator, amount });
}
function setupEventListener() {
const wsProvider = new ethers.WebSocketProvider("wss://rpc.testnet.stable.xyz");
wsProvider.on("error", (error) => {
console.error("Provider error:", error);
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
setTimeout(() => setupEventListener(), 5000);
}
});
const stableSystem = new ethers.Contract(
STABLE_SYSTEM_ADDRESS,
STABLE_SYSTEM_ABI,
wsProvider
);
stableSystem.on("UnbondingCompleted", handleUnbonding);
}
setupEventListener();
```
## Next recommended
Understand how protocol-level events reach the EVM.
Review the delegation and unbonding flow.
Look up the methods that trigger the events tracked here.
# Get testnet USDT0
Source: https://docs.stable.xyz/en/how-to/use-faucet
How to get testnet USDT0 via the faucet or by bridging from Ethereum Sepolia.
Stable uses USDT0 as the gas token, so you need USDT0 in your wallet to submit transactions. There are two ways to fund a testnet wallet: the faucet for small amounts, or bridging from Ethereum Sepolia for larger amounts.
## Faucet
The faucet is the fastest way to get testnet USDT0 for basic development and testing.
1. Visit [https://faucet.stable.xyz](https://faucet.stable.xyz).
2. Connect your browser wallet or paste a wallet address.
3. Select the button to receive testnet USDT0.
The faucet sends 1 USDT0 per request, which is enough to deploy and interact with several contracts.
### Verify your balance
Confirm the funds arrived:
```bash theme={"dark"}
cast balance YOUR_ADDRESS --rpc-url https://rpc.testnet.stable.xyz
```
You should see a non-zero value. If the balance is still `0`, wait a few seconds and re-run. Stable produces a new block roughly every 0.7 seconds, so funds settle quickly.
## Bridge from Sepolia (larger amounts)
If you need more USDT0 than the faucet provides, you can bridge Test USDT from Ethereum Sepolia to the Stable Testnet.
### 1. Mint Test USDT on Sepolia
Call the `mint` function on the [Ethereum Sepolia Test USDT contract](https://sepolia.etherscan.io/token/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract) to get the desired amount.
### 2. Bridge to Stable Testnet
Send a cross-chain transfer to the LayerZero bridge contract on Ethereum Sepolia to bridge Test USDT to the Stable Testnet.
For the full bridge script and contract addresses, see [Bridge USDT0 to Stable](/en/tutorial/bridge-usdt0).
# Use system modules
Source: https://docs.stable.xyz/en/how-to/use-system-modules
Call Stable's Bank, Distribution, and Staking precompiles from Solidity and ethers.js with minimal ABIs and working examples.
Stable exposes protocol-level settlement logic through **precompiled contracts** at fixed addresses. The precompiles let EVM code call Stable SDK modules (staking, reward distribution, STABLE token operations) without re-implementing them. They're significantly more gas efficient than equivalent Solidity implementations because they run at the protocol level.
This guide shows how to call a precompile from both Solidity and ethers.js, and when to use one over a regular contract.
**Concept**: For what system modules do and why they're precompiles, see [System modules](/en/explanation/system-modules-overview). For per-module method signatures and events, see the [System modules reference](/en/reference/system-modules-api-overview).
## What's exposed
| **Module** | **Precompile address** | **Use for** |
| :----------- | :------------------------------------------- | :----------------------------------------------------------------- |
| Bank | `0x0000000000000000000000000000000000001003` | STABLE token transfers and balance operations |
| Distribution | `0x0000000000000000000000000000000000000801` | Claiming staking rewards, reward queries, commission management |
| Staking | `0x0000000000000000000000000000000000000800` | Delegation, undelegation, redelegation, validator queries |
| StableSystem | `0x0000000000000000000000000000000000009999` | EVM event emission for system transactions (unbonding completions) |
All four are callable from any EVM contract or off-chain client. The addresses are stable and identical on mainnet and testnet.
## When to call a precompile vs a regular contract
* **Use a precompile** when the operation maps to a Stable SDK module: staking, reward distribution, STABLE token ops. Calling the precompile is both cheaper and the only way to trigger protocol-level behavior.
* **Use a regular contract** when the operation is application logic: escrow, pricing, access control. Wrap the precompile call in your own contract if you need custom authorization or validation.
Precompiles are not a replacement for application contracts. They're a stable interface into the underlying protocol.
## Call from Solidity
Declare an interface for the methods you need, then call the precompile as if it were a deployed contract.
```solidity theme={"dark"}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IStaking {
function delegate(
address delegatorAddress,
string calldata validatorAddress,
uint256 amount
) external returns (bool success);
function delegation(
address delegatorAddress,
string calldata validatorAddress
) external view returns (uint256 shares, uint256 balance);
}
contract StakingHelper {
address constant STAKING_PRECOMPILE =
0x0000000000000000000000000000000000000800;
/// @notice Delegate STABLE tokens to a validator from this contract.
function delegateToValidator(
string calldata validatorAddress,
uint256 amount
) external returns (bool) {
return IStaking(STAKING_PRECOMPILE).delegate(
address(this),
validatorAddress,
amount
);
}
/// @notice Read the current delegation of this contract to a validator.
function myDelegation(string calldata validatorAddress)
external
view
returns (uint256 shares, uint256 balance)
{
return IStaking(STAKING_PRECOMPILE).delegation(
address(this),
validatorAddress
);
}
}
```
Compile and deploy with Foundry or Hardhat. The precompile address is burned into the contract at the constant slot, so there's nothing to wire up post-deployment.
```solidity theme={"dark"}
// SAFE: precompile address is fixed on Stable and never changes.
```
## Call from ethers.js
For off-chain clients, declare the same interface as a minimal ABI and instantiate a contract pointed at the precompile address.
```typescript theme={"dark"}
// queryDelegation.ts
import { ethers } from "ethers";
import "dotenv/config";
const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz");
const STAKING_PRECOMPILE = "0x0000000000000000000000000000000000000800";
const staking = new ethers.Contract(
STAKING_PRECOMPILE,
[
"function delegation(address delegator, string validator) view returns (uint256 shares, uint256 balance)",
],
provider
);
const delegator = "0xDelegatorAddress";
const validator = "stablevaloper1..."; // bech32 validator operator address
const [shares, balance] = await staking.delegation(delegator, validator);
console.log("Delegation shares: ", shares.toString());
console.log("Delegation balance:", ethers.formatUnits(balance, 18), "STABLE");
```
```bash theme={"dark"}
npx tsx queryDelegation.ts
```
```text theme={"dark"}
Delegation shares: 1000000000000000000000
Delegation balance: 1000.0 STABLE
```
## Subscribe to system transaction events
Some Stable SDK operations (unbonding completions, for example) don't naturally emit EVM events. Stable closes this gap with **system transactions**: validator-generated transactions that call the `StableSystem` precompile to emit standard EVM events during the next block.
To watch `UnbondingCompleted`, subscribe at the precompile address like any ERC-20 `Transfer` listener.
```typescript theme={"dark"}
// watchUnbonding.ts
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz");
const STABLE_SYSTEM = "0x0000000000000000000000000000000000009999";
const stableSystem = new ethers.Contract(
STABLE_SYSTEM,
[
"event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)",
],
provider
);
stableSystem.on("UnbondingCompleted", (delegator, validator, amount, event) => {
console.log("Unbonding completed for:", delegator);
console.log("Amount:", ethers.formatEther(amount), "STABLE");
console.log("Tx:", event.log.transactionHash);
});
console.log("Listening for UnbondingCompleted events...");
```
```bash theme={"dark"}
npx tsx watchUnbonding.ts
```
```text theme={"dark"}
Listening for UnbondingCompleted events...
Unbonding completed for: 0xabcd...
Amount: 100.0 STABLE
Tx: 0x12ab...
```
For the full system-transaction mechanism and the filter-by-user / historical-query patterns, see [Track unbonding completions](/en/how-to/track-unbonding).
## Per-module references
Each precompile's full method list, events, and authorization rules live in its reference page.
* [Bank precompile](/en/reference/bank-module-api): STABLE token transfers and supply queries.
* [Distribution precompile](/en/reference/distribution-module-api): reward claims and commission.
* [Staking precompile](/en/reference/staking-module-api): delegate, undelegate, redelegate, validator queries.
* [System transactions](/en/reference/system-transactions-api): StableSystem event format and authorization.
## Next recommended
Subscribe to the UnbondingCompleted event emitted via the StableSystem precompile.
Jump to the per-module ABI, method signatures, and event schemas.
Understand why Stable exposes SDK modules through precompiles.
# Verify a smart contract
Source: https://docs.stable.xyz/en/how-to/verify-contract
Verify your Stable contract source on Stablescan with forge verify-contract so users can read and interact with it.
Verification uploads your contract's source code to the block explorer and proves it compiles to the deployed bytecode. Once verified, users can read state, call functions, and audit the source on Stablescan without re-hosting your code. This guide walks through verifying a Foundry-deployed contract on Stable.
## Prerequisites
* A contract already deployed on Stable testnet or mainnet. If you haven't deployed yet, see [Deploy a smart contract](/en/tutorial/smart-contract).
* Foundry installed (`forge` available in your PATH).
* The deployed contract address from your `forge create` output.
## 1. Confirm the deployed address
Make sure you have the `Deployed to` address from your earlier deployment. From the [Deploy a smart contract](/en/tutorial/smart-contract) flow, this was the value printed after `forge create`.
```bash theme={"dark"}
cast code 0xDeployedContractAddress --rpc-url https://rpc.testnet.stable.xyz | head -c 20
```
```text theme={"dark"}
0x6080604052600436...
```
A non-empty bytecode confirms the contract is deployed at that address.
## 2. Run forge verify-contract
Foundry's verification flow submits your source to the Stablescan verifier.
```bash theme={"dark"}
forge verify-contract \
0xDeployedContractAddress \
src/Counter.sol:Counter \
--chain-id 2201 \
--verifier blockscout \
--verifier-url https://testnet.stablescan.xyz/api \
--watch
```
```text theme={"dark"}
Start verifying contract `0xDeployedContractAddress` deployed on 2201
Submitting verification of contract: Counter
Submitted contract for verification:
Response: `OK`
GUID: `abc123...`
URL: https://testnet.stablescan.xyz/address/0xDeployedContractAddress
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
```
`--watch` blocks until verification finishes so you don't have to poll. On mainnet, swap the chain ID to `988` and the verifier URL to `https://stablescan.xyz/api`.
**Constructor arguments**: If your contract takes constructor arguments, add `--constructor-args $(cast abi-encode "constructor(uint256,address)" 42 0xSomeAddress)` to the command. Without this flag, verification fails for any contract with a non-empty constructor.
## 3. Confirm verification on Stablescan
Open the contract page on the explorer.
```text theme={"dark"}
https://testnet.stablescan.xyz/address/0xDeployedContractAddress
```
The **Contract** tab should now show source code, a green "Verified" badge, and the full ABI. Users can read state under **Read Contract** and send transactions under **Write Contract**.
## Troubleshooting
* **"Bytecode does not match"**: your source compiles to different bytecode than what's deployed. Most often caused by mismatched Solidity version or optimizer settings. Pass `--compiler-version` and `--optimizer-runs` explicitly to match your `foundry.toml`.
* **"GUID not found"**: the verifier hasn't registered your submission yet. Re-run with `--watch` or manually check the URL printed in the response.
* **Contract uses libraries**: add `--libraries src/Lib.sol:Lib:0xDeployedLibAddress` for each linked library.
## Next recommended
Subscribe to on-chain events with ethers.js and build a live event stream.
Scaffold a fresh Foundry project and deploy to Stable testnet.
See which `eth_*` methods Stable supports for on-chain interactions.
# Work with USDT0 as gas
Source: https://docs.stable.xyz/en/how-to/work-with-usdt-gas
Port Ethereum transaction construction to Stable: set priority fee to zero, read baseFee in USDT0, and denominate value in USDT0.
On Stable, USDT0 is both the chain's native asset and an ERC-20 token. The gas token is USDT0, not a separate native asset. Standard Ethereum gas estimation works once you adjust three things: `maxPriorityFeePerGas` is always `0`, `baseFee` is denominated in USDT0, and the `value` field in a native transfer carries USDT0 (not ETH).
This guide shows how to construct transactions correctly on Stable and what to change when porting Ethereum code.
## What changes vs. Ethereum
| **Field** | **Ethereum** | **Stable** |
| :-------------------------------- | :----------------- | :------------------- |
| Gas token | ETH | USDT0 |
| `maxPriorityFeePerGas` | Used for ordering | Ignored (set to `0`) |
| `baseFeePerGas` | Denominated in ETH | Denominated in USDT0 |
| `value` (native transfer) | Transfers ETH | Transfers USDT0 |
| EIP-1559 transaction format | Supported | Supported |
| `eth_estimateGas`, `eth_gasPrice` | Supported | Supported |
| `eth_maxPriorityFeePerGas` | Returns a tip | Returns `0` |
Because the transaction format is unchanged, existing ethers.js, viem, Hardhat, and Foundry code runs on Stable without changes. The differences are in how you *compute* gas fields, not how you encode them.
## Construct a transaction
Fetch the base fee, set `maxPriorityFeePerGas` to `0`, and double the base fee as a safety margin.
```typescript theme={"dark"}
// sendNative.ts
import { ethers } from "ethers";
import "dotenv/config";
const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz");
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const block = await provider.getBlock("latest");
const baseFee = block!.baseFeePerGas!;
const maxPriorityFeePerGas = 0n; // always 0 on Stable
const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; // 2x headroom
const tx = await wallet.sendTransaction({
to: "0xRecipientAddress",
value: ethers.parseEther("0.001"), // 0.001 USDT0, 18 decimals
maxFeePerGas,
maxPriorityFeePerGas,
});
const receipt = await tx.wait(1);
console.log("Tx:", receipt!.hash);
console.log("Gas used:", receipt!.gasUsed.toString());
console.log("Effective gas price:", receipt!.gasPrice.toString(), "(USDT0 wei-equivalent)");
```
```bash theme={"dark"}
npx tsx sendNative.ts
```
```text theme={"dark"}
Tx: 0x8f3a...2d41
Gas used: 21000
Effective gas price: 1000000000 (USDT0 wei-equivalent)
```
The effective gas price is a USDT0-denominated value. At `1 gwei`, a 21,000-gas native transfer costs approximately `0.000021` USDT0.
## Estimate gas cost in USDT0
`eth_estimateGas` and `eth_gasPrice` behave identically to Ethereum. The result is already in USDT0 because that is the gas token.
```typescript theme={"dark"}
// estimate.ts
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://rpc.testnet.stable.xyz");
const gasPrice = await provider.send("eth_gasPrice", []);
const gasEstimate = await provider.estimateGas({
to: "0xContractAddress",
data: "0x...",
});
const feeInUSDT0 = BigInt(gasPrice) * gasEstimate;
console.log("Estimated fee:", ethers.formatEther(feeInUSDT0), "USDT0");
```
```bash theme={"dark"}
npx tsx estimate.ts
```
```text theme={"dark"}
Estimated fee: 0.000021 USDT0
```
`eth_maxPriorityFeePerGas` always returns `0` on Stable. If your wallet or SDK adds the RPC-returned priority fee on top of the base fee, it still works, but fee UIs that display a separate tip will show `0` and should be hidden.
## Tooling configuration
* **Hardhat / Foundry**: no special configuration needed. Standard EVM settings work. If your config explicitly sets a priority fee, set it to `0`.
* **Wallets**: hide or disable the priority tip input field. Displaying it is misleading because the value has no effect on ordering or inclusion.
* **Monitoring**: fee analytics dashboards should not chart priority fees. They are always zero on Stable.
## Common mistakes when porting from Ethereum
* **Applying an ETH-denominated tip**: copying a priority-fee constant from Ethereum doesn't produce faster inclusion. Stable orders transactions by base fee only.
* **Treating `value` as ETH**: a native transfer's `value` is USDT0. Don't convert it through ETH/USD prices.
* **Hard-coding a fee cap**: set `maxFeePerGas` from the live `baseFeePerGas` (e.g., `baseFee * 2`) rather than a fixed value, so transactions don't stall when the base fee rises.
## Next recommended
Full base-fee model, EIP-1559 format, and `eth_*` method behavior.
Let an application cover gas via the Gas Waiver.
Balance reconciliation and contract design with USDT0's dual role.
# Zero gas transactions
Source: https://docs.stable.xyz/en/how-to/zero-gas-transactions
Send a USDT0 transfer that costs the user no gas using Stable's Gas Waiver, and verify that the receipt shows a zero fee.
Gas Waiver lets an application cover gas on behalf of a user. The user signs a transaction with `gasPrice = 0`, a governance-registered waiver wraps it, and validators execute the call at zero cost to the user. This guide walks through a qualifying transfer, shows how to verify gas was waived, and explains what the waiver does and doesn't cover.
**Concept**: For the wrapper transaction mechanism, authorization model, and security guarantees, see [Gas waiver](/en/explanation/gas-waiver) and the [Gas waiver protocol reference](/en/reference/gas-waiver-api).
## What you'll build
A two-script flow that submits a USDT0 transfer through the hosted Waiver Server, fetches the receipt, and confirms `gasPrice = 0`.
### Demo
```text theme={"dark"}
step 1. Connect wallet, balance displayed as 0.01 USDT0
step 2. Send transaction via Gas Waiver → [Run]
step 3. Result
tx: 0x8f3a...2d41
Gas fee paid by you: 0.000000 USDT0
Balance after: 0.01 USDT0
```
## When the waiver applies
A transaction qualifies when all of these hold:
* The user signs the inner transaction with `gasPrice = 0`.
* The submitter is a governance-registered waiver address.
* The target `to` address and method selector are on the waiver's `AllowedTarget` policy.
* The wrapper is sent to the marker address `0x000000000000000000000000000000000000f333` with `value = 0` and `gasPrice = 0`.
If any of these fails, validators reject the wrapper without executing the inner call. Contract calls not listed in `AllowedTarget` are not covered. Arbitrary self-serve waivers are not possible; every waiver must be registered through validator governance.
## Prerequisites
* An API key for the Waiver Server, issued by the Stable team.
* The target contract address and method selector registered on the waiver's `AllowedTarget` policy.
* A user wallet on testnet with no USDT0 required for gas.
## Step 1: sign a qualifying InnerTx
The user signs a standard transaction with `gasPrice = 0`. In this example the call is a USDT0 `transfer`, which is a common `AllowedTarget` for application-covered gas flows.
```typescript theme={"dark"}
// config.ts
import { ethers } from "ethers";
import "dotenv/config";
export const CONFIG = {
RPC_URL: "https://rpc.testnet.stable.xyz",
CHAIN_ID: 2201, // 988 for mainnet
WAIVER_SERVER: "https://waiver.testnet.stable.xyz",
USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
};
export const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL);
export const userWallet = new ethers.Wallet(process.env.USER_PRIVATE_KEY!, provider);
```
```typescript theme={"dark"}
// signInner.ts
import { ethers } from "ethers";
import { CONFIG, provider, userWallet } from "./config";
const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [
"function transfer(address to, uint256 amount) returns (bool)"
], provider);
const callData = usdt0.interface.encodeFunctionData("transfer", [
"0xRecipientAddress",
ethers.parseUnits("0.001", 18),
]);
const gasLimit = await provider.estimateGas({
from: userWallet.address,
to: CONFIG.USDT0_ADDRESS,
data: callData,
});
const nonce = await provider.getTransactionCount(userWallet.address);
const innerTx = {
to: CONFIG.USDT0_ADDRESS,
data: callData,
value: 0,
gasPrice: 0,
gasLimit,
nonce,
chainId: CONFIG.CHAIN_ID,
};
export const signedInnerTx = await userWallet.signTransaction(innerTx);
console.log("Signed InnerTx:", signedInnerTx);
```
```bash theme={"dark"}
npx tsx signInner.ts
```
```text theme={"dark"}
Signed InnerTx: 0xf8a8...c1
```
`gasPrice` must be `0`. A non-zero value causes the waiver server to reject the submission and validators to reject the wrapper.
## Step 2: submit through the Waiver Server
The Waiver Server wraps the signed inner transaction and broadcasts it. You need a server-issued API key.
```typescript theme={"dark"}
// submit.ts
import { CONFIG } from "./config";
import { signedInnerTx } from "./signInner";
const response = await fetch(`${CONFIG.WAIVER_SERVER}/v1/submit`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.WAIVER_API_KEY}`,
},
body: JSON.stringify({ transactions: [signedInnerTx] }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let txHash = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).trim().split("\n")) {
const result = JSON.parse(line);
if (result.success) {
txHash = result.txHash;
console.log(`tx confirmed: ${txHash}`);
} else {
console.error(`tx failed: ${result.error.message}`);
}
}
}
export { txHash };
```
```bash theme={"dark"}
npx tsx submit.ts
```
```text theme={"dark"}
tx confirmed: 0x8f3a...2d41
```
## Step 3: verify the receipt shows zero gas
Fetch the receipt and confirm `effectiveGasPrice` is 0. That is the cryptographic proof that the user paid no gas.
```typescript theme={"dark"}
// verify.ts
import { provider } from "./config";
import { txHash } from "./submit";
const receipt = await provider.getTransactionReceipt(txHash);
const gasUsed = receipt!.gasUsed;
const effectiveGasPrice = receipt!.gasPrice;
const totalFee = gasUsed * effectiveGasPrice;
console.log("Gas used: ", gasUsed.toString());
console.log("Effective gas price:", effectiveGasPrice.toString());
console.log("Gas fee paid: ", `${totalFee.toString()} USDT0 (wei-equivalent)`);
```
```bash theme={"dark"}
npx tsx verify.ts
```
```text theme={"dark"}
Gas used: 21000
Effective gas price: 0
Gas fee paid: 0 USDT0 (wei-equivalent)
```
An `effectiveGasPrice` of `0` confirms the transaction executed under a registered waiver and the user was not charged.
## What Gas Waiver doesn't cover
* **Contracts outside `AllowedTarget`**: arbitrary contract calls aren't covered. Every target is scoped per waiver through governance.
* **User-submitted wrappers**: if the user submits directly to `0x...f333`, it fails. Only registered waiver addresses can wrap.
* **Fee extraction**: validators don't accept a non-zero `gasPrice` on either the inner or wrapper transaction.
For the full policy model and per-waiver scope rules, see [Gas waiver protocol](/en/reference/gas-waiver-api).
## Next recommended
Full API reference, batch submissions, error codes, and NDJSON streaming.
Register your own waiver address and broadcast wrappers without the hosted API.
Read the full spec: marker routing, wrapper format, governance controls.
# Facilitators
Source: https://docs.stable.xyz/en/reference/agentic-facilitators
x402 payment facilitators that settle agentic payments on Stable.
Agentic payments on Stable allow AI agents and autonomous systems to pay for services without human intervention. Facilitators settle x402 payments on-chain so that developers do not need to run their own settlement infrastructure.
## Overview table
| **Provider** | **Category** | **Docs / Get Started** | **Notes** |
| :---------------------------------------------- | :--------------- | :--------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------- |
| [**Semantic Pay**](https://x402.semanticpay.io) | x402 facilitator | [https://docs.semanticpay.io/supported-chains#stable](https://docs.semanticpay.io/supported-chains#stable) | Public x402 facilitator for Stable; verifies and settles USDT0 payments via ERC-3009 |
## Semantic Pay
Payment infrastructure for AI agents: trustless, P2P, and permissionless. Semantic Pay operates as an x402-compatible payment facilitator on Stable, settling USDT0 payments via ERC-3009 (`transferWithAuthorization`). Agents do not need gas tokens in their wallets.
Developers integrating x402 on Stable point their middleware to `https://x402.semanticpay.io`. No custom settlement infrastructure is required.
**Capabilities**
* Payment payload verification (`/verify`) and on-chain settlement (`/settle`) via USDT0
* Gasless transfers using ERC-3009 `transferWithAuthorization`
* Spend limits, approval flows, and kill switches for agent oversight
* End-to-end traceability with full audit logging from intent through settlement
* Event callbacks for real-time payment lifecycle updates
**Facilitator endpoint:** `https://x402.semanticpay.io`
**Docs:** [https://docs.semanticpay.io/supported-chains#stable](https://docs.semanticpay.io/supported-chains#stable)
***
Have an agentic payments integration with Stable? Reach out at [bizdev@stable.xyz](mailto:bizdev@stable.xyz).
# Wallets
Source: https://docs.stable.xyz/en/reference/agentic-wallets
Wallet infrastructure that enables AI agents to hold funds and sign x402 payments on Stable.
Agent wallets give AI agents and autonomous systems self-custodial signing so they can participate in x402 payment flows without human-driven setup.
## Overview table
| **Provider** | **Category** | **Docs / Get Started** | **Notes** |
| :---------------------------------------------------------------- | :--------------- | :------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- |
| [**Wallet Development Kit (WDK)**](https://docs.wallet.tether.io) | Agent wallet SDK | [https://docs.wallet.tether.io](https://docs.wallet.tether.io) | Tether's open-source SDK; `WalletAccountEvm` satisfies the x402 client signer interface natively |
## Wallet Development Kit (WDK) by Tether
An open-source SDK from Tether for building self-custodial AI agent wallets. WDK enables agents to generate and store private keys locally, without relying on cloud-based KMS or TEE infrastructure.
The `WalletAccountEvm` instance from WDK natively satisfies the client signer interface required by the x402 SDK. An agent equipped with WDK and USDT0 on Stable can automatically intercept 402 HTTP responses, sign ERC-3009 authorizations, and resubmit requests.
**Packages:** `@tetherto/wdk`, `@tetherto/wdk-wallet-evm`
**Capabilities**
* Self-custodial key generation and local storage
* Native x402 client signer compatibility via `WalletAccountEvm`
* Automatic 402 response interception and ERC-3009 signing
* Multi-chain support including Stable
**Docs:** [https://docs.wallet.tether.io](https://docs.wallet.tether.io)
***
Have an agent wallet integration with Stable? Reach out at [bizdev@stable.xyz](mailto:bizdev@stable.xyz).
# Bank precompile reference
Source: https://docs.stable.xyz/en/reference/bank-module-api
Bank precompile reference: ERC-20 methods, mint/burn authorization, and event signatures.
**Concept:** For what the bank module does and when to use it, see [Bank module](/en/explanation/bank-module).
## Abstract
The `x/bank` module in Stable SDK only provides basic token management features.
You can transfer any token to any account without restriction, but you cannot delegate another account to transfer your tokens.
For these reasons, the `bank` precompiled contract offers additional authorization and delegation features on top of the existing `x/bank` module in Stable SDK.
## Contents
1. **[Concepts](#concepts)**
2. **[Configuration](#configuration)**
3. **[Methods](#methods)**
4. **[Events](#events)**
## Concepts
This precompiled contract provides ERC-20 standard methods - such as `transfer` and `balanceOf` for transfer and `transferFrom`, `approve` and `allowance` for delegation. You can call these methods directly without registering the contract address.
However, the `x/precompile` module must whitelist and register the contract address before you can use the `mint` and `burn` methods.
```go theme={"dark"}
func (p *Precompile) mint(
ctx sdk.Context,
contract *vm.Contract,
denom string,
method *abi.Method,
stateDB vm.StateDB,
args []interface{},
) ([]byte, error) {
// ...
// mint method is only allowed for the registered caller contract
if _, err := precompilecommon.CheckPermissions(ctx, p.precompileKeeper, contract.CallerAddress, CallerPermissions); err != nil {
return nil, err
}
```
This additional verification process guarantees that the token contract calling this precompiled contract is authorized.
To register a token contract address and its denom in the `x/precompile` module whitelist, you must submit a governance proposal.
## Configuration
The contract address and gas cost are predefined.
### Contract address
* `0x0000000000000000000000000000000000001003` for STABLE (governance token)
## Methods
### `mint`
Mints requested amount of new tokens and transfer to the account.
The amount of tokens to be minted must be greater than zero.
`PrecompiledBankMint` is emitted when the tokens are successfully minted and transferred to the account.
NOTE:
* Governance token minting is prohibited.
* Caller contracts calling the mint method must be registered in x/precompile module.
#### Inputs
| Name | Type | Description |
| ------ | ------- | ---------------------------------------- |
| to | address | the address to receive the minted tokens |
| amount | uint256 | the amount of tokens to be minted |
#### Outputs
| Name | Type | Description |
| ------- | ---- | ------------------------------------------------------------------------- |
| success | bool | true if the tokens are successfully minted and transferred to the account |
### `burn`
Burns requested amount of tokens from the account.
The amount of tokens to be burned must be greater than zero.
`PrecompiledBankBurn` is emitted when the tokens are successfully burned.
NOTE:
* Burning governance token is prohibited.
* Caller contracts calling the burn method must be registered in x/precompile module.
#### Inputs
| Name | Type | Description |
| ------ | ------- | --------------------------------- |
| from | address | the address to burn the tokens |
| amount | uint256 | the amount of tokens to be burned |
#### Outputs
| Name | Type | Description |
| ------- | ---- | ------------------------------------------ |
| success | bool | true if the tokens are successfully burned |
### `transfer`
Transfers requested amount of tokens from sender to the recipient.
Token must be set sendable. The amount of tokens to be transferred must be greater than zero.
`PrecompiledBankTransfer` is emitted when the tokens are successfully transferred.
#### Inputs
| Name | Type | Description |
| ------ | ------- | -------------------------------------- |
| to | address | the address to receive the tokens |
| amount | uint256 | the amount of tokens to be transferred |
#### Outputs
| Name | Type | Description |
| ------- | ---- | ----------------------------------------------- |
| success | bool | true if the tokens are successfully transferred |
### `transferFrom`
Transfers requested amount of tokens from owner to recipient by authorized spender within the limits of the allowance.
Token must be set sendable.
The amount of tokens to be transferred must be greater than zero and less than or equal to the current allowance.
`PrecompiledBankTransfer` is emitted when the tokens are successfully transferred.
#### Inputs
| Name | Type | Description |
| ------ | ------- | -------------------------------------- |
| from | address | the address to transfer the tokens |
| to | address | the address to receive the tokens |
| amount | uint256 | the amount of tokens to be transferred |
#### Outputs
| Name | Type | Description |
| ------- | ---- | ----------------------------------------------- |
| success | bool | true if the tokens are successfully transferred |
### `multiTransfer`
Transfers tokens from single account to multiple accounts.
Token must be set sendable.
The amount of tokens to be transferred to each recipient must be greater than zero.
`PrecompiledBankTransfer` is emitted per each recipient when the tokens are successfully transferred.
#### Inputs
| Name | Type | Description |
| ------ | ---------- | -------------------------------------------------------- |
| to | address\[] | the addresses to receive the transferred tokens |
| amount | uint256\[] | the amount of tokens to be transferred to each recipient |
#### Outputs
| Name | Type | Description |
| ------- | ---- | ----------------------------------------------------------------- |
| success | bool | true if the tokens are successfully transferred to each recipient |
### `approve`
Authorizes a spender to transfer tokens from the owner’s account.
The amount of tokens to be authorized must be greater than zero.
`PrecompiledBankApproval` is emitted when the authorization is successfully set.
#### Inputs
| Name | Type | Description |
| ------- | ------- | ------------------------------------- |
| spender | address | the address to authorize |
| value | uint256 | the amount of tokens to be authorized |
#### Outputs
| Name | Type | Description |
| ------- | ---- | --------------------------------------------- |
| success | bool | true if the authorization is successfully set |
### `revoke`
Revokes the authorization of spender for transferring tokens from owner.
`PrecompiledBankRevoke` is emitted when the authorization is successfully revoked.
#### Inputs
| Name | Type | Description |
| ------- | ------- | --------------------- |
| spender | address | the address to revoke |
#### Outputs
| Name | Type | Description |
| ------- | ---- | ------------------------------------------------- |
| success | bool | true if the authorization is successfully revoked |
### `balanceOf`
Returns balance of tokens from the account.
#### Inputs
| Name | Type | Description |
| ------- | ------- | ---------------------------------------- |
| account | address | the address to get the balance of tokens |
#### Outputs
| Name | Type | Description |
| ------- | ------- | ----------------------------------- |
| balance | uint256 | the amount of tokens in the account |
### `totalSupply`
Returns total supply of tokens.
#### Inputs
none
#### Outputs
| Name | Type | Description |
| ----------- | ------- | -------------------------- |
| totalSupply | uint256 | the total amount of tokens |
### `allowance`
Returns the amount which spender is still allowed to withdraw from owner.
#### Inputs
| Name | Type | Description |
| ------- | ------- | -------------------------- |
| owner | address | the address of the owner |
| spender | address | the address of the spender |
#### Outputs
| Name | Type | Description |
| ------ | ------- | ------------------------------- |
| amount | uint256 | the amount of tokens authorized |
## Events
All events emitted from this precompiled contract are prefixed with `PrecompiledBank`.
To avoid ambiguity, token contract calling this precompiled contract should avoid using event names with the same prefix.
### PrecompiledBankMint
| Name | Type | Indexed | Description |
| ------ | ------- | ------- | ---------------------------------------- |
| from | address | Y | the address that minted the tokens |
| to | address | Y | the address to receive the minted tokens |
| amount | uint256 | N | the amount of tokens minted |
### PrecompiledBankBurn
| Name | Type | Indexed | Description |
| ------ | ------- | ------- | ---------------------------------- |
| from | address | Y | the address that burned the tokens |
| to | address | Y | not used in this method |
| amount | uint256 | N | the amount of tokens burned |
### PrecompiledBankTransfer
| Name | Type | Indexed | Description |
| ------ | ------- | ------- | --------------------------------------------- |
| from | address | Y | the address that transferred the tokens |
| to | address | Y | the address to receive the transferred tokens |
| amount | uint256 | N | the amount of tokens transferred |
### PrecompiledBankApproval
| Name | Type | Indexed | Description |
| ------- | ------- | ------- | -------------------------------------- |
| owner | address | Y | the address that authorized the tokens |
| spender | address | Y | the address to authorize |
| value | uint256 | N | the amount of tokens authorized |
### PrecompiledBankRevoke
| Name | Type | Indexed | Description |
| ------- | ------- | ------- | ----------------------------------- |
| owner | address | Y | the address that revoked the tokens |
| spender | address | Y | the address to revoke |
| value | uint256 | N | the amount of tokens authorized |
# Bridges
Source: https://docs.stable.xyz/en/reference/bridges
Bridge providers, contract addresses, and fee structures for USDT0 transfers to and from Stable.
Bridge providers supporting USDT0 transfers to and from Stable. For how cross-chain USDT0 movement works, see [Bridging to Stable](/en/explanation/usdt0-bridging). For a hands-on walkthrough, see the [Bridge USDT0 to Stable Testnet](/en/infrastructure-and-ecosystem/infrastructure-and-tooling/bridge-tutorial) tutorial.
***
## Supported source chains
Any chain with USDT0 can bridge to Stable via the OFT Mesh. Any chain with native USDT can route through the Legacy Mesh via the Arbitrum hub. Current participants:
| Path | Example chains | Mechanism | Fee |
| :-------------- | :------------------------------------- | :----------------------------------------------------- | :----------------------- |
| **OFT Mesh** | Arbitrum, Ethereum, Ink, Bera, MegaETH | Burn on source, mint on Stable | Source chain gas only |
| **Legacy Mesh** | Tron, TON | Lock native USDT → Arbitrum hub → mint USDT0 on Stable | 0.03% + source chain gas |
Ethereum and Arbitrum support both paths: users holding native USDT can use the Legacy Mesh, while users holding USDT0 can use the OFT Mesh directly.
***
## Contract addresses
| | Testnet (chain ID 2201) | Mainnet (chain ID 988) |
| :-------------------------------- | :------------------------------------------- | :----------------------------------------------------------------------------------------------------------------- |
| **LayerZero EID** | `40374` | See [LayerZero deployed contracts](https://docs.layerzero.network/v2/deployments/deployed-contracts?chains=stable) |
| **LayerZero Endpoint V2** | `0x3aCAAf60502791D199a5a5F0B173D78229eBFe32` | See LayerZero docs |
| **USDT0 proxy (on Stable)** | `0x78Cf24370174180738C5B8E352B6D14c83a6c9A9` | `0x779Ded0c9e1022225f8E0630b35a9b54bE713736` |
| **Source USDT0 (Sepolia)** | `0xc4DCC311c028e341fd8602D8eB89c5de94625927` | Use mainnet USDT0 on source chain |
| **Source OApp (Sepolia)** | `0xc099cD946d5efCC35A99D64E808c1430cEf08126` | Use mainnet OApp on source chain |
| **LiFi Diamond (Stable mainnet)** | N/A | `0x026F252016A7C47CDEf1F05a3Fc9E20C92a49C37` |
For the full testnet contract list (LayerZero endpoint, DVN, executor), see [Testnet ecosystem contracts](/en/reference/testnet-ecosystem).
***
## Bridge providers
| Provider | Type | Status | Description | Docs |
| :------------------------------------------------------------------ | :--------------------------------- | :---------------------- | :----------------------------------------------------------- | :--------------------------------------------------------------------------- |
| **[LayerZero](https://docs.layerzero.network/v2)** | Cross-chain messaging (OFT) | Live | Powers USDT0 OFT burn/mint transfers; dual DVN verification | [docs.layerzero.network/v2](https://docs.layerzero.network/v2) |
| **[Stargate](https://docs.stargate.finance/introduction/overview)** | Direct bridge (liquidity pools) | Live | Unified liquidity pools; stablecoin-optimized routing | [docs.stargate.finance](https://docs.stargate.finance/introduction/overview) |
| **[Gas.Zip](https://dev.gas.zip/overview)** | Direct bridge (liquidity routing) | Live | Liquidity routing across 350+ chains; fast finality | [dev.gas.zip](https://dev.gas.zip/overview) |
| **[LiFi](https://docs.li.fi/api-reference/introduction)** | Bridge aggregator | Live | Routes across multiple bridges and DEX swaps; SDK + REST API | [docs.li.fi](https://docs.li.fi/api-reference/introduction) |
| **[Polymer](https://docs.polymerlabs.org/docs/build/start/)** | Cross-chain interoperability (IBC) | Integration in progress | IBC-based messaging for Ethereum-native chains | [docs.polymerlabs.org](https://docs.polymerlabs.org/docs/build/start/) |
| **[Relay](https://docs.relay.link/what-is-relay)** | Intent-based bridge | Integration in progress | Gasless execution via solver network | [docs.relay.link](https://docs.relay.link/what-is-relay) |
### LayerZero
Cross-chain messaging protocol powering USDT0 OFT burn/mint transfers with dual DVN verification.
**Capabilities**
* OFT-standard burn-on-source, mint-on-destination transfers
* Dual DVN (Decentralized Verifier Network) message verification
* Powers both OFT Mesh and Legacy Mesh paths
### Stargate
Liquidity pool-based bridge optimized for stablecoin routing.
**Capabilities**
* Unified liquidity pools across chains
* Stablecoin-optimized routing
* Instant guaranteed finality
### Gas.Zip
Liquidity routing protocol supporting fast transfers across 350+ chains.
**Capabilities**
* Cross-chain liquidity routing
* Fast finality
* Broad chain coverage (350+ chains)
### LiFi
Bridge aggregator routing transfers across multiple bridges and DEX swaps.
**Capabilities**
* Multi-bridge route optimization
* SDK and REST API integration
* DEX swap aggregation
### Polymer
IBC-based cross-chain messaging for Ethereum-native chains. Integration in progress.
**Capabilities**
* IBC protocol messaging on Ethereum
* Native interoperability without external validators
### Relay
Intent-based bridge with gasless execution via a solver network. Integration in progress.
**Capabilities**
* Intent-based bridging
* Gasless execution for users
* Solver network settlement
***
## Fee structure
| Provider | Fee model |
| :-------------------------- | :-------------------------------------------------------------------------------------------------- |
| **LayerZero (OFT Mesh)** | Source chain gas only (no protocol fee) |
| **LayerZero (Legacy Mesh)** | 0.03% of transferred amount (charged by USDT0 team) + source chain gas |
| **Stargate** | Liquidity pool fees apply; see [Stargate docs](https://docs.stargate.finance/introduction/overview) |
| **LiFi** | Aggregator routing fee may apply depending on path |
| **Gas.Zip** | See [Gas.Zip docs](https://dev.gas.zip/overview) for current fee schedule |
| **Relay** | Solver fees; see [Relay docs](https://docs.relay.link/what-is-relay) |
***
Have a bridge integrating Stable? Reach out at [bizdev@stable.xyz](mailto:bizdev@stable.xyz).
# Connect
Source: https://docs.stable.xyz/en/reference/connect
Mainnet and testnet chain IDs, RPC endpoints, block explorers, and faucet for Stable.
This page consolidates the network details you need to connect to Stable.
## Mainnet
| **Field** | **Value** |
| :-------------- | :----------------------------------------------- |
| Network Name | Stable Mainnet |
| Chain ID | `988` |
| Currency Symbol | USDT0 |
| EVM JSON-RPC | `https://rpc.stable.xyz` |
| WebSocket | `wss://rpc.stable.xyz` |
| Block Explorer | [https://stablescan.xyz](https://stablescan.xyz) |
## Testnet
| **Field** | **Value** |
| :-------------- | :--------------------------------------------------------------- |
| Network Name | Stable Testnet |
| Chain ID | `2201` |
| Currency Symbol | USDT0 |
| EVM JSON-RPC | `https://rpc.testnet.stable.xyz` |
| WebSocket | `wss://rpc.testnet.stable.xyz` |
| Block Explorer | [https://testnet.stablescan.xyz](https://testnet.stablescan.xyz) |
For third-party RPC providers, see [RPC Providers](/en/reference/rpc-providers).
USDT0 uses **18 decimals** as the native gas token (returned by `address(x).balance`) and **6 decimals** as an ERC-20 token (returned by `USDT0.balanceOf(x)`). Both interfaces operate on the same underlying balance. Libraries like viem and ethers.js report 18 decimals because they read the native gas token. For details on how the precision gap is reconciled, see [USDT0 Behavior on Stable](/en/explanation/usdt0-behavior).
## Add Stable to your wallet
To add Stable manually, open your browser wallet's network settings and enter the values from the tables above. The required fields are:
* **Network Name**
* **RPC URL** (the EVM JSON-RPC endpoint)
* **Chain ID**
* **Currency Symbol**: `USDT0`
## Verify connectivity
Confirm your RPC endpoint is reachable by querying the chain ID:
```bash theme={"dark"}
cast chain-id --rpc-url https://rpc.stable.xyz
```
Expected output:
```text theme={"dark"}
988
```
For the testnet:
```bash theme={"dark"}
cast chain-id --rpc-url https://rpc.testnet.stable.xyz
```
Expected output:
```text theme={"dark"}
2201
```
## Next recommended
Send your first testnet transaction in five minutes.
Fund a wallet from the faucet or bridge from Sepolia.
Understand the 18/6-decimal dual role before you code against balances.
# Distribution precompile reference
Source: https://docs.stable.xyz/en/reference/distribution-module-api
Distribution precompile reference: reward withdrawal, withdraw-address management, and query methods.
**Concept:** For what the distribution module does and when to use it, see [Distribution module](/en/explanation/distribution-module).
## Abstract
The `distribution` precompiled contract acts as a bridge that enables EVM environments to use the Stable SDK's `x/distribution` module functionality.
## Contents
1. **[Concepts](#concepts)**
2. **[Configuration](#configuration)**
3. **[Methods](#methods)**
4. **[Events](#events)**
## Concepts
The `distribution` precompiled contract performs additional checks to ensure that the delegator or depositor is the caller.
## Configuration
The contract address and gas cost are predefined.
### Contract address
* `0x0000000000000000000000000000000000000801`
## Methods
### `setWithdrawAddress`
Sets the address to receive the reward for the token delegated by the delegator to the validator.
Sometimes, when the delegator is self-delegated, the validator address is used as the delegator.
`SetWithdrawAddress` is emitted when the withdrawer address is successfully set.
#### Inputs
| Name | Type | Description |
| ----------------- | ------- | ----------------------------------------------- |
| delegatorAddress | address | the address of the delegator |
| withdrawerAddress | address | the address to receive the reward of delegation |
#### Outputs
| Name | Type | Description |
| ------- | ---- | -------------------------------------------------- |
| success | bool | true if the withdrawer address is successfully set |
### `withdrawDelegatorRewards`
Withdraws the reward to be received by the delegator from the validator.
All types of tokens that the validator rewards to the delegator are withdrawn in a single transaction.
`WithdrawDelegatorRewards` is emitted when the reward is successfully withdrawn.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------- |
| delegatorAddress | address | the address of the delegator |
| validatorAddress | address | the address of the validator |
#### Outputs
| Name | Type | Description |
| ------ | ------- | --------------------------------------------------------- |
| amount | Coin\[] | rewards of various tokens to be received by the delegator |
`Coin` is a struct with the following fields:
| Name | Type | Description |
| ------ | ------- | ------------------------ |
| denom | string | the denom of the reward |
| amount | uint256 | the amount of the reward |
### `withdrawValidatorCommission`
Withdraws the commission of the validator.
All types of tokens that the validator receives as commission are withdrawn in a single transaction.
`WithdrawValidatorCommission` is emitted when the commission is successfully withdrawn.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------- |
| validatorAddress | address | the address of the validator |
#### Outputs
| Name | Type | Description |
| ------ | ------- | ------------------------------------------------------------- |
| amount | Coin\[] | commissions of various tokens to be received by the validator |
### `validatorDistributionInfo`
Returns the distribution information representing the reward the validator will receive. A validator can delegate tokens to itself at its own address to act as a delegator, called self-bonded.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------- |
| validatorAddress | address | the address of the validator |
#### Outputs
| Name | Type | Description |
| ---------------- | ------------------------- | ----------------------------------------- |
| distributionInfo | ValidatorDistributionInfo | distribution information of the validator |
`ValidatorDistributionInfo` is a struct with the following fields:
| Name | Type | Description |
| --------------- | ---------- | -------------------------------------------- |
| operatorAddress | address | the address of the operator of the validator |
| selfBondRewards | DecCoin\[] | the self-bonded amount of the validator |
| commission | DecCoin\[] | the commission of the validator |
`DecCoin` is a struct with the following fields:
| Name | Type | Description |
| --------- | ------- | --------------------------- |
| denom | string | the denom of the reward |
| amount | uint256 | the amount of the reward |
| precision | uint8 | the precision of the reward |
### `validatorOutstandingRewards`
Returns the outstanding rewards of the validator. Outstanding rewards represent the total reward pool: the validator's commission and self-bonded rewards, plus the total rewards owed to delegators. For example, if validator A has delegators B, C, and D, the outstanding rewards equal A's commission and self-bonded rewards, plus the rewards of B, C, and D.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------- |
| validatorAddress | address | the address of the validator |
#### Outputs
| Name | Type | Description |
| ------- | ---------- | ------------------------------------ |
| rewards | DecCoin\[] | outstanding rewards of the validator |
### `validatorCommission`
Returns the commission of the validator. This method is used to retrieve the commission of the validator before calling `withdrawValidatorCommission` method.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------- |
| validatorAddress | address | the address of the validator |
#### Outputs
| Name | Type | Description |
| ---------- | ---------- | --------------------------- |
| commission | DecCoin\[] | commission of the validator |
### `validatorSlashes`
Returns the history of slashes of the validator between the starting height and ending height. Slashing is the fines imposed when a validator behaves maliciously or violates network rules such as double signing, misbehavior, or not following the chain rules.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------- |
| validatorAddress | address | the address of the validator |
| startingHeight | uint64 | the starting height |
| endingHeight | uint64 | the ending height |
| pageRequest | PageReq | the pagination request |
`PageReq` is a struct with the following fields:
| Name | Type | Description |
| ---------- | ------ | ------------------------------------------ |
| key | bytes | the key of the pagination |
| offset | uint64 | the offset of the pagination |
| limit | uint64 | the limit of the pagination |
| countTotal | bool | whether to count the total number of pages |
| reverse | bool | whether to reverse the pagination |
#### Outputs
| Name | Type | Description |
| ---------- | ---------------------- | ------------------------ |
| slashes | ValidatorSlashEvent\[] | slashes of the validator |
| pagination | PageResp | the pagination response |
`ValidatorSlashEvent` is a struct with the following fields:
| Name | Type | Description |
| --------------- | ------ | --------------------------- |
| validatorPeriod | uint64 | the period of the validator |
| fraction | Dec | the fraction of the slash |
`Dec` is a struct with the following fields:
| Name | Type | Description |
| --------- | ------ | ------------------------ |
| value | uint64 | the value of the Dec |
| precision | uint8 | the precision of the Dec |
`PageResp` is a struct with the following fields:
| Name | Type | Description |
| ------- | ------ | ------------------------------ |
| nextKey | bytes | the next key of the pagination |
| total | uint64 | the total number of pages |
### `delegationRewards`
Returns the rewards that delegator receives from the validator.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | -------------------------------- |
| delegatorAddress | address | the hex address of the delegator |
| validatorAddress | address | the address of the validator |
#### Outputs
| Name | Type | Description |
| ------- | ---------- | -------------------------------------------------- |
| rewards | DecCoin\[] | rewards that delegator receives from the validator |
### `delegationTotalRewards`
Returns the total rewards that delegator receives from all validators.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | -------------------------------- |
| delegatorAddress | address | the hex address of the delegator |
#### Outputs
| Name | Type | Description |
| ------- | ---------------------------- | --------------------------------------------------------- |
| rewards | DelegationDelegatorReward\[] | total rewards that delegator receives from all validators |
| total | DecCoin\[] | the total amount of the rewards |
`DelegationDelegatorReward` is a struct with the following fields:
| Name | Type | Description |
| ---------------- | ---------- | -------------------------------------------------- |
| validatorAddress | address | the address of the validator |
| reward | DecCoin\[] | rewards that delegator receives from the validator |
### `delegatorValidators`
Returns the validators that delegator is bonded to.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | -------------------------------- |
| delegatorAddress | address | the hex address of the delegator |
#### Outputs
| Name | Type | Description |
| ---------- | --------- | -------------------------------------- |
| validators | string\[] | validators that delegator is bonded to |
### `delegatorWithdrawAddress`
Returns the address to receive the reward of delegation set by `setWithdrawAddress` method.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | -------------------------------- |
| delegatorAddress | address | the hex address of the delegator |
#### Outputs
| Name | Type | Description |
| --------------- | ------- | ----------------------------------------------- |
| withdrawAddress | address | the address to receive the reward of delegation |
## Events
### SetWithdrawAddress
| Name | Type | Indexed | Description |
| --------------- | ------- | ------- | ----------------------------------------------- |
| caller | address | Y | the address of the caller (delegator) |
| withdrawAddress | address | N | the address to receive the reward of delegation |
### WithdrawDelegatorRewards
| Name | Type | Indexed | Description |
| ---------------- | ------- | ------- | ---------------------------- |
| delegatorAddress | address | Y | the address of the delegator |
| validatorAddress | address | Y | the address of the validator |
| amount | uint256 | N | the amount of the reward |
### WithdrawValidatorCommission
| Name | Type | Indexed | Description |
| ---------------- | ------- | ------- | ---------------------------------- |
| validatorAddress | address | Y | the address of the validator |
| commission | uint256 | N | the total amount of the commission |
# EIP-7702
Source: https://docs.stable.xyz/en/reference/eip-7702-api
EIP-7702 support on Stable. EOAs can set their account code to an existing smart contract, enabling batch payments, spending limits, and session keys.
Stable supports **EIP-7702**, which allows an EOA to set its account code to an existing smart contract. EOAs keep their original address and private key while executing the delegate's logic.
**Concept:** For what EIP-7702 enables on Stable, the delegation model, and security considerations, see [EIP-7702](/en/explanation/eip-7702). For the full specification, see the [EIP-7702 spec](https://eips.ethereum.org/EIPS/eip-7702).
## Transaction format
EIP-7702 uses transaction type `0x04` with an `authorizationList` field. Each authorization designates a delegate contract whose code the EOA executes for that transaction.
```typescript theme={"dark"}
{
type: 4,
to: eoa.address,
data: delegateCallData,
authorizationList: [signedAuthorization],
maxPriorityFeePerGas: 0n, // always 0 on Stable
// ... standard EIP-1559 fields
}
```
The authorization carries:
* `chainId`: must match the target chain.
* `address`: the delegate contract address.
* `nonce`: the authorization nonce (separate from the transaction nonce).
Wallets and libraries that support EIP-7702 handle the authorization format automatically.
## Tooling
* **ethers.js**: `wallet.signAuthorization({ chainId, address, nonce })` produces the signed authorization for inclusion in the `authorizationList`.
* **viem**: use `signAuthorization` with a walletClient, then pass the result to `sendTransaction`.
* **Hardhat / Foundry**: standard EIP-7702 transaction format works when your toolchain version supports the Pectra hardfork.
## Next recommended
Understand the delegation model and when to use it.
Implement batch payments, spending limits, and session keys step by step.
# FAQ
Source: https://docs.stable.xyz/en/reference/faq
Get quick answers to common onboarding questions: what Stable is, how to start, what differs from Ethereum, and where to find deeper docs.
## Getting started
**What is Stable?**
A Layer 1 where USDT0 is the native gas token and settlement asset. Standard EVM tooling works unchanged.
**Who is Stable for?**
Three builder profiles: payment and wallet teams moving USDT0, smart contract developers deploying to an EVM, and infrastructure teams running nodes or RPC. The [Learn overview](/en/explanation/learn-overview) has a card per path.
**Where should I start: Payments, Contracts, AI/Agents, or Infrastructure?**
* Moving USDT0 or building payment flows → [Payments](/en/explanation/payments-overview).
* Deploying contracts → [Contracts](/en/explanation/contracts-overview).
* Wiring AI editors or building agent-paid services → [AI/Agents](/en/explanation/ai-agents-overview).
* Running nodes or covering gas for users → [Infrastructure](/en/explanation/infrastructure-overview).
If you haven't connected to testnet yet, start with [Quick start](/en/tutorial/quick-start).
## Technical
**Is Stable EVM-compatible?**
Yes. Solidity, Vyper, Foundry, Hardhat, ethers, viem, and the `eth_*` JSON-RPC methods all work unchanged. Four behaviors differ from Ethereum — see [Differences from Ethereum](/en/explanation/ethereum-comparison).
**Why is USDT0 the gas token?**
So you pay fees in the asset you're already transacting in. No second token to fund, and fees stay denominated in a stablecoin. The protocol also optimizes USDT0-heavy workloads through guaranteed blockspace and transfer aggregation; see [Core concepts](/en/explanation/core-concepts).
**How do I get USDT0?**
* **Testnet:** use the faucet at [faucet.stable.xyz](https://faucet.stable.xyz), or bridge Test USDT from Ethereum Sepolia. Walkthrough in [Get testnet USDT0](/en/how-to/use-faucet).
* **Mainnet:** bridge USDT0 from another chain via LayerZero, or acquire through an exchange or custodian.
**What changes when I port a contract from Ethereum?**
Most contracts deploy unchanged. Three things to fix if they apply:
* Don't mirror native balance in internal variables. `transferFrom` can drain native without calling the contract.
* Don't transfer to `address(0)`. Both native and ERC-20 transfers to zero revert.
* Don't rely on `EXTCODEHASH` for address-reuse detection. Permit-based approvals change native balance without a nonce increment.
Full checklist: [USDT0 behavior on Stable](/en/explanation/usdt0-behavior).
## Resources
**Where do I find tokenomics, roadmap, and architecture?**
* [Tokenomics](/en/reference/tokenomics): STABLE supply, allocation, and vesting.
* [Technical roadmap](/en/explanation/technical-roadmap): phased optimization plan.
* [Tech overview](/en/explanation/tech-overview): StableBFT, Stable EVM, StableDB, and RPC design.
* [USDT-specific features](/en/explanation/usdt-features-overview): detail on gas, blockspace, aggregation, and confidential transfer.
**Where do I go for help?**
* [Developer assistance](/en/reference/developer-assistance): FAQ and reference pointers.
* [Discord](https://discord.gg/stablexyz): community support and protocol updates.
* `bizdev@stable.xyz`: partnership and integration conversations.
## Next recommended
Send a first transaction on testnet.
Learn the four core concepts you need before you build.
Pick the docs path that fits what you're building.
Validate an integration before shipping to mainnet.
# Gas pricing reference
Source: https://docs.stable.xyz/en/reference/gas-pricing-api
Construct transactions, estimate gas, and configure tooling against Stable's single-component fee model.
Transaction construction, gas estimation, and tooling configuration for Stable.
**Concept:** For why Stable uses a single-component fee model and how it compares to Ethereum, see [Gas pricing](/en/explanation/gas-pricing).
## Transaction construction
When constructing transactions on Stable, set `maxPriorityFeePerGas` to `0`. Clients should fetch the latest base fee from the most recent block and include a safety margin when computing `maxFeePerGas`.
```javascript theme={"dark"}
// ethers.js v6
const block = await provider.getBlock("latest");
const baseFee = block.baseFeePerGas;
const maxPriorityFeePerGas = 0n; // always 0 on Stable
const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas; // double the base fee as safety margin
const tx = await wallet.sendTransaction({
to: "0xRecipientAddress",
value: parseEther("0.01"),
maxFeePerGas,
maxPriorityFeePerGas,
});
```
```text theme={"dark"}
Native USDT0 transfer confirmed. Fee ≈ 0.0000021 USDT0 at baseFee = 1 gwei.
```
## Gas estimation
Use `eth_estimateGas` and `eth_gasPrice` as you would on Ethereum. The key difference is that `eth_maxPriorityFeePerGas` will always return `0`.
```javascript theme={"dark"}
const gasPrice = await provider.send("eth_gasPrice", []);
const gasEstimate = await provider.estimateGas({
to: contractAddress,
data: callData,
});
const estimatedFeeInUSDT0 = gasPrice * gasEstimate;
```
## Tooling configuration
* **Hardhat / Foundry**: no special configuration needed; standard EVM settings work. If your config explicitly sets a priority fee, set it to `0`.
* **Wallets**: hide or disable the priority tip input field. Displaying it may confuse users since the value has no effect.
* **Monitoring**: fee analytics dashboards should not track priority fees. They will always be zero.
## Next recommended
Understand why Stable uses a single-component fee model.
Review every behavior difference you'll hit porting from Ethereum.
Reference the `eth_*` methods Stable exposes.
# Gas waiver protocol
Source: https://docs.stable.xyz/en/reference/gas-waiver-api
Protocol-level specification for Gas Waiver: transaction formats, marker routing, governance controls, and the Waiver Server API.
This document specifies the Gas Waiver mechanism: transaction formats, marker routing, governance controls, and the Waiver Server API.
**Concept:** For what Gas Waiver is and why it exists, see [Gas waiver](/en/explanation/gas-waiver). For the how-to integration guide against the hosted Waiver Server, see [Enable gas-free transactions](/en/how-to/integrate-gas-waiver).
## Abstract
Gas Waiver enables gasless end-user transactions on Stable by allowing a small set of governance-approved addresses (“waivers”) to submit transactions with `gasPrice = 0`. Stable currently operates a waiver service (the “Waiver Server”) that you can integrate with to provide gasless UX without implementing protocol-specific wrapper logic.
## Scope
This specification covers:
* Protocol-level rules for gas-waived transactions
* The wrapper transaction mechanism and marker address
* Governance-controlled authorization and allowed targets
* The Waiver Server interface for submitting signed user transactions
## Definitions
* **Waiver**: An Ethereum address registered on-chain via validator governance that is authorized to submit gas-waived transactions.
* **InnerTx**: The end user’s signed transaction with `gasPrice = 0`.
* **WrapperTx**: A transaction signed by a waiver that transports the user’s `InnerTx` to the chain and authorizes execution.
* **Marker address**: A sentinel address used to identify waiver wrapper transactions: `0x000000000000000000000000000000000000f333`.
* **AllowedTarget**: A policy that limits a waiver to specific contract addresses and method selectors.
## Overview
Gas Waiver uses a wrapper transaction pattern:
1. The user signs an `InnerTx` with `gasPrice = 0`.
2. A waiver wraps the `InnerTx` into a `WrapperTx` and broadcasts it.
3. Validators detect marker transactions, verify the waiver authorization and policy constraints, then execute the embedded `InnerTx`.
Stable operates a waiver service (Waiver Server) that is registered on-chain as an authorized waiver. You integrate with the Waiver Server API to submit signed `InnerTx` payloads.
## Protocol specification
### Marker address routing
A transaction is treated as a waiver wrapper transaction if and only if:
* `to == 0x000000000000000000000000000000000000f333`.
The protocol interprets the transaction `data` field as an encoded inner transaction payload and processes it using the waiver verification rules below.
### Authorization and policy checks
For each candidate wrapper transaction, validators must enforce:
1. **Waiver authorization**
* `WrapperTx.from` must be a waiver address registered on-chain via governance.
2. **Gas waiver**
* `WrapperTx.gasPrice` must equal `0`.
* `InnerTx.gasPrice` must equal `0`.
3. **Target allowlist**
* `InnerTx.to` and the method selector extracted from `InnerTx.data` must be permitted by the waiver’s `AllowedTarget` policy.
4. **Value restrictions**
* `WrapperTx.value` must equal `0`.
If any check fails, validators reject the wrapper transaction and do not execute the inner transaction.
### Execution semantics
If all checks pass:
1. The protocol executes `InnerTx` as the user, preserving the user’s `from`, `nonce`, and call semantics.
2. Gas accounting is handled by the waiver mechanism: the user pays no gas, and the waiver transaction uses `gasPrice = 0` by definition of the feature.
3. The wrapper transaction must supply sufficient `gasLimit` to cover the execution of `InnerTx` (including overhead for unwrap and verification).
## Transaction formats
### WrapperTx
The wrapper transaction is signed by the waiver and sent to the marker address.
```javascript theme={"dark"}
WrapperTx {
from: waiver_address,
to: 0x000000000000000000000000000000000000f333,
value: 0, // must be zero
data: RLP(InnerTx), // RLP-encoded inner transaction
gasPrice: 0, // must be zero
gasLimit: sufficient_for_inner, // must cover inner execution + overhead
nonce: waiver_nonce
}
```
### InnerTx
The inner transaction is signed by the end user.
```javascript theme={"dark"}
InnerTx {
from: user_address,
to: target_contract,
value: value,
data: call_data,
gasPrice: 0, // must be zero
gasLimit: execution_gas,
nonce: user_nonce
}
```
## Governance-controlled access
Waiver authorization is governed on-chain by validator governance.
Governance control provides:
* Reviewable authorization of waiver addresses
* On-chain transparency of waiver registration and updates
* Revocation capability
* Per-waiver scoping via `AllowedTarget`
## Security model
### End-user signature integrity
The user signs the `InnerTx`. The waiver cannot modify the inner transaction payload without invalidating the signature. You must still ensure that the user signs only the intended transaction payload.
### Trust boundary
Gas Waiver introduces a service dependency if partners route submissions through the Waiver Server:
* Availability of the service affects the ability to submit gasless transactions.
* Authorization remains on-chain; only registered waiver addresses can produce valid wrapper submissions.
## Integration
You integrate by:
1. Collect a signed `InnerTx` from the user (`gasPrice = 0`).
2. Submit the signed inner transaction to the Waiver Server API.
3. Handle streamed results and surfacing transaction hashes to end users.
## Waiver server
### Overview
The Waiver Server wraps and broadcasts signed user `InnerTx` payloads as waiver-authorized wrapper transactions. You do not need to construct wrapper transactions or operate a waiver address.
### Endpoints and base URLs
Base URLs:
* Mainnet: TBD
* Testnet: `https://waiver.testnet.stable.xyz`
### Authentication
All endpoints except health require bearer token authentication:
```
Authorization: Bearer
```
### API
#### GET `/v1/health`
Health check endpoint.
Authentication: none.
#### POST `/v1/submit`
Submit a batch of signed inner transactions.
Authentication: required (`Bearer`).
Request body:
```json theme={"dark"}
{
"transactions": ["0x", "0x"]
}
```
Response is streamed as NDJSON (newline-delimited JSON). Each line corresponds to a submitted transaction index.
Example:
```json theme={"dark"}
{"index":0,"id":"abc123","success":true,"txHash":"0x..."}
{"index":1,"id":"def456","success":false,"error":{"code":"VALIDATION_FAILED","message":"invalid signature"}}
```
#### GET `/v1/submit`
WebSocket interface for streaming submissions.
Authentication: required (`Bearer`).
### Integration example
```javascript theme={"dark"}
const WAIVER_SERVER = "https://waiver.testnet.stable.xyz";
async function submitGaslessTransaction(signedInnerTxHex, apiKey) {
const response = await fetch(`${WAIVER_SERVER}/v1/submit`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify({
transactions: [signedInnerTxHex],
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const lines = decoder.decode(value).trim().split("\n");
for (const line of lines) {
const result = JSON.parse(line);
console.log(result);
}
}
}
```
### Creating a user InnerTx
You are responsible for constructing an `InnerTx` with `gasPrice = 0`, then collecting the user signature.
Example:
```javascript theme={"dark"}
import { ethers } from "ethers";
async function createInnerTx(userWallet, contractAddress, callData, nonce) {
const innerTx = {
to: contractAddress,
data: callData,
value: value,
gasPrice: 0, // must be 0 for waiver
gasLimit: 100000,
nonce: nonce,
chainId: 2201, // 988 for mainnet, 2201 for testnet
};
return await userWallet.signTransaction(innerTx);
}
```
### Error codes
* `PARSE_ERROR`: Failed to parse transaction
* `INVALID_REQUEST`: Malformed request body
* `BATCH_SIZE_EXCEEDED`: Batch size exceeds allowed maximum
* `VALIDATION_FAILED`: Transaction validation failed
* `BROADCAST_FAILED`: Failed to broadcast to chain
* `RATE_LIMITED`: Rate limit exceeded
* `QUEUE_FULL`: Server queue at capacity
* `TIMEOUT`: Request timed out
## Next recommended
Demo-focused walkthrough with a receipt showing zero gas fee.
Full hosted-API integration guide with batch submissions and error handling.
Run your own waiver infrastructure without the hosted API.
# Settle invoices
Source: https://docs.stable.xyz/en/reference/invoices
Settle invoices on Stable using ERC-3009 with deterministic nonces derived from invoice metadata for automatic reconciliation.
Each invoice maps to a unique, deterministic nonce derived from invoice metadata: invoice number, parties, amount, and due date. This nonce drives settlement via [ERC-3009](/en/explanation/erc-3009) and creates an immutable receipt that can be reconciled with existing accounting systems.
## How it works
Both the buyer and the vendor independently compute the same nonce from the same invoice metadata. No external registry is required to coordinate payment.
The nonce is derived deterministically:
```
nonce = keccak256(invoiceNumber, vendor, buyer, amount, dueDate)
```
When the buyer signs the ERC-3009 authorization using this nonce, the on-chain settlement event serves as a tamper-proof payment receipt.
### Settlement flow
1. **Invoice issued**: the vendor creates an invoice with a unique number, amount, and due date.
2. **Nonce computed**: both parties independently derive the same nonce from the invoice metadata.
3. **Buyer signs**: the buyer signs an ERC-3009 authorization off-chain using the deterministic nonce. The `validBefore` field can be set to the due date plus a grace period.
4. **Settlement**: the buyer or vendor submits `transferWithAuthorization` on-chain. Settlement confirms in under a second.
5. **Reconciliation**: the emitted `AuthorizationUsed` event contains the nonce, linking the on-chain settlement to the exact invoice. The `Transfer` event in the same transaction verifies sender, recipient, and amount.
### Double-payment prevention
The nonce is consumed on-chain upon payment. The same invoice cannot be settled twice; resubmitting an authorization with an already-used nonce reverts.
## What makes it different
Traditional B2B invoicing involves bank wires (1–5 business days), manual reconciliation, and no cryptographic proof of payment tied to the invoice itself. With deterministic nonces, the on-chain payment is self-documenting: the nonce links the settlement to the exact invoice, and the blockchain event log provides an immutable audit trail.
| **Aspect** | **Traditional (bank wire)** | **Stable (ERC-3009)** |
| :------------- | :-------------------------------------- | :-------------------------------------------------------- |
| Settlement | 1–5 business days | Under 1 second |
| Reconciliation | Manual matching against bank statements | `AuthorizationUsed` event links payment to invoice nonce |
| Payment proof | Bank confirmation letter | On-chain transaction, cryptographically linked to invoice |
| Intermediaries | Correspondent banks | None |
| Fees | Wire fees (\$15–45) + FX spread | \~0.00021 USDT0 (or 0 with Gas Waiver) |
**See also:**
* [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009)
* [Gas Waiver](/en/how-to/integrate-gas-waiver)
# JSON-RPC API
Source: https://docs.stable.xyz/en/reference/json-rpc-api
Supported JSON-RPC API methods across eth, net, web3, and debug namespaces on Stable.
## eth\_namespace
| API | support |
| ------------------------------------------- | ------- |
| eth\_syncing | ✅ |
| eth\_gasPrice | ✅ |
| eth\_maxPriorityFeePerGas | ✅ |
| eth\_feeHistory | ✅ |
| eth\_blobBaseFee | ❌ |
| eth\_chainId | ✅ |
| eth\_blockNumber | ✅ |
| eth\_getBalance | ✅ |
| eth\_getProof | ✅ |
| eth\_getHeaderByNumber | ❌ |
| eth\_getHeaderByHash | ❌ |
| eth\_getBlockByNumber | ✅ |
| eth\_getBlockByHash | ✅ |
| eth\_getUncleByBlockNumberAndIndex | ❌ |
| eth\_getUncleByBlockHashAndIndex | ❌ |
| eth\_getUncleCountByBlockNumber | ❌ |
| eth\_getUncleCountByBlockHash | ❌ |
| eth\_getCode | ✅ |
| eth\_getStorageAt | ✅ |
| eth\_getBlockReceipts | ❌ |
| eth\_call | ✅ |
| eth\_simulateV1 | ❌ |
| eth\_estimateGas | ✅ |
| eth\_createAccessList | ❌ |
| eth\_getBlockTransactionCountByNumber | ✅ |
| eth\_getBlockTransactionCountByHash | ✅ |
| eth\_getTransactionByBlockNumberAndIndex | ✅ |
| eth\_getTransactionByBlockHashAndIndex | ✅ |
| eth\_getRawTransactionByBlockNumberAndIndex | ❌ |
| eth\_getRawTransactionByBlockHashAndIndex | ❌ |
| eth\_getTransactionCount | ✅ |
| eth\_getTransactionByHash | ✅ |
| eth\_getRawTransactionByHash | ❌ |
| eth\_getTransactionReceipt | ✅ |
| eth\_sendTransaction | ✅ |
| eth\_fillTransaction | ❌ |
| eth\_sendRawTransaction | ✅ |
| eth\_sign | ✅ |
| eth\_signTransaction | ❌ |
| eth\_pendingTransactions | ✅ |
| eth\_resend | ✅ |
| eth\_accounts | ✅ |
| eth\_subscribe | ✅ |
| eth\_unsubscribe | ✅ |
| eth\_getTransactionLogs | ✅ |
| eth\_signTypedData | ✅ |
| eth\_newPendingTransactionFilter | ✅ |
| eth\_newBlockFilter | ✅ |
| eth\_newFilter | ✅ |
| eth\_getFilterChanges | ✅ |
| eth\_getFilterLogs | ✅ |
| eth\_uninstallFilter | ✅ |
| eth\_getLogs | ✅ |
## debug\_namespace
| API | support |
| ---------------------------------- | ------- |
| debug\_accountRange | ❌ |
| debug\_backtraceAt | ❌ |
| debug\_blockProfile | ✅ |
| debug\_chaindbCompact | ❌ |
| debug\_chaindbProperty | ❌ |
| debug\_cpuProfile | ✅ |
| debug\_dbAncient | ❌ |
| debug\_dbAncients | ❌ |
| debug\_dbGet | ❌ |
| debug\_dumpBlock | ❌ |
| debug\_freeOSMemory | ✅ |
| debug\_freezeClient | ❌ |
| debug\_gcStats | ✅ |
| debug\_getAccessibleState | ❌ |
| debug\_getBadBlocks | ❌ |
| debug\_getRawBlock | ❌ |
| debug\_getRawHeader | ❌ |
| debug\_getRawTransaction | ❌ |
| debug\_getModifiedAccountsByHash | ❌ |
| debug\_getModifiedAccountsByNumber | ❌ |
| debug\_getRawReceipts | ❌ |
| debug\_goTrace | ✅ |
| debug\_intermediateRoots | ✅ |
| debug\_memStats | ✅ |
| debug\_mutexProfile | ✅ |
| debug\_preimage | ❌ |
| debug\_printBlock | ✅ |
| debug\_setBlockProfileRate | ✅ |
| debug\_setGCPercent | ✅ |
| debug\_setHead | ❌ |
| debug\_setMutexProfileFraction | ✅ |
| debug\_setTrieFlushInterval | ❌ |
| debug\_stacks | ✅ |
| debug\_standardTraceBlockToFile | ❌ |
| debug\_standardTraceBadBlockToFile | ❌ |
| debug\_startCPUProfile | ✅ |
| debug\_startGoTrace | ✅ |
| debug\_stopCPUProfile | ✅ |
| debug\_stopGoTrace | ✅ |
| debug\_storageRangeAt | ❌ |
| debug\_traceBadBlock | ❌ |
| debug\_traceBlock | ❌ |
| debug\_traceBlockByNumber | ✅ |
| debug\_traceBlockByHash | ✅ |
| debug\_traceBlockFromFile | ❌ |
| debug\_traceCall | ❌ |
| debug\_traceChain | ❌ |
| debug\_traceTransaction | ✅ |
| debug\_verbosity | ❌ |
| debug\_vmodule | ❌ |
| debug\_writeBlockProfile | ✅ |
| debug\_writeMemProfile | ✅ |
| debug\_writeMutexProfile | ✅ |
## Next recommended
Construct EIP-1559 transactions against Stable's base-fee-only model.
Build type-4 transactions with the `authorizationList` field.
Call Bank, Distribution, and Staking at their fixed precompile addresses.
# Mainnet information
Source: https://docs.stable.xyz/en/reference/mainnet-information
Chain ID, RPC endpoints, block explorer, and network parameters for the Stable Mainnet.
Everything you need to know to access Stable Mainnet.
## Network overview
| Configuration | Value |
| ---------------- | -------------- |
| **Network Name** | Stable Mainnet |
| **Chain ID** | `988` |
| **Gas Token** | USDT0 |
| **Gov Token** | STABLE |
| **Block Time** | \~0.7 seconds |
## Block explorers
| Explorer | URL |
| -------------- | ------------------------------------------------ |
| **Stablescan** | [https://stablescan.xyz](https://stablescan.xyz) |
## RPC endpoints
### Primary endpoints
| Type | Endpoint | Purpose |
| ---------------- | ------------------------------------------------ | ----------------- |
| **EVM JSON-RPC** | [https://rpc.stable.xyz](https://rpc.stable.xyz) | EVM transactions |
| **WebSocket** | wss\://rpc.stable.xyz | Real-time updates |
## Chain information
| Parameter | EVM |
| ------------- | ------- |
| **Chain ID** | `988` |
| **Gas Token** | `USDT0` |
| **Decimals** | 18 |
## Tools
| Tool | URL | Description |
| ------------- | --------------------------------------------------------- | --------------- |
| **Snapshots** | See [Node Operators Guide](/en/how-to/use-node-snapshots) | Chain snapshots |
# Version history
Source: https://docs.stable.xyz/en/reference/mainnet-version-history
Version history and upgrade schedule for the Stable Mainnet network.
Complete version history and related documentation for the Stable Mainnet.
## Current version information
* **Current Version**: `v1.2.2`
* **Next Upgrade**: `TBD`
* **Upgrade Height**: `TBD`
* **Expected Time**: `TBD`
## Version history
### Current & previous versions
| Version | Commit | Upgrade Height | Binary | Status |
| ---------- | --------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| **v1.2.2** | `76da1da` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.2-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.2-linux-arm64-mainnet.tar.gz) | Current |
| **v1.2.1** | `7955bb7` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.1-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.1-linux-arm64-mainnet.tar.gz) | |
| **v1.2.0** | `47e355b` | 12004000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.2.0-linux-arm64-mainnet.tar.gz) | |
| **v1.1.4** | `c795773` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.4-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.4-linux-arm64-mainnet.tar.gz) | |
| **v1.1.2** | `3d83aa3` | 3263600 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.2-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.2-linux-arm64-mainnet.tar.gz) | |
| **v1.1.0** | `17ceaa7` | 1694000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.1.0-linux-arm64-mainnet.tar.gz) | |
| **v1.0.0** | `d996084` | Genesis | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.0.0-linux-amd64-mainnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-1.0.0-linux-arm64-mainnet.tar.gz) | Genesis |
## Related documentation
* [Upgrade Guide](/en/how-to/upgrade-node) - Step-by-step upgrade procedures
* [Mainnet Information](/en/reference/mainnet-information) - Current network details
# Network routing
Source: https://docs.stable.xyz/en/reference/network-routing
Decentralized networking and data routing providers optimized for Stable.
Network routing providers optimizing connectivity and data delivery for applications on Stable.
## Overview table
| **Provider** | **Category** | **Docs / Get Started** | **Notes** |
| :---------------------------------- | :----------------------- | :---------------------------------- | :--------------------------------- |
| [**Optimum**](https://optimum.xyz/) | Decentralized networking | [optimum.xyz](https://optimum.xyz/) | High-performance routing for dApps |
## Optimum
A decentralized internet protocol optimized for speed and scalable web3 interactions.
**Capabilities**
* High-performance decentralized networking
* Faster application data routing
* Reliable infra for dApps
**Get started**: Visit [optimum.xyz](https://optimum.xyz/) to learn how to route your Stable dApp traffic through Optimum's decentralized network infrastructure.
***
Have a networking integration with Stable? Reach out at [bizdev@stable.xyz](mailto:bizdev@stable.xyz).
# Network upgrades
Source: https://docs.stable.xyz/en/reference/network-upgrades
Release notes and upgrade history for Stable network protocol versions.
# Oracles
Source: https://docs.stable.xyz/en/reference/oracles
Oracle price feed providers on Stable, including contract addresses, supported pairs, and integration examples.
Oracles provide smart contracts with off-chain data such as asset prices. Chaos Labs operates price feeds on Stable.
## Overview table
| **Provider** | **Category** | **Supported Pairs** | **Docs / Get Started** | **Notes** |
| :--------------------------------------- | :----------------- | :------------------------------------------------------------ | :--------------------------------------------------------------------------------- | :-------------------------- |
| [**Chaos Labs**](https://chaoslabs.xyz/) | Oracle Price Feeds | BTC/USD, ETH/USD, SOL/USD, FRXUSD/USD, SFRXUSD/USD, ezETH/ETH | [https://docs.chaoslabs.xyz/oracles/docs](https://docs.chaoslabs.xyz/oracles/docs) | Live on mainnet and testnet |
## Chaos Labs
Chaos Labs provides oracle price feeds on Stable. Feed contracts expose the `IEdgePushOracle` interface documented in the [Chaos Labs EVM integration guide](https://docs.chaoslabs.xyz/oracles/docs/integration-guides/evm).
**Capabilities**
* Push-based price feeds with configurable deviation thresholds and heartbeat intervals
* `IEdgePushOracle` interface with `latestRoundData()`, `decimals()`, and `description()`
* On-chain price data for DeFi protocols, lending, and liquidation engines
### Mainnet price feed addresses
Source: [Chaos Labs push feeds dashboard](https://docs.chaoslabs.xyz/oracles/docs/dashboard/push-feeds)
| **Price Feed** | **Contract Address** | **Deviation** | **Heartbeat** |
| :---------------- | :---------------------------------------------------------------------------------------------------------------------- | :------------ | :------------ |
| **FRXUSD / USD** | [0xAe48F22903d43f13f66Cc650F57Bd4654ac222cb](https://stablescan.xyz/address/0xAe48F22903d43f13f66Cc650F57Bd4654ac222cb) | 0.05% | 8m |
| **ezETH / ETH** | [0x45D531E6BB4eF640BF4bFc1DDE832e1EDFFea8a5](https://stablescan.xyz/address/0x45D531E6BB4eF640BF4bFc1DDE832e1EDFFea8a5) | 0.05% | 8m |
| **SFRXUSD / USD** | [0x955998975cFDAFD0e0dc60f5A92E14fA72384AaE](https://stablescan.xyz/address/0x955998975cFDAFD0e0dc60f5A92E14fA72384AaE) | 0.05% | 8m |
| **ETH / USD** | [0x163131609562E578754aF12E998635BfCa56712C](https://stablescan.xyz/address/0x163131609562E578754aF12E998635BfCa56712C) | 0.5% | 47m |
| **BTC / USD** | [0x9f6aA2aB14bFF53e4b79A81ce1554F1DFdbb6608](https://stablescan.xyz/address/0x9f6aA2aB14bFF53e4b79A81ce1554F1DFdbb6608) | 0.5% | 45m |
### Testnet price feed addresses
| **Price Feed** | **Contract Address** |
| :------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- |
| **BTC / USD** | [0xECA49340544541957eC64B7635418D2159616826](https://testnet.stablescan.xyz/address/0xECA49340544541957eC64B7635418D2159616826?tab=read_write_proxy) |
| **ETH / USD** | [0x176A9536feaC0340de9f9811f5272E39E80b424f](https://testnet.stablescan.xyz/address/0x176A9536feaC0340de9f9811f5272E39E80b424f?tab=read_write_proxy) |
| **SOL / USD** | [0x7fa367967CE7903Fc5cE25a969cb7dB792a8f6b9](https://testnet.stablescan.xyz/address/0x7fa367967CE7903Fc5cE25a969cb7dB792a8f6b9?tab=read_write_proxy) |
## Reading a price feed
Feed contracts implement `IEdgePushOracle`. The following example is adapted from the [Chaos Labs documentation](https://docs.chaoslabs.xyz/oracles/docs/integration-guides/evm).
```solidity theme={"dark"}
pragma solidity ^0.8.25;
interface IEdgePushOracle {
function latestRoundData()
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
function decimals() external view returns (uint8);
function description() external view returns (string memory);
}
contract OracleConsumer {
IEdgePushOracle public oracle;
constructor(address oracleAddress) {
oracle = IEdgePushOracle(oracleAddress);
}
function getLatestPriceData()
external
view
returns (
uint80 roundId,
int256 answer,
uint256 updatedAt
)
{
(roundId, answer, , updatedAt, ) = oracle.latestRoundData();
return (roundId, answer, updatedAt);
}
}
```
This code is from the Chaos Labs documentation and is provided for illustrative purposes. Test thoroughly before production use.
### Deploying to Stable Testnet
This assumes you have Foundry installed and a funded wallet. See the [Deploy Smart Contract](/en/tutorial/smart-contract) tutorial for full setup instructions.
1. Save the contract above to `src/OracleConsumer.sol` in a Foundry project.
2. Deploy with the ETH/USD testnet feed address:
```bash theme={"dark"}
source .env ;
forge create src/OracleConsumer.sol:OracleConsumer --broadcast --rpc-url $STABLE_TESTNET_RPC_URL --private-key $PRIVATE_KEY --constructor-args 0x176A9536feaC0340de9f9811f5272E39E80b424f
```
3. Read the latest price from your deployed contract:
```bash theme={"dark"}
cast call "getLatestPriceData()(uint80,int256,uint256)" --rpc-url $STABLE_TESTNET_RPC_URL
```
## Have an oracle integrating Stable?
Reach the team at [bizdev@stable.xyz](mailto:bizdev@stable.xyz) to be listed on this page.
# Send and receive USDT0
Source: https://docs.stable.xyz/en/reference/p2p-payments
Peer-to-peer USDT0 transfers on Stable via native transfers or ERC-3009 delegated settlement.
On Stable, P2P payments settle in under a second. Two transfer methods are available depending on the use case.
## Native transfer
The sender signs and broadcasts a transaction directly to the recipient. This costs 21,000 gas (\~0.00021 USDT0 at 100 gwei). No contract interaction is required.
A native transfer is the simplest path: the sender signs a transaction sending USDT0 directly to the recipient. This is equivalent to entering an amount and selecting "Send" in any payment app.
For a code walkthrough, see [Send your First USDT0](/en/tutorial/send-usdt0).
## Application-initiated transfer (ERC-3009)
The sender signs an off-chain authorization. An application or facilitator submits the transaction on their behalf. Combined with the [Gas Waiver](/en/how-to/integrate-gas-waiver), the gas cost is 0.
[ERC-3009](/en/explanation/erc-3009) is more appropriate for application-initiated payments (e.g., a payment in a web app) because it separates the signer from the submitter. The sender only signs an authorization off-chain, and the application or a facilitator handles on-chain submission.
## What makes it different
On traditional payment rails, a P2P transfer involves bank processing, clearing, and settlement that can take 1–3 business days. Even on other blockchains, the sender needs to hold a volatile gas token (ETH, SOL) alongside the payment token. On Stable, the sender holds only USDT0, gas can be waived, and settlement is final in under a second.
| **Aspect** | **Traditional (bank transfer)** | **Other blockchains** | **Stable** |
| :--------------- | :------------------------------ | :-------------------------------------------------- | :---------------------------------------------------- |
| Settlement time | 1–3 business days | Seconds to minutes, depending on the chain | Under 1 second (single-slot finality) |
| Required assets | Fiat currency | Payment token + separate gas token (ETH, SOL, etc.) | USDT0 only (single asset) |
| Transaction cost | Wire / intermediary fees | Can spike with network congestion | \~0.00021 USDT0 native transfer or \$0 via Gas Waiver |
**See also:**
* [Send your First USDT0](/en/tutorial/send-usdt0)
* [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009)
* [Gas Waiver](/en/how-to/integrate-gas-waiver)
# Bill per API request
Source: https://docs.stable.xyz/en/reference/pay-per-call
Monetize HTTP endpoints with per-request USDT0 payments using x402 middleware on Stable.
Monetize any HTTP endpoint with per-request pricing using [x402](/en/explanation/x402) middleware. A server declares its price, a client pays per call, and settlement happens within the request lifecycle. No accounts, no API keys, no billing cycles.
## How it works
The server adds x402 middleware to the endpoints it wants to monetize. When a request arrives without payment, the server responds with HTTP `402 Payment Required` and a `PAYMENT-REQUIRED` header containing the price, token, and network. The client signs an [ERC-3009](/en/explanation/erc-3009) authorization for the specified amount and resubmits. The facilitator settles the payment on-chain, and the server returns the resource.
### Request flow
1. Client sends an HTTP request to the server.
2. Server returns `402 Payment Required` with a `PAYMENT-REQUIRED` header containing the price, token, network, and recipient.
3. Client signs an ERC-3009 authorization for the specified amount and resubmits the request with a `PAYMENT-SIGNATURE` header.
4. Facilitator verifies the signature and settles the transfer on-chain.
5. Server returns the resource with a `PAYMENT-RESPONSE` header containing the settlement receipt.
### Pricing
Prices are denominated in USDT0 atomic units (6 decimals). A cost parameter of `"1000"` translates to exactly $0.001. A cost of `"50000"` is $0.05. This precision allows servers to set prices at fractions of a cent.
### Infrastructure
On Stable, [Semantic Pay](https://x402.semanticpay.io) operates a public facilitator. Developers can point their middleware to this endpoint without running their own settlement infrastructure.
x402 provides middleware for Express (`@x402/express`), Hono (`@x402/hono`), and Next.js (`@x402/next`). The pattern is the same across all frameworks: create a facilitator client, register the EVM scheme, and apply middleware.
## What makes it different
Traditional API monetization requires user registration, API key management, usage tracking, billing cycles, and payment processor integration. With x402, the server attaches a payment handler to each endpoint, the client pays per request, and settlement completes within the same HTTP lifecycle. The server does not need to know who the client is, only that a valid payment was submitted.
| **Aspect** | **Traditional (API key + billing cycle)** | **Stable (x402)** |
| :----------------------- | :------------------------------------------------------------------------ | :---------------------------------------- |
| Server-side setup | Registration, API keys, usage tracking, billing cycles, payment processor | x402 payment handler per endpoint |
| Client onboarding | Account creation, API key issuance | None (wallet only) |
| Billing model | Monthly or usage-based invoicing | Per-request settlement |
| Client identity required | Yes (API key) | No (only valid payment) |
| Settlement | End of billing cycle | Within request lifecycle (under 1 second) |
| Minimum viable price | \~\$0.30 (card processing floor) | \$0.001 (USDT0 atomic units) |
| Client type | Human users only (sign-up required) | Any wallet: humans, AI agents, scripts |
**See also:**
* [x402 (HTTP-Native Payments)](/en/explanation/x402)
* [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009)
* [Gas Waiver](/en/how-to/integrate-gas-waiver)
# Ramps
Source: https://docs.stable.xyz/en/reference/ramps
On/off ramp and payment gateway providers for converting between fiat currencies and USDT on Stable.
On/off ramp partners connect Stable to global fiat systems, enabling users and businesses to move between USDT, local currencies, and payment rails.
## On/off ramp overview table
| **Provider** | **Category** | **Docs / Get Started** | **Notes** |
| :----------------------------------------------------------------------- | :------------- | :----------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
| [**Onmeta**](https://onmeta.in/on-off-ramp) | On/Off Ramp | [https://docs.onmeta.in/](https://docs.onmeta.in/) | Compliant fiat-to-USDT + cross-border payouts |
| [**Halliday**](https://halliday.xyz/) | On/Off Ramp | [https://docs.halliday.xyz/pages/home](https://docs.halliday.xyz/pages/home) | CEX + Stripe integration |
| [**Alchemy Pay**](https://alchemypay.org/about) | Gateway | [https://alchemypay.readme.io/docs/alchemypay-on-ramp](https://alchemypay.readme.io/docs/alchemypay-on-ramp) | 300+ payment methods |
| [**DFX**](https://www.dfx.swiss/) | Off-Ramp FX | [https://docs.dfx.swiss/](https://docs.dfx.swiss/) | Regulated stablecoin-to-fiat |
| [**Onramp Money**](https://onramp.money/) | On/Off Ramp | [https://docs.onramp.money/onramp/](https://docs.onramp.money/onramp/) | Emerging market coverage |
| [**MoonPay**](https://www.moonpay.com/business/onramps) | Universal Ramp | [https://dev.moonpay.com/docs/on-ramp-overview](https://dev.moonpay.com/docs/on-ramp-overview) | Global card + bank support |
| [**Transak**](https://transak.com/off-ramp) | On/Off Ramp | [https://docs.transak.com/](https://docs.transak.com/) | 450+ integrations |
| [**Banxa**](https://banxa.com/solutions/by-use-case/on-and-off-ramping/) | Regulated Ramp | [https://docs.banxa.com/docs/overview](https://docs.banxa.com/docs/overview) | Local rails in 100+ countries |
## Category guide
* **On/Off Ramps:** Platforms enabling direct conversion between fiat currencies and USDT on Stable.
* **Payment Gateways:** Services connecting apps, merchants, or fintechs to global fiat rails for seamless transactions.
* **FX & Off-Ramp Networks:** Regulated infrastructure providing compliant, stablecoin-to-fiat settlement at scale.
## On/off ramps & payment gateways
### MoonPay
Universal crypto on/off ramp used globally for instant asset purchases.
**Capabilities**
* Card + bank + local payment rails
* Fast global onboarding
* Multi-chain support
**Get started**: Follow the [MoonPay on-ramp integration guide](https://dev.moonpay.com/docs/on-ramp-overview) to embed fiat-to-USDT purchasing on Stable into your app.
### Transak
On/off ramp providing frictionless ways to buy, sell, and transfer crypto across 450+ apps.
**Capabilities**
* Global fiat methods
* Easy integration APIs
* Country-level compliance support
**Get started**: Review the [Transak integration docs](https://docs.transak.com/) to add on/off ramp widgets or APIs with Stable as a supported network.
### Onmeta
On/off ramp and cross-border payout infrastructure with built-in compliance for VDA platforms.
**Capabilities**
* Fiat ↔ USDT conversion
* Compliance-ready flows
* Cross-border payouts
* Local settlement rails
**Get started**: Refer to the [Onmeta API documentation](https://docs.onmeta.in/) to integrate fiat-to-USDT conversion and cross-border payouts on Stable.
### Halliday
End-to-end payment suite enabling seamless on/off ramping from top CEXs and payment platforms like Coinbase and Stripe.
**Capabilities**
* Direct connection to major CEXs
* Stripe + payment rail integration
* Merchant and app-friendly flows
**Get started**: Read the [Halliday integration docs](https://docs.halliday.xyz/pages/home) to connect CEX and Stripe-based payment flows to Stable in your product.
### Alchemy Pay
Global payment gateway bridging traditional finance and crypto with 300+ payment methods.
**Capabilities**
* Global fiat on/off ramps
* Merchant payments
* Bank transfers + cards + local rails
**Get started**: Use the [Alchemy Pay on-ramp API](https://alchemypay.readme.io/docs/alchemypay-on-ramp) to add 300+ payment methods for USDT purchases on Stable.
### DFX
Swiss-regulated decentralized FX and off-ramp network with over 100M transactions.
**Capabilities**
* Stablecoin-to-fiat conversion
* Regulated FX layer
* Deep multi-currency support
**Get started**: Consult the [DFX API documentation](https://docs.dfx.swiss/) to set up compliant stablecoin-to-fiat off-ramp flows using Stable as the source network.
### Onramp Money
Low-cost fiat-to-crypto gateway specializing in emerging markets.
**Capabilities**
* Local payment method coverage
* Instant settlement
* 1.3M+ supported users
**Get started**: Follow the [Onramp Money developer guide](https://docs.onramp.money/onramp/) to integrate fiat-to-crypto flows for emerging market users on Stable.
### Banxa
Regulated on/off ramp infrastructure connecting users to crypto via local rails in 100+ countries.
**Capabilities**
* Regulated fiat gateways
* Broad global coverage
* Bank and local payment options
**Get started**: Read the [Banxa integration overview](https://docs.banxa.com/docs/overview) to enable regulated on/off ramp flows with Stable as a supported network.
***
Have an on/off ramp integrating Stable? Reach out at [bizdev@stable.xyz](mailto:bizdev@stable.xyz).
# RPC providers
Source: https://docs.stable.xyz/en/reference/rpc-providers
RPC providers, simulation platforms, and developer infrastructure available on Stable.
RPC and developer infrastructure providers supporting Stable.
## Overview table
| **Provider** | **Category** | **Docs / Get Started** | **Notes** |
| :--------------------------------------------------------------------------------------------------------------- | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------- |
| [**Alchemy**](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) | RPC + developer platform | [Get started with Alchemy](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) | RPC, WebSocket, monitoring, SDKs |
| [**Tenderly**](https://tenderly.co/) | Simulation + debugging | [tenderly.co](https://tenderly.co/) | Real-time simulation, tracing, transaction workflows |
## Alchemy
A complete blockchain development platform trusted globally. [Get started with Alchemy](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable)
**Capabilities**
* RPC + WebSocket infrastructure
* Monitoring dashboards
* Developer APIs and SDKs
**Get started**: Create a Stable app in the [Alchemy dashboard](https://dashboard.alchemy.com/?utm_source=chain_partner\&utm_medium=referral\&utm_campaign=stable) to get an RPC URL, then use it as your JSON-RPC endpoint.
## Tenderly
A full-stack developer platform offering simulation, debugging, monitoring, and execution tooling.
**Capabilities**
* Real-time contract simulation
* Debugging and tracing
* Transaction workflows for devs
**Get started**: Set up a Stable project in the [Tenderly dashboard](https://tenderly.co/) to access simulation, debugging, and transaction tracing for your contracts.
***
Have an RPC or infrastructure integration with Stable? Reach out at [bizdev@stable.xyz](mailto:bizdev@stable.xyz).
# Staking precompile reference
Source: https://docs.stable.xyz/en/reference/staking-module-api
Staking precompile reference: delegation, undelegation, validator management, and query methods.
**Concept:** For what the staking module does and when to use it, see [Staking module](/en/explanation/staking-module).
## Abstract
The `staking` precompiled contract acts as a bridge that enables EVM environments to use the Stable SDK's `x/staking` module functionality.
## Contents
1. **[Concepts](#concepts)**
2. **[Configuration](#configuration)**
3. **[Methods](#methods)**
4. **[Events](#events)**
## Concepts
In `x/staking` module in Stable SDK, bond denom must be registered during chain initialization for staking.
Validators and delegators can only use the bond denom staking token.
The `staking` precompiled contract performs additional checks to ensure that the validator or delegator is the caller.
## Configuration
The contract address and gas cost are predefined.
### Contract address
* `0x0000000000000000000000000000000000000800`
## Methods
### `createValidator`
Creates a validator.
The validator must be created with an initial delegation from the operator.
For potential delegators, the validator should offer information and a plan for the commission rate.
Delegators can choose a validator to delegate their tokens to based on disclosed information, with natural regulation from market mechanisms.
`CreateValidator` is emitted when the validator is successfully registered.
#### Inputs
| Name | Type | Description |
| ----------------- | --------------- | ------------------------------------------------------------------------- |
| description | Description | information of the validator |
| commissionRates | CommissionRates | commission rate of the staking token rewarded by the validator |
| minSelfDelegation | uint256 | the minimum self-delegation amount of the validator |
| validatorAddress | address | the address of the validator |
| pubkey | string | the public key of the validator |
| value | uint256 | the amount of the staking token initially self-delegated to the validator |
`Description` is a struct with the following fields:
| Name | Type | Description |
| --------------- | ------ | --------------------------------------- |
| moniker | string | name of the validator |
| identity | string | the identity of the validator |
| website | string | the url of the validator's website |
| securityContact | string | security contract information |
| details | string | additional description of the validator |
`CommissionRates` is a struct with the following fields:
| Name | Type | Description |
| ------------- | ------- | --------------------------------------------------------------- |
| rate | uint256 | current commission rate that the validator receives |
| maxRate | uint256 | the maximum commission rate (cannot be set higher than this) |
| maxChangeRate | uint256 | the maximum commission rate that a validator can change per day |
`rate` should be set to an appropriate value acceptable to the market.
* If validator's commission rate is higher, delegator's profit is lower.
* If validator's commission rate is lower, validator's profit is lower and it makes operation difficult.
Since a high `maxRate` can make delegators concerned about an unexpected high commission rate from the validator, `maxRate` should be set carefully. `maxChangeRate` is unchangeable when initialized.
#### Outputs
| Name | Type | Description |
| ------- | ---- | ------------------------------------------------ |
| success | bool | true if the validator is successfully registered |
### `editValidator`
Validator updates its information.
Validator only can update information except unchangeable fields such as `maxRate` and `maxChangeRate` in `CommissionRates` struct.
`EditValidator` is emitted when the validator is successfully updated.
#### Inputs
| Name | Type | Description |
| ----------------- | ----------- | ------------------------------------------------------------------ |
| description | Description | information of the validator |
| validatorAddress | address | the address of the validator |
| commissionRate | int256 | the commission rate of the staking token rewarded by the validator |
| minSelfDelegation | int256 | the minimum self-delegation amount of the validator |
#### Outputs
| Name | Type | Description |
| ------- | ---- | --------------------------------------------- |
| success | bool | true if the validator is successfully updated |
### `delegate`
Delegator sets the amount of token to be delegated to the validator.
`Delegate` is emitted when the delegation is successfully done.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------------------------------------- |
| delegatorAddress | address | the address of the delegator |
| validatorAddress | address | the address of the validator |
| amount | uint256 | the amount of the staking token delegated to the validator |
#### Outputs
| Name | Type | Description |
| ------- | ---- | ------------------------------------------- |
| success | bool | true if the delegation is successfully done |
#### Events
`newShares` indicates the ownership ratio of the delegator.
The shares calculated may vary depending on the time even though the same amount of tokens are delegated.
### `undelegate`
Delegator withdraws the amount of token delegated to the validator.
`Unbond` is emitted when the undelegation is successfully done.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ------------------------------------------------------------------------ |
| delegatorAddress | address | the address of the delegator |
| validatorAddress | address | the address of the validator |
| amount | uint256 | the amount of the staking token willing to undelegate from the validator |
#### Outputs
| Name | Type | Description |
| ------- | ---- | --------------------------------------------- |
| success | bool | true if the undelegation is successfully done |
### `redelegate`
Delegator redelegates the amount of token delegated to the validator to another validator.
`Redelegate` is emitted when the redelegate is successfully done.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ------------------------------------------------ |
| delegatorAddress | address | the address of the delegator |
| validatorSrc | string | the address of the source validator |
| validatorDst | string | the address of the destination validator |
| amount | uint256 | the amount of the staking token for redelegation |
#### Outputs
| Name | Type | Description |
| ------- | ---- | ------------------------------------------- |
| success | bool | true if the redelegate is successfully done |
### `delegation`
Returns the delegation information between a delegator and a validator.
If there is no delegation found, the `shares` and `balance` will be `0`.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------- |
| delegatorAddress | address | the address of the delegator |
| validatorAddress | address | the address of the validator |
#### Outputs
| Name | Type | Description |
| ------- | ------- | --------------------------------------- |
| shares | uint256 | the delegated shares |
| balance | Coin | the amount of delegated token and denom |
`Coin` is a struct with the following fields:
| Name | Type | Description |
| ------ | ------- | ------------------------ |
| denom | string | the denom of the reward |
| amount | uint256 | the amount of the reward |
### `unbondingDelegation`
Returns the unbonding delegation information between a delegator and a validator.
If there is no unbonding delegation found, empty `UnbondingDelegationOutput` will be returned.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------- |
| delegatorAddress | address | the address of the delegator |
| validatorAddress | address | the address of the validator |
#### Outputs
| Name | Type | Description |
| ------------------- | ------------------------- | ------------------------------------------- |
| unbondingDelegation | UnbondingDelegationOutput | the information of the unbonding delegation |
`UnbondingDelegationOutput` is a struct with the following fields:
| Name | Type | Description |
| ---------------- | --------------------------- | --------------------------------------- |
| validatorAddress | address | the address of the validator |
| delegatorAddress | address | the address of the delegator |
| entries | UnbondingDelegationEntry\[] | the entries of the unbonding delegation |
`UnbondingDelegationEntry` is a struct with the following fields:
| Name | Type | Description |
| -------------- | ------ | -------------------------------- |
| creationHeight | uint64 | the creation height of the entry |
| completionTime | uint64 | the completion time of the entry |
| initialBalance | Coin | the initial balance of the entry |
| balance | Coin | the balance of the entry |
### `validator`
Returns the validator information.
If there is no validator found, empty `ValidatorOutput` will be returned.
#### Inputs
| Name | Type | Description |
| ---------------- | ------- | ---------------------------- |
| validatorAddress | address | the address of the validator |
#### Outputs
| Name | Type | Description |
| --------- | --------- | -------------------------------- |
| validator | Validator | the information of the validator |
`Validator` is a struct with the following fields:
| Name | Type | Description |
| ----------------- | ------- | ------------------------------------------------------------------ |
| operatorAddress | address | the address of the validator |
| consensusPubkey | string | the public key of the validator |
| jailed | bool | whether the validator is jailed or not |
| status | int32 | the status of the validator |
| tokens | uint256 | the amount of the staking token delegated to the validator |
| delegatorShares | uint256 | the amount of the delegation shares |
| description | string | the description of the validator |
| unbondingHeight | int64 | the height at which the validator is unbonding |
| unbondingTime | int64 | the time at which the validator is unbonding |
| commission | uint256 | the commission rate of the staking token rewarded by the validator |
| minSelfDelegation | uint256 | the minimum self-delegation amount of the validator |
### `validators`
Returns all validators matched with the status.
If there is no validator found, empty `ValidatorsOutput` will be returned.
Status, declared in `x/staking` module, can be one of the following:
* 0 : "BOND\_STATUS\_UNSPECIFIED", unspecified status
* 1 : "BOND\_STATUS\_UNBONDING", validator is unbonding
* 2 : "BOND\_STATUS\_UNBONDED", validator is unbonded
* 3 : "BOND\_STATUS\_BONDED", validator is bonded
#### Inputs
| Name | Type | Description |
| ----------- | ------- | --------------------------- |
| status | string | the status of the validator |
| pageRequest | PageReq | request for pagination |
`PageReq` is a struct with the following fields:
| Name | Type | Description |
| ---------- | ----- | --------------------------------------------------- |
| key | bytes | the key of the page |
| offset | int64 | the offset of the page |
| limit | int64 | the limit of the page |
| countTotal | bool | whether to count the total number of results or not |
| reverse | bool | whether to reverse the results or not |
#### Outputs
| Name | Type | Description |
| ------------ | ------------ | ---------------------------- |
| validators | Validator\[] | the arrays of the validators |
| pageResponse | PageResp | response for pagination |
`PageResp` is a struct with the following fields:
| Name | Type | Description |
| ------- | ------ | ------------------------------- |
| nextKey | bytes | the next key of the page |
| total | uint64 | the total number of the results |
### `redelegation`
Returns the redelegation information of delegator, source validator and destination validator.
If there is no redelegation found, empty `RedelegationOutput` will be returned.
#### Inputs
| Name | Type | Description |
| ------------------- | ------- | ---------------------------------------- |
| delegatorAddress | address | the address of the delegator |
| srcValidatorAddress | address | the address of the source validator |
| dstValidatorAddress | address | the address of the destination validator |
#### Outputs
| Name | Type | Description |
| ------------ | ------------------ | ----------------------------------- |
| redelegation | RedelegationOutput | the information of the redelegation |
`RedelegationOutput` is a struct with the following fields:
| Name | Type | Description |
| ------------------- | -------------------- | ---------------------------------------- |
| delegatorAddress | address | the address of the delegator |
| validatorSrcAddress | address | the address of the source validator |
| validatorDstAddress | address | the address of the destination validator |
| entries | RedelegationEntry\[] | the entries of the redelegation |
`RedelegationEntry` is a struct with the following fields:
| Name | Type | Description |
| -------------- | ------ | -------------------------------- |
| creationHeight | uint64 | the creation height of the entry |
| completionTime | uint64 | the completion time of the entry |
| initialBalance | Coin | the initial balance of the entry |
| balance | Coin | the balance of the entry |
### `redelegations`
Returns all redelegations of delegator, source validator and destination validator.
If there is no redelegation found, empty `RedelegationResponse` and `PageResp` will be returned.
#### Inputs
| Name | Type | Description |
| ------------------- | ------- | ---------------------------------------- |
| delegatorAddress | address | the address of the delegator |
| srcValidatorAddress | address | the address of the source validator |
| dstValidatorAddress | address | the address of the destination validator |
| pageRequest | PageReq | request for pagination |
#### Outputs
| Name | Type | Description |
| ------------ | ----------------------- | ------------------------------------ |
| response | RedelegationResponse\[] | the information of the redelegations |
| pageResponse | PageResp | response for pagination |
## Events
### CreateValidator
| Name | Type | Indexed | Description |
| -------- | ------- | ------- | ------------------------------------------------------------------------- |
| valiAddr | address | Y | the address of the validator |
| value | uint256 | N | the amount of the staking token initially self-delegated to the validator |
### EditValidator
| Name | Type | Indexed | Description |
| ----------------- | ------- | ------- | -------------------------------------------------------------------------- |
| valiAddr | address | Y | the address of the validator |
| commissionRate | int256 | N | the updated commission rate of the staking token rewarded by the validator |
| minSelfDelegation | int256 | N | the updated minimum self-delegation amount of the validator |
### Delegate
| Name | Type | Indexed | Description |
| ------------- | ------- | ------- | ---------------------------------------------------------- |
| delegatorAddr | address | Y | the address of the delegator |
| validatorAddr | string | Y | the address of the validator |
| amount | uint256 | N | the amount of the staking token delegated to the validator |
| newShares | uint256 | N | the amount of the delegation shares after the delegation |
### Unbond
| Name | Type | Indexed | Description |
| -------------- | ------- | ------- | -------------------------------------------------------------- |
| delegatorAddr | address | Y | the address of the delegator |
| validatorAddr | string | Y | the address of the validator |
| amount | uint256 | N | the amount of the staking token undelegated from the validator |
| completionTime | uint256 | N | the completion time of the undelegation |
### Redelegate
| Name | Type | Indexed | Description |
| ------------------- | ------- | ------- | ------------------------------------------------ |
| delegatorAddr | address | Y | the address of the delegator |
| validatorSrcAddress | address | Y | the address of the source validator |
| validatorDstAddress | address | Y | the address of the destination validator |
| amount | uint256 | N | the amount of the staking token for redelegation |
| completionTime | uint256 | N | the completion time of the redelegate |
# Set up recurring billing
Source: https://docs.stable.xyz/en/reference/subscriptions
Pull-based subscription billing on Stable using EIP-7702 account abstraction for automated recurring USDT0 collection.
Pull-based subscriptions let a service provider collect payments on a schedule without requiring the subscriber to initiate each payment.
This pattern is enabled by [EIP-7702](/en/reference/eip-7702-api) account abstraction. The subscriber's EOA delegates execution authority to a subscription delegate contract, which the provider calls each billing cycle.
The subscriber acts only twice: once to subscribe, once to cancel.
## How it works
The subscriber delegates their EOA to a contract that enforces billing terms. Through EIP-7702, the subscriber's account temporarily gains contract logic, allowing a service provider to collect payments at each billing cycle without the subscriber signing every time.
### Subscription lifecycle
1. **Delegate**: the subscriber delegates their EOA to a subscription delegate contract via EIP-7702.
2. **Subscribe**: the subscriber registers billing terms: provider address, amount per interval, and billing interval.
3. **Collect**: the service provider triggers collection each billing cycle. The delegate contract verifies the caller, interval, and amount before executing the USDT0 transfer.
4. **Cancel**: the subscriber revokes the subscription or clears the delegation to stop future collections.
### Important considerations
* **Persistent delegation**: The EIP-7702 delegation persists until the subscriber explicitly changes or clears it. No re-delegation is needed each billing cycle.
* **Single delegation per EOA**: EIP-7702 supports one active delegation per EOA. If the subscriber later delegates to a different contract, the subscription delegate logic is replaced and collection fails. Use a modular delegate contract that supports multiple functions (subscriptions, batch payments, spending limits) under a single delegation.
* **Use audited delegates**: A delegate contract has full execution authority over the subscriber's EOA. Only delegate to contracts that have been audited.
## What makes it different
Traditional subscriptions store card data, retry failed charges, and manage complex billing state. With EIP-7702 subscriptions, the billing terms are enforced by the delegate logic on the subscriber's own EOA. The provider can only collect the agreed amount per interval, and the subscriber can cancel at any time by revoking the delegation.
| **Aspect** | **Traditional (card-on-file)** | **Stable** |
| :------------------ | :---------------------------------------- | :-------------------------------------- |
| Setup | Card registration with payment processor | Single EIP-7702 delegation transaction |
| Billing | Processor charges stored card | Provider calls delegate contract |
| Stored payment data | Card number, CVV held by processor | No payment credentials stored off-chain |
| Cancellation | Contact provider or card issuer | Subscriber revokes delegation on-chain |
| Overcharge risk | Depends on provider-side billing controls | Billing terms enforced by contract |
**See also:**
* [EIP-7702](/en/reference/eip-7702-api)
* [ERC-3009 (Transfer With Authorization)](/en/explanation/erc-3009)
# System modules reference
Source: https://docs.stable.xyz/en/reference/system-modules-api-overview
Bank, distribution, and staking precompile references: method signatures, ABIs, events, and authorization.
Stable exposes core settlement behavior through **System Modules**, implemented as **precompiled contracts** for gas efficiency and predictable control.
**Concept:** For what system modules do and why they're exposed as precompiles, see [System modules](/en/explanation/system-modules-overview).
**Key modules:**
* [Bank Module](/en/reference/bank-module-api)
* Handles USDT transfers, balance accounting, and escrow flows
* [Distribution Module](/en/reference/distribution-module-api)
* Fee distribution and reward logic for network participants
* [Staking Module](/en/reference/staking-module-api)
* Controls validator participation and staking (coming with mainnet)
**dApps can leverage built-in modules instead of re-implementing token or settlement logic.**
# System transactions reference
Source: https://docs.stable.xyz/en/reference/system-transactions-api
StableSystem precompile reference: interface, sender authorization, batch limits, and gas accounting.
**Concept:** For how system transactions bridge SDK events to the EVM and why this matters, see [System transactions](/en/explanation/system-transactions).
## Abstract
System transactions provide a way for the Stable protocol to emit EVM events for Stable SDK operations. When staking events like unbonding completions occur in the SDK layer, the protocol automatically generates EVM transactions that emit corresponding events. This makes these operations fully visible to EVM tooling and applications.
## Motivation
You and your applications on Stable expect to monitor blockchain events through standard EVM interfaces like `eth_getLogs`. But critical operations happen in Stable SDK modules that don't naturally emit EVM events. This creates a visibility gap: EVM dApps can't easily track when a user's tokens finish unbonding.
System transactions bridge this gap. When the staking module completes an unbonding operation, Stable's x/stable module detects the event and generates a system transaction that calls the StableSystem precompile ( `0x0000000000000000000000000000000000009999`). And then, the precompile emits proper EVM events that any dApp can subscribe to. System transactions run with a special sender address (`0x0000000000000000000000000000000000000001`) that only the protocol can use. This prevents anyone from spoofing protocol events while keeping the event emission trustless and verifiable on-chain.
## Specification
System transactions work through three main components: the x/stable module's EndBlocker, the PrepareProposal handler, and the StableSystem precompile.
### Architecture overview
### StableSystem precompile
The StableSystem precompile lives at `0x0000000000000000000000000000000000009999` and handles protocol-level operations that need to emit EVM events. Currently it supports unbonding completion notifications.
```solidity theme={"dark"}
interface IStableSystem {
/// @notice Processes queued unbonding completions and emits EVM events
/// @param blockHeight The block height at which to process completions
/// @dev Only callable by system transactions (from = 0x1)
/// @dev Processes up to 100 completions per call
/// @dev Automatically deletes processed completions from the queue
function notifyUnbondingCompletions(int64 blockHeight) external;
/// @notice Emitted when an unbonding operation completes
/// @param delegator The address that delegated the tokens
/// @param validator The validator address the tokens were delegated to
/// @param amount The amount of tokens that finished unbonding (in uusdc)
event UnbondingCompleted(
address indexed delegator,
address indexed validator,
uint256 amount
);
/// @notice The caller is not authorized (not system transaction sender)
error Unauthorized();
}
```
### System transaction sender
System transactions use `0x0000000000000000000000000000000000000001` as the sender address. This address:
* Requires no signature verification
* Can only be used by transactions created in PrepareProposal
* Cannot be spoofed by users or contracts
* Skips fee deduction via the SystemTxDecorator ante handler
The EVM recognizes system transactions by checking `msg.sender == 0x1`. Precompiles can use this to gate protocol-only operations.
### Event-driven flow
When a user's unbonding period completes, here's what happens:
1. **Stable SDK Layer:** The staking module's EndBlocker completes the unbonding and emits EventTypeCompleteUnbonding with the delegator address, validator address, and amount.
2. **Detection:** The x/stable module's EndBlocker runs after staking and scans for unbonding events in the block's event log. For each completion, it queues an entry in state with the delegator address, validator address, amount, and block height.
3. **System TX Generation**: In the next block's PrepareProposal, the app queries all queued completions. If any exist, it creates a system transaction calling StableSystem.notifyUnbondingCompletions(blockHeight) with the current block height. This transaction goes at the front of the block, before any user transactions.
4. **Execution:** During block execution, the system transaction runs first. The precompile queries state for queued completions at that block height, emits an UnbondingCompleted event for each one (up to 100), and deletes them from the queue.
5. **EVM Visibility:** The events appear in transaction receipts and logs, visible to eth\_getLogs queries, block explorers, and any application monitoring the StableSystem precompile.
### Batch processing
To prevent blocks from becoming too large, the system processes at most 100 unbonding completions per block. If 150 completions queue up:
* Block N: Creates system tx processing completions 0-99
* Block N+1: Creates system tx processing completions 100-149
The precompile queries state directly rather than receiving completion data in calldata. This keeps transaction size predictable and moves the data from expensive calldata to cheaper state reads.
## Usage examples
The most common use case is a staking dashboard that needs to notify users when their unbonding periods complete. Here's how to set up a listener for unbonding completions.
```javascript theme={"dark"}
import { ethers } from 'ethers';
// StableSystem precompile address
const STABLE_SYSTEM_ADDRESS = '0x0000000000000000000000000000000000009999';
// ABI for the UnbondingCompleted event
const STABLE_SYSTEM_ABI = [
'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)'
];
// Connect to the Stable network
const provider = new ethers.JsonRpcProvider('https://rpc.testnet.stable.xyz');
const stableSystem = new ethers.Contract(
STABLE_SYSTEM_ADDRESS,
STABLE_SYSTEM_ABI,
provider
);
// Subscribe to all unbonding completions
stableSystem.on('UnbondingCompleted', (delegator, validator, amount, event) => {
console.log('Unbonding completed!');
console.log('Delegator:', delegator);
console.log('Validator:', validator);
console.log('Amount:', ethers.formatEther(amount), 'tokens');
console.log('Block:', event.log.blockNumber);
console.log('Tx Hash:', event.log.transactionHash);
});
```
This listener fires every time any user's unbonding completes. For a production dApp, filter events for specific users as shown below.
### Filtering events for specific users
To only receive events for a particular delegator address, use the indexed event parameters to create a filter:
```javascript theme={"dark"}
// Only watch unbondings for a specific user
const userAddress = '0xabcd...';
const filter = stableSystem.filters.UnbondingCompleted(userAddress);
stableSystem.on(filter, (delegator, validator, amount, event) => {
// This only fires for the specified user's unbondings
showNotification(`Your unbonding of ${ethers.formatEther(amount)} tokens completed!`);
refreshUserBalance(userAddress);
});
```
You can also filter by validator if you're building a validator-specific dashboard:
```javascript theme={"dark"}
// Watch all unbondings from a specific validator
const validatorAddress = '0x1234...';
const validatorFilter = stableSystem.filters.UnbondingCompleted(null, validatorAddress);
stableSystem.on(validatorFilter, (delegator, validator, amount) => {
updateValidatorStats(validator, amount);
});
```
### Querying historical events
If your dApp needs to show a history of past unbonding completions, you can query historical events using event filters with block ranges:
```javascript theme={"dark"}
// Get all unbondings for a user in the last 1000 blocks
const currentBlock = await provider.getBlockNumber();
const filter = stableSystem.filters.UnbondingCompleted(userAddress);
const events = await stableSystem.queryFilter(
filter,
currentBlock - 1000,
currentBlock
);
const unbondingHistory = events.map(event => ({
delegator: event.args.delegator,
validator: event.args.validator,
amount: ethers.formatEther(event.args.amount),
blockNumber: event.blockNumber,
txHash: event.transactionHash
}));
console.log('Recent unbondings:', unbondingHistory);
```
## Integration guide
### Step 1: Add the Stable System contract interface
First, add the StableSystem precompile interface to your project. If you're using Foundry or Hardhat, create a new interface file:
```solidity theme={"dark"}
interface IStableSystem {
event UnbondingCompleted(
address indexed delegator,
address indexed validator,
uint256 amount
);
}
```
If you're building a pure frontend dApp without Solidity contracts, you just need the ABI fragment for the event:
```javascript theme={"dark"}
const STABLE_SYSTEM_ABI = [
'event UnbondingCompleted(address indexed delegator, address indexed validator, uint256 amount)'
];
```
### Step 2: Set up event listeners
Initialize your ethers.js provider and create a contract instance pointing to the StableSystem precompile address. The precompile is always deployed at `0x00000000000....0000009999` on both Stable Testnet and Stable Mainnet.
*Note: The precompile is not deployed on Stable Mainnet yet, it will be provided after v1.2.0 upgrade.*
```javascript theme={"dark"}
const provider = new ethers.JsonRpcProvider(RPC_URL);
const stableSystem = new ethers.Contract(
'0x0000000000000000000000000000000000009999',
STABLE_SYSTEM_ABI,
provider
);
```
### Step 3: Handle events in your application logic
Subscribe to events and update your application state accordingly. Common patterns include:
* **Balance Updates**: When an unbonding completes, refresh the user's token balance
* **Notification System**: Show toast notifications when the user's unbondings complete
* **Dashboard Statistics**: Update staking metrics and charts in real-time
* **Transaction History**: Add completed unbondings to the user's activity feed
### Step 4: Handle connection issues
Since event subscriptions rely on persistent websocket connections, implement reconnection logic for production dApps:
```javascript theme={"dark"}
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
function setupEventListener() {
const provider = new ethers.WebSocketProvider('wss://rpc.testnet.stable.xyz');
provider.on('error', (error) => {
console.error('Provider error:', error);
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
setTimeout(() => setupEventListener(), 5000);
}
});
const stableSystem = new ethers.Contract(
'0x0000000000000000000000000000000000009999',
STABLE_SYSTEM_ABI,
provider
);
stableSystem.on('UnbondingCompleted', handleUnbonding);
}
```
## Why this approach?
### Compared to custom indexers
Previously, Stable SDK required you to run custom indexers that watch for SDK events and store them in a database. This adds operational overhead and introduces potential points of failure.
With system transactions, there's no need for separate indexer infrastructure. Events are natively available through the EVM's log system, which every RPC node already indexes and serves. Any standard web3 library can subscribe to these events without additional tooling.
### Compared to polling SDK endpoints
Without system transactions, EVM dApps would need to periodically call Stable SDK REST endpoints to check if unbonding periods have completed. This creates several problems:
* **Increased latency**: Polling intervals of 5-10 seconds mean users might wait that long before seeing updates
* **Higher load**: Every dApp instance polling endpoints increases load on RPC infrastructure
* **Complexity**: dApps need to handle both web3 providers (for EVM interactions) and Stable SDK REST clients (for SDK queries)
* **No real-time updates**: Polling inherently can't provide instant notifications
System transactions provide real-time event notifications through the same websocket connections dApps already use for EVM interactions. This simplifies the developer experience and reduces infrastructure costs.
## Security guarantees
### Trustless event emission
System transactions are created during the `PrepareProposal` ABCI phase, which only validators can execute. User-submitted transactions cannot spoof the system sender address (`0x1`). The EVM's state transition logic enforces that only transactions to the StableSystem precompile address can skip signature verification.
This means:
* Users cannot forge unbonding completion events
* Users cannot call `notifyUnbondingCompletions` from their own transactions
* The only way to emit an `UnbondingCompleted` event is for an actual unbonding to complete in the Stable SDK staking module
### No additional trust assumptions
System transactions don't introduce new security assumptions beyond what's already required for blockchain consensus. If you trust that validators are correctly executing blocks, you can trust that system transaction events accurately reflect Stable SDK state changes.
The event emission process is deterministic: given the same SDK events in `EndBlock`, all honest validators will produce identical system transactions during `PrepareProposal`. The consensus mechanism ensures validators agree on which system transactions to include.
### Block finality
The Stable blockchain uses fast finality through StableBFT's consensus mechanism. Once a block is committed, it's immediately final and cannot be reorganized. This means that once you receive an `UnbondingCompleted` event, you can trust it's permanent.
There's no need to wait for multiple confirmations like on probabilistic finality chains. dApps can update user balances and display notifications immediately upon receiving the event.
## Performance & limitations
### Batch size constraints
Each block processes at most 100 unbonding completions through system transactions. This limit exists to prevent unbounded block sizes during periods of high unbonding activity.
In practice, 100 completions per block provides throughput of \~9000 completions per minute assuming the average block time of 0.7 seconds. Normal staking activity rarely reaches this limit. During exceptional circumstances, completions might queue for several blocks before fully processing.
### Gas consumption
System transactions consume gas during execution, which is accounted for in the block's gas limit. The gas cost scales linearly with the number of completions being processed:
* Base function call: \~21,000 gas
* Per-event emission: \~3,000 gas
* Reading state: \~2,000 gas per completion
A full batch of 100 completions consumes approximately 521,000 gas. As Stable’s block gas limit is 100,000,000, this represents less than 0.6% of available block space.
### Notification latency
When an unbonding period completes during block N:
1. The Stable module's `EndBlock` queues the completion in block N's state
2. Block N+1's `PrepareProposal` creates a system transaction
3. The system transaction executes during block N+1, emitting the event
This means there's a one-block delay (approximately 0.7 seconds) between the unbonding completing and the EVM event being emitted. For most use cases, this latency is acceptable since the unbonding period itself is 7 days.
### High load scenarios
If unbonding completions arrive faster than 100 per block, they accumulate in the queue. The queue is processed in FIFO order, so the oldest completions are always notified first.
During sustained high load, the queue could grow temporarily. However, once the spike subsides, subsequent blocks with fewer completions will gradually drain the queue. The system is designed to handle bursts without dropping events.
## Future extensions
The system transaction mechanism provides a general pattern for bridging any Stable SDK operation into the EVM event space. While currently used only for unbonding completions, the architecture can be extended to cover additional use cases:
### Staking operations
Beyond unbonding, other staking events could emit EVM notifications:
* Commission rate changes by validators
* Validator jailing and unjailing
### Governance execution
When governance proposals pass and execute, system transactions could emit events with proposal IDs and execution results. This would allow dApps to react to parameter changes or upgrades without polling the governance module.
### Generic event bridge
The pattern could be generalized into a configurable event bridge where each module registers which SDK events should be mirrored to the EVM. This would provide comprehensive visibility into all Stable SDK operations without requiring per-module custom logic. The key architectural principle is that system transactions remain a protocol-level feature, created only by validators during block proposal.
# Ecosystem
Source: https://docs.stable.xyz/en/reference/testnet-ecosystem
Contract addresses for LayerZero, USDT0, and Chaos Labs price feeds deployed on Stable Testnet.
In this document, you can find the information for bridge (LayerZero), USDT0, and price feeds (oracle provided by Chaos Labs).
## LayerZero on Stable Testnet
| Name | Value |
| ----------------- | ------------------------------------------ |
| eid | 40374 |
| chainKey | stable-testnet |
| stage | testnet |
| endpointV2View | 0x6Ac7bdc07A0583A362F1497252872AE6c0A5F5B8 |
| endpointV2 | 0x3aCAAf60502791D199a5a5F0B173D78229eBFe32 |
| sendUln302 | 0x9eCf72299027e8AeFee5DC5351D6d92294F46d2b |
| receiveUln302 | 0xB0487596a0B62D1A71D0C33294bd6eB635Fc6B09 |
| blockedMessageLib | 0xa229b65cc2191bf60bc24efcda3487d7b5c0c9f0 |
| executor | 0x701f3927871EfcEa1235dB722f9E608aE120d243 |
| deadDVN | 0xC1868e054425D378095A003EcbA3823a5D0135C9 |
## USDT0 on Stable Testnet
| Name | Value |
| ----------- | ------------------------------------------ |
| wrapper | 0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb |
| composer | 0xe7cd86e13AC4309349F30B3435a9d337750fC82D |
| OFT | 0x779Ded0c9e1022225f8E0630b35a9b54bE713736 |
| USDT0 impl | 0x3f9E27457ac494fC729beB50e6af04Ec34e3828E |
| USDT0 proxy | 0x78Cf24370174180738C5B8E352B6D14c83a6c9A9 |
## Sepolia OFT contract and USDT0 contract (for reference)
| Name | Value |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Sepolia OFT | [https://sepolia.etherscan.io/address/0xc099cd946d5efcc35a99d64e808c1430cef08126](https://sepolia.etherscan.io/address/0xc099cd946d5efcc35a99d64e808c1430cef08126) |
| Sepolia USDT | [https://sepolia.etherscan.io/address/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract](https://sepolia.etherscan.io/address/0xc4DCC311c028e341fd8602D8eB89c5de94625927#writeContract) |
## Price feed information
| Price Type | Contract Address |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **BTC / USD** | [0xECA49340544541957eC64B7635418D2159616826](https://testnet.stablescan.xyz/address/0xECA49340544541957eC64B7635418D2159616826?tab=read_write_proxy) |
| **ETH / USD** | [0x176A9536feaC0340de9f9811f5272E39E80b424f](https://testnet.stablescan.xyz/address/0x176A9536feaC0340de9f9811f5272E39E80b424f?tab=read_write_proxy) |
| **SOL / USD** | [0x7fa367967CE7903Fc5cE25a969cb7dB792a8f6b9](https://testnet.stablescan.xyz/address/0x7fa367967CE7903Fc5cE25a969cb7dB792a8f6b9?tab=read_write_proxy) |
# Testnet information
Source: https://docs.stable.xyz/en/reference/testnet-information
Chain ID, RPC endpoints, faucet, and block explorer links for the Stable Testnet network.
Everything you need to know to access the Stable Testnet.
## Network overview
| Configuration | Value |
| ---------------- | -------------- |
| **Network Name** | Stable Testnet |
| **Chain ID** | `2201` |
| **Gas Token** | USDT0 |
| **Gov Token** | STABLE |
| **Block Time** | \~0.7 seconds |
## Block explorers
| Explorer | URL |
| -------------- | ---------------------------------------------------------------- |
| **Stablescan** | [https://testnet.stablescan.xyz](https://testnet.stablescan.xyz) |
## RPC endpoints
### Primary endpoints
| Type | Endpoint | Purpose |
| ---------------- | ---------------------------------------------------------------- | ----------------- |
| **EVM JSON-RPC** | [https://rpc.testnet.stable.xyz](https://rpc.testnet.stable.xyz) | EVM transactions |
| **WebSocket** | wss\://rpc.testnet.stable.xyz | Real-time updates |
## Chain information
| Parameter | EVM |
| ------------------ | ------- |
| **Chain ID** | `2201` |
| **Address Format** | `0x...` |
| **Gas Token** | `USDT0` |
| **Decimals** | 18 |
## Faucet & tools
| Tool | URL | Description |
| ------------- | --------------------------------------------------------- | --------------- |
| **Faucet** | [https://faucet.stable.xyz](https://faucet.stable.xyz) | Get test tokens |
| **Snapshots** | See [Node Operators Guide](/en/how-to/use-node-snapshots) | Chain snapshots |
# Version history
Source: https://docs.stable.xyz/en/reference/testnet-version-history
Version history and upgrade schedule for the Stable Testnet network.
Complete version history and related documentation for the Stable Testnet.
## Current version information
* **Current Version**: `v1.3.0-rc0`
* **Next Upgrade**: `TBD`
* **Upgrade Height**: `TBD`
* **Expected Time**: `TBD`
## Version history
### Current & previous versions
| Version | Commit | Upgrade Height | Binary | Status |
| -------------- | ---------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| **v1.3.0-rc0** | `864d54c` | 49,855,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.3.0-rc0-linux-arm64-testnet.tar.gz) | Current |
| **v1.2.2-rc0** | `8bd5d5e` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.2-rc0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.2-rc0-linux-arm64-testnet.tar.gz) | |
| **v1.2.1-rc1** | `7ff9a8a` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.1-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.1-rc1-linux-arm64-testnet.tar.gz) | |
| **v1.2.0-rc1** | `263c033` | 41,306,450 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-arm64-testnet.tar.gz) | |
| **v1.2.0** | `ee8ca35` | 40,392,500 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-linux-arm64-testnet.tar.gz) | |
| **v1.1.2** | `3d83aa3` | 34,649,300 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.2-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.2-linux-arm64-testnet.tar.gz) | |
| **v1.1.1** | `8becd6b` | 33,152,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.1-linux-arm64-testnet.tar.gz) | |
| **v1.1.0** | `17ceaa7` | 32,309,700 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.1.0-linux-arm64-testnet.tar.gz) | |
| **v0.8.1** | `1eb65d5` | 30,770,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.1-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.1-linux-arm64-testnet.tar.gz) | |
| **v0.8.0** | `e55efb6` | 29,410,999 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.0-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.0-linux-arm64-testnet.tar.gz) | Bank precompile enhancements |
| **v0.7.2** | `3c53e14` | 27,258,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-amd64-testnet.tar.gz) / [ARM64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-arm64-testnet.tar.gz) | StableBFT integration |
| **v0.6.0** | `5cc1ad6` | 19,587,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.6.0-linux-amd64-testnet.tar.gz) | Minor fixes |
| **v0.5.0** | `919281d` | 18,719,000 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.5.0-linux-amd64-testnet.tar.gz) | Minor fixes |
| **v0.4.0** | `c6240c0` | 18,666,150 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.4.0-linux-amd64-testnet.tar.gz) | Stable SDK v0.53.4 |
| **v0.3.0** | `a4f5ac5` | 9,166,131 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.3.0-linux-amd64-testnet.tar.gz) | EVM value transfer allow list |
| **v0.2.1** | `53e6e073` | - | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.2.1-linux-amd64-testnet.tar.gz) | Non-breaking update |
| **v0.2.0** | `8bdd771` | 8,956,584 | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.2.0-linux-amd64-testnet.tar.gz) | Feature update |
| **v0.1.0** | `10dfg542` | Genesis | [AMD64](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.1.0-linux-amd64-testnet.tar.gz) | Genesis (2025-04-07) |
## Related documentation
* [Upgrade Guide](/en/how-to/upgrade-node) - Step-by-step upgrade procedures
* [Testnet Information](/en/reference/testnet-information) - Current network details
# Tokenomics
Source: https://docs.stable.xyz/en/reference/tokenomics
Supply, distribution, and economic design of the STABLE token
STABLE is the governance token of the Stable Mainnet. It secures the network through delegated Proof-of-Stake, governs protocol upgrades, and entitles stakers to a share of USDT0 gas revenue distributed by validators.
## Overview
| Item | Details |
| :--------------- | :----------------------------- |
| **Symbol** | STABLE |
| **Total supply** | 100,000,000,000 tokens |
| **Standard** | ERC-20 (on Stable Mainnet EVM) |
| **Decimals** | 18 |
STABLE is the governance token of the Stable Mainnet and Ecosystem, designed to support long-term economic alignment across validators, developers, and users.
***
## Token allocation
**Total supply:** 100,000,000,000 STABLE tokens
| Category | Allocation | Amount of STABLE |
| :------------------------ | :--------- | :--------------- |
| **Investors & advisors** | 25% | 25,000,000,000 |
| **Team** | 25% | 25,000,000,000 |
| **Ecosystem & community** | 40% | 40,000,000,000 |
| **Genesis distribution** | 10% | 10,000,000,000 |
| **Total** | 100% | 100,000,000,000 |
***
## Emission model & supply schedule
* Total supply is fixed at 100,000,000,000 STABLE tokens.
* Only a portion of supply enters circulation at launch of the Stable Mainnet.
* Team and Investors & advisors allocations follow a 4-year linear vesting model, with a 1-year cliff, to ensure long-term commitment.
***
## Allocations
### Genesis distribution - 10% of total token supply
* Designed to bootstrap usage, provide liquidity to market, conduct airdrop events, reward early supporters and campaigns with exchanges and ecosystem partners.
**Vesting schedule**
* 100% unlocked at the Stable Mainnet launch
### Ecosystem & community - 40% of total token supply
Supports long-term ecosystem and community growth:
* Support the development of the Stable software and ecosystem
* Developer grants
* User onboarding incentives
* Payment partner integrations
* On-chain activity rewards
* Hackathons, ambassador programs
* Infrastructure grants
**Vesting schedule**
* **Initial unlock:** 8% of total supply unlocked at the Stable Mainnet launch. These tokens fund incentives for strategic launch partners, liquidity needs, and early ecosystem growth campaigns.
* **Total vesting period:** 3-year linear vesting thereafter for the 32% of total supply
### Team - 25% of total token supply
* Allocated to founding team members, engineers, researchers, and contributors
* Designed to ensure long-term alignment between the team and the Stable ecosystem.
**Vesting schedule**
* **1-year cliff:** No tokens are unlocked in the first 12 months
* **Total vesting period:** 48 months linear vesting from the Stable Mainnet launch
### Investors & advisors - 25% of total token supply
* Allocated for fundraising rounds and advisory support.
**Vesting schedule**
* **1-year cliff:** No tokens are unlocked in the first 12 months
* **Total vesting period:** 48 months linear vesting from the Stable Mainnet launch
***
## Emissions chart
***
## Economic design principles
Stable's token economics were designed around three foundational goals:
### 1. Power a payments-optimized Layer 1
The STABLE token incentivizes high-throughput, low-latency infrastructure, supporting sub-second block confirmations and enterprise settlement guarantees.
### 2. Support sustainable ecosystem growth
40% of total token supply is dedicated to long-term growth, focusing on key development and growth areas.
* Developer grants
* Partner integrations
* New ecosystem applications
### 3. Align long-term contributors via vesting
The team allocation uses a 4-year linear vesting model, with a 1-year cliff, ensuring long-term alignment and continued contributions toward network development.
***
## Utility of the STABLE token
The STABLE Token is an ERC-20 governance token on the Stable Mainnet. It can be used for:
* Electing validators
* Voting on protocol upgrades
* Handling governance proposals
* Serving as a credential to receive gas fee distribution from validators
On the Stable network, all transactions use USDT0 as the native gas token. These USDT0 gas fees are collected into a treasury managed by smart contracts. When token holders stake their STABLE tokens to validators, validators may choose to distribute gas fees from the treasury proportionally to stakers.
# Wallets
Source: https://docs.stable.xyz/en/reference/wallets
Wallet providers and SDKs for holding, sending, and receiving USDT on Stable.
## Wallets overview table
| **Provider** | **Category** | **Security Method** | **Docs / Get Started** | **Notes** |
| :----------------------------------------------------------------- | :---------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- |
| Stable Pay | User Wallet | TSS-MPC-based self-custody | [https://blog.stable.xyz/introducing-stable-pay-the-stablecoin-payment-wallet-on-stablechain](https://blog.stable.xyz/introducing-stable-pay-the-stablecoin-payment-wallet-on-stablechain) | USDT-native payment wallet built on Stable; optimized for instant transfers |
| [Wallet Development Kit by Tether (WDK)](https://wallet.tether.io) | Wallet SDK | Self-custody | [https://docs.wallet.tether.io](https://docs.wallet.tether.io) | Tether's open-source SDK for building self-custodial wallets across multi-chain |
| [Binance Wallet](https://www.binance.com/en/web3wallet) | User Wallet | MPC-based self-custody / semi-custody wallet | [https://developers.binance.com/docs/binance-spot-api-docs/README](https://developers.binance.com/docs/binance-spot-api-docs/README) | Multi-chain wallet, supports Stable USDT |
| [Reown](https://reown.com/) (formerly WalletConnect / WalletKit) | Connectivity / Wallet Infra | Protocol-level signing & secure relay | [https://docs.reown.com/appkit/overview](https://docs.reown.com/appkit/overview) | Supports 600+ wallets, multi-chain, SDK-based integration, ideal for embedded wallet flows |
| [Bitget Wallet](https://web3.bitget.com/en) | User Wallet | Non-custodial wallet (private keys managed by user) | [https://web3.bitget.com/en/docs/](https://web3.bitget.com/en/docs/) | Built-in dApp browser; multi-asset & multi-chain support |
| [Gate Wallet](https://www.gate.com/) (Gate Onchain) | User Wallet | Exchange-linked wallet | [https://www.gate.com/](https://www.gate.com/) | Exchange-linked wallet; suitable for CEX ↔ wallet flows |
| [OKX Wallet](https://web3.okx.com/) (OKX Onchain) | User Wallet | Non-custodial / MPC for recovery | [https://www.okx.com/earn/onchain-earn](https://www.okx.com/earn/onchain-earn) | Multi-chain wallet with exchange integration |
| [Anchorage](https://www.anchorage.com/) | Custodial / Institutional Wallet | Bank-grade regulated custody (federally chartered bank) | [https://www.anchorage.com/who-we-serve](https://www.anchorage.com/who-we-serve) | Institutional-grade custody for stable assets |
| [Dynamic](https://www.dynamic.xyz/) | Embedded / In-App Wallet Infra | Managed-key / custody-infra via SDK or backend | [https://www.dynamic.xyz/docs/introduction/welcome](https://www.dynamic.xyz/docs/introduction/welcome) | Enables apps to embed wallet flows without external wallets |
| [Alchemy](https://www.alchemy.com/) | Smart Wallets & Account Abstraction Infra | Bundler + Paymaster infrastructure (ERC-4337) | [https://docs.alchemy.com](https://docs.alchemy.com) | Powers AA wallets; supports sponsored gas, smart accounts |
## Category guide
* **User Wallets:** These are traditional consumer-facing wallets such as mobile apps, browser extensions, or exchange-linked wallets. They allow users to hold USDT, make transfers, connect to dApps, and interact directly with Stable.
* **Wallet SDK:** A software development kit that provides developers with prebuilt tools, APIs, and infrastructure to integrate wallet creation, key management, transaction signing, and blockchain interactions directly into their applications.
* **Custodial / Institutional Wallets:** Platforms providing regulated, enterprise-grade asset custody for institutions. These solutions focus on compliance, governance controls, secure key management, and treasury operations rather than end-user flows.
* **Embedded / In-App Wallets:** Wallets generated inside applications through SDKs or backend systems. These enable seamless onboarding for mainstream users without requiring them to install or understand external crypto wallets.
* **Smart Wallets / Account Abstraction:** Programmable wallets that support custom logic such as gasless transactions, bundled operations, or automated execution. These extend basic wallet functionality with developer-defined behaviors.
* **MPC Wallet Providers:** Key-management systems using multi-party computation (MPC) to distribute private key control across multiple parties or devices. Ideal for apps or enterprises needing high-security custody without traditional seed phrases.
* **Connectivity Providers:** Protocols such as WalletConnect (Reown) that connect wallets and dApps. They don't store assets or act as wallets themselves; instead, they provide secure communication channels for transaction signing and interaction.
## 1. User wallets
These are end-user wallets offered by major global exchanges. They allow users to hold USDT, transfer funds, and connect to applications on Stable.
### Stable Pay
A non-custodial payment wallet built on Stable, designed for fast, stablecoin-native transactions. Stable Pay delivers instant USDT payments, predictable fees, and a simple user experience optimized for everyday transfers and commerce.
**Capabilities**
* Non-custodial wallet for Stable
* Instant USDT payments
* Predictable and consistent transaction costs
* Built directly on Stable’s USDT-native settlement layer
* Consumer-friendly UI designed for payments and commerce
### Binance Wallet
A widely used multi-chain wallet integrated with the world’s largest exchange by volume.
**Capabilities**
* Supports Stable USDT
* Direct integration with Binance ecosystem
* Mobile and extension wallet options
### Bitget Wallet
A multi-asset wallet connected to the Bitget exchange ecosystem, supporting crypto, stocks, and ETFs.
**Capabilities**
* Supports Stable USDT
* Built-in dApp browser
* Seamless integration with Bitget trading accounts
### Gate Wallet (Gate Onchain)
A wallet product backed by one of the largest spot exchanges globally.
**Capabilities**
* Supports Stable USDT
* Easy transfers between Gate exchange and wallet
* dApp and web-app connectivity
### OKX Wallet (OKX Onchain)
A powerful, multi-chain wallet used globally.
**Capabilities**
* Supports Stable USDT
* Deep OKX ecosystem integration
* Web, mobile, and extension wallet options
## 2. Wallet SDK
### Development Kit by Tether (WDK)
An open-source SDK from Tether for building self-custodial wallets across any platform and blockchain.
**Capabilities**
* Multi-Chain Support: Bitcoin, Ethereum, TON, TRON, Solana, Spark, and more
* Agentic Wallets: Native support for AI agent wallets and x402 payments on Stable
* DeFi Integration: Plug-in support for swaps, bridges, and lending protocols
* Extensible Design: Add custom modules for new blockchains or protocols
**Get started**: Install [`@tetherto/wdk`](https://www.npmjs.com/package/@tetherto/wdk) and [`@tetherto/wdk-wallet-evm`](https://www.npmjs.com/package/@tetherto/wdk-wallet-evm), then follow the [WDK documentation](https://docs.wallet.tether.io) to configure Stable as your target chain.
## 3. Custodial & institutional wallets
### Anchorage
A federally chartered national bank providing institutional-grade custody for digital assets.
**Capabilities**
* Secure custody for Stable USDT
* Full compliance and regulatory oversight
* Enterprise-grade key management and access controls
## 4. Embedded / in-app wallets
Wallets embedded directly into applications via SDKs, enabling seamless user onboarding and payment flows.
### Dynamic
Enterprise-grade wallet infrastructure serving thousands of applications and over 40M users.
**Capabilities**
* Wallet creation and authentication
* Embedded wallet flows
* User onboarding for apps and fintechs
**Get started**: Follow the [Dynamic SDK setup docs](https://docs.dynamic.xyz/introduction/welcome) to install the SDK and configure Stable as a supported network in your app.
### Reown (formerly WalletConnect)
A widely adopted standard for connecting wallets to applications.
**Capabilities**
* Secure wallet-to-dApp connections
* Supports mobile, desktop, and extension wallets
* Broad ecosystem compatibility
**Reown SDK for wallet onboarding**
Stable supports integrations with the **Reown SDK** to help developers deliver seamless wallet and onboarding experiences for users.
Reown provides an open-source, all-in-one SDK that serves as the official gateway to the WalletConnect Network. It enables smooth wallet connections, transactions, logins, embedded wallets (email and social login), on-chain payments, in-app swaps, and more within your application.
**Get Started**
* Visit Reown's documentation: [https://docs.reown.com/overview](https://docs.reown.com/overview)
## 5. Smart wallets & account abstraction
Infrastructure enabling programmable wallets, gasless transactions, spending rules, and advanced UX.
### Overview table
| **Provider** | **Category** | **Security Method** | **Docs / Get Started** | **Notes** |
| :------------------------------------------ | :------------------------------------ | :------------------------------------- | :------------------------------------------------------------------------------------- | :------------------------------------ |
| [**Holdstation**](https://holdstation.com/) | Smart Wallet (AA) | Smart contract wallet + biometric auth | [https://docs.holdstation.com/holdstation/](https://docs.holdstation.com/holdstation/) | Gasless flows, DeFi-native wallet |
| [**Daimo**](https://pay.daimo.com/) | AA Payments Wallet | Smart account, no seed phrase | [https://paydocs.daimo.com/](https://paydocs.daimo.com/) | One-click payments, stablecoin-first |
| [**Alchemy**](https://alchemy.com) | AA Infrastructure (Bundler/Paymaster) | Bundler + paymaster infra (ERC-4337) | [https://docs.alchemy.com](https://docs.alchemy.com) | Enables AA wallets to build on Stable |
### Alchemy
Alchemy provides the core AA infrastructure and APIs needed to deploy smart accounts, sponsor gas, and build consumer-grade wallets.
**Capabilities**
* Smart account SDK & APIs
* Paymaster support for gasless actions
* Gas abstraction tooling
* Scalable infra for smart wallet developers
**Get started**: Use the [Alchemy smart account SDK](https://docs.alchemy.com) to deploy ERC-4337 smart accounts and configure a paymaster for sponsored gas on Stable.
**Docs**\
[https://docs.alchemy.com](https://docs.alchemy.com)
### Holdstation
A smart contract wallet offering account abstraction and biometric-secured interactions.\
**Capabilities**
* Full AA-enabled smart wallet
* Gasless transactions and sponsored fees
* Biometric authorization and session keys
* Integrated trading and DeFi execution layer
**Get started**: Explore the [Holdstation developer docs](https://docs.holdstation.com/holdstation/) to integrate smart wallet flows and sponsored transactions into your application.
### Daimo
A consumer-grade, account-abstraction wallet designed for instant stablecoin spending and payments.\
**Capabilities**
* AA-based UX with smart wallet execution
* One-click payments across any chain
* No seed phrases; secure key recovery
* Ideal for payments apps and stablecoin utilities
**Get started**: Visit the [Daimo Pay documentation](https://paydocs.daimo.com/) to add one-click stablecoin payment flows to your application on Stable.
## Stable network setup
### Adding Stable to your wallet
**Network parameters :**
* **Network Name:** Stable
* **Chain ID:** 988
* **Currency:** USDT0
* **RPC URL:** [https://rpc.stable.xyz](https://rpc.stable.xyz)
* **Block Explorer:** [https://stablescan.xyz](https://stablescan.xyz)
### Building wallet integrations
You can add Stable support by:
* Enabling signing and gas estimation via Stable RPC
* Supporting USDT-native transfers
* Integrating WalletConnect for dApps
* Adding Stable to chain lists or metadata registries
## Have a wallet integrating Stable?
You can reach the team at [bizdev@stable.xyz](mailto:bizdev@stable.xyz) to be listed in this section.
# Bridge USDT0 to Stable
Source: https://docs.stable.xyz/en/tutorial/bridge-usdt0
A step-by-step tutorial to bridge USDT0 from Ethereum Sepolia to Stable Testnet using LayerZero's OFT protocol.
In this tutorial, you will bridge USDT0 from Ethereum Sepolia to the Stable Testnet programmatically using TypeScript and ethers v6. You will build the script incrementally, adding one function per step.
This tutorial uses the OFT Mesh path. The OFT Adapter on Sepolia locks your tokens, LayerZero's dual-DVN verification confirms the message, and USDT0 is minted on Stable. For a full explanation of how this works, see [Bridging to Stable](/en/explanation/usdt0-bridging).
## Prerequisites
* Node.js 18.0.0 or higher (`node --version` to verify)
* A Sepolia wallet with a private key you control (never use a key holding real funds)
* SepoliaETH for gas (get some from [sepoliafaucet.com](https://sepoliafaucet.com) or [faucets.chain.link/sepolia](https://faucets.chain.link/sepolia))
* Basic familiarity with running scripts from the terminal
***
## 1. Set up the project
```bash theme={"dark"}
mkdir stable-bridge && cd stable-bridge
npm init -y
npm install ethers@6 @layerzerolabs/lz-v2-utilities
npm install -D tsx
```
Your `package.json` should include:
```json theme={"dark"}
{
"name": "stable-bridge",
"version": "1.0.0",
"scripts": {
"bridge": "tsx --env-file=.env bridge.ts"
},
"dependencies": {
"@layerzerolabs/lz-v2-utilities": "^2.3.39",
"ethers": "^6.13.0"
},
"devDependencies": {
"tsx": "^4.19.0"
}
}
```
## 2. Configure your environment
Create a `.env` file with your credentials:
```bash theme={"dark"}
PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
SEPOLIA_RPC_URL=https://rpc.sepolia.org
```
For `SEPOLIA_RPC_URL`, any of these work:
* Public: `https://rpc.sepolia.org` or `https://ethereum-sepolia-rpc.publicnode.com`
* Alchemy: `https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY`
* Infura: `https://sepolia.infura.io/v3/YOUR_KEY`
## 3. Scaffold the script
Create `bridge.ts` with the imports, configuration, and a `main` function. You will add functions to this file in the following steps, and call them from `main`.
```ts theme={"dark"}
import { ethers, Contract, Wallet, JsonRpcProvider } from "ethers";
import { Options } from "@layerzerolabs/lz-v2-utilities";
const PRIVATE_KEY = process.env.PRIVATE_KEY!;
const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL || "https://rpc.sepolia.org";
// Contract addresses
const SEPOLIA_USDT0 = "0xc4DCC311c028e341fd8602D8eB89c5de94625927";
const SEPOLIA_OFT_ADAPTER = "0xc099cD946d5efCC35A99D64E808c1430cEf08126";
const STABLE_USDT0 = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
// Destination: Stable Testnet
const STABLE_TESTNET_EID = 40374;
// Minimal ABIs — only the functions we call
const ERC20_ABI = [
"function balanceOf(address) view returns (uint256)",
"function approve(address, uint256) returns (bool)",
"function allowance(address, address) view returns (uint256)",
"function mint(address, uint256)",
];
const OFT_ADAPTER_ABI = [
"function quoteSend((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd), bool) view returns ((uint256 nativeFee, uint256 lzTokenFee))",
"function send((uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd), (uint256 nativeFee, uint256 lzTokenFee), address) payable returns ((bytes32, uint64, (uint256, uint256)), (uint256, uint256))",
];
function addressToBytes32(addr: string): string {
return ethers.zeroPadValue(ethers.getBytes(ethers.getAddress(addr)), 32);
}
// You will add functions here.
async function main() {
const provider = new JsonRpcProvider(SEPOLIA_RPC_URL);
const wallet = new Wallet(PRIVATE_KEY, provider);
const usdt0 = new Contract(SEPOLIA_USDT0, ERC20_ABI, wallet);
const oftAdapter = new Contract(SEPOLIA_OFT_ADAPTER, OFT_ADAPTER_ABI, wallet);
const amount = ethers.parseEther("1"); // 1 USDT0 (18 decimals)
// You will add function calls here.
}
main().catch((err) => {
console.error(err.message);
process.exit(1);
});
```
## 4. Mint test USDT0 on Sepolia
The test USDT0 contract on Sepolia exposes a public `mint` function. Add the following function to `bridge.ts` above `main`:
```ts theme={"dark"}
async function mint(usdt0: Contract, receiver: string, amount: bigint) {
console.log(`Minting ${ethers.formatEther(amount)} USDT0 on Sepolia...`);
const tx = await usdt0.mint(receiver, amount);
await tx.wait();
console.log(`Mint tx: ${tx.hash} confirmed`);
const balance = await usdt0.balanceOf(receiver);
console.log(`USDT0 balance: ${ethers.formatEther(balance)}`);
}
```
Then call it from `main`:
```ts theme={"dark"}
await mint(usdt0, wallet.address, amount);
```
Run the script:
```bash theme={"dark"}
npx tsx --env-file=.env bridge.ts
```
***
**Checkpoint:** You should see a non-zero USDT0 balance logged after the mint confirms.
***
## 5. Approve the OFT Adapter
Before the OFT Adapter can move your tokens, it needs an ERC-20 allowance. Add this function above `main`:
```ts theme={"dark"}
async function approve(usdt0: Contract, spender: string, owner: string, amount: bigint) {
console.log("Approving OFT Adapter...");
const tx = await usdt0.approve(spender, amount);
await tx.wait();
console.log(`Approve tx: ${tx.hash} confirmed`);
const allowance = await usdt0.allowance(owner, spender);
console.log(`Allowance: ${ethers.formatEther(allowance)}`);
}
```
Add the call in `main` after `mint`:
```ts theme={"dark"}
// await mint(usdt0, wallet.address, amount);
await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount);
```
Run the script. You can comment out the `await mint(...)` call if you already have tokens from the previous run.
***
**Checkpoint:** The script should log a non-zero allowance after the approval confirms.
***
## 6. Quote the fee and send the bridge transaction
The `quoteSend` call returns the LayerZero messaging fee in SepoliaETH, which you pass as `msg.value` to `send`. Add this function above `main`:
```ts theme={"dark"}
async function send(oftAdapter: Contract, receiver: string, amount: bigint) {
const options = Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes();
const sendParams = {
dstEid: STABLE_TESTNET_EID,
to: addressToBytes32(receiver),
amountLD: amount,
minAmountLD: amount,
extraOptions: options,
composeMsg: "0x",
oftCmd: "0x",
};
console.log("Quoting bridge fee...");
const feeResult = await oftAdapter.quoteSend(sendParams, false);
const fee = { nativeFee: feeResult.nativeFee, lzTokenFee: feeResult.lzTokenFee };
console.log(`Bridge fee: ${ethers.formatEther(fee.nativeFee)} ETH`);
console.log("Sending bridge transaction...");
const tx = await oftAdapter.send(sendParams, fee, receiver, {
value: fee.nativeFee,
});
await tx.wait();
console.log(`Bridge tx: ${tx.hash} confirmed`);
console.log(`Sepolia Etherscan: https://sepolia.etherscan.io/tx/${tx.hash}`);
console.log(`LayerZero Scan: https://testnet.layerzeroscan.com/tx/${tx.hash}`);
}
```
Add the call in `main` after `approve`:
```ts theme={"dark"}
// await mint(usdt0, wallet.address, amount);
// await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount);
await send(oftAdapter, wallet.address, amount);
```
## 7. Verify arrival on Stable Testnet
After sending, the script can poll the Stable Testnet RPC until the tokens arrive. Add this function above `main`:
```ts theme={"dark"}
async function verify(receiver: string) {
console.log("Waiting for DVN verification (~2 minutes)...");
const stableProvider = new JsonRpcProvider("https://rpc.testnet.stable.xyz");
const stableUsdt0 = new Contract(STABLE_USDT0,
["function balanceOf(address) view returns (uint256)"], stableProvider);
const before: bigint = await stableUsdt0.balanceOf(receiver);
for (let i = 0; i < 24; i++) {
await new Promise((r) => setTimeout(r, 5000));
const current: bigint = await stableUsdt0.balanceOf(receiver);
if (current > before) {
console.log(`\nUSDT0 on Stable: ${ethers.formatEther(current)}`);
console.log(`Explorer: https://testnet.stablescan.xyz/address/${receiver}`);
return;
}
process.stdout.write(".");
}
console.log("\nTokens have not arrived yet. Check manually:");
console.log(`Explorer: https://testnet.stablescan.xyz/address/${receiver}`);
}
```
Add the call in `main` after `send`:
```ts theme={"dark"}
// await mint(usdt0, wallet.address, amount);
// await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount);
// await send(oftAdapter, wallet.address, amount);
await verify(wallet.address);
```
## 8. Run the complete bridge
Your `main` function should now look like this:
```ts theme={"dark"}
async function main() {
const provider = new JsonRpcProvider(SEPOLIA_RPC_URL);
const wallet = new Wallet(PRIVATE_KEY, provider);
const usdt0 = new Contract(SEPOLIA_USDT0, ERC20_ABI, wallet);
const oftAdapter = new Contract(SEPOLIA_OFT_ADAPTER, OFT_ADAPTER_ABI, wallet);
const amount = ethers.parseEther("1"); // 1 USDT0 (18 decimals)
await mint(usdt0, wallet.address, amount);
await approve(usdt0, SEPOLIA_OFT_ADAPTER, wallet.address, amount);
await send(oftAdapter, wallet.address, amount);
await verify(wallet.address);
}
```
Run it:
```bash theme={"dark"}
npx tsx --env-file=.env bridge.ts
```
***
**Checkpoint:** You should see output like this:
```
Minting 1.0 USDT0 on Sepolia...
Mint tx: 0x3a1f...c9d2 confirmed
USDT0 balance: 1.0
Approving OFT Adapter...
Approve tx: 0x7b2e...f401 confirmed
Allowance: 1.0
Quoting bridge fee...
Bridge fee: 0.000101 ETH
Sending bridge transaction...
Bridge tx: 0xa94f...8c11 confirmed
Sepolia Etherscan: https://sepolia.etherscan.io/tx/0xa94f...8c11
LayerZero Scan: https://testnet.layerzeroscan.com/tx/0xa94f...8c11
Waiting for DVN verification (~2 minutes)...
......
USDT0 on Stable: 1.0
```
You can also search your wallet address on the [Stable Testnet explorer](https://testnet.stablescan.xyz) to confirm the mint event.
***
## What you have built
You bridged USDT0 from Ethereum Sepolia to Stable Testnet. You now know how to:
* Mint test USDT0 on Sepolia using the contract's public `mint` function
* Approve an OFT Adapter to spend ERC-20 tokens on your behalf
* Construct LayerZero `sendParams` with 32-byte address encoding and executor options
* Quote the cross-chain messaging fee with `quoteSend` before committing funds
* Execute a cross-chain token transfer with `send` and confirm delivery on the destination chain
* Verify on-chain state using Stable's RPC (`https://rpc.testnet.stable.xyz`, chain ID `2201`) and Stablescan
## Next recommended
Use the bridged USDT0 with native and ERC-20 transfers.
Deep dive on OFT Mesh vs Legacy Mesh mechanics.
Full network parameters, RPC endpoints, and faucet details.
# Quick start
Source: https://docs.stable.xyz/en/tutorial/quick-start
Connect to Stable testnet, fund a wallet from the faucet, and send your first USDT0 transaction in under 5 minutes.
This quick start gets you from zero to a confirmed USDT0 transaction on Stable testnet. The only tools you need are Node.js and a private key. Everything else, including gas, comes from the faucet.
Stable uses USDT0 as its gas token, so you only need USDT0 to transact. There is no separate gas asset to fund.
## Prerequisites
* Node.js 20 or later
* A private key you control (a fresh test key is fine)
## 1. Install and configure
Create a project, install `ethers`, and save the testnet config.
```bash theme={"dark"}
mkdir stable-quickstart && cd stable-quickstart
npm init -y && npm install ethers
```
```text theme={"dark"}
added 1 package, audited 2 packages in 1s
```
Save your private key to `.env`:
```bash theme={"dark"}
echo "PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE" > .env
```
Create `config.ts`:
```typescript theme={"dark"}
// config.ts
import { ethers } from "ethers";
import "dotenv/config";
export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const CHAIN_ID = 2201;
export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
```
## 2. Fund the wallet
Print your address, then request testnet USDT0 from the faucet.
```typescript theme={"dark"}
// address.ts
import { wallet } from "./config";
console.log("Wallet address:", wallet.address);
```
```bash theme={"dark"}
npx tsx address.ts
```
```text theme={"dark"}
Wallet address: 0x1234...abcd
```
Go to [https://faucet.stable.xyz](https://faucet.stable.xyz), paste the address, and select the button to receive testnet USDT0. The faucet sends 1 USDT0, enough for thousands of native transfers.
## 3. Send your first transaction
Send 0.001 USDT0 natively. On Stable, USDT0 is the native asset, so a simple value transfer is the cheapest path (21,000 gas).
```typescript theme={"dark"}
// send.ts
import { ethers } from "ethers";
import { provider, wallet } from "./config";
const recipient = "0xRecipientAddress"; // replace with any address
const amount = ethers.parseEther("0.001"); // 0.001 USDT0 (18 decimals, native)
const block = await provider.getBlock("latest");
const baseFee = block!.baseFeePerGas!;
const tx = await wallet.sendTransaction({
to: recipient,
value: amount,
maxFeePerGas: baseFee * 2n,
maxPriorityFeePerGas: 0n, // always 0 on Stable
});
const receipt = await tx.wait(1);
console.log("Tx:", receipt!.hash);
console.log("Explorer:", `https://testnet.stablescan.xyz/tx/${receipt!.hash}`);
```
```bash theme={"dark"}
npx tsx send.ts
```
```text theme={"dark"}
Tx: 0x8f3a...2d41
Explorer: https://testnet.stablescan.xyz/tx/0x8f3a...2d41
```
Open the explorer link to confirm the transaction. Block time is roughly 0.7 seconds, so it should already be final.
`maxPriorityFeePerGas` is ignored by Stable and must be set to `0`. See [Gas pricing](/en/reference/gas-pricing-api) for how the base-fee-only model changes transaction construction.
## Where to go next
Scaffold a Foundry project and deploy to Stable testnet.
Create wallet, send, receive, and query payment history.
Wire MCP servers and agent skills into your AI editor.
# Send your first USDT0
Source: https://docs.stable.xyz/en/tutorial/send-usdt0
Send USDT0 on Stable as both a native transfer and an ERC-20 transfer, and verify that both operate on the same balance.
On Stable, USDT0 is both the chain's native asset and an ERC-20 token. This means `approve`, `transferFrom`, and `permit` remain fully available alongside standard value transfers, and both paths move funds from the same underlying balance.
This page walks you through sending USDT0 through both paths and confirming they draw from one balance.
**18 vs 6 decimals**: Native USDT0 uses 18 decimals (standard EVM precision), while the ERC-20 interface reports 6 decimals (standard USDT precision). Both reflect the same balance, so `address(x).balance` and `USDT0.balanceOf(x)` may differ by up to 0.000001 USDT0 due to fractional reconciliation. See [USDT0 behavior on Stable](/en/explanation/usdt0-behavior).
## What you'll build
A two-script flow that sends 0.001 USDT0 as a native transfer, sends 0.001 USDT0 as an ERC-20 transfer, and prints both balances.
### Demo
```text theme={"dark"}
step 1. Connect wallet → balance displayed
0.01 USDT0
step 2. Send 0.001 USDT0 (choose native or ERC-20 transfer)
step 3. Result
Sent: 0.001 USDT0
Gas fee: 0.000021 USDT0
Native balance: 0.008979 USDT0
ERC-20 balance: 0.008979 USDT0
```
## Prerequisites
* Node.js 20 or later
* A private key with testnet USDT0. See [Quick start](/en/tutorial/quick-start) to fund a wallet.
**USDT0 contract addresses**
* Mainnet: `0x779ded0c9e1022225f8e0630b35a9b54be713736`
* Testnet: `0x78cf24370174180738c5b8e352b6d14c83a6c9a9`
## Setup
```typescript theme={"dark"}
// config.ts
import { ethers } from "ethers";
import "dotenv/config";
export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const CHAIN_ID = 2201;
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
```
## Option 1 (recommended): send as native transfer
Native transfers work the same as sending ETH on Ethereum. The `value` field carries the USDT0 amount. A native transfer costs only 21,000 gas, the cheapest way to send USDT0.
```typescript theme={"dark"}
// sendNative.ts
import { ethers } from "ethers";
import { provider, wallet } from "./config";
const recipient = "0xRecipientAddress";
const amount = ethers.parseUnits("0.001", 18); // 18 decimals for native
const block = await provider.getBlock("latest");
const baseFee = block!.baseFeePerGas!;
const tx = await wallet.sendTransaction({
to: recipient,
value: amount,
maxFeePerGas: baseFee * 2n,
maxPriorityFeePerGas: 0n, // always 0 on Stable
});
const receipt = await tx.wait(1);
console.log("Native transfer tx:", receipt!.hash);
```
```bash theme={"dark"}
npx tsx sendNative.ts
```
```text theme={"dark"}
Native transfer tx: 0x8f3a...2d41
```
## Option 2: send as ERC-20 transfer
USDT0 can also be sent as an ERC-20 transfer. This deducts from the same balance, but uses the ERC-20 interface with 6-decimal precision.
```typescript theme={"dark"}
// sendERC20.ts
import { ethers } from "ethers";
import { wallet, USDT0_ADDRESS } from "./config";
const recipient = "0xRecipientAddress";
const amount = ethers.parseUnits("0.001", 6); // 6 decimals for ERC-20
const usdt0 = new ethers.Contract(USDT0_ADDRESS, [
"function transfer(address to, uint256 amount) returns (bool)"
], wallet);
const tx = await usdt0.transfer(recipient, amount);
const receipt = await tx.wait(1);
console.log("ERC-20 transfer tx:", receipt!.hash);
```
```bash theme={"dark"}
npx tsx sendERC20.ts
```
```text theme={"dark"}
ERC-20 transfer tx: 0xa2b1...77c0
```
## Verify the unified balance
After either transfer, query both balances to confirm they draw from the same source.
```typescript theme={"dark"}
// balances.ts
import { ethers } from "ethers";
import { provider, wallet, USDT0_ADDRESS } from "./config";
const nativeBalance = await provider.getBalance(wallet.address);
console.log("Native balance:", ethers.formatEther(nativeBalance), "USDT0");
const usdt0 = new ethers.Contract(USDT0_ADDRESS, [
"function balanceOf(address) view returns (uint256)"
], provider);
const erc20Balance = await usdt0.balanceOf(wallet.address);
console.log("ERC-20 balance:", ethers.formatUnits(erc20Balance, 6), "USDT0");
```
```bash theme={"dark"}
npx tsx balances.ts
```
```text theme={"dark"}
Native balance: 0.008979 USDT0
ERC-20 balance: 0.008979 USDT0
```
Both values represent the same balance. They may differ by up to 0.000001 USDT0 due to [fractional balance reconciliation](/en/explanation/usdt0-behavior#balance-reconciliation).
## Next recommended
Send USDT0 with gas fees paid by a waiver service.
Create wallet, send, receive, and query payment history.
Understand dual-role balance reconciliation and contract design.
# Deploy a smart contract
Source: https://docs.stable.xyz/en/tutorial/smart-contract
Scaffold a Foundry project, fund a wallet with USDT0, deploy a contract to Stable testnet, and verify it on-chain.
In this tutorial, you will deploy a simple smart contract to the Stable Testnet and read its state from the chain. Along the way, you will learn how Stable's network is configured, how USDT0 works as a gas token, and how to point standard EVM tooling at Stable.
This tutorial assumes basic familiarity with Solidity and a Unix-like terminal. No prior Stable experience is required.
## What you'll build
A fresh Foundry project with the sample `Counter` contract, deployed to Stable testnet, with one state-changing call and one read call.
### Demo
```text theme={"dark"}
step 1. Scaffold Foundry project → stable-hello/
step 2. Configure testnet
RPC: https://rpc.testnet.stable.xyz
Chain ID: 2201
step 3. Fund wallet from faucet (1 USDT0)
step 4. forge create Counter
Deployed to: 0xContract...
step 5. cast send Counter.setNumber(42)
step 6. cast call Counter.number()
→ 42
```
## Prerequisites
* [Foundry](https://book.getfoundry.sh/getting-started/installation) installed (`forge`, `cast`, and `anvil` available in your PATH)
* A wallet with a private key you control (a fresh test key is fine; never use a key holding real funds on testnet)
* An internet connection to reach the testnet RPC and faucet
***
## 1. Create a new Foundry project
Run the following command to scaffold a fresh project:
```bash theme={"dark"}
forge init stable-hello && cd stable-hello
```
Foundry creates a `src/` directory with a sample `Counter.sol` contract and a matching test file. You will deploy this contract as-is. The goal is to get something real on-chain, not to write novel Solidity.
## 2. Review the contract you are deploying
Open [src/Counter.sol](src/Counter.sol). It contains two functions:
```solidity theme={"dark"}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
```
`number` is a public state variable stored on-chain. `increment()` and `setNumber()` are the two ways to change it. Reading `number` costs no gas. It is a free `eth_call`.
## 3. Configure the Stable Testnet
Create a file named [.env](.env) at the project root to store your network credentials:
```bash theme={"dark"}
touch .env
```
Add the following, replacing the placeholder with your actual private key:
```bash theme={"dark"}
PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
```
Next, open [foundry.toml](foundry.toml) and add the Stable Testnet as a named network profile. Append this block below the existing `[profile.default]` section:
```toml theme={"dark"}
[rpc_endpoints]
stable_testnet = "https://rpc.testnet.stable.xyz"
```
This tells Foundry where to send transactions when you target `stable_testnet`. Stable is EVM-compatible, so no other configuration is needed.
***
**Checkpoint:** Confirm your RPC endpoint is reachable:
```bash theme={"dark"}
cast chain-id --rpc-url https://rpc.testnet.stable.xyz
```
Expected output:
```
2201
```
Chain ID `2201` is the Stable Testnet. If you see this number, your machine can reach the network.
***
## 4. Get your wallet address
Derive your deployer address from your private key so you know which account to fund:
```bash theme={"dark"}
source .env
cast wallet address $PRIVATE_KEY
```
Copy the address that is printed. You need it in the next step.
## 5. Fund your wallet with USDT0
Stable uses **USDT0** as its gas token. The same asset you use to pay for goods and services is used directly to pay for computation. There is no secondary native token.
Visit the testnet faucet and request funds:
```
https://faucet.stable.xyz
```
Paste the address from the previous step. The faucet sends 1 USDT0 to your wallet, which is enough to deploy and interact with several contracts.
***
**Checkpoint:** Confirm your balance arrived:
```bash theme={"dark"}
cast balance $PRIVATE_KEY --rpc-url https://rpc.testnet.stable.xyz
```
You should see a non-zero value. If the balance is still `0`, wait a few seconds and re-run. Stable produces a new block roughly every 0.7 seconds, so funds settle quickly.
***
## 6. Deploy the contract
Run the deployment with `forge create`:
```bash theme={"dark"}
source .env
forge create src/Counter.sol:Counter \
--rpc-url https://rpc.testnet.stable.xyz \
--private-key $PRIVATE_KEY \
--broadcast
```
Foundry compiles the contract, broadcasts a deployment transaction, and waits for the receipt. Because block time is \~0.7 seconds, this takes only a moment.
***
**Checkpoint:** The output should look like this:
```
[⠒] Compiling...
No files changed, compilation skipped
Deployer: 0xYourAddress
Deployed to: 0xSomeContractAddress
Transaction hash: 0xSomeTxHash
```
Copy the `Deployed to` address. You need it in the next two steps.
***
## 7. Call a write function
Now call `setNumber()` to store a value on-chain:
```bash theme={"dark"}
cast send 0xSomeContractAddress "setNumber(uint256)" 42 \
--rpc-url https://rpc.testnet.stable.xyz \
--private-key $PRIVATE_KEY
```
This sends a transaction. You are paying a small USDT0 fee for the state change. The value `42` is now stored in the `number` variable on the Stable Testnet.
## 8. Read state from the chain
Call `number()` to read the value back. This is a free read, with no transaction and no gas:
```bash theme={"dark"}
cast call 0xSomeContractAddress "number()(uint256)" \
--rpc-url https://rpc.testnet.stable.xyz
```
Expected output:
```
42
```
You just wrote to and read from the Stable Testnet. The round-trip — deploy, write, read — is the core loop of EVM development, and it works identically here to any other EVM chain.
## 9. Inspect your deployment on Stablescan
Open the Stable Testnet block explorer and paste your contract address:
```
https://testnet.stablescan.xyz
```
You will see your deployment transaction and the `setNumber` call you made. Stablescan is the canonical tool for inspecting on-chain state, verifying contract source code, and reading transaction history on Stable.
***
## What you have built
You deployed a contract, sent a state-changing transaction, and read on-chain state — all on the Stable Testnet. You now know how to:
* Configure Foundry (or any EVM toolchain) to target Stable using a standard RPC endpoint
* Fund a wallet using the USDT0 faucet
* Pay for transactions with USDT0 as the gas token
* Inspect your work on Stablescan
## Next recommended
Upload your source to Stablescan so users can read and interact with it.
Subscribe to events with ethers.js and backfill historical logs.
Understand how USDT0-denominated fees are calculated.
# Installation guide
Source: https://docs.stable.xyz/en/how-to/install-node
Step-by-step installation guide for Stable node binaries on Linux using pre-compiled releases.
This guide provides detailed instructions for installing and setting up a Stable node on various platforms.
## Prerequisites
Before starting the installation, ensure you have:
* Met all [System Requirements](/en/reference/node-system-requirements)
* Root or sudo access to your server
* Basic knowledge of Linux command line
## Installation method
Use the pre-compiled binaries for your platform. Stable does not currently support building from source.
#### Linux AMD64
```bash theme={"dark"}
# Download the latest binary for AMD64 architecture
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-latest-linux-amd64-mainnet.tar.gz
# Extract the archive
tar -xvzf stabled-latest-linux-amd64-mainnet.tar.gz
# Move binary to system path
sudo mv stabled /usr/bin/
# Verify installation
stabled version
```
#### Linux ARM64
```bash theme={"dark"}
# Download the binary for ARM64 architecture
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/binary/stabled-latest-linux-arm64-mainnet.tar.gz
# Extract and install
tar -xvzf stabled-latest-linux-arm64-mainnet.tar.gz
sudo mv stabled /usr/bin/
# Verify installation
stabled version
```
#### Linux AMD64
```bash theme={"dark"}
# Download the latest binary for AMD64 architecture
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-latest-linux-amd64-testnet.tar.gz
# Extract the archive
tar -xvzf stabled-latest-linux-amd64-testnet.tar.gz
# Move binary to system path
sudo mv stabled /usr/bin/
# Verify installation
stabled version
```
#### Linux ARM64
```bash theme={"dark"}
# Download the binary for ARM64 architecture
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-latest-linux-arm64-testnet.tar.gz
# Extract and install
tar -xvzf stabled-latest-linux-arm64-testnet.tar.gz
sudo mv stabled /usr/bin/
# Verify installation
stabled version
```
## Node initialization
After installing the binary, initialize your node:
### Step 1: set node name
```bash theme={"dark"}
# Set your node's moniker (choose a unique name)
export MONIKER="your-node-name"
```
### Step 2: initialize the node
```bash theme={"dark"}
# Initialize with the mainnet chain ID
stabled init $MONIKER --chain-id stable_988-1
# This creates the configuration directory at ~/.stabled/
```
> **Note**: For current network parameters including chain ID, see [Mainnet Information](/en/reference/mainnet-information)
```bash theme={"dark"}
# Initialize with the testnet chain ID
stabled init $MONIKER --chain-id stabletestnet_2201-1
# This creates the configuration directory at ~/.stabled/
```
> **Note**: For current network parameters including chain ID, see [Testnet Information](/en/reference/testnet-information)
### Step 3: download genesis file
```bash theme={"dark"}
# Create backup of default genesis
mv ~/.stabled/config/genesis.json ~/.stabled/config/genesis.json.backup
# Download mainnet genesis
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/genesis.zip
unzip genesis.zip
# Move genesis to config directory
cp genesis.json ~/.stabled/config/genesis.json
# Verify genesis checksum
sha256sum ~/.stabled/config/genesis.json
# Expected: e1ceda79a3cc48a1028ca8646a2e9e2d156f610637cfb8b428ca8354277921f1
```
```bash theme={"dark"}
# Create backup of default genesis
mv ~/.stabled/config/genesis.json ~/.stabled/config/genesis.json.backup
# Download testnet genesis
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/configuration/genesis.zip
unzip genesis.zip
# Move genesis to config directory
cp genesis.json ~/.stabled/config/genesis.json
# Verify genesis checksum
sha256sum ~/.stabled/config/genesis.json
# Expected: 66afbb6e57e6faf019b3021de299125cddab61d433f28894db751252f5b8eaf2
```
### Step 4: configure node
#### Download configuration files
```bash theme={"dark"}
# Download optimized configuration (choose one based on your node type)
# For RPC/Full nodes:
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/rpc_node_config.zip
unzip rpc_node_config.zip
# For Archive nodes:
# wget https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/configuration/archive_node_config.zip
# unzip archive_node_config.zip
# Backup original config
cp ~/.stabled/config/config.toml ~/.stabled/config/config.toml.backup
# Apply new configuration
cp config.toml ~/.stabled/config/config.toml
# Update moniker in config
sed -i "s/^moniker = \".*\"/moniker = \"$MONIKER\"/" ~/.stabled/config/config.toml
```
```bash theme={"dark"}
# Download optimized configuration
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/configuration/rpc_node_config.zip
unzip rpc_node_config.zip
# Backup original config
cp ~/.stabled/config/config.toml ~/.stabled/config/config.toml.backup
# Apply new configuration
cp config.toml ~/.stabled/config/config.toml
# Update moniker in config
sed -i "s/^moniker = \".*\"/moniker = \"$MONIKER\"/" ~/.stabled/config/config.toml
```
#### Essential configuration updates
Edit `~/.stabled/config/app.toml`:
```toml theme={"dark"}
# Enable JSON-RPC for EVM compatibility
[json-rpc]
enable = true
address = "0.0.0.0:8545"
ws-address = "0.0.0.0:8546"
allow-unprotected-txs = true
```
Edit `~/.stabled/config/config.toml`:
```toml theme={"dark"}
# P2P Configuration
[p2p]
# Maximum number of peers
max_num_inbound_peers = 50
max_num_outbound_peers = 30
# Seed nodes
seeds = "9aa181b20248e948567cb47a15eae35d58cd549d@seed1.stable.xyz:46656"
# Persistent peers (mainnet seed nodes)
persistent_peers = "b896f6f8ca5a4d1cc40de09407df0c96e76df950@peer1.stable.xyz:26656"
# Enable peer exchange
pex = true
# RPC Configuration
[rpc]
# Listen address
laddr = "tcp://0.0.0.0:26657"
# Maximum number of simultaneous connections
max_open_connections = 900
# CORS settings (adjust for production)
cors_allowed_origins = ["*"]
```
```toml theme={"dark"}
# P2P Configuration
[p2p]
# Maximum number of peers
max_num_inbound_peers = 50
max_num_outbound_peers = 30
# Seed nodes
seeds = "6f3195823f7e5ee6f911a0a0ceb9ea689e0dc5bd@seed1.testnet.stable.xyz:56656"
# Persistent peers (testnet seed nodes)
persistent_peers = "128accd3e8ee379bfdf54560c21345451c7048c7@peer1.testnet.stable.xyz:26656"
# Enable peer exchange
pex = true
# RPC Configuration
[rpc]
# Listen address
laddr = "tcp://0.0.0.0:26657"
# Maximum number of simultaneous connections
max_open_connections = 900
# CORS settings (adjust for production)
cors_allowed_origins = ["*"]
```
## Systemd service setup
Create a systemd service for automatic management:
### Step 1: create service file
```bash theme={"dark"}
sudo tee /etc/systemd/system/stabled.service > /dev/null <
```bash theme={"dark"}
sudo tee /etc/systemd/system/stabled.service > /dev/null <
### Step 2: enable and start service
```bash theme={"dark"}
# Reload systemd configuration
sudo systemctl daemon-reload
# Enable service to start on boot
sudo systemctl enable stabled
# Start the service
sudo systemctl start stabled
# Check service status
sudo systemctl status stabled
# View logs
sudo journalctl -u stabled -f
```
## Cosmovisor setup (recommended for automatic upgrades)
Cosmovisor is a process manager that automatically handles chain upgrades. This is the recommended setup for production nodes.
### Step 1: install Cosmovisor
```bash theme={"dark"}
# Install Cosmovisor
go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest
# Or download pre-built binary
wget https://github.com/cosmos/cosmos-sdk/releases/download/cosmovisor%2Fv1.7.0/cosmovisor-v1.7.0-linux-amd64.tar.gz
tar -xvzf cosmovisor-v1.7.0-linux-amd64.tar.gz
sudo mv cosmovisor /usr/bin/
# Verify installation
cosmovisor version
```
### Step 2: set environment variables
```bash theme={"dark"}
# Add to ~/.bashrc or ~/.profile
echo "# Cosmovisor Configuration" >> ~/.bashrc
echo "export DAEMON_NAME=stabled" >> ~/.bashrc
echo "export DAEMON_HOME=$HOME/.stabled" >> ~/.bashrc
echo "export DAEMON_ALLOW_DOWNLOAD_BINARIES=true" >> ~/.bashrc
echo "export DAEMON_RESTART_AFTER_UPGRADE=true" >> ~/.bashrc
echo "export DAEMON_LOG_BUFFER_SIZE=512" >> ~/.bashrc
echo "export UNSAFE_SKIP_BACKUP=true" >> ~/.bashrc
# Load variables
source ~/.bashrc
```
### Step 3: setup Cosmovisor directory structure
```bash theme={"dark"}
# Create cosmovisor directory structure
mkdir -p ~/.stabled/cosmovisor/genesis/bin
mkdir -p ~/.stabled/cosmovisor/upgrades
# Copy current binary to genesis
cp /usr/bin/stabled ~/.stabled/cosmovisor/genesis/bin/
# Create current symlink
ln -s ~/.stabled/cosmovisor/genesis ~/.stabled/cosmovisor/current
# Verify setup
ls -la ~/.stabled/cosmovisor/
cosmovisor run version
```
### Step 4: set environment variable
```bash theme={"dark"}
# Set service name (default: stable)
export SERVICE_NAME=stable
```
### Step 5: create service file
```bash theme={"dark"}
sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null <
```bash theme={"dark"}
sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null <
### Step 6: enable and start service
```bash theme={"dark"}
# Reload systemd
sudo systemctl daemon-reload
# Enable service
sudo systemctl enable ${SERVICE_NAME}
# Start the service
sudo systemctl start ${SERVICE_NAME}
# Check status
sudo systemctl status ${SERVICE_NAME}
# View logs
sudo journalctl -u ${SERVICE_NAME} -f
```
### Preparing for upgrades
When a chain upgrade is announced:
```bash theme={"dark"}
# Example: Preparing for v0.8.0 upgrade
UPGRADE_NAME="v0.8.0"
mkdir -p ~/.stabled/cosmovisor/upgrades/$UPGRADE_NAME/bin
# Download new binary
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.8.0-linux-amd64-testnet.tar.gz
tar -xvzf stabled-0.8.0-linux-amd64-testnet.tar.gz
# Place in upgrade directory
mv stabled ~/.stabled/cosmovisor/upgrades/$UPGRADE_NAME/bin/
# Verify the binary
~/.stabled/cosmovisor/upgrades/$UPGRADE_NAME/bin/stabled version
# Cosmovisor will automatically switch to this binary at the upgrade height
```
### Monitoring Cosmovisor
```bash theme={"dark"}
# Check current binary version
cosmovisor run version
# View upgrade info
ls -la ~/.stabled/cosmovisor/upgrades/
# Check service logs for upgrade status
sudo journalctl -u ${SERVICE_NAME} | grep -i upgrade
# Monitor real-time logs during upgrade
sudo journalctl -u ${SERVICE_NAME} -f
```
## Manual Systemd service setup (alternative)
If you prefer not to use Cosmovisor, you can create a standard systemd service:
### Step 1: set environment variable
```bash theme={"dark"}
# Set service name (default: stable)
export SERVICE_NAME=stable
```
### Step 2: create service file
```bash theme={"dark"}
sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null <
```bash theme={"dark"}
sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null <
### Step 3: enable and start service
```bash theme={"dark"}
# Reload systemd configuration
sudo systemctl daemon-reload
# Enable service to start on boot
sudo systemctl enable ${SERVICE_NAME}
# Start the service
sudo systemctl start ${SERVICE_NAME}
# Check service status
sudo systemctl status ${SERVICE_NAME}
# View logs
sudo journalctl -u ${SERVICE_NAME} -f
```
## Quick sync options
For faster synchronization, see the [Snapshots & Sync Guide](/en/how-to/use-node-snapshots) for:
* Archive node snapshots
* Pruned node snapshots
## Post-installation verification
### Check node status
```bash theme={"dark"}
# Check if node is running
sudo systemctl status stabled
# Check sync status
curl -s localhost:26657/status | jq '.result.sync_info'
# Check connected peers
curl -s localhost:26657/net_info | jq '.result.n_peers'
# Check latest block
curl -s localhost:26657/status | jq '.result.sync_info.latest_block_height'
```
### Monitor logs
```bash theme={"dark"}
# Follow service logs
sudo journalctl -u stabled -f
# Check for errors
sudo journalctl -u stabled --since "1 hour ago" | grep -i error
# Check for peer connections
sudo journalctl -u stabled --since "10 minutes ago" | grep -i "peer"
```
## Security hardening
After installation, secure your node:
### Firewall configuration
```bash theme={"dark"}
# Configure UFW
sudo ufw allow 22/tcp # SSH
sudo ufw allow 26656/tcp # P2P
sudo ufw allow 26657/tcp # RPC (only if needed externally)
sudo ufw enable
```
## Next steps
1. **Configure your node**: See [Configuration Guide](/en/reference/node-configuration)
2. **Speed up sync**: Check [Snapshots & Sync](/en/how-to/use-node-snapshots)
3. **Monitor your node**: Set up [Monitoring](/en/how-to/monitor-node)
4. **Join the community**: Get support in [Discord](https://discord.gg/stablexyz)
## Troubleshooting
If you encounter issues during installation:
* Check the [Troubleshooting Guide](/en/how-to/troubleshoot-node)
* Verify system requirements are met
* Ensure ports are not blocked by firewall
* Check disk space and permissions
* Review systemd logs for errors
# Monitoring & maintenance guide
Source: https://docs.stable.xyz/en/how-to/monitor-node
Monitoring setup using Prometheus, Grafana, and AlertManager for Stable node observability and alerting.
Comprehensive guide for monitoring Stable nodes and performing routine maintenance tasks.
## Monitoring stack overview
### Recommended stack
* **Prometheus**: Metrics collection
* **Grafana**: Visualization and dashboards
* **AlertManager**: Alert routing and management
* **Node Exporter**: System metrics
* **Loki**: Log aggregation (optional)
## Quick monitoring setup
### Step 1: enable Prometheus metrics
```toml theme={"dark"}
# Edit ~/.stabled/config/config.toml
[instrumentation]
prometheus = true
prometheus_listen_addr = ":26660"
namespace = "stablebft"
```
Restart node:
```bash theme={"dark"}
sudo systemctl restart ${SERVICE_NAME}
```
### Step 2: install Prometheus
```bash theme={"dark"}
# Download Prometheus
wget https://github.com/prometheus/prometheus/releases/download/v2.45.0/prometheus-2.45.0.linux-amd64.tar.gz
tar xvf prometheus-2.45.0.linux-amd64.tar.gz
sudo mv prometheus-2.45.0.linux-amd64 /opt/prometheus
# Create config
sudo tee /opt/prometheus/prometheus.yml > /dev/null < /dev/null < 3 |
| `stablebft_consensus_block_interval` | Block time | > 10s |
| `stablebft_p2p_peers` | Connected peers | \< 3 |
| `stablebft_mempool_size` | Mempool size | > 1500 |
| `stablebft_mempool_failed_txs` | Failed transactions | > 100/min |
### System metrics
| Metric | Description | Alert Threshold |
| ---------------------------------- | ---------------- | ---------------- |
| `node_cpu_seconds_total` | CPU usage | > 80% for 5m |
| `node_memory_MemAvailable_bytes` | Available memory | \< 10% |
| `node_filesystem_avail_bytes` | Available disk | \< 10% |
| `node_network_receive_bytes_total` | Network RX | > 100MB/s |
| `node_disk_io_time_seconds_total` | Disk I/O | > 80% |
| `node_load15` | System load | > CPU cores \* 2 |
## Grafana dashboard setup
### Import Stable dashboard
```json theme={"dark"}
{
"dashboard": {
"title": "Stable Node Monitoring",
"panels": [
{
"title": "Block Height",
"targets": [
{
"expr": "stablebft_consensus_height{chain_id=\"stabletestnet_2201-1\"}"
}
]
},
{
"title": "Peers",
"targets": [
{
"expr": "stablebft_p2p_peers"
}
]
},
{
"title": "Block Time",
"targets": [
{
"expr": "rate(stablebft_consensus_height[1m]) * 60"
}
]
},
{
"title": "Mempool Size",
"targets": [
{
"expr": "stablebft_mempool_size"
}
]
}
]
}
}
```
### Custom dashboard import
Import dashboards via Grafana UI:
```bash theme={"dark"}
# Navigate to Dashboards > Import > Upload JSON file
# Or use Dashboard ID in Grafana's dashboard library
```
## AlertManager configuration
### Install AlertManager
```bash theme={"dark"}
# Download AlertManager
wget https://github.com/prometheus/alertmanager/releases/download/v0.26.0/alertmanager-0.26.0.linux-amd64.tar.gz
tar xvf alertmanager-0.26.0.linux-amd64.tar.gz
sudo mv alertmanager-0.26.0.linux-amd64 /opt/alertmanager
# Configure
sudo tee /opt/alertmanager/alertmanager.yml > /dev/null < 1500
for: 10m
labels:
severity: warning
annotations:
summary: "High mempool size: {{ $value }}"
- alert: DiskSpaceLow
expr: node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "Low disk space: {{ $value | humanizePercentage }}"
- alert: HighCPUUsage
expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 10m
labels:
severity: warning
annotations:
summary: "High CPU usage: {{ $value }}%"
```
## Log monitoring
### Systemd logs
```bash theme={"dark"}
# View recent logs
sudo journalctl -u ${SERVICE_NAME} -n 100
# Follow logs
sudo journalctl -u ${SERVICE_NAME} -f
# Filter by time
sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago"
# Export logs
sudo journalctl -u ${SERVICE_NAME} --since today > stable-logs-$(date +%Y%m%d).log
```
### Log analysis scripts
```bash theme={"dark"}
#!/bin/bash
# analyze-logs.sh
# Count errors in last hour
echo "Errors in last hour:"
sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" | grep -c ERROR
# Show peer connections
echo "Peer connections:"
sudo journalctl -u ${SERVICE_NAME} --since "10 minutes ago" | grep "Peer connection" | tail -10
# Check for consensus issues
echo "Consensus rounds:"
sudo journalctl -u ${SERVICE_NAME} --since "30 minutes ago" | grep -E "enterNewRound|Timeout" | tail -20
# Memory usage patterns
echo "Memory warnings:"
sudo journalctl -u ${SERVICE_NAME} --since "1 day ago" | grep -i memory
```
### Loki setup (optional)
```bash theme={"dark"}
# Install Loki
wget https://github.com/grafana/loki/releases/download/v2.9.0/loki-linux-amd64.zip
unzip loki-linux-amd64.zip
sudo mv loki-linux-amd64 /usr/local/bin/loki
# Install Promtail
wget https://github.com/grafana/loki/releases/download/v2.9.0/promtail-linux-amd64.zip
unzip promtail-linux-amd64.zip
sudo mv promtail-linux-amd64 /usr/local/bin/promtail
# Configure Promtail
sudo tee /etc/promtail-config.yml > /dev/null < ~/reports/daily_$(date +%Y%m%d).log
curl -s localhost:26657/status | jq >> ~/reports/daily_$(date +%Y%m%d).log
```
### Weekly maintenance
```bash theme={"dark"}
#!/bin/bash
# weekly-maintenance.sh
# Prune old data
stabled prune
# Compact database
stabled compact
# Update peer list
wget https://raw.githubusercontent.com/stable-chain/networks/main/testnet/peers.txt
cat peers.txt >> ~/.stabled/config/config.toml
# Create snapshot (optional)
./create-snapshot.sh
# System updates
sudo apt update
sudo apt upgrade -y
# Restart node (during low activity)
sudo systemctl restart ${SERVICE_NAME}
```
### Database maintenance
```bash theme={"dark"}
# Check database size
du -sh ~/.stabled/data/
# Analyze database
stabled debug db stats ~/.stabled/data
```
## Performance monitoring
### Resource usage tracking
```bash theme={"dark"}
#!/bin/bash
# track-resources.sh
while true; do
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
CPU=$(top -bn1 | grep "stabled" | awk '{print $9}')
MEM=$(top -bn1 | grep "stabled" | awk '{print $10}')
IO=$(iostat -x 1 2 | tail -n2 | awk '{print $14}')
echo "$TIMESTAMP,CPU:$CPU,MEM:$MEM,IO:$IO" >> ~/metrics/resources.csv
sleep 60
done
```
### Query performance
```bash theme={"dark"}
# Monitor RPC response times
while true; do
START=$(date +%s%N)
curl -s http://localhost:26657/status > /dev/null
END=$(date +%s%N)
DIFF=$((($END - $START) / 1000000))
echo "RPC response time: ${DIFF}ms"
sleep 5
done
```
## Monitoring best practices
1. **Set up redundant monitoring**
* Use external monitoring services
* Implement cross-node monitoring
* Set up dead man's switch alerts
2. **Alert fatigue prevention**
* Tune alert thresholds based on baseline
* Use alert grouping and inhibition
* Implement escalation policies
3. **Data retention**
* Keep metrics for 30 days minimum
* Archive important logs
* Regular backup of monitoring configs
4. **Security**
* Secure Grafana with strong passwords
* Use HTTPS for all endpoints
* Restrict prometheus access
5. **Documentation**
* Document all custom metrics
* Maintain runbooks for alerts
* Keep dashboard descriptions updated
## Next steps
* [Review Troubleshooting Guide](/en/how-to/troubleshoot-node) for issue resolution
* [Configure Upgrades](/en/how-to/upgrade-node) with monitoring
* Set up custom alerts based on your requirements
# Production readiness
Source: https://docs.stable.xyz/en/how-to/production-readiness
Validate an integration before shipping to mainnet: security, reliability, operations, and where to escalate.
Work through each section below before switching from testnet to mainnet.
## Before you launch
* **Network targets.** Your application reads mainnet values, not testnet: chain ID `988`, RPC `https://rpc.stable.xyz`, explorer `https://stablescan.xyz`. Full configuration is in [Connect](/en/reference/connect).
* **Contracts verified.** Deployed contracts are verified on [stablescan.xyz](https://stablescan.xyz) so users and partners can inspect them.
* **Mainnet funding path.** You have a documented way for production wallets to acquire USDT0: direct, bridge via LayerZero, or custodian. Faucets are testnet-only.
* **Environment isolation.** Keys, RPC credentials, and signing paths are separated between testnet and mainnet.
## Security checks
USDT0's dual-role behavior breaks a handful of assumptions ported from Ethereum. Each item below should be validated. The full list is in the [migration checklist](/en/explanation/usdt0-behavior).
**Solvency checks read real native balance, not a mirror.**
Tracking deposited native value in an internal variable is unsafe. An external `USDT0.transferFrom` call can drain the contract's native balance without invoking any contract code.
```solidity theme={"dark"}
// SAFE — checks real balance at the moment of transfer
function withdraw() external {
uint256 amount = credit[msg.sender];
credit[msg.sender] = 0;
require(address(this).balance >= amount, "insufficient balance");
payable(msg.sender).call{value: amount}("");
}
```
**Allowance-based drain paths are covered by tests.** Every `approve` / `transferFrom` / `permit` path has a test that attempts to drain the contract's native balance.
**Zero-address transfers are rejected before the call.**
Both native and ERC-20 transfers to `address(0)` revert on Stable. Validate recipients explicitly, or your transaction will fail.
```solidity theme={"dark"}
require(recipient != address(0), "zero address recipient");
payable(recipient).call{value: amount}("");
```
**Address-reuse detection does not rely on `EXTCODEHASH`.** Permit-based approvals change native balance without a nonce increment, so `EXTCODEHASH` can oscillate between zero hash and empty hash. Use explicit tracking instead.
## Performance and reliability
* **RPC redundancy.** Production traffic has a failover plan. Third-party providers are listed in [RPC providers](/en/reference/rpc-providers).
* **Gas estimation.** Transactions set `maxPriorityFeePerGas` to `0` and compute `maxFeePerGas` from the current base fee. See [Gas pricing](/en/reference/gas-pricing-api).
* **Block time.** Blocks are produced roughly every 0.7 seconds with single-slot finality. Poll intervals and confirmation thresholds are tuned to this cadence.
* **Retries.** Transient RPC errors are retried idempotently. For financially sensitive flows, inclusion is verified via receipts or logs before downstream state changes.
## Operational ownership
* **Monitoring.** If you run your own nodes, alerts watch block production, peer health, and RPC latency; see [Monitoring](/en/how-to/monitor-node). If you use a third-party RPC, track provider SLAs and failover telemetry.
* **Upgrades.** Protocol releases are tracked so node operators can schedule upgrades; see [Mainnet version history](/en/reference/mainnet-version-history).
* **Runbooks.** Rollback procedures exist for contract pauses, key rotation, and RPC provider switches.
## Support and escalation
* [Developer assistance](/en/reference/developer-assistance): FAQ and reference pointers.
* [Discord](https://discord.gg/stablexyz): community support and protocol updates.
* `bizdev@stable.xyz`: partnership and integration conversations.
## Next recommended
Read the full migration checklist and contract design requirements.
Check mainnet chain parameters and version history.
Pick third-party RPC providers for redundancy.
Wire metrics and alerts for block production and RPC health.
# Troubleshooting guide
Source: https://docs.stable.xyz/en/how-to/troubleshoot-node
Diagnostic scripts and solutions for common Stable node connectivity, sync, and performance issues.
This comprehensive guide helps diagnose and resolve common issues with Stable nodes.
## Quick diagnostics
### Node health check script
```bash theme={"dark"}
#!/bin/bash
# quick-diagnosis.sh
# Set service name (default: stable)
export SERVICE_NAME=stable
echo "=== Stable Node Diagnostics ==="
echo "Timestamp: $(date)"
echo ""
# 1. Service Status
echo "1. SERVICE STATUS:"
systemctl status ${SERVICE_NAME} --no-pager | head -10
# 2. Sync Status
echo -e "\n2. SYNC STATUS:"
curl -s localhost:26657/status | jq '.result.sync_info' 2>/dev/null || echo "RPC not responding"
# 3. Peer Connections
echo -e "\n3. PEER COUNT:"
curl -s localhost:26657/net_info | jq '.result.n_peers' 2>/dev/null || echo "Cannot get peer info"
# 4. Recent Errors
echo -e "\n4. RECENT ERRORS (last 20):"
sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" | grep -i error | tail -20
# 5. System Resources
echo -e "\n5. SYSTEM RESOURCES:"
df -h / | grep -v Filesystem
free -h | grep Mem
top -bn1 | grep "load average"
# 6. Port Status
echo -e "\n6. PORT STATUS:"
ss -tulpn | grep ${SERVICE_NAME} || echo "No ${SERVICE_NAME} ports found"
echo -e "\n=== Diagnostics Complete ==="
```
## Common issues and solutions
### Node won't start
#### Issue: binary not found
**Error message:**
```
stabled: command not found
```
**Solution:**
```bash theme={"dark"}
# Check if binary exists
ls -la /usr/bin/stabled
# If missing, reinstall (use arm64 if needed)
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-0.7.2-linux-amd64-testnet.tar.gz
tar -xvzf stabled-0.7.2-linux-amd64-testnet.tar.gz
sudo mv stabled /usr/bin/
sudo chmod +x /usr/bin/stabled
```
#### Issue: permission denied
**Error message:**
```
Error: open /home/user/.stabled/config/config.toml: permission denied
```
**Solution:**
```bash theme={"dark"}
# Fix ownership
sudo chown -R $USER:$USER ~/.stabled/
# Fix permissions
chmod 700 ~/.stabled/
chmod 600 ~/.stabled/config/*.json
chmod 644 ~/.stabled/config/*.toml
```
#### Issue: address already in use
**Error message:**
```
Error: listen tcp 0.0.0.0:26657: bind: address already in use
```
**Solution:**
```bash theme={"dark"}
# Find process using port
sudo lsof -i :26657
# Kill the process
sudo kill -9
# Or change port in config
sed -i 's/laddr = "tcp:\/\/0.0.0.0:26657"/laddr = "tcp:\/\/0.0.0.0:26658"/' ~/.stabled/config/config.toml
```
### Sync issues
#### Issue: node stuck at certain height
**Symptoms:**
* Block height not increasing
* No new blocks for > 1 minute
**Solution:**
```bash theme={"dark"}
# 1. Check peers
curl localhost:26657/net_info | jq '.result.n_peers'
# If no peers, add persistent peers
echo "persistent_peers = \"5ed0f977a26ccf290e184e364fb04e268ef16430@37.187.147.27:26656,128accd3e8ee379bfdf54560c21345451c7048c7@37.187.147.22:26656\"" >> ~/.stabled/config/config.toml
# 2. Reset and resync
sudo systemctl stop ${SERVICE_NAME}
stabled comet unsafe-reset-all --keep-addr-book
sudo systemctl start ${SERVICE_NAME}
# 3. Use snapshot (see Snapshots guide)
```
#### Issue: "wrong Block.Header.AppHash" error
**Error message:**
```
panic: Wrong Block.Header.AppHash. Expected XXXX, got YYYY
```
**Solution:**
```bash theme={"dark"}
# This indicates state corruption - rollback to previous block
sudo systemctl stop ${SERVICE_NAME}
# Rollback one block
stabled rollback
# Restart node
sudo systemctl start ${SERVICE_NAME}
# If rollback doesn't work, restore from snapshot
# Backup important files
cp ~/.stabled/config/priv_validator_key.json ~/backup/
cp ~/.stabled/config/node_key.json ~/backup/
# Reset state
stabled comet unsafe-reset-all
# Restore from snapshot
wget https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4
tar -I lz4 -xf snapshot.tar.lz4 -C ~/.stabled/
sudo systemctl start ${SERVICE_NAME}
```
#### Issue: slow sync speed
**Symptoms:**
* Less than 100 blocks/minute
* High CPU/disk usage
**Solution:**
```bash theme={"dark"}
# 1. Check disk I/O
iostat -x 1 5
# 2. Optimize configuration
cat >> ~/.stabled/config/config.toml <> ~/.stabled/config/config.toml < db_dump.txt
# 4. If repair fails, resync
rm -rf ~/.stabled/data
# Restore from snapshot
# 5. Start node
sudo systemctl start ${SERVICE_NAME}
```
#### Issue: "too many open files"
**Error message:**
```
accept: too many open files
```
**Solution:**
```bash theme={"dark"}
# 1. Check current limits
ulimit -n
# 2. Increase limits
echo "* soft nofile 65535" | sudo tee -a /etc/security/limits.conf
echo "* hard nofile 65535" | sudo tee -a /etc/security/limits.conf
# 3. Update systemd service
sudo sed -i '/\[Service\]/a LimitNOFILE=65535' /etc/systemd/system/stabled.service
# 4. Reload and restart
sudo systemctl daemon-reload
sudo systemctl restart ${SERVICE_NAME}
```
### Memory issues
#### Issue: out of memory (OOM) kills
**Symptoms:**
```
stabled.service: Main process exited, code=killed, status=9/KILL
```
**Solution:**
```bash theme={"dark"}
# 1. Check memory usage
free -h
dmesg | grep -i "killed process"
# 2. Add swap space
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# 3. Optimize memory usage
cat >> ~/.stabled/config/app.toml < $OUTPUT_DIR/system.txt
df -h >> $OUTPUT_DIR/system.txt
free -h >> $OUTPUT_DIR/system.txt
# Service status
systemctl status ${SERVICE_NAME} --no-pager > $OUTPUT_DIR/service-status.txt
# Recent logs
sudo journalctl -u ${SERVICE_NAME} --since "1 hour ago" > $OUTPUT_DIR/recent-logs.txt
# Config files (remove sensitive data)
grep -v "priv" ~/.stabled/config/config.toml > $OUTPUT_DIR/config.toml
grep -v "priv" ~/.stabled/config/app.toml > $OUTPUT_DIR/app.toml
# Node status
curl -s localhost:26657/status > $OUTPUT_DIR/node-status.json 2>/dev/null
# Create archive
tar -czf $OUTPUT_DIR.tar.gz $OUTPUT_DIR/
echo "Debug info collected: $OUTPUT_DIR.tar.gz"
echo "Share this file when requesting support"
```
## Next steps
* Review [Monitoring Setup](/en/how-to/monitor-node) to prevent issues
* Check [Upgrade Guide](/en/how-to/upgrade-node) for version-specific issues
# Upgrade guide
Source: https://docs.stable.xyz/en/how-to/upgrade-node
Node upgrade procedures, upgrade types, and rollback strategies for Stable network version changes.
This guide covers the upgrade process for Stable nodes, including upgrade procedures and rollback strategies.
> For complete version history and upgrade details, see [Version History](/en/reference/testnet-version-history).
## Upgrade types
### Soft upgrades (non-breaking)
* Can be performed at any time
* Backward compatible
### Hard upgrades (breaking)
* Requires upgrade at specific height
* Not backward compatible
### Emergency upgrades
* Critical security fixes
* Immediate action required
* May require chain halt
## Standard upgrade procedure
### Step 1: preparation
```bash theme={"dark"}
# Check current version
stabled version --long
# Backup critical data
cp -r ~/.stabled/config ~/stable-backup-$(date +%Y%m%d)/
# For validators only: Backup validator state
cp ~/.stabled/data/priv_validator_state.json ~/stable-backup-$(date +%Y%m%d)/
# Check disk space (need 2x current data size)
df -h ~/.stabled
```
### Step 2: download new binary
```bash theme={"dark"}
# For v1.2.0-rc1 upgrade (January 22, 2026)
# Choose your architecture:
# Linux AMD64
BINARY_URL="https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-amd64-testnet.tar.gz"
# OR Linux ARM64
BINARY_URL="https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/binary/stabled-1.2.0-rc1-linux-arm64-testnet.tar.gz"
# Download new binary
wget $BINARY_URL
# Extract to temporary location
tar -xvzf stabled-1.2.0-rc1-linux-*.tar.gz -C /tmp/
# Verify new version
/tmp/stabled version --long
```
### Step 3: perform upgrade
#### For soft upgrades
```bash theme={"dark"}
# Stop node
sudo systemctl stop ${SERVICE_NAME}
# Backup current binary
sudo mv /usr/bin/stabled /usr/bin/stabled.backup
# Install new binary
sudo mv /tmp/stabled /usr/bin/stabled
sudo chmod +x /usr/bin/stabled
# Verify installation
stabled version --long
# Start node
sudo systemctl start ${SERVICE_NAME}
# Monitor logs
sudo journalctl -u ${SERVICE_NAME} -f
```
#### For hard upgrades
```bash theme={"dark"}
# Monitor for upgrade height
while true; do
HEIGHT=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height')
echo "Current height: $HEIGHT"
if [ $HEIGHT -ge $UPGRADE_HEIGHT ]; then
break
fi
sleep 10
done
# Node will halt automatically at upgrade height
# Wait for halt message in logs
sudo journalctl -u ${SERVICE_NAME} -f | grep "UPGRADE"
# Once halted, perform upgrade
sudo systemctl stop ${SERVICE_NAME}
sudo mv /usr/bin/stabled /usr/bin/stabled.backup
sudo mv /tmp/stabled /usr/bin/stabled
# Start with new binary
sudo systemctl start ${SERVICE_NAME}
```
### Step 4: post-upgrade verification
```bash theme={"dark"}
# Check node status
curl -s localhost:26657/status | jq '.result'
# Verify version
curl -s localhost:26657/status | jq '.result.node_info.version'
# Check peers
curl -s localhost:26657/net_info | jq '.result.n_peers'
# Monitor sync status
watch -n 2 'curl -s localhost:26657/status | jq ".result.sync_info"'
# Check for errors
sudo journalctl -u ${SERVICE_NAME} --since "10 minutes ago" | grep -i error
```
## Cosmovisor setup (automated upgrades)
Cosmovisor automates the upgrade process for coordinated upgrades.
### Installation
```bash theme={"dark"}
# Install cosmovisor
go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest
# Or download binary
wget https://github.com/cosmos/cosmos-sdk/releases/download/cosmovisor%2Fv1.7.0/cosmovisor-v1.7.0-linux-amd64.tar.gz
tar -xzf cosmovisor-v1.7.0-linux-amd64.tar.gz
sudo mv cosmovisor /usr/bin/
```
### Configuration
```bash theme={"dark"}
# Set environment variables
cat >> ~/.bashrc < /dev/null < > export.json
# 3. Wait for coordinated restart instructions
```
## Next steps
* [Version History](/en/reference/testnet-version-history) - Complete upgrade history and release notes
* [Monitor your node](/en/how-to/monitor-node) after upgrades
* Review [Troubleshooting](/en/how-to/troubleshoot-node) for common issues
# Snapshots & sync guide
Source: https://docs.stable.xyz/en/how-to/use-node-snapshots
Snapshot-based sync methods for rapidly bootstrapping Stable full and archive nodes.
This guide covers various methods to synchronize your Stable node quickly using snapshots and state sync.
## Sync methods overview
| Method | Sync Time | Storage Required | Use Case |
| -------------------- | --------- | ---------------- | ------------------------------ |
| **Pruned Snapshot** | \~10 min | \< 5 GiB | Regular full nodes |
| **Archive Snapshot** | \~1 hours | \~500 GB | Archive nodes, block explorers |
## Official snapshots
Stable provides official snapshots updated daily (00:00 UTC).
### Snapshot information
| Type | Compression | Size | URL | Update Frequency |
| ----------- | ----------- | -------- | -------------------------------------------------------------------------------------------------------- | ---------------- |
| **Pruned** | LZ4 | \< 5 GiB | [Download](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/snapshot.tar.lz4) | Daily |
| **Archive** | ZSTD | \~300 GB | [Download](https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/stable_archive.tar.zst) | Weekly |
| Type | Compression | Size | URL | Update Frequency |
| ----------- | ----------- | -------- | -------------------------------------------------------------------------------------------------------- | ---------------- |
| **Pruned** | LZ4 | \< 5 GiB | [Download](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4) | Daily |
| **Archive** | ZSTD | \~800 GB | [Download](https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/stable_archive.tar.zst) | Weekly |
## Using pruned snapshots
Pruned snapshots contain recent blockchain state (last 100-1000 blocks).
### Step 1: set environment variable
```bash theme={"dark"}
# Set service name (default: stable)
export SERVICE_NAME=stable
```
### Step 2: stop node service
```bash theme={"dark"}
# Stop the running node
sudo systemctl stop ${SERVICE_NAME}
# Verify it's stopped
sudo systemctl status ${SERVICE_NAME}
```
### Step 3: backup current data (optional)
```bash theme={"dark"}
# Create backup directory
mkdir -p ~/stable-backup
# Backup current state (optional, requires significant space)
cp -r ~/.stabled/data ~/stable-backup/
```
### Step 4: download and extract pruned snapshot
```bash theme={"dark"}
# Install dependencies
sudo apt install -y wget zstd pv
# Create snapshot directory
mkdir -p ~/snapshot
cd ~/snapshot
# Download pruned snapshot with progress
wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/snapshot.tar.lz4
# Remove old data
rm -rf ~/.stabled/data/*
# Extract snapshot with progress indicator
pv stable_pruned.tar.zst | zstd -d -c | tar -xf - -C ~/.stabled/
# Alternative extraction without pv
zstd -d stable_pruned.tar.zst -c | tar -xvf - -C ~/.stabled/
# Clean up
rm stable_pruned.tar.zst
```
```bash theme={"dark"}
# Install dependencies
sudo apt install -y wget lz4 pv
# Create snapshot directory
mkdir -p ~/snapshot
cd ~/snapshot
# Download pruned snapshot with progress
wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4
# Alternative: Download with resume support
curl -C - -o snapshot.tar.lz4 https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/snapshot.tar.lz4
# Remove old data
rm -rf ~/.stabled/data/*
# Extract snapshot with progress indicator
pv snapshot.tar.lz4 | tar -I lz4 -xf - -C ~/.stabled/
# Alternative extraction without pv
tar -I lz4 -xvf snapshot.tar.lz4 -C ~/.stabled/
# Clean up
rm snapshot.tar.lz4
```
### Step 5: restart node
```bash theme={"dark"}
# Start the node
sudo systemctl start ${SERVICE_NAME}
# Check status
sudo systemctl status ${SERVICE_NAME}
# Monitor logs
sudo journalctl -u stabled -f
```
## Using archive snapshots
Archive snapshots contain complete blockchain history.
### Step 1: prepare system
```bash theme={"dark"}
# Stop node
sudo systemctl stop ${SERVICE_NAME}
# Install dependencies
sudo apt install -y wget zstd pv
# Check available disk space (need 2x snapshot size)
df -h ~/.stabled
```
### Step 2: download and extract archive snapshot
```bash theme={"dark"}
# Create working directory
mkdir -p ~/snapshot
cd ~/snapshot
# Download archive snapshot
wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/mainnet/snapshots/stable_archive.tar.zst
# Clear old data
rm -rf ~/.stabled/data/*
# Extract with high memory for better performance
pv stable_archive.tar.zst | zstd -d --long=31 --memory=2048MB -c - | tar -xf - -C ~/.stabled/
# Alternative: Standard extraction
zstd -d --long=31 stable_archive.tar.zst -c | tar -xvf - -C ~/.stabled/
# Clean up
rm stable_archive.tar.zst
```
```bash theme={"dark"}
# Create working directory
mkdir -p ~/snapshot
cd ~/snapshot
# Download archive snapshot
wget -c https://stable-data-dist.s3.us-east-1.amazonaws.com/testnet/snapshots/stable_archive.tar.zst
# Clear old data
rm -rf ~/.stabled/data/*
# Extract with high memory for better performance
pv archive.tar.zst | zstd -d --long=31 --memory=2048MB -c - | tar -xf - -C ~/.stabled/
# Alternative: Standard extraction
zstd -d --long=31 archive.tar.zst -c | tar -xvf - -C ~/.stabled/
# Clean up
rm archive.tar.zst
```
### Step 3: start node
```bash theme={"dark"}
# Start service
sudo systemctl start ${SERVICE_NAME}
# Verify sync status
curl -s localhost:26657/status | jq '.result.sync_info'
```
## Creating your own snapshots
### Manual snapshot creation
```bash theme={"dark"}
# Stop node
sudo systemctl stop ${SERVICE_NAME}
# Create snapshot archive
cd ~/.stabled
tar -cf - data/ | lz4 -9 > ~/stable-snapshot-$(date +%Y%m%d).tar.lz4
# Create checksum
sha256sum ~/stable-snapshot-*.tar.lz4 > checksums.txt
# Restart node
sudo systemctl start ${SERVICE_NAME}
```
### Automated snapshot script
```bash theme={"dark"}
#!/bin/bash
# snapshot.sh - Automated snapshot creation
# Configuration
SNAPSHOT_DIR="/var/snapshots"
STABLED_HOME="$HOME/.stabled"
KEEP_DAYS=7
# Create snapshot directory
mkdir -p $SNAPSHOT_DIR
# Stop node
sudo systemctl stop ${SERVICE_NAME}
# Create snapshot
SNAPSHOT_NAME="stable-snapshot-$(date +%Y%m%d-%H%M%S).tar.lz4"
tar -cf - -C $STABLED_HOME data/ | lz4 -9 > $SNAPSHOT_DIR/$SNAPSHOT_NAME
# Generate metadata
cat > $SNAPSHOT_DIR/latest.json <
Run your first testnet transaction in five minutes.
Validate an integration before shipping to mainnet.
Common answers about chain IDs, endpoints, and onboarding.
# Indexers
Source: https://docs.stable.xyz/en/reference/indexers
Indexing and analytics providers for Stable including The Graph, Goldsky, Allium, Stablescan, and CoinGecko.
Indexers and analytics platforms provide structured access to on-chain data, enabling developers to query transactions, balances, logs, events, and application-specific data at scale. Stable is EVM-compatible, so standard Ethereum indexing tools work seamlessly.
This page lists current and upcoming indexing providers, along with the capabilities developers can expect.
## Overview table
| **Provider** | **Category** | **Docs / Get Started** | **Notes** |
| :------------------------------------------------------------------- | :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------- |
| [**Stablescan**](https://stablescan.xyz/) | Blockchain Explorer | [https://docs.etherscan.io/introduction](https://docs.etherscan.io/introduction) | Blockchain explorer; transaction, block, and contract visibility. |
| [**The Graph**](https://thegraph.com/explorer/participants/indexers) | Indexer | [https://thegraph.com/docs/en/developing/creating-a-subgraph/](https://thegraph.com/docs/en/developing/creating-a-subgraph/) | Build, deploy, and query subgraphs using GraphQL. |
| [**Goldsky**](https://goldsky.com/) | Indexer | [https://docs.goldsky.com/introduction](https://docs.goldsky.com/introduction) | High-performance indexing and real-time data streaming. |
| [**Ormi Labs**](https://ormilabs.com/) | Indexer | [https://docs.ormilabs.com/subgraphs/quickstart](https://docs.ormilabs.com/subgraphs/quickstart) | Next-gen subgraph indexer with real-time data capabilities. |
| [**Allium**](https://www.allium.so/) | Analytics / Data Platform | [https://docs.allium.so/](https://docs.allium.so/) | Normalized blockchain datasets & analytics tooling. |
| [**Chaos Labs**](https://chaoslabs.xyz/) | Analytics / Risk Intelligence | [https://chaoslabs.xyz/about](https://chaoslabs.xyz/about) | Institutional-grade risk models and protocol analytics. |
| [**CoinMarketCap**](https://coinmarketcap.com/api/) | Market Data Aggregator | [https://coinmarketcap.com/api/documentation/v1/](https://coinmarketcap.com/api/documentation/v1/) | Market prices, listings, and tracking tools. |
| [**CoinGecko**](https://www.coingecko.com/en/api) | Market Data Aggregator | [https://docs.coingecko.com/](https://docs.coingecko.com/) | Independent market data with developer APIs. |
| [**Dexscreener**](https://docs.dexscreener.com/) | DEX Analytics | [https://docs.dexscreener.com/](https://docs.dexscreener.com/) | Real-time DEX charts, liquidity analytics, and dashboards. |
| [**DeBank**](https://debank.com/) | Portfolio / Wallet Analytics | [https://cloud.debank.com/](https://cloud.debank.com/) | EVM wallet tracking, transactions, and portfolio insights. |
## 1. Indexers
Indexers transform raw blockchain data into searchable, queryable formats. They power dashboards, analytics, wallets, block explorers, and application backends.
### The Graph
Decentralized indexing protocol powering data access for over 75,000 projects.
**Capabilities**
* Subgraphs for Stable
* GraphQL-based querying
* Distributed indexing network
**Docs**
Follow this quick-start guide to create, deploy, and query a subgraph within minutes:
[https://thegraph.com/docs/en/developing/creating-a-subgraph/](https://thegraph.com/docs/en/developing/creating-a-subgraph/)
### Ormi Labs
Ormi is a next-generation indexer built to deliver real-time and historical blockchain data at scale.
**Capabilities**
* Tip of the chain indexing
* Sub-second query latency
* No-code feature
**Docs**
Start querying real-time data with Ormi:
[https://docs.ormilabs.com/subgraphs/quickstart](https://docs.ormilabs.com/subgraphs/quickstart)
Learn how to query USDT0 data on Stable in real-time:
[https://docs.ormilabs.com/subgraphs/tutorials/query-usdt0](https://docs.ormilabs.com/subgraphs/tutorials/query-usdt0)
### Goldsky
High-performance indexing platform with instant subgraphs and developer tooling.
**Capabilities**
* Subgraph deployment and management
* Webhook creation for real-time event streaming
* Multi-subgraph sync to external databases
**Docs**
[https://docs.goldsky.com/introduction](https://docs.goldsky.com/introduction)
For 24/7 support, contact [support@goldsky.com](mailto:support@goldsky.com).
## 2. Analytics providers
Analytics tools help teams track network activity, real-world payments, dashboards, usage flows, and contract interactions.
### Allium
A foundational data platform for engineers and analysts across the blockchain ecosystem.
**Capabilities**
* Normalized blockchain datasets
* Query tooling for analytics teams
* Enterprise data infrastructure
**Docs**
Get started with Allium’s data platform and API resources:
[https://www.allium.so/](https://www.allium.so/)
**Get started**: Sign up at [allium.so](https://www.allium.so/) to get API access, then query normalized Stable on-chain datasets using the REST API.
### Chaos Labs
Institutional-grade intelligence platform supporting over 3 trillion transactions.
**Capabilities**
* Risk modeling
* Protocol analytics
* Market monitoring tools
**Docs**
Discover Chaos Labs’ research, dashboards, and analytics tools:
[https://chaoslabs.xyz/](https://chaoslabs.xyz/)
**Get started**: Contact Chaos Labs through [chaoslabs.xyz](https://chaoslabs.xyz/) to access risk models and analytics dashboards covering Stable protocol activity.
### CoinMarketCap
The largest global market tracking platform for crypto assets.
**Capabilities**
* Price tracking for Stable assets
* Portfolio tool
* Market data APIs
**Docs**
Explore the CMC API and integration resources:
[https://coinmarketcap.com/api/](https://coinmarketcap.com/api/)
**Get started**: Register for an API key at [coinmarketcap.com/api](https://coinmarketcap.com/api/) and query Stable asset price and market data via the REST API.
### CoinGecko
The world’s largest independent crypto data aggregator.
**Capabilities**
* Market listings and price data
* Historical analytics
* API access for developers
**Docs**
Access CoinGecko’s API documentation:
[https://www.coingecko.com/en/api](https://www.coingecko.com/en/api)
**Get started**: Get a free or Pro API key from [coingecko.com/en/api](https://www.coingecko.com/en/api) and query Stable token price and market data via the REST API.
### Dexscreener
Real-time charts and analytics for decentralized venues.
**Capabilities**
* Live DEX charts
* Liquidity and pair analytics
* Trading dashboards
**Docs**
Explore Dexscreener’s API endpoints and developer tools:
[https://docs.dexscreener.com/](https://docs.dexscreener.com/)
**Get started**: Browse the [Dexscreener API docs](https://docs.dexscreener.com/) to query real-time pair data, liquidity, and trading activity for Stable-based DEX pools.
### DeBank
Portfolio tracker for Ethereum and EVM ecosystems.
**Capabilities**
* Wallet analytics
* Transaction summaries
* Portfolio tracking across chains
**Docs**
Read DeBank’s API reference and integration documentation:
[https://docs.debank.com/](https://docs.debank.com/)
**Get started**: Sign up for [DeBank Cloud](https://cloud.debank.com/) to access API keys and query Stable wallet balances, transaction history, and portfolio data.
***
Have an indexing or analytics platform integrating Stable? Reach out at [bizdev@stable.xyz](mailto:bizdev@stable.xyz).
# Node configuration
Source: https://docs.stable.xyz/en/reference/node-configuration
wha---
title: Node configuration guide
description: "Configuration reference for config.toml and app.toml settings on Stable validator and full nodes."
diataxis: "reference"
---------------------
This guide covers all configuration options for Stable nodes, including optimization for different use cases.
## Configuration files overview
Stable nodes use two main configuration files:
* **`config.toml`**: Core StableBFT configuration
* **`app.toml`**: Application-specific configuration
Both files are located in `~/.stabled/config/`
## Core configuration (config.toml)
### Basic settings
```toml theme={"dark"}
# The ID of the chain to join
chain_id = "stable_988-1"
# A custom human-readable name for this node
moniker = "your-node-name"
# Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb
db_backend = "goleveldb"
```
```toml theme={"dark"}
# The ID of the chain to join
chain_id = "stabletestnet_2201-1"
# A custom human-readable name for this node
moniker = "your-node-name"
# Database backend: goleveldb | cleveldb | boltdb | rocksdb | badgerdb
db_backend = "goleveldb"
```
### P2P configuration
```toml theme={"dark"}
[p2p]
# Address to listen for incoming connections
laddr = "tcp://0.0.0.0:26656"
# Address to advertise to peers for them to dial
external_address = "YOUR_PUBLIC_IP:26656"
# Comma separated list of seed nodes
seeds = "17a539fda42863a99755547e1c9b3ec4c38a4439@seed1.stable.xyz:26656"
# Comma separated list of persistent peers
persistent_peers = "b896f6f8ca5a4d1cc40de09407df0c96e76df950@peer1.stable.xyz:26656"
```
```toml theme={"dark"}
[p2p]
# Address to listen for incoming connections
laddr = "tcp://0.0.0.0:26656"
# Address to advertise to peers for them to dial
external_address = "YOUR_PUBLIC_IP:26656"
# Comma separated list of seed nodes
seeds = "39e061b167162f6621ddadcf1be21d6fa585a468@seed1.testnet.stable.xyz:26656"
# Comma separated list of persistent peers
persistent_peers = "5ed0f977a26ccf290e184e364fb04e268ef16430@peer1.testnet.stable.xyz:26656"
```
Additional P2P settings (same for both networks):
```toml theme={"dark"}
# Maximum number of inbound peers
max_num_inbound_peers = 50
# Maximum number of outbound peers
max_num_outbound_peers = 30
# Toggle to disable guard against peers connecting from the same ip
allow_duplicate_ip = false
# Peer connection configuration
handshake_timeout = "20s"
dial_timeout = "3s"
# Time to wait before flushing messages out on the connection
flush_throttle_timeout = "100ms"
# Maximum size of a message packet payload
max_packet_msg_payload_size = 1024
# Rate limiting
send_rate = 5120000 # 5 MB/s
recv_rate = 5120000 # 5 MB/s
# Seed mode (for seed nodes only)
seed_mode = false
# Enable peer exchange reactor
pex = true
```
### RPC server configuration
```toml theme={"dark"}
[rpc]
# TCP or UNIX socket address for the RPC server
laddr = "tcp://127.0.0.1:26657"
# A list of origins a cross-domain request can be executed from
cors_allowed_origins = ["*"]
# A list of methods the client is allowed to use with cross-domain requests
cors_allowed_methods = ["HEAD", "GET", "POST"]
# A list of non simple headers the client is allowed to use with cross-domain requests
cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"]
# TCP or UNIX socket address for the gRPC server
grpc_laddr = "tcp://127.0.0.1:9090"
# Maximum number of simultaneous connections
grpc_max_open_connections = 900
# Activate unsafe RPC commands like /dial_seeds and /unsafe_flush_mempool
unsafe = false
# Maximum number of simultaneous connections (including WebSocket)
max_open_connections = 900
# Maximum number of unique clientIDs that can connect
max_subscription_clients = 100
# Maximum number of unique queries a given client can subscribe to
max_subscriptions_per_client = 5
# How long to wait for a tx to be committed
timeout_broadcast_tx_commit = "10s"
# Maximum size of request body
max_body_bytes = 1000000
# Maximum size of request header
max_header_bytes = 1048576
```
### Mempool configuration
```toml theme={"dark"}
[mempool]
# Mempool version to use
version = "v1"
# Recheck enabled
recheck = true
# Broadcast enabled
broadcast = true
# Maximum number of transactions in the mempool
size = 3000
# Limit the total size of all txs in the mempool
max_txs_bytes = 1073741824 # 1GB
# Size of the cache
cache_size = 10000
# Do not remove invalid transactions from the cache
keep-invalid-txs-in-cache = false
# Maximum size of a single transaction
max_tx_bytes = 1048576 # 1MB
# Maximum size of a batch of transactions to send to a peer
max_batch_bytes = 0
```
### Consensus configuration
```toml theme={"dark"}
[consensus]
# How long we wait for a proposal block before prevoting nil
timeout_propose = "5s"
# How much timeout_propose increases with each round
timeout_propose_delta = "10ms"
# How long we wait after receiving +2/3 prevotes
timeout_prevote = "150ms"
# How much the timeout_prevote increases with each round
timeout_prevote_delta = "10ms"
# How long we wait after receiving +2/3 precommits
timeout_precommit = "150s"
# How much the timeout_precommit increases with each round
timeout_precommit_delta = "10ms"
# Make progress as soon as we have all the precommits
skip_timeout_commit = false
# Enable/disable double sign check
double_sign_check_height = 2
# EmptyBlocks mode
create_empty_blocks = true
create_empty_blocks_interval = "0s"
# Reactor sleep duration
peer_gossip_sleep_duration = "100ms"
peer_query_maj23_sleep_duration = "2s"
```
## Application configuration (app.toml)
### Basic application settings
```toml theme={"dark"}
# Pruning strategy
pruning = "default"
# HaltHeight contains a non-zero block height at which a node will halt
halt-height = 0
# HaltTime contains a non-zero time at which a node will halt
halt-time = 0
# MinRetainBlocks defines the number of blocks for which a node will retain
min-retain-blocks = 0
# InterBlockCache enables inter-block caching
inter-block-cache = true
# IndexEvents defines the set of events in the form {eventType}.{attributeKey}
index-events = []
# IavlCacheSize set the size of the iavl tree cache
iavl-cache-size = 781250
```
### API configuration
```toml theme={"dark"}
[api]
# Enable defines if the API server should be enabled
enable = true
# Swagger defines if swagger documentation should automatically be registered
swagger = true
# Address defines the API server to listen on
address = "tcp://0.0.0.0:1317"
# MaxOpenConnections defines the number of maximum open connections
max-open-connections = 1000
# EnabledUnsafeCORS defines if CORS should be enabled
enabled-unsafe-cors = true
```
### gRPC configuration
```toml theme={"dark"}
[grpc]
# Enable defines if the gRPC server should be enabled
enable = true
# Address defines the gRPC server address to bind to
address = "0.0.0.0:9090"
```
### EVM JSON-RPC configuration
```toml theme={"dark"}
[json-rpc]
# Enable the JSON-RPC server
enable = true
# Address to bind the JSON-RPC server
address = "0.0.0.0:8545"
# Address to bind the WebSocket server
ws-address = "0.0.0.0:8546"
# APIs to enable
api = "eth,net,web3,txpool,personal,debug"
# Gas cap for eth_call/estimateGas
gas-cap = 25000000
# EVM timeout for eth_call/estimateGas
evm-timeout = "5s"
# Tx fee cap for transactions
txfee-cap = 1
# Filter cap for eth_getLogs
filter-cap = 200
# FeeHistory cap
feehistory-cap = 100
# Block range cap for eth_getLogs
logs-cap = 10000
# Block range cap
block-range-cap = 10000
# HTTP timeout
http-timeout = "30s"
# HTTP idle timeout
http-idle-timeout = "120s"
# Allow unprotected transactions
allow-unprotected-txs = true
# Maximum number of transactions in the pool
max-tx-in-pool = 3000
# Enable indexer
enable-indexer = false
# Enable metrics
metrics = true
```
## Configuration profiles
### Full node (default)
Balanced configuration for full nodes:
```bash theme={"dark"}
# config.toml adjustments
sed -i 's/^indexer = ".*"/indexer = "kv"/' ~/.stabled/config/config.toml
sed -i 's/^max_num_inbound_peers = .*/max_num_inbound_peers = 50/' ~/.stabled/config/config.toml
sed -i 's/^max_num_outbound_peers = .*/max_num_outbound_peers = 30/' ~/.stabled/config/config.toml
# app.toml adjustments
sed -i 's/^pruning = ".*"/pruning = "default"/' ~/.stabled/config/app.toml
sed -i 's/^snapshot-interval = .*/snapshot-interval = 1000/' ~/.stabled/config/app.toml
```
### Archive node
No pruning, full history:
```bash theme={"dark"}
# config.toml adjustments
sed -i 's/^indexer = ".*"/indexer = "kv"/' ~/.stabled/config/config.toml
# app.toml adjustments
sed -i 's/^pruning = ".*"/pruning = "nothing"/' ~/.stabled/config/app.toml
```
### RPC node
Public RPC endpoint configuration:
```bash theme={"dark"}
# config.toml adjustments
sed -i 's/^max_num_inbound_peers = .*/max_num_inbound_peers = 30/' ~/.stabled/config/config.toml
sed -i 's/^max_open_connections = .*/max_open_connections = 30/' ~/.stabled/config/config.toml
sed -i 's/^cors_allowed_origins = .*/cors_allowed_origins = ["*"]/' ~/.stabled/config/config.toml
# app.toml adjustments
sed -i 's/^enable = .*/enable = true/' ~/.stabled/config/app.toml
sed -i 's/^swagger = .*/swagger = true/' ~/.stabled/config/app.toml
sed -i 's/^enabled-unsafe-cors = .*/enabled-unsafe-cors = true/' ~/.stabled/config/app.toml
```
## Monitoring configuration
### Prometheus metrics
```toml theme={"dark"}
# config.toml
[instrumentation]
# Enable Prometheus metrics
prometheus = true
# Metrics listen address
prometheus_listen_addr = ":26660"
# Namespace for metrics
namespace = "stablebft"
```
### Logging
```toml theme={"dark"}
# config.toml
[log]
# Log level (trace|debug|info|warn|error|fatal|panic)
level = "info"
# Log format (plain|json)
format = "plain"
```
## Applying configuration changes
After making configuration changes:
```bash theme={"dark"}
# Restart the node
sudo systemctl restart ${SERVICE_NAME}
# Check logs for errors
sudo journalctl -u ${SERVICE_NAME} -f
# Verify configuration loaded
curl localhost:26657/status | jq '.result.node_info'
```
## Next steps
* [Set up Monitoring](/en/how-to/monitor-node) for your node
* Review [Troubleshooting Guide](/en/how-to/troubleshoot-node) for common issues
# Operate
Source: https://docs.stable.xyz/en/reference/node-operations-overview
Run a Stable node. Install, configure, monitor, and upgrade.
Operate covers running a Stable node: full or archive, testnet or mainnet, from install through monitoring. For the chain-level behavior your node enforces (fee model, finality, USDT0 as gas), see [Gas pricing](/en/explanation/gas-pricing), [Finality](/en/explanation/finality), and the [Architecture overview](/en/explanation/core-optimization-overview).
## Quick links
* **[System Requirements](/en/reference/node-system-requirements)** - Hardware and software requirements for different node types
* **[Installation Guide](/en/how-to/install-node)** - Step-by-step installation instructions for various platforms
* **[Configuration](/en/reference/node-configuration)** - Detailed configuration options and best practices
* **[Snapshots & Sync](/en/how-to/use-node-snapshots)** - Fast sync options using snapshots
* **[Upgrade Guide](/en/how-to/upgrade-node)** - Node upgrade procedures and version history
* **[Monitoring](/en/how-to/monitor-node)** - Tools and metrics for node monitoring
* **[Troubleshooting](/en/how-to/troubleshoot-node)** - Common issues and solutions
## Node types
### Full node
A full node maintains a complete copy of the blockchain and validates all transactions and blocks. Full nodes:
* Verify all transactions and blocks
* Maintain the entire blockchain history
* Can serve data to other nodes
* Support the network's decentralization
### Archive node
An archive node stores the complete history of all states and can serve historical queries. Archive nodes:
* Store all historical states
* Support historical queries at any block height
* Require significantly more storage
* Essential for block explorers and analytics
## Network information
For complete network details including RPC endpoints, block explorers, and chain parameters, see:
* **[Mainnet](/en/reference/mainnet-information)** - Mainnet details
* **[Testnet](/en/reference/testnet-information)** - Testnet details
## Support and community
* **Discord**: [Join the Stable Discord](https://discord.gg/stablexyz)
## Quick start
For experienced operators who want to get started quickly:
1. Check [System Requirements](/en/reference/node-system-requirements)
2. Follow the [Installation Guide](/en/how-to/install-node)
3. Configure your node using [Configuration Guide](/en/reference/node-configuration)
4. Speed up sync with [Snapshots](/en/how-to/use-node-snapshots)
5. Monitor your node with [Monitoring Guide](/en/how-to/monitor-node)
For network parameters and RPC endpoints, see [Mainnet Information](/en/reference/mainnet-information) or [Testnet Information](/en/reference/testnet-information).
## How node ops connect to the chain
Running a node means enforcing Stable's chain-level rules. These pages explain the behavior your node implements:
* **[Contracts overview](/en/explanation/contracts-overview)** covers the fee model, JSON-RPC surface, and system modules your node serves.
* **[Finality](/en/explanation/finality)** explains single-slot finality and what "confirmed" means at the consensus layer.
* **[Architecture overview](/en/explanation/core-optimization-overview)** walks through consensus, execution, database, and RPC layers.
* **[Gas pricing](/en/explanation/gas-pricing)** explains how USDT0-denominated fees are priced and collected.
# System requirements
Source: https://docs.stable.xyz/en/reference/node-system-requirements
Hardware and software requirements for running full, archive, and validator nodes on Stable.
This page outlines the hardware and software requirements for running different types of Stable nodes.
## Hardware requirements
### Full node (minimum)
| Component | Requirement | Notes |
| ----------- | ----------------------------- | ---------------------------------------- |
| **CPU** | 4 cores | AMD Ryzen 5 / Intel Core i5 or better |
| **RAM** | 8 GB | 16 GB recommended for better performance |
| **Storage** | 500 GB NVMe/SSD | Write throughput > 1000 MiBps required |
| **Network** | 100 Mbps | Stable, low-latency connection |
| **OS** | Ubuntu 22.04/24.04, Debian 12 | 64-bit Linux required |
### Full node (recommended)
| Component | Requirement | Notes |
| ----------- | ------------ | ------------------------------------- |
| **CPU** | 8 cores | AMD Ryzen 7 / Intel Core i7 or better |
| **RAM** | 16 GB | 32 GB for optimal performance |
| **Storage** | 1 TB NVMe | Write throughput > 2000 MiBps |
| **Network** | 1 Gbps | Dedicated connection preferred |
| **OS** | Ubuntu 24.04 | Latest LTS recommended |
### Archive node
| Component | Requirement | Notes |
| ----------- | ------------ | ----------------------------------------- |
| **CPU** | 16 cores | AMD Ryzen 9 / Intel Core i9 or equivalent |
| **RAM** | 32 GB | 64 GB recommended |
| **Storage** | 4 TB NVMe | Fast growing, plan for expansion |
| **Network** | 1 Gbps | Unmetered connection required |
| **OS** | Ubuntu 24.04 | Latest LTS recommended |
## Software requirements
### Operating system
#### Supported distributions
* **Ubuntu 24.04 LTS** (recommended)
* **Ubuntu 22.04 LTS**
* **Debian 12 (Bookworm)**
#### System dependencies
```bash theme={"dark"}
# Update system packages
sudo apt update && sudo apt upgrade -y
# Install essential tools
sudo apt install -y \
build-essential \
git \
wget \
curl \
jq \
lz4 \
zstd \
htop \
net-tools \
ufw
```
## Network requirements
### Bandwidth usage
| Node Type | Download | Upload | Monthly Data |
| ------------ | -------------- | ------------- | ------------ |
| Full Node | \~50 Mbps avg | \~25 Mbps avg | \~15 TB |
| Archive Node | \~100 Mbps avg | \~50 Mbps avg | \~30 TB |
## Cloud provider recommendations
### AWS
* **Full node**: t3.xlarge or c5.xlarge
* **Archive node**: m5.2xlarge or c5.2xlarge
* **Storage**: gp3 with provisioned IOPS
### Google Cloud
* **Full node**: n2-standard-4
* **Archive node**: n2-standard-8
* **Storage**: pd-ssd or pd-extreme
### Azure
* **Full node**: Standard\_D4s\_v5
* **Archive node**: Standard\_D8s\_v5
* **Storage**: Premium SSD v2
### DigitalOcean
* **Full node**: General Purpose 8GB
* **Archive node**: CPU-Optimized 16GB
* **Storage**: Volume Block Storage
## Monitoring requirements
For production deployments, ensure you have:
* **Prometheus**: For metrics collection
* **Grafana**: For visualization
* **AlertManager**: For alerting
* **Node exporter**: For system metrics
* **Log aggregation**: ELK or Loki recommended
## Security considerations
### System hardening
* Keep OS and packages updated
* Configure automatic security updates
* Use SSH keys only (disable password auth)
* Configure fail2ban
* Enable firewall (UFW/iptables)
* Regular security audits
## Pre-installation checklist
Before proceeding with installation, verify:
* [ ] Hardware meets minimum requirements
* [ ] Operating system is supported and updated
* [ ] Storage has sufficient IOPS
* [ ] Network bandwidth is adequate
* [ ] Firewall rules are configured
* [ ] System monitoring is set up
* [ ] Backup strategy is defined
* [ ] Security measures are in place