@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 +55 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +35 -0
- package/scripts/get_price.py +193 -0
- package/src/index.ts +51 -0
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;
|