# Yieldbay Public API

Base URL: https://api.yieldbay.fi

Yieldbay exposes curated health monitoring data for Solana yield protocols, the live protocol yield overview, and the same wallet portfolio and rebalance engine that powers the Yieldbay dashboard. The public API is built for dashboards, health feeds, monitors, bots, and LLM/agent integrations.

## Authentication

Use one of:

```http
Authorization: Bearer yb_live_<key_id>.<secret>
X-API-Key: yb_live_<key_id>.<secret>
```

API keys are owned by a connected Solana wallet. There are no email-based API users.
Get or rotate an API key from the API page at https://app.yieldbay.fi/developers.
The raw API key is shown only when it is created or rotated. If the key is lost, rotate it from the Yieldbay app with the owning wallet.

## Scopes

Every endpoint under `/v1` declares one scope:

- `health:read`: `/v1/health/*`.
- `yields:read`: `/v1/yields`.
- `portfolio:read`: `/v1/wallets/:wallet/portfolio` and `/v1/wallets/:wallet/snapshots`.

A key carries the scopes chosen when it was created (all of them by default). The wallet's tier decides which scopes are entitled; a request needs both. `403 tier_upgrade_required` means the tier lacks the scope, `403 missing_scope` means the key does.

## Tiers and Limits

Limits come from the wallet's tier (`wallet_limits.tier`) plus optional per-wallet overrides. Every call counts once against the global limits and once against its endpoint group:

- `rate_limit_per_minute` and `quota_per_month`: the global ceiling for all requests, wallet-scoped.
- `group_limits`: per-endpoint-group windows on top of the global ones, each with an optional `per_minute` and `per_month`. Groups: `health` (`/v1/health/*`), `yields` (`/v1/yields*`), `portfolio` (`/v1/wallets/:wallet/portfolio`), `snapshots` (`/v1/wallets/:wallet/snapshots`), `account` (`/v1/me/usage`). A group without windows is bounded by the global limits only. `429 group_rate_limited` names the group and window.
- `max_active_keys`: wallet-scoped active key cap.

The current public tier is `free_beta`:

- global: 60 per minute, 300,000 per month
- `portfolio`: 2 per minute, 300 per month (live wallet scans are the expensive part; this is a trial allowance)
- `yields`: 30 per minute
- `snapshots`: 30 per minute
- `health` and `account`: global limits only
- 1 active key

Every monthly window, global and per group, is anchored to the moment the wallet first got API access. If a wallet gets API access on April 25 at 10:30 UTC, its monthly windows reset on May 25 at 10:30 UTC. Month-end dates are clamped when needed.

Successful authenticated responses include:

```http
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1777098000
X-RateLimit-Month-Limit: 300000
X-RateLimit-Month-Remaining: 299999
X-RateLimit-Month-Reset: 1779689400
```

Routes in a group with windows add the group's headers (only the windows the tier defines):

```http
X-RateLimit-Group: portfolio
X-RateLimit-Group-Limit: 2
X-RateLimit-Group-Remaining: 1
X-RateLimit-Group-Reset: 1777098000
X-RateLimit-Group-Month-Limit: 300
X-RateLimit-Group-Month-Remaining: 299
X-RateLimit-Group-Month-Reset: 1779689400
```

## Error Format

```json
{
  "error": { "code": "rate_limited", "message": "API rate limit exceeded" },
  "request_id": "..."
}
```

## Public

### GET /health

No API key required.

Response:

```json
{
  "ok": true,
  "service": "yieldbay-api",
  "timestamp": 1777097429754,
  "request_id": "..."
}
```

### GET /docs

Returns a human-readable HTML documentation page.

### GET /docs.md

Returns this markdown document for agents and CLI clients.

### GET /llms.txt

Returns a short LLM discovery file.

## Health Data

API key required. Scope: `health:read`.
Protocol filter values: `spl_stake_pools`, `meteora_dv`, `jupiter_borrow`, `jupiter_earn`, `perena`, `kamino`, `meteora_amm_tx`.

### GET /v1/health/summary

Returns current system state and one row for every monitored protocol.

Response shape:

```json
{
  "data": {
    "status": "ok|warning|critical|stale",
    "freshness": "fresh|stale",
    "critical_open": 0,
    "warning_open": 0,
    "info_open": 0,
    "visible_events": 0,
    "last_run": {
      "run_id": 5970,
      "status": "ok",
      "started_at": "2026-04-25T10:00:00.000Z",
      "duration_ms": 1234,
      "protocols": ["spl_stake_pools"]
    },
    "protocols": [
      {
        "protocol": "perena",
        "protocol_name": "Perena USD*",
        "status": "ok",
        "critical_open": 0,
        "warning_open": 0,
        "info_open": 0,
        "links": {
          "app": "https://app.yieldbay.fi/health",
          "protocol": "https://app.perena.org/portfolio"
        },
        "display": {
          "protocol_name": "Perena USD*",
          "protocol_name_key": "protocol.perena.name",
          "status_label": "Operational",
          "status_label_key": "health_status.ok.label"
        }
      }
    ],
    "display": {
      "title": "Yieldbay health: Operational",
      "summary": "0 critical, 0 warning, 0 info open incidents. 0 visible feed events.",
      "status_label": "Operational",
      "status_label_key": "health_status.ok.label",
      "freshness_label": "Fresh",
      "freshness_label_key": "freshness.fresh.label"
    }
  },
  "meta": { "as_of": "2026-04-25T10:00:00.000Z", "stale_after_ms": 900000 },
  "request_id": "..."
}
```

### GET /v1/health/insight

Returns the latest saved AI health insight as `text/plain`.

Example:

```text
All monitored protocols are operational. No critical incidents are open.
```

### GET /v1/health/events

Latest public feed events. Query params:

- `limit`: 1-100, default 50.
- `cursor`: pagination cursor from `meta.next_cursor`.
- `protocol`: comma-separated protocol ids.
- `severity`: `critical,warning,info`.
- `status`: `open,detected,recovered,normalized`.
- `from`, `to`: millisecond timestamps.
- `sort`: `latest` or `severity`, default `latest`.

### GET /v1/health/incidents

Current open incidents. Query params:

- `limit`: 1-100, default 50.
- `cursor`: pagination cursor from `meta.next_cursor`.
- `protocol`: comma-separated protocol ids.
- `severity`: `critical,warning,info`.
- `from`, `to`: millisecond timestamps.
- `sort`: `severity` or `latest`, default `severity`.

Events and incidents response shape:

```json
{
  "data": [
    {
      "id": "incident-key",
      "kind": "incident",
      "source_kind": "incident",
      "type": "SUPPLY_DROP",
      "alert_type": "SUPPLY_DROP",
      "title": "Balance dropped",
      "summary": "Vault balance is down 15.20% from its reference level.",
      "protocol": "kamino",
      "protocol_name": "Kamino kVaults",
      "entity": { "name": "Vault", "address": "...", "type": "vault" },
      "severity": "warning",
      "status": "open",
      "field": {
        "name": "shares_supply",
        "label": "Shares supply",
        "label_key": "field.shares_supply.label"
      },
      "display": {
        "title": "Balance dropped",
        "title_key": "alert.SUPPLY_DROP.title",
        "summary": "Vault balance is down 15.20% from its reference level.",
        "protocol_name": "Kamino kVaults",
        "protocol_name_key": "protocol.kamino.name",
        "entity_name": "Vault",
        "severity_label": "Warning",
        "severity_label_key": "severity.warning.label",
        "status_label": "Open",
        "status_label_key": "status.open.label",
        "field_label": "Shares supply",
        "field_label_key": "field.shares_supply.label"
      },
      "values": {
        "reference": "1,000.00",
        "current": "850.00",
        "delta": "-150.00",
        "delta_pct": "-15.00%",
        "worst": "800.00",
        "worst_delta": "-200.00",
        "worst_delta_pct": "-20.00%"
      },
      "raw_values": {
        "reference": "1000",
        "current": "850",
        "delta": -150,
        "delta_pct": -15
      },
      "started_at": "2026-04-25T10:00:00.000Z",
      "updated_at": "2026-04-25T10:05:00.000Z",
      "resolved_at": null,
      "sticky_until": "2026-04-26T10:05:00.000Z",
      "run_id": 5970,
      "links": {
        "app": "https://app.yieldbay.fi/health",
        "explorer": "https://solscan.io/account/...",
        "protocol": "https://kamino.com/lend/usdc-prime"
      }
    }
  ],
  "meta": {
    "limit": 50,
    "cursor": null,
    "next_cursor": null,
    "sort": "severity",
    "as_of": "2026-04-25T10:05:00.000Z",
    "offset": 0
  },
  "request_id": "..."
}
```

