Skip to content
Open app

Keeper HTTP API

The keeper serves a small read-only JSON API with live venue rates, rate history, the decision log, target weights and fee sweeps. Every endpoint can be called from this page.

On this page

Basics#

Base URLhttps://keeper-production-199f.up.railway.app
MethodsGET only (and OPTIONS for CORS preflight)
AuthNone. Everything is public and read-only
CORSAny origin, so browsers can call it directly
FormatJSON. Times are ISO 8601 UTC, APYs in percent, amounts in USDC, weights as fractions of the vault
Errors{ "error": "message" } with status 400, 404 or 500

The examples below are real responses captured on 8 October 2026. Send request fetches the live answer from your browser. The web app reads the same endpoints; you can point it at another keeper under Settings.

GET /health#

Liveness, the time the last cycle finished, and the cluster the keeper runs against. It answers 200 as long as the process runs.

GET/health
200 OK
{ "ok": true, "lastCycle": "2026-10-08T06:45:58.271Z", "cluster": "devnet" }
FieldMeaning
okAlways true while the server is up
lastCycleWhen the last full cycle (both tiers) finished; null before the first one
clusterdevnet, mainnet-beta, localnet, custom, or paper for simulated vaults

A recent lastCycle is the real health signal: cycles run every 10 minutes, so anything older than about 25 minutes means the keeper is stuck.

GET /rates#

One snapshot per strategy, Bedrock first, in registry order. Cached by the keeper for 5 minutes.

GET/rates
200 OK (first of 11 entries)
[
  {
    "id": "kamino-main-usdc",
    "venue": "Kamino",
    "product": "Main market USDC",
    "chain": "solana",
    "asset": "USDC",
    "apy": 4.3429,
    "apyBase": 4.3429,
    "apyReward": 0,
    "tvlUsd": 124254435,
    "utilization": 0.9024,
    "riskScore": 2,
    "tier": "bedrock",
    "source": "kamino-api",
    "fetchedAt": "2026-10-08T06:41:48.368Z"
  }
]
FieldMeaning
idRegistry strategy id; see Tiers and venues
apy, apyBase, apyRewardTotal APY and its base and reward parts, percent
tvlUsdVenue TVL as the source reports it (0 when unknown)
utilizationBorrowed share of the venue, 0 to 1, or null when the venue does not expose it
sourcekamino-api, jupiter-api, defillama, exponent-api, or fallback when every source failed
fetchedAtWhen the quote was fetched upstream

GET /rates/history#

Thirty days of APY history for one strategy. id is required. Venues with a history API return daily points; the others return what the keeper has recorded itself.

GET/rates/history?id=kamino-main-usdc
200 OK (trimmed)
{
  "id": "kamino-main-usdc",
  "points": [
    { "ts": "2026-09-08T00:00:00.000Z", "apy": 3.6213 },
    { "ts": "2026-09-09T00:00:00.000Z", "apy": 3.8013 },
    …
    { "ts": "2026-10-07T00:00:00.000Z", "apy": 4.4005 }
  ]
}
RequestResponse
/rates/history400 { "error": "missing ?id=<strategy id>" }
/rates/history?id=nope404 { "error": "unknown strategy nope" }

GET /decisions#

The latest 200 rebalancer decisions, newest first, two per cycle (one per tier). The response is about 200 KB. Field by field, it is described under The decision log.

