diff --git a/.changelog/unreleased/640-dx1-content-batch.md b/.changelog/unreleased/640-dx1-content-batch.md new file mode 100644 index 00000000..d9a02a56 --- /dev/null +++ b/.changelog/unreleased/640-dx1-content-batch.md @@ -0,0 +1,8 @@ +--- +type: added +area: docs +pr: 640 +breaking: false +--- + +Add the documentation contribution guide, page templates, and PR template; the docs home page; and pages for oracles, pools, earn, referrals, and the faucet. diff --git a/.github/PULL_REQUEST_TEMPLATE/documentation.md b/.github/PULL_REQUEST_TEMPLATE/documentation.md new file mode 100644 index 00000000..7f1a1b88 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/documentation.md @@ -0,0 +1,56 @@ + + +## Page(s) + + + +- Route: `/` + File: `apps/docs/content/` + Issue: closes # + +## Reader + + + +Primary reader: + +## Sources + + + +| Claim in the page | Source file | +| ----------------- | ----------- | +| | | + +## Worked example + + + +## Definition of done (content map §4) + +- [ ] The page exists at its mapped path with valid frontmatter (`title` ≤ 60, `description` 50–160, real `updated:` date, valid `status`). +- [ ] `bun run --cwd apps/docs check:content` passes. +- [ ] `bun run --cwd apps/docs check:links` passes. +- [ ] `bun run --cwd apps/docs lint:prose` passes with no new warnings. +- [ ] Every mechanical claim above names its source file. +- [ ] The worked example's numbers were computed, not estimated. +- [ ] The page is listed in `apps/docs/content/meta.json` in content-map order (or, for `/index`, is intentionally excluded). +- [ ] Reading level fits the target reader. +- [ ] Renders correctly in light and dark themes at a mobile and a desktop width. +- [ ] For guides: every documented action was performed by the author on testnet — confirmed here: + +## Anything unverified + + + +## Changelog + +- [ ] Added `.changelog/unreleased/-.md` with `area: docs` (see `.changelog/unreleased/README.md`). diff --git a/AGENTS.md b/AGENTS.md index 3d03f655..3bfc6cb3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -157,6 +157,9 @@ Violating these silently breaks the build for everyone. - **Docs content is validated centrally.** Run `bun run --cwd apps/docs check:content` or the root `bun run check:content` before changing docs content, manifests, or docs assets. + [`apps/docs/CONTRIBUTING.md`](./apps/docs/CONTRIBUTING.md) is the contract for + documentation pull requests: the frontmatter rules, the manifest, the concept + / guide / reference page templates, and the checks that gate them. --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f95d7ea..b9c92e94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,9 +159,14 @@ even when your fork has drifted behind. - Prefer workspace imports (`@workspace/ui/...`) over deep relative paths. - Use design tokens, not raw values — `check:tokens` enforces this. Text sizes and radii have named tokens in `packages/ui/src/styles/globals.css`. -- For docs content, run `bun run --cwd apps/docs check:content`. Use - `bun run --cwd apps/docs check:content -- --fix` only for the mechanically - fixable subset: `updated:` dates and manifest ordering. +- For docs content, follow + [`apps/docs/CONTRIBUTING.md`](./apps/docs/CONTRIBUTING.md) — it covers the + frontmatter contract, the `content/meta.json` manifest, the concept / guide / + reference page templates, and the + [documentation PR template](./.github/PULL_REQUEST_TEMPLATE/documentation.md). + Run `bun run --cwd apps/docs check:content`, `check:links`, and `lint:prose`; + `check:content -- --fix` only mechanically fixes `updated:` dates and manifest + ordering. - Comment non-obvious intent only. Don't restate the code. ### 3. Format diff --git a/apps/docs/CONTRIBUTING.md b/apps/docs/CONTRIBUTING.md new file mode 100644 index 00000000..8810ddeb --- /dev/null +++ b/apps/docs/CONTRIBUTING.md @@ -0,0 +1,162 @@ +# Contributing to the SO4 documentation + +This guide covers documentation content only — MDX pages under +`apps/docs/content/`. For code, the toolchain, and the commit gate, read the +root [`CONTRIBUTING.md`](../../CONTRIBUTING.md) and [`AGENTS.md`](../../AGENTS.md). + +The information architecture, reader personas, and page contract this guide +enforces are defined in +[`docs/dx_1/003_content_map.md`](../../docs/dx_1/003_content_map.md). This file +makes those rules runnable; the content map is the source of truth if the two +ever disagree. + +--- + +## Where content lives + +``` +apps/docs/ + content/ + index.mdx docs home (exempt from the sidebar; see meta.json note) + get-started/*.mdx + concepts/*.mdx + guides/*.mdx + developers/*.mdx + reference/*.mdx includes *.generated.mdx — do not hand-edit those + resources/*.mdx + meta.json the sidebar manifest — every page except /index is listed here + templates/ copy-me starting points: concept.mdx, guide.mdx, reference.mdx + PROSE_STYLE.md the full voice guide; prose lint enforces a subset + REVIEWER_CHECKLIST.md what a reviewer checks before approving + scripts/ check-content.ts, check-links.ts, lint-prose.ts +``` + +A page's route is its path under `content/` with `.mdx` removed: +`content/guides/pools.mdx` serves at `/guides/pools`. + +## The frontmatter contract + +Every page starts with a fenced frontmatter block. The parser +(`scripts/content.ts`) is line-based `key: value` — no nested keys, no +multi-line values, no comments. + +```mdx +--- +title: Pools +description: Adding and removing liquidity in an SO4 GM pool, what a deposit is exposed to, and how to exit a position. +updated: 2026-08-31 +status: stable +--- +``` + +| Key | Rule (`scripts/check-content.ts`, `src/lib/frontmatter.ts`) | +| ------------- | ---------------------------------------------------------------------- | +| `title` | 1–60 characters. Appears in the sidebar and the browser tab. | +| `description` | 50–160 characters. Used for search and social cards. | +| `updated` | `YYYY-MM-DD`. Set it to the date you finished the page. | +| `status` | `stable`, `beta`, or `draft`. Use `draft` if a maintainer still needs to verify claims. | + +Optional keys the schema allows: `sidebarLabel`, `order`, `tags`. + +## Adding a page to the manifest + +`content/meta.json` is the sidebar. A page that is not listed there fails +`check:content` with `orphan page /your/route`, and a listed page that does not +exist fails with `sidebar references missing /your/route`. + +Add your route (path without the leading slash) to the right section's `pages` +array, in the order it appears in the content map §2: + +```json +{ + "label": "Guides", + "pages": ["guides/pools", "guides/earn", "guides/referrals", "guides/faucet", "guides/troubleshooting"] +} +``` + +`/index` is the one deliberate exception — it is the home page and is not in +the sidebar. + +## The page contract (content map §3) + +Every page: + +1. **Answers its title question in the first paragraph**, before any scrolling. +2. **Carries one worked example with real numbers you computed**, not estimated. + "A 10x long on 100 USDC liquidates at roughly a 9.1% adverse move" is + checkable; "leverage is risky" is not. +3. **Links to adjacent concepts inline**, where the reader needs them — not in a + trailing "see also" list. +4. **States a true `updated:` date.** + +And avoids screenshots of weekly-changing UI, numbers that will silently rot +(fees, addresses, schema — those come from the `*.generated.mdx` reference +pages), and second-person imperative stacked ten deep. + +## Which checks to run + +From the repository root, or with `--cwd apps/docs`: + +```bash +bun run --cwd apps/docs check:content # frontmatter, manifest, orphans, alt text, glossary +bun run --cwd apps/docs check:links # internal links and heading anchors resolve +bun run --cwd apps/docs lint:prose # banned words, capitalisation, exclamation marks +bun run --cwd apps/docs check:faq # only if you touched resources/faq.mdx +``` + +`bun run --cwd apps/docs check:content -- --fix` mechanically fixes the +`updated:` date and manifest ordering — nothing else. + +These three checks run in CI (`.github/workflows/ci.yml`, "Documentation +content and prose checks" and "Documentation link and FAQ drift checks"). Run +them locally before you commit; a gate you did not execute did not pass. + +## Voice rules + +Prose lint (`scripts/lint-prose.ts`) fails the build on: + +- **Exclamation marks** in prose. +- **The words `simply`, `just`, `obviously`, `easy`, `easily`.** If it were + simple the page would not exist. +- **Lower-cased `soroban`, `stellar`, `freighter`, `turborepo`** — capitalise + them. Contract names are `OrderVault`, `ExchangeRouter`, `SyntheticsReader`, + `DataStore`; never write them hyphenated in prose. + +It warns (does not fail) on passive voice and sentences over 30 words. Treat the +warnings as review comments from the linter. + +The rest of the voice guide — plain, specific, unhurried; concrete nouns over +abstract ones; hedge only when the uncertainty is real and then name it — is in +[`PROSE_STYLE.md`](./PROSE_STYLE.md). + +## Previewing locally + +```bash +bun install +bun run --cwd apps/docs dev +``` + +This builds the content and starts the Nitro dev server. Open the printed URL, +find your page in the sidebar, and check it in both light and dark themes at a +mobile and a desktop width. + +## Sourcing claims + +Every statement about protocol mechanics must be traceable to contract code, +the indexer, or `apps/web` behaviour — and the PR description must name the +file. "The faucet rejects a repeat claim with contract error code 6 +(`ClaimTooSoon`), surfaced as a cooldown message" +(`apps/web/src/features/faucet/hooks/useClaim.tsx`) is sourced. "The faucet has +a cooldown" is not. + +If the code does not handle a case, the page says so. A page that describes +intended behaviour as if it were current behaviour is worse than no page. + +## Opening the PR + +Use [`.github/PULL_REQUEST_TEMPLATE/documentation.md`](../../.github/PULL_REQUEST_TEMPLATE/documentation.md). +Append `?template=documentation.md` to the compare URL, or pick it from the +template dropdown. It mirrors the content map's definition of done (§4): the +page exists at its mapped path, the three checks pass, every mechanical claim +names its source file, the worked example's numbers were computed, and the page +is listed in `meta.json`. diff --git a/apps/docs/REVIEWER_CHECKLIST.md b/apps/docs/REVIEWER_CHECKLIST.md new file mode 100644 index 00000000..e80347f4 --- /dev/null +++ b/apps/docs/REVIEWER_CHECKLIST.md @@ -0,0 +1,60 @@ +# Documentation reviewer checklist + +For reviewing content pull requests under `apps/docs/content/`. This checklist +is about **accuracy and reader fit**, not style — prose style is enforced by +`lint:prose` and refined in DX-053, and you should not spend review time on it. + +The authoring rules are in [`CONTRIBUTING.md`](./CONTRIBUTING.md); the +information architecture and definition of done are in +[`docs/dx_1/003_content_map.md`](../../docs/dx_1/003_content_map.md). + +--- + +## Accuracy + +- [ ] **Every mechanical claim has a source.** The PR's Sources table names a + file for each statement about how the protocol behaves. Open two or three + of them and confirm the page matches the code, not a README summary. +- [ ] **The worked example is arithmetic, not a vibe.** The inputs, the steps, + and the result are all shown, and the result follows from the inputs. +- [ ] **Failure modes describe real behaviour.** Where the code does not handle + a case, the page says so plainly rather than describing the intended + handling as if it shipped. +- [ ] **No rotting numbers.** Fees, contract addresses, and schema are not + pasted inline — they are linked to the generated reference pages, or + stated as "current as of ". +- [ ] **Links point somewhere true.** `check:links` proves the routes resolve; + you confirm the linked page actually answers the question the link + implies. + +## Reader fit + +- [ ] **One reader per page.** It is obvious from the first paragraph whether + this is for a trader, a liquidity provider, or an integrator. A page + serving all three serves none. +- [ ] **The first paragraph answers the title.** A reader who stops after it + has a correct, if shallow, model. +- [ ] **Reading level matches the reader.** A concepts page for traders does + not assume Soroban knowledge; a developers page may. +- [ ] **Adjacent concepts are linked where the reader needs them**, not dumped + in a trailing list. + +## Mechanics + +- [ ] `check:content`, `check:links`, and `lint:prose` are green in CI, with no + new prose-lint warnings. +- [ ] The page is in `content/meta.json` in content-map §2 order (or is + `/index`, which is intentionally excluded). +- [ ] The `updated:` date is the date the work was actually finished. +- [ ] `status:` is `draft` if any claim in the PR's "Anything unverified" + section is still open. +- [ ] For guides: the PR confirms the author performed each documented action + on testnet. +- [ ] A `.changelog/unreleased/` entry with `area: docs` is included. + +## Diagrams + +- [ ] Renders in both themes. +- [ ] Has a caption that states what the diagram shows. +- [ ] Any image has non-empty alt text (`check:content` enforces this, but + confirm the alt text is descriptive, not a filename). diff --git a/apps/docs/content/concepts/oracles.mdx b/apps/docs/content/concepts/oracles.mdx new file mode 100644 index 00000000..15a7fb5b --- /dev/null +++ b/apps/docs/content/concepts/oracles.mdx @@ -0,0 +1,123 @@ +--- +title: Oracles +description: Where SO4 prices come from, the order the app falls back through sources, how staleness is judged, and what happens when a source fails. +updated: 2026-08-31 +status: beta +--- + +SO4 reads a USD price for each market from a chain of sources, tries them in a +fixed order, and uses the first one that answers with data. The price feed is +implemented in `apps/web/src/features/trade/lib/oracle.ts`; this page describes +what that file does today, not what the README summary says. + +> The README and the DX1 content map describe the intended design as "Binance +> primary, GMX fallback". The shipped `fetchTokenPrices` puts the SO4 oracle +> service first and Pyth second, with Binance and GMX below them. This page +> follows the code. + +## Sources, in priority order + +`fetchTokenPrices()` in `oracle.ts` walks these sources top to bottom and +returns the first that produces at least one price: + +| # | Source | Endpoint | Notes | +| - | ------ | -------- | ----- | +| 1 | **SO4 oracle service** | `oracle.biscotti-proxy-worker.workers.dev/prices` | The primary source. A hosted aggregator; its own comments say it combines Binance, Coinbase, and Pyth. Returns a `min` and `max` price per token, scaled by `10^30`, with a Unix-seconds timestamp. | +| 2 | **Pyth Hermes** | via `fetchPythAttestations` (`apps/web/src/features/trade/lib/pyth.ts`) | Used only if source 1 threw or returned nothing. Binance display prices are layered on top to fill any symbol Pyth did not return. | +| 3 | **Binance REST** | `api.binance.com/api/v3/ticker/bookTicker` | Bid becomes the min price, ask becomes the max price. | +| 4 | **GMX oracle** | `${ENV.ORACLE_URL}/prices/tickers` | Last networked source. Prices are decoded with GMX v2 scaling: `usdPrice × 10^(30 − tokenDecimals)`. | +| 5 | **Built-in dummy prices** | none | A hard-coded `DUMMY_PRICES` array. Reached only if every source above failed. Prices are static and wrong; the UI has no live data at this point. | + +The four tracked markets are BTC, ETH, XLM, and USDC, mapped from the test token +symbols `TWBTC`, `TETH`, `TXLM`, `TUSDC`. + +## What triggers a fallback + +Each source is wrapped in its own `try`/`catch`. The app moves to the next +source when the current one: + +- throws — a network error, or a non-200 HTTP response, or +- returns an empty result — for source 1, an empty map; for source 2, no Pyth + attestations. + +There is no health scoring, no retry, and no circuit breaker. A source that +succeeds once is not preferred later; every call re-walks the list from the +top. + +## Update frequency and staleness + +The trade page re-fetches prices on its own interval and marks each price with +a staleness level, from `OracleStalenessIndicator` +(`apps/web/src/features/trade/components/OracleStalenessIndicator.tsx`) and the +`OracleStaleness` type in `pyth.ts`: + +| Level | Age of the price | Indicator | +| ----- | ---------------- | --------- | +| `fresh` | under 5 seconds | green dot | +| `warning` | 5 to 30 seconds | yellow dot | +| `stale` | over 30 seconds | red dot, "Stale" label | + +The indicator is a display cue. It does not block trading, and the app does not +substitute a different source when a price goes stale — it keeps showing the +last value it has with the red dot. + +## Mark price versus execution price + +Sources 1, 3, and 4 return a `min` and a `max` price — the two sides of a +spread. The displayed mark price and the price used for position valuation and +the liquidation check are derived from these; which side is used depends on the +direction of the position and the operation. The exact selection lives in the +contract and client code that [/concepts/liquidation](/concepts/liquidation) +describes, and in `oracle.ts`'s consumers rather than in `oracle.ts` itself. +If you are integrating, read those before assuming a single price. + +## Failure modes + +| Situation | What the code does today | +| --------- | ------------------------ | +| SO4 oracle service outage | Falls through to Pyth, then Binance, then GMX. | +| All networked sources fail | Serves the static `DUMMY_PRICES`. The values are placeholders and the staleness indicator will read stale. There is no banner telling the user the feed is dead — this is unhandled. | +| One source returns a price far from the others | Nothing. `fetchTokenPrices` takes the first source that answers; it does not cross-check sources for deviation. Deviation checks, if any, happen in the contract at execution time, not in the price feed. | +| Price older than 30 seconds | Marked `stale` in the UI. Trading is not blocked client-side. | + +## Worked example + +The SO4 oracle service returns `{ symbol: "TWBTC", min: 7.87e34, max: 7.871e34, +timestamp: 1_760_000_000 }`. `oracle.ts` divides by `1e30`, giving a min of +78,700 and a max of 78,710 USD, with `updatedAt = 1_760_000_000 × 1000` ms. If +the current time is 1,760,000,020,000 ms, the price is 20 seconds old, so the +trade page shows a yellow `warning` dot. At 1,760,000,035,000 ms it is 35 +seconds old and the dot turns red. + +## The price path + +From source to liquidation check, the value flows like this: + +```text + SO4 oracle service ─┐ + Pyth Hermes ────────┤ first source that answers wins + Binance REST ───────┤ (fetchTokenPrices, oracle.ts) + GMX oracle ─────────┤ + DUMMY_PRICES ───────┘ + │ + ▼ + TokenPrice { minPrice, maxPrice, updatedAt, source } + │ + ├──▶ trade page: mark price + staleness dot + │ + ▼ + position valuation ──▶ maintenance-margin check ──▶ liquidation + (contract + client, see /concepts/liquidation) +``` + +Caption: the client only owns the top half — choosing a source and shaping a +`TokenPrice`. Position valuation and the liquidation decision run against that +price in contract and client code covered by +[/concepts/liquidation](/concepts/liquidation). + +## Related + +[/concepts/liquidation](/concepts/liquidation) — the price the oracle produces +is the input to the liquidation check. [/concepts/risk](/concepts/risk) — oracle +failure is one of the enumerated ways a position can move against you without +the market moving. diff --git a/apps/docs/content/guides/earn.mdx b/apps/docs/content/guides/earn.mdx new file mode 100644 index 00000000..a3b456cb --- /dev/null +++ b/apps/docs/content/guides/earn.mdx @@ -0,0 +1,74 @@ +--- +title: Earn +description: The earn page portfolio view, how SO4 staking and reward accrual work, and how to claim distributions or compound them. +updated: 2026-08-31 +status: beta +--- + +The earn page is where you stake SO4, see every yield-bearing position you +hold, and claim what has accrued. The staking and claim actions are in +`apps/web/src/features/earn/lib/earn.ts`; the portfolio and rewards views are +under `apps/web/src/features/earn/components/`. + +## Portfolio view + +The **Portfolio** tab (`components/portfolio/portfolio-tab.tsx`) lists your +assets and a **Rewards** bar (`components/portfolio/rewards-bar.tsx`) showing +what has accrued but not yet been claimed. GM and GLV pool positions you opened +from [/guides/pools](/guides/pools) show here alongside staked SO4, so the earn +page is the single place to read your total yield exposure. + +## Staking SO4 + + +1. Hold SO4. The token uses 7 decimals (`SO4_DECIMALS` in `earn.ts`). +2. Open the **Stake** dialog (`components/stake/StakeDialog.tsx`), enter an + amount, and confirm. The app builds a stake transaction + (`buildStakeSO4Transaction`), simulates it, and asks your wallet to sign. +3. On success the toast reads "SO4 staked successfully" and your staking + queries refresh. + + +Unstaking is the mirror action (`unstakeSO4`, `buildUnstakeSO4Transaction`). +Both reject an amount of zero or less, and both require a connected wallet +whose address matches `^G[A-Z2-7]{55}$`. + +## Reward accrual + +Staked SO4 accrues rewards over time. The accrued figure is read on-chain by +`queries/useRewardsAccrued.ts` and shown in the rewards bar. This page does not +state a reward rate or an APR: those are not constants in the client code, and +`data/pools.ts` marks the pool APY values it does carry as placeholders. Read +the live accrued amount on the page rather than projecting from a rate quoted +elsewhere. + +## Claiming and compounding + +- **Claim** builds `buildClaimRewardsTransaction` and transfers the accrued + rewards to your wallet. +- **Compound** builds `buildCompoundTransaction`, which restakes the accrued + rewards instead of transferring them, so they begin accruing too. +- **Vesting.** Some distributions vest rather than unlock immediately; + `depositForVesting` (`buildDepositForVestingTransaction`) and + `queries/useVestingSchedule.ts` drive the vesting view. Check the schedule + before assuming a claimed amount is liquid. + +The **Distributions** tab (`components/distributions/distributions-tab.tsx`) +lists past distributions and their status. + +## Worked example + +You stake 10,000 SO4. After some time the rewards bar shows 42 SO4 accrued. +**Claim** moves 42 SO4 to your wallet and resets the accrued figure toward +zero; your staked balance stays 10,000. **Compound** instead sets your staked +balance to 10,042 SO4 and resets the accrued figure, so the next accrual is +computed on the larger stake. The 42 figure is whatever the contract reports — +it is not derived from a rate in this repository. + +## Related + +[/guides/pools](/guides/pools) for the GM and GLV positions that also surface +here. [/concepts/funding-and-fees](/concepts/funding-and-fees) for where the +fees that fund pool rewards come from. Note the +[/guides/faucet](/guides/faucet) dispenses TUSDC, TWBTC, TETH, and TXLM only — +SO4 is not one of its tokens. diff --git a/apps/docs/content/guides/faucet.mdx b/apps/docs/content/guides/faucet.mdx new file mode 100644 index 00000000..36c97772 --- /dev/null +++ b/apps/docs/content/guides/faucet.mdx @@ -0,0 +1,68 @@ +--- +title: Faucet +description: Requesting SO4 testnet tokens, the per-token claim cooldown measured in ledgers, and what to do when a claim is rejected. +updated: 2026-08-31 +status: stable +--- + +The faucet at `/faucet` gives a connected wallet a fixed amount of each SO4 +testnet token — TUSDC, TWBTC, TETH, and TXLM — so you can trade, provide +liquidity, or test an integration without buying anything. Every value on the +page is read from the faucet contract, not hard-coded in the app +(`apps/web/src/features/faucet/hooks/useFaucetData.ts`). + +## Before you start + +- A wallet connected to **testnet**. If the wallet is on mainnet the page shows + a network-mismatch banner and claims target the wrong network underneath it — + see [/guides/troubleshooting](/guides/troubleshooting). +- Nothing else. The faucet does not require an existing balance or a prior + transaction. + +## Claiming tokens + + +1. Open `/faucet` and connect your wallet. +2. For one token, use its **Claim** button. For all four at once, use **Claim + all** — the app sends a single `claim_many` transaction with every token id + (`apps/web/src/features/faucet/hooks/useClaim.tsx`). +3. Sign the transaction in your wallet. On success a toast links the + transaction on the explorer and the token balances on the page refresh. + + +The claim amount per token comes from the contract's `claim_amount` view and is +shown on each token card. The four tokens all use 7 decimals +(`apps/web/src/features/faucet/data/tokens.ts`). + +## Cooldown + +The faucet enforces one cooldown, expressed as a number of **ledgers**, not +seconds. The contract exposes it as `cooldown_ledgers()`, and the page reads it +once for all tokens. The cooldown is tracked per account **and per token**: +`last_claim_ledger(account, token)` records your most recent claim of each +token independently. + +So you can claim TUSDC and TWBTC in the same session, but a second TUSDC claim +must wait `cooldown_ledgers` ledgers after the first. Stellar testnet closes a +ledger roughly every 5 seconds, so a cooldown of, for example, 720 ledgers is +about one hour — the app does not do this conversion or show a countdown. It +shows the ledger number of your last claim of each token +(`Last claim ledger 1,234,567`) and nothing more. + +## When a claim fails + +- **"Cooldown active — please wait before claiming again."** The contract + rejected the claim with error code 6 (`ClaimTooSoon`). You claimed that token + within the last `cooldown_ledgers` ledgers. Wait, or claim a different token. +- **Any other error** is passed through `parseSorobanError` and shown as-is. + A wallet-signature rejection, an expired transaction, or an RPC timeout all + land here — [/guides/troubleshooting](/guides/troubleshooting) covers each. +- The page auto-refreshes balances and claim state every 30 seconds, so a + balance that has not updated yet is usually a read lag, not a lost claim. + +## Related + +Once you have testnet tokens: [/guides/pools](/guides/pools) to provide +liquidity, [/concepts/liquidation](/concepts/liquidation) before opening a +leveraged position, and [/concepts/risk](/concepts/risk) for what testnet does +and does not simulate. diff --git a/apps/docs/content/guides/pools.mdx b/apps/docs/content/guides/pools.mdx new file mode 100644 index 00000000..8f0480a1 --- /dev/null +++ b/apps/docs/content/guides/pools.mdx @@ -0,0 +1,87 @@ +--- +title: Pools +description: Adding and removing liquidity in an SO4 GM pool, what a deposit is exposed to, and how a deposit or withdrawal is executed. +updated: 2026-08-31 +status: beta +--- + +An SO4 GM pool holds one long token and TUSDC as the short token, and stands as +the counterparty to every trader on that market. Depositing mints GM tokens +that represent your share of the pool; withdrawing burns them for the +underlying assets. The three markets and their tokens are defined in +`apps/web/src/features/pools/data/markets.ts`. + +## The pools + +| Pool | Displayed as | Long token | Short token | +| ---- | ------------ | ---------- | ----------- | +| `TWBTC/TUSDC` | BTC/USD | TWBTC | TUSDC | +| `TETH/TUSDC` | ETH/USD | TETH | TUSDC | +| `TXLM/TUSDC` | XLM/USD | TXLM | TUSDC | + +All GM tokens use 7 decimals. TVL and APY are shown as `0` on the pools page +today — `apps/web/src/features/earn/data/pools.ts` hard-codes `apy: 0` and +`tvlUsd: 0` with a note to replace them with live values. Do not read a yield +number off the current UI. + +## What a deposit is exposed to + +A GM pool provides unified liquidity: the same pool backs both the long and the +short side of trading on its market. As a depositor you take on: + +- **Trader profit and loss.** When traders on the market are net profitable, + their gains are paid from the pool; when they are net losing, their losses + accrue to the pool. +- **Price exposure to the long token.** The pool holds a real balance of the + long token, so its value moves with that token's price. +- **The mechanics in [/concepts/liquidation](/concepts/liquidation) and + [/concepts/funding-and-fees](/concepts/funding-and-fees)**, from the other + side. Funding and borrow fees paid by traders flow toward the pool; a + liquidation that leaves bad debt is the pool's loss. + +This is not a delta-neutral position and it is not a fixed yield. + +## Adding liquidity + + +1. Get the pool's tokens from [/guides/faucet](/guides/faucet) — you can deposit + the long token, TUSDC, or both. +2. On the pools page, use **Deposit** on the pool's row. Enter an amount and + confirm. The app builds a create-deposit request transaction + (`buildCreateDepositTransaction`, via `apps/web/src/features/pools`). +3. Sign it. A "Deposit pending" note appears with the transaction hash and the + line "Keeper execution usually completes within ~60s" + (`apps/web/src/features/pools/components/pool-actions.tsx`). A keeper picks + up the request and mints your GM tokens in a second transaction. + + +Deposits and withdrawals are two-step by design: you submit a request, and a +keeper executes it against the current oracle price. Your GM balance updates +after the keeper step, not after your signature. + +## Removing liquidity + +Use **Withdraw** on the pool row (enabled once you hold GM for that pool). +Enter a GM amount and confirm. This builds a create-withdrawal request +(`buildCreateWithdrawalTransaction`); the keeper burns the GM and returns the +underlying long token and TUSDC in proportion to the pool's current +composition. The same ~60 second keeper window applies. + +There is no lock-up in the pool contract. The practical exit constraint is +pool utilisation: if traders have borrowed most of one side, a large +withdrawal of that asset may be limited until positions close. + +## Worked example + +You deposit 1,000 TUSDC into the BTC/USD pool when the pool is worth 100,000 +USD and holds 100,000 GM. You receive `1,000 / 100,000 × 100,000 = 1,000` GM, +a 1% share. If traders on BTC/USD then lose 5,000 USD net and pay 500 USD in +fees, the pool is worth `100,000 − (−5,000) + 500 = 105,500` USD. Your 1% is +now worth 1,055 USD. The same 1% share absorbs 1% of a loss if traders win +instead. + +## Related + +[/concepts/risk](/concepts/risk) is the full list of what can go wrong on the +depositor side. [/concepts/oracles](/concepts/oracles) explains the price the +keeper uses when it executes your deposit or withdrawal. diff --git a/apps/docs/content/guides/referrals.mdx b/apps/docs/content/guides/referrals.mdx new file mode 100644 index 00000000..6d860b4c --- /dev/null +++ b/apps/docs/content/guides/referrals.mdx @@ -0,0 +1,74 @@ +--- +title: Referrals +description: Creating and using SO4 referral codes, the three affiliate tiers, and how the trader discount and affiliate commission are set. +updated: 2026-08-31 +status: beta +--- + +SO4 referrals do two things: a trader who applies a code pays lower fees, and +the affiliate who owns that code earns a commission on the fees their referred +traders pay. The tier table, the discount, and the commission percentages are +defined in `apps/web/src/features/referrals/data/tiers.ts`; the on-chain +actions are in `apps/web/src/features/referrals/lib/referrals.ts`. + +## Tiers + +An affiliate's tier is set by the combined monthly trading volume of the +traders using their code (`getTierFromVolume` picks the highest tier whose +threshold the volume meets): + +| Tier | Monthly referred volume | Trader fee discount | Affiliate commission | +| ---- | ----------------------- | ------------------- | -------------------- | +| Bronze | 0 USD and up | 5% | 5% | +| Silver | 2,500 USD and up | 5% | 10% | +| Gold | 25,000 USD and up | 5% | 15% | + +The trader discount is a flat **5%** at every tier — moving up tiers only +raises the affiliate's commission, from 5% to 10% to 15%. The percentages +apply to trading fees, not to trade size. Referred volume is computed by +`apps/web/src/features/referrals/lib/computeReferralVolume.ts`. + +## Using a code as a trader + + +1. Get a code from an affiliate. +2. On the referrals page, or the first-trade prompt on the trade panel, enter + the code. The app uppercases and trims it, then builds + `buildSetTraderReferralCodeTransaction` (`setTraderReferralCode` in + `lib/referrals.ts`). +3. Sign it. On success the toast reads `Referral code "CODE" applied`. From the + next trade, your trading fee is reduced by the tier's trader discount. + + +A trader address can hold one referral code. Applying a new code replaces the +old one. + +## Creating a code as an affiliate + + +1. On the referrals page, open the create-code dialog + (`components/CreateReferralDialog.tsx`). +2. Enter a code. It is validated by `validateReferralCode` before submission — + an invalid code is rejected client-side with the reason. +3. Confirm and sign. The app builds `buildRegisterCodeTransaction`; on success + the code is stored locally and the toast confirms registration. + + +Once traders use your code, the **Affiliates** and **Distributions** tabs show +your referred traders, their volume, your current tier, and the commission +accrued. Claiming a rebate builds `buildClaimRebatesTransaction`. + +## Worked example + +Three traders use your code and together trade 30,000 USD of volume this month. +30,000 is at or above the Gold threshold of 25,000, so you are Gold: a **15%** +affiliate commission. If those traders pay 300 USD in trading fees over the +month, your commission is `300 × 0.15 = 45` USD, claimable as a rebate. Each of +those traders separately pays 5% less in fees than they would with no code — so +the 300 USD figure is already the post-discount total they were charged. + +## Related + +[/guides/earn](/guides/earn) is where claimed rebates and other distributions +appear. [/concepts/funding-and-fees](/concepts/funding-and-fees) defines the +trading fees that both the discount and the commission are a percentage of. diff --git a/apps/docs/content/index.mdx b/apps/docs/content/index.mdx index 5e77b954..ac31224b 100644 --- a/apps/docs/content/index.mdx +++ b/apps/docs/content/index.mdx @@ -1,11 +1,54 @@ --- title: SO4 Docs -description: Documentation for SO4 Markets, including concepts, developer references, and operational guides. +description: Documentation for SO4, a perpetuals exchange on Stellar. Three starting points, one for traders, liquidity providers, and integrators. +updated: 2026-08-31 status: stable -updated: 2026-08-27 --- -# SO4 Docs +SO4 is a perpetual-futures exchange built on Stellar and Soroban. Traders take +leveraged long or short positions on a handful of markets; a single pool per +market provides the liquidity for both sides. These docs explain how that +works, including the parts that can lose you money. -This placeholder page keeps the documentation workspace buildable while content -and navigation are added in follow-up DX issues. +## Pick your path + +### I want to trade + +Start with the [quickstart](/get-started/quickstart): install Freighter, +connect it to testnet, claim test tokens, and place one small order. Before you +use leverage, read [liquidation](/concepts/liquidation) — it is the mechanic +that decides what happens to your collateral when a position moves against you. + +For: anyone opening a position. + +### I want to provide liquidity + +Read the [pools guide](/guides/pools). It covers adding and removing liquidity, +and — more importantly — what a deposit is exposed to: a GM pool is the +counterparty to every trader on its market, so you take on their net profit and +loss, not a fixed yield. + +For: anyone considering a deposit into a GM or GLV pool. + +### I want to integrate + +Start with the [architecture overview](/developers/architecture) for the +monorepo map, then [local setup](/developers/local-setup) to get the stack +running. The [oracles](/concepts/oracles) page traces the price path from +source to liquidation check, which most integrations need to reason about. + +For: anyone calling the contracts or running the stack locally. + +## Popular pages + +- [Quickstart](/get-started/quickstart) — wallet to first trade in about ten minutes. +- [Liquidation](/concepts/liquidation) — maintenance margin and the liquidation price. +- [Faucet](/guides/faucet) — getting testnet assets and the claim cooldown. +- Contract addresses per network are in the generated reference at `/reference/contracts.generated`. + +## Elsewhere + +- **Search** the docs from the button in the header, or press Cmd/Ctrl + K. +- **Changelog:** [/resources/changelog](/resources/changelog). +- **Source:** [github.com/SO4-Markets/interface](https://github.com/SO4-Markets/interface). +- **Error codes:** [/reference/errors](/reference/errors) if you have an on-screen message. diff --git a/apps/docs/content/meta.json b/apps/docs/content/meta.json index 3b1ad61b..8ec13c91 100644 --- a/apps/docs/content/meta.json +++ b/apps/docs/content/meta.json @@ -22,7 +22,8 @@ "pages": [ "concepts/risk", "concepts/funding-and-fees", - "concepts/liquidation" + "concepts/liquidation", + "concepts/oracles" ] }, { @@ -41,7 +42,7 @@ }, { "label": "Guides", - "pages": ["guides/troubleshooting"] + "pages": ["guides/pools", "guides/earn", "guides/referrals", "guides/faucet", "guides/troubleshooting"] }, { "label": "Resources", diff --git a/apps/docs/templates/concept.mdx b/apps/docs/templates/concept.mdx new file mode 100644 index 00000000..d8136565 --- /dev/null +++ b/apps/docs/templates/concept.mdx @@ -0,0 +1,43 @@ +--- +title: Concept title +description: One sentence naming the mechanic this page explains and the reader question it answers, between fifty and one hundred sixty characters. +updated: 2026-08-31 +status: draft +--- + +{/* + CONCEPT template. Copy to content/concepts/.mdx, then: + 1. Replace every placeholder, including the frontmatter. + 2. Add "concepts/" to the Concepts section of content/meta.json. + 3. Run check:content, check:links, and lint:prose from apps/docs. + A concept page explains how one mechanic works and how it can fail. It is + read by a trader or liquidity provider before they need it, not during a task. +*/} + +The first paragraph answers the page's title question in two or three +sentences. No history, no motivation — those come next. A reader who stops +here should still have a correct, if shallow, mental model. + +## How it works + +The mechanic, in plain terms and in the order a reader would reason about it. +Name the contract or module that implements it and link the file in the pull +request, not in the page body. + +## Worked example + +State real inputs, show the arithmetic, state the result. Compute the numbers; +do not estimate them. Example shape: "A position of X, with parameter Y, +reaches state Z at input W — here is the calculation." + +## How it can fail + +Enumerate the failure modes and what the system does in each. If a case is +unhandled today, say so; do not describe intended behaviour as current +behaviour. Link to [/concepts/risk](/concepts/risk) for the broader picture. + +## Related + +Link adjacent concepts inline where the reader needs them above, and close with +the one or two pages a reader should read next — for example +[/concepts/liquidation](/concepts/liquidation). diff --git a/apps/docs/templates/guide.mdx b/apps/docs/templates/guide.mdx new file mode 100644 index 00000000..6d9d2b41 --- /dev/null +++ b/apps/docs/templates/guide.mdx @@ -0,0 +1,47 @@ +--- +title: Guide title +description: One sentence naming the task this guide walks through and the surface it happens on, between fifty and one hundred sixty characters long. +updated: 2026-08-31 +status: draft +--- + +{/* + GUIDE template. Copy to content/guides/.mdx, then: + 1. Replace every placeholder, including the frontmatter. + 2. Add "guides/" to the Guides section of content/meta.json. + 3. Run check:content, check:links, and lint:prose from apps/docs. + A guide is task-first: what the page is for, how to do the main action, and + what to check afterwards. Perform every documented action yourself before + writing it, and confirm that in the pull request. +*/} + +One or two sentences: what this page is for and who does it. Name the route in +`apps/web` where the action happens. + +## Before you start + +What the reader needs first — a connected wallet, testnet assets from +[/guides/faucet](/guides/faucet), a position, whatever applies. Link each +prerequisite rather than re-explaining it. + +## Doing the main action + +Use `` only when it is genuinely a procedure: + + +1. First step, phrased as one concrete action. +2. Second step. Name the button or field the reader clicks or fills. +3. What confirms success — a toast, a balance change, a new row. + + +If a step can fail in a way the reader will hit, say what the failure looks +like and link [/guides/troubleshooting](/guides/troubleshooting). + +## After + +What to verify, where the result shows up, and how to reverse it if the action +is reversible. + +## Related + +The concept pages behind this surface, linked inline above and listed here. diff --git a/apps/docs/templates/reference.mdx b/apps/docs/templates/reference.mdx new file mode 100644 index 00000000..a3282d37 --- /dev/null +++ b/apps/docs/templates/reference.mdx @@ -0,0 +1,33 @@ +--- +title: Reference title +description: One sentence naming exactly what this page lists and where the values come from, between fifty and one hundred sixty characters in length. +updated: 2026-08-31 +status: draft +--- + +{/* + REFERENCE template. Copy to content/reference/.mdx, then: + 1. Replace every placeholder, including the frontmatter. + 2. Add "reference/" to the Reference section of content/meta.json. + 3. Run check:content, check:links, and lint:prose from apps/docs. + A reference page is a lookup table, not a narrative. If the values are + generated (addresses, schema, tokens), do not create this by hand — extend + the generator and commit the *.generated.mdx output instead. +*/} + +One sentence stating what the reader looks up here and the authority for the +values. Then the table or list, ordered so a reader can scan to a row fast +(alphabetical, by network, by code — whatever fits the lookup). + +## Section + +| Key | Value | Notes | +| --- | ----- | ----- | +| ... | ... | ... | + +State the "current as of" date for anything that can change, or link the +generated page that owns it. + +## Related + +The concept or guide pages that explain what these values mean.