For `/v1/health/events`, item `kind` is `event`. For `/v1/health/incidents`, item `kind` is `incident`. `source_kind` identifies the underlying source.

## Yields

API key required. Scope: `yields:read`.

### GET /v1/yields

Protocol-level yield overview, refreshed every 5 minutes: SOL options (Jito, Meteora Dynamic Vault, The Vault), Perena USD*, every enabled Meteora Dynamic Vault, Jupiter Lend Earn vaults and Kamino Earn vaults. Pair-scoped pools (DLMM, DAMM) are wallet-dependent and only appear in the wallet portfolio.

Response shape:

```json
{
  "data": {
    "generated_at": "2026-09-09T07:15:00.000Z",
    "sol": {
      "jito": { "apy": 4.86, "sol_per_jitosol": "1.3003" },
      "meteora_dv": { "token_mint": "So111...", "virtual_price": "1.1264", "lp_mint": "...", "apy_7d": 0.56, "average_apy": 0.55, "closest_apy": 0.55 },
      "the_vault": { "mint": "EUWoT...", "apy": 8.18, "apy_7d": 8.9, "apy_30d": 8.18, "exchange_rate": 0.996, "total_balance_sol": 6139.7, "updated_at": "..." }
    },
    "perena": { "mint": "star9...", "price": 1.0937, "apy_24h": 10.76, "apy_7d": 9.62 },
    "meteora_dv": [ { "token_address": "...", "symbol": "USDC", "pubkey": "...", "lp_mint": "...", "closest_apy": 3.1, "average_apy": 3.0, "long_apy": 3.2, "usd_rate": 1 } ],
    "jupiter_lend": [ { "asset_mint": "...", "jl_token": "...", "symbol": "jlUSDC", "name": "...", "decimals": 6, "supply_rate": 5.08, "rewards_rate": 0.99, "total_apy": 6.07, "tvl": null, "liquidity": "..." } ],
    "kamino": [ { "address": "...", "name": "USDC Prime", "url": "https://kamino.com/lend/usdc-prime", "token_mint": "...", "shares_mint": "...", "apy": 6.2, "apy_7d": 0.06, "apy_30d": 0.06, "tvl": 133820000, "share_price": 1.05 } ]
  },
  "meta": { "as_of": "2026-09-09T07:15:00.000Z", "cache": "hit|miss", "engine_version": "v1" },
  "request_id": "..."
}
```

APY values are percentages (`4.86` = 4.86%). Rates that are exact on-chain ratios are decimal strings.

### GET /v1/yields/tokens/:mint

Yield options for one token, without a wallet: the same registries the wallet scan loads, ranked by the same rule the dashboard uses to pick a token's best option. Query params: `min_pool_tvl` (default 10000).

```json
{
  "data": {
    "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "symbol": "USDC",
    "name": "USD Coin",
    "decimals": 6,
    "usd_price": 0.9999,
    "logo": "https://...",
    "settings": { "minPoolTvl": 10000, "minSolThreshold": 0.1, "bpsThreshold": 0.005 },
    "best": { "type": "dlmm", "apy": 306.8 },
    "links": { "meteora_dv": "https://app.meteora.ag/vaults", "dlmm": "https://app.meteora.ag/dlmm/<pool>", "damm_v2": "https://app.meteora.ag/dammv2/<pool>", "jupiter_lend": "https://jup.ag/lend/earn/USDC/deposit", "kamino": "https://kamino.com/lend/elemental-usdc-turbo", "perena": "https://app.perena.org/portfolio" },
    "meteora_dv": { "pubkey": "...", "lp_mint": "...", "closest_apy": 3.1, "average_apy": 3.0 },
    "dlmm": { "best": { "address": "...", "name": "SOL-USDC", "apy": 306.8, "liquidity": 5625978 }, "pools": [] },
    "damm_v2": { "best": null, "pools": [] },
    "damm_v1": { "best": null, "pools": [] },
    "jupiter_lend": { "jlTokenAddress": "...", "totalApy": 4.2, "supplyRate": 3.8, "rewardsRate": 0.4 },
    "kamino": { "best": { "address": "...", "name": "Elemental USDC Turbo", "url": "https://kamino.com/lend/elemental-usdc-turbo", "apy": 7.9, "tvl": 6000000 }, "vaults": [] },
    "perena": { "apy_7d": 9.37, "price": 1.0937, "mint": "star9..." },
    "the_vault": null,
    "jito": null
  },
  "meta": { "as_of": "2026-09-09T08:00:00.000Z", "engine_version": "v1" },
  "request_id": "..."
}
```

