Prompt: Proportional Liquidity Join
Lets users add liquidity with all pool tokens in ratio — the fee-free way to LP on Ryze. The generated code computes proportional amounts, quotes, checks every token’s allowance, signs an EIP-712 JoinProportionalIntent, submits, and polls.
What the agent will build:
- Proportional amount calculation from a single user-entered amount
- Join quoting with
minPoolTokensOutderivation (and theminSharesOutsigned-field mapping) - Multi-token allowance detection returning the full list of missing approvals
- EIP-712
JoinProportionalIntentbuilder with correct array encoding - Submission, polling, and tests for the array/naming pitfalls
How to use
- Open your AI coding agent (Claude Code, Cursor, Windsurf, Copilot, Codex, … — any model works) inside the repository you want to integrate. The agent needs zero prior knowledge of Ryze; the prompt contains everything.
- Optionally type one line of placement guidance first, e.g.
Put this under src/integrations/ryze/. Then: - Hit the Copy prompt button below and paste the prompt as your message.
- When the agent finishes, hold the result to the acceptance checklist at the end of the prompt — every box should check.
Prerequisites
None for running the prompt itself. To exercise the generated code you’ll need a Base Sepolia RPC URL and a test wallet holding testnet USDC/WETH plus a little ETH for the one-time approve transaction. All endpoints and addresses are already embedded in the prompt with Base Sepolia defaults; mainnet values are included for the switch.
The prompt
The prompt is fully self-contained — one click copies all of it.
Preview the full prompt text
md
You are integrating the **Ryze protocol** (an intent-based AMM on Base) into this
repository so users can add liquidity to a Ryze pool by depositing **all pool tokens in
proportion** (fee-free) and receive LP shares. Implement it in this repository's existing
language, framework, HTTP client, and conventions — study the repo first and match its style.
## How Ryze proportional joins work (authoritative)
Depositing all pool tokens at the pool's current ratio incurs **no fees** (unlike
single-asset joins). The pool contract itself is the ERC-20 LP token. The user never sends
the join transaction:
1. Compute proportional amounts and **quote** via the Ryze Router service.
2. Ensure the user has **approved** the `MultiHopRouter` contract for **every** input token.
3. Read the user's **proportional-join nonce** from the `MultiHopRouter` contract.
4. Have the user **sign an EIP-712 `JoinProportionalIntent`**.
5. **POST** the signed intent to the Ryze execution relayer.
6. **Poll** until `confirmed` or `failed`.
The relayer batches intents, attaches all oracle data (Pyth + signed CEX prices), executes
on-chain, and pays gas. **Do not** fetch/attach prices, call `execute*` contract functions,
or build raw calldata.
## Configuration
Read from environment/config (never hardcode):
| Env var | Default (Base Sepolia, chainId 84532) | Base mainnet (chainId 8453) |
|---|---|---|
| `RYZE_CHAIN_ID` | `84532` | `8453` |
| `RYZE_RELAYER_URL` | `https://sepolia.relayer.ryze.pro/api/v1` | `https://mainnet.relayer.ryze.pro/api/v1` |
| `RYZE_ROUTER_URL` | `https://sepolia.router.ryze.pro` | `https://mainnet.router.ryze.pro` |
| `RYZE_API_URL` | `https://sepolia.api.ryze.pro/api` | `https://mainnet.api.ryze.pro/api` |
| `RYZE_MULTIHOP_ROUTER` | `0x477A780Fab142F9289a01C9B22Ed322dE6c6af1A` | `0x8e20A1534a204DE569E9d62D410c19795b81DF70` |
| `RPC_URL` | any Base Sepolia RPC | any Base mainnet RPC |
Pool discovery: `GET {RYZE_API_URL}/pools/list` → pools with `tokens[{address,weight,decimals}]`.
Reference pools:
- Sepolia: WETH-USDC `0x1aB1cC9923ed45F1C7F291B222CC3d0D2a578723`, cbBTC-USDC `0xaC522dD3B13c8b2F4b765d100b1Ea13493Cf55bA`
- Mainnet: WETH-USDC `0x22f902cEfcF8b0bEc6489Cb8ac11FdDa9B2aF125`, cbBTC-USDC `0x40F3DAaE59BfE03f9Fb019Bb089Bb0C381DE27Cf`
## Step 1 — Quote
`POST {RYZE_ROUTER_URL}/join-quote` with **all** pool tokens present:
```json
{
"poolAddress": "0x…",
"tokensIn": [
{ "token": "0xWETH…", "amount": "500000000000000000" },
{ "token": "0xUSDC…", "amount": "1250000000" }
]
}
```
When every pool token is included, the response has `joinType: "proportional"` and zero
fees. Key fields: `sharesOut` (18-decimal string), `tvl`, `shareOfPool`, `prices[]`
(per-token USD prices in WAD — use these to compute the proportional counterpart amount
from a user-entered primary amount). Errors: 400 invalid params, 404 unknown pool.
UX guidance to implement: let the user enter ONE token amount, derive the other token
amount(s) from the current pool ratio (pool balances or the quote's `prices[]`), then quote
to confirm and get `sharesOut`. If the ratio drifts before submission, the intent still
executes proportionally — surplus tolerance is captured by `minSharesOut`.
Deriving `minPoolTokensOut` MUST account for the **flat intent fee** (deployed behavior,
verified live): `MultiHopRouter.intentFee()` (view → `uint256`) is a flat USD fee in WAD
(currently 1e16 = $0.01) converted to units of **`tokensIn[0]`** at oracle price
(`feeTok0 = ceilDiv(intentFee × 10^decimals(tokensIn[0]), blendedPrice(tokensIn[0]))`,
prices from the quote's `prices[]`) and deducted from `amountsIn[0]` before the join —
which caps the proportional ratio and thus the shares. Quotes exclude it, so:
`minPoolTokensOut = floor(sharesOut × (amountsIn[0] − feeTok0) / amountsIn[0] × (10000 − slippageBps) / 10000)`
with big-integer math; must be > 0. Warn (or refuse) when `amountsIn[0]` is worth < ~$10 —
the fee alone then exceeds typical slippage and the intent reverts `InvalidSlippage()`.
## Step 2 — Approvals (plural)
Every token in `tokensIn` needs `ERC20.allowance(user, RYZE_MULTIHOP_ROUTER) ≥ amount`.
Build the allowance check to return the full list of missing approvals so the UI can
request them all before signing.
## Step 3 — Nonce
Proportional joins have their own nonce mapping. `eth_call` on `RYZE_MULTIHOP_ROUTER`:
```solidity
function joinProportionalNonces(address user) external view returns (uint256);
```
Submitted nonce must EXACTLY equal this value. Fresh read before signing; never two
in-flight proportional-join intents for the same user.
## Step 4 — EIP-712 signature
Domain:
```json
{ "name": "MultiHopRouter", "version": "1", "chainId": <RYZE_CHAIN_ID>, "verifyingContract": "<RYZE_MULTIHOP_ROUTER>" }
```
Type (exact field order):
```text
JoinProportionalIntent(address user,address pool,address[] tokensIn,uint256[] amountsIn,uint256 minSharesOut,address recipient,uint256 deadline,uint256 nonce)
```
⚠️ Naming trap: the **signed field is `minSharesOut`**, but the **HTTP body field is
`minPoolTokensOut`** — same value, two names. Sign `minSharesOut = minPoolTokensOut`.
`tokensIn` and `amountsIn` must be same length, same order, ≥ 2 entries, no duplicate
tokens, every amount > 0. `recipient` receives LP shares; `deadline` ≈ `now + 1800`
(unix seconds). Sign with the user's key and submit the 65-byte signature as one hex
string (`r‖s‖v`, `v` = 27/28; low-`s` required). ERC-1271 smart-contract wallets (e.g.
Safe) submit their contract signature bytes instead.
## Step 5 — Submit
`POST {RYZE_RELAYER_URL}/intents/submit`:
```json
{
"type": "joinProportional",
"user": "0x…",
"pool": "0x…",
"tokensIn": ["0xWETH…", "0xUSDC…"],
"amountsIn": ["500000000000000000", "1250000000"],
"minPoolTokensOut": "1234500000000000000",
"recipient": "0x…",
"deadline": 1789000000,
"nonce": 0,
"signature": "0x…"
}
```
Amounts are base-10 strings. Responses: HTTP **202**
`{ "success": true, "intentId": "0x…", "status": "pending" }` (accepted, idempotent on
resubmit); HTTP **400** with a `message` (bad signature / nonce mismatch with hint /
expired deadline / array length mismatch / duplicate tokens); HTTP **429** rate limit
(~100 req/min/IP) — back off.
## Step 6 — Poll status
`GET {RYZE_RELAYER_URL}/intents/{intentId}` every ~2s until `confirmed`/`failed`
(typical 3–12s; timeout ~90s, return `intentId` for resumable polling). `failed` carries a
decoded revert reason in `error`; no on-chain effect.
⚠ **LP shares are auto-staked** (deployed behavior, verified live): when
`MultiHopRouter.poolToGaugeVault(pool)` (view → `address`) is non-zero — true for the
listed pools — join shares are staked into the LPGaugeVault for the recipient, so
`ERC20(pool).balanceOf(recipient)` stays 0. The LP position is
`pool.balanceOf(recipient) + gauge.getEligibleBalance(recipient)`.
## What to build
1. A **Ryze proportional-join module** in this repo's conventions:
- `getProportionalAmounts(pool, primaryToken, primaryAmount)` → all-token amounts at
current ratio
- `getProportionalJoinQuote(pool, tokensIn[], slippageBps)` → quote + `minPoolTokensOut`
- multi-token allowance check / approval helpers
- `buildJoinProportionalIntent(...)` → unsigned EIP-712 payload
- `submitJoinProportionalIntent(intent, signature)` → `intentId`
- `waitForIntent(intentId, {pollMs, timeoutMs})`
- convenience `joinProportional(...)` composing the flow
2. **Config** via the env vars above.
3. **Typed errors** (quote failure, missing approvals — listed per token, nonce conflict,
validation 400s, timeout).
4. **Tests**: mock HTTP with exact shapes above; assert EIP-712 array encoding and field
order, the `minSharesOut`(signed)/`minPoolTokensOut`(body) mapping, integer math,
multi-approval detection, polling state machine.
## Acceptance checklist
- [ ] No oracle/price code, no direct contract execution, no raw calldata.
- [ ] `minSharesOut` in the signed message equals body `minPoolTokensOut`, derated for
`intentFee()` on `tokensIn[0]`.
- [ ] LP position read as wallet + gauge (`poolToGaugeVault`/`getEligibleBalance`).
- [ ] `tokensIn`/`amountsIn` aligned, ≥2, no duplicates, all > 0 — validated before signing.
- [ ] All input tokens' allowances verified before submission.
- [ ] `joinProportionalNonces` used; fresh read; in-flight serialization.
- [ ] 202/400/429 and `pending/confirmed/failed` handled and tested.
- [ ] Everything configurable via env; defaults point at Base Sepolia.