diff --git a/README.md b/README.md
index e7902cb..969644d 100644
--- a/README.md
+++ b/README.md
@@ -37,9 +37,9 @@ SO4 is a unified-liquidity perpetuals DEX built on Stellar/Soroban. Deep order b
## Overview
-SO4 Market is the front-end interface for the SO4 perpetuals protocol. It connects to Stellar Soroban smart contracts (ExchangeRouter, DataStore, SyntheticsReader, OrderVault) and streams live prices from Binance (primary) with GMX oracle as automatic fallback.
+SO4 Market is the front-end interface for the SO4 perpetuals protocol. It connects to Stellar Soroban smart contracts (ExchangeRouter, DataStore, SyntheticsReader) and streams live prices from the SO4 oracle (primary) with Pyth, Binance, and GMX as fallbacks.
-> **Status:** Active development. On-chain contract integration is in progress — the current build uses mock transactions with real UI and live price feeds.
+> **Status:** Active development on Stellar testnet. The interface builds, signs, and submits real Soroban transactions to the deployed testnet contracts (ExchangeRouter, DataStore, SyntheticsReader) and streams live prices through the SO4 oracle with Pyth, Binance, and GMX fallbacks. The OrderVault client is stubbed pending integration; staking, GLV, and vesting contracts are not deployed on testnet.
---
@@ -70,7 +70,7 @@ SO4 Market is the front-end interface for the SO4 perpetuals protocol. It connec
| Charts | [lightweight-charts v5](https://tradingview.github.io/lightweight-charts/) |
| Notifications | [Sonner](https://sonner.emilkowal.ski) |
| Blockchain | [Stellar](https://stellar.org) / [Soroban](https://soroban.stellar.org) |
-| Oracle | Binance REST (primary) · GMX oracle (fallback) |
+| Oracle | SO4 oracle (primary) · Pyth · Binance REST · GMX oracle (fallback) |
| Type safety | TypeScript 5.9 |
---
@@ -246,18 +246,13 @@ handlers in `apps/web/test/msw/handlers.ts` or test-specific handlers with
### Oracle / Price feeds
-Live candle data and token prices are fetched from the Binance public REST API. If Binance is unavailable or rate-limited, the oracle module automatically retries against the GMX oracle endpoint. Both sources are normalised into a shared `OhlcBar` type (oldest-first, prices as numbers, time in Unix seconds).
+The SO4 oracle is the primary price source (`apps/web/src/features/trade/lib/oracle.ts`). If it is unavailable, prices fall back to Pyth Hermes, then Binance REST, then the GMX oracle. Live candle data for the chart is fetched from the Binance public REST API with a Pyth Benchmarks fallback. Sources are normalised into a shared `OhlcBar` type (oldest-first, prices as numbers, time in Unix seconds).
### Contract integration
-The `lib/stellar.ts`, `lib/earn.ts`, and `lib/referrals.ts` files define the full contract call surface. Each function is currently a **stub** that simulates latency and shows a toast — the real Stellar SDK + Soroban RPC calls are documented inline with `TODO` comments. Contracts to integrate:
+The real Soroban transaction paths live in `apps/web/src/features/trade/lib/stellar.ts`, `apps/web/src/features/earn/lib/earn.ts`, and `apps/web/src/features/pools/lib/pool-transactions.ts`. Each builds a transaction through `apps/web/src/lib/contracts.ts` (for example `buildCreateOrderTransaction`, `buildCreateDepositTransaction`), prepares and signs it with the connected wallet via `apps/web/src/lib/soroban/tx-builder.ts`, then submits and polls it through `apps/web/src/lib/tx-builder.ts`. Account and market reads go through `SyntheticsReader` over Soroban RPC, falling back to contract-only data when the indexer is disabled (`apps/web/src/app/config/indexer.ts`).
-- `ExchangeRouter` — `createOrder` (increase / decrease / swap)
-- `DataStore` — on-chain key-value protocol config
-- `SyntheticsReader` — `getMarketInfo`, `getPositionInfo`, `getOrderInfo` (batched)
-- `OrderVault` — holds collateral between order creation and execution
-- `StakingRouter` — `stakeSO4`, `unstakeSO4`
-- `ReferralsRouter` — `setTraderReferralCodeByUser`, `registerCode`
+The OrderVault client (`packages/contracts/src/clients/order-vault.ts`) is a stub awaiting integration. `StakingRouter`, the GLV router, and `VestingRouter` are not deployed on testnet and their features are disabled in `apps/web/.env.testnet`. Referral codes are served by `ReferralStorage` (`apps/web/src/features/referrals/`).
### Theme system
diff --git a/apps/docs/content/get-started/introduction.mdx b/apps/docs/content/get-started/introduction.mdx
new file mode 100644
index 0000000..4f61e17
--- /dev/null
+++ b/apps/docs/content/get-started/introduction.mdx
@@ -0,0 +1,24 @@
+---
+title: Introduction
+description: What SO4 is: a unified-liquidity perpetuals DEX on Stellar/Soroban, settling on the public ledger while the oracle and indexer serve prices and history.
+updated: 2026-08-30
+status: beta
+---
+
+SO4 is a perpetuals exchange built on Stellar's Soroban smart-contract platform. A perpetual (perp) is a derivative that tracks an asset's market price without requiring you to own the asset. SO4 runs one shared liquidity pool per market and uses self-custodied collateral: your wallet signs every transaction, and the protocol contracts hold only what you post as margin.
+
+If you know perps from a centralised venue, the difference is where your money sits and what settling means. On a centralised venue your balance is a row in the venue's database, and a trade settles when the venue updates its own ledger. On SO4 your wallet holds your keys, and settling a trade means the Soroban contracts on the Stellar network executing it — visible to anyone on the public ledger. The web interface builds transactions and submits the signed ones; it never holds your assets or your keys.
+
+If you know Stellar but have not traded a perp: you deposit collateral, choose a market and a direction (long or short), and your position gains or loses value as the market price moves. Leverage lets you control a larger position than your collateral alone would allow, and it multiplies losses as much as gains. Funding periodically transfers value between longs and shorts to keep a perp price near the underlying asset's price. The mechanics live on the concept pages: [funding and fees](/concepts/funding-and-fees), [liquidation](/concepts/liquidation), and the [risk overview](/concepts/risk) cover them, and the `/concepts/perpetuals` concept page explains the instrument itself.
+
+What is on-chain versus what is served off-chain decides what you have to trust.
+
+**On-chain.** Your orders, positions, and token balances are ledger state managed by the Soroban contracts deployed on Stellar testnet: ExchangeRouter routes orders, DataStore holds market configuration, and SyntheticsReader reads positions, orders, and prices back. The interface builds transactions against these contracts and submits them through the public Stellar RPC once your wallet signs. Anyone can read the ledger and verify a trade.
+
+**Served off-chain.** The prices you see come from the SO4 oracle (primary) with Pyth, Binance, and GMX as fallbacks. The indexer is a separate service that watches the ledger and stores historical data in a database for fast queries. Neither the oracle nor the indexer holds funds or settles a trade. You trust them for accurate price data; you trust the contracts to settle correctly.
+
+## Current status
+
+Active development on Stellar testnet. The interface builds, signs, and submits real Soroban transactions to the deployed testnet contracts — ExchangeRouter, DataStore, and SyntheticsReader — and streams live prices through the SO4 oracle with Pyth, Binance, and GMX fallbacks. Three markets are live: TETH/TUSDC, TWBTC/TUSDC, and TXLM/TUSDC, funded by the testnet faucet. The OrderVault client is a placeholder awaiting integration, and the staking, GLV, and vesting contracts are not yet deployed on testnet. Testnet tokens have no real value.
+
+To place a first trade, follow the `/get-started/quickstart` page. To understand perps from first principles, start at the `/concepts/perpetuals` concept page.
\ No newline at end of file
diff --git a/apps/docs/content/get-started/quickstart.mdx b/apps/docs/content/get-started/quickstart.mdx
new file mode 100644
index 0000000..16c50f6
--- /dev/null
+++ b/apps/docs/content/get-started/quickstart.mdx
@@ -0,0 +1,79 @@
+---
+title: Quickstart
+description: Place your first SO4 testnet trade in about ten minutes — install Freighter, connect it, switch to Testnet, fund the account, and submit an order.
+updated: 2026-08-30
+status: beta
+---
+
+This page walks you through your first SO4 trade on the Stellar testnet. You install the Freighter wallet, connect it, switch it to Testnet, fund the account, and submit a small order. Every button name and message quoted below is what the interface showed during a clean-profile run on 2026-08-30, with Freighter 5.46 in a fresh Chrome profile. Dollar amounts and fees move with the market; treat the figures as current as of that run.
+
+## Before you start
+
+You need the Freighter wallet extension in your browser and a Freighter account with a recovery phrase you saved. Everything below runs on the Stellar testnet, where tokens have no real value. If you have not read [what SO4 is](/get-started/introduction), that page gives the two-paragraph version first.
+
+## 1. Install the Freighter wallet
+
+Install Freighter from its official website and follow the account creation flow. Freighter walks you through setting a password and saving a recovery phrase. The recovery phrase is the only way to restore the account, so keep it somewhere safe.
+
+A dedicated wallet guide that covers Freighter, xBull, and Hana in more depth is planned. This page uses Freighter because it is the wallet the interface is tested with.
+
+## 2. Connect the wallet to SO4
+
+Open the SO4 app. In the top-right of the page, click **Connect Wallet**.
+
+The wallet modal opens with the heading **Connect Wallet** and the description **Choose a supported Stellar wallet to continue.** The Freighter row shows **Freighter** with **Detected** on the right. Click it.
+
+Freighter opens a **Connection Request** page for the site. It says **Allow [site] to view your wallet address, balance, activity and request approval for transactions**, and it lists your **Wallet** address and **Network**. Click **Connect**.
+
+The navbar now shows your shortened address in the format of the first six characters, three dots, and the last four, like **GDXO4N...D4HH**. A prompt appears that reads **I have a referral code**, with **Apply** and **Skip for now** buttons. Click **Skip for now**.
+
+## 3. Switch Freighter to Testnet
+
+A new Freighter wallet starts on **Mainnet**. The SO4 app runs on **Testnet**, so you must switch before any transaction can be approved. Open the Freighter extension, and in the wallet header choose **Testnet** next to **Mainnet**.
+
+> **Wrong network.** If you approve a transaction while Freighter is still on Mainnet, Freighter rejects it. The approval screen reads: "Freighter is set to Main Net". Then: "The transaction you're trying to sign is on Test Net." Then: "Signing this transaction is not possible at the moment."
+> A **Got it** button dismisses the message, and the transaction is never submitted. You can also catch the mismatch earlier: the connection request in step 2 lists **Network Main Net** when Freighter is on the wrong network. Switch Freighter to **Testnet** and try the transaction again.
+
+## 4. Fund the account and claim test tokens
+
+A brand-new account holds zero XLM, and every transaction needs a little XLM to pay the Stellar network fee. Fund the account before you claim or trade.
+
+> **No XLM for fees.** If you try to claim or trade before funding, the app shows a toast that reads exactly: "Transaction failed. Please try again." The transaction never reaches the network. Get XLM first, then claim.
+
+
+1. Get testnet XLM. Click your address in the navbar to open the account menu, then click **Testnet Faucet**. That opens the Stellar testnet friendbot, which credits the account with 10,000 testnet XLM in a few seconds. Freighter's own **Add XLM** button on Testnet does the same.
+2. Open the **Faucet** page from the app's top navigation. The page heading reads **Testnet Faucet** with the description **Claim test tokens to try trading on SO4. Tokens have no real value.**
+3. Each token card (TUSDC, TWBTC, TETH, TXLM) shows **Your balance**, **Claim amount**, and a **Claim** button. The example trade in step 5 needs TWBTC, so click **Claim** on the TWBTC card.
+4. Freighter opens **Confirm Transaction** and lists **Network Test Net**, the **Fee** in XLM (about 0.007 XLM for one token on the 2026-08-30 run), and **Memo None**. Click **Confirm**.
+5. The card shows **Claiming** while the transaction confirms, then a toast appears that reads **Test token claimed** with a **View transaction** link. One token took about 8 to 12 seconds from clicking **Claim** to the success toast.
+
+
+The **Claim Test Tokens** button below the cards claims all four tokens in one transaction. On the current testnet build that transaction fails on-chain and the app shows the toast "Transaction failed. Please try again." Claim one token at a time with the per-token **Claim** buttons.
+
+## 5. Open a small position
+
+Open the **Trade** page. The market defaults to **TWBTC/USD**, and the three trade-type tabs read **Long**, **Short**, and **Swap**. Leave **Long** selected. On this market the collateral for a long is TWBTC, so the collateral you enter is priced in test Bitcoin.
+
+
+1. Enter **0.0005** in the collateral field. At the price on the 2026-08-30 run, that is about $39 of collateral. The **Leverage** slider defaults to **10x**, and the **Position size** row reads about $393.
+2. Review the rows above the button: **Entry price**, **Liq. price**, **Position fee**, **Price impact**, **Execution fee**, and **Total fees**. On the example runs these sat around $78,700 entry, $71,200 liquidation, $0.39 position fee, -0.05% price impact, ~0.30 XLM execution fee, and $0.64 total fees.
+3. Click **Long TWBTC**. A confirmation dialog opens titled **Confirm Long CCFTOP…XDUO**, showing **Size**, **Leverage 10x**, **Entry price**, **Liq. price**, **Network fee ~0.0175 XLM**, **Execution fee ~0.01 XLM**, **Collateral 0.0005 CCFTOP…XDUO**, and **Total fees**. Click **Confirm Long**.
+4. Freighter opens **Confirm Transaction** with **Network Test Net** and a **Fee** around 0.0175 XLM. Click **Confirm**.
+5. The toast reads **Batch order submitted**, with **1 operations**, the transaction hash, and a **View transaction** link. Signing and confirmation took about 6 to 12 seconds on the example runs.
+
+
+## 6. Check the order
+
+The panel under the chart has **Positions**, **Orders**, **Trades**, and **Claims** tabs. The order you submitted appears under **Orders**. The tab label shows the count, **Orders (1)**, and the row lists **Market**, **Type**, **Size**, **Trigger**, and **Created** columns with a **Cancel** button. On the example run the row read **BTC/USD**, **Long**, **MarketIncrease**, **$393.51**, **$0.00**, and a created time.
+
+The **Positions** tab currently shows **No open positions**. On the current testnet build, the order is recorded on-chain but not executed into a position, so no position row appears. The order-execution pipeline is still being wired up on testnet; the project status note in the README describes the OrderVault client as pending integration. Once execution is live, an executed position appears here with its market, size, entry, mark, liquidation, and PnL columns.
+
+## 7. Cancel the order
+
+To undo the order, click **Cancel** on the order row in the **Orders** tab. The button shows an ellipsis while the request is pending, and a toast reads **Cancelling order...**. Freighter opens **Confirm Transaction** with a fee around 0.0046 XLM. Click **Confirm**, and the toast reads **Order cancelled**, with the transaction hash and a **View transaction** link. The cancel took about 9 seconds on the example run.
+
+## What happened
+
+Your 0.0005 TWBTC went to the protocol as collateral for a long position about ten times its value. The order was a market increase of about $393. The position fee (about $0.39, roughly 0.1% of size) is taken from your collateral. The network and execution fees are paid in XLM, the asset Stellar charges transaction fees in. The market's funding rate reads **+0.000%/h**, which means neither side currently pays the other.
+
+If the price moves against a leveraged position, your collateral can be consumed before the position is closed. [Liquidation](/concepts/liquidation) explains that mechanic with a worked example. For positions held across funding intervals, funding transfers value between longs and shorts — see [funding and fees](/concepts/funding-and-fees). The maintenance margin concept is defined in the [glossary](/reference/glossary), and the [risk overview](/concepts/risk) lists the full set of ways a trade can lose money.
diff --git a/apps/docs/content/meta.json b/apps/docs/content/meta.json
index dc71e61..3b1ad61 100644
--- a/apps/docs/content/meta.json
+++ b/apps/docs/content/meta.json
@@ -1,5 +1,9 @@
{
"sections": [
+ {
+ "label": "Get started",
+ "pages": ["get-started/introduction", "get-started/quickstart"]
+ },
{
"label": "Developers",
"pages": [
diff --git a/apps/docs/scripts/check-content.ts b/apps/docs/scripts/check-content.ts
index 4dd6f37..ad43bd6 100644
--- a/apps/docs/scripts/check-content.ts
+++ b/apps/docs/scripts/check-content.ts
@@ -43,9 +43,8 @@ const navRoutes = meta.sections.flatMap((section) =>
for (const route of navRoutes)
if (!routes.has(route)) errors.push(`sidebar references missing ${route}`)
for (const route of routes)
- // The home page is the site root — reachable by definition, not a sidebar
- // entry — so it is exempt from the orphan check.
- if (!navRoutes.includes(route) && route !== "/index")
+ // `/index` is the docs home page and is intentionally not in the sidebar.
+ if (route !== "/index" && !navRoutes.includes(route))
errors.push(`orphan page ${route}`)
const glossary = pages.find((page) => page.route === "/reference/glossary")
diff --git a/apps/docs/scripts/content.ts b/apps/docs/scripts/content.ts
index cb4f0e5..bb925b5 100644
--- a/apps/docs/scripts/content.ts
+++ b/apps/docs/scripts/content.ts
@@ -78,14 +78,26 @@ export async function loadPages(): Promise> {
}
export function headingEntries(body: string) {
- return [
- ...body.matchAll(
- /^(#{2,6}) (.+?)(?: \{#([a-z0-9-]+)\})?$/gm,
- ),
- ].map(([, , title, explicitId]) => ({
- title,
- id: explicitId ?? slugifyHeading(title),
- }))
+ const lines = body.split("\n")
+ const entries: Array<{
+ title: string
+ id: string
+ answer?: string
+ }> = []
+ for (let i = 0; i < lines.length; i++) {
+ const match = lines[i].match(/^(#{2,6}) (.+?)(?: \{#([a-z0-9-]+)\})?$/)
+ if (!match) continue
+ const title = match[2]
+ const id = match[3] ?? slugifyHeading(title)
+ let answer = ""
+ for (let j = i + 1; j < lines.length; j++) {
+ const next = lines[j]
+ if (/^(#{2,6}) /.test(next)) break
+ if (next.trim()) answer = (answer ? `${answer} ` : "") + next.trim()
+ }
+ entries.push({ title, id, answer: answer || undefined })
+ }
+ return entries
}
export function slugifyHeading(title: string): string {
diff --git a/apps/docs/src/lib/content.ts b/apps/docs/src/lib/content.ts
index ec30f3d..95da796 100644
--- a/apps/docs/src/lib/content.ts
+++ b/apps/docs/src/lib/content.ts
@@ -125,7 +125,7 @@ export async function loadContentIndex(
}
}
for (const route of byRoute.keys()) {
- if (!sidebarRoutes.has(route)) {
+ if (route !== "/index" && !sidebarRoutes.has(route)) {
errors.push(`orphan page ${route}`)
}
}