`links` is only present for protocols the token can actually use. `perena` is set only for USDC; `the_vault` and `jito` only for SOL. Pool and vault objects are the registry records as the dashboard uses them (camelCase for Jupiter Lend and Kamino, snake_case for Meteora).

## Wallet Portfolio

API key required. Scope: `portfolio:read`. Every `/portfolio` call counts against the `portfolio` group windows as well as the global limits. Results for the same wallet are cached for 60 seconds (`meta.cache`); cached responses count like any other call.

### GET /v1/wallets/:wallet/portfolio

Runs the Yieldbay dashboard engine for any Solana wallet: balances across Token and Token-2022 accounts, positions in Jito, Meteora Dynamic Vaults, DLMM, Jupiter Lend, Kamino Earn, Perena and The Vault, the best yield opportunity per token, the rebalance suggestions, and the single headline recommendation for SOL. It is the same computation the app shows, returned as JSON.

Query params (all optional, defaults are the dashboard defaults):

- `min_pool_tvl`: minimum pool TVL in USD for DLMM/DAMM pools to be considered. Default 10000.
- `min_sol`: minimum idle SOL balance before staking is suggested. Default 0.1.
- `rebalance_threshold_bps`: minimum APY improvement, in basis points, before a rebalance is suggested. Default 50.

