Skip to content

Demo Prompt: Go

Generates a standalone, runnable Go project (go-ethereum) demonstrating all four Ryze operations on Base Sepolia — four commands under cmd/, heavily commented so the code doubles as a tutorial.

What the agent will build:

  • A fresh ryze-demo-go module (go-ethereum, godotenv) with .env.example
  • An internal/ryze package: config, quotes, EIP-712 typed data, relayer client, chain reads
  • Four runnable commands: go run ./cmd/swap, joinsingle, joinproportional, exit
  • A README with setup, wallet funding, and the mainnet switch

How to use

  1. 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.
  2. Hit the Copy prompt button below and paste the prompt as your message.
  3. When the agent finishes, create .env from .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.

View raw
Preview the full prompt text
md
Create a self-contained, runnable **Go** demo project (using `github.com/ethereum/go-ethereum`)
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* — heavily commented,
each step explicit, each operation runnable as its own command.

## 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), executes on-chain, and pays gas. The demo must contain **no price/oracle code**,
**no `execute*` contract calls**, and **no raw swap calldata** — the only transaction the
demo wallet sends is `approve`.

## Configuration — `.env` (provide `.env.example`; load with godotenv or plain os.Getenv)

```bash
PRIVATE_KEY=            # funded Base Sepolia test wallet (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
USDC=0x6aEB5326b5DcA2163e0995824DcB816194b157a6
WETH=0x6Dd0C6a2e058bc3F7D6AaCe23965180BcBbb2635
POOL_WETH_USDC=0x1aB1cC9923ed45F1C7F291B222CC3d0D2a578723
```

(Mainnet, for the README: chainId 8453, `mainnet.*.ryze.pro`, MultiHopRouter
`0x8e20A1534a204DE569E9d62D410c19795b81DF70`, PoolQueries
`0xD4b5DD638d09aB7b6Eb4Ec3490F02A96d0DD4100`.)

## API surface (response shapes verified against the live deployment)

**Swap quote:** `POST {RYZE_ROUTER_URL}/quote` body
`{"tokenIn","tokenOut","amountIn","slippageTolerance":<bps>,"maxHops":3,"userAddress"}`.
Response (relevant fields — ignore the rest):

```json
{
  "output":      {"token":"0x…","amount":"13997342590282446","decimals":18},
  "steps":       [{"pool":"0x…","tokenIn":"0x…","tokenOut":"0x…", "…":"…"}],
  "priceImpact": "0.00092408",
  "prices":      [{"token":"0x…","pythPrice":"…","cexPrice":"…",
                   "blendFactor":"…","blendedPrice":"999894724499999900"}]
}
```

- `output.amount` is a base-unit base-10 string; **`priceImpact` is a decimal
  STRING**, not a JSON number.
- Intent path = the steps' `{pool,tokenIn,tokenOut}` triples in order.
- `prices[]` gives the blended oracle price per token in **USD WAD per whole
  token**; use `blendedPrice` for all intent-fee conversions.
- ⚠ Addresses in `steps[]`, `path[]`, and `prices[].token` are returned
  **lowercase** — compare addresses case-insensitively everywhere.

**Join quote:** `POST {RYZE_ROUTER_URL}/join-quote` body
`{"poolAddress","tokensIn":[{"token","amount"},…]}` — one entry ⇒ `joinType:"single"`,
all pool tokens ⇒ `"proportional"`. Response: `sharesOut` (18-dec base-unit string) and
the same `prices[]` array as the swap quote (use it for fee derating).

**Exit quote:** `eth_call` `WeightedPoolQueries.queryProportionalExit(address pool, uint256 sharesIn)`.
⚠ The deployed contract returns a **struct-wrapped array** — ABI output is
`tuple(uint256[] amountsOut)`, NOT bare `uint256[]` (decoding as `uint256[]` fails).
Amounts are ordered like the pool's asset list. The eth_call carries no prices, so for
exit fee-derating harvest `prices[]` from a nominal swap quote between the pool assets.

**Asset metadata:** `GET {RYZE_API_URL}/assets` returns a **Uniswap-style token list**,
NOT a bare array:

```json
{"name":"Ryze Protocol List","timestamp":"…","version":{},
 "tokens":[
   {"chainId":84532,"name":"Ethereum","symbol":"ETH","decimals":18,"isNative":true, "…":"…"},
   {"chainId":84532,"name":"USD Coin","symbol":"USDC","decimals":6,"address":"0x6aEB…"}
 ]}
```

Read `tokens[]`; the native-ETH entry has **no `address` field** and must be skipped
(the demo deals only in ERC-20s). Use `symbol`/`decimals` for human-readable printing.

**Pool metadata:** `GET {RYZE_API_URL}/pools/list` returns:

```json
{"pools":[{"address":"0x…",
           "tokens":[{"address":"0x…","weight":"500000000000000000","decimals":6},
                     {"address":"0x…","weight":"500000000000000000","decimals":18}],
           "totalSupplyLP":"…"}],
 "total":4}
```

The `tokens[]` order is the pool's canonical asset order — exit amounts,
`minAmountsOut`, and the submit body's `poolTokens[]` all align with it. Pool tokens
carry `decimals` but **no `symbol`** — enrich symbols from `/assets` for display.

Slippage with `math/big` only: `min = expected × (10000 − bps) / 10000`; must be > 0.

## Execution-reality rules (verified against the live deployment — build ALL of these in)

