Overview
Creates a strategy in pending_setup and returns it wrapped in a
strategy object (201). Body is a SignedRequest, action
strategy.create; the signed payload is the body minus siwe. The
recovered wallet owns the strategy; any wallet field in the body is
ignored.
Optional idempotencyKey (string, 1-128 chars, else 400) rides inside
the signed payload and is scoped per wallet. A repeated key returns the
original stored response verbatim, so do not reuse a key across different
requests.
Endpoint
POST https://earn-api.quicknode.dev/functions/v1/api/v1/strategies
Authorization
Include the published apikey header and a valid Sign-In with Ethereum (SIWE) proof in the request body. The SIWE message binds the wallet to this action, payload, and request path.
Request body
All fields except siwe are part of the signed payload. Name regex ^[a-zA-Z0-9 _\-.]{1,50}$. chain_ids is an ARRAY of chain ids on input (stored and returned as a CSV string); a missing/invalid chain silently falls back to the first supported chain.
| Field | Type | Required | Description |
|---|---|---|---|
| siwe | object | Yes | A Sign-In-With-Ethereum proof binding the wallet to (action, payload, issuedAt, expirationTime). |
| siwe.message | string | Yes | SIWE message for action `strategy.create`, including the payload hash and `/strategies` path resource. |
| siwe.signature | string | Yes | Wallet signature over `siwe.message`. |
| name | string | Yes | Strategy name. Maximum 50 characters; letters, numbers, spaces, underscores, hyphens, and periods are supported. |
| capital_usdc | number | Yes | Capital in decimal dollars. |
| delta_pct | number | No | Same-network APY-gap threshold (percentage points). Optional: when omitted it defaults to the same-network floor. Minimum 3, or 5 when the strategy's networks include Ethereum (chain id 1) — a present value below the floor is rejected with a 400. |
| cross_chain_delta_pct | number | No | APY-gap threshold (percentage points) required when the source and destination vaults are on different chains. When omitted, defaults to 0 (inherit delta_pct), so integrations that predate this field keep single-threshold behaviour: one bar gates every move, and later delta_pct updates move both. A split bar is always an explicit opt-in. For a multi-network strategy the EFFECTIVE cross bar (this value when > 0, else the inherited delta_pct) must be >= 5, or >= 7 when the networks include Ethereum — requests below the floor are rejected with a 400. |
| delta_confirmations | integer | No | Number of consecutive qualifying APY observations required before rebalancing. |
| max_positions | integer | Yes | Maximum number of vault positions. Must be at least 1. |
| min_tvl_usd | number | No | Minimum vault TVL in decimal US dollars. |
| min_liquidity_usd | number | No | Minimum vault liquidity in decimal US dollars. |
| min_liquidity_entry_multiplier | number | No | Multiplier on min_liquidity_usd for the non-held entry threshold. Defaults to 2 when omitted. |
| chain_id | integer | No | Optional primary chain id; defaults to the first supported chain. |
| chain_ids | array<integer> | No | Chain ids to span. Omit for a single-chain strategy on `chain_id`. |
| apy_smoothing_minutes | integer | No | Defaults to 30 when omitted. |
| covered | boolean | No | Opt the strategy into OpenCover coverage. Set once at create and immutable thereafter. When true the strategy is forced Base-only (`chain_ids` → "8453") and the wallet MUST be on the covered-access allowlist AND have acknowledged OpenCover's terms, else the create is a 403 (`covered_not_allowed` / `covered_terms_required`). Defaults to false (uncovered). |
| hidden_vault_keys | array<string> | No | Vault keys to exclude, formatted as `chainId:0xaddress`. |
| idempotencyKey | string | No | Optional dedupe key (part of the signed payload). A repeat returns the ORIGINAL stored response verbatim, so never reuse a key across logically different requests. Malformed values are 400 `invalid_request`. |
Example request
curl --request POST \
--url 'https://earn-api.quicknode.dev/functions/v1/api/v1/strategies' \
--header 'Accept: application/json' \
--header 'apikey: <EARN_PUBLIC_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"name": "My USDC auto-pilot",
"capital_usdc": 1000,
"delta_pct": 3,
"cross_chain_delta_pct": 5,
"delta_confirmations": 2,
"max_positions": 2,
"min_tvl_usd": 1000000,
"min_liquidity_usd": 50000,
"chain_ids": [
8453,
42161
],
"apy_smoothing_minutes": 30,
"hidden_vault_keys": [],
"idempotencyKey": "b3d1c2e4-0000-4000-8000-000000000001",
"siwe": {
"message": "earn.quicknode.com wants you to sign in with your Ethereum account:\n0x1234...\n\nAuthorize strategy.create\n\nURI: https://earn.quicknode.com\nVersion: 1\nChain ID: 8453\nNonce: 5c6d...\nIssued At: 2026-07-07T18:00:00.000Z\nExpiration Time: 2026-07-07T18:05:00.000Z\nResources:\n- sha256:<hex>\n- path:/strategies",
"signature": "0xabcd..."
}
}'
Response
| Field | Type | Required | Description |
|---|---|---|---|
| strategy | object | Yes | The strategy row returned (inside `strategy`) by create and update: config fields plus cycle/gauge fields (`cycle_state`, `swap_signals`, `last_skip`, ...). No computed rollups. |
| strategy.id | string | Yes | Created strategy UUID. |
| strategy.wallet_address | string | Yes | Recovered SIWE wallet address. |
| strategy.name | string | Yes | Strategy name. |
| strategy.capital_usdc | number | Yes | Configured capital in decimal US dollars. |
| strategy.delta_pct | number | Yes | Same-chain APY threshold in percentage points. |
| strategy.cross_chain_delta_pct | number | No | Cross-chain APY threshold; `0` inherits `delta_pct`. |
| strategy.delta_confirmations | integer | Yes | Required consecutive threshold confirmations. |
| strategy.max_positions | integer | Yes | Maximum vault positions. |
| strategy.min_tvl_usd | number | Yes | Minimum vault TVL in decimal US dollars. |
| strategy.min_liquidity_usd | number | Yes | Minimum vault liquidity in decimal US dollars. |
| strategy.min_liquidity_entry_multiplier | number | Yes | Liquidity multiplier for new entries. |
| strategy.chain_id | integer | Yes | Legacy primary chain ID. |
| strategy.chain_ids | string | null | Yes | Stored comma-separated chain IDs. |
| strategy.apy_smoothing_minutes | integer | Yes | APY smoothing window in minutes. |
| strategy.hidden_vault_keys | array<string> | Yes | Strategy-level hidden vault keys. |
| strategy.status | string | Yes | Initial status, `pending_setup`. |
| strategy.active | boolean | Yes | Whether the strategy is active. |
| strategy.created_at | string | Yes | Creation timestamp. |
| strategy.updated_at | string | Yes | Last-update timestamp. |
| strategy.first_deposit_at | string | null | Yes | First-deposit timestamp. |
| strategy.deactivated_at | string | null | Yes | Deactivation timestamp. |
| strategy.final_value_usdc | number | null | Yes | Final value for a closed strategy. |
| strategy.final_realized_apy | number | null | Yes | Final realized APY percentage. |
| strategy.last_cycle_at | string | null | Yes | Most recent strategy-cycle timestamp. |
| strategy.cycle_state | string | null | Yes | Current strategy cycle state. |
| strategy.last_error | string | null | Yes | Most recent cycle error. |
| strategy.swap_signals | array<object> | null | Yes | Opaque internal swap signals. |
| strategy.unmatched_exits | array<object> | null | Yes | Exit records not matched to positions. |
| strategy.last_skip | object | null | Yes | Most recent skipped-cycle details. |
Example response
The API returns the stored strategy row. The exact UUID and timestamps are generated at creation.
{
"strategy": {
"id": "b7c1e2d3-0000-4000-8000-000000000001",
"wallet_address": "0x1234567890abcdef1234567890abcdef12345678",
"name": "My USDC auto-pilot",
"capital_usdc": 1000,
"delta_pct": 3,
"cross_chain_delta_pct": 5,
"delta_confirmations": 2,
"max_positions": 2,
"chain_ids": "8453,42161",
"apy_smoothing_minutes": 30,
"hidden_vault_keys": [],
"status": "pending_setup",
"cycle_state": null
}
}
HTTP responses
| Status | Description |
|---|---|
| 201 | The created strategy, wrapped in a `strategy` object. |
| 400 | Malformed request or SIWE block. `code` is one of: `invalid_request`, `siwe_missing` (no `siwe` block), `siwe_shape` (message/signature not strings), `siwe_parse` (message does not match the documented line format). |
| 401 | SIWE verification failed. `code` is one of: `siwe_domain` (message domain not allowlisted), `siwe_action` (statement/action mismatch), `siwe_stale` (expired or validity window out of bounds), `siwe_payload` (payload hash or resource mismatch — canonicalization drift, a re-serialized body, or a `- path:` entry that does not match the request path), `siwe_chain` (bad chain id), `siwe_recover` (signature does not match the claimed address), `siwe_replay` (nonce already used — sign a fresh request). |
| 403 | Covered-access gate (only when the signed payload has `covered: true`). `covered_not_allowed`: the recovered wallet is not on the covered-access allowlist (`approved_wallets`). `covered_terms_required`: the wallet has not acknowledged OpenCover's terms (`POST /v1/wallets/{addr}/opencover-terms`). Both are checked BEFORE any insert; uncovered creates never hit this gate. A failed lookup during the check is a 503 `covered_access_lookup_failed` (fails closed — the boundary never falls through to an insert). |
| 409 | `hide_floor_violated`: the supplied `hidden_vault_keys` would leave fewer than `max_positions + 1` eligible vaults. Hiding vaults that were never eligible passes. A failed read of the wallet's global hide list is a 503 `hide_lookup_failed`. |
| 429 | Rate limit exceeded. `Retry-After` (seconds) tells you when to retry. Reads, feedback, and push writes are keyed per IP; create/update/delete are keyed per wallet+IP; deposit/withdraw/claim calldata is keyed per strategy+IP. |
| 500 | Internal server error. |
| 503 | Signature verification could not reach the chain to validate a contract (ERC-1271) wallet. Transient — retry. Only occurs for smart-wallet signers. |
This page is based on operation createStrategy in the Earn OpenAPI 3.1 specification.