Response shape (token and opportunity objects mirror the app's internal model and use camelCase):

```json
{
  "data": {
    "engine_version": "v1",
    "network": "solana:mainnet-beta",
    "wallet": "<solana-wallet>",
    "scanned_at": 1788938122456,
    "settings": { "minPoolTvl": 10000, "minSolThreshold": 0.1, "bpsThreshold": 0.005 },
    "tokens": [
      {
        "mint": "So11111111111111111111111111111111111111112",
        "symbol": "SOL",
        "name": "Wrapped SOL",
        "decimals": 9,
        "isNative": true,
        "walletBalance": 0.0064,
        "usdPrice": 104.86,
        "lpBalance": 0,
        "jupiterLendBalance": 0.0205,
        "kaminoBalance": 0,
        "dlmmBalance": 0,
        "hasDV": true, "hasDLMM": true, "hasDammV2": true, "hasDammV1": false,
        "hasJupiterLend": true, "hasKamino": true, "hasTheVault": true,
        "bestType": "dlmm",
        "apy": 278.5,
        "dlmmPair": { "address": "...", "name": "SOL-USDC", "apy": 278.5, "liquidity": 1234567 },
        "jupiterLendVault": { "jlTokenAddress": "...", "totalApy": 3.9 },
        "kaminoVault": { "address": "...", "name": "Allez SOL", "apy": 4.5 },
        "dlmmPositions": [], "kaminoPositions": []
      }
    ],
    "rates": { "jito": "1.3003", "meteora": "1.1264" },
    "growth": { "jito": "0.00082", "meteora": "0.00011" },
    "apys": { "jito": 4.86, "meteora": 0.56, "perena": 9.62, "dlmm": 278.5, "kamino": 0, "theVault": 8.18 },
    "sol_price": 104.86,
    "balances": { "sol": 0.0064, "jitoSol": 0.0079, "meteoraLp": 0, "perenaUsdStar": 0.028, "theVaultVlp": 0.0205 },
    "totals": { "total_usd": 6.48, "total_sol": 0.0618, "weighted_apy": 5.79 },
    "snapshot": { "total_usd": 6.48, "wallet_usd": 1.07, "jito_usd": 1.08, "meteora_usd": 0, "perena_usd": 0.03, "the_vault_usd": 2.14, "jupiter_lend_usd": 2.15, "dlmm_usd": 0, "kamino_usd": 0 },
    "protocol_values": { "walletUsd": 1.07, "jitoUsd": 1.08, "meteoraUsd": 0, "perenaUsd": 0.03, "theVaultUsd": 2.14, "jupiterLendUsd": 2.15, "jupiterLendPositionsCount": 1, "dlmmUsd": 0, "dlmmPositionsCount": 0, "kaminoUsd": 0, "kaminoPositionsCount": 0 },
    "active_protocols": ["Jito", "Perena", "The Vault", "Jupiter Lend"],
    "portfolio_sources": ["Wallet", "Jito", "Perena", "The Vault"],
    "opportunities": [
      { "symbol": "USDC", "opportunityType": "deposit", "action": "deposit_dlmm", "fromState": "idle", "toState": "dlmm", "currentApy": 0, "targetApy": 278.5, "apyGain": 278.5, "displayBalance": 0.39 },
      { "symbol": "JitoSOL", "opportunityType": "rebalance", "action": "rebalance_jito_to_the-vault", "fromState": "jito", "toState": "the-vault", "currentApy": 4.86, "targetApy": 8.18, "apyGain": 3.32, "displayBalance": 0.0103, "displaySymbol": "SOL" }
    ],
    "recommendation": {
      "action": "switch",
      "target": "the-vault",
      "from": "jito",
      "enabled": true,
      "label": "Switch to The Vault",
      "steps": ["unstake_jito", "deposit_the_vault"],
      "tooltip": null,
      "recommendation": "Switch 0.01 SOL: JitoSOL (4.9%) → The Vault (8.2%)"
    }
  },
  "meta": { "as_of": "2026-09-09T07:15:29.166Z", "scanned_at": "2026-09-09T07:15:22.456Z", "cache": "miss", "engine_version": "v1" },
  "request_id": "..."
}
```

Token objects carry the full per-token detail the dashboard uses (registry entries for the best DLMM/DAMM pool, Jupiter Lend and Kamino vault records, DLMM and Kamino position lists); the example above is abbreviated. Opportunity objects are the token object plus the fields shown.

Every token also carries `links` (one URL per protocol it can use, same shape as in `/v1/yields/tokens/:mint`), every opportunity carries `links: { withdraw, deposit }` (`withdraw` is `null` for idle-balance deposits; each is `{ url, label }`), and the recommendation carries `links: { from, target }`. These are the exact URLs the Yieldbay dashboard buttons open.

`data.health` is the position-health section: incidents and recent events from the Yieldbay health monitor that touch the protocols and entities this wallet is exposed to (Jito stake pool, its Meteora vaults, Jupiter Earn vaults, Kamino vaults, the DLMM program, Perena).

```json
{
  "health": {
    "exposure": true,
    "targets": {
      "exact": [ { "protocol": "spl_stake_pools", "entity_address": "Jito4AP...", "label": "JitoSOL" } ],
      "protocol_level": [ { "protocol": "perena", "label": "Perena USD*" } ]
    },
    "items": [
      {
        "dashboard_source": "incident",
        "id": "...", "protocol": "jupiter_earn", "entity_address": "...", "entity_name": null,
        "alert_type": "SUPPLY_DROP", "severity": "warning", "field": "tvl",
        "prev_value": "...", "curr_value": "...", "delta": -1.2, "delta_pct": -3.4,
        "timestamp": 1788938000000, "time": 1788938000000,
        "labels": { "protocol": "Jupiter Earn", "entity": "Jupiter Earn SOL", "type": "Balance dropped", "field": "TVL", "state": "Open" },
        "links": { "protocol": { "url": "https://jup.ag/lend/earn/SOL/deposit", "label": "Open earn" } }
      }
    ]
  }
}
```

`exposure` is `false` (with empty targets and items) when the wallet holds nothing a monitor covers. Items are sorted critical first, then newest. Incident items carry the fields of `health_open_incidents`; event items (`dashboard_source: "event"`) carry the fields of the public events feed plus `status`.

### GET /v1/wallets/:wallet/snapshots

Portfolio value history for that wallet, oldest first. A point is saved after every fresh scan, by the Yieldbay app on a dashboard load and by `/v1/wallets/:wallet/portfolio` on a cache miss, merged into one point per five minutes. Any wallet accumulates history from its first scan.

```json
{
  "data": {
    "wallet": "<solana-wallet>",
    "snapshots": [
      { "timestamp": 1788938122456, "at": "2026-09-09T07:15:22.456Z", "total_usd": 6.48, "wallet_usd": 1.07, "jito_usd": 1.08, "meteora_usd": 0, "perena_usd": 0.03, "the_vault_usd": 2.14, "jupiter_lend_usd": 2.15, "dlmm_usd": 0, "kamino_usd": 0 }
    ]
  },
  "meta": { "as_of": "2026-09-09T07:15:29.166Z", "count": 1 },
  "request_id": "..."
}
```

## API Key Self-Service

Connected-wallet access state is read-only and does not require a signature. Mutating key actions use signed Solana messages.

### GET /v1/wallets/:wallet/api-access

No API key or wallet signature required. Returns the wallet's tier and effective limits, active key metadata, and current minute/month/daily-scan usage. The raw API key secret is never returned.
Usage is returned once at `data.usage`; individual key rows do not include usage counters.

Response shape:

```json
{
  "data": {
    "wallet": "<solana-wallet>",
    "wallet_limits": {
      "wallet": "<solana-wallet>",
      "tier": "free_beta",
      "tier_name": "Free beta",
      "scopes": ["health:read", "yields:read", "portfolio:read"],
      "rate_limit_per_minute": 60,
      "quota_per_month": 300000,
      "max_active_keys": 1,
      "group_limits": { "portfolio": { "per_minute": 2, "per_month": 300 }, "yields": { "per_minute": 30 }, "snapshots": { "per_minute": 30 } },
      "overrides": null,
      "created_at": "2026-04-25T10:00:00.000Z",
      "updated_at": "2026-04-25T10:00:00.000Z"
    },
    "usage": {
      "minute": { "scope": "wallet", "used": 0, "limit": 60, "remaining": 60, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" },
      "month": { "scope": "wallet", "used": 0, "limit": 300000, "remaining": 300000, "rate_limited": 0, "period_start": "2026-04-25T10:00:00.000Z", "reset_at": "2026-05-25T10:00:00.000Z" },
      "groups": {
        "portfolio": {
          "minute": { "scope": "wallet", "used": 0, "limit": 2, "remaining": 2, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" },
          "month": { "scope": "wallet", "used": 0, "limit": 300, "remaining": 300, "rate_limited": 0, "period_start": "2026-04-25T10:00:00.000Z", "reset_at": "2026-05-25T10:00:00.000Z" }
        },
        "yields": { "minute": { "scope": "wallet", "used": 0, "limit": 30, "remaining": 30, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" } },
        "snapshots": { "minute": { "scope": "wallet", "used": 0, "limit": 30, "remaining": 30, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" } }
      }
    },
    "active_key": {
      "key_id": "ak...",
      "wallet": "<solana-wallet>",
      "label": "Yieldbay API",
      "scopes": ["health:read"],
      "status": "active",
      "allowed_origins": [],
      "origin_required": false,
      "wallet_limits": {
        "wallet": "<solana-wallet>",
        "tier": "free_beta",
        "tier_name": "Free beta",
        "scopes": ["health:read", "yields:read", "portfolio:read"],
        "rate_limit_per_minute": 60,
        "quota_per_month": 300000,
        "max_active_keys": 1,
        "group_limits": { "portfolio": { "per_minute": 2, "per_month": 300 }, "yields": { "per_minute": 30 }, "snapshots": { "per_minute": 30 } },
        "overrides": null,
        "created_at": "2026-04-25T10:00:00.000Z",
        "updated_at": "2026-04-25T10:00:00.000Z"
      },
      "created_at": "2026-04-25T10:00:00.000Z",
      "expires_at": null,
      "revoked_at": null,
      "last_used_at": null,
      "last_origin": null,
      "username": null,
      "nft_address": null,
      "source": null
    },
    "keys": []
  },
  "meta": { "as_of": "2026-04-25T10:00:00.000Z" },
  "request_id": "..."
}
```

For create, rotate, and revoke: first request a nonce, sign the returned `message`, then submit the signed payload.

### POST /v1/auth/nonce

No API key required.

Body:

```json
{ "wallet": "<solana-wallet>", "action": "create_api_key|rotate_api_key|revoke_api_key", "resource": "ak..." }
```

`resource` is omitted or `null` for `create_api_key`; it is required for `rotate_api_key` and `revoke_api_key`.

Response:

```json
{
  "data": {
    "wallet": "<solana-wallet>",
    "action": "create_api_key",
    "resource": null,
    "nonce": "...",
    "message": "Yieldbay API key request\nWallet: ...",
    "expires_at": "2026-04-25T10:05:00.000Z"
  },
  "request_id": "..."
}
```

### POST /v1/me/api-keys

Create a key for the signing wallet.

Body:

```json
{
  "wallet": "<solana-wallet>",
  "nonce": "...",
  "message": "...",
  "signature": "...",
  "label": "Production monitor",
  "scopes": ["health:read"],
  "allowed_origins": ["https://example.com"],
  "origin_required": false,
  "expires_at": 1777098000000
}
```

`scopes` may be any subset of `health:read`, `yields:read`, `portfolio:read`; when omitted the key gets all three. `expires_at` is optional and must be a future millisecond timestamp.
The raw `key` is returned only in the create response. Store it securely. If it is lost, rotate the key.

Response:

```json
{
  "data": {
    "key": "yb_live_ak...<secret>",
    "key_id": "ak...",
    "wallet": "<solana-wallet>",
    "label": "Production monitor",
    "scopes": ["health:read"],
    "status": "active",
    "wallet_limits": {
      "wallet": "<solana-wallet>",
      "tier": "free_beta",
      "tier_name": "Free beta",
      "scopes": ["health:read", "yields:read", "portfolio:read"],
      "rate_limit_per_minute": 60,
      "quota_per_month": 300000,
      "max_active_keys": 1,
      "group_limits": { "portfolio": { "per_minute": 2, "per_month": 300 }, "yields": { "per_minute": 30 }, "snapshots": { "per_minute": 30 } },
      "overrides": null,
      "created_at": "2026-04-25T10:00:00.000Z",
      "updated_at": "2026-04-25T10:00:00.000Z"
    },
    "allowed_origins": ["https://example.com"],
    "origin_required": false,
    "created_at": "2026-04-25T10:00:00.000Z",
    "expires_at": null,
    "wallet_usage": {
      "minute": { "scope": "wallet", "used": 0, "limit": 60, "remaining": 60, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" },
      "month": { "scope": "wallet", "used": 0, "limit": 300000, "remaining": 300000, "rate_limited": 0, "period_start": "2026-04-25T10:00:00.000Z", "reset_at": "2026-05-25T10:00:00.000Z" },
      "groups": {
        "portfolio": {
          "minute": { "scope": "wallet", "used": 0, "limit": 2, "remaining": 2, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" },
          "month": { "scope": "wallet", "used": 0, "limit": 300, "remaining": 300, "rate_limited": 0, "period_start": "2026-04-25T10:00:00.000Z", "reset_at": "2026-05-25T10:00:00.000Z" }
        },
        "yields": { "minute": { "scope": "wallet", "used": 0, "limit": 30, "remaining": 30, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" } },
        "snapshots": { "minute": { "scope": "wallet", "used": 0, "limit": 30, "remaining": 30, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" } }
      }
    },
    "warning": "Copy this key now and store it securely. If it is lost, rotate the key from the Yieldbay app."
  },
  "request_id": "..."
}
```

### POST /v1/me/api-keys/:keyId/rotate

Revokes one active key owned by the signing wallet and returns a new raw `key`. This requires a fresh signed nonce with `action = "rotate_api_key"` and `resource = keyId`. The new key is issued with every scope (like a freshly created key), so rotating is how a key created before newer scopes existed picks them up; the wallet's tier still decides what each request may use. Label, allowed origins and expiry carry over unless the body overrides them. The response also includes `wallet_usage` for the owning wallet.
The request body is the same signed payload as create. Optional key settings follow create semantics. Response shape is the same as create and adds `rotated_from`.

### POST /v1/me/api-keys/:keyId/revoke

Revokes one key owned by the signing wallet.
The request body must include `wallet`, `nonce`, `message`, and `signature` for `action = "revoke_api_key"` and `resource = keyId`.

Response:

```json
{
  "data": { "key_id": "ak...", "revoked": true },
  "request_id": "..."
}
```

## Authenticated Usage

### GET /v1/me/usage

API key required. The key authenticates the wallet, then the endpoint returns wallet-level limits, current wallet-scoped minute/month usage, wallet-level rollup totals, and metadata for the authenticated key. It does not return key-scoped usage totals.

Query params:

- `granularity`: `hour`, `day`, or `month`.
- `limit`: 1-500, default 30.

Response:

```json
{
  "data": {
    "wallet": "<solana-wallet>",
    "wallet_limits": {
      "wallet": "<solana-wallet>",
      "tier": "free_beta",
      "tier_name": "Free beta",
      "scopes": ["health:read", "yields:read", "portfolio:read"],
      "rate_limit_per_minute": 60,
      "quota_per_month": 300000,
      "max_active_keys": 1,
      "group_limits": { "portfolio": { "per_minute": 2, "per_month": 300 }, "yields": { "per_minute": 30 }, "snapshots": { "per_minute": 30 } },
      "overrides": null,
      "created_at": "2026-04-25T10:00:00.000Z",
      "updated_at": "2026-04-25T10:00:00.000Z"
    },
    "usage": {
      "minute": { "scope": "wallet", "used": 1, "limit": 60, "remaining": 59, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" },
      "month": { "scope": "wallet", "used": 10, "limit": 300000, "remaining": 299990, "rate_limited": 0, "period_start": "2026-04-25T10:00:00.000Z", "reset_at": "2026-05-25T10:00:00.000Z" },
      "groups": {
        "portfolio": {
          "minute": { "scope": "wallet", "used": 1, "limit": 2, "remaining": 1, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" },
          "month": { "scope": "wallet", "used": 2, "limit": 300, "remaining": 298, "rate_limited": 0, "period_start": "2026-04-25T10:00:00.000Z", "reset_at": "2026-05-25T10:00:00.000Z" }
        },
        "yields": { "minute": { "scope": "wallet", "used": 0, "limit": 30, "remaining": 30, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" } },
        "snapshots": { "minute": { "scope": "wallet", "used": 0, "limit": 30, "remaining": 30, "rate_limited": 0, "reset_at": "2026-04-25T10:01:00.000Z" } }
      }
    },
    "totals": [
      {
        "period_start_ms": 1777065600000,
        "period_start": "2026-04-25T00:00:00.000Z",
        "granularity": "day",
        "request_count": 10,
        "error_count": 0,
        "rate_limited_count": 0,
        "duration_ms_avg": 120,
        "duration_ms_max": 350,
        "response_bytes_sum": 4096
      }
    ],
    "authenticated_key": {
      "key_id": "ak...",
      "wallet": "<solana-wallet>",
      "label": "Yieldbay API",
      "scopes": ["health:read"],
      "status": "active",
      "allowed_origins": [],
      "origin_required": false,
      "wallet_limits": {
        "wallet": "<solana-wallet>",
        "tier": "free_beta",
        "tier_name": "Free beta",
        "scopes": ["health:read", "yields:read", "portfolio:read"],
        "rate_limit_per_minute": 60,
        "quota_per_month": 300000,
        "max_active_keys": 1,
        "group_limits": { "portfolio": { "per_minute": 2, "per_month": 300 }, "yields": { "per_minute": 30 }, "snapshots": { "per_minute": 30 } },
        "overrides": null,
        "created_at": "2026-04-25T10:00:00.000Z",
        "updated_at": "2026-04-25T10:00:00.000Z"
      },
      "created_at": "2026-04-25T10:00:00.000Z",
      "expires_at": null,
      "revoked_at": null,
      "last_used_at": "2026-04-25T10:00:30.000Z",
      "last_origin": null,
      "username": null,
      "nft_address": null,
      "source": null
    }
  },
  "meta": { "as_of": "2026-04-25T10:00:30.000Z" },
  "request_id": "..."
}
```
