Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Each skill is a self-contained directory with a `SKILL.md` (used by Claude Code
| [pillar](./pillar/) | `pillar/pillar.ts`, `pillar/pillar-direct.ts` | Pillar smart wallets — browser-handoff mode and agent-signed direct mode for sBTC operations, DCA programs, leveraged positions, and stacking. |
| [query](./query/) | `query/query.ts` | Stacks blockchain queries — STX fees, account info, transaction history, block info, mempool, contract info and events, network status, read-only calls. |
| [x402](./x402/) | `x402/x402.ts` | x402 paid API endpoints — execute and probe endpoints, send inbox messages, scaffold new x402 Cloudflare Worker projects, and explore OpenRouter AI models. |
| [lunarcrush](./lunarcrush/) | `lunarcrush/lunarcrush.ts` | LunarCrush social/market intelligence via x402 on Stacks — Galaxy Score, AltRank, market cap rank, price, 24h change. USD-pegged pricing (~$0.005/call) recomputed hourly from live STX/USD. |
| [lunarcrush](./lunarcrush/) | `lunarcrush/lunarcrush.ts` | LunarCrush social/market intelligence via x402 on Stacks — `oracle` verdict + vibe, Galaxy Score, AltRank, social velocity/sentiment, price, 24h change. USD-pegged pricing (~$0.005-$0.025/call) recomputed hourly from live STX/USD. |
| [yield-hunter](./yield-hunter/) | `yield-hunter/yield-hunter.ts` | Autonomous sBTC yield daemon — monitors wallet sBTC balance and automatically deposits to Zest Protocol when balance exceeds a configurable threshold. |
| [sbtc-yield-maximizer](./sbtc-yield-maximizer/) | `sbtc-yield-maximizer/sbtc-yield-maximizer.ts` | Routes idle sBTC to the highest safe live yield path — compares Zest Protocol rates against Bitflow HODLMM APR with stale-price, volume, and TVL safety gates, and executes a capped Zest supply when Zest is the winning route. Mainnet-only. |
| [credentials](./credentials/) | `credentials/credentials.ts` | AES-256-GCM encrypted credential store — add, retrieve, list, and delete named secrets (API keys, tokens, passwords) at `~/.aibtc/credentials.json`. Independent of the wallet system. |
Expand Down
13 changes: 11 additions & 2 deletions lunarcrush/AGENT.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: lunarcrush-agent
skill: lunarcrush
description: Pay-per-call LunarCrush social and market intelligence via x402 on Stacks — oracle, score, health, and meta subcommands with automatic STX payment handling.
description: Pay-per-call LunarCrush social and market intelligence via x402 on Stacks — oracle, score, velocity, health, and meta subcommands with automatic STX payment handling.
---

# LunarCrush Skill — Agent Operations
Expand All @@ -11,7 +11,7 @@ This document covers autonomous-mode rules for invoking the `lunarcrush` skill:
## Prerequisites

- An unlocked Stacks wallet (managed via the `wallet` skill or via `MNEMONIC` env var).
- For `mainnet` calls (default): the wallet must hold STX. Each `score` call costs ~$0.005 USD worth of STX (~22,000 microSTX at $0.22 STX). Verify balance is at least 100,000 microSTX before calling to leave headroom.
- For `mainnet` calls (default): the wallet must hold STX. Each `score` / `velocity` call costs ~$0.005 USD (~22,000 microSTX at STX $0.22); each `oracle` call costs ~$0.025 (~115,000 microSTX). Verify balance is at least **`(per_call_microSTX * planned_call_count) + 100,000 microSTX safety reserve`** before starting a batch — the 100K reserve covers price drift only, NOT additional calls. For ad-hoc single calls, 100K microSTX over the per-call cost is sufficient.
- For `testnet` calls: free STX from the Hiro testnet faucet.

The skill itself does not need a separate API key — it makes paid x402 HTTP calls to a public Cloudflare Worker.
Expand Down Expand Up @@ -50,6 +50,15 @@ Before calling `score`:
| `400 invalid_symbol` | Did not consume payment. Symbol must be lowercase a-z + 0-9 only, max 16 chars. Check input. |
| `5xx` from worker | Worker outage. Retry once after 30s. If persists, alert operator. |

## How verdict + vibe are generated (deterministic, not LLM)

Both `verdict` and `vibe` on the `oracle` response are **deterministic synthesis** — no LLM, no upstream variability, no extra latency beyond the LunarCrush API call:

- `verdict` is bucketed from the composite score (Galaxy 45% / AltRank 35% / 24h momentum 20%) into one of six fixed tiers (`STRONG-BUY` ≥75, `BUY` 60-74, `NEUTRAL` 45-59, `WATCH` 30-44, `AVOID` <30, `UNKNOWN` for missing data).
- `vibe` is selected from six templated one-liners keyed by verdict — same composite inputs always produce the same vibe string. Emoji are static parts of each template.

This means `oracle` latency is the LunarCrush v4 fetch + a few microseconds of arithmetic. No GPT/Claude/Gemini call inside, so calling `oracle` at scale carries no variable AI cost on top of the $0.025 fixed price.

## Safety checks

- **No private key handling.** This skill never logs, prints, or transmits the wallet seed phrase. All signing happens via `getAccount()` returning a properly-scoped account object.
Expand Down
49 changes: 47 additions & 2 deletions lunarcrush/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ metadata:
author: "joevezzani"
author-agent: "Prime Spoke"
user-invocable: "false"
arguments: "oracle | score | health | meta"
arguments: "oracle | score | velocity | health | meta"
entry: "lunarcrush/lunarcrush.ts"
requires: "wallet"
tags: "l2, read-only, requires-funds"
Expand All @@ -23,10 +23,11 @@ LunarCrush is the leading social intelligence platform for crypto and equities
|---|---|---|---|
| `oracle` | mainnet (default) or testnet | $0.025 USD in STX | Premium combined: verdict + confidence + reasoning + vibe one-liner + structured signals. One paid call, both trading-model and chatbot audiences served. |
| `score` | mainnet (default) or testnet | $0.005 USD in STX | Galaxy Score, AltRank, market cap rank, price, 24h change |
| `velocity` | mainnet (default) or testnet | $0.005 USD in STX | 24h interactions, social volume, sentiment, percent_change_24h, momentum tier (accelerating/stable/cooling) |
| `health` | n/a | free | Liveness probe |
| `meta` | n/a | free | Live STX/USD price, full endpoint catalog with current microSTX amounts |

Endpoints planned (not yet live): `altrank` (dedicated), `topic`, `social-velocity`, `sentiment`, `top-movers`, `whale-flow`. The full catalog is documented at the meta endpoint.
Endpoints planned (not yet live): `altrank` (dedicated), `topic`, `sentiment`, `top-movers`, `whale-flow`. The full catalog is documented at the meta endpoint.

## Usage

Expand Down Expand Up @@ -115,6 +116,40 @@ Output (fields surfaced by the worker; `payment_receipt` is decoded from the `pa
}
```

### velocity

Fetch social momentum signals — 24h interactions, social volume, sentiment, and a coarse momentum tier (`accelerating`, `stable`, or `cooling`) derived from `percent_change_24h`. Pays via x402.

```
bun run lunarcrush/lunarcrush.ts velocity --symbol BTC
bun run lunarcrush/lunarcrush.ts velocity --symbol STX --network testnet
```

Options:
- `--symbol` (required) — Crypto ticker symbol (e.g. `BTC`, `ETH`, `STX`).
- `--network` (optional, default `mainnet`) — `mainnet` or `testnet`.

Output:
```json
{
"symbol": "BTC",
"name": "Bitcoin",
"interactions_24h": 4823100,
"social_volume_24h": 18420,
"sentiment": 3.8,
"percent_change_24h": 1.40,
"momentum_tier": "stable",
"source": "lunarcrush",
"network": "mainnet",
"endpoint": "https://lunarcrush-x402-poc-prod.lunarcrush.workers.dev/social-velocity/btc",
"payment_receipt": { "success": true, "payer": "SP...", "transaction": "...", "network": "stacks:1" }
}
```

Useful as a fast-twitch signal for trading agents that want to detect sentiment shifts before they show up in price.

`momentum_tier` is bucketed deterministically from `percent_change_24h`: `accelerating` if > +5%, `cooling` if < −5%, `stable` otherwise. Same pattern as the `oracle` verdict tiers — inspectable without calling the Worker.

### health

Liveness probe (free). Returns `{ ok: true, ts: ... }`.
Expand Down Expand Up @@ -162,6 +197,16 @@ Output:

Recipient wallet on Stacks mainnet: `SP3TH5S631RYN7Z485TY0KPFVX24R7RW7P25HVZ73` (Prime Spoke, on-chain identity registered with aibtc.news).

## Companion: Bitflow × LunarCrush Sentiment Scanner

A separate Worker bundles LunarCrush signals across the top Stacks-ecosystem tokens and cross-references against Bitflow XYK + HODLMM tradability, returning a single ranked opportunity list per call. Useful when an agent wants "what should I look at right now on Stacks" instead of fetching and ranking N individual symbols itself.

- **HTML dashboard (free):** `https://bitflow-sentiment-scanner.lunarcrush.workers.dev/`
- **Free preview (top 3):** `GET /scan/preview`
- **Paid full ranking:** `GET /scan` — $0.01 USD in STX. Returns all 10 ranked tokens with composite scores, verdicts, Bitflow pool counts, and HODLMM availability.

Same payment wallet as this skill; one funded agent can use both.

## Notes

- The skill reads your wallet via the shared `getAccount()` helper (env mnemonic or unlocked wallet skill). x402 payment is handled automatically by `createApiClient`.
Expand Down
100 changes: 65 additions & 35 deletions lunarcrush/lunarcrush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ function decodePaymentReceipt(header: unknown): Record<string, unknown> | undefi
}
}

