@moonpay/cli 1.49.1 → 1.50.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.
@@ -0,0 +1,667 @@
1
+ ---
2
+ name: moonpay-artifact-builder
3
+ description: Author interactive HTML artifacts for the MoonPay Agents desktop app. Use when the user wants a custom interactive UI, a one-off dashboard, a status report, an editor, or a walkthrough that drives the MoonPay CLI under the hood. Artifacts are single HTML files that render in the app's sandboxed Artifacts view, call `mp` via window.mp.run, and inherit the host's dark design system. This skill defines the contract, the bridge API, and — primarily — the design system every artifact should use so the catalog feels like one product.
4
+ ---
5
+
6
+ # MoonPay Artifact Builder
7
+
8
+ The MoonPay Agents desktop app ships an **Artifacts** view that renders self-contained HTML files inside a sandboxed iframe. Each artifact can call the local `mp` CLI through a postMessage bridge — same way Claude/ChatGPT artifacts work, but first-party and able to invoke real money-moving operations.
9
+
10
+ When you author an artifact, the goal isn't just "an HTML file that works" — it's an artifact that looks like it belongs next to the rest of the MoonPay Agents catalog. The design system below is how you get that for free.
11
+
12
+ ## When to use this skill
13
+
14
+ The user wants a custom interactive UI for something they do recurringly: a dashboard, a status report, an editor, a walkthrough, a teaching artifact. Save to `~/.moonpay-agents/artifacts/<slug>.html` — it shows up in the app's Artifacts view on next list.
15
+
16
+ If the user wants a *procedural* workflow (a multi-step automation), that's a skill, not an artifact — invoke **moonpay-skill-builder** instead.
17
+
18
+ ## On-disk format
19
+
20
+ A single self-contained HTML file. No build step, no bundler, no external assets except CDN libs.
21
+
22
+ ### Frontmatter
23
+
24
+ The first thing in the file MUST be an HTML comment with key/value pairs:
25
+
26
+ ```html
27
+ <!--
28
+ name: my-artifact-slug
29
+ title: My Artifact Title
30
+ description: One short sentence shown on the artifact card.
31
+ tags: [explorer, portfolio]
32
+ -->
33
+ ```
34
+
35
+ | key | required | notes |
36
+ |---|---|---|
37
+ | `name` | required | URL-safe slug. Must match the filename (`<slug>.html`). |
38
+ | `title` | required | Displayed at the top of the artifact card and in the header bar. |
39
+ | `description` | required | One sentence on the card. Aim for ≤ 100 chars. |
40
+ | `tags` | optional | Comma-separated, e.g. `[explorer, tokens]`. Used for filtering. |
41
+
42
+ ### Save location
43
+
44
+ ```
45
+ ~/.moonpay-agents/artifacts/<slug>.html
46
+ ```
47
+
48
+ Drop the file in this dir and it shows up the next time the app lists artifacts. No reload needed.
49
+
50
+ (Built-in MoonPay artifacts live inside the app bundle. Don't try to write there — user artifacts always go to the path above.)
51
+
52
+ ## The bridge: `window.mp.run`
53
+
54
+ Artifacts call the MoonPay CLI through `window.mp.run(argv: string[]): Promise<string>`. The result is the raw stdout of `mp <argv...>` — usually JSON if you pass `--json`.
55
+
56
+ ```js
57
+ const data = await window.mp.run(["token", "retrieve", "--token", addr, "--chain", "polygon", "--json"]);
58
+ const token = JSON.parse(data);
59
+ ```
60
+
61
+ ### Bridge contract
62
+
63
+ - **argv strings only.** The host validates every element is a string and rejects anything else. Numbers must be `String(x)`.
64
+ - **Sandboxed iframe** with `sandbox="allow-scripts"`. No same-origin → no access to host cookies, localStorage, or storage. `fetch` to outside origins is restricted by browser CORS.
65
+ - **One-way trust**: the host has full agency, the artifact has none unless the user has approved the underlying CLI command.
66
+
67
+ ### Bridge polyfill (copy/paste into every artifact)
68
+
69
+ The host injects `window.mp.run` automatically. Include this polyfill anyway so the artifact works when previewed outside the app:
70
+
71
+ ```js
72
+ const mp = window.mp || (() => {
73
+ let nextId = 1;
74
+ const pending = new Map();
75
+ window.addEventListener("message", (e) => {
76
+ const { id, type, result, error } = e.data || {};
77
+ if (type !== "mp.result" || !pending.has(id)) return;
78
+ const { resolve, reject } = pending.get(id);
79
+ pending.delete(id);
80
+ error ? reject(new Error(error)) : resolve(result);
81
+ });
82
+ return {
83
+ run(argv) {
84
+ const id = nextId++;
85
+ return new Promise((resolve, reject) => {
86
+ pending.set(id, { resolve, reject });
87
+ window.parent.postMessage({ id, type: "mp.run", argv }, "*");
88
+ });
89
+ },
90
+ };
91
+ })();
92
+ ```
93
+
94
+ ---
95
+
96
+ # Design system
97
+
98
+ This is the meat of this skill. Use these tokens in every artifact you author.
99
+
100
+ The host (the MoonPay Agents app) injects a set of `--mp-*` CSS variables into the iframe. Inherit them via `var(--mp-thing, fallback)` so the artifact looks correct standalone too.
101
+
102
+ ## Color tokens
103
+
104
+ ```css
105
+ :root {
106
+ color-scheme: dark;
107
+
108
+ /* Surfaces */
109
+ --bg: var(--mp-bg-1, #0a0a0b); /* surface 0 — page background */
110
+ --bg-2: var(--mp-bg-2, #131316); /* surface 1 — cards, panels */
111
+ --bg-3: var(--mp-bg-3, #1c1c20); /* surface 2 — inner cards, hovered states */
112
+
113
+ /* Text */
114
+ --fg: var(--mp-fg, #e7e7ea); /* primary text */
115
+ --fg-dim: var(--mp-fg-dim, #8a8a90); /* secondary text — labels, metadata */
116
+ --fg-faint: var(--mp-fg-faint, #5a5a60); /* tertiary text — placeholders, disabled */
117
+
118
+ /* Borders & dividers */
119
+ --border: var(--mp-border, #232328); /* default border */
120
+ --border-2: var(--mp-border-2, #2e2e34); /* emphasized border (active inputs, hover) */
121
+
122
+ /* Brand */
123
+ --accent: var(--mp-accent, #7e6cff); /* MoonPay purple — primary actions, focus */
124
+ --accent-2: var(--mp-accent-2, #9d8eff); /* lighter purple — hover, gradients */
125
+ }
126
+ ```
127
+
128
+ ### Semantic colors (use directly, not via var)
129
+
130
+ | Role | Hex | When to use |
131
+ |---|---|---|
132
+ | Success / positive | `#56d364` | Gains, completed states, "Active" pills |
133
+ | Negative / loss / error | `#f47067` | Losses, error messages, declined states |
134
+ | Warning | `#f4a261` | Caution, throttle, partial state |
135
+ | Info | `#5cc8e7` | Neutral chains, metadata badges |
136
+
137
+ ## Typography
138
+
139
+ ```css
140
+ :root {
141
+ --font-sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", sans-serif;
142
+ --font-mono: ui-monospace, "JetBrains Mono", "Fira Code", monospace;
143
+
144
+ --text-xs: 11px; /* labels, badges, chart axes */
145
+ --text-sm: 12px; /* secondary text, table cells */
146
+ --text-base: 13px; /* body */
147
+ --text-md: 14px; /* primary text in lists */
148
+ --text-lg: 18px; /* section titles */
149
+ --text-xl: 22px; /* card headings */
150
+ --text-2xl: 32px; /* hero values (e.g. portfolio total) */
151
+ }
152
+ ```
153
+
154
+ Weights: **400** for body, **500** for emphasis, **600** for headings and stat values. Never 700+ in dark UI — too aggressive.
155
+
156
+ ## Spacing scale
157
+
158
+ 4px base unit. Use these as `padding` / `margin` / `gap`.
159
+
160
+ ```css
161
+ --space-1: 4px; /* tight (icon-text gap) */
162
+ --space-2: 8px; /* compact (button padding-y, chip gap) */
163
+ --space-3: 12px; /* default (card inner gap) */
164
+ --space-4: 16px; /* comfortable (card padding) */
165
+ --space-6: 24px; /* page padding */
166
+ --space-8: 32px; /* section separator */
167
+ --space-12: 48px; /* hero spacing */
168
+ ```
169
+
170
+ ## Radii
171
+
172
+ ```css
173
+ --radius-sm: 6px; /* badges, small pills */
174
+ --radius: 12px; /* cards, buttons */
175
+ --radius-lg: 16px; /* hero panels */
176
+ --radius-full: 9999px; /* status pills, avatars */
177
+ ```
178
+
179
+ ## Motion
180
+
181
+ ```css
182
+ --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
183
+ --duration-1: 120ms; /* hover state */
184
+ --duration-2: 180ms; /* card transitions */
185
+ --duration-3: 240ms; /* page-level transitions */
186
+ ```
187
+
188
+ Default to `transition: <prop> var(--duration-1) var(--ease)`. Never over 300ms.
189
+
190
+ ---
191
+
192
+ # Component recipes
193
+
194
+ These are the shapes the host app uses. Copy them into your artifact — that's how the catalog stays visually consistent.
195
+
196
+ ## Page wrapper
197
+
198
+ ```html
199
+ <body>
200
+ <div class="page">
201
+ <h1>My Artifact</h1>
202
+ <p class="sub">One-line description of what this artifact shows.</p>
203
+ <!-- … content … -->
204
+ </div>
205
+ </body>
206
+ ```
207
+
208
+ ```css
209
+ * { box-sizing: border-box; }
210
+ html,
211
+ body { margin: 0; padding: 0; }
212
+ body { background: var(--bg); color: var(--fg); font-family: var(--font-sans); }
213
+ .page { max-width: 1100px; margin: 0 auto; padding: var(--space-6); min-height: 100vh; }
214
+ h1 { font-size: var(--text-xl); font-weight: 600; margin: 0 0 4px; }
215
+ .sub { color: var(--fg-dim); font-size: var(--text-sm); margin: 0 0 var(--space-6); }
216
+ h2 { font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--fg-dim); margin: var(--space-6) 0 var(--space-3); }
217
+ ```
218
+
219
+ ## Card
220
+
221
+ ```html
222
+ <div class="card">
223
+ <div class="k">Spent this month</div>
224
+ <div class="v">$847.20</div>
225
+ <div class="sub-v">12 transactions</div>
226
+ </div>
227
+ ```
228
+
229
+ ```css
230
+ .card { background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); }
231
+ .k { color: var(--fg-dim); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
232
+ .v { font-size: var(--text-xl); font-weight: 600; font-variant-numeric: tabular-nums; }
233
+ .sub-v { color: var(--fg-dim); font-size: var(--text-xs); margin-top: 2px; }
234
+ ```
235
+
236
+ ## Stat row (4-up grid)
237
+
238
+ ```html
239
+ <div class="stat-row">
240
+ <div class="card"><div class="k">Today</div><div class="v">$120</div></div>
241
+ <div class="card"><div class="k">This week</div><div class="v">$640</div></div>
242
+ <div class="card"><div class="k">This month</div><div class="v">$2,340</div></div>
243
+ <div class="card"><div class="k">YTD</div><div class="v">$18,200</div></div>
244
+ </div>
245
+ ```
246
+
247
+ ```css
248
+ .stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); margin-bottom: var(--space-6); }
249
+ @media (max-width: 800px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
250
+ ```
251
+
252
+ ## Button
253
+
254
+ ```html
255
+ <button class="btn">Primary action</button>
256
+ <button class="btn-ghost">Secondary</button>
257
+ ```
258
+
259
+ ```css
260
+ .btn { background: var(--accent); color: white; border: 0; border-radius: var(--radius); padding: var(--space-2) var(--space-4); font-size: var(--text-sm); font-weight: 500; cursor: pointer; transition: background var(--duration-1) var(--ease); }
261
+ .btn:hover { background: var(--accent-2); }
262
+ .btn-ghost { background: transparent; color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-2) var(--space-4); font-size: var(--text-sm); cursor: pointer; transition: border-color var(--duration-1) var(--ease); }
263
+ .btn-ghost:hover { border-color: var(--border-2); }
264
+ ```
265
+
266
+ ## Status pill
267
+
268
+ ```html
269
+ <span class="pill pill-success"><span class="dot"></span>Active</span>
270
+ <span class="pill pill-warning"><span class="dot"></span>Throttled</span>
271
+ <span class="pill pill-error"><span class="dot"></span>Declined</span>
272
+ ```
273
+
274
+ ```css
275
+ .pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: var(--radius-full); font-size: var(--text-xs); font-weight: 500; }
276
+ .pill .dot { width: 6px; height: 6px; border-radius: 50%; }
277
+ .pill-success { background: rgba(86, 211, 100, 0.12); color: #56d364; } .pill-success .dot { background: #56d364; }
278
+ .pill-warning { background: rgba(244, 162, 97, 0.12); color: #f4a261; } .pill-warning .dot { background: #f4a261; }
279
+ .pill-error { background: rgba(244, 112, 103, 0.12); color: #f47067; } .pill-error .dot { background: #f47067; }
280
+ ```
281
+
282
+ ## Text input
283
+
284
+ ```html
285
+ <input class="input" type="text" placeholder="0x…" />
286
+ <input class="input" type="text" disabled value="locked" />
287
+ <input class="input input-error" type="text" value="not a valid address" />
288
+ ```
289
+
290
+ ```css
291
+ .input { width: 100%; background: var(--bg-2); color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-2) var(--space-3); font-size: var(--text-sm); font-family: var(--font-sans); transition: border-color var(--duration-1) var(--ease), box-shadow var(--duration-1) var(--ease); }
292
+ .input::placeholder { color: var(--fg-faint); }
293
+ .input:hover { border-color: var(--border-2); }
294
+ .input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
295
+ .input:disabled { color: var(--fg-faint); cursor: not-allowed; }
296
+ .input-error { border-color: #f47067; }
297
+ .input-error:focus { box-shadow: 0 0 0 3px rgba(244, 112, 103, 0.18); }
298
+ ```
299
+
300
+ ## Select (native, restyled)
301
+
302
+ ```html
303
+ <select class="select">
304
+ <option>USDC</option>
305
+ <option>USDT</option>
306
+ <option>SOL</option>
307
+ </select>
308
+ ```
309
+
310
+ ```css
311
+ .select { appearance: none; background: var(--bg-2) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path fill='%238a8a90' d='M2 4l4 4 4-4z'/></svg>") right 10px center no-repeat; color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-2) 32px var(--space-2) var(--space-3); font-size: var(--text-sm); font-family: var(--font-sans); cursor: pointer; }
312
+ .select:hover { border-color: var(--border-2); }
313
+ .select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
314
+ ```
315
+
316
+ ## Switch (toggle)
317
+
318
+ ```html
319
+ <label class="switch">
320
+ <input type="checkbox" />
321
+ <span class="track"></span>
322
+ <span class="label">Auto-refresh</span>
323
+ </label>
324
+ ```
325
+
326
+ ```css
327
+ .switch { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; font-size: var(--text-sm); user-select: none; }
328
+ .switch input { position: absolute; opacity: 0; pointer-events: none; }
329
+ .switch .track { position: relative; width: 32px; height: 18px; background: var(--bg-3); border: 1px solid var(--border); border-radius: var(--radius-full); transition: background var(--duration-1) var(--ease); }
330
+ .switch .track::after { content: ""; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; background: var(--fg-dim); border-radius: 50%; transition: transform var(--duration-1) var(--ease), background var(--duration-1) var(--ease); }
331
+ .switch input:checked + .track { background: color-mix(in srgb, var(--accent) 30%, var(--bg-3)); border-color: var(--accent); }
332
+ .switch input:checked + .track::after { transform: translateX(14px); background: var(--accent); }
333
+ ```
334
+
335
+ ## Checkbox
336
+
337
+ ```html
338
+ <label class="check">
339
+ <input type="checkbox" />
340
+ <span class="box"></span>
341
+ <span>Include declined transactions</span>
342
+ </label>
343
+ ```
344
+
345
+ ```css
346
+ .check { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; font-size: var(--text-sm); user-select: none; }
347
+ .check input { position: absolute; opacity: 0; pointer-events: none; }
348
+ .check .box { width: 16px; height: 16px; background: var(--bg-2); border: 1px solid var(--border); border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; transition: background var(--duration-1) var(--ease), border-color var(--duration-1) var(--ease); }
349
+ .check input:checked + .box { background: var(--accent); border-color: var(--accent); }
350
+ .check input:checked + .box::after { content: ""; width: 10px; height: 6px; border-left: 2px solid white; border-bottom: 2px solid white; transform: rotate(-45deg) translate(1px, -1px); }
351
+ ```
352
+
353
+ ## Tabs
354
+
355
+ ```html
356
+ <div class="tabs" role="tablist">
357
+ <button class="tab is-active" data-tab="overview">Overview</button>
358
+ <button class="tab" data-tab="transactions">Transactions</button>
359
+ <button class="tab" data-tab="wallets">Wallets</button>
360
+ </div>
361
+ ```
362
+
363
+ ```css
364
+ .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: var(--space-4); }
365
+ .tab { background: transparent; color: var(--fg-dim); border: 0; border-bottom: 2px solid transparent; padding: var(--space-2) var(--space-3); font-size: var(--text-sm); font-weight: 500; cursor: pointer; transition: color var(--duration-1) var(--ease), border-color var(--duration-1) var(--ease); margin-bottom: -1px; }
366
+ .tab:hover { color: var(--fg); }
367
+ .tab.is-active { color: var(--fg); border-bottom-color: var(--accent); }
368
+ ```
369
+
370
+ ```js
371
+ // Wire-up: toggle .is-active and show the matching panel
372
+ document.querySelectorAll(".tab").forEach(t => t.addEventListener("click", () => {
373
+ document.querySelectorAll(".tab").forEach(x => x.classList.toggle("is-active", x === t));
374
+ document.querySelectorAll("[data-panel]").forEach(p => p.hidden = p.dataset.panel !== t.dataset.tab);
375
+ }));
376
+ ```
377
+
378
+ ## Dialog (modal)
379
+
380
+ Use the native `<dialog>` element — it gets accessibility for free.
381
+
382
+ ```html
383
+ <dialog id="confirm-dialog" class="dialog">
384
+ <h3>Approve $500 delegation?</h3>
385
+ <p>This signs an SPL Approve transaction. The cap is the loss ceiling if anything goes wrong.</p>
386
+ <div class="dialog-actions">
387
+ <button class="btn-ghost" onclick="document.getElementById('confirm-dialog').close()">Cancel</button>
388
+ <button class="btn" onclick="approveDelegation()">Approve</button>
389
+ </div>
390
+ </dialog>
391
+
392
+ <button class="btn" onclick="document.getElementById('confirm-dialog').showModal()">Delegate wallet</button>
393
+ ```
394
+
395
+ ```css
396
+ .dialog { background: var(--bg-2); color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-6); max-width: 420px; box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.6); }
397
+ .dialog::backdrop { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); }
398
+ .dialog h3 { margin: 0 0 var(--space-2); font-size: var(--text-md); font-weight: 600; }
399
+ .dialog p { margin: 0 0 var(--space-4); color: var(--fg-dim); font-size: var(--text-sm); line-height: 1.5; }
400
+ .dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); }
401
+ ```
402
+
403
+ ## Tooltip
404
+
405
+ CSS-only, attribute-driven. Add `data-tooltip` to anything.
406
+
407
+ ```html
408
+ <span data-tooltip="On-chain delegation transaction">tx hash</span>
409
+ ```
410
+
411
+ ```css
412
+ [data-tooltip] { position: relative; cursor: help; }
413
+ [data-tooltip]::after { content: attr(data-tooltip); position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--bg-3); color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 10px; font-size: var(--text-xs); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity var(--duration-1) var(--ease); }
414
+ [data-tooltip]:hover::after { opacity: 1; }
415
+ ```
416
+
417
+ ## Skeleton (loading placeholder)
418
+
419
+ ```html
420
+ <div class="skel" style="width:120px;height:14px"></div>
421
+ ```
422
+
423
+ ```css
424
+ .skel { background: linear-gradient(90deg, var(--bg-2) 0%, var(--bg-3) 50%, var(--bg-2) 100%); background-size: 200% 100%; border-radius: var(--radius-sm); animation: skel-shimmer 1.4s infinite; }
425
+ @keyframes skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
426
+ ```
427
+
428
+ ## Empty state
429
+
430
+ ```html
431
+ <div class="empty">
432
+ No transactions yet.
433
+ <div class="hint">Click <strong>Start shopping</strong> above to make the first one.</div>
434
+ </div>
435
+ ```
436
+
437
+ ```css
438
+ .empty { background: var(--bg-2); border: 1px dashed var(--border); border-radius: var(--radius); padding: var(--space-6); text-align: center; color: var(--fg-dim); font-size: var(--text-sm); }
439
+ .empty .hint { color: var(--fg-faint); font-size: var(--text-xs); margin-top: 4px; }
440
+ ```
441
+
442
+ ## Loading / error
443
+
444
+ Every `mp.run` call should produce a visible state for both — never silent.
445
+
446
+ ```css
447
+ .loading { color: var(--fg-dim); font-size: var(--text-sm); padding: var(--space-4) 0; }
448
+ .error { color: #f47067; font-family: var(--font-mono); font-size: var(--text-xs); padding: var(--space-3) 0; }
449
+ ```
450
+
451
+ ## Charts
452
+
453
+ Use [Chart.js](https://www.chartjs.org/) as the default. It loads from CDN, supports the chart types artifacts typically need (line, bar, doughnut, sparkline), is easy to theme with our tokens, and the agent already knows it well.
454
+
455
+ ```html
456
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
457
+ <canvas id="chart" style="max-height: 260px"></canvas>
458
+ ```
459
+
460
+ ```js
461
+ // Read design tokens off the document so the chart inherits the host theme.
462
+ const css = getComputedStyle(document.documentElement);
463
+ const fg = css.getPropertyValue("--fg").trim() || "#e7e7ea";
464
+ const fgDim = css.getPropertyValue("--fg-dim").trim() || "#8a8a90";
465
+ const border = css.getPropertyValue("--border").trim() || "#232328";
466
+ const accent = css.getPropertyValue("--accent").trim() || "#7e6cff";
467
+
468
+ // Apply defaults once, before instantiating any charts.
469
+ Chart.defaults.color = fgDim;
470
+ Chart.defaults.borderColor = border;
471
+ Chart.defaults.font.family = "-apple-system, Inter, sans-serif";
472
+ Chart.defaults.font.size = 11;
473
+ Chart.defaults.plugins.legend.labels.color = fg;
474
+ Chart.defaults.plugins.tooltip.backgroundColor = "#1c1c20";
475
+ Chart.defaults.plugins.tooltip.titleColor = fg;
476
+ Chart.defaults.plugins.tooltip.bodyColor = fgDim;
477
+ Chart.defaults.plugins.tooltip.borderColor = border;
478
+ Chart.defaults.plugins.tooltip.borderWidth = 1;
479
+ Chart.defaults.plugins.tooltip.padding = 10;
480
+ Chart.defaults.plugins.tooltip.cornerRadius = 8;
481
+
482
+ new Chart(document.getElementById("chart"), {
483
+ type: "line",
484
+ data: {
485
+ labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
486
+ datasets: [{
487
+ label: "Portfolio",
488
+ data: [12400, 12550, 12480, 12700, 12860, 13020, 13180],
489
+ borderColor: accent,
490
+ backgroundColor: "rgba(126, 108, 255, 0.12)",
491
+ fill: true,
492
+ tension: 0.3,
493
+ pointRadius: 0,
494
+ borderWidth: 2,
495
+ }],
496
+ },
497
+ options: {
498
+ maintainAspectRatio: false,
499
+ plugins: { legend: { display: false } },
500
+ scales: {
501
+ x: { grid: { display: false }, ticks: { color: fgDim } },
502
+ y: { grid: { color: border }, ticks: { color: fgDim, callback: v => "$" + v.toLocaleString() } },
503
+ },
504
+ },
505
+ });
506
+ ```
507
+
508
+ ### Chart color palette (when you need multiple series)
509
+
510
+ ```js
511
+ const SERIES = [
512
+ "#7e6cff", // accent (primary)
513
+ "#5cc8e7", // info / chain
514
+ "#56d364", // success
515
+ "#f4a261", // warning
516
+ "#f47067", // negative
517
+ "#fbbf24", // shopping
518
+ "#34d399", // hardware
519
+ "#9d8eff", // accent-2
520
+ ];
521
+ ```
522
+
523
+ Use them in order. Two series → first two. Six categories → first six. Don't pick at random — the order matters for consistency across artifacts.
524
+
525
+ ### Sparkline shorthand
526
+
527
+ For inline sparklines in tables / stat cards, set `pointRadius: 0`, `borderWidth: 1.5`, and disable both axes:
528
+
529
+ ```js
530
+ new Chart(canvas, {
531
+ type: "line",
532
+ data: { labels: data.map((_, i) => i), datasets: [{ data, borderColor: accent, borderWidth: 1.5, pointRadius: 0, fill: false }] },
533
+ options: { maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { display: false } } },
534
+ });
535
+ ```
536
+
537
+ ### Alternatives
538
+
539
+ - **uPlot** — better for high-density time-series (100K+ points). Use when Chart.js feels slow.
540
+ - **D3** — only when you need a chart Chart.js doesn't have (sankey, force-directed, custom geometries). Has a learning cliff.
541
+ - **Echarts** — also fine; treat as a Chart.js replacement, not an addition.
542
+
543
+ If you reach for Recharts, Plotly, or anything React-flavored — don't. Artifacts are vanilla; keep them that way.
544
+
545
+ ## Number formatting
546
+
547
+ Two rules:
548
+ 1. Use `font-variant-numeric: tabular-nums` for any column of numbers so digits align vertically.
549
+ 2. Format USD via `Intl.NumberFormat`:
550
+ ```js
551
+ const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
552
+ ```
553
+
554
+ ---
555
+
556
+ ---
557
+
558
+ # Living references
559
+
560
+ The MoonPay Agents app ships built-in artifacts. These are the canonical
561
+ examples — read them when you need to see how the design system + bridge
562
+ look in a real artifact. They live inside the moonpay-agents app bundle at
563
+ `src-tauri/artifacts/<slug>.html`.
564
+
565
+ | Artifact | What it shows |
566
+ |---|---|
567
+ | `moon-agents-card` | Hero panel with embedded image, stat row, status pill, delegated-wallet cards with progress meters, on-chain explorer links, transactions list with empty state, "Start shopping" CTA that deep-links into a chat via the `mp.openChat` bridge extension. The reference for any "card with live status + history" artifact. |
568
+
569
+ When you're authoring something new, **read the closest built-in artifact first**. Copy its structure rather than inventing your own — that's the fastest way to make the catalog feel coherent.
570
+
571
+ ## Authoring checklist
572
+
573
+ Before saving an artifact:
574
+
575
+ 1. **Frontmatter present and matches filename.** `name:` field === filename without `.html`.
576
+ 2. **Bridge polyfill included.**
577
+ 3. **Design tokens used everywhere.** `var(--mp-*)` for colors / radii / spacing — no hardcoded `#hexvalues` for theme colors. Semantic colors (success/error) can be hardcoded since they don't theme.
578
+ 4. **Loading + error states.** Every `mp.run` call wrapped in try/catch with a visible state.
579
+ 5. **Self-contained.** No external CSS or JS files. CDN libs OK but optional.
580
+ 6. **Tight scope.** Each artifact does one thing well. If you find yourself wanting two views, that's two artifacts.
581
+
582
+ ## Common pitfalls
583
+
584
+ - **Numbers as argv.** `mp.run([..., size])` fails — the host rejects non-strings. Use `String(size)`.
585
+ - **Forgetting `--json`.** Most `mp` commands need `--json` to return parseable output.
586
+ - **CORS for outside APIs.** `fetch("https://api.example.com")` works from inside the iframe only if that origin sends CORS headers. Prefer routing through `mp` if possible.
587
+ - **Hardcoded colors.** `background: #1c1c20` won't theme if MoonPay re-skins. Use `var(--mp-bg-3, #1c1c20)`.
588
+ - **Mock data unmarked.** It's fine to ship a mocked artifact, but leave a `// MOCK` comment so the agent doesn't ship it as real.
589
+
590
+ ## Minimal starter
591
+
592
+ ```html
593
+ <!--
594
+ name: gas-tracker
595
+ title: Gas Tracker
596
+ description: Current gas prices across Ethereum, Polygon, and Base.
597
+ tags: [tracker, gas]
598
+ -->
599
+ <!doctype html>
600
+ <html><head><meta charset="utf-8"><title>Gas Tracker</title>
601
+ <style>
602
+ :root {
603
+ color-scheme: dark;
604
+ --bg: var(--mp-bg-1, #0a0a0b);
605
+ --bg-2: var(--mp-bg-2, #131316);
606
+ --fg: var(--mp-fg, #e7e7ea);
607
+ --fg-dim: var(--mp-fg-dim, #8a8a90);
608
+ --border: var(--mp-border, #232328);
609
+ --accent: var(--mp-accent, #7e6cff);
610
+ --radius: var(--mp-radius, 12px);
611
+ }
612
+ * { box-sizing: border-box; }
613
+ body { margin: 0; background: var(--bg); color: var(--fg); font-family: -apple-system, Inter, sans-serif; }
614
+ .page { max-width: 600px; margin: 0 auto; padding: 24px; }
615
+ h1 { font-size: 18px; font-weight: 600; margin: 0 0 4px; }
616
+ .sub { color: var(--fg-dim); font-size: 13px; margin: 0 0 24px; }
617
+ .row { display: flex; justify-content: space-between; padding: 12px 16px; background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 8px; font-variant-numeric: tabular-nums; }
618
+ .name { text-transform: capitalize; }
619
+ .gwei { color: var(--accent); font-weight: 600; }
620
+ .loading { color: var(--fg-dim); font-size: 13px; }
621
+ .error { color: #f47067; font-family: ui-monospace, monospace; font-size: 12px; }
622
+ </style></head>
623
+ <body>
624
+ <div class="page">
625
+ <h1>Gas tracker</h1>
626
+ <p class="sub">Current gas prices across major chains.</p>
627
+ <div id="rows" class="loading">Loading…</div>
628
+ </div>
629
+ <script>
630
+ // Bridge polyfill — copy into every artifact
631
+ const mp = window.mp || (() => {
632
+ let nextId = 1; const pending = new Map();
633
+ window.addEventListener("message", (e) => {
634
+ const { id, type, result, error } = e.data || {};
635
+ if (type !== "mp.result" || !pending.has(id)) return;
636
+ const { resolve, reject } = pending.get(id);
637
+ pending.delete(id);
638
+ error ? reject(new Error(error)) : resolve(result);
639
+ });
640
+ return { run(argv) { const id = nextId++; return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); window.parent.postMessage({ id, type: "mp.run", argv }, "*"); }); } };
641
+ })();
642
+
643
+ (async () => {
644
+ try {
645
+ const chains = ["ethereum", "polygon", "base"];
646
+ const results = await Promise.all(chains.map(c =>
647
+ mp.run(["chain", "gas-price", "--chain", c, "--json"]).then(r => JSON.parse(r))
648
+ ));
649
+ document.getElementById("rows").classList.remove("loading");
650
+ document.getElementById("rows").innerHTML = chains.map((c, i) =>
651
+ `<div class="row"><span class="name">${c}</span><span class="gwei">${results[i].gwei} gwei</span></div>`
652
+ ).join("");
653
+ } catch (e) {
654
+ document.getElementById("rows").outerHTML =
655
+ `<div class="error">Failed to load: ${e.message}</div>`;
656
+ }
657
+ })();
658
+ </script>
659
+ </body></html>
660
+ ```
661
+
662
+ That's the shape. Color tokens, font sizes, spacing, radii, error state, tabular nums — all the design system rules in one short file.
663
+
664
+ ## Related
665
+
666
+ - **moonpay-skill-builder** — author procedural skills (the chat-side companion to artifacts).
667
+ - **moonpay-automation-builder** — schedule a skill to run on a cron.