Skip to content

Prompt: Single-Asset Liquidity Join

Lets users add liquidity to a Ryze pool with a single token and receive LP shares. The generated code quotes the join, handles the otherPoolToken requirement, signs an EIP-712 JoinIntent, submits, and polls to completion.

What the agent will build:

  • Join quoting via the Router’s /join-quote with minSharesOut derivation
  • Resolution of the pool’s other asset (otherPoolToken, required by the Relayer but never signed)
  • Allowance check + approval helper for the deposited token
  • EIP-712 JoinIntent builder, signing, submission, and polling
  • Tests covering the signed-vs-body field distinction

How to use

  1. 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.
  2. Optionally type one line of placement guidance first, e.g. Put this under src/integrations/ryze/. Then:
  3. Hit the Copy prompt button below and paste the prompt as your message.
  4. 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.

View raw
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 with a **single token** 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 single-asset joins work (authoritative)

Ryze is intent-based: the user deposits one of the pool's tokens; the pool internally
rebalances (so swap fees apply to part of the deposit) and mints LP shares (the pool
contract itself is the ERC-20 LP token). The user never sends the join transaction:

1. **Quote** the join from the Ryze Router service.
2. Ensure the user has **approved** the `MultiHopRouter` contract for `tokenIn`.
3. Read the user's **join nonce** from the `MultiHopRouter` contract.
4. Have the user **sign an EIP-712 `JoinIntent`** (typed data, no gas).
5. **POST** the signed intent to the Ryze execution relayer.
6. **Poll** the relayer 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 |

Reference pools (discover dynamically via `GET {RYZE_API_URL}/pools/list`, which returns
each pool's `tokens[{address,weight,decimals}]`):
- Sepolia: WETH-USDC `0x1aB1cC9923ed45F1C7F291B222CC3d0D2a578723`, cbBTC-USDC `0xaC522dD3B13c8b2F4b765d100b1Ea13493Cf55bA`
- Mainnet: WETH-USDC `0x22f902cEfcF8b0bEc6489Cb8ac11FdDa9B2aF125`, cbBTC-USDC `0x40F3DAaE59BfE03f9Fb019Bb089Bb0C381DE27Cf`

## Step 1 — Quote

`POST {RYZE_ROUTER_URL}/join-quote`:

```json
{ "poolAddress": "0x…", "tokensIn": [ { "token": "0x…", "amount": "1000000" } ] }
```

Exactly one entry in `tokensIn` → the response comes back with `joinType: "single"`.
Key response fields: `sharesOut` (expected LP shares, 18 decimals, string), `tvl`,
`shareOfPool`, `fees{swapFee,takerFee,slippageFee,wbfFee,wbrReward,…}`, `prices[]`.
Errors: 400 invalid params, 404 unknown pool.

Deriving `minSharesOut` 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 tokenIn units at oracle price
(`feeTok = ceilDiv(intentFee × 10^decimals(tokenIn), blendedPrice(tokenIn))`, prices from
the quote's `prices[]`) and deducted from `amountIn` before the join. Quotes exclude it, so:
`minSharesOut = floor(sharesOut × (amountIn − feeTok) / amountIn × (10000 − slippageBps) / 10000)`
with big-integer math (never floats); must be > 0. Warn (or refuse) when `amountIn` is
worth < ~$10 — the fee alone then exceeds typical slippage and the intent reverts
`InvalidSlippage()`.

Also determine `otherPoolToken`: the pool's *other* asset (for two-token pools, the one the
user is not depositing) from the pool's `tokens[]` list. It is required by the relayer
(used off-chain for oracle coverage) but is **NOT part of the signed message**.

## Step 2 — Approval

Check `ERC20(tokenIn).allowance(user, RYZE_MULTIHOP_ROUTER)` and have the user
`approve(RYZE_MULTIHOP_ROUTER, amountIn)` if insufficient. This is the only on-chain
transaction the user sends.

## Step 3 — Nonce

Joins have their own nonce mapping. `eth_call` on `RYZE_MULTIHOP_ROUTER`:

```solidity
function joinNonces(address user) external view returns (uint256);
```

Submitted nonce must EXACTLY equal this value. Read fresh before signing; never allow two
in-flight 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 — note `otherPoolToken` is absent):

```text
JoinIntent(address user,address pool,address tokenIn,uint256 amountIn,uint256 minSharesOut,address recipient,uint256 deadline,uint256 nonce)
```

`recipient` receives the LP shares (usually the user); `deadline` = unix seconds ≈
`now + 1800`. 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": "join",
  "user": "0x…",
  "pool": "0x…",
  "tokenIn": "0x…",
  "amountIn": "1000000",
  "minSharesOut": "998877665544332211",
  "otherPoolToken": "0x…",
  "recipient": "0x…",
  "deadline": 1789000000,
  "nonce": 0,
  "signature": "0x…"
}
```

Amounts are base-10 strings. Responses: HTTP **202**
`{ "success": true, "intentId": "0x…", "status": "pending" }` (accepted, not yet executed;
idempotent on resubmit); HTTP **400** with `message` (bad signature / nonce mismatch with
expected-value hint / expired deadline / missing `otherPoolToken`); HTTP **429** rate limit
(~100 req/min/IP) — back off.

## Step 6 — Poll status

`GET {RYZE_RELAYER_URL}/intents/{intentId}` every ~2s until `confirmed` or `failed`
(typical 3–12s; timeout ~90s but return `intentId` for resumable polling). `failed`
includes a decoded revert reason in `error` and has 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 join module** in this repo's conventions exposing at minimum:
   - `getJoinQuote(pool, tokenIn, amountIn, slippageBps)` → quote + `minSharesOut` +
     `otherPoolToken`
   - allowance check / approval-tx helper
   - `buildJoinIntent(...)` → unsigned EIP-712 payload (domain + types + message)
   - `submitJoinIntent(intent, signature)``intentId`
   - `waitForIntent(intentId, {pollMs, timeoutMs})`
   - a one-call convenience `joinSingle(...)` composing the flow when a signer is available
2. **Config** via the env vars above.
3. **Typed errors** for quote failures, missing allowance, nonce conflict, validation 400s,
   timeout.
4. **Tests**: mock Router/Relayer HTTP with the exact JSON shapes above; assert the EIP-712
   type string and field order (especially that `otherPoolToken` is in the HTTP body but
   NOT the signed message), integer slippage math, signature hex encoding, polling state machine.

## Acceptance checklist

- [ ] No oracle/price code, no direct contract execution, no raw calldata.
- [ ] `otherPoolToken` present in submit body, absent from EIP-712 message.
- [ ] Amounts as big-integer-backed strings end to end; `minSharesOut > 0` and derated for
      `intentFee()`.
- [ ] LP position read as wallet + gauge (`poolToGaugeVault`/`getEligibleBalance`).
- [ ] `joinNonces` (not `swapNonces`) used; fresh read per signing; in-flight serialization.
- [ ] 202/400/429 and `pending/confirmed/failed` handled and tested.
- [ ] Everything configurable via env; defaults point at Base Sepolia.