@superpack/crypto-price 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # openclaw-crypto-price
2
+
3
+ Live cryptocurrency prices for OpenClaw via `/price` command. Runs deterministically — no LLM invocation.
4
+
5
+ ## Usage
6
+
7
+ ```
8
+ /price # All default coins (BTC, PAXG, XMR, ZANO)
9
+ /price btc xmr # Specific coins
10
+ /price bitcoin # Aliases work too
11
+ ```
12
+
13
+ ## Supported Coins
14
+
15
+ | Symbol | Name | Aliases |
16
+ |--------|-----------|--------------------|
17
+ | BTC | Bitcoin | bitcoin, btc |
18
+ | PAXG | PAX Gold | pax-gold, paxg |
19
+ | XMR | Monero | monero, xmr |
20
+ | ZANO | Zano | zano |
21
+
22
+ ## Installation
23
+
24
+ Add to your `openclaw.json`:
25
+
26
+ ```json
27
+ {
28
+ "plugins": {
29
+ "allow": ["openclaw-crypto-price"]
30
+ }
31
+ }
32
+ ```
33
+
34
+ Optional config to override default coins:
35
+
36
+ ```json
37
+ {
38
+ "plugins": {
39
+ "config": {
40
+ "openclaw-crypto-price": {
41
+ "coins": ["BTC", "XMR"]
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Requirements
49
+
50
+ - Python 3.6+
51
+ - Internet access (fetches from CryptoCompare API)
52
+
53
+ ## Data Source
54
+
55
+ [CryptoCompare](https://www.cryptocompare.com/) public API — no API key required.
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "openclaw-crypto-price",
3
+ "name": "OpenClaw Crypto Price",
4
+ "description": "Live cryptocurrency prices via /price command. No LLM invocation — runs deterministically.",
5
+ "configSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "coins": {
10
+ "type": "array",
11
+ "items": { "type": "string" },
12
+ "description": "Default coins to show when /price is called with no arguments. Defaults to [\"BTC\", \"PAXG\", \"XMR\", \"ZANO\"]."
13
+ }
14
+ }
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@superpack/crypto-price",
3
+ "version": "0.0.2",
4
+ "description": "Demo Plugin for OpenClaw Superpack. Adds /price to get crypto prices.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Rob Vella <me@robvella.com>",
8
+ "scripts": {
9
+ "lint": "eslint src --ext .ts",
10
+ "price": "python3 scripts/get_price.py"
11
+ },
12
+ "devDependencies": {
13
+ "@typescript-eslint/eslint-plugin": "^8",
14
+ "@typescript-eslint/parser": "^8",
15
+ "eslint": "^9"
16
+ },
17
+ "keywords": [
18
+ "openclaw",
19
+ "crypto",
20
+ "bitcoin",
21
+ "price",
22
+ "plugin"
23
+ ],
24
+ "openclaw": {
25
+ "extensions": [
26
+ "./src/index.ts"
27
+ ]
28
+ },
29
+ "files": [
30
+ "src/",
31
+ "scripts/",
32
+ "openclaw.plugin.json",
33
+ "README.md"
34
+ ]
35
+ }
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env python3
2
+ """Fetch cryptocurrency prices from CryptoCompare API.
3
+ Supports: BTC (Bitcoin), PAXG (Paxos Gold), XMR (Monero), ZANO (Zano)
4
+ """
5
+ import json
6
+ import urllib.request
7
+ import urllib.error
8
+ from typing import Dict, Union, Optional, List
9
+
10
+ CRYPTOCOMPARE_API = "https://min-api.cryptocompare.com/data/pricemultifull"
11
+
12
+ # Symbol mapping for display names
13
+ COIN_NAMES = {
14
+ 'BTC': 'Bitcoin',
15
+ 'PAXG': 'PAX Gold',
16
+ 'XMR': 'Monero',
17
+ 'ZANO': 'Zano',
18
+ }
19
+
20
+ # Symbol aliases
21
+ SYMBOL_ALIASES = {
22
+ 'bitcoin': 'BTC',
23
+ 'btc': 'BTC',
24
+ 'pax-gold': 'PAXG',
25
+ 'paxg': 'PAXG',
26
+ 'monero': 'XMR',
27
+ 'xmr': 'XMR',
28
+ 'zano': 'ZANO',
29
+ }
30
+
31
+ DEFAULT_COINS = ['BTC', 'PAXG', 'XMR', 'ZANO']
32
+
33
+
34
+ def fetch_all_coins(fsyms: List[str] = None, tsyms: str = "USD") -> Optional[Dict]:
35
+ """Fetch market data for multiple coins from CryptoCompare."""
36
+ if fsyms is None:
37
+ fsyms = DEFAULT_COINS
38
+
39
+ symbols = ','.join(fsyms)
40
+ url = f"{CRYPTOCOMPARE_API}?fsyms={symbols}&tsyms={tsyms}"
41
+
42
+ try:
43
+ req = urllib.request.Request(
44
+ url,
45
+ headers={
46
+ "Accept": "application/json",
47
+ "User-Agent": "Mozilla/5.0 (OpenClaw Bot)"
48
+ }
49
+ )
50
+
51
+ with urllib.request.urlopen(req, timeout=30) as response:
52
+ return json.loads(response.read().decode('utf-8'))
53
+ except (urllib.error.URLError, json.JSONDecodeError) as e:
54
+ print(f"Error fetching data: {e}")
55
+ return None
56
+
57
+
58
+ def parse_coin_data(raw_data: Dict, symbol: str, tsym: str = "USD") -> Optional[Dict[str, Union[float, int]]]:
59
+ """Parse raw API response for a specific coin."""
60
+ try:
61
+ coin_raw = raw_data['RAW'][symbol][tsym]
62
+ return {
63
+ 'symbol': symbol,
64
+ 'name': COIN_NAMES.get(symbol, symbol),
65
+ 'price_usd': coin_raw['PRICE'],
66
+ 'change_24h': coin_raw['CHANGEPCT24HOUR'],
67
+ 'market_cap': coin_raw.get('MKTCAP', 0),
68
+ 'volume_24h': coin_raw.get('VOLUME24HOURTO', 0),
69
+ 'high_24h': coin_raw.get('HIGH24HOUR', 0),
70
+ 'low_24h': coin_raw.get('LOW24HOUR', 0),
71
+ }
72
+ except (KeyError, TypeError):
73
+ return None
74
+
75
+
76
+ def get_all_coins(fsyms: List[str] = None, tsyms: str = "USD") -> Dict[str, Optional[Dict]]:
77
+ """Fetch and parse data for multiple coins."""
78
+ raw = fetch_all_coins(fsyms, tsyms)
79
+ if not raw or 'RAW' not in raw:
80
+ return {sym: None for sym in (fsyms or DEFAULT_COINS)}
81
+
82
+ results = {}
83
+ for sym in (fsyms or DEFAULT_COINS):
84
+ results[sym] = parse_coin_data(raw, sym, tsyms)
85
+ return results
86
+
87
+
88
+ def get_coin_data(symbol: str, tsyms: str = "USD") -> Optional[Dict[str, Union[float, int]]]:
89
+ """Fetch data for a single coin."""
90
+ sym = resolve_symbol(symbol)
91
+ if not sym:
92
+ return None
93
+
94
+ raw = fetch_all_coins([sym], tsyms)
95
+ if not raw:
96
+ return None
97
+
98
+ return parse_coin_data(raw, sym, tsyms)
99
+
100
+
101
+ def get_coin_price(symbol: str, tsyms: str = "USD") -> Optional[float]:
102
+ """Get just the price for a specific coin."""
103
+ data = get_coin_data(symbol, tsyms)
104
+ return data['price_usd'] if data else None
105
+
106
+
107
+ def resolve_symbol(symbol_or_name: str) -> Optional[str]:
108
+ """Resolve common symbol or name to standard symbol."""
109
+ upper = symbol_or_name.upper()
110
+ if upper in COIN_NAMES:
111
+ return upper
112
+ return SYMBOL_ALIASES.get(symbol_or_name.lower())
113
+
114
+
115
+ def format_price(price: float) -> str:
116
+ """Format price with commas and appropriate decimals."""
117
+ if price >= 1000:
118
+ return f"${price:,.2f}"
119
+ elif price >= 1:
120
+ return f"${price:,.4f}"
121
+ else:
122
+ return f"${price:,.6f}"
123
+
124
+
125
+ def format_market_cap(market_cap: float) -> str:
126
+ """Format market cap in trillions/billions/millions."""
127
+ if market_cap >= 1e12:
128
+ return f"${market_cap/1e12:.2f}T"
129
+ elif market_cap >= 1e9:
130
+ return f"${market_cap/1e9:.2f}B"
131
+ elif market_cap >= 1e6:
132
+ return f"${market_cap/1e6:.2f}M"
133
+ else:
134
+ return f"${market_cap:,.0f}"
135
+
136
+
137
+ def format_volume(volume: float) -> str:
138
+ """Format volume in billions/millions."""
139
+ if volume >= 1e9:
140
+ return f"${volume/1e9:.2f}B"
141
+ elif volume >= 1e6:
142
+ return f"${volume/1e6:.2f}M"
143
+ else:
144
+ return f"${volume:,.0f}"
145
+
146
+
147
+ def print_coin(data: Dict) -> None:
148
+ """Print formatted coin data."""
149
+ if not data:
150
+ return
151
+ name = data.get('name', data['symbol'])
152
+ print(f"\n{name} ({data['symbol']})")
153
+ print(f" Price: {format_price(data['price_usd'])}")
154
+ print(f" 24h Change: {data['change_24h']:+.2f}%")
155
+ print(f" Market Cap: {format_market_cap(data['market_cap'])}")
156
+ print(f" 24h Volume: {format_volume(data['volume_24h'])}")
157
+ print(f" 24h High: {format_price(data['high_24h'])}")
158
+ print(f" 24h Low: {format_price(data['low_24h'])}")
159
+
160
+
161
+ # Backwards compatibility aliases
162
+ get_btc_data = lambda tsyms="USD": get_coin_data('BTC', tsyms)
163
+ get_btc_price = lambda tsyms="USD": get_coin_price('BTC', tsyms)
164
+
165
+
166
+ if __name__ == "__main__":
167
+ import sys
168
+
169
+ # Check for command line arguments
170
+ if len(sys.argv) > 1:
171
+ # Parse requested coins
172
+ requested = []
173
+ for arg in sys.argv[1:]:
174
+ sym = resolve_symbol(arg)
175
+ if sym:
176
+ requested.append(sym)
177
+ else:
178
+ print(f"Unknown coin: {arg}")
179
+ coins_to_fetch = requested if requested else DEFAULT_COINS
180
+ else:
181
+ coins_to_fetch = DEFAULT_COINS
182
+
183
+ # Fetch and display
184
+ print("Cryptocurrency Prices (USD)")
185
+ print("=" * 40)
186
+
187
+ results = get_all_coins(coins_to_fetch)
188
+ for sym in coins_to_fetch:
189
+ data = results.get(sym)
190
+ if data:
191
+ print_coin(data)
192
+ else:
193
+ print(f"\nFailed to fetch {sym}")
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { execFile } from "node:child_process";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const SCRIPT_PATH = join(__dirname, "get_price.py");
8
+
9
+ function runPriceScript(args: string[]): Promise<string> {
10
+ return new Promise((resolve, reject) => {
11
+ execFile("python3", [SCRIPT_PATH, ...args], { timeout: 15_000 }, (err, stdout, stderr) => {
12
+ if (err) {
13
+ reject(new Error(stderr?.trim() || err.message));
14
+ return;
15
+ }
16
+ resolve(stdout.trim());
17
+ });
18
+ });
19
+ }
20
+
21
+ const plugin = {
22
+ id: "openclaw-crypto-price",
23
+ name: "OpenClaw Crypto Price",
24
+ description: "Live cryptocurrency prices via /price command",
25
+ register(api: OpenClawPluginApi) {
26
+ const cfg = api.pluginConfig as Record<string, unknown> | undefined;
27
+ const defaultCoins = (cfg?.coins as string[]) ?? [];
28
+
29
+ api.registerCommand({
30
+ name: "price",
31
+ description: "Fetch live cryptocurrency prices (BTC, PAXG, XMR, ZANO)",
32
+ acceptsArgs: true,
33
+ requireAuth: false,
34
+ handler: async (ctx) => {
35
+ const userArgs = ctx.args?.trim() ?? "";
36
+ const args = userArgs.length > 0
37
+ ? userArgs.split(/\s+/)
38
+ : defaultCoins;
39
+
40
+ try {
41
+ const output = await runPriceScript(args);
42
+ return { text: output || "No data returned." };
43
+ } catch (err) {
44
+ return { text: `Error fetching prices: ${String(err)}` };
45
+ }
46
+ },
47
+ });
48
+ },
49
+ };
50
+
51
+ export default plugin;