SettlX is a Web3 payment platform for global merchants in volatile markets like Nigeria. It lets them accept USDC and receive instant NGN settlements at a locked rate, eliminating FX risk completely.
Instant FX + Insurance for Web3 Payments | Built on Arbitrum Stylus (Rust)
SettlX is a Web3 payment settlement platform that lets global merchants accept stablecoin (USDC) payments and receive guaranteed local-currency (NGN) settlements at a locked exchange rate — eliminating FX volatility risk entirely. Smart contracts written in Rust using Arbitrum Stylus handle escrow, rate locking, and settlement on-chain.
Production: https://settl-x.vercel.app/
Network: Arbitrum Sepolia
A merchant in Lagos sells a product for $100 USDC. By the time they convert it, the FX rate has slipped — and they net only ₦138,000 instead of ₦150,000. Their margin is gone.
SettlX solves this.
When a $100 USDC payment arrives, SettlX instantly quotes the merchant a guaranteed fiat value. The merchant clicks "Lock Rate" — the exchange rate is cryptographically locked on-chain at that exact moment. The merchant receives exactly ₦150,000, regardless of what the market does next. The platform manages the FX exposure in the background.
Crypto payment margins in emerging markets are razor-thin
FX volatility between payment creation and settlement can wipe out merchant profits
No existing solution offers guaranteed, locked-rate settlement at the point of sale
Merchants have no on-chain recourse if rates shift between payment and payout
Payer → SettlX Contract → Merchant
Payer approves USDC to the contract
Payer calls payMerchant()
USDC is held in escrow
Merchant calls acceptPaymentWithRate()
Rate is locked on-chain
USDC is transferred to admin treasury
Admin calls markAsPaid() after NGN transfer
Payment status becomes Paid
Pending (0) — Payment created, awaiting merchant action
Accepted (1) — Rate locked, USDC transferred to admin
Rejected (2) — Merchant rejected, USDC refunded to payer
Paid (3) — Admin confirmed NGN bank transfer sent
The contract is written in Rust using the Arbitrum Stylus SDK and compiled to WASM for on-chain execution. This provides significantly lower gas costs compared to equivalent Solidity contracts.
A payer sends USDC to the contract, which securely holds the funds in escrow until the merchant takes action.
The merchant accepts the payment and locks the FX rate on-chain at that exact moment.
This guarantees the NGN amount they will receive, eliminating volatility risk.
If the merchant declines the transaction, funds are automatically refunded to the payer.
After sending NGN to the merchant’s bank account, the admin confirms the payout on-chain, marking the payment as fully settled.
Merchants register their bank details (stored as hashes for privacy) so off-chain NGN settlements can be executed securely.
| Event | Parameters | Description |
| --------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `MerchantRegistered` | `merchant (indexed)`, `bankName`, `accountName`, `accountNumber` | Emitted on bank detail registration. Contains plaintext strings. |
| `PaymentCreated` | `id (indexed)`, `payer (indexed)`, `merchant (indexed)`, `amount`, `rfce` | Emitted when a payment is created. Contains plaintext `rfce` reference. |
| `PaymentAccepted` | `id (indexed)`, `lockedRate` | Emitted when merchant locks rate. `lockedRate` = NGN × 10^18. |
| `PaymentRejected` | `id (indexed)` | Emitted when merchant rejects payment. |
| `PaymentMarkedAsPaid` | `id (indexed)` | Emitted when admin confirms NGN settlement. |
> **Important:** Because `rfce` and bank details are hashed on-chain, the plaintext values only exist in event logs. Frontend clients should index `PaymentCreated` and `MerchantRegistered` events to display human-readable references and bank info.Rust (stable toolchain)
cargo-stylus CLI
Arbitrum Sepolia ETH for deployment gas
Clone the repository
git clone https://github.com/Chigozie0706/SettlX
cd SettlXFor contract deployment
cd contract-stylusInstall cargo-stylus:
cargo install cargo-stylus
Add WASM target:
rustup target add wasm32-unknown-unknown
Build for Arbitrum Sepolia
cargo stylus check --endpoint https://sepolia-rollup.arbitrum.io/rpcContract Deployment:
Deploy to Arbitrum Sepolia:
cargo stylus deploy \
--endpoint='https://sepolia-rollup.arbitrum.io/rpc' \
--private-key="YOUR_PRIVATE_KEY"Initialize the contract:
cast send CONTRACT_ADDRESS \
"init(address)" \
USDC_TOKEN_ADDRESS \
--rpc-url https://sepolia-rollup.arbitrum.io/rpc \
--private-key YOUR_PRIVATE_KEYExport ABI:
cargo stylus export-abi --json > settlX.json| Contract | Network | Address |
| ------------------ | ---------------- | -------------------------------------------- |
| SettlX | Arbitrum Sepolia | `0x4855dcefa1a1ecf8b2fbd7eae38b6f73a90f48d1` |
| USDC (Test) | Arbitrum Sepolia | `0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d` |
| Chainlink USDC/USD | Arbitrum Sepolia | `0x50834F3163758fcC1Df9973b6e91f0F0F0434aD3` |The frontend is built with Next.js 16, Privy (wallet authentication), Wagmi v2, and Viem.
cd frontend
cd settlX
pnpm install
pnpm run devTransact page for payer approvals and payments
Merchant dashboard to view and lock FX rates
Admin dashboard for full settlement management
Plain text data such as rfce and bank details are recovered from event logs on the frontend.
| Layer | Technology |
| ------------------ | -------------------------- |
| Smart Contract | Rust + Arbitrum Stylus SDK |
| Blockchain | Arbitrum Sepolia (L2) |
| Token Standard | ERC-20 (USDC) |
| Price Oracle | Chainlink AggregatorV3 |
| Frontend Framework | Next.js 16 (App Router) |
| Wallet Auth | Privy |
| Web3 Client | Wagmi v2 + Viem |
| FX Rate API | exchangerate.host |Lower gas costs
Fast finality
Familiar Rust tooling
Strong DeFi ecosystem
MIT OR Apache-2.0
<h1>SettlX — Progress Summary</h1><h2>What We're Building</h2><p><strong>SettlX</strong> is a cross-border payment settlement protocol built on Arbitrum using Stylus (Rust smart contracts). It solves a real problem for Nigerian merchants who receive USDC payments from international customers but need to settle in NGN — without trusting a centralized exchange or intermediary.</p><p>The core flow:</p><ol><li><p><strong>Payer</strong> sends USDC to a merchant via the SettlX contract — funds enter escrow</p></li><li><p><strong>Merchant</strong> accepts the payment and locks the current NGN/USDC exchange rate on-chain</p></li><li><p><strong>Admin</strong> transfers the NGN equivalent to the merchant's bank account, then marks the payment as Paid on-chain</p></li><li><p>If the merchant rejects, USDC is automatically refunded to the payer</p></li></ol><h2>Smart Contract (Stylus / Rust)</h2><p>Deployed on <strong>Arbitrum Sepolia</strong> at <code data-inline="true" spellcheck="false">0x4855dcefa1a1ecf8b2fbd7eae38b6f73a90f48d1</code></p><p><strong>What's complete:</strong></p><ul><li><p>Full payment lifecycle — <code data-inline="true" spellcheck="false">payMerchant</code>, <code data-inline="true" spellcheck="false">acceptPaymentWithRate</code>, <code data-inline="true" spellcheck="false">rejectPayment</code>, <code data-inline="true" spellcheck="false">markAsPaid</code></p></li><li><p>Merchant registration — <code data-inline="true" spellcheck="false">registerMerchantBankDetails</code>, <code data-inline="true" spellcheck="false">updateMerchantBankDetails</code></p></li><li><p>USDC escrow using ERC20 <code data-inline="true" spellcheck="false">transferFrom</code> / <code data-inline="true" spellcheck="false">transfer</code></p></li><li><p>Exchange rate locking — NGN/USDC rate is locked at acceptance time (scaled ×1e18), preventing rate slippage between acceptance and settlement</p></li><li><p>Custom error types for every failure case (<code data-inline="true" spellcheck="false">NotYourPayment</code>, <code data-inline="true" spellcheck="false">AlreadyProcessed</code>, <code data-inline="true" spellcheck="false">InvalidRate</code>, etc.)</p></li><li><p>Payment status machine: <code data-inline="true" spellcheck="false">Pending → Accepted → Paid</code> or <code data-inline="true" spellcheck="false">Pending → Rejected</code></p></li><li><p>Event emission for all state transitions — <code data-inline="true" spellcheck="false">PaymentCreated</code>, <code data-inline="true" spellcheck="false">PaymentAccepted</code>, <code data-inline="true" spellcheck="false">PaymentRejected</code>, <code data-inline="true" spellcheck="false">PaymentMarkedAsPaid</code>, <code data-inline="true" spellcheck="false">MerchantRegistered</code>, <code data-inline="true" spellcheck="false">MerchantUpdated</code></p></li><li><p>Bank details stored as <code data-inline="true" spellcheck="false">keccak256</code> hashes on-chain for privacy; plaintext recoverable only from events</p></li><li><p>Unlimited bank detail updates via <code data-inline="true" spellcheck="false">erase()</code> before <code data-inline="true" spellcheck="false">set()</code> — fixing a Stylus bytes32 overwrite limitation</p></li></ul><h2>Frontend (Next.js + Wagmi + Privy)</h2><p>Three separate dashboards, each fully functional:</p><p><strong>Merchant Dashboard</strong></p><ul><li><p>View all incoming payments with live NGN/USD exchange rate</p></li><li><p>Accept payments (locks rate on-chain) or reject (triggers USDC refund)</p></li><li><p>Register and update bank details (collapsible update form, pre-filled with current values)</p></li><li><p>Real-time event indexing for <code data-inline="true" spellcheck="false">MerchantRegistered</code> + <code data-inline="true" spellcheck="false">MerchantUpdated</code> — always shows latest bank details</p></li><li><p><code data-inline="true" spellcheck="false">rfce</code> (payment reference) recovered from <code data-inline="true" spellcheck="false">PaymentCreated</code> events and displayed as human-readable strings</p></li></ul><p><strong>Payer (Transact) Page</strong></p><ul><li><p>Send USDC to any registered merchant</p></li><li><p>Input payment reference (rfce), amount, and merchant address</p></li><li><p>USDC approval + payment in two steps</p></li></ul><p><strong>Admin Dashboard</strong></p><ul><li><p>View all payments across all merchants</p></li><li><p>"Mark as Paid" per payment with per-button processing state (Set-based locking — multiple buttons can process simultaneously without blocking each other)</p></li><li><p>Merchant bank details recovered from events and displayed alongside each payment</p></li></ul><p><strong>Shared technical details across all dashboards:</strong></p><ul><li><p><code data-inline="true" spellcheck="false">writeContractAsync</code> + <code data-inline="true" spellcheck="false">waitForTransactionReceipt</code> — toasts only fire after on-chain confirmation, not on wallet popup</p></li><li><p>No hardcoded gas parameters — MetaMask estimates dynamically (fixes "max fee less than base fee" errors)</p></li><li><p>Rich toast notifications for every action with context-aware loading → waiting → success/error states</p></li><li><p>All components that need inputs are inlined in JSX (not defined as sub-components) to prevent focus loss on keystrokes</p></li></ul><h2>Key Technical Decisions</h2><table style="min-width: 50px"><colgroup><col style="min-width: 25px"><col style="min-width: 25px"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Decision</p></th><th colspan="1" rowspan="1"><p>Why</p></th></tr><tr><td colspan="1" rowspan="1"><p>Stylus (Rust) over Solidity</p></td><td colspan="1" rowspan="1"><p>Lower gas costs, Rust's type safety, Arbitrum-native</p></td></tr><tr><td colspan="1" rowspan="1"><p>Rate locked at acceptance</p></td><td colspan="1" rowspan="1"><p>Protects merchant from slippage between acceptance and NGN transfer</p></td></tr><tr><td colspan="1" rowspan="1"><p>Bank details as hashes</p></td><td colspan="1" rowspan="1"><p>Privacy — account numbers not publicly readable on-chain</p></td></tr><tr><td colspan="1" rowspan="1"><p>Event sourcing for plaintext</p></td><td colspan="1" rowspan="1"><p>Hash stored on-chain; original string lives only in the event log</p></td></tr><tr><td colspan="1" rowspan="1"><p><code data-inline="true" spellcheck="false">erase()</code> before <code data-inline="true" spellcheck="false">set()</code> for updates</p></td><td colspan="1" rowspan="1"><p>Stylus bytes32 slots corrupt on 3rd+ overwrite without zeroing first</p></td></tr><tr><td colspan="1" rowspan="1"><p>Chainlink price feed (USDC/USD)</p></td><td colspan="1" rowspan="1"><p>Off-chain NGN/USD rate combined with on-chain USDC price for accurate NGN amounts</p></td></tr></tbody></table><h2>What's Working End-to-End</h2><ul><li><p> Contract deployed and verified on Arbitrum Sepolia</p></li><li><p> Merchant can register and update bank details</p></li><li><p> Payer can send USDC and create a payment</p></li><li><p> Merchant can accept (rate locks) or reject (refund fires)</p></li><li><p> Admin can mark payments as paid</p></li><li><p> All three dashboards reflect live on-chain state via event indexing</p></li><li><p> Exchange rate pulled live from Chainlink + external FX API</p></li></ul><h2>Stack</h2><ul><li><p><strong>Smart Contract</strong>: Rust + Arbitrum Stylus SDK</p></li><li><p><strong>Chain</strong>: Arbitrum Sepolia (testnet)</p></li><li><p><strong>Frontend</strong>: Next.js, TypeScript, TailwindCSS</p></li><li><p><strong>Wallet</strong>: Privy (embedded wallet + external wallet support)</p></li><li><p><strong>Chain interaction</strong>: Wagmi + Viem</p></li><li><p><strong>Price data</strong>: Chainlink USDC/USD feed + <a href="http://exchangerate.host">exchangerate.host</a> for NGN</p></li><li><p><strong>Token</strong>: USDC (6 decimals)</p></li></ul>
<p>None</p>