Become a merchant
Merchants are the supply side of the marketplace: you park stablecoins in a vault the escrow program controls, advertise a price and the payment methods you accept, and release funds once the fiat lands. Your coins move only through the program's own instructions — no counterparty, and no node, can take them.
Tonggak 2 dari 2 · Make and take offers
Sebelum memulai
- A Solana wallet, plus enough SOL on devnet to pay transaction fees and account rent
- OPEN to bond. The devnet OPEN mint's authority is permanently unset, so nobody can mint more — on devnet it has to come from the presale allocation or an existing holder
- The stablecoin you intend to sell, held in a Token-2022 account owned by that same wallet
- Reachable access to an OpenFiat node's JSON-RPC endpoint, to publish the advertisement itself
- For publishing and managing ads: a script using the OpenFiat SDK. Only the bond has a browser flow today
Langkah
- 01
Bond OPEN as a merchant
The bond is what makes spamming the order book expensive and gives the protocol something to slash for misconduct. On chain the staking program keeps a single flat minimum for every role except arbitrator, deployed with OFS-4100 §4's proposed 1,000 OPEN — the 5,000 figure in the reference app's staking page is simulated display data, not the deployed config. Read the StakingConfig account if you need the number that is actually enforced. Bonding is the one step with a real browser flow: the app's staking page builds and submits exactly the two instructions below.
TypeScriptimport { PublicKey, Transaction } from "@solana/web3.js"; import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; import { onchain } from "@openfiat/sdk"; // OPEN has 9 decimals (OFS-4100 §1). const amount = 1_000n * 1_000_000_000n; const from = getAssociatedTokenAddressSync(openMint, owner, false, TOKEN_2022_PROGRAM_ID); // The stake account is per (wallet, role): one wallet can hold separate // merchant and node-operator stakes, each its own PDA. const instructions = [ onchain.staking.initializeStakeAccountIx(owner, onchain.Role.Merchant), onchain.staking.stakeIx(owner, openMint, onchain.Role.Merchant, from, amount), ]; const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); const tx = new Transaction({ feePayer: owner, blockhash, lastValidBlockHeight }).add(...instructions); await wallet.signAndSendTransaction(tx); - 02
Open a liquidity vault and fund it
The vault is one account per (merchant, stablecoin) whose token balance only the escrow program's own instructions can move. It tracks total, reserved, available, pending_settlement and settled separately, so a reservation can hold inventory against your advertisement without any tokens leaving yet — they move only when a trade escrow is actually funded. Note the vault and the bond are independent on chain: depositing does not check that you have staked, so bond first if you intend to advertise.
TypeScriptimport { onchain } from "@openfiat/sdk"; // One vault per stablecoin you sell. Creating it also creates the // program-owned token account that actually holds the balance. const create = onchain.escrow.createLiquidityVaultIx(merchant, usdcMint); // `from` is your own Token-2022 account for that mint. const deposit = onchain.escrow.depositLiquidityIx( merchant, usdcMint, from, 200_000n * 1_000_000n, // 200,000 USDC, 6 decimals ); - 03
Publish the advertisement
The advertisement is an off-chain, gossiped record: you sign it and submit it to any node, and the network propagates it. Fixed pricing works today. Floating pricing accepts an oracle provider and a premium in basis points, but the record only carries that configuration — resolving it against a live oracle mid-price is not implemented yet, so quote Fixed unless you are deliberately testing the unfinished path.
TypeScriptimport { Client, advertisements, peerIdFromPublicKey, toBytes, type AdvertisementCreate } from "@openfiat/sdk"; const client = new Client({ endpoint: "http://localhost:7080", timeoutMs: 30_000 }); const create: AdvertisementCreate = { id: "my-usdt-kes-sell-1", merchant: toBytes(peerIdFromPublicKey(keypair.publicKey)), merchant_public_key: toBytes(keypair.publicKey), asset: "USDT", direction: "Sell", fiat_currency: "KES", // Amounts are { base_units, decimals } — 10.00 to 500.00 KES-priced trades. min_trade: { base_units: 1_000, decimals: 2 }, max_trade: { base_units: 50_000, decimals: 2 }, initial_liquidity: { base_units: 200_000, decimals: 2 }, pricing: { Fixed: { price: { base_units: 12_950, decimals: 2 } } }, payment_methods: ["M-Pesa"], timestamp: Date.now(), }; const adId = await advertisements.sendAdvertisementCreate(client, create, keypair);Verify it propagatedcurl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"getAdvertisements","params":{}}' - 04
Changing price or pausing — not wired up yet
Be aware of this before you build against it. The advertisements crate defines signed AdvertisementDisable and AdvertisementPriceUpdate events, and the record has Active, Disabled, Vacation and Deleted states — but no JSON-RPC method submits either event. sendAdvertisementCreate is the only advertisement mutation a node accepts today, so in practice an ad is publish-only. Available liquidity is the exception: it is adjusted automatically by reservation and settlement activity, never by a fresh signature from you per trade.
- 05
Confirm the fiat, then approve
When a buyer marks payment sent, check your own bank or mobile-money account — never the counterparty's word, and never a screenshot. Approval happens in two places: the signed off-chain event that moves the settlement's own state machine, and the on-chain approve_settlement that flips the escrow's approved flag. Approving does not move money; release_escrow is the only instruction that does, and it computes and routes the fee split at the same time.
Off-chain — the settlement state machinecurl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"getSettlements","params":{}}' # Then submit a signed SettlementApproved for the one you have verified — # see the SDK's send_settlement_approved / sendSettlementApproved.On-chain — approve, then releaseimport { onchain } from "@openfiat/sdk"; // Only valid while the escrow is AwaitingFiatSettlement, and only from // the seller's own wallet — both are enforced by the program. const approve = onchain.escrow.approveSettlementIx(merchant, reservationId); // Permissionless once approved: it pays the buyer and splits the // settlement fee across the four treasuries in FeeConfig. const release = onchain.escrow.releaseEscrowIx(usdcMint, merchant, reservationId, { buyerTokenAccount, devTreasury, ecosystemTreasury, infraTreasury, emergencyReserve, }); - 06
Taking liquidity back out
Withdrawing only draws against the vault's available balance, so anything reserved against an open trade stays put until that trade settles, cancels or expires. Unbonding the stake is separate and deliberately slower: you request it, wait out the unbonding period, then withdraw. Effective stake drops the moment you request, not when the timer ends — so unbonding costs you eligibility immediately.
TypeScriptimport { onchain } from "@openfiat/sdk"; const pullOut = onchain.escrow.withdrawLiquidityIx(merchant, usdcMint, to, 50_000n * 1_000_000n); // Unbonding: request, wait out StakingConfig.unbonding_period_secs, withdraw. const request = onchain.staking.requestUnstakeIx(owner, onchain.Role.Merchant, 1_000n * 1_000_000_000n); const collect = onchain.staking.withdrawUnstakedIx(owner, openMint, onchain.Role.Merchant, to);