GET/decisions
200 OK (first entry)
[
  {
    "id": "af686e85-7b13-43bc-8b21-8130a9cde751",
    "ts": "2026-10-08T06:45:52.796Z",
    "tier": "bedrock",
    "kind": "hold",
    "summary": "Held allocation at 3.86% net APY: Jupiter Lend → Aave v3 14,896 USDC: -0.09% spread gains -6.85 USDC over 30d vs 9.48 cost (needs 2x, after a 0.5% margin)",
    "expectedApyBefore": 3.8551,
    "expectedApyAfter": 3.8551,
    "moves": [],
    "sentinel": { "model": "rules-v1", "verdict": "ok", "score": 0, "note": "All 5 venues nominal" },
    "signatures": [],
    "rationale": {
      "legs": [],
      "held": ["Jupiter Lend → Aave v3 14,896 USDC: -0.09% spread gains -6.85 USDC over 30d vs 9.48 cost (needs 2x, after a 0.5% margin)"],
      "frozen": ["Kamino Lend", "Kamino Earn"],
      "turnover24hUsd": 0,
      "turnoverCap24hUsd": 50013.21,
      "smoothedApy": { "kamino-main-usdc": 4.4499, "jup-lend-usdc": 4.1688, "kamino-earn-steakhouse": 4.242, "aave-base-usdc": 4.0799, "morpho-steakhouse-eth": 4.3858 },
      "spotApy": { "kamino-main-usdc": 4.3429, "jup-lend-usdc": 4.1, "kamino-earn-steakhouse": 4.1167, "aave-base-usdc": 4.2761, "morpho-steakhouse-eth": 4.3589 }
    }
  }
]

Decisions logged before the rebalancer recorded its reasoning have no rationale. moves uses venue names; smoothedApy and spotApy are keyed by strategy id.

GET /targets#

The optimizer's latest target weights per tier, as fractions of the vault. These are where the vault would sit if moving were free; the actual allocation only moves toward them when a move pays. See Targets.

GET/targets
200 OK (Rapids entry)
[
  {
    "tier": "rapids",
    "generatedAt": "2026-10-08T06:45:57.839Z",
    "targets": [
      { "strategy": "rapids-jup-buffer", "weight": 0.3 },
      { "strategy": "kamino-rwa-onre", "weight": 0 },
      { "strategy": "perena-usd-star", "weight": 0.1 },
      { "strategy": "solstice-eusx", "weight": 0 },
      { "strategy": "exponent-pt", "weight": 0.1 },
      { "strategy": "maple-syrup-eth", "weight": 0.25 }
    ]
  }
]

Weights add up to at most 0.95; the rest is the idle buffer. A frozen venue is held at or below its current weight.

GET /fees#

Status of the hourly fee sweep. Each tier shows the shares minted to the treasury by the last sweep and what the treasury holds in total. A keeper running simulated vaults reports enabled: false.

GET/fees
200 OK (Rapids omitted)
{
  "enabled": true,
  "intervalMinutes": 60,
  "lastRun": "2026-10-08T05:56:20.783Z",
  "nextRun": "2026-10-08T06:56:23.655Z",
  "treasury": "Hs4RLeoYBs8YtUirjjE8BYaqG596koUWyYMxuJ2GgfEd",
  "tiers": {
    "bedrock": {
      "tier": "bedrock",
      "signature": "3gdJTRespXuRd9QfxMmgB2BYJEbtiQUe2jgy4VNc846UibwD5bLuZg1xSLsVunwLRrFdmvivEH4cZGXLPPUCSZej",
      "treasury": "Hs4RLeoYBs8YtUirjjE8BYaqG596koUWyYMxuJ2GgfEd",
      "shares": 0.057325,
      "usdc": 0.057339,
      "treasuryShares": 3.654099,
      "treasuryUsdc": 3.654997,
      "ts": "2026-10-08T05:56:20.783Z"
    }
  }
}

When a sweep fails after its retries, the tier holds { "error": "...", "ts": "..." } instead.

Changes in the next keeper release#

Coming with the next keeper release

Every endpoint above keeps its current shape. The next keeper adds:

  • GET /ready, which returns 503 when the keeper is degraded (for uptime monitors), and GET /status with per-tier cycle health, rate freshness, the keeper's SOL balance, RPC health and recent alerts;
  • GET /decisions?tier=&kind=&limit=&before= for filtering and paging (with an X-Next-Cursor header) and GET /decisions/{id} for a single decision;
  • ?days= (1 to 90) on /rates/history;
  • GET /metrics in Prometheus format (optionally behind a bearer token) and GET /openapi.json with an OpenAPI 3.1 spec;
  • errors with a machine-readable code, ETags, cache headers and standard security headers.
Written against apps/keeper/src/server.ts, apps/keeper/src/types.ts and apps/keeper/src/fees.ts. If the code and this page disagree, the code wins.