Demo Prompt: TypeScript + viem
Generates a standalone, runnable TypeScript + viem project demonstrating all four Ryze operations on Base Sepolia — four scripts (swap, single join, proportional join, exit), heavily commented so the code doubles as a tutorial.
What the agent will build:
- A fresh
ryze-demo-tsproject (viem, dotenv, tsx) with.env.example - A small Ryze library: config, quotes, EIP-712, relayer client, chain reads
- Four runnable scripts:
npm run swap,join-single,join-proportional,exit - A README with setup, wallet funding, and the mainnet switch
How to use
- Open your AI coding agent (Claude Code, Cursor, Windsurf, Copilot, Codex, … — any model works) in an empty directory (or an
examples/folder). The agent needs zero prior knowledge of Ryze; the prompt contains everything. - Hit the Copy prompt button below and paste the prompt as your message.
- When the agent finishes, create
.envfrom.env.example, add a funded Base Sepolia test key, and run each script. Hold the result to the acceptance criteria at the end of the prompt.
Prerequisites
Node/Go/Rust toolchain for the chosen stack, a Base Sepolia RPC URL, and a test wallet holding testnet USDC/WETH plus a little ETH for approvals. Everything else — endpoints, addresses, schemas — is embedded in the prompt.
The prompt
The prompt is fully self-contained — one click copies all of it.
Preview the full prompt text
md
Create a self-contained, runnable **TypeScript + viem** demo project that integrates the
**Ryze protocol** (an intent-based AMM on Base) end to end: swap (exact-in), single-asset
liquidity join, proportional liquidity join, and proportional liquidity exit. The demo's
purpose is to *teach the integration* — code must be heavily commented, each step explicit,
and each operation runnable as its own script.
## Protocol model (authoritative — do not deviate)
Ryze is intent-based. For every operation:
**quote → (ERC-20 approve `MultiHopRouter` if needed) → read per-type nonce → sign an
EIP-712 intent with the user's key → POST it to the Ryze execution relayer → poll status.**
The relayer batches intents, attaches the required oracle data (Pyth payloads + signed CEX
prices) to the execution transaction, executes on-chain, and pays gas. The demo must
therefore contain **no price/oracle code**, **no `execute*` contract calls**, and **no raw
swap calldata** — the only transaction ever sent from the demo wallet is `approve`.
## Configuration — `.env` (provide `.env.example`)
```bash
PRIVATE_KEY= # funded Base Sepolia test wallet (needs test USDC/WETH + a little ETH for approvals)
RPC_URL= # Base Sepolia RPC
RYZE_CHAIN_ID=84532
RYZE_RELAYER_URL=https://sepolia.relayer.ryze.pro/api/v1
RYZE_ROUTER_URL=https://sepolia.router.ryze.pro
RYZE_API_URL=https://sepolia.api.ryze.pro/api
RYZE_MULTIHOP_ROUTER=0x477A780Fab142F9289a01C9B22Ed322dE6c6af1A
RYZE_POOL_QUERIES=0x1a7B7D9071c30935448b6cC3d114c0c794d0a3C0
# Base Sepolia reference addresses
USDC=0x6aEB5326b5DcA2163e0995824DcB816194b157a6
WETH=0x6Dd0C6a2e058bc3F7D6AaCe23965180BcBbb2635
POOL_WETH_USDC=0x1aB1cC9923ed45F1C7F291B222CC3d0D2a578723
```
(Mainnet, for the README: chainId 8453, `mainnet.*.ryze.pro`, MultiHopRouter
`0x8e20A1534a204DE569E9d62D410c19795b81DF70`, PoolQueries
`0xD4b5DD638d09aB7b6Eb4Ec3490F02A96d0DD4100`.)
## API surface to implement against
**Quotes (Router):**
- Swap: `POST {RYZE_ROUTER_URL}/quote` body
`{ tokenIn, tokenOut, amountIn, slippageTolerance: <bps>, maxHops: 3, userAddress }` →
`output.amount` (string, base units), `steps[{pool,tokenIn,tokenOut}]`, `priceImpact`,
`prices[]`. Intent `path = steps.map(({pool,tokenIn,tokenOut}) => ({pool,tokenIn,tokenOut}))`.
- Join (single & proportional): `POST {RYZE_ROUTER_URL}/join-quote` body
`{ poolAddress, tokensIn: [{token, amount}, …] }` — one entry ⇒ `joinType:"single"`,
all pool tokens ⇒ `"proportional"` → `sharesOut` (18-dec string).
- Exit: on-chain view `WeightedPoolQueries.queryProportionalExit(pool, sharesIn)`.
⚠ The deployed contract returns a **struct-wrapped array** — ABI output is
`tuple(uint256[] amountsOut)`, NOT bare `uint256[]` (decoding as `uint256[]` throws).
Amounts are ordered like the pool's asset list (pool token order from
`GET {RYZE_API_URL}/pools/list`).
Slippage (bigint only): `min = expected * (10_000n - slippageBps) / 10_000n`; must be > 0.
## Execution-reality rules (verified against the live deployment — build ALL of these in)
These two behaviors are real, verified on-chain, and integrations that skip them fail:
1. **Flat intent fee.** `MultiHopRouter.intentFee()` (view → `uint256`) is a flat **USD fee
in WAD** (currently 1e16 = $0.01) charged per execution, converted to token units at the
oracle price: `feeTok(t) = ceilDiv(intentFee * 10^decimals(t), blendedPrice(t))` using
the quote's `prices[]`. Deduction point: swap — `feeTok(tokenIn) × path.length` off
`amountIn` before swapping; single join — `feeTok(tokenIn)` off `amountIn`;
proportional join — `feeTok(tokensIn[0])` off `amountsIn[0]`; exit — `feeTok(poolAsset0)`
off the **outputs** (contract requires `out[0] ≥ min[0] + fee`). Quotes do NOT include
this fee, so derate mins: scale the expected amount by `(input − fee)/input` (swap/joins)
before applying slippage; for exits subtract `feeTok(asset0)` from `minAmountsOut[0]`
(floor 1n). Use **$25-scale amounts** (e.g. 25 USDC) so the fee is well inside the
slippage budget — with 1-USDC amounts the $0.01 fee alone is 1% and every intent reverts
with `InvalidSlippage()`.
2. **LP shares are auto-staked in the pool's gauge vault.** When
`MultiHopRouter.poolToGaugeVault(pool)` (view → `address`) is set (it is for these
pools), join shares go to the LPGaugeVault, not the wallet: `pool.balanceOf(user)` stays
0. LP position = `pool.balanceOf(user) + gauge.getEligibleBalance(user)` (view →
`uint256`). Exits burn directly from the gauge balance — still no approval needed.
**Nonces** — per-user AND per-type, `readContract` on `RYZE_MULTIHOP_ROUTER`:
`swapNonces(address)`, `joinNonces(address)`, `joinProportionalNonces(address)`,
`exitProportionalNonces(address)` — all `view returns (uint256)`. The submitted nonce must
exactly equal the on-chain value; read it right before signing.
**EIP-712** — domain for all types:
`{ name: 'MultiHopRouter', version: '1', chainId: RYZE_CHAIN_ID, verifyingContract: RYZE_MULTIHOP_ROUTER }`.
Types (exact order):
```ts
const TYPES = {
SwapIntent: [
{ name: 'user', type: 'address' }, { name: 'tokenIn', type: 'address' },
{ name: 'tokenOut', type: 'address' }, { name: 'amountIn', type: 'uint256' },
{ name: 'minAmountOut', type: 'uint256' }, { name: 'path', type: 'Hop[]' },
{ name: 'recipient', type: 'address' }, { name: 'deadline', type: 'uint256' },
{ name: 'nonce', type: 'uint256' } ],
Hop: [
{ name: 'pool', type: 'address' }, { name: 'tokenIn', type: 'address' },
{ name: 'tokenOut', type: 'address' } ],
JoinIntent: [
{ name: 'user', type: 'address' }, { name: 'pool', type: 'address' },
{ name: 'tokenIn', type: 'address' }, { name: 'amountIn', type: 'uint256' },
{ name: 'minSharesOut', type: 'uint256' }, { name: 'recipient', type: 'address' },
{ name: 'deadline', type: 'uint256' }, { name: 'nonce', type: 'uint256' } ],
JoinProportionalIntent: [
{ name: 'user', type: 'address' }, { name: 'pool', type: 'address' },
{ name: 'tokensIn', type: 'address[]' }, { name: 'amountsIn', type: 'uint256[]' },
{ name: 'minSharesOut', type: 'uint256' }, { name: 'recipient', type: 'address' },
{ name: 'deadline', type: 'uint256' }, { name: 'nonce', type: 'uint256' } ],
ExitProportionalIntent: [
{ name: 'user', type: 'address' }, { name: 'pool', type: 'address' },
{ name: 'sharesIn', type: 'uint256' }, { name: 'minAmountsOut', type: 'uint256[]' },
{ name: 'recipient', type: 'address' }, { name: 'deadline', type: 'uint256' },
{ name: 'nonce', type: 'uint256' } ],
}
```
Sign with `account.signTypedData({domain, types, primaryType, message})` and submit
the returned hex string as-is — no splitting into `v`/`r`/`s`.
**Submit** — `POST {RYZE_RELAYER_URL}/intents/submit`. Common: `user`, `recipient`,
`deadline` (number, unix seconds ≈ now+1800), `nonce` (number),
`signature` (the raw hex string). All amounts base-10 **strings**. Type-specific:
- `{ type: "swap", tokenIn, tokenOut, amountIn, minAmountOut, path: Hop[] }`
- `{ type: "join", pool, tokenIn, amountIn, minSharesOut, otherPoolToken }` —
`otherPoolToken` = the pool's other asset; REQUIRED in body, NOT in the signed message.
- `{ type: "joinProportional", pool, tokensIn: [], amountsIn: [], minPoolTokensOut }` —
body says `minPoolTokensOut`, signed field is `minSharesOut` (same value).
- `{ type: "exitProportional", pool, sharesIn, minAmountsOut: [], poolTokens: [] }` —
`poolTokens` (ordered pool assets, aligned with `minAmountsOut`) REQUIRED in body, NOT
signed. No approval needed for exits.
Responses: 202 `{ success, intentId, status: "pending" }` (idempotent on resubmit);
400 `{ success: false, message }`; 429 rate limit → backoff.
**Poll** — `GET {RYZE_RELAYER_URL}/intents/{intentId}` every 2s until
`intent.status ∈ {confirmed, failed}` (typical 3–12s; timeout 90s printing the intentId).
Print `txHash`, `blockNumber`, `gasUsed`; on `failed` print the decoded revert in `error`.
## Project to generate
```
ryze-demo-ts/
├── package.json # type: module; deps: viem, dotenv; tsx for running
├── tsconfig.json
├── .env.example
├── README.md # setup, funding a test wallet, how to run each script, mainnet switch
└── src/
├── config.ts # env loading + validation
├── ryze/
│ ├── types.ts # quote/intent/response types
│ ├── quotes.ts # swap quote, join quote, exit view-call quote, slippage math
│ ├── eip712.ts # domain + TYPES + buildXxxIntent + sign helpers
│ ├── relayer.ts # submitIntent, waitForIntent (polling), error surfacing
│ └── chain.ts # viem clients, nonce reads, allowance check + ensureApproval
└── scripts/
├── 01-swap.ts # USDC → WETH swap
├── 02-join-single.ts # USDC-only join into WETH-USDC pool
├── 03-join-proportional.ts # WETH+USDC proportional join
└── 04-exit.ts # exit a fraction of the LP position
```
Each script: loads config → prints the quote (human-readable with decimals from
`GET {RYZE_API_URL}/assets`) → applies the execution-reality rules (fee-derated mins) →
ensures approval (skipped for exit) → reads the correct
nonce → builds + signs the intent → prints the full submit body → submits → polls,
printing status transitions to `confirmed`/`failed`. Use $25-scale amounts (e.g. 25 USDC)
— large enough that the flat intent fee is negligible, small enough to be cheap.
## Acceptance
- `npm install && npm run swap` (and `join-single`, `join-proportional`, `exit`) each run
end to end against Base Sepolia with a funded key and a printed txHash.
- Type-checks clean (`tsc --noEmit`). All amount math in `bigint`.
- All execution-reality rules implemented: intent-fee derating and gauge-vault position
reads.
- Comments at each step explain WHY (intent model, per-type nonces, the flat intent fee,
the gauge auto-stake, and the three field traps: `otherPoolToken`, `minPoolTokensOut`
vs `minSharesOut`, `poolTokens`).
- README documents the flow diagram, prerequisites, and the mainnet env switch.