Skip to content

Prompt: Token Swaps

Adds Ryze token swaps (exact-in, multi-hop) to an existing codebase. The generated code quotes a swap from the Router, guards the ERC-20 allowance, has the user sign an EIP-712 SwapIntent, submits it to the Relayer, and polls until the swap is confirmed on-chain.

What the agent will build:

  • A Ryze client module in your repo’s own language and conventions
  • Swap quoting with integer-exact slippage math (minAmountOut derivation)
  • Allowance check + approval helper for the input token
  • EIP-712 SwapIntent builder and signing (raw 65-byte hex signature)
  • Intent submission, status polling, and typed error handling
  • Tests against the exact Router/Relayer JSON shapes

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 it can swap ERC-20 tokens. Implement it in this repository's existing
language, framework, HTTP client, and coding conventions — study the repo first and match
its style (module layout, config handling, error types, logging, test framework).

## How Ryze swaps work (authoritative — trust this over anything you infer)

Ryze is intent-based: the user never sends a swap transaction. The flow you must implement:

1. **Quote** the swap from the Ryze Router service.
2. Ensure the user has **approved** the `MultiHopRouter` contract for `tokenIn`
   (standard ERC-20 `approve` — the only on-chain transaction in the whole flow).
3. Read the user's **swap nonce** from the `MultiHopRouter` contract.
4. Have the user **sign an EIP-712 `SwapIntent`** (typed data, no gas).
5. **POST** the signed intent to the Ryze execution relayer.
6. **Poll** the relayer until the intent is `confirmed` or `failed`.

The relayer batches intents, attaches all required oracle data (Pyth + signed CEX prices)
to the execution transaction, executes on-chain, and pays gas. Therefore:
**do not** fetch or attach any price/oracle data, **do not** call any `execute*` function
on the contract, and **do not** build raw swap calldata. If you find yourself doing any of
those, you are off the intended path.

## Configuration

Read these from environment/config (never hardcode; defaults shown are Base Sepolia
testnet, mainnet values in parentheses):

| 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_MULTIHOP_ROUTER` | `0x477A780Fab142F9289a01C9B22Ed322dE6c6af1A` | `0x8e20A1534a204DE569E9d62D410c19795b81DF70` |
| `RPC_URL` | any Base Sepolia RPC | any Base mainnet RPC |

Reference tokens (native ETH is not supported — use WETH):
- Sepolia: USDC `0x6aEB5326b5DcA2163e0995824DcB816194b157a6`, WETH `0x6Dd0C6a2e058bc3F7D6AaCe23965180BcBbb2635`, cbBTC `0xa7BaE7da3E4950700BE9f107EB186e89a474d180`
- Mainnet: USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`, WETH `0x4200000000000000000000000000000000000006`, cbBTC `0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf`

## Step 1 — Quote

`POST {RYZE_ROUTER_URL}/quote` with JSON:

```json
{
  "tokenIn":  "<address>",
  "tokenOut": "<address>",
  "amountIn": "<base-unit decimal string>",
  "slippageTolerance": 50,
  "maxHops": 3,
  "userAddress": "<user address>"
}
```

- `slippageTolerance` is basis points (50 = 0.5%); make it a parameter of your API.
- `maxHops` must be 1–3.
- Response key fields: `output.amount` (expected out, base units), `steps[]` (each with
  `pool`, `tokenIn`, `tokenOut`), `priceImpact`, `executionPrice`, `prices[]`.

Derive:
- `path = steps.map(s => ({ pool: s.pool, tokenIn: s.tokenIn, tokenOut: s.tokenOut }))`
- `minAmountOut` — see the deployed behavior below; use big-integer math, never
  floats.

One deployed behavior (verified live) MUST be handled here:

1. **Flat intent fee.** `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 `feeTok × path.length` from `amountIn` before
   the swap. Quotes exclude it, so derate:
   `minAmountOut = floor(output.amount × (amountIn − feeTok×hops) / amountIn × (10000 − slippageBps) / 10000)`.
   Warn (or refuse) when `amountIn` is worth < ~$10 — the fee alone then exceeds typical
   slippage and the intent reverts `InvalidSlippage()`/`SlippageExceeded()`.

## Step 2 — Approval

Check `ERC20(tokenIn).allowance(user, RYZE_MULTIHOP_ROUTER)`; if below `amountIn`, the user
must send `approve(RYZE_MULTIHOP_ROUTER, amount)`. Expose this as a separate step in your
API (allowance check + approval tx builder), since in most products the user's wallet sends it.

## Step 3 — Nonce

Nonces are per-user and **per-intent-type** on the contract. For swaps, `eth_call`:

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

on `RYZE_MULTIHOP_ROUTER`. The submitted nonce must EXACTLY equal this value. Read it fresh
right before signing, and never allow two in-flight (unconfirmed) swap intents for the same
user — serialize them.

## Step 4 — EIP-712 signature

Domain:

```json
{ "name": "MultiHopRouter", "version": "1", "chainId": <RYZE_CHAIN_ID>, "verifyingContract": "<RYZE_MULTIHOP_ROUTER>" }
```

Types (exact field order):

```text
SwapIntent(address user,address tokenIn,address tokenOut,uint256 amountIn,uint256 minAmountOut,Hop[] path,address recipient,uint256 deadline,uint256 nonce)
Hop(address pool,address tokenIn,address tokenOut)
```

Message values: `user` = signer address, `recipient` = who receives `tokenOut` (usually the
user), `deadline` = unix seconds ≈ `now + 1800`, `nonce` from step 3, the rest from the quote.

Sign with the user's key (`signTypedData` / equivalent) and submit the 65-byte
signature as one hex string (`r‖s‖v`, `v` = 27/28; low-`s` required — standard signers
comply automatically). 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": "swap",
  "user": "0x…",
  "tokenIn": "0x…",
  "tokenOut": "0x…",
  "amountIn": "1000000",
  "minAmountOut": "412000000000000000",
  "path": [ { "pool": "0x…", "tokenIn": "0x…", "tokenOut": "0x…" } ],
  "recipient": "0x…",
  "deadline": 1789000000,
  "nonce": 0,
  "signature": "0x…"
}
```

All amounts are base-10 **strings**. `deadline`/`nonce` are JSON numbers.

- Success → HTTP **202**: `{ "success": true, "intentId": "0x…", "status": "pending", … }`.
  202 means *accepted for execution*, not executed. `intentId` is the EIP-712 digest;
  resubmitting the identical signed intent is idempotent.
- HTTP **400**`{ "success": false, "status": "error", "message": "…" }` — surface
  `message` to the caller (covers bad signature, nonce mismatch with expected-value hint,
  expired deadline, discontinuous path).
- HTTP **429** → rate limited (~100 req/min/IP); retry with backoff.

## Step 6 — Poll status

`GET {RYZE_RELAYER_URL}/intents/{intentId}` every ~2s until `intent.status` is `confirmed`
or `failed` (typical total: 3–12s; timeout after ~90s but return the `intentId` so callers
can resume polling). Response includes `txHash`, `blockNumber`, `gasUsed`, and on failure a
decoded revert reason in `error`. A `failed` intent had no on-chain effect. Treat transient
HTTP 500s from this endpoint as retryable.

## What to build

1. A **Ryze client module** in this repo's conventions exposing at minimum:
   - `getSwapQuote(tokenIn, tokenOut, amountIn, slippageBps, user)` → quote + derived
     `path`/`minAmountOut`
   - `checkAllowance(user, token, amount)` / approval-tx helper
   - `buildSwapIntent(quote, user, recipient, deadlineSecs)` → unsigned EIP-712 payload
     (domain + types + message) ready for any signer
   - `submitSwapIntent(intent, signature)``intentId`
   - `waitForIntent(intentId, {pollMs, timeoutMs})` → final status
   - a one-call convenience `swap(...)` composing all of the above when a signer is available
2. **Config** via the env vars above, wired into this repo's config system.
3. **Error handling**: typed/structured errors for quote failures, allowance-missing,
   nonce conflicts, 4xx validation messages, timeouts.
4. **Tests** in this repo's framework: mock the Router and Relayer HTTP responses (use the
   exact JSON shapes above), assert the EIP-712 payload (domain, type string, field order),
   the big-integer slippage math, the signature hex encoding, and the polling state machine.

## Acceptance checklist

- [ ] No oracle/price code, no direct contract execution calls, no raw swap calldata.
- [ ] All amounts flow through big-integer types; JSON emits them as strings.
- [ ] Nonce read fresh per signing; concurrent same-user swaps are serialized or rejected.
- [ ] `minAmountOut > 0` always; slippage math is integer-exact and derates for
      `intentFee()` (× hops).
- [ ] EIP-712 domain/type strings byte-match the spec above.
- [ ] 202/400/429 and `pending/confirmed/failed` all handled and tested.
- [ ] Everything configurable via env; defaults point at Base Sepolia.