1. **Flat intent fee.** `MultiHopRouter.intentFee()` (view → `uint256`) is a flat **USD fee
   in WAD** (currently 1e16 = $0.01) per execution, converted to token units at oracle
   price: `feeTok(t) = ceilDiv(intentFee × 10^decimals(t), blendedPrice(t))` using the
   quote's `prices[]`. Deducted: swap — `feeTok(tokenIn) × len(path)` off `amountIn`;
   single join — `feeTok(tokenIn)` off `amountIn`; proportional join — `feeTok(tokensIn[0])`
   off `amountsIn[0]`; exit — `feeTok(poolAsset0)` off the outputs (contract enforces
   `out[0] ≥ min[0] + fee`). Quotes exclude this fee → derate mins by the fee ratio before
   slippage (exit: subtract `feeTok(asset0)` from `minAmountsOut[0]`, floor 1). Use
   **$25-scale amounts** — at $1 the fee alone is 1% and intents revert `InvalidSlippage()`.
2. **Validate the swap path on-chain before signing.** The quote service can route through
   pools NOT registered on the executing router (`PoolNotRegistered()`; its `excludedPools`
   param is ignored). Call `MultiHopRouter.validatePath(path) → bool`; if false, substitute
   a single-hop path through `POOL_WETH_USDC`, re-validate, then sign (pools are
   oracle-priced, so the quoted output stays a valid min basis).
3. **LP shares are auto-staked in the pool's gauge vault.** `MultiHopRouter.poolToGaugeVault(pool)`
   (view → `address`) is set for these pools: join shares go to the LPGaugeVault, and
   `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 view functions on `RYZE_MULTIHOP_ROUTER`:
`swapNonces(address)`, `joinNonces(address)`, `joinProportionalNonces(address)`,
`exitProportionalNonces(address)`. Submitted nonce must exactly equal the on-chain value;
read right before signing.

**EIP-712** — use `github.com/ethereum/go-ethereum/signer/core/apitypes`:

```go
domain := apitypes.TypedDataDomain{
    Name: "MultiHopRouter", Version: "1",
    ChainId: math.NewHexOrDecimal256(chainID),
    VerifyingContract: multiHopRouter,
}
```

Types (exact order; include `EIP712Domain` with name/version/chainId/verifyingContract):

```go
"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: `hash, _, err := apitypes.TypedDataAndHash(typedData)` then
`sig, err := crypto.Sign(hash, key)`; set `sig[64] += 27` and hex-encode the full 65
bytes: `"0x" + hex.EncodeToString(sig)`. (go-ethereum produces low-`s` — required by the
relayer.)

**Submit**`POST {RYZE_RELAYER_URL}/intents/submit`, JSON. Common fields: `user`,
`recipient`, `deadline` (unix seconds ≈ now+1800, JSON number), `nonce` (JSON number),
`signature: "0x…"` (the 65-byte hex string). All amounts base-10 **strings**. Type-specific:

- `{"type":"swap", "tokenIn","tokenOut","amountIn","minAmountOut","path":[{pool,tokenIn,tokenOut}]}`
- `{"type":"join", "pool","tokenIn","amountIn","minSharesOut","otherPoolToken"}`
  `otherPoolToken` = the pool's other asset; REQUIRED in body, NOT in the signed struct.
- `{"type":"joinProportional", "pool","tokensIn":[],"amountsIn":[],"minPoolTokensOut"}`
  body field `minPoolTokensOut`, signed field `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":true,"intentId":"0x…","status":"pending"}` (idempotent);
400 `{"success":false,"message":"…"}` (surface the message — includes nonce hints);
429 rate limit → exponential backoff.

**Poll**`GET {RYZE_RELAYER_URL}/intents/{intentId}` every 2s until
`intent.status ∈ {confirmed, failed}` (typical 3–12s; 90s timeout printing the intentId).
Print `txHash`, `blockNumber`, `gasUsed`; on `failed` print the decoded revert in `error`.

## Project to generate

```
ryze-demo-go/
├── go.mod                      # deps: go-ethereum, godotenv
├── .env.example
├── README.md                   # setup, funding, run instructions, mainnet switch
├── internal/ryze/
│   ├── config.go               # env config + validation
│   ├── quotes.go               # swap/join HTTP quotes, exit view-call quote, slippage math
│   ├── eip712.go               # typed-data builders for all 4 intents + sign
│   ├── relayer.go              # submit + poll (status structs, error surfacing)
│   └── chain.go                # ethclient: nonce reads, allowance/EnsureApproval, balances
└── cmd/
    ├── swap/main.go            # USDC → WETH
    ├── joinsingle/main.go      # USDC-only join into WETH-USDC pool
    ├── joinproportional/main.go
    └── exit/main.go            # exit a fraction of the LP position
```

Each command: load config → print quote (human-readable using decimals from
`GET {RYZE_API_URL}/assets`) → apply the execution-reality rules (fee-derated mins; path
validation for the swap) → ensure approval (skip for exit) → read the correct nonce →
build + sign the intent → print the submit body → submit → poll with status transitions to
`confirmed`/`failed`. Use $25-scale amounts (e.g. 25 USDC).

## Acceptance

- `go build ./...` clean; `go run ./cmd/swap` (and the other three) complete end to end on
  Base Sepolia with a funded key and a printed txHash.
- All three execution-reality rules implemented: intent-fee derating, `validatePath`
  fallback, gauge-vault position reads.
- All amount math via `*big.Int`; JSON amounts marshaled as strings.
- JSON decoding matches the documented response shapes exactly: `/assets` as a token
  list with the address-less native entry skipped, `/pools/list` under `pools[]`,
  `priceImpact` as a string, and case-insensitive address matching against the
  lowercase addresses in `steps[]`/`prices[]`.
- Comments explain the intent model, per-type nonces, the flat intent fee, the gauge
  auto-stake, and the three field traps
  (`otherPoolToken`, `minPoolTokensOut` vs `minSharesOut`, `poolTokens`).
- README includes a flow diagram, prerequisites, and the mainnet env switch.