// ---------------------------------------------------------------------------
// Shared paid-command helper
// ---------------------------------------------------------------------------
// The three paid actions (oracle, score, velocity) share identical structure:
// create client → call endpoint → merge receipt → print. arc0btc flagged the
// duplication in PR #392; this helper consolidates it.

async function runPaidCommand(opts: {
symbolRaw: unknown;
networkRaw: unknown;
path: (s: string) => string;
apiKey: string;
}): Promise<void> {
const symbol = normalizeSymbol(opts.symbolRaw as string | undefined);
const { network, baseUrl } = resolveHost(opts.networkRaw as string | undefined);

const api = await createApiClient(baseUrl, opts.apiKey);
const response = await api.request({ method: "GET", url: opts.path(symbol) });

const output: Record<string, unknown> = {
...((response.data as Record<string, unknown>) ?? {}),
network,
endpoint: `${baseUrl}${opts.path(symbol)}`,
};
const receipt = decodePaymentReceipt(response.headers?.["payment-response"]);
if (receipt) output.payment_receipt = receipt;

printJson(output);
}

// ---------------------------------------------------------------------------
// Commander setup
// ---------------------------------------------------------------------------
Expand All @@ -77,30 +107,18 @@ program
program
.command("oracle")
.description(
"Premium combined LunarCrush oracle for a symbol — verdict (STRONG-BUY/BUY/NEUTRAL/WATCH/AVOID), confidence (0-1), reasoning, and a vibe one-liner. One paid call, ~$0.025 USD in STX."
"Premium combined LunarCrush oracle for a symbol — verdict (STRONG-BUY/BUY/NEUTRAL/WATCH/AVOID), confidence (0-1), reasoning, and a deterministic vibe one-liner. One paid call, ~$0.025 USD in STX."
)
.requiredOption("--symbol <symbol>", "Crypto ticker (e.g. BTC, ETH, STX)")
.option("--network <network>", "mainnet or testnet (default: mainnet)", "mainnet")
.action(async (opts) => {
try {
const symbol = normalizeSymbol(opts.symbol);
const { network, baseUrl } = resolveHost(opts.network);

const api = await createApiClient(baseUrl, "lunarcrush.oracle");
const response = await api.request({
method: "GET",
url: `/oracle/${symbol}`,
await runPaidCommand({
symbolRaw: opts.symbol,
networkRaw: opts.network,
path: (s) => `/oracle/${s}`,
apiKey: "lunarcrush.oracle",
});

const output: Record<string, unknown> = {
...((response.data as Record<string, unknown>) ?? {}),
network,
endpoint: `${baseUrl}/oracle/${symbol}`,
};
const receipt = decodePaymentReceipt(response.headers?.["payment-response"]);
if (receipt) output.payment_receipt = receipt;

printJson(output);
} catch (error) {
handleError(error);
}
Expand All @@ -119,24 +137,36 @@ program
.option("--network <network>", "mainnet or testnet (default: mainnet)", "mainnet")
.action(async (opts) => {
try {
const symbol = normalizeSymbol(opts.symbol);
const { network, baseUrl } = resolveHost(opts.network);

const api = await createApiClient(baseUrl, "lunarcrush.score");
const response = await api.request({
method: "GET",
url: `/galaxy-score/${symbol}`,
await runPaidCommand({
symbolRaw: opts.symbol,
networkRaw: opts.network,
path: (s) => `/galaxy-score/${s}`,
apiKey: "lunarcrush.score",
});
} catch (error) {
handleError(error);
}
});

const output: Record<string, unknown> = {
...((response.data as Record<string, unknown>) ?? {}),
network,
endpoint: `${baseUrl}/galaxy-score/${symbol}`,
};
const receipt = decodePaymentReceipt(response.headers?.["payment-response"]);
if (receipt) output.payment_receipt = receipt;
// ---------------------------------------------------------------------------
// velocity
// ---------------------------------------------------------------------------

printJson(output);
program
.command("velocity")
.description(
"Fetch social-velocity signals (24h interactions, social volume, sentiment, momentum tier) for a symbol. Costs ~$0.005 USD in STX."
)
.requiredOption("--symbol <symbol>", "Crypto ticker (e.g. BTC, ETH, STX)")
.option("--network <network>", "mainnet or testnet (default: mainnet)", "mainnet")
.action(async (opts) => {
try {
await runPaidCommand({
symbolRaw: opts.symbol,
networkRaw: opts.network,
path: (s) => `/social-velocity/${s}`,
apiKey: "lunarcrush.velocity",
});
} catch (error) {
handleError(error);
}
Expand Down Expand Up @@ -177,10 +207,10 @@ program
.action(async (opts) => {
try {
const { network, baseUrl } = resolveHost(opts.network);
const data = await fetchFree(baseUrl, "/");
const data = await fetchFree(baseUrl, "/meta");
printJson({
network,
endpoint: baseUrl,
endpoint: `${baseUrl}/meta`,
...(typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {}),
});
} catch (error) {
Expand Down
Loading
Loading