@xpr-agents/openclaw 0.3.1 → 0.4.0
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 +51 -10
- package/openclaw.plugin.json +15 -1
- package/package.json +7 -4
- package/skills/code-sandbox/SKILL.md +30 -0
- package/skills/code-sandbox/dist/index.js +188 -0
- package/skills/code-sandbox/skill.json +13 -0
- package/skills/code-sandbox/src/index.ts +212 -0
- package/skills/creative/SKILL.md +32 -0
- package/skills/creative/dist/index.js +667 -0
- package/skills/creative/skill.json +13 -0
- package/skills/creative/src/index.ts +679 -0
- package/skills/defi/SKILL.md +123 -0
- package/skills/defi/dist/index.js +1745 -0
- package/skills/defi/skill.json +44 -0
- package/skills/defi/src/index.ts +1788 -0
- package/skills/defi/test-read.mjs +281 -0
- package/skills/governance/SKILL.md +69 -0
- package/skills/governance/dist/index.js +632 -0
- package/skills/governance/skill.json +21 -0
- package/skills/governance/src/index.ts +656 -0
- package/skills/governance/test-read.mjs +176 -0
- package/skills/lending/SKILL.md +63 -0
- package/skills/lending/dist/index.js +1039 -0
- package/skills/lending/skill.json +29 -0
- package/skills/lending/src/index.ts +1105 -0
- package/skills/lending/test-read.mjs +156 -0
- package/skills/nft/SKILL.md +95 -0
- package/skills/nft/dist/index.js +1520 -0
- package/skills/nft/skill.json +37 -0
- package/skills/nft/src/index.ts +1539 -0
- package/skills/shellbook/SKILL.md +59 -0
- package/skills/shellbook/dist/index.js +381 -0
- package/skills/shellbook/skill.json +29 -0
- package/skills/shellbook/src/index.ts +391 -0
- package/skills/shellbook/tsconfig.json +14 -0
- package/skills/smart-contracts/SKILL.md +128 -0
- package/skills/smart-contracts/dist/index.js +1225 -0
- package/skills/smart-contracts/skill.json +25 -0
- package/skills/smart-contracts/src/index.ts +1327 -0
- package/skills/smart-contracts/tsconfig.json +14 -0
- package/skills/structured-data/SKILL.md +36 -0
- package/skills/structured-data/dist/index.js +501 -0
- package/skills/structured-data/skill.json +13 -0
- package/skills/structured-data/src/index.ts +597 -0
- package/skills/tax/SKILL.md +109 -0
- package/skills/tax/dist/index.js +1749 -0
- package/skills/tax/skill.json +20 -0
- package/skills/tax/src/index.ts +1985 -0
- package/skills/web-scraping/SKILL.md +29 -0
- package/skills/web-scraping/dist/index.js +311 -0
- package/skills/web-scraping/skill.json +13 -0
- package/skills/web-scraping/src/index.ts +371 -0
- package/skills/xmd/SKILL.md +52 -0
- package/skills/xmd/dist/index.js +596 -0
- package/skills/xmd/skill.json +22 -0
- package/skills/xmd/src/index.ts +635 -0
- package/skills/xmd/test-read.mjs +178 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Scraping Skill — fetch, parse, and extract data from web pages
|
|
3
|
+
*
|
|
4
|
+
* Zero external dependencies — uses Node.js built-in fetch and regex-based HTML parsing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface ToolDef {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
parameters: { type: 'object'; required?: string[]; properties: Record<string, unknown> };
|
|
11
|
+
handler: (params: any) => Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SkillApi {
|
|
15
|
+
registerTool(tool: ToolDef): void;
|
|
16
|
+
getConfig(): Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── Constants ───────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const MAX_BODY_SIZE = 5 * 1024 * 1024; // 5MB
|
|
22
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
23
|
+
const USER_AGENT = 'XPR-Agent/1.0 (web-scraping-skill)';
|
|
24
|
+
|
|
25
|
+
// ── Shared helpers ──────────────────────────────
|
|
26
|
+
|
|
27
|
+
async function fetchPage(
|
|
28
|
+
url: string,
|
|
29
|
+
timeout: number = DEFAULT_TIMEOUT,
|
|
30
|
+
headers?: Record<string, string>,
|
|
31
|
+
): Promise<{ html: string; status: number; contentType: string; finalUrl: string }> {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const resp = await fetch(url, {
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
redirect: 'follow',
|
|
39
|
+
headers: {
|
|
40
|
+
'User-Agent': USER_AGENT,
|
|
41
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
42
|
+
...headers,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
47
|
+
const contentLength = parseInt(resp.headers.get('content-length') || '0');
|
|
48
|
+
if (contentLength > MAX_BODY_SIZE) {
|
|
49
|
+
throw new Error(`Response too large: ${contentLength} bytes (max ${MAX_BODY_SIZE})`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const html = await resp.text();
|
|
53
|
+
if (html.length > MAX_BODY_SIZE) {
|
|
54
|
+
throw new Error(`Response body too large: ${html.length} chars (max ${MAX_BODY_SIZE})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
html,
|
|
59
|
+
status: resp.status,
|
|
60
|
+
contentType: contentType.split(';')[0].trim(),
|
|
61
|
+
finalUrl: resp.url || url,
|
|
62
|
+
};
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function decodeEntities(text: string): string {
|
|
69
|
+
return text
|
|
70
|
+
.replace(/&/g, '&')
|
|
71
|
+
.replace(/</g, '<')
|
|
72
|
+
.replace(/>/g, '>')
|
|
73
|
+
.replace(/"/g, '"')
|
|
74
|
+
.replace(/'/g, "'")
|
|
75
|
+
.replace(/'/g, "'")
|
|
76
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n)))
|
|
77
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)))
|
|
78
|
+
.replace(/ /g, ' ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function stripHtml(html: string): string {
|
|
82
|
+
// Remove script, style, and noscript blocks
|
|
83
|
+
let text = html.replace(/<script[\s\S]*?<\/script>/gi, '');
|
|
84
|
+
text = text.replace(/<style[\s\S]*?<\/style>/gi, '');
|
|
85
|
+
text = text.replace(/<noscript[\s\S]*?<\/noscript>/gi, '');
|
|
86
|
+
// Remove HTML comments
|
|
87
|
+
text = text.replace(/<!--[\s\S]*?-->/g, '');
|
|
88
|
+
// Remove tags
|
|
89
|
+
text = text.replace(/<[^>]+>/g, ' ');
|
|
90
|
+
// Decode entities
|
|
91
|
+
text = decodeEntities(text);
|
|
92
|
+
// Normalize whitespace
|
|
93
|
+
text = text.replace(/[ \t]+/g, ' ');
|
|
94
|
+
text = text.replace(/\n\s*\n\s*\n/g, '\n\n');
|
|
95
|
+
return text.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function htmlToMarkdown(html: string): string {
|
|
99
|
+
// Remove script/style/noscript
|
|
100
|
+
let md = html.replace(/<script[\s\S]*?<\/script>/gi, '');
|
|
101
|
+
md = md.replace(/<style[\s\S]*?<\/style>/gi, '');
|
|
102
|
+
md = md.replace(/<noscript[\s\S]*?<\/noscript>/gi, '');
|
|
103
|
+
md = md.replace(/<!--[\s\S]*?-->/g, '');
|
|
104
|
+
|
|
105
|
+
// Headings
|
|
106
|
+
md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `# ${stripHtml(c)}\n\n`);
|
|
107
|
+
md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, (_, c) => `## ${stripHtml(c)}\n\n`);
|
|
108
|
+
md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, (_, c) => `### ${stripHtml(c)}\n\n`);
|
|
109
|
+
md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, (_, c) => `#### ${stripHtml(c)}\n\n`);
|
|
110
|
+
md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, (_, c) => `##### ${stripHtml(c)}\n\n`);
|
|
111
|
+
md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, (_, c) => `###### ${stripHtml(c)}\n\n`);
|
|
112
|
+
|
|
113
|
+
// Bold / italic
|
|
114
|
+
md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**');
|
|
115
|
+
md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*');
|
|
116
|
+
|
|
117
|
+
// Links
|
|
118
|
+
md = md.replace(/<a\s+[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
|
|
119
|
+
const cleanText = stripHtml(text);
|
|
120
|
+
return `[${cleanText}](${href})`;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// List items
|
|
124
|
+
md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, c) => `- ${stripHtml(c)}\n`);
|
|
125
|
+
|
|
126
|
+
// Paragraphs / line breaks
|
|
127
|
+
md = md.replace(/<br\s*\/?>/gi, '\n');
|
|
128
|
+
md = md.replace(/<\/p>/gi, '\n\n');
|
|
129
|
+
md = md.replace(/<p[^>]*>/gi, '');
|
|
130
|
+
|
|
131
|
+
// Block-level elements → newlines
|
|
132
|
+
md = md.replace(/<\/(div|section|article|header|footer|main|nav)>/gi, '\n');
|
|
133
|
+
|
|
134
|
+
// Remove remaining tags
|
|
135
|
+
md = md.replace(/<[^>]+>/g, '');
|
|
136
|
+
|
|
137
|
+
// Decode entities
|
|
138
|
+
md = decodeEntities(md);
|
|
139
|
+
|
|
140
|
+
// Clean up whitespace
|
|
141
|
+
md = md.replace(/[ \t]+/g, ' ');
|
|
142
|
+
md = md.replace(/\n{3,}/g, '\n\n');
|
|
143
|
+
return md.trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractTitle(html: string): string {
|
|
147
|
+
const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
148
|
+
return match ? decodeEntities(match[1].trim()) : '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractMetaDescription(html: string): string {
|
|
152
|
+
const match = html.match(/<meta\s+[^>]*name=["']description["'][^>]*content=["']([^"']*)["'][^>]*>/i)
|
|
153
|
+
|| html.match(/<meta\s+[^>]*content=["']([^"']*)["'][^>]*name=["']description["'][^>]*>/i);
|
|
154
|
+
return match ? decodeEntities(match[1].trim()) : '';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveUrl(href: string, base: string): string {
|
|
158
|
+
try {
|
|
159
|
+
return new URL(href, base).href;
|
|
160
|
+
} catch {
|
|
161
|
+
return href;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface ExtractedLink {
|
|
166
|
+
href: string;
|
|
167
|
+
text: string;
|
|
168
|
+
type: 'internal' | 'external';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extractLinksFromHtml(html: string, baseUrl: string): ExtractedLink[] {
|
|
172
|
+
const links: ExtractedLink[] = [];
|
|
173
|
+
const seen = new Set<string>();
|
|
174
|
+
const re = /<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi;
|
|
175
|
+
let match;
|
|
176
|
+
|
|
177
|
+
let baseOrigin: string;
|
|
178
|
+
try {
|
|
179
|
+
baseOrigin = new URL(baseUrl).origin;
|
|
180
|
+
} catch {
|
|
181
|
+
baseOrigin = '';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
while ((match = re.exec(html)) !== null) {
|
|
185
|
+
const rawHref = match[1].trim();
|
|
186
|
+
// Skip anchors, javascript:, mailto:, tel:
|
|
187
|
+
if (rawHref.startsWith('#') || rawHref.startsWith('javascript:') || rawHref.startsWith('mailto:') || rawHref.startsWith('tel:')) continue;
|
|
188
|
+
|
|
189
|
+
const absoluteHref = resolveUrl(rawHref, baseUrl);
|
|
190
|
+
if (seen.has(absoluteHref)) continue;
|
|
191
|
+
seen.add(absoluteHref);
|
|
192
|
+
|
|
193
|
+
const text = stripHtml(match[2]).slice(0, 200);
|
|
194
|
+
let linkType: 'internal' | 'external' = 'external';
|
|
195
|
+
try {
|
|
196
|
+
if (new URL(absoluteHref).origin === baseOrigin) linkType = 'internal';
|
|
197
|
+
} catch { /* keep external */ }
|
|
198
|
+
|
|
199
|
+
links.push({ href: absoluteHref, text, type: linkType });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return links;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Skill entry point ───────────────────────────
|
|
206
|
+
|
|
207
|
+
export default function webScrapingSkill(api: SkillApi): void {
|
|
208
|
+
// ── scrape_url ──
|
|
209
|
+
api.registerTool({
|
|
210
|
+
name: 'scrape_url',
|
|
211
|
+
description: [
|
|
212
|
+
'Fetch a web page and return its text content with metadata.',
|
|
213
|
+
'Strips HTML tags by default (format="text"). Use format="markdown" to preserve structure,',
|
|
214
|
+
'or format="html" to get raw HTML. Max response size: 5MB.',
|
|
215
|
+
].join(' '),
|
|
216
|
+
parameters: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
required: ['url'],
|
|
219
|
+
properties: {
|
|
220
|
+
url: { type: 'string', description: 'URL to fetch' },
|
|
221
|
+
format: { type: 'string', description: '"text" (default), "markdown", or "html"' },
|
|
222
|
+
timeout: { type: 'number', description: 'Request timeout in milliseconds (default 30000)' },
|
|
223
|
+
headers: { type: 'object', description: 'Optional custom HTTP headers' },
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
handler: async ({ url, format, timeout, headers }: {
|
|
227
|
+
url: string; format?: string; timeout?: number; headers?: Record<string, string>;
|
|
228
|
+
}) => {
|
|
229
|
+
if (!url || !/^https?:\/\//i.test(url)) {
|
|
230
|
+
return { error: 'Invalid URL. Must start with http:// or https://' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const page = await fetchPage(url, timeout || DEFAULT_TIMEOUT, headers);
|
|
235
|
+
|
|
236
|
+
let text: string;
|
|
237
|
+
const fmt = (format || 'text').toLowerCase();
|
|
238
|
+
if (fmt === 'html') {
|
|
239
|
+
text = page.html;
|
|
240
|
+
} else if (fmt === 'markdown') {
|
|
241
|
+
text = htmlToMarkdown(page.html);
|
|
242
|
+
} else {
|
|
243
|
+
text = stripHtml(page.html);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const linkCount = (page.html.match(/<a\s+[^>]*href=/gi) || []).length;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
url: page.finalUrl,
|
|
250
|
+
title: extractTitle(page.html),
|
|
251
|
+
description: extractMetaDescription(page.html),
|
|
252
|
+
text,
|
|
253
|
+
links_count: linkCount,
|
|
254
|
+
content_length: text.length,
|
|
255
|
+
status: page.status,
|
|
256
|
+
content_type: page.contentType,
|
|
257
|
+
};
|
|
258
|
+
} catch (err: any) {
|
|
259
|
+
return { error: `Failed to fetch ${url}: ${err.message}` };
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ── extract_links ──
|
|
265
|
+
api.registerTool({
|
|
266
|
+
name: 'extract_links',
|
|
267
|
+
description: [
|
|
268
|
+
'Fetch a web page and extract all links with context.',
|
|
269
|
+
'Returns resolved absolute URLs, link text, and internal/external classification.',
|
|
270
|
+
'Use pattern parameter to filter links by regex on href.',
|
|
271
|
+
].join(' '),
|
|
272
|
+
parameters: {
|
|
273
|
+
type: 'object',
|
|
274
|
+
required: ['url'],
|
|
275
|
+
properties: {
|
|
276
|
+
url: { type: 'string', description: 'URL to fetch and extract links from' },
|
|
277
|
+
pattern: { type: 'string', description: 'Optional regex pattern to filter link hrefs' },
|
|
278
|
+
limit: { type: 'number', description: 'Maximum links to return (default 50)' },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
handler: async ({ url, pattern, limit }: {
|
|
282
|
+
url: string; pattern?: string; limit?: number;
|
|
283
|
+
}) => {
|
|
284
|
+
if (!url || !/^https?:\/\//i.test(url)) {
|
|
285
|
+
return { error: 'Invalid URL. Must start with http:// or https://' };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const page = await fetchPage(url);
|
|
290
|
+
let links = extractLinksFromHtml(page.html, page.finalUrl);
|
|
291
|
+
|
|
292
|
+
if (pattern) {
|
|
293
|
+
try {
|
|
294
|
+
const re = new RegExp(pattern, 'i');
|
|
295
|
+
links = links.filter(l => re.test(l.href));
|
|
296
|
+
} catch (err: any) {
|
|
297
|
+
return { error: `Invalid regex pattern: ${err.message}` };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const maxLinks = Math.min(limit || 50, 200);
|
|
302
|
+
const total = links.length;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
url: page.finalUrl,
|
|
306
|
+
links: links.slice(0, maxLinks),
|
|
307
|
+
total_found: total,
|
|
308
|
+
returned: Math.min(total, maxLinks),
|
|
309
|
+
};
|
|
310
|
+
} catch (err: any) {
|
|
311
|
+
return { error: `Failed to fetch ${url}: ${err.message}` };
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ── scrape_multiple ──
|
|
317
|
+
api.registerTool({
|
|
318
|
+
name: 'scrape_multiple',
|
|
319
|
+
description: [
|
|
320
|
+
'Fetch multiple URLs in parallel and return their text content.',
|
|
321
|
+
'Useful for research and comparison tasks. Max 10 URLs per call.',
|
|
322
|
+
'Uses Promise.allSettled so one failure does not block others.',
|
|
323
|
+
].join(' '),
|
|
324
|
+
parameters: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
required: ['urls'],
|
|
327
|
+
properties: {
|
|
328
|
+
urls: { type: 'array', description: 'Array of URLs to fetch (max 10)', items: { type: 'string' } },
|
|
329
|
+
format: { type: 'string', description: '"text" (default) or "html"' },
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
handler: async ({ urls, format }: { urls: string[]; format?: string }) => {
|
|
333
|
+
if (!Array.isArray(urls) || urls.length === 0) {
|
|
334
|
+
return { error: 'urls must be a non-empty array of strings' };
|
|
335
|
+
}
|
|
336
|
+
if (urls.length > 10) {
|
|
337
|
+
return { error: 'Maximum 10 URLs per call' };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const fmt = (format || 'text').toLowerCase();
|
|
341
|
+
|
|
342
|
+
const results = await Promise.allSettled(
|
|
343
|
+
urls.map(async (url) => {
|
|
344
|
+
if (!url || !/^https?:\/\//i.test(url)) {
|
|
345
|
+
throw new Error('Invalid URL');
|
|
346
|
+
}
|
|
347
|
+
const page = await fetchPage(url);
|
|
348
|
+
const text = fmt === 'html' ? page.html : stripHtml(page.html);
|
|
349
|
+
return {
|
|
350
|
+
url: page.finalUrl,
|
|
351
|
+
title: extractTitle(page.html),
|
|
352
|
+
text,
|
|
353
|
+
status: page.status,
|
|
354
|
+
};
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const items = results.map((r, i) => {
|
|
359
|
+
if (r.status === 'fulfilled') {
|
|
360
|
+
return r.value;
|
|
361
|
+
}
|
|
362
|
+
return { url: urls[i], title: '', text: '', status: 0, error: r.reason?.message || 'Unknown error' };
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const succeeded = items.filter(r => !('error' in r)).length;
|
|
366
|
+
const failed = items.length - succeeded;
|
|
367
|
+
|
|
368
|
+
return { results: items, succeeded, failed };
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xmd
|
|
3
|
+
description: Metal Dollar (XMD) stablecoin — mint, redeem, supply analytics, collateral reserves, oracle prices
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Metal Dollar (XMD)
|
|
7
|
+
|
|
8
|
+
You have tools to interact with XMD, XPR Network's native stablecoin. XMD is a multi-collateral stablecoin pegged to $1 USD, minted and redeemed through the `xmd.treasury` contract.
|
|
9
|
+
|
|
10
|
+
### How XMD Works
|
|
11
|
+
|
|
12
|
+
- **Mint:** Send a supported stablecoin (e.g. XUSDC) to `xmd.treasury` with memo `mint` → receive equivalent XMD at oracle price
|
|
13
|
+
- **Redeem:** Send XMD to `xmd.treasury` with memo `redeem,SYMBOL` (e.g. `redeem,XUSDC`) → receive equivalent stablecoin back
|
|
14
|
+
- **1:1 peg:** Oracle-priced at $1, backed by stablecoin reserves in the treasury
|
|
15
|
+
- **Zero fees:** Currently 0% mint and redemption fees on all collateral types
|
|
16
|
+
|
|
17
|
+
### Supported Collateral
|
|
18
|
+
|
|
19
|
+
| Token | Contract | Oracle Feed | Max Treasury % | Status |
|
|
20
|
+
|-------|----------|-------------|----------------|--------|
|
|
21
|
+
| XUSDC | xtokens | USDC/USD | 60% | Mint + Redeem |
|
|
22
|
+
| XPAX | xtokens | PAX/USD | 15% | Mint + Redeem |
|
|
23
|
+
| XPYUSD | xtokens | PYUSD/USD | 15% | Mint + Redeem |
|
|
24
|
+
| MPD | mpd.token | MPD/USD | 2% | Mint + Redeem |
|
|
25
|
+
|
|
26
|
+
### Contracts
|
|
27
|
+
|
|
28
|
+
- `xmd.token` — XMD token contract (precision 6, issuer = xmd.treasury)
|
|
29
|
+
- `xmd.treasury` — Mint/redeem logic, collateral management, oracle integration
|
|
30
|
+
- `oracles` — On-chain price feeds from multiple providers
|
|
31
|
+
|
|
32
|
+
### Read-Only Tools (safe, no signing)
|
|
33
|
+
|
|
34
|
+
- `xmd_get_config` — treasury config: paused state, fee account, minimum oracle price threshold
|
|
35
|
+
- `xmd_list_collateral` — all supported collateral tokens with fees, limits, oracle prices, mint/redeem volumes
|
|
36
|
+
- `xmd_get_supply` — XMD total circulating supply
|
|
37
|
+
- `xmd_get_balance` — check any account's XMD balance
|
|
38
|
+
- `xmd_get_treasury_reserves` — current stablecoin reserves backing XMD, with USD valuations and collateralization ratio
|
|
39
|
+
- `xmd_get_oracle_price` — current oracle price for any collateral token (with individual provider data)
|
|
40
|
+
|
|
41
|
+
### Write Tools (require `confirmed: true`)
|
|
42
|
+
|
|
43
|
+
- `xmd_mint` — mint XMD by depositing a supported stablecoin
|
|
44
|
+
- `xmd_redeem` — redeem XMD for a supported stablecoin
|
|
45
|
+
|
|
46
|
+
### Safety Rules
|
|
47
|
+
|
|
48
|
+
- Oracle price must be >= 0.995 (`minOraclePrice`) for mint/redeem to proceed
|
|
49
|
+
- Each collateral has a `maxTreasuryPercent` cap — if the treasury already holds too much of one stablecoin, minting with it is blocked
|
|
50
|
+
- Check `isMintEnabled` / `isRedeemEnabled` before attempting operations
|
|
51
|
+
- The treasury can be paused by admins (`isPaused`) — check config first
|
|
52
|
+
- XMD has precision 6 — all amounts use 6 decimal places (e.g. `1.000000 XMD`)
|