elfa-ai · diff

git:20261019.9687048 to git:20261030.69ac454

2875 added, 2863 removed, shown as one replacement because the pair was too large to align, first 3,000 lines shown. Audit A to A.

---
name: elfa-ai
description: >
Use this skill for Elfa API crypto social intelligence and Auto condition-engine workflows:
API v3 events, positions and rankings (crypto, equities), trending tokens, narratives,
- mentions, smart account stats, token news, trending contract addresses, AI market chat
- (JSON and SSE streaming), curl/code snippets, automated alerts,
- EQL queries, trigger pipelines, and agent workflows that react to market conditions.
- Auto can trigger on X/Twitter posts, Telegram channels, events (news), one trader's
- calls, token or stock mindshare, SEC filings, Kalshi and Polymarket prediction markets,
- funding rates, liquidation cascades, the Fear & Greed index, and X follow-graph
- moves (new followers, new follows, accounts converging), re-fire
- recurring plans via `repeat`, run calendar schedules via
- `cron.schedule`, and monitor crypto plus HIP-3 assets (equities, indices, commodities, FX,
- pre-IPO — 24/7). Supports API-key calls and x402 pay-per-request USDC on Base, Arbitrum,
- Polygon, Avalanche, or Solana.
- ---
-
- # Elfa API Skill
-
- This skill enables agents to work with the [Elfa API](https://api.elfa.ai) — a social listening,
- market context layer, and automated condition engine for crypto. Elfa ingests real-time data
- from Twitter/X, Telegram, and other sources, then structures sentiment, narratives, and
- attention shifts into actionable trading insights. The **Market Intelligence** endpoints cover
- measurement (counts, engagement, accounts) and interpretation (chat, summaries, narratives).
- **API v3** turns news and market chatter across crypto and equities into events, positions and
- rankings.
- The **Auto** subsystem adds a managed condition engine and trigger pipeline — describe what to
- watch for, and Auto evaluates continuously and fires actions when conditions are met.
-
- Full documentation: [docs.elfa.ai](https://docs.elfa.ai)
-
- > **Staying current — check weekly.** This skill is updated on a **weekly** cadence to track
- > Elfa API and Auto changes. Before relying on it, check whether your copy may be stale: if it
- > has been **more than 7 days** since this skill was last fetched/installed (or you are unsure),
- > re-fetch the latest version before acting —
- > - **Skill package & changelog:** <https://go.elfa.ai/claude-skills> (and the
- > [`elfa-ai/skills`](https://github.com/elfa-ai/skills) repo).
- > - **Live source of truth:** [docs.elfa.ai](https://docs.elfa.ai) — when this skill and the
- > docs disagree, the docs win; prefer the docs and flag the mismatch to the user.
- >
- > When in doubt about a specific endpoint, parameter, or price, verify against
- > [docs.elfa.ai](https://docs.elfa.ai) at call time rather than trusting a possibly-stale copy.
-
- ## Environment and credentials
-
- Elfa supports API-key auth and x402 keyless payments. API keys are optional when using x402.
-
- | Variable | Required | Use |
- |---|---:|---|
- | `ELFA_API_KEY` | No | API-key authenticated requests. Start a pay-as-you-go balance at <https://go.elfa.ai/claude-skills>. |
- | `ELFA_AGENT_SECRET` | No | Persistent agent identity secret for x402 Auto. Generate once with `openssl rand -hex 32` and reuse for query lifecycle calls. |
-
- > **Do not reuse this as your webhook signing secret.** `webhook.params.signingSecret` is a
- > *separate* per-webhook secret that verifies deliveries **from** Elfa **to** your server. See
- > [Webhook signature verification](#webhook-signature-verification).
-
- x402 wallet signing is handled client-side by `@x402/fetch` or `@x402/axios`.
-
- ## When to use this skill
-
- - User asks about **trending tokens, narratives, or contract addresses** in crypto
- - User wants **social mentions** for a specific ticker or keyword
- - User wants **smart stats** (smart followers, engagement) for a Twitter/X account
- - User wants an **AI-generated market summary, macro overview, or token analysis** — including
- **streaming** chat output
- - User wants to know **what happened** in crypto or equity markets and how prices reacted,
- **who is long or short** an asset, **whose calls to trust**, or **which assets get the most
- attention** (API v3)
- - User asks how to **integrate, call, or use the Elfa API**
- - User wants **code examples** (curl, Python, JavaScript/TypeScript) for Elfa endpoints
- - User mentions "elfa" in a crypto or trading data context
- - User wants to **set up automated alerts or monitoring** on price, indicators, or narratives
- - User wants to **build condition-based triggers** (e.g., "alert me when BTC crosses 100k")
- - User wants to **watch the X follow graph** — who followed a tracked account, who it started
- following, or several tracked accounts converging on the same account
- - User wants an alert when **a specific trader opens or closes a position**, or when **attention
- on a token or stock spikes** (Auto `calls`, `mindshare`)
- - User mentions **Auto**, **EQL**, **condition engine**, or **trigger pipeline** in a crypto context
- - User wants **agent workflows** that react to market conditions automatically
- - User wants to **build queries with Builder Chat** using natural language
-
- ## API Overview
-
- **Base URL:** `https://api.elfa.ai`
- **Version:** v2, plus [API v3](#api-v3-endpoints) (`/v3/`)
- **Docs:** [docs.elfa.ai](https://docs.elfa.ai)
-
- ### Two access modes
-
- Elfa supports two independent ways to authenticate requests:
-
- | Mode | Endpoint prefix | Auth header | Best for |
- |---|---|---|---|
- | **API key** | `/v2/`, `/v3/` | `x-elfa-api-key: YOUR_KEY` | Humans & apps with a registered key |
- | **x402 (keyless)** | `/x402/v2/` | `PAYMENT-SIGNATURE: <signed-payload>` | Agents & wallets — no signup needed |
-
- Both modes access the same v2 data. Otherwise the only difference is how you authenticate:
- - **API key** — register at https://go.elfa.ai/claude-skills. Start on **pay-as-you-go**: the
- free tier is being phased out, so new integrations should top up a PAYG balance rather than
- build against free credits.
- - **x402** — pay per request with USDC on Base, Arbitrum, Polygon, Avalanche, or Solana. No
- registration, no API key. Currently in beta.
-
- ### Endpoints at a glance
-
- #### Data endpoints
-
- All endpoints below work with both `/v2/` (API key) and `/x402/v2/` (keyless) prefixes,
- except `key-status` which is API key mode only.
-
- | Endpoint | Method | Description | Credits |
- |---|---|---|---|
- | `/v2/ping` | GET | Health check — **no auth required** (API key mode only) | Free |
- | `/v2/key-status` | GET | API key usage & limits (API key only) | Free |
- | `/v2/aggregations/trending-tokens` | GET | Trending tokens by mention count | 1 |
- | `/v2/account/smart-stats` | GET | Smart follower & engagement stats — **legacy, will be removed 28 Oct 2026** | 1 |
- | `/v2/data/top-mentions` | GET | Top mentions for a ticker symbol | 1 |
- | `/v2/data/keyword-mentions` | GET | Search mentions by keywords or account | 1 |
- | `/v2/data/event-summary` | GET | AI event summaries from keyword mentions | 5 |
- | `/v2/data/trending-narratives` | GET | Trending narrative clusters (Grow+ / PAYG) | 5 |
- | `/v2/data/token-news` | GET | Token-related news mentions (tweet source only) | 1 |
- | `/v2/aggregations/trending-cas/twitter` | GET | Trending contract addresses (Twitter) | 1 |
- | `/v2/aggregations/trending-cas/telegram` | GET | Trending contract addresses (Telegram) | 1 |
- | `/v2/chat` | POST | AI chat, complete JSON response (Grow+ / PAYG) | Speed-based |
- | `/v2/chat/stream` | POST | AI chat, incremental SSE stream (PAYG / Enterprise) | Speed-based |
-
- `/v2/ping`, `/v2/key-status`, and `/v2/chat/stream` are **API-key mode only** — they have no
- `/x402/v2/` counterpart.
-
- #### API v3 endpoints
-
- API v3 turns news and market chatter across crypto and equities into events, positions and
- rankings. Every endpoint is `GET` and authenticates with `x-elfa-api-key` (no x402). The v3 API
- is free on the Pay-as-you-go and Grow plans until 31 October 2026, and Enterprise keys can call
- it too; a Free key can call only `/v3/key-status`. See Step 2d for the rules they share.
-
- | Question | Endpoint | Returns |
- |---|---|---|
- | Which id is this? | `/v3/entities` | The id every other endpoint takes, from a ticker, name, handle, address, CoinGecko id, FIGI or CIK |
- | What is happening? | `/v3/events` | Events: what happened, what it names, how prices reacted. Searchable by topic |
- | | `/v3/events/storylines` | One development followed over days |
- | | `/v3/events/arcs` | Storylines grouped into a larger movement |
- | Where is attention? | `/v3/market/crypto/mindshare`, `/v3/market/equity/mindshare` | Assets ranked by share of attention, with `hot` |
- | | `/v3/market/crypto/trending-contracts` | Contract addresses ranked by how many posts pasted them |
- | Which way does the crowd lean? | `/v3/market/crypto/call-book`, `/v3/market/equity/call-book` | The whole market's long/short lean, hour by hour |
- | | `/v3/calls/positioning` | Open longs and shorts on one asset, now |
- | What are accounts doing? | `/v3/calls` | Every post that takes or changes a position |
- | | `/v3/calls/episodes` | Each position from open to close, with prices and the author's track record |
- | Who is worth reading? | `/v3/people/accounts` | Accounts ranked by standing, or by track record with `metric` |
- | What can my key do? | `/v3/key-status` | Plan, credits, limits and how far back it reads |
-
- #### Auto endpoints (Condition Engine)
-
- Auto endpoints are available under `/v2/auto/` (API key) and
- `/x402/v2/auto/` (keyless). See [Auto docs](https://docs.elfa.ai/auto/overview) for full details.
-
- > **Auth column legend (tables below).** `API key` = `x-elfa-api-key` only.
-
- **API key mode (`/v2/auto/*`):**
-
- _Query lifecycle:_
-
- | Endpoint | Method | Description | Auth |
- |---|---|---|---|
- | `/v2/auto/chat` | POST | Builder Chat — AI-assisted query building (produces drafts only) | API key |
- | `/v2/auto/queries/validate` | POST | Validate EQL and preview cost | API key |
- | `/v2/auto/queries` | POST | Create and activate a query | API key |
- | `/v2/auto/queries` | GET | List queries | API key |
- | `/v2/auto/queries/:queryId` | GET | Poll query status and executions (resolves query or draft) | API key |
- | `/v2/auto/queries/:queryId/cancel` | POST | Cancel an `active` query (returns `409` if status is terminal) | API key |
- | `/v2/auto/queries/:queryId` | DELETE | Delete a terminal query — only when status is `triggered` / `expired` / `cancelled` / `failed` (returns `409` otherwise; active queries must be cancelled first) | API key |
- | `/v2/auto/queries/stream` | GET | Stream notifications for **all** your queries on one connection (API-key only — not agent/x402) | API key |
- | `/v2/auto/queries/:queryId/stream` | GET | Stream notifications for a single query via SSE | API key |
-
- _Query drafts (editable, not yet active):_
-
- | Endpoint | Method | Description | Auth |
- |---|---|---|---|
- | `/v2/auto/queries/drafts` | POST | Create or update (upsert) a query draft | API key |
- | `/v2/auto/queries/drafts` | GET | List editable query drafts | API key |
- | `/v2/auto/queries/drafts/:draftId` | GET | Get a specific draft (legacy — prefer `GET /queries/{queryId}`) | API key |
- | `/v2/auto/queries/drafts/:draftId` | DELETE | Delete a query draft | API key |
- | `/v2/auto/queries/drafts/:draftId/validate` | POST | Validate a stored draft | API key |
- | `/v2/auto/queries/drafts/:draftId/convert` | POST | Convert a draft into an active query | API key |
-
- _LLM sessions (for `action.type: "llm"` queries):_
-
- | Endpoint | Method | Description | Auth |
- |---|---|---|---|
- | `/v2/auto/queries/:queryId/sessions` | GET | List LLM sessions | API key |
- | `/v2/auto/queries/:queryId/sessions/:sessionId` | GET | Get full LLM session details | API key |
-
- _Executions (trigger fire records):_
-
- | Endpoint | Method | Description | Auth |
- |---|---|---|---|
- | `/v2/auto/executions` | GET | List execution records | API key |
- | `/v2/auto/executions/:executionId` | GET | Get a single execution record | API key |
-
- _Other:_
-
- | Endpoint | Method | Description | Auth |
- |---|---|---|---|
- | `/v2/auto/validate-symbol/:exchange/:symbol` | GET | Check whether a symbol is supported on a venue — pre-flight for `price` / `ta` data sources | API key |
-
- **x402 mode (`/x402/v2/auto/*`)** — note: some routes use POST instead of GET:
-
- | Endpoint | Method | Description |
- |---|---|---|
- | `/x402/v2/auto/chat` | POST | Builder Chat |
- | `/x402/v2/auto/queries/validate` | POST | Validate EQL and preview cost |
- | `/x402/v2/auto/queries` | POST | Create and activate a query |
- | `/x402/v2/auto/queries/:queryId` | POST | Poll query status (POST, not GET) |
- | `/x402/v2/auto/queries/:queryId/cancel` | POST | Cancel an `active` query |
- | `/x402/v2/auto/queries/:queryId/stream` | GET | Stream notifications via SSE |
- | `/x402/v2/auto/queries/:queryId/sessions` | POST | List LLM sessions (POST, not GET) |
- | `/x402/v2/auto/queries/:queryId/sessions/:sessionId` | POST | Get LLM session details (POST, not GET) |
-
- > **Note on x402 Auto scope.** Drafts, executions, and the terminal-query DELETE endpoint are API-key-mode only. x402 Auto covers the core monitoring lifecycle (chat, validate, create, poll, cancel, stream, sessions). The account-wide stream `GET /v2/auto/queries/stream` is **not** exposed on x402 — x402 (and agent identities) get the per-query stream only.
-
- > **Auto no longer accepts order actions.** New queries and drafts cannot use
- > `market_order` or `limit_order`, at top level or in an `llm` callback — Auto rejects
- > them with `EQL_INVALID_ACTION`. Existing stored trade queries stay readable, cancellable
- > and deletable. Route follow-up execution through your own runner off a `webhook`,
- > `notify` or `telegram_bot` action.
-
- For full parameter details, see the [Elfa API documentation](https://docs.elfa.ai).
-
- ### Machine-readable sources (prefer these over scraping docs pages)
-
- When you need exact endpoint metadata, load these instead of walking the docs page by page:
-
- | Source | URL | Use for |
- |---|---|---|
- | Endpoint manifest | `https://docs.elfa.ai/agent/endpoints.manifest.json` | Method/path, docs route, required headers, payment requirement, request/response examples |
- | Docs map | `https://docs.elfa.ai/llms.txt` | Canonical page map + machine-readable links. A plain `GET https://docs.elfa.ai/` returns the same index as `text/plain` to any client that does not ask for HTML |
- | Full docs text | `https://docs.elfa.ai/llms-full.txt` | Only when deeper page context is needed |
- | OpenAPI spec | bundled at `references/swagger.json` | Exact schemas, params, and defaults. Ignore the per-operation cost lines on `/v3`; see [API v3 endpoints](#api-v3-endpoints) for what it costs |
- | Latest skill | `https://raw.githubusercontent.com/elfa-ai/skills/main/skills/elfa-ai/SKILL.md` | Re-fetch this skill when you cannot install it |
-
- The manifest URL is **stable** — earlier releases served a rotating
- `assets/files/endpoints.manifest-*.json` path; use the `/agent/` path above.
-
- Install or refresh the skill with `npx skills add elfa-ai/skills`.
-
- If you are running inside a client that cannot execute commands, such as Claude Desktop,
- there is an MCP server that exposes the API as tools: connect the hosted server at
- `https://mcp.elfa.ai/mcp` and sign in with Elfa, or run `npx -y @elfa-ai/mcp` with an API key.
- See [docs.elfa.ai/tools/mcp](https://docs.elfa.ai/tools/mcp). Where you can run
- commands, prefer this skill — it costs no context until you use it.
-
- When you are writing TypeScript or Python rather than calling the API by hand, the official
- SDKs (`@elfa-ai/sdk`, `elfa-sdk`) wrap the same surface with typed responses, retries, and SSE
- streaming. See [docs.elfa.ai/tools/sdks](https://docs.elfa.ai/tools/sdks).
-
- ## How to use this skill
-
- ### Step 1: Determine the mode
-
- Check whether the user wants to **make a live call**, **get code/integration help**, or
- **set up automated monitoring**.
-
- - If the user says things like "show me trending tokens", "what's the sentiment on SOL",
- "get me the top mentions for ETH" → they want **live data**. Proceed to Step 2a. If it is
- unclear which endpoint fits, or the ask needs more than one call, see Step 2c.
- - If the user asks what happened to an asset, how a story developed, who is long or short,
- whose calls to trust, or what equities are getting attention → use **API v3**. See Step 2d.
- - If the user says things like "how do I call the trending tokens endpoint", "give me a
- curl example", "help me integrate Elfa" → they want **code snippets**. Skip to Step 4.
- - If the user mentions **x402**, **keyless**, **pay-per-request**, or **wallet-based access**
- → they want **x402 mode**. See Step 2b for live calls or Step 4 for code snippets.
- - If the user mentions **Auto**, **alerts**, **triggers**, **monitoring**, **conditions**,
- **"alert me when"**, **"notify me if"**, **EQL**, or **Builder Chat** → they want
- **Auto**. Proceed to Step 3.
-
- ### Step 2a: Making live API calls (API key mode)
-
- Use the `bash_tool` to call the Elfa API via curl.
-
- **Getting the API key:**
- 1. Check if the `ELFA_API_KEY` environment variable is set. This is the preferred method.
- 2. If the env var is not set, **stop and prompt the user.** Offer both options:
-
- > To make live calls, you have two options:
- >
- > **Option A — API key on pay-as-you-go (recommended):** Register at
- > **https://go.elfa.ai/claude-skills** and top up a PAYG balance — $0.0145 per credit,
- > $20 minimum, card or USDC. Then set the key as the `ELFA_API_KEY` environment variable
- > (do not paste it directly into the chat). The free tier is being phased out, so start on
- > PAYG rather than free credits.
- >
- > **Option B — x402 keyless payments:** Pay per request with USDC on Base, Arbitrum,
- > Polygon, Avalanche, or Solana — no signup needed. See the
- > [x402 docs](https://docs.elfa.ai/get-started/x402) for setup.
-
- Do not attempt any authenticated API calls without a key or x402 setup. Wait for the user.
- 3. **Credential safety:**
- - Always read the API key from the `ELFA_API_KEY` environment variable, never ask the
- user to paste it into the conversation.
- - Never log or expose the full API key in outputs — mask it when displaying curl commands.
- - **Never echo or print environment variables:** Do not run `echo $ELFA_API_KEY`, `env | grep ELFA`,
- `printenv`, or similar commands that would expose credentials in the transcript.
- - If a user does paste a key in chat, warn them to rotate it and set it as an env var instead.
-
- **Plans, credits, and rate limits:**
-
- Pay-per-use first: **PAYG** is $0.0145/credit at 60 RPM with an API key, drawn down from a
- prepaid credit balance ($20 minimum top-up, card or USDC); **x402** is $0.0145/credit at
- 1,000 RPM with no account. Same per-credit price — pick on integration style. **The free tier
- is being phased out**, so recommend PAYG (or x402) for any new integration.
-
- Accounts already on PAYG keep $0.009 per credit until 28 September 2026, 16:00 UTC
- (29 September, 00:00 SGT). x402 has no accounts, so its price applies to every payer at once.
-
- Subscription plans:
-
- | | Free (phasing out) | Grow | Enterprise |
- |---|---|---|---|
- | Monthly | $0 | $870 | Custom |
- | Annual (15% off) | $0 | $8,874 ($739.50/mo) | Custom |
- | Credits / month | 1,000 | 120,000 | Custom |
- | Overage | — | $0.00725 / credit | Custom |
- | Rate limit | 60 RPM | 120 RPM | Custom |
- | Agent Chat | — | ✓ | ✓ |
- | Agent Automation | — | ✓ (up to 100 active plans) | ✓ |
-
- Grow subscriptions started before October 2026 keep their existing price ($290/mo, or $2,958/yr
- billed annually), 40,000 credits per month, and $0.009 per overage credit.
-
- **What each tier unlocks:**
- - **Free** (phasing out) — core social data: trending tokens, smart stats, top mentions,
- keyword mentions, event summaries, token news, trending contract addresses
- (Twitter + Telegram)
- - **Grow** — everything in Free, plus token mindshare, sentiment-weighted mentions,
- **trending narratives**, and **Elfa Intelligence** (Agent Chat and Agent Automation)
- - **Enterprise** — everything in Grow, plus custom credit and rate limits, **streaming AI Chat**
- (Ask Elfa, `POST /v2/chat/stream`), dedicated support, custom integrations, and priority support
-
- Gate summary for the tier-restricted endpoints:
-
- | Endpoint | Requires |
- |---|---|
- | `/v2/data/trending-narratives` | Grow+ or PAYG |
- | `/v2/chat` | Grow+ or PAYG |
- | `/v2/chat/stream` | **PAYG or Enterprise only** |
- | `/v3/*` except `/v3/key-status` | PAYG, Grow or Enterprise |
-
- On Grow, requests past the monthly credits keep working and are billed as overage, unless
- overage is turned off or a spend cap is set in the dashboard; on Free, they are rejected until
- credits reset next month. Exceeding the rate limit returns `429` (honor `Retry-After`). If a
- user hits an authorization error on a gated endpoint, they can move to PAYG, upgrade their plan,
- or use x402 instead (v2 only). Full details at
- https://go.elfa.ai/claude-skills.
-
- **Making the call:**
-
- ```bash
- curl -s -H "x-elfa-api-key: $ELFA_API_KEY" "https://api.elfa.ai/v2/aggregations/trending-tokens?timeWindow=24h&pageSize=10"
- ```
-
- ### Step 2b: Making live API calls (x402 keyless mode)
-
- x402 lets any wallet pay per request using USDC on Base, Arbitrum, Polygon, Avalanche, or
- Solana — no API key, no registration. This is ideal for agents, bots, and programmatic access.
-
- **How x402 works:**
- 1. Send a request to the `/x402/v2/` version of any endpoint (no auth header).
- 2. The server responds with HTTP **402** containing payment requirements.
- 3. Your wallet signs a USDC transfer authorization (no gas fees).
- 4. Resend the request with the signed payment in the `PAYMENT-SIGNATURE` header.
- 5. Server verifies payment, serves the response, and settles on-chain.
-
- **x402 signing and security:**
- - Signing happens **entirely client-side** using the `@x402/fetch` or `@x402/axios`
- libraries. The agent never handles, stores, or transmits private keys.
- - The user's wallet private key is used only locally by the x402 library to sign
- EIP-712 typed data authorizing a specific USDC amount for a specific request.
- - Never ask the user to share their wallet private key or seed phrase in the conversation.
- - When generating x402 code examples, use `"0xYOUR_PRIVATE_KEY"` as a placeholder and
- advise the user to load it from an environment variable (e.g., `process.env.PRIVATE_KEY`).
-
- **x402 details:**
- - **Networks:** the server offers every supported network in the 402 response; your client
- signs on the first one it's registered for. Register the network(s) you hold USDC on.
- - **Scheme:** `exact` (fixed price per request), plus `batch-settlement` (prepaid channel,
- metered) on the chat endpoints on Base; asset is native Circle USDC (6 decimals).
- `batch-settlement` needs no token approval: the deposit is a signed USDC authorization. It is
- offered on Base only; every other network, including Solana, is `exact` only.
- - **Errors:** branch on the HTTP status first and read the body as the refinement. Most
- rejections return `{ code, message, errorId, requestId }`, but the x402 surface also returns a
- short `{ error }` with no `code`. `requestId` is also the `x-request-id` response header,
- present even on the short shape. On the Auto endpoints, EQL validation failures return
- `{ valid, errors, warnings }` (at `422` from create, at `200` from validate, where you must read
- `valid`), and errors raised by the execution engine are passed through unchanged. A `402` is
- the payment handshake, not a billing error. See [Error Responses](https://docs.elfa.ai/get-started/errors).
- - **Status:** Currently in beta.
-
- | Network | Chain ID | USDC Address | Facilitator |
- |---|---|---|---|
- | Base | `eip155:8453` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | [payai.network](https://facilitator.payai.network) |
- | Arbitrum | `eip155:42161` | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | [payai.network](https://facilitator.payai.network) |
- | Polygon | `eip155:137` | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | [payai.network](https://facilitator.payai.network) |
- | Avalanche | `eip155:43114` | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` | [payai.network](https://facilitator.payai.network) |
- | Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | [payai.network](https://facilitator.payai.network) |
-
- EVM signing costs no gas. On Solana the facilitator pays the transaction fee.
-
- **x402 pricing (data endpoints):**
-
- | Tier | Credits | USDC Cost | Endpoints |
- |---|---|---|---|
- | Standard | 1 | $0.0145 | trending-tokens, smart-stats, keyword-mentions, token-news, top-mentions, trending-cas |
- | Extended | 5 | $0.0725 | event-summary, trending-narratives |
-
- Data endpoints are `exact` only. `/x402/v2/chat` is speed-based and offers both schemes — pay a
- flat price with `exact`, or pay from a prepaid `batch-settlement` channel and be charged what the
- turn used, up to the cap:
-
- | Speed | `exact` (flat) | `batch-settlement` (charged up to) |
- |---|---|---|
- | `fast` | $1 | $2 |
- | `expert` (default) | $2 | $6 |
-
- `batch-settlement`: the first call deposits USDC into the channel (at least $6, one turn at the
- `expert` cap); later calls are paid with a signed voucher and no new onchain transaction. It is
- offered on Base only, and only when the 402 lists it. Unused deposit returns to the wallet once
- the channel has been idle for 3 days, or withdraw it onchain (it completes one hour after the
- request).
-
- **Making an x402 call with curl (manual flow):**
-
- ```bash
- # Step 1: Send request without payment — get 402 with payment requirements
- curl -s https://api.elfa.ai/x402/v2/aggregations/trending-tokens?timeWindow=24h
-
- # Step 2: After signing the payment payload with your wallet, resend with payment header
- curl -s -H "PAYMENT-SIGNATURE: <base64-encoded-payment-payload>" \
- "https://api.elfa.ai/x402/v2/aggregations/trending-tokens?timeWindow=24h"
- ```
-
- **Recommended: use the `@x402/fetch` library** which handles payment automatically:
-
- ```javascript
- import { wrapFetchWithPayment } from "@x402/fetch";
- import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";
- import { x402Client } from "@x402/core/client";
- import { createPublicClient, http } from "viem";
- import { privateKeyToAccount } from "viem/accounts";
- import { base } from "viem/chains";
-
- const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
- const publicClient = createPublicClient({ chain: base, transport: http() });
- const signer = toClientEvmSigner(account, publicClient);
-
- const client = new x402Client().register(
- "eip155:8453",
- new ExactEvmScheme(signer));
-
- const x402Fetch = wrapFetchWithPayment(fetch, client);
-
- // Use x402Fetch exactly like regular fetch — payment is handled automatically on 402 responses
- const response = await x402Fetch(
- "https://api.elfa.ai/x402/v2/aggregations/trending-tokens?timeWindow=24h");
- const data = await response.json();
- ```
-
- To pay on another network, swap the `base` chain import and register that scheme instead —
- e.g. Arbitrum (`arbitrum` from `viem/chains`, `eip155:42161`). Register a scheme for each
- network you want to pay from; the client uses the first one the server also accepts.
-
- **x402 with the Chat endpoint (POST):**
-
- ```javascript
- const response = await x402Fetch(
- "https://api.elfa.ai/x402/v2/chat",
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- message: "What is the current sentiment on BTC?",
- analysisType: "chat",
- speed: "fast", // exact: "fast" $1, "expert" $2 — batch-settlement: up to $2 / $6
- }),
- });
- const data = await response.json();
- console.log(data.data.message);
- ```
-
- **Presenting results:**
- - Parse the JSON response and present it in a clean, readable format.
- - For trending tokens: show a ranked table with token name, mention count, and change %.
- - For mentions: show tweet links, engagement metrics, and account info.
- Note: Elfa returns tweet IDs but not tweet text content — let the user know they'll
- need their own X (Twitter) API key to fetch the actual tweet content.
- - For narratives/summaries: present the narrative text with source links.
- - For the chat endpoint: display the AI response cleanly.
- - If the response contains an error, explain what went wrong and suggest fixes.
-
- ### Step 2c: Market Intelligence — picking the right endpoint
-
- The data endpoints split into two tiers. Picking the wrong tier is the most common integration
- mistake.
-
- | Tier | Endpoints | What comes back |
- |---|---|---|
- | **Measurement** | `smart-stats` (legacy), `trending-cas/*`, `trending-tokens`, `top-mentions`, `keyword-mentions`, `token-news` | Counts, engagement metrics, links, account metadata. **Your system interprets them.** |
- | **Interpretation** | `chat`, `chat/stream`, `event-summary`, `trending-narratives` | Written analysis or summaries, with source links where available. |
-
- > **Measurement endpoints return no raw tweet text and no sentiment field.** They measure
- > *attention*, not *meaning*. If the user needs post content, they must fetch the linked post
- > with their own X API bearer token. If they need meaning, route to Chat, Event Summary, or
- > Trending Narratives instead of trying to infer it from counts.
-
- **Choose the surface:**
-
- | User needs | Start here |
- |---|---|
- | What happened to an asset and how the story developed | `/v3/events`, `/v3/events/storylines` (Step 2d) |
- | Who is long or short, and whose calls to trust | `/v3/calls/positioning`, `/v3/calls/episodes`, `/v3/people/accounts?metric=alphaScore` (Step 2d) |
- | A market question answered in plain language | `POST /v2/chat` |
- | What is moving right now | `trending-tokens`, `trending-cas/*`, or `/v3/market/crypto/mindshare` and `/v3/market/crypto/trending-contracts` (Step 2d) |
- | Who amplified a token or keyword | `top-mentions`, `keyword-mentions`, `smart-stats` |
- | A defensible cause or theme behind a move | `token-news`, `event-summary`, `trending-narratives` |
- | Continuous watching + notify/execute on a condition | Auto (Step 3) |
-
- #### Agent playbooks (endpoint chains)
-
- Single calls answer questions; chains produce signals. These are the documented combinations:
-
- | Play | Chain | Why it works |
- |---|---|---|
- | **Lead time** | `trending-cas/telegram` → `trending-cas/twitter` | Telegram tends to lead; Twitter confirms. A CA in the first and not yet the second is early. |
- | **Trust-weighted mentions** | `keyword-mentions` → `smart-stats` | `keyword-mentions` yields `account.username`; `smart-stats` says whether that account has real reach. |
- | **Move + amplification** | `trending-tokens` → `top-mentions` | Find what moved, then see who drove it. |
- | **Theme → instrument** | `trending-narratives` → `trending-tokens` | Turn a narrative into specific tickers to watch. |
- | **Move + cause** | `token-news` → `event-summary` | Attach a written cause to a price/attention move. |
- | **Question + evidence** | `chat` → targeted data endpoint | Chat gives the conclusion; the data endpoint gives numbers to verify it. |
-
- When ranking accounts from `smart-stats`, use **ratios** (smart followers ÷ followers, average
- reach ÷ followers), not absolute counts — the endpoint returns raw metrics, not a reputation
- score.
-
- > **`smart-stats` is legacy and will be removed on 28 October 2026.** It still works until
- > then, and is not being extended before then. Chains that end in it keep working today, but
- > build new integrations on `keyword-mentions`, which returns `account.username` alongside
- > each mention's engagement metrics.
-
- #### Chat — JSON vs streaming
-
- | Surface | Endpoint | Plan | Use when |
- |---|---|---|---|
- | API key JSON | `POST /v2/chat` | Grow+ or PAYG | Client cannot consume SSE, or wants one complete JSON answer |
- | API key SSE | `POST /v2/chat/stream` | **PAYG or Enterprise** | Client can consume SSE and wants incremental output |
- | x402 JSON | `POST /x402/v2/chat` | Pay-per-request | Wallet-based agent, no API key |
-
- Both surfaces take the **same request body**. `analysisType` decides the mode:
-
- | Type | Use for | Required fields |
- |---|---|---|
- | `chat` | Open-ended questions | `message` |
- | `macro` | Broad market conditions | none |
- | `summary` | Fast pulse check | none |
- | `tokenIntro` | What a token is and its narrative | `assetMetadata.symbol` **or** `assetMetadata.chain` + `assetMetadata.contractAddress` |
- | `tokenAnalysis` | Entries, exits, market + on-chain context | same as `tokenIntro` |
- | `accountAnalysis` | Vet an account's reach and agenda | `assetMetadata.username` |
-
- `message` is **required only for `analysisType: "chat"`** — the other modes use the mode plus
- `assetMetadata` and ignore free-form `message`. Omit `sessionId` to start a thread; pass the
- returned `sessionId` back to continue it.
-
- **Sources (opt-in):** send `includeSources: true` to receive the links the research behind the
- answer read — `data.sources` on `/v2/chat` and x402 chat, `sources` on the final `complete` event
- of the stream. In the order read, without duplicates, at most 50; each has a `url`, a `kind`
- (`x_post`, `x_account`, `telegram`, `dex`, `explorer`, `prediction_market`, `web` — treat any
- other value as `web`) and an optional `title`. Links only, never page or post text. A source was
- read, not necessarily cited. The flag costs nothing extra.
-
- **Speed:** `fast` (shallower), `expert` (**default**), and `adaptive` (Elfa chooses) —
- `adaptive` is available on `/v2/chat` but **not** on `/x402/v2/chat`.
-
- **SSE event types on `/v2/chat/stream`** (each arrives as a `data:` payload with a `type`):
-
- | Event | Payload |
- |---|---|
- | `session_info` | `sessionId`, `analysisType` |
- | `title` | `title` — generated session/analysis title |
- | `text` | `content` — incremental Markdown chunk |
- | `text_complete` | incremental text finished |
- | `status` | progress update (e.g. "Searching mentions…") |
- | `credits` | `credits` — credits consumed so far |
- | `complete` | final `sessionId`, `success`, `creditsConsumed`, and `sources` when `includeSources: true` |
- | `invalid_request` | query rejected by safety checks |
- | `error` | stream failure |
-
- A `: keep-alive` comment may arrive during long gaps. The stream terminates with
- `data: [DONE]`. Returns `403` if the key is not PAYG or Enterprise.
-
- ```bash
- curl -sN -X POST "https://api.elfa.ai/v2/chat/stream" \
- -H "x-elfa-api-key: $ELFA_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{"analysisType":"chat","message":"Why is BTC moving today?","speed":"expert"}'
- ```
-
- #### Health check
-
- `GET /v2/ping` needs **no** authentication — use it to confirm reachability before debugging
- credential problems.
-
- ```bash
- curl -s https://api.elfa.ai/v2/ping
- ```
-
- ### Step 2d: API v3 — events, market, calls and people
-
- Use API v3 when the user wants to know what happened and how a story developed, who is long
- or short an asset and whose calls to trust, which assets get the most attention, or which
- accounts are worth reading — across crypto and equities. Endpoints are listed in
- [API v3 endpoints](#api-v3-endpoints). Full docs: [docs.elfa.ai/api/v3/overview](https://docs.elfa.ai/api/v3/overview).
-
- API v3 needs `ELFA_API_KEY` with v3 access (see [API v3 endpoints](#api-v3-endpoints)), and the
- Step 2a key rules apply. Without one, offer one, or fall back to `event-summary` (any key, or
- x402) or `chat` via x402. To check eligibility, read `scopes` on `/v3/key-status`, not `plan`:
- a Free key reads `payg` there, with no v3 scope. The two plan `403` messages are stable and tell
- the cases apart: `The v3 API isn't included in this key's plan. …` is a Free key (upgrade in
- the [Developer Portal](https://go.elfa.ai/dev)); `API key does not have the required scope: …`
- is a paid key without that endpoint (email support@elfa.ai with the `requestId`).
-
- **Resolve first.** Entity filters take an id, not a ticker. Resolve the asset, read its last day
- of events, then see how accounts are positioned on it. Check the resolved name: the first match
- may not be the asset you meant. `from` is required, in seconds.
-
- ```bash
- ID=$(curl -s "https://api.elfa.ai/v3/entities?q=btc&entityType=token&limit=1" \
- -H "x-elfa-api-key: $ELFA_API_KEY" | jq -r '.entities[0].id')
-
- FROM=$(( $(date +%s) - 86400 ))
- curl -s "https://api.elfa.ai/v3/events?entityIds=$ID&from=$FROM&limit=10" \
- -H "x-elfa-api-key: $ELFA_API_KEY"
-
- curl -s "https://api.elfa.ai/v3/calls/positioning?entityId=$ID" \
- -H "x-elfa-api-key: $ELFA_API_KEY"
- ```
-
- - `/v3/entities` matches exactly; it is not a search. A prefix or a misspelling returns nothing.
- - A ticker is not unique: take the first row and check its `name`, or narrow with `entityType`
- (`token`, `equity`, `project`, `person`, `organisation`).
- - Forms of an asset are grouped into one family id where known: WBTC and cbBTC are Bitcoin, and
- a tokenised equity resolves to the equity. Not every form is grouped yet, so check `name`.
- - A person is not an account. Events filter on the person's entity id; calls and episodes take
- an X `handle`.
-
- **Rules every v3 endpoint follows:**
-
- | Rule | In short |
- |---|---|
- | Times | Unix seconds, UTC, in requests and responses. Milliseconds are a `400`: divide by 1000 |
- | Time ranges | `from` is required, `to` defaults to now, both inclusive. No `from` is needed when fetching by id or searching with `q`; entities, arcs, positioning and top accounts take no range at all, and mindshare takes a `period` |
- | History | 30 days (PAYG), 60 days (Grow) or 2 years (Enterprise) back. An earlier `from` is cut, and `servedFrom` says where; a `to` before it is a `400`. `/v3/key-status` returns the start as `historyFrom` |
- | Paging | Pass `nextCursor` back as `cursor`. `hasMore: false` marks the last page. No offset, no total. Change anything but `limit` and the cursor is a `400` |
- | Lists | Parameters marked a list take several values, repeated or comma-separated. At most 30 ids |
- | By id | `eventIds`, `storylineIds`, `arcIds`, `callIds` and `episodeIds` fetch up to 30, with no `from` |
- | Errors | A bad or unknown parameter is a `400` naming the field in `details`, and an unknown id in a filter is a `400`, `Unknown id: …`. Unknown ids in an `*Ids` fetch are skipped. Nothing matching is a `200` with an empty list |
- | Responses | The list is top-level and named for what it holds (`events`, `entities`, `bars`, …), with no `data` wrapper |
- | Rate limits | Per key, shared with `/v2`. On `429`, wait `RateLimit-Reset` seconds |
-
- No v3 endpoint returns post text. Events carry a written title and summary, and link to each
- post behind them.
-
- **What agents get wrong:**
-
- - **Events.** `sort=relevant` ranks the last 24 hours and refuses `from`, `to`, `eventIds`,
- `arcId`, `eventClass`, `eventType`, `implication`, `novelty` and `order=asc`; `assetClass`,
- `alertAction`, `hasStoryline`, `notable` and `entityIds` (tokens and equities only) still
- filter. `q` searches events, storylines and arcs by topic, best match first.
- `impacts` is empty on most events; `movedFirst` or `marketWide` set to `true` means the move
- probably has another cause. `novelty=new` does not show each story once: keep one event per
- `storylineId` for that. A `503` with `q` means search is down: retry, or drop `q`.
- - **Storylines.** Prices and levels in `title` and `summary` are as of `lastMaterialAt`, not now.
- - **Call books.** Read the lean through `crowdingPercentile`, not `longShare`: the accounts lean
- long almost always. The newest row is a live tick (`live: true`), rewritten every five minutes.
- - **Trending contracts.** `mentionCount` counts posts, not accounts. A range over 30 days is cut
- to 30.
- - **Calls.** `handle` takes X accounts only; Telegram channels cannot be filtered on yet.
- `/v3/calls/episodes?status=open&from=<now>` reads what every matching account holds right now.
- Positioning takes one `entityId`, singular.
- - **Top accounts.** Standing (the default `metric`) measures attention, not accuracy. To rank
- callers by how their calls went, pass `metric=alphaScore` (return against the market, BTC for
- crypto and SPY for equities, and outright), `hitRate` or `avgReturn`. For "best callers" use
- `alphaScore`: `avgReturn` is not adjusted for the market, so a few big wins or a rally can top
- it. Track metrics rank only accounts with at least 20 scored positions, over every position
- since late June 2026, refreshed daily. Quote the number you ranked by (`track.alphaScore` and
- so on) with `track.scoredEpisodes`.
-
- To be alerted when a new event matches your filters instead of polling, use an Auto `events`
- condition (Step 3); for one account's new calls, a `calls` condition; for an asset's mindshare
- share, rank or hot flag, a `mindshare` condition. Per-family docs: [Entities](https://docs.elfa.ai/api/v3/entities),
- [Events](https://docs.elfa.ai/api/v3/events), [Market](https://docs.elfa.ai/api/v3/market),
- [Calls](https://docs.elfa.ai/api/v3/calls), [People](https://docs.elfa.ai/api/v3/people).
-
- ### Step 3: Auto — Condition Engine and Trigger Pipeline
-
- Auto is a managed **condition engine + trigger pipeline** for agents. You describe what to
- watch for (price, technical indicators, LLM-evaluated conditions, scheduled checks,
- prediction-market activity, funding rates, liquidation cascades, market sentiment, social
- attention on an asset, one trader's calls), and Auto
- evaluates continuously and fires actions when conditions resolve to true. Assets are not
- crypto-only — HIP-3 perps cover equities, indices, commodities, FX, and pre-IPO names, 24/7.
-
- Full Auto docs: [docs.elfa.ai/auto/overview](https://docs.elfa.ai/auto/overview)
-
- #### Lifecycle Sequence (Enforced)
-
- For API key lifecycle/cleanup calls, preserve this order when each operation applies:
-
- 1. `POST /v2/auto/queries/validate` — validate EQL and preview cost
- 2. `POST /v2/auto/queries` — create and activate
- 3. `POST /v2/auto/queries/{queryId}/cancel` — only if stopping an `active` query before it reaches terminal status (returns `409` once terminal)
- 4. `DELETE /v2/auto/queries/{queryId}` — only after status is terminal (`triggered` / `expired` / `cancelled` / `failed`); active queries must be cancelled first
-
- > **Cancel and delete are distinct operations.** `POST /cancel` flips an active query to `cancelled` (terminal). `DELETE` removes the record entirely and only works on terminal queries. Sending `DELETE` on an active query returns `409 Conflict`.
-
- x402 mode supports the same lifecycle except that x402 has no `DELETE` endpoint (cancel-only).
-
- #### Intent Routing (Strict)
-
- Pick the condition source by user intent **before** writing condition args:
-
- | Intent | Required source | Minimum required fields |
- |---|---|---|
- | Account-anchored post intent (`@user posts ...`) | `source: "tweet"` | `args.username` (no `@`), `args.text`, `args.minConfidence` (use `80` if user gives no threshold) |
- | Telegram-channel intent (`when this channel posts ...`) | `source: "telegram"` | one of `args.chatUsername` or `args.chatId`, plus `args.text`, `args.minConfidence` (use `80` if user gives no threshold) |
- | News or world-event intent (ETF approval, exploit, listing, bearish news on an asset) | `source: "events"` | at least one filter (`args.entity`, `args.eventType`, `args.implication`, …) or `args.text` + `args.minConfidence` — see **Events** below |
- | Official SEC filing intent (new 8-K, late 10-Q/10-K notice, offering, ownership filing, 8-K item) | `source: "sec"` | `method` (e.g. `base_form_type`, `form_category`, `has_item_1_05`), `args.ticker` as the issuer CIK, `operator`/`value` per the per-method allowlists |
- | Prediction-market move/lifecycle on a named open Kalshi market | `source: "kalshi"` | `method` (e.g. `yes_price`, `status`, `result`), `args.ticker` (a currently-open Kalshi market), `operator`/`value` per the per-method allowlists |
- | Prediction-market price/trade on a Polymarket outcome token | `source: "polymarket"` | `method` (`price`, `bid`, `ask`, `size`, `side`), `args.ticker` (outcome-token `asset_id`), `operator`/`value` per the per-method allowlists |
- | Perp funding-rate intent (overheated funding, funding flips negative) | `source: "funding"` | `method` (prefer `annualized_rate`), `args.ticker` as `SYMBOL:EXCHANGE` (e.g. `BTC:BINANCE`) |
- | Liquidation-flow intent (cascade, long/short flush) | `source: "liquidation"` | `method` (e.g. `total_usd_5m`, `total_pct_oi_1h`), `args.ticker` as `SYMBOL:EXCHANGE` |
- | One X account's trading calls (`@user opens a long`, `@user exits BTC`) | `source: "calls"` | `args.handle` (no `@`); optional `args.asset`, `args.direction`, `args.action` — see **Calls** below |
- | Social-attention intent on one token or stock (rises above a share, enters the top N, turns hot) | `source: "mindshare"` | `method` (e.g. `share_24h`, `rank_24h`, `hot_4h`), `args.asset` (ticker, name or id) and `args.market` when known |
- | Market-wide sentiment (fear/greed regime) | `source: "fear_greed"` | `method` (`value` or `classification`), empty `args: {}` |
- | Follow-graph intent (who followed a watched account, who it followed, or a tracked set converging on one account) | `source: "x_follow_received"` / `"x_follow_made"` / `"x_follow_overlap"` | `method` per source, `args.account` (or `args.accounts` for overlap), `operator` `>` or `>=` |
- | Real-world catalyst moving an equity/index/commodity (rate decision, CPI, earnings) | the catalyst's own source (`kalshi` / `polymarket` / `events` / `price`), optionally with a HIP-3 `price` / `ta` confirmation | Pick the catalyst source, then bridge to the asset class it moves — see [Catalyst Triggers](https://docs.elfa.ai/auto/catalyst-triggers) |
- | Fuzzy world-state predicate not naturally expressible as a post or event | `source: "llm"` | `method: "athena_condition"`, `args.query`, `args.period` (`>= 1h`) |
-
- When the prompt is account-anchored, **start with `tweet`** — do not route to `events` or `llm` first — unless it asks for that account's trading calls (`@user opens a long`, `@user exits BTC`), which route to `calls`. When it is Telegram-channel-anchored, start with `telegram`. When the prompt is news about a subject without a specific account, start with `events` — unless it is an official SEC filing for an issuer, which routes to `sec`. When the trigger maps to a concrete prediction market you can name (a Kalshi ticker or a Polymarket outcome-token id), use `kalshi` / `polymarket` (prefer them over `llm` for supported methods). Use `llm` (`athena_condition`) only when the predicate cannot reasonably be matched against a post, event, or named prediction market.
-
- #### When to suggest Auto
-
- - User wants alerts based on **price thresholds** ("alert me when BTC crosses 100k")
- - User wants alerts based on **technical indicators** ("notify when RSI drops below 30")
- - User wants **scheduled checks** ("check every 4 hours") or **calendar schedules** ("every weekday at 9am New York time" — use `cron.schedule`)
- - User wants the **same alert to keep firing on its own condition** ("notify me every time BTC dips below 60k") — add the top-level `repeat` object (`cooldown` + `maxTriggers`)
- - User wants **narrative/sentiment monitoring** ("alert when AI token narrative shifts")
- - User wants **multi-condition triggers** ("BTC above 100k AND ETH above 3500")
- - User wants to **compare live metrics** ("alert when price crosses above Bollinger Band")
- - User wants **LLM analysis on trigger** ("when it triggers, run a full analysis")
- - User wants **account-anchored social triggers** ("notify me when @cz_binance posts that Binance Alpha is listing a new token") — use **Signal: X/Twitter Post** (`source: "tweet"`)
- - User wants **Telegram-channel triggers** ("alert me when this channel posts a new Binance listing rumor") — use **Signal: Telegram Channel** (`source: "telegram"`)
- - User wants **news triggers** ("alert me when SEC approves a spot ETH ETF", "bearish news on ETH", "any exchange hack") — use **Events** (`source: "events"`)
- - User wants **official SEC filing triggers** ("tell me when Alphabet files a new 8-K", "alert on any late 10-Q notice") — use `source: "sec"` with the issuer CIK
- - User wants **prediction-market triggers** ("alert when this Kalshi market's YES probability crosses 60%", "notify when the market settles YES", "alert when this Polymarket outcome trades above 60c") — use **Prediction Markets** (`source: "kalshi"` or `source: "polymarket"`)
- - User wants **funding / liquidation / sentiment triggers** ("alert when BTC funding flips negative", "notify on an ETH liquidation cascade", "alert when Fear & Greed drops below 20") — use `source: "funding"` / `"liquidation"` / `"fear_greed"`
- - User wants **follow-graph triggers** ("tell me when someone big follows @elfa_ai", "alert when cobie follows a new account", "notify when 3 funds I track all follow the same account") — use `source: "x_follow_received"` / `"x_follow_made"` / `"x_follow_overlap"`
- - User wants to **follow one trader's calls** ("ping me every time @3PeaksTrading opens a long", "tell me when this trader closes or is stopped out of BTC") — use **Calls** (`source: "calls"`)
- - User wants **attention triggers on a token or stock** ("alert me when SOL turns hot on mindshare", "when ETH enters the top 10 by mindshare") — use **Mindshare** (`source: "mindshare"`)
- - User wants to **watch a macro catalyst on stocks/indices/commodities** ("tell me when the market prices a Fed cut", "alert on gold if CPI runs hot") — fire on the catalyst source and reference the HIP-3 perp, which trades 24/7. See [Catalyst Triggers](https://docs.elfa.ai/auto/catalyst-triggers)
-
- #### Auto access models
-
- | Mode | Route prefix | Auth | Best for |
- |---|---|---|---|
- | API key | `/v2/auto/*` | `x-elfa-api-key` on all routes | Apps, dashboards |
- | x402 keyless | `/x402/v2/auto/*` | x402 payment + `x-elfa-agent-secret` | AI agents, bots |
-
- #### x402 Auto (keyless agent mode)
-
- For x402 Auto, no API key is needed. Instead:
- - Send the x402 payment header `PAYMENT-SIGNATURE`. x402 **v2 only** — `X-PAYMENT` is accepted
- as an alias for the header name, but the payload must be a v2 payment payload
- - Include `x-elfa-agent-secret` on all query lifecycle routes
-
- **Agent secret management:**
- Generate a strong secret once and reuse it for all calls:
-
- ```bash
- openssl rand -hex 32
- # or: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
- ```
-
- Persist as `ELFA_AGENT_SECRET`. **Do not rotate per request** — x402 session ownership is
- derived from `SHA256(secret)`. If you change secrets, your agent identity changes and
- existing queries/sessions may become inaccessible.
-
- #### Auto pricing (both modes)
-
- **API-key mode (`/v2/auto/*`) — charged against your credit balance:**
-
- | Operation | Credits | Notes |
- |---|---|---|
- | `POST /v2/auto/chat` (Builder Chat) | `1 + dynamic` | Base 1 credit + `ceil(request_cost * 750)` dynamic charge based on LLM usage |
- | `POST /v2/auto/queries` (Create) | Simulation-driven | Baseline `5` + per simulated LLM call: fast `+5`, expert `+18` |
- | `POST /v2/auto/queries/validate` | Free | Returns cost estimate — always call before Create |
- | Per-post X account (`tweet` on an account outside Elfa's real-time set) | `creditsPerPost` (`1`) per post while the plan runs | Not in `estimatedCredits` or `x-elfa-credits`; see `tweetAccounts` on Validate |
- | Per-day follow account (`x_follow_made` / `x_follow_overlap` on an account Elfa doesn't monitor) | `creditsPerDay` (`1`) per UTC day its follows are read | Not in `estimatedCredits` or `x-elfa-credits`; see `followAccounts` on Validate |
- | `GET /v2/auto/queries/*` (list, poll, stream, sessions) | Free | |
- | `POST /v2/auto/queries/:queryId/cancel` (Cancel active query) | Free | |
- | `DELETE /v2/auto/queries/:queryId` (Delete terminal query) | Free | |
- | `GET /v2/auto/validate-symbol/:exchange/:symbol` | Free | |
-
- > Reference USD values for Create: baseline `$0.0725`, fast call `+$0.0725`, expert call `+$0.261`. Use `/queries/validate` to preview exact cost before committing.
-
- **x402 mode (`/x402/v2/auto/*`) — pay-per-request in USDC on Base, Arbitrum, Polygon, Avalanche, or Solana:**
-
- Builder Chat is speed-based with two schemes — a flat `exact` price, or, on Base when the 402
- lists it, a prepaid `batch-settlement` channel charged only what the turn used, up to the cap:
-
- | Speed | `exact` (flat) | `batch-settlement` (charged up to) |
- |---|---|---|
- | `fast` | $1 | $2 |
- | `expert` (default) | $2 | $6 |
-
- | Operation | Credits | USDC Cost |
- |---|---|---|
- | Query creation — baseline | 5 | $0.0725 |
- | Per fast LLM call | +5 | +$0.0725 |
- | Per expert LLM call | +18 | +$0.261 |
- | Validate, poll, cancel, sessions, stream | Free | Free |
-
- **x402 Auto example:**
-
- ```javascript
- // Validate a query (x402 Auto)
- const response = await x402Fetch(
- "https://api.elfa.ai/x402/v2/auto/queries/validate",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "x-elfa-agent-secret": process.env.ELFA_AGENT_SECRET,
- },
- body: JSON.stringify({
- query: {
- conditions: { AND: [{ source: "price", method: "current", args: { symbol: "BTC", exchange: "hyperliquid" }, operator: ">", value: 100000 }] },
- actions: [{ stepId: "step_1", type: "notify", params: { message: "BTC crossed 100k" } }],
- expiresIn: "24h"
- }
- }),
- });
- ```
-
- #### Recommended call sequence (deterministic agent path)
-
- **API key mode (`/v2/auto/*`):**
-
- 1. `POST /v2/auto/chat` — Ask Builder Chat to draft a query
- 2. `POST /v2/auto/queries/validate` — Validate EQL and preview cost
- 3. `POST /v2/auto/queries` — Create and activate
- 4. `GET /v2/auto/queries/{queryId}/stream` — Stream notifications (or poll)
- 5. `GET /v2/auto/queries/{queryId}/sessions` + `/sessions/{sessionId}` — Fetch LLM output (if using `llm` action)
- 6. (Optional cleanup) `POST /v2/auto/queries/{queryId}/cancel` — Cancel only while `active` (returns `409` if already terminal)
- 7. (Optional cleanup) `DELETE /v2/auto/queries/{queryId}` — Delete only after terminal (`triggered` / `expired` / `cancelled` / `failed`); rejects active queries with `409`
-
- **x402 mode (`/x402/v2/auto/*`):**
-
- 1. `POST /x402/v2/auto/chat` — Ask Builder Chat to draft a query
- 2. `POST /x402/v2/auto/queries/validate` — Validate EQL and preview cost
- 3. `POST /x402/v2/auto/queries` — Create and activate
- 4. `GET /x402/v2/auto/queries/{queryId}/stream` — Stream notifications (or poll via POST)
- 5. `POST /x402/v2/auto/queries/{queryId}/sessions` + `/sessions/{sessionId}` — Fetch LLM output
- 6. (Optional cleanup) `POST /x402/v2/auto/queries/{queryId}/cancel` — Cancel only while `active`. (x402 has no terminal-delete endpoint.)
-
- **Always validate before create.** Validate returns structured errors you can iterate on
- without spending credits.
-
- **Failure handling order** (apply in sequence):
-
- 1. Retry transient network errors with exponential backoff. If `5xx` responses persist across endpoints after a few retries, check [status.elfa.ai](https://status.elfa.ai) before debugging further (`https://status.elfa.ai/status.json` serves the same data to scripts; `https://status.elfa.ai/feed.xml` announces outages and recoveries).
- 2. On `400` / `422` validation failure → repair query using [Validation Errors table](#validation-errors--next-action-table), re-validate.
- 3. On `401` / `403` auth failure → refresh credentials or re-check the key's permissions.
- 4. On `402` x402 payment failure → re-price and retry with valid payment payload.
- 5. On `410` (SSE stream closed) → re-open stream or fall back to polling.
-
- **Common agent flows:**
-
- _Poll-based LLM flow:_
- ```
- POST /v2/auto/queries → create query with action.type = "llm"
- GET /v2/auto/queries/{queryId} → poll until execution with sessionId appears
- (latestEvaluation.conditionStates[] shows
- progress while you wait)
- GET /v2/auto/queries/{queryId}/sessions/{sessionId} → fetch full analysis
- ```
-
- _Webhook-based LLM flow:_
- ```
- POST /v2/auto/queries → create with action.type = "llm" and params.action
- (wait for webhook) → receive session reference / output
- (optional) GET session fetch → /v2/auto/queries/{queryId}/sessions/{sessionId}
- ```
-
- #### Builder Chat
-
- Builder Chat (`POST /v2/auto/chat` or `POST /x402/v2/auto/chat`) uses AI to translate
- natural language into EQL queries. Use `sessionId` for multi-turn conversations.
-
- ```json
- {
- "message": "Alert me when BTC breaks 100k with RSI confirmation above 55",
- "speed": "expert",
- "sessionId": "optional-session-id"
- }
- ```
-
- Response (API-key mode):
-
- ```json
- {
- "sessionId": "session-uuid",
- "response": "I can help with that... (markdown + EQL JSON code block)",
- "title": "BTC Breakout Alert",
- "reasoning": null,
- "planIds": [],
- "credits": 100
- }
- ```
-
- `credits` carries what the turn cost — the same total as the `x-elfa-credits` header. Builder
- Chat is dynamically priced, so read it instead of assuming a flat per-call cost.
-
- The response message contains the AI's reply in Markdown. When it generates EQL, it will be
- in a JSON code block — extract, validate via `/queries/validate`, then submit via `/queries`.
-
- **Prompting tips for Builder Chat:**
- - Include `title` and `description` (shown in notifications so recipients know what fired hours/days later)
- - Specify symbols, timeframe, trigger behavior (one-time — the default — vs recurring), delivery target. For recurring on the **same condition**, ask for the `repeat` object (`cooldown` + `maxTriggers`); for a fixed schedule, use a `cron` condition
- - For Signal triggers, give a **factual** match description (avoid vague phrasing like "bullish vibes")
- - Append `"If anything is unsupported, return the closest supported query and list substitutions"` to handle edge cases gracefully
- - Prefer `expiresIn` of `24h`–`3d` for fresh signals
- - Persist `sessionId` and reuse it for follow-up prompts so the model keeps context across turns
-
- **High-impact prompt pack** — drop these into `POST /v2/auto/chat` as the `message` field:
-
- _1) Complex TA breakout with direction filter:_
-
- ```
- Build an Auto query:
- - title + description: short human-readable summary and 1-2 sentence thesis
- - symbols: BTC, ETH, SOL
- - timeframe: 5m
- - trigger when price breaks previous 1h range high or low
- - confirm direction with RSI(14): >55 for upside, <45 for downside
- - actions: telegram alert + webhook to https://your-runner.example/auto/events
- - one-time trigger, expires in 48h
- If anything is unsupported, return the closest supported query and list substitutions.
- ```
-
- _2) CEX + DEX monitoring pack:_
-
- ```
- Build an Auto query pack for supported CEX + DEX symbols:
- - title + description per query: short summary and thesis sentence
- - watchlist: WBTC, ETH, SOL, HYPE
- - trigger when 15m volume surge aligns with price momentum
- - action: webhook to https://your-runner.example/auto/events
- - include symbol and trigger summary in payload
- - expires in 24h
- If any symbol/source is unsupported, skip it and report skipped items.
- ```
-
- _3) News + X sentiment context on trigger:_
-
- ```
- Build an Auto query:
- - title + description: short summary and the thesis behind watching this
- - monitor BTC and ETH for abnormal 1h move + volume confirmation
- - on trigger run llm action that adds:
- - latest market news context
- - X sentiment summary
- - risk note
- - also send telegram alert with a short summary
- - expires in 24h
- ```
-
- _4) Prediction-market thesis watcher (Kalshi):_
-
- ```
- Build an Auto query:
- - title + description: name the market thesis and the move you're watching for
- - source: kalshi
- - ticker: <a confirmed-open Kalshi market ticker, e.g. KXBTC-26APR0803-T77799.99>
- - trigger when the YES implied probability crosses above 0.6
- - action: llm to produce a catalyst hypothesis + invalidation level
- - deliver to webhook: https://your-runner.example/auto/events
- - include decision priority: high/medium/low
- - expires in 2d
- If the ticker is not open or anything is unsupported, return the closest supported query and list substitutions.
- ```
-
- _5) Portfolio risk guardrail:_
-
- ```
- Build an Auto query:
- - title + description: portfolio guardrail label and the risk scenario it covers
- - watch my portfolio symbols: BTC, ETH, SOL, HYPE
- - trigger on downside acceleration and momentum weakness
- - action: telegram alert with severity and suggested next check
- - recurring checks, expires in 3d
- ```
-
- _6) Agent handoff with strict follow-up contract:_
-
- ```
- Build an Auto query:
- - title + description: breakout playbook name and the follow-up intent
- - trigger on breakout + trend-confirmation conditions
- - action: webhook to https://your-runner.example/auto/events
- - include fields: eventId, symbol, triggerReason, priority, queryId
- - downstream agent decides next action under policy constraints
- - expires in 24h
- ```
-
- _7) Signal — account-anchored X/Twitter Post watcher:_
-
- ```
- Build an Auto query:
- - title + description: short summary anchored to the account + intent
- - use Signal category:
- - condition source: tweet
- - username: cz_binance (no leading @)
- - match description: "Binance Alpha is listing a new token"
- - minConfidence: 80
- - action: notify (or telegram_bot) with a concise message
- - expires in 24h
- If unsupported, return closest supported query and list substitutions.
- ```
-
- _8) Signal — event-first catalyst watcher:_
-
- ```
- Build an Auto query:
- - title + description: catalyst name and why it matters now
- - use an events condition:
- - condition source: events
- - filters: entity ETH, eventType regulatory
- - claim: "the SEC approves a spot ETH ETF"
- - minConfidence: 80
- - action: webhook to https://your-runner.example/auto/events
- - include queryId, eventId, and short trigger reason in payload
- - expires in 24h
- ```
-
- #### Query model (EQL)
-
- A query contains `conditions`, `actions`, and `expiresIn` (plus an optional `repeat`
- sibling — by default a plan is one-shot; see **Repeat** below):
-
- ```json
- {
- "title": "BTC RSI oversold on 1h",
- "description": "Mean-reversion entry: if BTC 1h RSI dips under 30, consider scaling in.",
- "conditions": {
- "AND": [
- {
- "source": "ta",
- "method": "rsi",
- "args": { "symbol": "BTC", "timeframe": "1h", "period": 14, "exchange": "hyperliquid" },
- "operator": "<",
- "value": 30
- }
- ]
- },
- "actions": [
- { "stepId": "step_1", "type": "webhook", "params": { "url": "https://your-endpoint.example/events" } }
- ],
- "expiresIn": "24h"
- }
- ```
-
- **Condition rules:**
- - Root group must be `AND` or `OR`
- - Nest groups up to depth 3, max 10 leaf conditions
- - Multi-symbol: a single query can require BTC AND ETH AND SOL conditions jointly
-
- **Allowed `expiresIn` values:** `1h`, `2h`, `4h`, `8h`, `12h`, `24h`, `48h` (2d), `72h` (3d),
- `120h` (5d), `168h` (7d), `240h` (10d), `336h` (14d), `504h` (21d), `720h` (30d — hard max).
- **Prefer `24h`–`72h`** — signals decay and short windows force re-evaluation. Only reach for
- `240h`–`720h` on slow structural theses (token unlocks, options expiries, monthly
- copy-trading), never on short-timeframe TA/scalps.
-
- **Plan limits (active plans per account):** an account can only hold so many **active** plans
- at once, by API key tier — Free `2`, Grow `50`, PAYG/Enterprise unlimited. A hard
- ceiling of **500 active plans** applies regardless of tier; exceeding either cap returns
- `409`. Only *active* plans count — expired/cancelled/terminal plans free capacity, so
- cancelling stale plans (or choosing a shorter `expiresIn`) is the fastest way to make room.
-
- **Repeat (`repeat`) — recurring plans on their own conditions:**
-
- By default a plan is **one-shot**: it fires once and moves to a terminal state. The optional
- top-level `repeat` object (a sibling of `expiresIn`) keeps a plan active and makes it re-fire
- on **its own** conditions — the right tool for "notify me *every time* BTC dips below 60k"
- without a fixed cron cadence.
-
- ```json
- {
- "conditions": { "AND": ["..."] },
- "actions": ["..."],
- "expiresIn": "7d",
- "repeat": { "cooldown": "1h", "maxTriggers": 10 }
- }
- ```
-
- Both fields are **mandatory** when `repeat` is present:
-
- | Field | Type | Meaning |
- |---|---|---|
- | `cooldown` | string | Minimum time between fires. `"0"` disables the rate limit. |
- | `maxTriggers` | number | Lifetime cap on fires (`1`–`1000`); reaching it makes the plan terminal. |
-
- - **`cooldown` allowed values** (its own set — sub-hour is permitted, unlike the cron/llm 1h minimum): `1m`, `5m`, `15m`, `30m`, `1h`, `2h`, `4h`, `8h`, `12h`, `24h`, `1d`, `7d`, plus `"0"`. Use `"0"` for "fire on every distinct event" (e.g. every matching tweet or Telegram message).
- - **`maxTriggers`** counts how many times the plan **fires** over its lifetime (distinct from the number of action steps). Sanity-check `cooldown × maxTriggers` against `expiresIn` — if there isn't enough runway, the plan expires before reaching the cap. A longer `expiresIn` buys patience through quiet periods; it does **not** raise the fire cap.
- - **Single reset rule.** A `repeat` plan re-fires only once its triggering signal has cleared. **Level** conditions (`price` / `ta` / `llm`-boolean) disarm on fire and re-arm when the whole condition tree next evaluates false; **event** conditions (`tweet.semantic` / `telegram.semantic` / `events.match` / `sec`) consume the matched event and re-fire on the **next distinct** event.
-
- > **Still rejected:** `repeat` combined with a recurring cron condition (`cron.every` /
- > `cron.schedule`) — both provide recurrence, so combining them fails validation as
- > `EQL_INVALID_REPEAT`. For a fixed-cadence recurring plan, use the cron condition on its own;
- > for a condition-driven recurring plan, use `repeat`.
-
- **Avoiding over-triggering:** the main risk is a condition that keeps flipping true — a
- metric flapping around its threshold, or a chatty account. `cooldown` is your rate-limit
- floor (never use `"0"` on level/metric conditions); `maxTriggers` is your hard circuit
- breaker (set it conservatively so a runaway condition self-limits). A **level** condition
- like `price < 60000` fires on *every* re-crossing, so prefer transition operators
- (`crosses_below` / `crosses_above` over `<` / `>`), give the threshold room from the current
- value, match `cooldown` to the signal's timeframe, and start conservative then loosen.
-
- Recommended starting points:
-
- | Condition type | `cooldown` | `maxTriggers` | Notes |
- |---|---|---|---|
- | Price / TA level (`price`, `ta`) | `1h`–`4h` (≥ your timeframe) | `3`–`10` | Prefer `crosses_*` operators; avoid `"0"` here. |
- | LLM predicate (`llm.athena_condition`) | `≥ period` (min `1h`) | `3`–`5` | The LLM already re-checks on `period` — don't re-alert faster than it re-evaluates. |
- | Event — single / quiet account or channel (`tweet`, `telegram`) | `0`–`15m` | `10`–`20` | Each distinct post/message is a discrete event; `"0"` is fine unless the source is chatty. |
- | Event — noisy account/channel or broad `events` filters | `15m`–`1h` | `10`–`20` | A small cooldown collapses a burst of mentions, or follow-up events on one story, into one alert. |
- | SEC filing event (`sec`) | `0`–`1h` | `10`–`20` | Use `"0"` when you want every matching filing; add cooldown for noisy form categories. |
- | Liquidation cascade (`liquidation`) | `1h`+ | `5`–`10` | Windows decay to zero, so the plan re-arms per cascade; the cooldown stops one long cascade re-alerting. |
-
- **Allowed action types:** `webhook`, `notify`, `telegram_bot`, `llm`. The `actions` array is **exactly one step** per query — run standalone LLM work with `params.action`; chain follow-up work via `llm` action with `params.callback.action`, or have your runner fan out from the trigger event.
-
- **Action params shape (key fields):**
-
- | `type` | Required `params` | Optional `params` |
- |---|---|---|
- | `notify` | `message` (1–1000 chars) | — |
- | `webhook` | `url` (https only, allowlisted host) | `signingSecret` (write-only — **set this in production**), `allNotifications` (default `false`) |
- | `telegram_bot` | `botToken`, `chatId` | `allNotifications` (default `false`) |
- | `llm` | `action` ∈ `chat` / `summary` / `macro` / `accountAnalysis` / `tokenDiscovery` / `tokenAnalysis` | `callback.action` for chained follow-up (a callback `webhook` also takes `includeAnalysis`), `speed` (`fast` / `expert`), per-action extras (for example `message` on `chat`) |
-
- `telegram_bot` does **not** take a `message` field — the message body is auto-composed from the query `title` + `description` + trigger context. Use `notify` (in-app push) when you want to specify the message text yourself. `allNotifications: true` on `webhook` / `telegram_bot` opts the destination into lifecycle notifications (failed/expired/run-failed) in addition to the trigger fire.
-
- #### Triggers — condition sources
-
- **Market-data venue (`exchange`) — REQUIRED on every `price` and `ta` condition:**
-
- Every `price` and `ta` condition must include an `exchange` arg that selects which venue's
- market data backs the condition. Allowed values: `hyperliquid`, `binance` (exact
- lowercase enum). `gmx` is no longer accepted: create and validate reject it with
- `EQL_INVALID_ARG_VALUE`. Drafts still store a `gmx` condition, and convert rejects it with
- `EQL_VALIDATION_ERROR`. This arg **only affects the data source** of the condition. A
- `price`/`ta` condition without `exchange` fails validation.
-
- Per-venue data notes:
- - `binance` reads its **USD-M perp** markets and uses Binance's own base-symbol convention
- (`1000PEPE`, not `kPEPE`), validated against Binance's live perp catalog at create time.
-
- ```json
- { "source": "price", "method": "current", "args": { "symbol": "BTC", "exchange": "hyperliquid" }, "operator": ">", "value": 100000 }
- ```
-
- **Price source (`price`):**
-
- | Method | Args | Returns | Description |
- |---|---|---|---|
- | `current` | `symbol`, `exchange` | number | Current price |
- | `change` | `symbol`, `exchange`, `period` | number | % change over period |
- | `high` | `symbol`, `exchange`, `period` | number | High in period |
- | `low` | `symbol`, `exchange`, `period` | number | Low in period |
- | `volume` | `symbol`, `exchange`, `period` | number | Volume (USD) over period |
-
- **TA source (`ta`) — technical indicators:** every method also requires `symbol`, `timeframe`, **and `exchange`**.
-
- | Method | Required args | Optional args | Returns |
- |---|---|---|---|
- | `rsi` | `symbol`, `timeframe`, `exchange` | `period` (default 14) | RSI (0-100) |
- | `macd_value` | `symbol`, `timeframe`, `exchange` | — | MACD line |
- | `macd_signal` | `symbol`, `timeframe`, `exchange` | — | MACD signal line |
- | `macd_histogram` | `symbol`, `timeframe`, `exchange` | — | MACD histogram |
- | `bbands_upper` | `symbol`, `timeframe`, `exchange` | `period` (default 20) | Upper Bollinger Band |
- | `bbands_middle` | `symbol`, `timeframe`, `exchange` | `period` (default 20) | Middle Bollinger Band |
- | `bbands_lower` | `symbol`, `timeframe`, `exchange` | `period` (default 20) | Lower Bollinger Band |
- | `ema` | `symbol`, `timeframe`, `exchange`, `period` | — | Exponential MA |
- | `sma` | `symbol`, `timeframe`, `exchange`, `period` | — | Simple MA |
- | `atr` | `symbol`, `timeframe`, `exchange` | `period` (default 14) | Average True Range |
- | `stoch_k` | `symbol`, `timeframe`, `exchange` | — | Stochastic %K |
- | `stoch_d` | `symbol`, `timeframe`, `exchange` | — | Stochastic %D |
- | `cci` | `symbol`, `timeframe`, `exchange` | `period` (default 20) | CCI |
- | `willr` | `symbol`, `timeframe`, `exchange` | `period` (default 14) | Williams %R |
-
- **TA critical rules:**
- - Every `ta` (and `price`) condition **requires `exchange`** (`hyperliquid` or `binance`)
- - `ema` and `sma` **require** `period` — it is NOT optional
- - `rsi`, `bbands_*`, `atr`, `cci`, and `willr` accept optional `period` with documented defaults above
- - `period` must be a JSON number (`14`), not a string (`"14"`)
- - Use `period` not `length` — `length` is not a recognized alias
- - `timeframe` values: `1m`, `5m`, `15m`, `30m`, `1h`, `2h`, `4h`, `8h`, `12h`, `1d`
-
- **Cron source (`cron`):**
-
- | Method | Args | Description |
- |---|---|---|
- | `once` | `period` | True on first due evaluation at/after creation + period |
- | `onceRemainTrue` | `period` | True on first due evaluation and stays true |
- | `every` | `period` | True at each period interval |
- | `schedule` | `expression`, `timezone?` | True whenever the 5-field cron `expression` matches in `timezone` |
-
- `once`, `onceRemainTrue`, and `every` are **interval** methods measured from query creation.
- `schedule` is a **calendar/wall-clock** method:
-
- - `expression` is a classic **5-field** cron string (`minute hour day-of-month month day-of-week`) — e.g. `0 9 * * 1-5` (weekdays at 09:00).
- - `timezone` is optional and defaults to `UTC`. When set it must be a valid **IANA** name (e.g. `America/New_York`, `Asia/Singapore`) so daylight-saving transitions are handled correctly. Fixed-offset strings like `+02:00` are **not** accepted.
- - `schedule` enforces the same **1h-minimum cadence** as the other cron methods: the minute field must be a single fixed value (`0`–`59`). Wildcards, steps, lists, or ranges in the minute field are rejected.
- - Allowed: `7 * * * *` (hourly at minute 7), `0 9 * * 1-5` (weekdays 09:00), `30 8 * * *` (daily 08:30).
- - Not allowed: `* * * * *` (every minute), `*/15 * * * *` (every 15 min), `0,30 * * * *` (twice per hour).
- - `schedule` takes **no** `period` arg.
-
- > **Builder Chat support pending:** `cron.schedule` is available on the direct EQL/API path
- > (Validate / Create). Builder Chat does not yet reliably generate `cron.schedule` — author
- > calendar schedules directly and confirm with `/v2/auto/queries/validate`.
-
- **LLM source (`llm`):**
-
- | Method | Args | Description |
- |---|---|---|
- | `athena_condition` | `query`, `period`, `speed?` | LLM-evaluated condition (natural language) |
-
- **Signal source — X/Twitter Post (`tweet`):**
-
- In the Builder Chat catalog this is the `Signal` category, **X/Twitter Post**.
-
- | Method | Required args | Returns | Description |
- |---|---|---|---|
- | `semantic` | `username`, `text`, `minConfidence` | boolean | Matches posts from a specific X/Twitter account when semantic confidence meets threshold |
-
- Rules:
- - `username` must be passed **without** `@` (e.g. `cz_binance`, not `@cz_binance`).
- - `username` can be any public X account. Validate's `tweetAccounts` says how each is billed: `included` (Elfa's real-time set) costs nothing extra; `per_post` costs `creditsPerPost` for every post while the plan runs (posts, quotes, notes and articles; not replies or reposts), matched or not, and is not in `estimatedCredits`. Tell the user before Create. A key without a paid API plan, and x402, can watch only `included` accounts; a rejected account is an `errors[]` entry (see [Validation Errors](#validation-errors--next-action-table)). Builder Chat drafts `tweet` conditions only for `included` accounts: write any other yourself. Details: [X Accounts and Per-Post Billing](https://docs.elfa.ai/auto/signal#x-accounts-and-per-post-billing).
- - `minConfidence` must be a JSON integer between `0` and `100`; use `80` when the user gives no threshold.
- - `method`, `operator`, and `value` are auto-filled defaults (`semantic`, `==`, `true`) — do **not** include them in the condition JSON.
-
- ```json
- {
- "source": "tweet",
- "args": {
- "username": "cz_binance",
- "text": "Binance Alpha is listing a new token",
- "minConfidence": 80
- }
- }
- ```
-
- **Signal source — Telegram Channel (`telegram`):**
-
- In the Builder Chat catalog this is the `Signal` category, **Telegram Channel**.
-
- | Method | Required args | Returns | Description |
- |---|---|---|---|
- | `semantic` | `chatUsername` and/or `chatId`, `text`, `minConfidence` | boolean | Matches messages from a monitored public Telegram channel when semantic confidence meets threshold |
-
- Rules:
- - Provide `chatUsername`, `chatId`, or both on the full method form. The shorthand condition form requires exactly one identifier; if both are present on a full condition they must resolve to the same channel.
- - Prefer `chatUsername` without `@` or `t.me/` when you know the public channel username.
- - Use `chatId` only when it came from tooling or an existing plan; it must be a signed 64-bit integer string.
- - The channel must resolve to a monitored public Telegram channel at create-time, otherwise validation/create fails.
- - `minConfidence` must be a JSON integer between `0` and `100`; use `80` when the user gives no threshold.
-
- ```json
- {
- "source": "telegram",
- "args": {
- "chatUsername": "<monitored_public_channel_username>",
- "text": "new Binance listing rumor",
- "minConfidence": 80
- }
- }
- ```
-
- **Events (`events`):**
-
- In the Builder Chat catalog this is the `Signal` category, **Events**.
-
- Triggers on events: news about an asset, project, company, person, or macro — listings, hacks, regulatory decisions, macro releases, company news, large flows, bullish or bearish news. These are the events `GET /v3/events` (API v3) serves, each written up from many posts, and the condition takes that endpoint's filter values, matched the same way. Use it for news **about** a subject; use `tweet` for posts **from** an account. Not yet in the app's visual builder catalog; Builder Chat and the API both author it. Full reference: https://docs.elfa.ai/auto/events — the events themselves: https://docs.elfa.ai/api/v3/events
-
- | Method | Args (all optional; at least one filter or `text`) | Returns |
- |---|---|---|
- | `match` | `entity`, `entityType`, `eventClass`, `eventType`, `implication`, `alertAction`, `assetClass`, `novelty`, `relation`, `storylineId`, `text` + `minConfidence` | boolean |
-
- - `entity`: one id from `GET /v3/entities` (exact: the same id `/v3/events` takes in `entityIds`), or one ticker or name (`"ETH"`, `"Hyperliquid"`). A ticker or name resolves **ticker first**: `"Elon Musk"` resolves to a token named MUSK and `"Fed"` to a stock with the ticker FED, so set `entityType` (`token`, `equity`, `project`, `person`, `organisation`) for anything that is not a token or a stock. Central banks, regulators, and macro themes are not entities — use `eventType` + `text`.
- - `eventClass`: `market` (news, including about one asset) or `call` (trader positioning). For one asset use `entity`, not `asset`.
- - `eventType`: `security`, `listing`, `regulatory`, `macro`, `protocol`, `company`, `marketFlow`, `price`, `social`, `other`.
- - `implication`: `bullish`, `bearish`, `mixed`, `unclear` — the likely direction for the assets named. Put stance here, never in `text`.
- - `alertAction`: `alert` (judged impactful) or `monitor` (worth watching). `assetClass`: `crypto` or `equity` (events that name no asset pass). `novelty`: `new` or `update` — does **not** reliably filter repeats: most later readings of a story still say `new`; use `relation`.
- - `relation`: where the event sits in its story — `origin`, `update`, `confirmation`, `contradiction`, `mitigation`, `consequence`, `reaction`, `context`. `origin` is the event that started a story (a story that grew out of an earlier one is a story of its own, so its first event is `origin` too): add `relation: "origin"` for one alert per story. About 6 in 10 events start a story, so `origin` keeps more than half of what the plan would fire on and drops the follow-ups; with `repeat`, the plan fires once per new story.
- - `storylineId`: the later events of one story. Take the id from the API, never compose it: an event's `storylineId` in `/v3/events`, or `storylines[0].id` from `GET /v3/events/storylines?q=<topic>`. Validate and Create reject an id that names no storyline. Most stories never get a second event, so follow one that is still developing; add `relation` to narrow it (e.g. `contradiction` for reports that dispute the story so far). A story that grows out of the followed one has its own id and is not followed.
- - Call events, and the few other events not threaded into a story, match neither `relation` nor `storylineId`.
- - `text` + `minConfidence` (both or neither, start at `80`): one specific claim, checked against the event's title and summary. Filter-only conditions consume no credits.
- - One value per argument; several assets or types → several `events` conditions under `OR`. An array is rejected with `EQL_INVALID_JSON`.
- - Shorthand only in practice: the full form takes just `operator: "=="`, `value: true`. No dynamic values.
- - Runtime: events arrive minutes to hours after the first post (to be first, watch the account with `tweet`); only events less than an hour old are matched; each event fires a plan at most once; about 1 in 9 events continues a story already reported that day. To fire once per story, add `relation: "origin"`, or use `repeat.cooldown` to space alerts out.
- - Preview before creating: `GET /v3/events` with the same filters and `from=<now - 1d>` (`entityIds=<id>` for `entity`) returns what the condition would have fired on. `q` has no counterpart: `text` checks a claim per event instead. `GET /v3/events/storylines?storylineIds=<id>` maps to `"storylineId": "<id>"`, one storyline per condition. One value per argument, no time range. Mapping: https://docs.elfa.ai/auto/events#from-v3-events
- - `match` in the trigger payload: `eventId`, `title` (the event's `alertTitle`), `eventClass`, `eventType`, `implication`, `alertAction`, `novelty`, `timeSensitivity`, `citedSourceCount`, `analyzedAt` (ISO 8601), `entities[]` (`symbol`, `name`, `entityType`; up to 10), and `confidence` when `text` was checked. No link, no summary.
- - `match.eventId` is the event's `id` in `/v3/events` — **not** the webhook delivery id (`X-Auto-Event-Id`, the one to dedupe deliveries on). `GET /v3/events?eventIds=<match.eventId>` returns the summary, cited posts with links, price impacts and `storylineId`. To be alerted once per story, filter on `relation: "origin"` rather than comparing `storylineId` values yourself; to follow the story an alert belongs to, create a condition on its `storylineId`.
- - The `/v3` endpoints need a Pay-as-you-go, Grow or Enterprise key. Without one, name the entity by ticker or name and read the Validate response for what it resolved to.
-
- ```json
- {
- "source": "events",
- "args": { "entity": "ETH", "implication": "bearish" }
- }
- ```
-
- **Calls (`calls`):**
-
- Triggers on the trading calls one X account makes: each time it opens a long or short, adds to it, trims it, closes it, gets stopped out, or reverses it. These are the calls `GET /v3/calls` (API v3) serves, so a plan fires on the same call the endpoint returns, as soon as Elfa records it. Use `calls` for an account's calls, `tweet` for its posts in general, and `events` with `eventClass: "call"` for many traders calling the same asset. A `calls` condition reads the call Elfa recorded, so it needs no `text` and costs no credits. Full reference: https://docs.elfa.ai/auto/calls
-
- | Method | Args (`handle` required, the rest optional) | Returns |
- |---|---|---|
- | `match` | `handle`, `asset`, `market`, `direction`, `action` | boolean |
-
- - `handle`: the caller's X handle, with or without `@`; write it without. One account per condition; several callers → several `calls` conditions under `OR`.
- - `asset`: an id from `GET /v3/entities`, a ticker or a name of the token or stock the call is on, stored as `assetId` (the asset's family id, the `asset.id` a call carries). `market` (`crypto` or `equity`, requires `asset`) settles a ticker that names both a coin and a stock; without it, the one that leads its own market wins. A project, person or organisation is rejected. When other assets match `asset` just as closely, the market leader wins and Validate returns a warning naming the others; only a tie that nothing breaks is rejected, as "Ambiguous asset".
- - `direction`: `long` or `short`. `action`: `open`, `add`, `trim`, `reverse`, `close`, `stopOut`; leave it out to fire on every action. One value per argument: for "close or stop out", or "BTC or ETH", write an `OR` of conditions.
- - Shorthand `{ "source": "calls", "args": { … } }` expands to `method: "match"`, `operator: "=="`, `value: true`. No `text`, no `minConfidence`, no dynamic values.
- - Which accounts: Elfa records calls from the X accounts it collects posts from, and Validate/Create reject any other with `EQL_INVALID_ARG_VALUE`. Find callers with `GET /v3/calls` or `GET /v3/people/accounts?metric=alphaScore`; for an account outside them, use `tweet`. Some accounts are collected with a delay: Validate accepts them with a warning, and their calls are recorded hours after the post, typically two to five. Telegram channels cannot be followed yet.
- - Runtime: usually within a minute of the post (hours later for a delayed account). Each call fires a plan once; with `repeat`, each new call fires again. Only calls made after the plan was created, and never a call recorded more than 12 hours after its post. Restating an open position (`reaffirm` in `/v3/calls`) is not an action, and neither is a position expiring at the end of its horizon, which is how most positions end: "tell me when the position ends" hears only explicit exits. A reversal is two calls (`reverse` on the side it leaves, `open` on the side it takes), so a condition without `action` fires twice; a post naming several assets is one call per asset.
- - `match` in the trigger payload: `callId` and `episodeId` (the call's `id` and `episodeId` in `/v3/calls`; read the position with `GET /v3/calls/episodes`), `handle`, `source` (`twitter`), `callAction`, `direction`, `horizon`, `asset` (`id`, `symbol`, `name`, `entityType`), `setup`, `statedTarget`, `statedStop`, `entryPrice`, `occurredAt` (ISO 8601 here; Unix seconds on `/v3/calls`) and `postUrl`. `exitPrice` and `closeReason` appear on `close`, `stopOut` and `reverse`; fields the call does not have are left out.
-
- ```json
- {
- "source": "calls",
- "args": { "handle": "3PeaksTrading", "direction": "long", "action": "open" }
- }
- ```
-
- **Signal selection policy** (which source to pick):
-
- - One account's trading calls (`@user opens a long`, `@user exits BTC`) → `calls`.
- - Account-anchored intent (handle in the prompt) → `tweet`.
- - Channel-anchored intent (a public Telegram channel in the prompt) → `telegram`.
- - News about a subject (no specific account) → `events`.
- - Official SEC filing intent (issuer CIK, form type, 8-K item) → `sec`.
- - Prefer `tweet`/`telegram`/`events` over `llm` when the trigger is plausibly expressed as a post or event.
- - Fall back to `llm` (`athena_condition`) only for predicates that are not naturally a post/event match.
-
- **Signal `args.text` authoring rubric** — match quality is dominated by `text` quality. Write a short factual claim, not a monitoring command.
-
- | Weak phrasing (bad) | Actionable phrasing (good) |
- |---|---|
- | `Bearish vibes` | `Opens a short position on oil` |
- | `Something bullish` | `Announces a new stake in TSLA` |
- | `Bullish on a coin` | `Posts that they're bullish on $HYPE and $SOL` |
- | `Alpha group update` | `new Binance listing rumor` |
- | `Team news` | `team confirms mainnet launch` |
- | `Market crash` (events `text`) | `Major DeFi protocol suffers a $200M exploit` |
- | `War conflict` (events `text`) | `US imposes new sanctions on Russia` |
- | `Bearish ETH news` (events `text`) | no `text` — `{ "entity": "ETH", "implication": "bearish" }` |
-
- Additional constraints:
- - Keep `text` atomic — one event/theme per condition.
- - For `tweet`, do not restate account identity in `text` — `username` already scopes that.
- - For `telegram`, do not restate channel identity in `text` — `chatUsername` or `chatId` already scopes that.
- - Split `A or B` intents into multiple Signal conditions joined by `OR`; split `A and B` into multiple conditions joined by `AND`.
- - Prefer separate queries for event-driven intents (`tweet`/`telegram`/`events`) and recurring schedule intents (`cron.every`) for clearer runtime semantics.
-
- **`minConfidence` tuning:** default `80`; raise to `85`–`90` for fewer false positives; lower to `70`–`75` if you need higher recall.
-
- **Scheduling period (cron / llm):** for `cron` **interval** methods (`once`,
- `onceRemainTrue`, `every`) and `llm` sources, `period` is a scheduling interval with a
- minimum of `1h`. Allowed: `1h`, `2h`, `4h`, `8h`, `12h`, `24h`, `1d`, `7d`. `cron.schedule`
- does **not** take a `period` — it is driven by its 5-field cron `expression` and follows the
- same 1h-minimum cadence (at most one fire per hour).
-
- **Signal sources are event-driven**, not schedule-driven — they evaluate when relevant mention events arrive, not on a polling interval. They can still be combined with other condition types via `AND`/`OR`.
-
- **SEC filings source (`sec`):**
-
- Trigger on enriched SEC filing metadata — a new 8-K, a late periodic filing notice, an
- offering form, or a specific 8-K item. `sec` conditions use the full EQL condition form and
- obey the standard limits (depth 3, 10 leaves), so they combine with other sources in
- `AND`/`OR` groups.
-
- **CIK routing:** `args.ticker` is the issuer's **CIK**, not the stock ticker. It must be 1–10
- digits, is trimmed, and is normalized to a 10-digit zero-padded CIK (`"1652044"` →
- `"0001652044"`). Validate/create checks the CIK against SEC submissions metadata and rejects
- unknown or malformed values rather than creating a plan that can never fire.
-
- | Method | Returns | Operators | Description |
- |---|---|---|---|
- | `base_form_type` | enum | `==` `!=` | Normalized base form — amendments stripped (`10-K/A` → `10-K`), `424B*` → `424B` |
- | `form_category` | enum | `==` `!=` | `current_report`, `periodic_report`, `ownership`, `offering`, `proxy_ma`, `listing`, `registered_fund`, `sec_review`, `other` |
- | `is_amendment` | boolean | `==` `!=` | Form ends in `/A` |
- | `is_late_filing` | boolean | `==` `!=` | Late notification form (`NT 10-K`, `NT 10-Q`, `NT 20-F`) |
- | `is_offering` | boolean | `==` `!=` | Offering, registration, or prospectus-style filing |
- | `is_ownership_filing` | boolean | `==` `!=` | Insider, beneficial, or institutional ownership filing |
- | `has_item_1_03` | boolean | `==` `!=` | 8-K Item 1.03 — bankruptcy or receivership |
- | `has_item_1_05` | boolean | `==` `!=` | 8-K Item 1.05 — material cybersecurity incident |
- | `has_item_2_02` | boolean | `==` `!=` | 8-K Item 2.02 — results of operations |
- | `has_item_2_05` | boolean | `==` `!=` | 8-K Item 2.05 — exit or disposal costs |
- | `has_item_3_01` | boolean | `==` `!=` | 8-K Item 3.01 — delisting or listing-compliance notice |
- | `has_item_4_01` | boolean | `==` `!=` | 8-K Item 4.01 — change of certifying accountant |
- | `has_item_5_02` | boolean | `==` `!=` | 8-K Item 5.02 — director/officer changes |
- | `has_item_8_01` | boolean | `==` `!=` | 8-K Item 8.01 — other reported events |
- | `has_primary_document` | boolean | `==` `!=` | Primary filing document name was found |
- | `has_xbrl` | boolean | `==` `!=` | Archive includes XBRL artifacts |
- | `submission_match_found` | boolean | `==` `!=` | Accession matched inside SEC submissions metadata |
- | `document_count` | number | `>` `<` `>=` `<=` `==` `!=` | Number of archive documents discovered |
-
- ```json
- {
- "source": "sec",
- "method": "base_form_type",
- "args": { "ticker": "0001652044" },
- "operator": "==",
- "value": "8-K"
- }
- ```
-
- Use `base_form_type` when the user names a specific form, `form_category` for a filing
- family, and the boolean methods for normalized flags (`is_late_filing` for NT notices,
- `is_offering` for registrations and prospectuses, `has_item_1_05` for cybersecurity
- disclosures). `sec` is event-driven like the Signal sources, so with `repeat` it re-fires on
- the next distinct filing. Full reference:
- [SEC Filings](https://docs.elfa.ai/auto/sec-filings).
-
- **Prediction Markets source (`kalshi`):**
-
- Trigger on Kalshi prediction-market activity — live trade prints and market lifecycle. Each
- `kalshi` condition takes exactly one arg, `ticker`: a full open Kalshi market ticker
- (e.g. `KXBTC-26APR0803-T77799.99` = series + event + strike). Prefer `kalshi` over `llm` when
- the trigger is a supported Kalshi method, and over `price`/`ta` when you care about an event's
- *implied probability* rather than an asset's spot price.
-
- _Trade-backed methods_ — evaluate on the latest executed trade print:
-
- | Method | Returns | Description |
- |---|---|---|
- | `yes_price` | number `0`–`1` | Executed YES price = live implied probability of YES |
- | `no_price` | number `0`–`1` | Executed NO price (implied probability of NO) |
- | `trade_size` | number ≥ 0 | Contracts on that print |
- | `taker_outcome_side` | enum `yes` / `no` | Which outcome the aggressor (taker) positioned for |
- | `taker_book_side` | enum `bid` / `ask` | `bid` = YES-side pressure, `ask` = NO-side pressure |
- | `is_block_trade` | boolean | `true` for large off-book negotiated block trades |
-
- _Market-backed methods_ — evaluate on market lifecycle updates:
-
- | Method | Returns | Description |
- |---|---|---|
- | `status` | enum `active` / `closed` / `determined` / `settled` / `finalized` | Lifecycle state (`active` = only tradeable state) |
- | `result` | enum `yes` / `no` | Settlement outcome (empty until the market resolves) |
- | `settlement_value` | number ≥ 0 | Final settlement value in dollars (populated on settlement) |
-
- **Operators by method** (restricted per method):
-
- | Method(s) | Allowed operators |
- |---|---|
- | `yes_price`, `no_price` | `>` `<` `>=` `<=` `==` `!=` `crosses_above` `crosses_below` |
- | `trade_size`, `settlement_value` | `>` `<` `>=` `<=` `==` `!=` |
- | `taker_outcome_side`, `taker_book_side`, `status`, `result`, `is_block_trade` | `==` `!=` |
-
- Only the price methods (`yes_price` / `no_price`) support the transition operators
- `crosses_above` / `crosses_below`; enum and boolean methods are equality-only. `value` must be
- a **literal** matching the method's type (no dynamic field-vs-field values), and
- `is_block_trade` takes a JSON boolean `true` / `false` (not the strings `"true"` / `"false"`).
-
- **Open markets only:** at create/validate time `args.ticker` is checked against the catalog of
- **currently-open** Kalshi markets. A `closed` / `determined` / `settled` / `finalized` ticker
- is rejected with `EQL_INVALID_ARG_VALUE` ("was not found or is not open"). Resolve a real,
- currently-open ticker first — never invent or guess tickers. Lifecycle conditions are still
- useful on a market you already watch: a plan created while a market is `active` can fire as it
- later moves to `settled` (`status`) or resolves (`result` / `settlement_value`) within its
- `expiresIn` window.
-
- ```json
- {
- "source": "kalshi",
- "method": "yes_price",
- "args": { "ticker": "KXBTC-26APR0803-T77799.99" },
- "operator": "crosses_below",
- "value": 0.4
- }
- ```
-
- Full method tables, value enums, and example automations:
- [Prediction Markets → Kalshi](https://docs.elfa.ai/auto/prediction-markets#kalshi).
-
- **Prediction Markets source (`polymarket`):**
-
- Trigger on live Polymarket outcome-token activity: last traded price, best bid/ask, and the
- size and aggressor side of the most recent trade. Unlike Kalshi, Polymarket exposes **price
- and trade methods only** — there are **no** market-lifecycle (`status` / `result`) methods.
-
- **Ticker = outcome-token `asset_id`.** For Polymarket, `args.ticker` is the **outcome token
- `asset_id`** — the long numeric id of a single outcome (e.g. the YES token), **not** the
- top-level Polymarket market id (which is shared by both sides of a binary market). At create
- time the token is validated against Polymarket's live markets; an unknown or inactive token
- is rejected ("was not found or is not active"). Resolve a real, currently-active
- outcome-token id first — never invent or guess ids.
-
- Each method takes exactly one arg, `ticker`:
-
- | Method | Returns | Description |
- |---|---|---|
- | `price` | number in `[0, 1]` | Last traded probability / price for the outcome token |
- | `bid` | number in `[0, 1]` | Best bid for the outcome token (when present on the event) |
- | `ask` | number in `[0, 1]` | Best ask for the outcome token (when present on the event) |
- | `size` | number ≥ 0 | Size of the last-trade update (when present on the event) |
- | `side` | enum `"BUY"` / `"SELL"` | Aggressor side of the last trade, in uppercase |
-
- The feed mixes several event subtypes (`price_change`, `best_bid_ask`, `last_trade_price`,
- `book`), and **not every field updates on every event** — `best_bid_ask` updates `bid`/`ask`
- only, while `last_trade_price` updates `price`/`size`/`side` only. Design a condition around
- the specific field you need rather than assuming all fields move together.
-
- **Operators by method** (restricted per method):
-
- | Method(s) | Allowed operators |
- |---|---|
- | `price`, `bid`, `ask` | `>` `<` `>=` `<=` `==` `!=` `crosses_above` `crosses_below` |
- | `size` | `>` `<` `>=` `<=` `==` `!=` |
- | `side` | `==` `!=` |
-
- `value` must be a **literal** matching the method's type (`0`–`1` for `price`/`bid`/`ask`,
- ≥ 0 for `size`, uppercase `"BUY"` / `"SELL"` for `side`). Polymarket conditions **do not
- support dynamic (field-vs-field) values** — dynamic Polymarket targets are rejected at
- validation.
-
- ```json
- {
- "source": "polymarket",
- "method": "price",
- "args": { "ticker": "115556263888245616435851357148058235707004733438163639091106356867234218207169" },
- "operator": "crosses_below",
- "value": 0.4
- }
- ```
-
- Standard EQL limits apply to both prediction-market sources: combinable with other sources
- inside `AND`/`OR` groups, up to depth 3 and 10 leaf conditions. Full method tables and
- example automations:
- [Prediction Markets → Polymarket](https://docs.elfa.ai/auto/prediction-markets#polymarket).
-
- **Market-structure source (`funding`) — perp funding rate:**
-
- Trigger on a perp's funding rate — an overheated long bias, a flip to negative, or a
- cross-venue divergence. Funding and liquidation flow often lead the spot price reaction.
-
- **Ticker = composite `SYMBOL:EXCHANGE`** (not `symbol` + `exchange` args). `SYMBOL` is the
- base asset (`BTC`, not `BTCUSDT`); the venue rides in the ticker. Venues: `binance`,
- `hyperliquid`. Examples: `BTC:BINANCE`, `ETH:HYPERLIQUID`.
-
- | Method | Args | Returns | Operators | Description |
- |---|---|---|---|---|
- | `annualized_rate` | `ticker` | number | all 8 | **Canonical.** Funding as annualized percent (APR): `32.85` = 32.85% APR. Positive = longs pay shorts. |
- | `interval_rate` | `ticker` | number | all 8 | Percent per settlement interval. |
- | `interval_hours` | `ticker` | number | level only | Settlement interval in hours: `8` (Binance) or `1` (Hyperliquid). Venue metadata. |
- | `exchange` | `ticker` | enum | `==` / `!=` | The venue: `"binance"` or `"hyperliquid"`. |
-
- - **Prefer `annualized_rate`** — Binance settles every 8h and Hyperliquid every 1h, so the
- same `interval_rate` threshold means different things per venue; annualizing removes that
- trap. "Funding flips negative" is exactly `annualized_rate crosses_below 0`.
- - "level only" = `>` `<` `>=` `<=` `==` `!=` (no `crosses_*`).
- - HIP-3 dex-prefixed symbols (`xyz:TSLA`) are **rejected** here (`EQL_INVALID_ARG_VALUE`,
- `details.hip3: true`) — the funding feed never publishes keys for them.
-
- ```json
- { "source": "funding", "method": "annualized_rate", "args": { "ticker": "BTC:BINANCE" }, "operator": "crosses_below", "value": 0 }
- ```
-
- **Market-structure source (`liquidation`) — liquidation flow:**
-
- Trigger on liquidation flow in a trailing window — a cascade crossing a USD threshold, a
- one-sided flush, or liquidations as a share of open interest. Same composite
- `SYMBOL:EXCHANGE` ticker as `funding`. Venues: `binance`, `bybit`, `hyperliquid`.
-
- | Method | Args | Returns | Operators | Description |
- |---|---|---|---|---|
- | `total_usd_1m` / `total_usd_5m` / `total_usd_1h` | `ticker` | number | all 8 | Total USD liquidated in the trailing window. |
- | `long_usd_1m` / `long_usd_5m` / `long_usd_1h` | `ticker` | number | all 8 | Longs liquidated (forced selling). |
- | `short_usd_1m` / `short_usd_5m` / `short_usd_1h` | `ticker` | number | all 8 | Shorts liquidated (forced buying). |
- | `largest_order_usd_1h` | `ticker` | number | all 8 | Single biggest liquidation order in the trailing hour, USD. |
- | `count_1h` | `ticker` | number | level only | Number of liquidation orders in the trailing hour. |
- | `total_pct_oi_1m` / `total_pct_oi_5m` / `total_pct_oi_1h` | `ticker` | number | all 8 | Window total as a **percent of open interest** (compares across symbols). |
- | `exchange` | `ticker` | enum | `==` / `!=` | The venue: `"binance"` / `"bybit"` / `"hyperliquid"`. |
-
- - **Feed quality differs by venue.** Binance is **sampled** (≤1 liquidation/symbol/second) so
- its values are a **lower bound** and undercount most during the cascades you care about;
- Bybit and Hyperliquid are **complete** (exact totals). Set a lower threshold on Binance for
- the same real-world event.
- - **Windows decay to zero** once a cascade subsides, which re-arms the condition — so a
- `repeat` plan fires **once per cascade**, not once ever.
- - `total_pct_oi_*` magnitudes are **small**: ~0.1%/hr is already heavy for a major; start
- around `0.5`–`1` (percent), not raw fractions. HIP-3 symbols are **rejected** here too.
-
- ```json
- { "source": "liquidation", "method": "total_usd_5m", "args": { "ticker": "ETH:BYBIT" }, "operator": "crosses_above", "value": 500000 }
- ```
-
- Full method tables, feed-quality table, and examples:
- [Funding and Liquidations](https://docs.elfa.ai/auto/funding-and-liquidations).
-
- **Market-sentiment source (`fear_greed`) — Crypto Fear & Greed Index:**
-
- Trigger on the market-wide Crypto Fear & Greed Index. This is a **keyless** source — one
- global reading, no per-market identity — so its methods take **no ticker**. Pass
- `args: {}`.
-
- | Method | Args | Returns | Operators | Description |
- |---|---|---|---|---|
- | `value` | _(none)_ | number | all 8 | Index score, integer `0`–`100` (0 = extreme fear, 100 = extreme greed). |
- | `classification` | _(none)_ | enum | `==` / `!=` | Bucket label (see below). |
-
- - `classification` value ∈ `"Extreme fear"` (`0–19`), `"Fear"` (`20–39`), `"Neutral"`
- (`40–59`), `"Greed"` (`60–79`), `"Extreme greed"` (`80–100`). Bands map to fixed `value`
- ranges — **prefer `value`** for a precise threshold.
- - Updates ~every 15 min. A `crosses_*` leaf needs a prior reading, so it can only fire on the
- **second** reading after activation. No dynamic values.
-
- ```json
- { "source": "fear_greed", "method": "value", "args": {}, "operator": "crosses_below", "value": 20 }
- ```
-
- **Mindshare source (`mindshare`) — social attention to one token or stock:**
-
- Trigger on an asset's share of its market's attention, its rank, how fast that share is
- changing, and whether it is hot: the numbers `GET /v3/market/{market}/mindshare` (API v3)
- returns. One asset per condition; several → several conditions under `OR` (there is no "any
- asset turns hot" condition). Write the full form. A match carries the condition, the resolved
- `assetId` and the observed value — **no post, no event, no name**; read the board or
- `/v3/events` for the reason behind a move. Full reference: https://docs.elfa.ai/auto/mindshare
-
- | Method | Args | Returns | Operators | Description |
- |---|---|---|---|---|
- | `share_1h`, `share_4h`, `share_24h`, `share_7d` | `asset`, `market?` | number | all 8 | Share of the market's mentions in the period, weighted by who posted, `0`–`100` (`5` = 5%). The endpoint's `share`. |
- | `rank_1h` … `rank_7d` | `asset`, `market?` | number | all 8 | Place by share, `1` = most attention. The endpoint's `rank`. |
- | `change_pct_1h` … `change_pct_7d` | `asset`, `market?` | number | all 8 | Change of its share against the previous period, as a percent of itself (`100` = doubled). The endpoint's `changePct`. |
- | `hot_4h`, `hot_24h`, `hot_7d` | `asset`, `market?` | boolean | `==` / `!=` | Heating up in the last hour, as the endpoint's `hot` defines it. No `hot_1h`. |
-
- - `asset` (required): an id from `GET /v3/entities`, a ticker or a name, stored as `assetId`
- (the asset's family id, the `id` the mindshare endpoint returns). `market` (`crypto` or
- `equity`): set it whenever you know the asset class — without it, a ticker that names both a
- coin and a stock goes to whichever leads its own market (Coinbase is `"asset": "COIN",
- "market": "equity"`). Projects, people and organisations are rejected. When other assets
- match just as closely, the market leader wins and Validate warns; only a tie that nothing
- breaks is rejected, as "Ambiguous asset".
- - Periods end at the last whole five minutes. Use `24h` when the user gives no period. "Enters
- the top N" is `rank_<p>` `crosses_below` N; "drops out of the top N" is `crosses_above` N + 1.
- No dynamic values.
- - Off the board, readings stay numeric: no mentions in the period reads share `0`, rank
- `1000000`, `change_pct` `-100` (or `0` if it had no share before either) and hot `false`; a
- new entrant's `change_pct` is `0`, so catch a first appearance with `share_*` or `rank_*`.
- - Runtime: readings arrive every five minutes. A condition fires on the reading where it turns
- true (an already-true one about five minutes after creation); `crosses_*` needs two readings.
- With `repeat`, it fires again only after turning false and true again. An asset with no
- mentions in 7 days validates and waits.
- - Preview: read `GET /v3/market/crypto/mindshare?period=24h&limit=50` and set the threshold
- beyond the current values, so the plan does not fire on its first reading.
- - `match.observedValue` is the reading that fired (a `hot_*` reading is `1` or `0`).
-
- ```json
- { "source": "mindshare", "method": "rank_24h", "args": { "asset": "SOL", "market": "crypto" }, "operator": "crosses_below", "value": 10 }
- ```
-
- **Follow-graph sources (`x_follow_received`, `x_follow_made`, `x_follow_overlap`) — new X
- follow edges:**
-
- Three sources ask three questions about the same data. The account you name is always the
- **watched** account; the method describes the account on the other end of the edge.
-
- | Source | Method | Args | Returns | Operators | Measures |
- |---|---|---|---|---|---|
- | `x_follow_received` | `follower_count` | `account`, `window?`, `period?` | number | `>` / `>=` | Follower count of the account that followed the watched account. |
- | `x_follow_made` | `followee_follower_count` | `account`, `window?`, `period?` | number | `>` / `>=` | Follower count of the account the watched account followed. |
- | `x_follow_overlap` | `distinct_followers` | `accounts` (2–25), `window?`, `period?` | number | `>` / `>=` | How many watched accounts followed the same account. |
-
- Only `>` and `>=` are accepted — no `crosses_*`, no dynamic field-vs-field values. Accounts are
- bare X usernames (1–15 letters, digits or underscores); a leading `@` is stripped, routing is
- lowercased, and duplicates in `accounts` collapse to one.
-
- | Arg | Values | Default |
- |---|---|---|
- | `window` | `6h`, `12h`, `24h`, `48h`, `72h`, `7d` | `24h` (`72h` for `x_follow_overlap`) |
- | `period` | `1h`, `2h`, `4h`, `8h`, `12h`, `24h`, `1d`, `7d` | `1h` |
-
- `window` is the lookback over **detection** time; `period` is the minimum interval between
- reads. A `period` longer than `window` is silently clamped down to `window` rather than
- rejected.
-
- ```json
- { "source": "x_follow_received", "method": "follower_count", "args": { "account": "elfa_ai", "window": "24h" }, "operator": ">", "value": 10000 }
- ```
-
- ```json
- { "source": "x_follow_overlap", "method": "distinct_followers", "args": { "accounts": ["pantheracapital", "a16z", "paradigm"], "window": "72h" }, "operator": ">=", "value": 2 }
- ```
-
- On `x_follow_received` / `x_follow_made`, `>= 0` matches every detected follow and a higher
- value narrows to accounts with a real audience. On `x_follow_overlap` the `value` floor is `2`
- and it cannot exceed the size of `accounts` (one below it with `>`) — either is rejected at
- create time.
-
- Coverage is bounded and the limits change how you should write the plan: only follows by
- accounts Elfa reads are seen — the accounts it monitors, plus the per-day accounts your own
- queries watch with `x_follow_made` or `x_follow_overlap` — data is collected on a recurring
- check rather than streamed so alerts are **not real time** (a per-day account's follows are
- read once a day, so a new follow can take up to a day to fire), at most 500 matches land per
- evaluation, and unfollows and re-follows never fire. A quiet plan means no account Elfa reads
- made that follow — it does not prove nobody did.
-
- `x_follow_made` and `x_follow_overlap` read the watched account's own following list and can
- watch any public X account. Accounts Elfa already monitors for follows cost nothing extra; any
- other account needs a paid API key and is billed for each day its follows are read.
- `x_follow_received` works for any public account at no extra cost, because it is the followers
- that must be monitored. Validate is free and tells you which applies.
-
- | Watched account (`x_follow_made`, `x_follow_overlap`) | Free API key | Paid API key | x402 |
- |---|---|---|---|
- | Monitored for follows | Included | Included | Included |
- | Any other public account | Rejected (`paid_plan_required`) | Billed per day | Rejected |
-
- - `followAccounts[]` (Validate and Create): one entry per watched account — `path` (counting
- leaf conditions in order: `conditions[1]` is the second, whatever `AND`/`OR` groups hold
- them, unlike `tweetAccounts[].path`, which includes the group; an `x_follow_overlap`
- condition has one entry per account, all with the same `path`), `username` (as stored:
- lowercase, without `@`), `accountSourceRefId`, `billing` (`included` or `per_day`) and
- `creditsPerDay` (`per_day` only: `1`). Match entries on `path` and `username`. Absent when
- the query has no such condition, on x402, or when an account was rejected.
- - Cost: each UTC day on which a `per_day` account's follows are read costs `creditsPerDay`,
- whether or not the query fires; follows are read once a day, and a day with no read is not
- billed. The first read, when a query starts watching the account, is free. A key pays once
- per account and day, however many of its queries watch it. Not in `estimatedCredits` or
- `x-elfa-credits` — check `/v2/key-status`. `billing` is a snapshot. Example: 3 `per_day`
- accounts for 30 days cost about 90 credits, even if the query never fires.
- - If a charge is refused (credits exhausted, spend cap or monthly limit reached with overage
- off, or the key is no longer on a paid tier), every query of that key watching the account
- stops: Poll returns `status: "failed"` with `failure.code` `SUBSCRIPTION_CREDITS_EXHAUSTED`
- and `source: "billing"`, and the lifecycle notification (`status: "stopped"`) carries the
- same text in `reasonUser`. Top up, upgrade or raise the limit, then create the query again.
-
- Each matched account is a distinct event: without `repeat` the first match triggers the plan
- once, and with `repeat` it fires once per distinct matched account (`cooldown: "0"` for
- "notify me about every one"). A matched account will not fire again while it stays inside
- `window`, and a follow condition reports no value until its first scheduled evaluation, so
- there is no preview before execution. Builder Chat cannot author these yet; build them against
- Validate/Create directly. Read [Follow Graph](https://docs.elfa.ai/auto/account-follows) before
- using these sources.
-
- **Supported operators:** `>`, `<`, `>=`, `<=`, `==`, `!=`, `crosses_above`, `crosses_below`
-
- **Dynamic comparisons:** `value` can reference another live data source instead of a literal:
-
- ```json
- {
- "source": "price",
- "method": "current",
- "args": { "symbol": "ETH", "exchange": "hyperliquid" },
- "operator": "crosses_above",
- "value": {
- "source": "ta",
- "method": "bbands_upper",
- "args": { "symbol": "ETH", "timeframe": "4h", "exchange": "hyperliquid" }
- }
- }
- ```
-
- #### Copy-paste query templates
-
- **1) Breakout alert (webhook):**
-
- ```json
- {
- "title": "BTC breakout above 100k",
- "description": "Notify runner when BTC spot trades above the 100k level.",
- "conditions": { "AND": [{ "source": "price", "method": "current", "args": { "symbol": "BTC", "exchange": "hyperliquid" }, "operator": ">", "value": 100000 }] },
- "actions": [{ "stepId": "step_1", "type": "webhook", "params": { "url": "https://your-endpoint.example/events" } }],
- "expiresIn": "24h"
- }
- ```
-
- **2) Downside guardrail (telegram_bot):**
-
- ```json
- {
- "title": "ETH downside guardrail (< 2500)",
- "description": "Risk-off alert: flag if ETH breaks below 2500. The notification body is auto-composed from title + description + trigger context.",
- "conditions": { "AND": [{ "source": "price", "method": "current", "args": { "symbol": "ETH", "exchange": "hyperliquid" }, "operator": "<", "value": 2500 }] },
- "actions": [{ "stepId": "step_1", "type": "telegram_bot", "params": { "botToken": "<TELEGRAM_BOT_TOKEN>", "chatId": "<TELEGRAM_CHAT_ID>" } }],
- "expiresIn": "24h"
- }
- ```
-
- **3) Triggered LLM analysis:**
-
- ```json
- {
- "title": "BTC > 100k — LLM review",
- "description": "On BTC breakout, run an LLM pass to decide the next follow-up action.",
- "conditions": { "AND": [{ "source": "price", "method": "current", "args": { "symbol": "BTC", "exchange": "hyperliquid" }, "operator": ">", "value": 100000 }] },
- "actions": [{
- "stepId": "step_1",
- "type": "llm",
- "params": { "action": "chat", "message": "Analyze trigger context and return the next follow-up action." }
- }],
- "expiresIn": "24h"
- }
- ```
-
- **4) Multi-symbol confirmation:**
-
- ```json
- {
- "title": "BTC + ETH joint breakout",
- "description": "Confirm majors moving together before acting.",
- "conditions": {
- "AND": [
- { "source": "price", "method": "current", "args": { "symbol": "BTC", "exchange": "hyperliquid" }, "operator": ">", "value": 100000 },
- { "source": "price", "method": "current", "args": { "symbol": "ETH", "exchange": "hyperliquid" }, "operator": ">", "value": 3500 }
- ]
- },
- "actions": [{ "stepId": "step_1", "type": "notify", "params": { "message": "BTC and ETH confirmation fired" } }],
- "expiresIn": "24h"
- }
- ```
-
- **5) Dynamic comparison (price vs Bollinger Band):**
-
- ```json
- {
- "title": "ETH breakout above 4h upper BBand",
- "description": "Dynamic: fire when ETH price crosses above its own 4h upper Bollinger Band.",
- "conditions": {
- "AND": [{
- "source": "price", "method": "current", "args": { "symbol": "ETH", "exchange": "hyperliquid" },
- "operator": "crosses_above",
- "value": { "source": "ta", "method": "bbands_upper", "args": { "symbol": "ETH", "timeframe": "4h", "exchange": "hyperliquid" } }
- }]
- },
- "actions": [{ "stepId": "step_1", "type": "webhook", "params": { "url": "https://your-endpoint.example/events" } }],
- "expiresIn": "2d"
- }
- ```
-
- **6) Scheduled cron check:**
-
- ```json
- {
- "title": "Every 4h: portfolio sweep",
- "description": "Recurring LLM pass every 4h.",
- "conditions": { "AND": [{ "source": "cron", "method": "every", "args": { "period": "4h" }, "operator": "==", "value": true }] },
- "actions": [{
- "stepId": "step_1",
- "type": "llm",
- "params": { "action": "chat", "message": "Summarize BTC/ETH/SOL context and flag any risk shifts." }
- }],
- "expiresIn": "3d"
- }
- ```
-
- **7) LLM-evaluated narrative condition:**
-
- ```json
- {
- "title": "AI narrative shift watcher",
- "description": "Fire when dominant AI-token narrative shifts based on news + X sentiment.",
- "conditions": {
- "AND": [{
- "source": "llm", "method": "athena_condition",
- "args": { "query": "Has the dominant narrative around AI-sector tokens shifted materially in the last 6 hours?", "period": "1h" },
- "operator": "==", "value": true
- }]
- },
- "actions": [{ "stepId": "step_1", "type": "telegram_bot", "params": { "botToken": "<TELEGRAM_BOT_TOKEN>", "chatId": "<TELEGRAM_CHAT_ID>" } }],
- "expiresIn": "2d"
- }
- ```
-
- **8) Signal — X/Twitter Post (account-anchored):**
-
- ```json
- {
- "title": "Binance Alpha listing post watcher",
- "description": "Fire when cz_binance posts that Binance Alpha is listing a new token so the runner can review follow-through.",
- "conditions": {
- "AND": [{
- "source": "tweet",
- "args": {
- "username": "cz_binance",
- "text": "Binance Alpha is listing a new token",
- "minConfidence": 80
- }
- }]
- },
- "actions": [{ "stepId": "step_1", "type": "webhook", "params": { "url": "https://your-runner.example/auto/events" } }],
- "expiresIn": "24h"
- }
- ```
-
- **9) Events (news-driven catalyst):**
-
- ```json
- {
- "title": "ETH ETF approval event watcher",
- "description": "Fire when an event reports a spot ETH ETF approval so I can kick off a post-event playbook.",
- "conditions": {
- "AND": [{
- "source": "events",
- "args": {
- "entity": "ETH",
- "eventType": "regulatory",
- "text": "the SEC approves a spot ETH ETF",
- "minConfidence": 80
- }
- }]
- },
- "actions": [{ "stepId": "step_1", "type": "notify", "params": { "message": "Event fired: spot ETH ETF approval" } }],
- "expiresIn": "168h"
- }
- ```
-
- **10) Prediction Market — Kalshi (YES probability crossing):**
-
- ```json
- {
- "title": "Kalshi YES crosses 60%",
- "description": "Fire when the market's implied YES probability crosses up through 60%, signalling the market now expects this outcome.",
- "conditions": { "AND": [{ "source": "kalshi", "method": "yes_price", "args": { "ticker": "KXBTC-26APR0803-T77799.99" }, "operator": "crosses_above", "value": 0.6 }] },
- "actions": [{ "stepId": "step_1", "type": "webhook", "params": { "url": "https://your-runner.example/auto/events" } }],
- "expiresIn": "48h"
- }
- ```
-
- **11) Prediction Market — Kalshi (settlement recap via LLM):**
-
- ```json
- {
- "title": "Kalshi market resolved YES — recap",
- "description": "When the market settles with a YES result, generate a concise recap of what happened and why.",
- "conditions": { "AND": [{ "source": "kalshi", "method": "result", "args": { "ticker": "KXBTC-26APR0803-T77799.99" }, "operator": "==", "value": "yes" }] },
- "actions": [{ "stepId": "step_1", "type": "llm", "params": { "action": "chat", "message": "This Kalshi market just resolved YES. Write a concise recap explaining what resolved and what to watch next." } }],
- "expiresIn": "48h"
- }
- ```
-
- **12) Prediction Market — Polymarket (outcome-token price crossing):**
-
- ```json
- {
- "title": "Polymarket outcome crosses 60%",
- "description": "Fire when the outcome token's last traded price crosses up through 0.60, signalling the market now expects this outcome.",
- "conditions": { "AND": [{ "source": "polymarket", "method": "price", "args": { "ticker": "115556263888245616435851357148058235707004733438163639091106356867234218207169" }, "operator": "crosses_above", "value": 0.6 }] },
- "actions": [{ "stepId": "step_1", "type": "webhook", "params": { "url": "https://your-runner.example/auto/events" } }],
- "expiresIn": "48h"
- }
- ```
-
- **13) Recurring trigger (`repeat`):**
-
- Re-fire on the plan's own condition instead of a fixed cron cadence — notify every time BTC
- crosses down through 60k, rate-limited to once per hour, capped at 10 fires. Note the
- safer-pattern choices: `crosses_below` (reacts to the crossing edge, not a value sitting
- below the line) plus a `1h` cooldown and a low `maxTriggers` ceiling.
-
- ```json
- {
- "title": "Notify when BTC crosses below 60k",
- "description": "Recurring risk alert: fire each time BTC price crosses down through 60000, no more than once per hour, up to 10 times.",
- "conditions": { "AND": [{ "source": "price", "method": "current", "args": { "symbol": "BTC", "exchange": "hyperliquid" }, "operator": "crosses_below", "value": 60000 }] },
- "actions": [{ "stepId": "step_1", "type": "notify", "params": { "message": "BTC dipped below 60k again" } }],
- "expiresIn": "7d",
- "repeat": { "cooldown": "1h", "maxTriggers": 10 }
- }
- ```
-
- **14) Calendar schedule (`cron.schedule`):**
-
- Run on a wall-clock schedule — every weekday at 09:00 New York time. Direct EQL/API path
- only (Builder Chat support pending).
-
- ```json
- {
- "title": "Weekday 9am NY: market open recap",
- "description": "Every weekday at 09:00 America/New_York, run an LLM market-open recap.",
- "conditions": { "AND": [{ "source": "cron", "method": "schedule", "args": { "expression": "0 9 * * 1-5", "timezone": "America/New_York" }, "operator": "==", "value": true }] },
- "actions": [{ "stepId": "step_1", "type": "llm", "params": { "action": "chat", "message": "Give a concise market-open recap for BTC/ETH/SOL and flag overnight risk shifts." } }],
- "expiresIn": "7d"
- }
- ```
-
- **15) Funding flip (`funding`):**
-
- Fire when Binance BTC funding turns negative — shorts start paying longs (crowded-short
- signal). Note the composite `SYMBOL:EXCHANGE` ticker and venue-comparable `annualized_rate`.
-
- ```json
- {
- "title": "BTC funding flips negative",
- "description": "Shorts are now paying longs on Binance BTC — a crowded-short signal worth reviewing.",
- "conditions": { "AND": [{ "source": "funding", "method": "annualized_rate", "args": { "ticker": "BTC:BINANCE" }, "operator": "crosses_below", "value": 0 }] },
- "actions": [{ "stepId": "step_1", "type": "notify", "params": { "message": "BTC funding on Binance flipped negative" } }],
- "expiresIn": "7d"
- }
- ```
-
- **16) Liquidation cascade (`liquidation` + `repeat`):**
-
- Fire each time ETH liquidations cross $500k in a 5-minute window. Windows decay to zero once
- a cascade subsides, so with `repeat` this fires **once per cascade**.
-
- ```json
- {
- "title": "ETH liquidation cascade (5m > $500k)",
- "description": "Alert on each distinct ETH liquidation cascade on Bybit, capped at 10 alerts.",
- "conditions": { "AND": [{ "source": "liquidation", "method": "total_usd_5m", "args": { "ticker": "ETH:BYBIT" }, "operator": "crosses_above", "value": 500000 }] },
- "actions": [{ "stepId": "step_1", "type": "webhook", "params": { "url": "https://your-runner.example/auto/events" } }],
- "expiresIn": "7d",
- "repeat": { "cooldown": "1h", "maxTriggers": 10 }
- }
- ```
-
- **17) Catalyst → equity/index alert (HIP-3):**
-
- The prediction-market-to-perp bridge: when a Fed-decision market reprices, alert your runner
- on the S&P perp — including overnight and at weekends, when cash equities are closed.
- `xyz:SP500` is a HIP-3 market on Hyperliquid. Resolve a real open Kalshi ticker first — don't
- guess.
-
- ```json
- {
- "title": "Fed cut odds → S&P alert",
- "description": "When the market prices a cut as more likely than not, notify my runner to review the S&P perp.",
- "conditions": { "AND": [{ "source": "kalshi", "method": "yes_price", "args": { "ticker": "<an open Kalshi Fed-decision market ticker>" }, "operator": "crosses_above", "value": 0.6 }] },
- "actions": [{ "stepId": "step_1", "type": "webhook", "params": { "url": "https://your-runner.example/auto/events" } }],
- "expiresIn": "168h",
- "repeat": { "cooldown": "4h", "maxTriggers": 3 }
- }
- ```
-
- **18) Asset enters the mindshare top 10 (`mindshare`):**
-
- A rank falls as the asset climbs, so "enters the top 10" is `crosses_below 10`: its rank goes
- from 11 or more to 10 or less.
-
- ```json
- {
- "title": "SOL enters the mindshare top 10",
- "description": "Notify when SOL enters the top 10 crypto assets by 24h mindshare.",
- "conditions": { "AND": [{ "source": "mindshare", "method": "rank_24h", "args": { "asset": "SOL", "market": "crypto" }, "operator": "crosses_below", "value": 10 }] },
- "actions": [{ "stepId": "step_1", "type": "notify", "params": { "message": "SOL entered the top 10 by 24h mindshare" } }],
- "expiresIn": "7d"
- }
- ```
-
- **19) Every long a caller opens (`calls`):**
-
- Fires each time one X account opens a long, up to 50 times, as soon as Elfa records the call.
-
- ```json
- {
- "title": "3PeaksTrading goes long",
- "description": "Notify each time 3PeaksTrading opens a long.",
- "conditions": { "AND": [{ "source": "calls", "args": { "handle": "3PeaksTrading", "direction": "long", "action": "open" } }] },
- "actions": [{ "stepId": "step_1", "type": "notify", "params": { "message": "3PeaksTrading opened a long" } }],
- "expiresIn": "720h",
- "repeat": { "cooldown": "0", "maxTriggers": 50 }
- }
- ```
-
- #### Poll response shape
-
- > **All query and execution identifiers are UUIDs** (e.g.
- > `a12d20ff-6cb2-433e-afed-cc2e6a0380b6`) — **not** prefixed strings like `q_123` or
- > `exec_123`. The short `q_123` forms used elsewhere in this skill are placeholders for
- > readability only; never construct or pattern-match on a prefixed ID.
-
- `GET /v2/auto/queries/{queryId}` (and the x402 `POST` equivalent) returns `queryId`, `status`,
- an optional `credits`, `latestEvaluation` (current state), and `executions` (what actually
- fired). A query that failed with a recorded reason also carries `failure` (`code`, `message`,
- which may be `null`, and `source`); `source: "billing"` means a per-post X account or per-day
- follow account charge was refused, so top up, upgrade or raise the limit, then create the plan
- again.
-
- **Distinguish the two runtime surfaces — this is the most common polling mistake:**
-
- | Surface | Answers | Read for the value |
- |---|---|---|
- | `latestEvaluation.conditionStates[]` | "what is true **right now**" | `currentValue` vs `targetValue` |
- | Auto Trigger Context on an execution | "what **fired**, and why" | `trigger.matchedConditions[].match.observedValue` |
-
- **`latestEvaluation`** is `null` until the query has evaluated at least once:
-
- ```json
- {
- "evaluatedAt": "2026-04-01T12:00:00.000Z",
- "conditionStates": [
- {
- "index": 0,
- "source": "price",
- "method": "current",
- "args": { "symbol": "BTC" },
- "currentValue": 97250.5,
- "targetValue": 100000,
- "operator": "<",
- "isMet": true,
- "lastUpdated": "2026-04-01T12:00:00.000Z"
- }
- ],
- "wouldTriggerNow": true,
- "matchingConditions": 1,
- "totalConditions": 1
- }
- ```
-
- `conditionStates[]` entries may also carry `reason`, and for `llm` conditions
- `conversationSessionId`, `runId`, and `cacheHit`. Use `matchingConditions` / `totalConditions`
- to show partial progress ("2 of 3 conditions met") without re-deriving it yourself.
-
- **Auto Trigger Context** appears on execution records once the query has fired. It is the
- **stable public representation of why the query fired** — raw stored trigger payloads are not
- part of the public API, so do not depend on them.
-
- ```json
- {
- "id": "a12d20ff-6cb2-433e-afed-cc2e6a0380b6",
- "queryId": "76e2e824-fc47-4d60-99b1-c227fbd2d3f5",
- "type": "notification",
- "status": "success",
- "details": { "notification": { "channel": "webhook" } },
- "triggerTime": "2026-04-01T12:00:00.000Z",
- "conditionsMet": 1,
- "trigger": {
- "type": "price",
- "time": "2026-04-01T12:00:00.000Z",
- "matchedConditions": [
- {
- "condition": {
- "source": "price",
- "method": "current",
- "args": { "symbol": "BTC" },
- "operator": "<",
- "value": 100000
- },
- "match": { "observedValue": 97250.5 }
- }
- ]
- },
- "createdAt": "2026-04-01T12:00:01.000Z"
- }
- ```
-
- - `trigger.matchedConditions[].condition.value` is the **threshold**;
- `.match.observedValue` is the **value seen at trigger time**. Report both when explaining a
- fire to a user.
- - For Signal sources (`tweet` / `telegram`), `match` also carries provenance — `url`,
- `mentionedAt`, `confidence`, `accountHandle`, and for Telegram `chatUsername`, `chatTitle`,
- `messageId` / `messageIds`, `groupId`. Use `url` to link the user straight to the post that
- fired the plan.
- - For `events`, `match` carries the event instead — `eventId` (its `/v3/events` id, not the
- delivery id), `title`, its classification, `entities`, `analyzedAt`, and `confidence` when
- `text` was checked; there is no `url`. Fetch the rest with `GET /v3/events?eventIds=<eventId>`.
- - `details` is keyed by action kind (`trade`, `notification`, `llm`, `auto`, `script`).
- - Failed executions carry `error` `{ code, message }`, e.g.
- `LLM_ACTION_UPSTREAM_ERROR`.
-
- Auto Trigger Context is also included in outbound delivery payloads and script trigger
- context — so a webhook receiver can explain a fire without a follow-up poll.
-
- Use polling for debugging or backfills. For production delivery, prefer webhook or SSE
- notifications. Store `sessionId` values from executions so you can fetch full LLM analysis
- only when needed via `GET /v2/auto/queries/:queryId/sessions/:sessionId` — for example when a
- callback webhook with `includeAnalysis` reports `analysisTruncated: true`.
-
- #### Notifications — delivery channels
-
- After a query triggers, Auto delivers events via one of three channels:
-
- | Channel | Best for | Setup |
- |---|---|---|
- | **Webhook** | Production agent automation | `action.type = "webhook"` with signature verification + queue/worker |
- | **Telegram** | Fast human-readable alerts | `action.type = "telegram_bot"` with `params.botToken` + `params.chatId` (direct), or webhook→bot relay for custom formatting |
- | **SSE Stream** | Real-time event consumers; **always available regardless of the chosen action** | Two streams, both free and `GET`, both using the **same auth as query creation**: `GET /v2/auto/queries/stream` (account-wide — every query on one connection, **default**, API-key only) or `GET /v2/auto/queries/{queryId}/stream` (single query; the x402 / agent-identity option). See [SSE frame format](#sse-frame-format). |
-
- **Query `title` and `description` in notifications:** both fields are embedded in every
- outbound notification (Telegram, webhook, SSE). Recipients often see an alert hours or days
- after the query was set up — these fields are what make it clear **what** fired and **why
- it was set up**. Always set them.
-
- #### Notification payload (what Auto actually sends)
-
- Auto's outbound **webhook** notification object has this shape:
-
- ```json
- {
- "id": 12345,
- "type": "athena_query_notify_only",
- "category": "alerts",
- "title": "BTC breakout above 100k",
- "body": "price > threshold",
- "data": { "queryId": "q_123" },
- "priority": "high",
- "createdAt": "2026-04-01T12:00:00.000Z"
- }
- ```
-
- `id` is the numeric notification ID; correlate back to a query via `data.queryId`. `title`
- and `body` come from the query's `title` / `description` plus trigger context. **SSE delivery
- uses a different, more compact frame** — see [SSE frame format](#sse-frame-format) below.
-
- #### Canonical event payload contract (internal normalization)
-
- It's useful to normalize incoming events (webhook, Telegram relay, SSE) to a single internal
- shape so downstream processing is channel-agnostic. This is a **recommended runner-side
- convention**, not the wire format Auto sends (see the payload above):
-
- ```json
- {
- "version": "1.0",
- "eventType": "query.triggered",
- "eventId": "evt_01J...",
- "timestamp": "2026-04-01T12:00:00.000Z",
- "queryId": "q_123",
- "channel": "webhook",
- "trigger": {
- "symbol": "BTC",
- "reason": "price > threshold"
- },
- "evaluation": { "triggered": true },
- "action": { "type": "webhook" }
- }
- ```
-
- **Webhook request headers** (what your receiver must read):
-
- | Header | Purpose |
- |---|---|
- | `X-Auto-Event-Id` | Unique event ID for deduplication |
- | `X-Auto-Signature-Timestamp` | Unix seconds for replay-window check |
- | `X-Auto-Signature` | `v1=<hex_hmac_sha256>` — verify against raw body |
-
- #### SSE frame format
-
- There are two SSE streams; both are **free**, both are `GET`, and both
- use the **same auth used to create the query** (`x-elfa-api-key` for API-key queries, the x402
- secret for x402 queries — *not* limited to `x-elfa-api-key`). For minimal setup, attach a
- `notify` action with a `message` — those notifications are retrievable only via SSE or poll.
-
- | Endpoint | Scope | Use when |
- |---|---|---|
- | `GET /v2/auto/queries/stream` | Every query you own, on one connection | **Default.** Correlate events by the payload's `queryId`. **API-key only** — agent identities (`x-elfa-agent-secret`) and x402 get `403`. |
- | `GET /v2/auto/queries/{queryId}/stream` | A single query | You only care about one query, or you are on x402 / an agent identity. |
-
- Prefer the account-wide stream over one connection per query. Open it **after** creating at
- least one query — it returns `410` when you have none.
-
- Both streams emit the same frames — `event: notification`, `id` = the **notification outbox
- event UUID** (not a query ID), and a `: keep-alive` comment every 15s. Events are
- **live-only**: there is no replay and `Last-Event-ID` is ignored.
-
- ```
- id: 9df34377-4d82-4b3c-a016-b0ba27556aa1
- event: notification
- data: {"status":"triggered","title":"Plan Triggered","body":"BTC RSI Breakout","queryId":"q_123","executionId":"2dbf0d70-a85f-4f67-9bd3-876e8fd89f86","triggerTime":"2026-04-01T12:00:00.000Z","conditionsMet":2,"timestamp":1774328400000}
- ```
-
- `status` is one of `triggered`, `stopped`, `ended`, or `update`. `queryId` is a **top-level**
- field on the payload — use it to correlate with poll results via `/v2/auto/queries/{queryId}`.
- The payload also carries execution context when present (`executionId`, `triggerTime`,
- `conditionsMet`, `autoDetails`).
-
- **Stream lifecycle.** Each stream closes once there is nothing left to deliver, emitting a
- final `end` event:
-
- | | `/queries/stream` | `/queries/{queryId}/stream` |
- |---|---|---|
- | `410` on connect | You have no active queries | The query is already terminal and drained |
- | Closes when | No active query and no pending execution, held 30s | The query reaches a terminal state and its notifications drain |
- | Final frame | `data: {"code":"USER_STREAM_CLOSED"}` | `data: {"code":"QUERY_STREAM_CLOSED","status":"<terminalStatus>","queryId":"<uuid>"}` |
-
- A `recurring` (`repeat`) query is never terminal, so its stream stays open across triggers. If
- a stream fails after it has started, it emits `event: error` with `{"code":"STREAM_UNAVAILABLE"}`.
-
- **Telegram relay job format** (when transforming webhook → Telegram Bot API):
-
- ```json
- {
- "eventId": "evt_01J...",
- "queryId": "q_123",
- "channel": "telegram",
- "chatId": "<CHAT_ID>",
- "text": "BTC trigger fired: price > threshold",
- "priority": "high"
- }
- ```
-
- #### Webhook signature verification
-
- **Set an explicit `signingSecret` on every production webhook.** It is a `webhook` action
- param, write-only — Elfa stores it for delivery signing and never returns it in query,
- execution, webhook, SSE, or LLM-callback payloads. **Without it, signature headers may be
- absent entirely** depending on access mode and server configuration, so a receiver that
- requires signatures will reject every delivery.
-
- ```json
- {
- "stepId": "step_1",
- "type": "webhook",
- "params": {
- "url": "https://your-runner.example/auto/events",
- "signingSecret": "<openssl rand -hex 32>",
- "allNotifications": true
- }
- }
- ```
-
- LLM callback webhooks take the same destination params under `params.callback.action.params`,
- plus `includeAnalysis` (default `false`). Without it, the callback tells your endpoint the query
- fired, not what the `llm` step wrote. Set it to `true` and the payload also carries the answer as
- `analysisText` (at most 32,000 characters) and `analysisTruncated`. Validate rejects
- `includeAnalysis` on a top-level `webhook`, and `telegram_bot` callbacks never carry the answer.
- See [LLM Analysis in a Callback Webhook](https://docs.elfa.ai/auto/notifications#llm-analysis-in-a-callback-webhook).
-
- **Which secret to use — two distinct secrets, never interchange them:**
-
- | Secret | Direction | Purpose |
- |---|---|---|
- | `x-elfa-api-key` | you → Elfa | Authenticates your client |
- | `webhook.params.signingSecret` | Elfa → you | Signs Elfa's outbound webhook *deliveries* to your server |
-
- Generate a separate high-entropy secret per webhook action (`openssl rand -hex 32`). Do **not**
- reuse the API key or the x402 agent secret here.
-
- Signing inputs:
-
- ```
- expected = HMAC_SHA256(signingSecret, timestamp + "." + eventId + "." + rawBody)
- ```
-
- > **The `signingSecret` is the HMAC key directly — do not hash it first.** Earlier revisions
- > of this skill documented `HMAC_SHA256(SHA256(secret), ...)`. That is wrong for explicit
- > signing secrets and will fail verification.
- >
- > **Legacy x402/agent fallback:** if an x402/agent webhook omits `signingSecret`, older
- > deliveries may be signed with `SHA256(x-elfa-agent-secret)` as the key. Treat that as a
- > legacy fallback, not a supported setup — set an explicit `signingSecret` instead.
-
- **Node.js verification:**
-
- ```typescript
- import crypto from "crypto";
-
- export function verifyAutoWebhook(
- signingSecret: string,
- rawBody: string,
- signatureHeader: string,
- timestamp: string,
- eventId: string,
- ): boolean {
- if (!signatureHeader?.startsWith("v1=")) return false;
- const givenHex = signatureHeader.slice(3);
- if (!/^[0-9a-f]{64}$/i.test(givenHex)) return false;
-
- const payload = `${timestamp}.${eventId}.${rawBody}`;
- const expectedHex = crypto
- .createHmac("sha256", signingSecret)
- .update(payload)
- .digest("hex");
-
- const given = Buffer.from(givenHex, "hex");
- const expected = Buffer.from(expectedHex, "hex");
- if (given.length !== expected.length) return false;
- return crypto.timingSafeEqual(given, expected);
- }
- ```
-
- Operational checklist:
- - Enforce a bounded replay window on `X-Auto-Signature-Timestamp` (reject drift >30s).
- - Deduplicate by `X-Auto-Event-Id` in durable storage.
- - Return `2xx` fast, then process asynchronously (queue + worker).
-
- #### Telegram bot setup (for `action.type: "telegram_bot"` or relay)
-
- 1. Open `@BotFather` in Telegram → `/newbot` → save bot token (treat as secret).
- 2. Send any message to the bot (or in a group where the bot is present).
- 3. `curl "https://api.telegram.org/bot<BOT_TOKEN>/getUpdates"` → read `message.chat.id`.
- 4. Send messages via:
-
- ```bash
- curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/sendMessage" \
- -H "Content-Type: application/json" \
- -d '{"chat_id": "<CHAT_ID>", "text": "Auto trigger fired for BTC RSI"}'
- ```
-
- **Direct chats, groups, and supergroups:** `telegram_bot` delivers to private chats, groups,
- and supergroups. **Channels are not supported** and are rejected at create time. Two gotchas
- for groups: (1) **group chat IDs are negative** (`-1001234567890`) — copy verbatim from
- `getUpdates`, including the leading `-`; (2) **the bot must be able to post** — at create time
- Auto checks the bot's membership/permissions, and if it can't post, create fails with
- `Telegram bot cannot send messages in this chat (check its permissions)`. If a group is later
- upgraded to a supergroup (new chat ID), Auto detects the migration, updates the stored ID, and
- retries — no action needed.
-
- #### Agent runner — reference architecture
-
- Auto handles query evaluation + event emission. Your runner handles event ingestion,
- verification, deduplication, strategy continuation, and audit logging.
-
- ```
- Auto Query
- → Auto Event Ingress (Webhook / SSE)
- → Signature Verification + Idempotency
- → Job Queue
- → Agent Decision Worker
- → Action Adapter (Telegram / Internal API / Order Router)
- → Logs + Metrics + Alerts
- ```
-
- **Core processing loop:**
-
- 1. Receive event (webhook or SSE frame).
- 2. Verify signature (webhook only).
- 3. Check idempotency — is `eventId` already processed?
- 4. Enqueue job and ACK `2xx` fast.
- 5. Worker resolves extra context (poll query, fetch LLM session).
- 6. Apply policy, decide next step.
- 7. Execute downstream action.
- 8. Record result for replay/debug.
-
- **Instruction envelope** — structured contract passed from ingress to worker:
-
- ```json
- {
- "eventId": "evt_123",
- "queryId": "q_123",
- "objective": "Handle trigger and decide next action",
- "allowedActions": ["notify", "fetch_session", "execute_adapter"],
- "constraints": {
- "maxExecutionSeconds": 30,
- "riskMode": "conservative"
- }
- }
- ```
-
- **Minimal TypeScript worker skeleton:**
-
- ```typescript
- type AutoJob = { eventId: string; queryId: string; raw: unknown };
- const queue: AutoJob[] = [];
- const processed = new Set<string>();
-
- function onWebhook(eventId: string, queryId: string, raw: unknown) {
- if (processed.has(eventId)) return; // idempotent
- queue.push({ eventId, queryId, raw });
- }
-
- async function workerLoop() {
- while (true) {
- const job = queue.shift();
- if (!job) { await new Promise((r) => setTimeout(r, 250)); continue; }
- // 1) Pull latest query/execution state if needed
- // 2) Fetch session details for llm actions
- // 3) Decide next action (policy + agent logic)
- // 4) Execute action (notify, relay, order adapter)
- processed.add(job.eventId);
- }
- }
- ```
-
- **Deployment patterns:**
-
- | Pattern | Best for |
- |---|---|
- | Single process (API + worker) | Early-stage prototypes |
- | API + queue + workers | Production reliability at scale |
- | Serverless consumer + queue worker | Spiky workloads with managed ops |
-
- **Local (Docker Compose) stack:**
-
- ```yaml
- version: "3.9"
- services:
- redis: { image: redis:7-alpine, ports: ["6379:6379"] }
- ingress:
- build: ./ingress
- environment:
- AUTO_SECRET: ${AUTO_SECRET}
- REDIS_URL: redis://redis:6379
- depends_on: [redis]
- ports: ["3000:3000"]
- worker:
- build: ./worker
- environment:
- AUTO_SECRET: ${AUTO_SECRET}
- REDIS_URL: redis://redis:6379
- ELFA_BASE_URL: https://api.elfa.ai/v2/auto
- depends_on: [redis]
- ```
-
- **Minimal environment contract** (same keys local + cloud):
-
- ```
- AUTO_SECRET=<event-signing secret>
- ELFA_BASE_URL=https://api.elfa.ai/v2/auto
- QUEUE_URL=<redis/sqs/pubsub endpoint>
- RUNNER_MODE=local|cloud
- ```
-
- **Reliability checklist:**
-
- - Store dedupe keys by `eventId` in durable storage
- - Retry policy with dead-letter queue
- - Keep webhook handler fast and non-blocking
- - Log decision input/output for every run
- - Health checks + alerting on worker lag
-
- **Recommended setup by stage:**
-
- | Stage | Pattern |
- |---|---|
- | Prototype | Auto Telegram + local worker |
- | Production | Auto webhook + queue + worker |
- | Real-time operations | Auto SSE + worker service |
-
- Full detail: [Notifications](https://docs.elfa.ai/auto/notifications) |
- [Agent Runner](https://docs.elfa.ai/auto/agent-runner)
-
- #### Notification troubleshooting
-
- | Symptom | Likely Cause | Fix |
- |---|---|---|
- | `400` / `401` when polling or streaming | Missing/invalid API key or auth headers | Send `x-elfa-api-key` |
- | Webhook signature mismatch | Signing wrong payload (not raw body), or hashing the secret before use | Key is the **raw** `signingSecret`, payload is `timestamp + "." + eventId + "." + rawBody`. Do not use `SHA256(secret)` as the key |
- | No signature headers on webhook deliveries | No explicit `signingSecret` set on the webhook action | Set `params.signingSecret` and recreate the query |
- | Duplicate downstream actions | No idempotency on event processing | Dedupe by `eventId` before enqueue/execute |
- | Event received but agent does nothing | Ingress processes inline and times out | ACK fast, push to queue, process in worker |
- | SSE disconnect/reconnect loops | No retry/backoff or unstable consumer | Add reconnect backoff + heartbeat monitoring |
- | Missing triggers after some time | Query expired or was cancelled | Poll status; check `expiresIn`, `status`, `latestEvaluation` |
- | "Why hasn't it fired yet?" | Condition is close but not met | Read `latestEvaluation.conditionStates[]` — compare `currentValue` vs `targetValue`, and `matchingConditions` vs `totalConditions`. Do **not** infer this from executions |
- | "Why did it fire?" | Need the value at trigger time, not the current value | Read the execution's `trigger.matchedConditions[]` — `.match.observedValue` vs `.condition.value`. `latestEvaluation` has since moved on |
- | Signature timestamp rejected | Runner clock skew | Sync clock (NTP); enforce bounded replay window |
- | `Telegram bot cannot send messages in this chat` at create | Bot isn't in the group, or the group revokes `can_send_messages` for it | Add the bot to the chat and grant send permission (or make it an admin), then retry |
- | Telegram create fails for a group | `chatId` missing the leading `-`, or the target is a channel | Group IDs are negative (`-1001234567890`); channels are not supported |
-
- #### Validation errors — next action table
-
- When `/v2/auto/queries/validate` (or Create) rejects, the error is almost always a phrasing
- issue, not a capability gap. Iterate on Validate instead of abandoning the query.
-
- | Error signal | What it means | Next action |
- |---|---|---|
- | `EQL_MISSING_ARG` | A required arg is absent (e.g. `period` on `ema`/`sma`, or `exchange` on any `price`/`ta` condition) | Add the missing arg from the TA Args Contract, re-validate |
- | Missing/invalid `exchange` on `price`/`ta` | Every `price`/`ta` condition needs `exchange` | Add `exchange` (`hyperliquid` or `binance`) to the condition `args`, re-validate |
- | `EQL_INVALID_ARG` / type errors | Wrong type (`"14"` instead of `14`) or unrecognized key (`length` vs `period`) | Use exact key names + JSON numeric types |
- | Unknown `method` | Indicator name not supported | Pick nearest supported method; ask Builder Chat to substitute |
- | Unsupported `timeframe` / `period` | Value outside enum | Snap to nearest allowed value |
- | Unsupported `symbol` / source | Asset not indexed or DEX pair unsupported | Skip symbol and report it; proceed with supported subset |
- | Depth / leaf-count exceeded | More than depth 3 or 10 leaves | Split into two queries joined by your runner |
- | `cron` / `llm` period too short | Below 1h minimum | Raise to `1h` or higher |
- | `tweet` username, `details.reason: paid_plan_required` | The key has no paid API plan, and the account isn't in Elfa's real-time set (a misspelled handle lands here too on such a key) | Check the spelling; use a paid API key, or pick an `included` account |
- | `tweet` username, `details.reason: not_found` | No X account has that handle, or the value isn't a handle | Check the spelling and pass the bare handle (no `@`) |
- | `tweet` username, `details.reason: protected` | The account is protected | Pick a public account |
- | `tweet` username, `details.reason: capacity_reached` | The per-post account pool is full | Retry later, or pick an `included` account |
- | `tweet` username on x402, `EQL_INVALID_ARG_TYPE` | x402 watches only accounts in the real-time set | Pick another account, or use a paid API key |
- | `503` `ERR_SERVICE_UNAVAILABLE`, `error.message: ACCOUNT_LOOKUP_UNAVAILABLE` | The X account lookup (`tweet` or follow account) failed; the account may well exist | Retry after a few seconds |
- | Invalid `minConfidence` (`tweet` / `telegram` / `events`) | Non-integer or outside `0..100` | Use a JSON integer between `0` and `100` (start with `80`) |
- | `events` unknown / ambiguous `entity` | No entity matches, or several match equally (`details.candidates`) | Set `entityType`, pass a candidate's `publishedId` as `entity`, or drop `entity` and use `eventType` + `text` |
- | `events` with no filter and no `text`, or `text`/`minConfidence`/`entityType` unpaired | At least one filter or `text`; `text` and `minConfidence` together; `entityType` only with `entity` | Add a filter, or pair the args |
- | `events` `storylineId`, "expected a storyline id" | `storylineId` is not shaped like a storyline id | Copy the id from an event's `storylineId` or a storyline's `id` |
- | `events` "Unknown storylineId" | No storyline has that id | Check it with `GET /v3/events/storylines?storylineIds=<id>` |
- | `calls` `handle`, "Unknown X account" / "not an account Elfa extracts calls from" | No X account has that handle, or Elfa does not record its calls, so the plan would never fire | Check the spelling; pick a caller from `GET /v3/calls`, or follow its posts with `tweet` |
- | `calls` `handle`, "expected an X handle" | Not letters, digits and underscores, at most 15 | Pass the bare handle |
- | `calls` / `mindshare` `asset`, "Unknown asset" / "Ambiguous asset" / "not a token or equity" | No token or stock matches; several match equally and none leads the others (`details.candidates`); or it resolved to a project, person or organisation | Set `market`, pass the `publishedId` you mean, or name its token or stock |
- | `calls` `market`, "'asset' is required" | `market` without `asset` | Add `asset`, or drop `market` |
- | `calls` `direction` / `action` out of set | Often `reaffirm` or `stop_out` | Use `long`/`short`, and `open`, `add`, `trim`, `reverse`, `close` or `stopOut` |
- | `calls` with `text` or `minConfidence` | A `calls` condition matches by its filters, not by text | Drop them; narrow with `asset`, `direction` or `action` |
- | `calls` warning "… arrive hours after the post" | Elfa collects the account with a delay | Expect alerts hours after the call, or follow another caller |
- | `mindshare` "is not one mindshare tracks", or `market` "is in the … market" | The asset is on no mindshare board, or an id's market differs from `market` | Pick another asset; drop `market` or pass the id in the market you mean |
- | `mindshare` `EQL_INVALID_METHOD` / `EQL_INVALID_OPERATOR` on `hot_*` | Often `hot_1h`; or an operator other than `==` / `!=` on `hot_*` | Use `hot_4h` for the last hour; use `== true` / `== false` |
- | `mindshare` / `calls` warning "… can also mean …" | `asset` resolved to the one that leads its market, but other assets match it just as closely | Confirm the resolved asset; otherwise set `market` or pass an id |
- | `x_follow_made` / `x_follow_overlap` account, `details.reason`: `paid_plan_required`, `not_found`, `protected`, `capacity_reached`, `too_many_follow_accounts` | Free key on an account Elfa doesn't monitor (a misspelling lands here too); no such account; protected; the per-day pool is full; more than 25 distinct accounts across the query's made/overlap conditions | Check the spelling, use a paid key or `x_follow_received` or a monitored account; pick a public account; retry later; drop or split accounts |
- | x402 follow account, "Elfa does not index this account" / "does not track this account's follows" | x402 watches only accounts Elfa tracks or monitors for follows | Use `x_follow_received`, a monitored account, or a paid API key |
- | Unresolved `telegram` channel | `chatUsername` / `chatId` does not resolve to a monitored public channel, or shorthand carried both identifiers | Use one identifier on shorthand, drop `@` and `t.me/`, and pick a monitored public channel |
- | Invalid `sec` CIK | `args.ticker` is not 1–10 digits, or the CIK is absent from SEC submissions metadata | Pass the issuer CIK (not the stock ticker) and re-validate |
- | `EQL_INVALID_ACTION` | The query or draft uses `market_order` / `limit_order`, at top level or in an `llm` callback | Replace with `webhook`, `notify`, `telegram_bot` or `llm`, and run execution from your own runner |
- | `kalshi` ticker not open (`EQL_INVALID_ARG_VALUE` on `args.ticker`) | Ticker is not a currently-open Kalshi market (closed/settled/unknown) | Resolve a currently-open full ticker and re-validate; don't guess tickers |
- | `kalshi` invalid enum / operator | Enum `value` outside its set, or operator not in the method's allowlist (e.g. `crosses_above` on `trade_size`) | Use the per-method operator allowlists and enum sets from the `kalshi` source reference |
- | `kalshi` `is_block_trade` type error | `value` passed as a string instead of a JSON boolean | Use JSON `true` / `false`, not `"true"` / `"false"` |
- | `polymarket` ticker not found / not active | `args.ticker` is not a live Polymarket outcome token (`asset_id`) — unknown or inactive | Resolve a currently-active outcome-token `asset_id` and re-validate; don't guess ids |
- | `polymarket` invalid operator / dynamic value | Operator not in the method's allowlist (e.g. `crosses_above` on `size`, `>` on `side`), or a dynamic (field-vs-field) `value` was used | Use the per-method operator allowlists; use a literal `value` (dynamic values are unsupported for `polymarket`) |
- | `cron.schedule` cadence too fast | Minute field isn't a single fixed value (e.g. `*/15 * * * *`), or a sub-hour cadence | Use a single fixed minute — see the `cron.schedule` allowed/not-allowed examples |
- | `EQL_INVALID_REPEAT` | `repeat` was combined with a recurring cron condition (`cron.every` / `cron.schedule`) — both provide recurrence | Remove `repeat` for recurring cron (cron already recurs), or drop the cron condition |
- | `EQL_INVALID_ARG_VALUE` with `details.hip3: true` | A HIP-3 (dex-prefixed) symbol was used on a `funding` / `liquidation` condition — those feeds don't publish HIP-3 keys | Use a plain base symbol (`BTC:BINANCE`). HIP-3 stays valid on `price`/`ta` |
- | Unknown symbol in a composite ticker | Symbol not listed on that venue's perp catalog (often a naming mismatch) | Check the venue's own base-symbol convention (`1000PEPE` on Binance/Bybit vs `KPEPE` on Hyperliquid) |
- | `SYMBOL_CATALOG_UNAVAILABLE` (HTTP `503`) | Market catalog was momentarily unreachable, so the symbol couldn't be checked — validation fails closed | Retry after the interval in the `Retry-After` header; this is transient — do not reshape the query |
- | Dynamic value in action params | Dynamic values only allowed in condition `value` | Move dynamic reference into condition; keep action params literal |
-
- Cross-operator semantics: `crosses_above` = previous `<` threshold AND current `>=` threshold.
- `crosses_below` = previous `>` threshold AND current `<=` threshold. Both require previous-state
- tracking server-side.
-
- #### Substitution ladder — if Auto doesn't fit
-
- Before concluding a use case is out of scope, walk this ladder. Most intents resolve at
- rung 1 or 2.
-
- 1. **Rephrase through Builder Chat** — append `"If anything is unsupported, return the closest supported query and list substitutions"` to your chat prompt.
- 2. **Iterate on Validate Query** — loop: validate → reshape → re-validate. Do NOT jump to "this isn't possible" after one rejection.
- 3. **Split into multiple queries** joined by your runner (for depth 3 / 10-leaf limits).
- 4. **Use `source: "llm"`** with `athena_condition` for fuzzy/narrative predicates.
- 5. **Pre-compute in your own service, use Auto as control plane** — only after rungs 1–4 fail.
-
- **Worked example — "alert on descending trendline break":**
-
- - **Rung 1:** Rephrase as a supported proxy — _"alert when BTC price crosses above 4h upper Bollinger Band AND 1h RSI > 55"_.
- - **Rung 4:** If the proxy isn't acceptable, use `source: "llm"` with a scheduled natural-language predicate — _"has BTC broken its recent descending trendline on the 4h chart?"_.
- - **Rung 5:** Only if both fail: compute trendline-break externally, feed a boolean into an Auto `cron` + `llm` query as the condition trigger.
-
- Do not build your own monitoring/evaluation/trigger stack before walking rungs 1–4.
-
- #### Query drafts
-
- Drafts let you stage an Auto query without activating it (and without spending credits).
- Useful for human-in-the-loop approval workflows, dashboards where users edit before
- committing, or batch authoring flows.
-
- **Draft lifecycle:**
-
- 1. `POST /v2/auto/queries/drafts` — create or update a draft (idempotent upsert).
- 2. `GET /v2/auto/queries/drafts` — list editable drafts.
- 3. `POST /v2/auto/queries/drafts/:draftId/validate` — validate stored draft.
- 4. `POST /v2/auto/queries/drafts/:draftId/convert` — promote draft → active query.
- 5. `DELETE /v2/auto/queries/drafts/:draftId` — discard draft.
-
- > `GET /v2/auto/queries/drafts/:draftId` still works but is legacy — prefer
- > `GET /v2/auto/queries/{queryId}` which resolves both active queries and drafts.
-
- Drafts are API-key-mode only. Not available via x402.
-
- #### Symbols
-
- Auto is **not crypto-only.** Crypto majors and on-chain tokens are plain uppercase tickers
- (`BTC`, `ETH`, `SOL`, `HYPE`, long-tail tokens). **HIP-3 assets** on Hyperliquid — tokenized
- equities, indices, commodities, FX, and pre-IPO names — use a **DEX-prefixed** form
- `<dex>:<SYMBOL>` and trade **24/7**, even when the underlying cash market is closed.
-
- **Two DEX prefixes matter in practice:** `xyz` lists nearly everything and carries almost all
- the volume; `mkts` lists a smaller overlapping set. Hyperliquid hosts other HIP-3 DEXs whose
- markets are **dormant** (listed but not trading) — Auto **rejects** them exactly like a typo
- (`EQL_INVALID_SYMBOL`). A symbol being listed on Hyperliquid is **not** enough; only the
- liquid listing is usable (`GOLD` appears on six DEXs). A **bare** HIP-3 symbol is rejected —
- `{"symbol": "GOLD"}` fails with `EQL_INVALID_SYMBOL`. Only plain crypto tickers work without a
- prefix (they live on Hyperliquid's main DEX).
-
- | Class | Examples |
- |---|---|
- | Crypto majors | `BTC`, `ETH`, `SOL`, `HYPE` (no prefix — main DEX) |
- | On-chain / long-tail tokens | `RAVE`, `TAO`, niche memes |
- | Indices (HIP-3) | `xyz:SP500`, `xyz:XYZ100` (Nasdaq 100), `xyz:JP225`, `mkts:US500`, `mkts:USTECH` |
- | Sector / country baskets (HIP-3) | `xyz:SMH` (semis), `xyz:XLE` (energy), `xyz:EWY`, `xyz:EWJ` |
- | Equities — mega-cap (HIP-3) | `xyz:NVDA`, `xyz:TSLA`, `xyz:AAPL`, `xyz:MSFT`, `xyz:GOOGL`, `xyz:META` |
- | Equities — semis / high-beta (HIP-3) | `xyz:AMD`, `xyz:TSM`, `xyz:ARM`, `xyz:COIN`, `xyz:MSTR`, `xyz:PLTR` |
- | Pre-IPO / private (HIP-3) | `xyz:SPCX` (SpaceX), `xyz:ZHIPU`, `xyz:MINIMAX` |
- | Commodities (HIP-3) | `xyz:CL` (WTI crude), `xyz:BRENTOIL`, `xyz:NATGAS`, `xyz:GOLD`, `xyz:SILVER`, `xyz:COPPER` |
- | FX (HIP-3) | `xyz:EUR`, `xyz:JPY`, `xyz:GBP` |
-
- **Illustrative, not exhaustive — and listings change.** Markets are added and go dormant.
- Never hardcode this list: confirm any HIP-3 symbol with
- `GET /v2/auto/validate-symbol/{exchange}/{symbol}` before building on it, and read a rejection
- as "that market is dead", not "Auto is missing the asset". HIP-3 market data comes from
- **`hyperliquid` only** — HIP-3 markets are listed only on Hyperliquid, so a HIP-3 symbol paired
- with any other `exchange` value is rejected.
-
- **Composite tickers (`funding` / `liquidation` only):** those two sources do **not** take a
- `symbol` + `exchange` arg — they take a single `SYMBOL:EXCHANGE` ticker (e.g. `BTC:BINANCE`,
- `ETH:HYPERLIQUID`, `SOL:BYBIT`). Use each venue's own base-symbol convention: `1000PEPE` on
- Binance/Bybit vs `KPEPE` on Hyperliquid. HIP-3 symbols are rejected for these two sources.
-
- **Tracking coverage:** conditions, alerts and webhooks cover DEX/on-chain assets —
- effectively unbounded (long-tail tokens, pre-CEX-listing assets, niche memes). Symbol
- support is per venue, so pre-flight anything unusual with
- `GET /v2/auto/validate-symbol/{exchange}/{symbol}` (which checks `hyperliquid` or `binance`)
- before building a `price`/`ta` condition
- on it. If a symbol is unsupported on the venue you picked, switch venue or symbol — or keep
- the condition on a supported pair and route follow-up work through `notify`, `webhook` or
- `telegram_bot`.
-
- Full reference: [Symbols](https://docs.elfa.ai/auto/symbols) and
- [Catalyst Triggers](https://docs.elfa.ai/auto/catalyst-triggers).
-
- #### Executions
-
- Every trigger fire produces an **execution record** — use these endpoints to audit trigger
- history, debug failed actions, or reconcile with your runner's audit log.
-
- | Endpoint | Method | Description |
- |---|---|---|
- | `/v2/auto/executions` | GET | List execution records (filterable) |
- | `/v2/auto/executions/:executionId` | GET | Get a single execution record |
-
- Execution records are also embedded in the poll response (`GET /v2/auto/queries/:queryId`)
- under the `executions` array, but the dedicated endpoints are useful for cross-query audits
- and pagination.
-
- Executions are API-key-mode only. Not available via x402.
-
- ### Step 4: Generating code snippets
-
- When the user wants integration help, generate correct, production-ready code.
- See the [Elfa API documentation](https://docs.elfa.ai) for the full parameter specs.
-
- **Principles for code generation:**
- - For `/v2`, always mention both access modes (API key and x402) so developers know their options
- - For `/v3`, the API key is the only mode (no x402). Use `from`/`to` in unix seconds (there is no
- `timeWindow`; mindshare takes `period`), page with `nextCursor` → `cursor`, and read the list at the top level of the
- response. See Step 2d
- - Include the signup link `https://go.elfa.ai/claude-skills` as a comment near the
- API key placeholder, and link to `https://docs.elfa.ai/get-started/x402` for x402
- - Always include proper error handling
- - For API key mode: show the `x-elfa-api-key` header (use a placeholder like `YOUR_API_KEY`)
- - For x402 mode: show the `/x402/v2/` prefix and recommend `@x402/fetch` or `@x402/axios`
- - For Auto mutations in x402 mode: include the agent-secret header
- - Include TypeScript types when generating TS code
- - Add comments explaining each parameter
- - For pagination endpoints, show how to paginate through results
- - For `/v2` time-windowed endpoints, explain the `timeWindow` vs `from`/`to` pattern
-
- **Language priorities** (use unless the user specifies otherwise):
- 1. TypeScript/JavaScript (fetch) — most Elfa integrators are web/Node devs
- 2. Python (requests)
- 3. curl
-
- **The Chat endpoint deserves special attention** — it's the most complex:
- - It supports multiple `analysisType` values: `chat`, `macro`, `summary`, `tokenIntro`,
- `tokenAnalysis`, `accountAnalysis`
- - Session management via `sessionId` for multi-turn conversations
- - Different `assetMetadata` requirements per analysis type (`message` is required only for
- `analysisType: "chat"`)
- - Three speed modes: `fast`, `expert` (default), and `adaptive` (`/v2/chat` only)
- - Two transports: `POST /v2/chat` (JSON) and `POST /v2/chat/stream` (SSE, PAYG/Enterprise) —
- same body, different plan gate. See [Step 2c](#chat--json-vs-streaming) for the SSE event
- types.
-
- **Auto code generation guidance:**
- - Always include the validate → create flow (never create without validating first)
- - For API key mode: send `x-elfa-api-key` on every call, including mutations
- - For x402 mode: include `x-elfa-agent-secret` header on all query lifecycle calls
- - Include Builder Chat examples when the user wants natural-language query building
- - Show how to poll or stream for results after query creation
-
- ### Common patterns
-
- These patterns are for `/v2`. API v3 follows the rules in Step 2d.
-
- **Time window parameters:**
- Many endpoints accept either `timeWindow` (e.g., "30m", "1h", "4h", "24h", "7d", "30d")
- OR `from`/`to` unix timestamps. If both are provided, `from`/`to` takes priority.
-
- **Pagination:**
- Aggregation endpoints (trending-tokens, trending-cas, top-mentions, token-news) use
- `page` + `pageSize`. The keyword-mentions endpoint uses cursor-based pagination instead
- (`cursor` + `limit`).
-
- **Defaults differ per endpoint — do not assume a global default.** From the current spec:
-
- | Endpoint | `timeWindow` | `page` | `pageSize` / `limit` | Other |
- |---|---|---|---|---|
- | `trending-tokens` | `7d` | `1` | `50` | `minMentions` (default `5`) |
- | `trending-cas/twitter` · `trending-cas/telegram` | `7d` | `1` | `50` | `minMentions` (default `5`) |
- | `top-mentions` | `1h` | `1` | `10` | `ticker` **required**, `reposts` |
- | `token-news` | `7d` | `1` | `20` | `coinIds`, `reposts` |
- | `keyword-mentions` | `7d` | — (cursor) | `limit` `20`, **max 30** | `keywords`, `accountName`, `searchType`, `cursor`, `reposts` |
- | `event-summary` | `7d` | — | — | `keywords` **required**, `searchType` (default `or`) |
- | `trending-narratives` | — | — | — | `timeFrame` (`day` \| `week`, default `day`), `maxNarratives` (default `7`, range `1`–`20`), `maxTweetsPerNarrative` (default `5`, range `1`–`20`) |
- | `smart-stats` | — | — | — | `username` **required** |
-
- Every endpoint above except `trending-narratives` and `smart-stats` also accepts `from`/`to`
- unix timestamps, which take priority over `timeWindow`.
-
- **Per-endpoint parameter notes:**
- - **`keyword-mentions`** — provide either `keywords` (up to 5, comma-separated) OR
- `accountName` (or both); `accountName` filters mentions by a specific account (e.g.
- `accountName=elonmusk`). `searchType` accepts **`and`** (all terms must be present) or
- **`or`** (any term matches). Cursor-paginated — pass `metadata.cursor` from the previous
- response as `cursor`. Returns `account.username`, which feeds directly into `smart-stats`.
- - **`event-summary`** — `keywords` is **required**; `searchType` defaults to `or`. A `503`
- means the upstream model declined to process the matched posts — retryable, and **not** an
- empty result. The 4 fixed credits are not charged; the 1 base per-request credit still is.
- - **`token-news`** — V2 always returns news mentions (no `isNews` parameter). Results are X
- posts from accounts tagged as news sources, not articles published by news outlets.
- - **`top-mentions`** — `ticker` is **required**. Account details are always included (no
- `includeAccountDetails` parameter).
- - **`smart-stats`** — the query param is `username`, not `accountName`.
- - **`minMentions`** (trending-tokens / trending-cas) — floor on mention count; raise it to cut
- long-tail noise.
- - **`reposts`** (keyword-mentions / top-mentions / token-news) — boolean toggle for including
- repost activity.
-
- **Ticker format (top-mentions):**
- The `ticker` parameter behavior changes based on whether you include the `$` prefix:
- - **With `$` prefix** (e.g., `ticker=$SOL` or URL-encoded `ticker=%24SOL`): Exact cashtag matching only. Searches the `cashtags` field for exact matches.
- - **Without `$` prefix** (e.g., `ticker=SOL`): Broader search across both cashtags (boosted 2x) and general token references.
-
- Use `$` when you want only cashtag-specific mentions. Omit `$` for a more inclusive search.
-
- **Credit costs (data endpoints — both modes):**
- - Most endpoints: 1 credit per call ($0.0145 via x402)
- - Event summary: 5 credits ($0.0725 via x402)
- - Trending narratives: 5 credits ($0.0725 via x402)
- - Chat: speed-based; via x402 pay `exact` (fast $1, expert $2) or `batch-settlement` (up to fast $2, expert $6)
- - API v3: free on the Pay-as-you-go and Grow plans until 31 October 2026. API key only
- - `/v2/ping`, `/v2/key-status`: free
- - Every `/v2/*` and `/v3/*` response to an authenticated request carries an **`x-elfa-credits`** header with
- the credits that request consumed — read it instead of diffing `/v2/key-status` around a call.
- It is present on error responses raised after the key is accepted, and absent when the request
- is rejected before the key resolves, because nothing was charged. Streaming responses and
- charges that finalize after the response is flushed carry the pre-flush subtotal, so
- `/v2/key-status` stays the authoritative balance for those.
-
- **Auto query lifecycle:**
- - **Validate before create (recommended):** Call `POST /v2/auto/queries/validate` first to preview
- estimated credits and catch validation errors before committing. The server validates internally
- during create anyway, but pre-validation lets you iterate on errors without side effects.
- - Prefer webhook or SSE for real-time delivery
- - Deduplicate events by `eventId`
- - Use shorter expiries (`24h`–`3d`) for fresh signals
- - Include `title` and `description` in queries — they appear in notifications
-
- ## Important notes
-
- - The Elfa API domain (`api.elfa.ai`) must be accessible from the network. If blocked,
- inform the user and provide the code snippet instead.
- - Use the v2 endpoints (paths starting with `/v2/` or `/x402/v2/`) and the API v3 endpoints
- (`/v3/`).
- - For experimental endpoints (trending-tokens), mention that behavior may
- change without notice. `/v2/account/smart-stats` is **legacy**: it still works, but
- **will be removed on 28 October 2026** — say so before building on it.
- `/v2/data/market-events` is no longer part of the public API: market events are served by
- API v3 Events (`/v3/events`).
- - `/v2/data/event-summary` can return `503` (`ERR_SERVICE_UNAVAILABLE`, 1 credit instead of 5)
- when the matched posts cannot be summarised. Retry, or narrow the keywords or the window. A
- `503` never means "no events": a quiet window is a `200` with an empty `data` array.
- - Measurement endpoints return **no raw tweet text and no sentiment field** — do not promise
- either. Fetching post content requires the user's own X API key.
- - Auto no longer accepts order actions (`market_order` / `limit_order`) on new queries or
- drafts — Auto rejects them with `EQL_INVALID_ACTION`. Trigger a `webhook`, `notify` or
- `telegram_bot` action and run execution from your own runner.
- - When the user asks about pricing or API key tiers, direct them to
- https://go.elfa.ai/claude-skills for full details on plans and pricing.
- - API-key request rate limits are per tier: Free / PAYG = **60 requests/min**,
- Grow = **120 requests/min** (Enterprise custom). These are independent of credit balances.
- A `429` response should be retried honoring `Retry-After`.
+ mentions, token news, trending contract addresses, AI market chat
+ (JSON and SSE streaming), curl/code snippets, automated alerts,
+ EQL queries, trigger pipelines, and agent workflows that react to market conditions.
+ Auto can trigger on X/Twitter posts, Telegram channels, events (news), one trader's
+ calls, token or stock mindshare, SEC filings, Kalshi and Polymarket prediction markets,
+ funding rates, liquidation cascades, the Fear & Greed index, and X follow-graph
+ moves (new followers, new follows, accounts converging), re-fire
+ recurring plans via `repeat`, run calendar schedules via
+ `cron.schedule`, and monitor crypto plus HIP-3 assets (equities, indices, commodities, FX,
+ pre-IPO — 24/7). Supports API keys and x402 pay-per-request in USDC on Base,
+ Arbitrum, Polygon, Avalanche, BNB Smart Chain or Solana, or USDT on BNB Smart Chain.
+ ---
+
+ # Elfa API Skill
+
+ This skill enables agents to work with the [Elfa API](https://api.elfa.ai) — a social listening,
+ market context layer, and automated condition engine for crypto. Elfa ingests real-time data
+ from Twitter/X, Telegram, and other sources, then structures sentiment, narratives, and
+ attention shifts into actionable trading insights. The **Market Intelligence** endpoints cover
+ measurement (counts, engagement, accounts) and interpretation (chat, summaries, narratives).
+ **API v3** turns news and market chatter across crypto and equities into events, positions and
+ rankings.
+ The **Auto** subsystem adds a managed condition engine and trigger pipeline — describe what to
+ watch for, and Auto evaluates continuously and fires actions when conditions are met.
+
+ Full documentation: [docs.elfa.ai](https://docs.elfa.ai)
+
+ > **Staying current — check weekly.** This skill is updated on a **weekly** cadence to track
+ > Elfa API and Auto changes. Before relying on it, check whether your copy may be stale: if it
+ > has been **more than 7 days** since this skill was last fetched/installed (or you are unsure),
+ > re-fetch the latest version before acting —
+ > - **Skill package & changelog:** <https://go.elfa.ai/claude-skills> (and the
+ > [`elfa-ai/skills`](https://github.com/elfa-ai/skills) repo).
+ > - **Live source of truth:** [docs.elfa.ai](https://docs.elfa.ai) — when this skill and the
+ > docs disagree, the docs win; prefer the docs and flag the mismatch to the user.
+ >
+ > When in doubt about a specific endpoint, parameter, or price, verify against
+ > [docs.elfa.ai](https://docs.elfa.ai) at call time rather than trusting a possibly-stale copy.
+
+ ## Environment and credentials
+
+ Elfa supports API-key auth and x402 keyless payments. API keys are optional when using x402.
+
+ | Variable | Required | Use |
+ |---|---:|---|
+ | `ELFA_API_KEY` | No | API-key authenticated requests. Start a pay-as-you-go balance at <https://go.elfa.ai/claude-skills>. |
+ | `ELFA_AGENT_SECRET` | No | Persistent agent identity secret for x402 Auto. Generate once with `openssl rand -hex 32` and reuse for query lifecycle calls. |
+
+ > **Do not reuse this as your webhook signing secret.** `webhook.params.signingSecret` is a
+ > *separate* per-webhook secret that verifies deliveries **from** Elfa **to** your server. See
+ > [Webhook signature verification](#webhook-signature-verification).
+
+ x402 wallet signing is handled client-side by `@x402/fetch` or `@x402/axios`.
+
+ ## When to use this skill
+
+ - User asks about **trending tokens, narratives, or contract addresses** in crypto
+ - User wants **social mentions** for a specific ticker or keyword
+ - User wants an X account's **standing or track record** as a caller
+ - User wants an **AI-generated market summary, macro overview, or token analysis** — including
+ **streaming** chat output
+ - User wants to know **what happened** in crypto or equity markets and how prices reacted,
+ **who is long or short** an asset, **whose calls to trust**, or **which assets get the most
+ attention** (API v3)
+ - User asks how to **integrate, call, or use the Elfa API**
+ - User wants **code examples** (curl, Python, JavaScript/TypeScript) for Elfa endpoints
+ - User mentions "elfa" in a crypto or trading data context
+ - User wants to **set up automated alerts or monitoring** on price, indicators, or narratives
+ - User wants to **build condition-based triggers** (e.g., "alert me when BTC crosses 100k")
+ - User wants to **watch the X follow graph** — who followed a tracked account, who it started
+ following, or several tracked accounts converging on the same account
+ - User wants an alert when **a specific trader opens or closes a position**, or when **attention
+ on a token or stock spikes** (Auto `calls`, `mindshare`)
+ - User mentions **Auto**, **EQL**, **condition engine**, or **trigger pipeline** in a crypto context
+ - User wants **agent workflows** that react to market conditions automatically
+ - User wants to **build queries with Builder Chat** using natural language
+
+ ## API Overview
+
+ **Base URL:** `https://api.elfa.ai`
+ **Version:** v2, plus [API v3](#api-v3-endpoints) (`/v3/`)
+ **Docs:** [docs.elfa.ai](https://docs.elfa.ai)
+
+ ### Two access modes
+
+ Elfa supports two independent ways to authenticate requests:
+
+ | Mode | Endpoint prefix | Auth header | Best for |
+ |---|---|---|---|
+ | **API key** | `/v2/`, `/v3/` | `x-elfa-api-key: YOUR_KEY` | Humans & apps with a registered key |
+ | **x402 (keyless)** | `/x402/v2/` | `PAYMENT-SIGNATURE: <signed-payload>` | Agents & wallets — no signup needed |
+
+ Both modes access the same v2 data. Otherwise the only difference is how you authenticate:
+ - **API key** — register at https://go.elfa.ai/claude-skills. A Free key gets 1,000 credits in
+ total, not per month; for ongoing use, top up a **pay-as-you-go** balance.
+ - **x402** — pay per request with USDC on Base, Arbitrum, Polygon, Avalanche, BNB Smart Chain
+ or Solana, or USDT on BNB Smart Chain. No registration, no API key. Currently in beta.
+
+ ### Endpoints at a glance
+
+ #### Data endpoints
+
+ All endpoints below work with both `/v2/` (API key) and `/x402/v2/` (keyless) prefixes,
+ except `key-status` which is API key mode only.
+
+ | Endpoint | Method | Description | Credits |
+ |---|---|---|---|
+ | `/v2/ping` | GET | Health check — **no auth required** (API key mode only) | Free |
+ | `/v2/key-status` | GET | API key usage & limits (API key only) | Free |
+ | `/v2/aggregations/trending-tokens` | GET | Trending tokens by mention count | 1 |
+ | `/v2/account/smart-stats` | GET | Smart follower & engagement stats — **deprecated, not maintained** | 1 |
+ | `/v2/data/top-mentions` | GET | Top mentions for a ticker symbol | 1 |
+ | `/v2/data/keyword-mentions` | GET | Search mentions by keywords or account | 1 |
+ | `/v2/data/event-summary` | GET | AI event summaries from keyword mentions | 5 |
+ | `/v2/data/trending-narratives` | GET | Trending narrative clusters | 5 |
+ | `/v2/data/token-news` | GET | Token-related news mentions (tweet source only) | 1 |
+ | `/v2/aggregations/trending-cas/twitter` | GET | Trending contract addresses (Twitter) | 1 |
+ | `/v2/aggregations/trending-cas/telegram` | GET | Trending contract addresses (Telegram) | 1 |
+ | `/v2/chat` | POST | AI chat, complete JSON response (Grow+ / PAYG) | Speed-based |
+ | `/v2/chat/stream` | POST | AI chat, incremental SSE stream (Grow+ / PAYG) | Speed-based |
+
+ `/v2/ping`, `/v2/key-status`, and `/v2/chat/stream` are **API-key mode only** — they have no
+ `/x402/v2/` counterpart.
+
+ #### API v3 endpoints
+
+ API v3 turns news and market chatter across crypto and equities into events, positions and
+ rankings. Every endpoint is `GET` and authenticates with `x-elfa-api-key` (no x402). Every plan,
+ Free included, can call every v3 endpoint. Each call but
+ `/v3/key-status` costs credits on Free, PAYG and Grow, priced per endpoint
+ ([pricing](https://docs.elfa.ai/get-started/pricing#api-v3)); Enterprise keys are not drawn down
+ per call. See Step 2d for the rules they share.