openclaw-looloo 0.1.1
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 +77 -0
- package/openclaw.plugin.json +12 -0
- package/package.json +35 -0
- package/skills/looloo-discovery/SKILL.md +27 -0
- package/skills/looloo-trading/SKILL.md +26 -0
- package/src/index.mjs +270 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# openclaw-looloo
|
|
2
|
+
|
|
3
|
+
`openclaw-looloo` is the phase 1 OpenClaw plugin for LooLoo.
|
|
4
|
+
|
|
5
|
+
It lets users:
|
|
6
|
+
|
|
7
|
+
- discover new internal-market tokens
|
|
8
|
+
- inspect token summaries
|
|
9
|
+
- check current positions
|
|
10
|
+
- fetch buy and sell quotes
|
|
11
|
+
- create website confirmation links for trades
|
|
12
|
+
|
|
13
|
+
## Phase 1 scope
|
|
14
|
+
|
|
15
|
+
This package is intentionally limited to the phase 1 workflow:
|
|
16
|
+
|
|
17
|
+
- `OpenClaw` handles discovery, quote lookup, and trade intent creation
|
|
18
|
+
- `looloo.lol` handles final wallet confirmation
|
|
19
|
+
|
|
20
|
+
This plugin does **not**:
|
|
21
|
+
|
|
22
|
+
- sign transactions directly
|
|
23
|
+
- submit onchain trades directly
|
|
24
|
+
- run unattended auto-trading
|
|
25
|
+
- launch new tokens
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
openclaw plugins install openclaw-looloo
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configure
|
|
34
|
+
|
|
35
|
+
Create an automation key from `looloo.lol`:
|
|
36
|
+
|
|
37
|
+
1. Open `Settings -> OpenClaw`
|
|
38
|
+
2. Create an automation key
|
|
39
|
+
3. Copy the key once when shown
|
|
40
|
+
|
|
41
|
+
Set the plugin config or environment variables:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export LOOLOO_API_BASE="https://looloo.lol"
|
|
45
|
+
export LOOLOO_AUTOMATION_KEY="llak_..."
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Tools
|
|
49
|
+
|
|
50
|
+
- `discover_new_tokens`
|
|
51
|
+
- `get_token_summary`
|
|
52
|
+
- `get_positions`
|
|
53
|
+
- `quote_trade`
|
|
54
|
+
- `create_trade_intent`
|
|
55
|
+
- `get_automation_profile`
|
|
56
|
+
|
|
57
|
+
## Example flow
|
|
58
|
+
|
|
59
|
+
1. Discover or inspect a token from chat.
|
|
60
|
+
2. Ask for a quote.
|
|
61
|
+
3. Create a trade intent.
|
|
62
|
+
4. Open the returned `confirmUrl`.
|
|
63
|
+
5. Confirm the transaction in `looloo.lol` with the user's wallet.
|
|
64
|
+
|
|
65
|
+
## Example prompts
|
|
66
|
+
|
|
67
|
+
- `Scan the newest LooLoo internal-market tokens`
|
|
68
|
+
- `Show me a summary for 0xTokenAddress`
|
|
69
|
+
- `Quote a buy of 0.05 ETH for 0xTokenAddress`
|
|
70
|
+
- `Create a trade intent to sell 100000 tokens`
|
|
71
|
+
- `Show my current LooLoo positions`
|
|
72
|
+
|
|
73
|
+
## Notes
|
|
74
|
+
|
|
75
|
+
- Phase 1 only supports the `internal` market.
|
|
76
|
+
- Notifications should use the user's existing OpenClaw chat channel.
|
|
77
|
+
- The plugin is an execution helper, not a profit guarantee or autonomous trading bot.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"name": "openclaw-looloo",
|
|
4
|
+
"displayName": "LooLoo",
|
|
5
|
+
"version": "0.1.1",
|
|
6
|
+
"description": "Phase 1 LooLoo plugin for token discovery, quotes, positions, and website trade confirmation links.",
|
|
7
|
+
"entry": "./src/index.mjs",
|
|
8
|
+
"skills": [
|
|
9
|
+
"./skills/looloo-discovery/SKILL.md",
|
|
10
|
+
"./skills/looloo-trading/SKILL.md"
|
|
11
|
+
]
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-looloo",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Phase 1 OpenClaw plugin for LooLoo discovery, quotes, positions, and trade intents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.mjs",
|
|
7
|
+
"openclaw.extensions": [
|
|
8
|
+
"./openclaw.plugin.json"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
"openclaw.plugin.json",
|
|
12
|
+
"README.md",
|
|
13
|
+
"src",
|
|
14
|
+
"skills"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"lint": "node --check ./src/index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"openclaw",
|
|
24
|
+
"looloo",
|
|
25
|
+
"plugin",
|
|
26
|
+
"skills",
|
|
27
|
+
"trading",
|
|
28
|
+
"automation",
|
|
29
|
+
"phase-1"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"openclaw-looloo": "file:openclaw-looloo-0.1.1.tgz"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: looloo-discovery
|
|
3
|
+
description: Use this skill when the user wants to discover new LooLoo tokens, inspect token activity, or review current positions from OpenClaw.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# LooLoo Discovery
|
|
7
|
+
|
|
8
|
+
Use the bundled `openclaw-looloo` plugin tools instead of calling the raw API directly.
|
|
9
|
+
|
|
10
|
+
## Use this skill for
|
|
11
|
+
|
|
12
|
+
- scanning new internal-market tokens
|
|
13
|
+
- reviewing one token's recent trades and top holders
|
|
14
|
+
- checking the authenticated wallet's current positions
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
1. Call `discover_new_tokens` for a market-wide scan.
|
|
19
|
+
2. If the user narrows to one token, call `get_token_summary`.
|
|
20
|
+
3. If the user asks about their wallet, call `get_positions`.
|
|
21
|
+
4. Summarize the strongest signals briefly and keep raw JSON available if needed.
|
|
22
|
+
|
|
23
|
+
## Notes
|
|
24
|
+
|
|
25
|
+
- Phase 1 only supports the `internal` market.
|
|
26
|
+
- OpenClaw should return notifications through the user's existing chat channel.
|
|
27
|
+
- Do not imply guaranteed profits; frame results as discovery and execution support.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: looloo-trading
|
|
3
|
+
description: Use this skill when the user wants a LooLoo trade quote or a website confirmation link for a buy or sell.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# LooLoo Trading
|
|
7
|
+
|
|
8
|
+
Phase 1 trading is quote-first and wallet-confirmed.
|
|
9
|
+
|
|
10
|
+
## Use this skill for
|
|
11
|
+
|
|
12
|
+
- quoting a buy or sell on the LooLoo internal market
|
|
13
|
+
- creating a confirmation link that sends the user back to `looloo.lol`
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. Call `quote_trade` with `tokenAddress`, `side`, and either `amountETH` or `amountToken`.
|
|
18
|
+
2. Show the quote summary and confirm the side and size with the user.
|
|
19
|
+
3. Call `create_trade_intent` only after the user explicitly wants to continue.
|
|
20
|
+
4. Return the `confirmUrl` and explain that the final wallet signature happens on the LooLoo website.
|
|
21
|
+
|
|
22
|
+
## Notes
|
|
23
|
+
|
|
24
|
+
- Phase 1 does not support unattended execution.
|
|
25
|
+
- The plugin does not hold keys or sign transactions.
|
|
26
|
+
- If the API rejects an intent, surface the policy error directly because it usually comes from the user's risk settings.
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
const DEFAULT_API_BASE = "https://looloo.lol";
|
|
2
|
+
|
|
3
|
+
function getConfig(overrides = {}) {
|
|
4
|
+
const apiBase = (overrides.apiBase || process.env.LOOLOO_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
5
|
+
const apiKey = overrides.apiKey || process.env.LOOLOO_AUTOMATION_KEY || "";
|
|
6
|
+
if (!apiKey) {
|
|
7
|
+
throw new Error("LOOLOO_AUTOMATION_KEY is required");
|
|
8
|
+
}
|
|
9
|
+
return { apiBase, apiKey };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function request(path, options = {}, overrides = {}) {
|
|
13
|
+
const { apiBase, apiKey } = getConfig(overrides);
|
|
14
|
+
const url = `${apiBase}${path}`;
|
|
15
|
+
const headers = {
|
|
16
|
+
Accept: "application/json",
|
|
17
|
+
Authorization: `Bearer ${apiKey}`,
|
|
18
|
+
...options.headers,
|
|
19
|
+
};
|
|
20
|
+
if (options.body && !headers["Content-Type"]) {
|
|
21
|
+
headers["Content-Type"] = "application/json";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
...options,
|
|
26
|
+
headers,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const raw = await response.text();
|
|
30
|
+
let data = null;
|
|
31
|
+
if (raw) {
|
|
32
|
+
try {
|
|
33
|
+
data = JSON.parse(raw);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(`Unexpected response from ${path}: ${raw}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const message =
|
|
41
|
+
(data && (data.error || data.message || data.details)) ||
|
|
42
|
+
response.statusText ||
|
|
43
|
+
`HTTP ${response.status}`;
|
|
44
|
+
throw new Error(message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toQuery(params = {}) {
|
|
51
|
+
const entries = Object.entries(params).filter(([, value]) => value !== undefined && value !== null && value !== "");
|
|
52
|
+
if (entries.length === 0) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
const query = new URLSearchParams();
|
|
56
|
+
for (const [key, value] of entries) {
|
|
57
|
+
query.set(key, String(value));
|
|
58
|
+
}
|
|
59
|
+
return `?${query.toString()}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function discoverNewTokens(input = {}, overrides = {}) {
|
|
63
|
+
const params = {
|
|
64
|
+
limit: input.limit,
|
|
65
|
+
market: input.market || "internal",
|
|
66
|
+
};
|
|
67
|
+
return request(`/v1/automation/discover${toQuery(params)}`, { method: "GET" }, overrides);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function getTokenSummary(input = {}, overrides = {}) {
|
|
71
|
+
if (!input.tokenAddress) {
|
|
72
|
+
throw new Error("tokenAddress is required");
|
|
73
|
+
}
|
|
74
|
+
return request(`/v1/automation/tokens/${encodeURIComponent(input.tokenAddress)}`, { method: "GET" }, overrides);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getPositions(input = {}, overrides = {}) {
|
|
78
|
+
const params = {
|
|
79
|
+
limit: input.limit,
|
|
80
|
+
market: input.market || "internal",
|
|
81
|
+
};
|
|
82
|
+
return request(`/v1/automation/positions${toQuery(params)}`, { method: "GET" }, overrides);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function quoteTrade(input = {}, overrides = {}) {
|
|
86
|
+
if (!input.tokenAddress) {
|
|
87
|
+
throw new Error("tokenAddress is required");
|
|
88
|
+
}
|
|
89
|
+
if (!input.side) {
|
|
90
|
+
throw new Error("side is required");
|
|
91
|
+
}
|
|
92
|
+
return request(
|
|
93
|
+
"/v1/automation/trades/quote",
|
|
94
|
+
{
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
tokenAddress: input.tokenAddress,
|
|
98
|
+
side: input.side,
|
|
99
|
+
amountETH: input.amountETH || "",
|
|
100
|
+
amountToken: input.amountToken || "",
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
overrides,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function createTradeIntent(input = {}, overrides = {}) {
|
|
108
|
+
if (!input.tokenAddress) {
|
|
109
|
+
throw new Error("tokenAddress is required");
|
|
110
|
+
}
|
|
111
|
+
if (!input.side) {
|
|
112
|
+
throw new Error("side is required");
|
|
113
|
+
}
|
|
114
|
+
return request(
|
|
115
|
+
"/v1/automation/trade-intents",
|
|
116
|
+
{
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
tokenAddress: input.tokenAddress,
|
|
120
|
+
side: input.side,
|
|
121
|
+
amountETH: input.amountETH || "",
|
|
122
|
+
amountToken: input.amountToken || "",
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
overrides,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function getAutomationProfile(_input = {}, overrides = {}) {
|
|
130
|
+
return request("/v1/automation/me", { method: "GET" }, overrides);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function jsonResult(data) {
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "json",
|
|
138
|
+
json: data,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const tools = [
|
|
145
|
+
{
|
|
146
|
+
name: "discover_new_tokens",
|
|
147
|
+
description: "List the newest LooLoo tokens for the internal market.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
152
|
+
market: { type: "string", enum: ["internal"] },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
async run(input, context) {
|
|
156
|
+
return jsonResult(await discoverNewTokens(input, context?.config));
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "get_token_summary",
|
|
161
|
+
description: "Fetch one token summary with latest trades and top holders.",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object",
|
|
164
|
+
required: ["tokenAddress"],
|
|
165
|
+
properties: {
|
|
166
|
+
tokenAddress: { type: "string" },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
async run(input, context) {
|
|
170
|
+
return jsonResult(await getTokenSummary(input, context?.config));
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "get_positions",
|
|
175
|
+
description: "Fetch the authenticated wallet's current LooLoo positions.",
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
180
|
+
market: { type: "string", enum: ["internal"] },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
async run(input, context) {
|
|
184
|
+
return jsonResult(await getPositions(input, context?.config));
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "quote_trade",
|
|
189
|
+
description: "Get a quote for a buy or sell on the LooLoo internal market.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
required: ["tokenAddress", "side"],
|
|
193
|
+
properties: {
|
|
194
|
+
tokenAddress: { type: "string" },
|
|
195
|
+
side: { type: "string", enum: ["buy", "sell"] },
|
|
196
|
+
amountETH: { type: "string" },
|
|
197
|
+
amountToken: { type: "string" },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
async run(input, context) {
|
|
201
|
+
return jsonResult(await quoteTrade(input, context?.config));
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "create_trade_intent",
|
|
206
|
+
description: "Create a website confirmation link for a quoted trade.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
required: ["tokenAddress", "side"],
|
|
210
|
+
properties: {
|
|
211
|
+
tokenAddress: { type: "string" },
|
|
212
|
+
side: { type: "string", enum: ["buy", "sell"] },
|
|
213
|
+
amountETH: { type: "string" },
|
|
214
|
+
amountToken: { type: "string" },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
async run(input, context) {
|
|
218
|
+
return jsonResult(await createTradeIntent(input, context?.config));
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "get_automation_profile",
|
|
223
|
+
description: "Inspect the active LooLoo automation identity and scopes.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {},
|
|
227
|
+
},
|
|
228
|
+
async run(input, context) {
|
|
229
|
+
return jsonResult(await getAutomationProfile(input, context?.config));
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
export function createPlugin(overrides = {}) {
|
|
235
|
+
return {
|
|
236
|
+
name: "openclaw-looloo",
|
|
237
|
+
configSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
apiBase: {
|
|
241
|
+
type: "string",
|
|
242
|
+
description: "Base URL for the LooLoo API.",
|
|
243
|
+
default: DEFAULT_API_BASE,
|
|
244
|
+
},
|
|
245
|
+
apiKey: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "Automation key created from looloo.lol settings.",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
required: ["apiKey"],
|
|
251
|
+
},
|
|
252
|
+
tools: tools.map((tool) => ({
|
|
253
|
+
...tool,
|
|
254
|
+
run: (input, context = {}) => tool.run(input, { ...context, config: { ...overrides, ...context.config } }),
|
|
255
|
+
})),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export {
|
|
260
|
+
DEFAULT_API_BASE,
|
|
261
|
+
createTradeIntent,
|
|
262
|
+
discoverNewTokens,
|
|
263
|
+
getAutomationProfile,
|
|
264
|
+
getPositions,
|
|
265
|
+
getTokenSummary,
|
|
266
|
+
quoteTrade,
|
|
267
|
+
request,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export default createPlugin();
|