Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: sync billing snapshots and runtime updates

+382 -56
+171
bills/all-services-2026-03-23.md
··· 1 + # Aesthetic Computer — Full Service & Billing Audit 2 + 3 + **Date:** 2026-03-23 4 + **Previous audit:** 2026-03-03 5 + **Account holder:** jeffrey (me@jas.life) 6 + 7 + --- 8 + 9 + ## Summary 10 + 11 + | Status | Service | Est. Monthly | Notes | 12 + |--------|---------|-------------|-------| 13 + | **CRITICAL** | Netlify | $19 | Period ends ~Mar 27 — site at risk | 14 + | **OFFLINE** | Shopify | $0 | shop.aesthetic.computer down | 15 + | **OVERDUE** | GitHub | ~$500 | Prior months (Codespaces/Copilot) | 16 + | Active | DigitalOcean | ~$132 | Paid — all droplets restored | 17 + | Active | MongoDB | $0 | Self-hosted on DO silo — restored | 18 + | Active | Cloudflare | $0 | Free plan, 8 zones | 19 + | Active | Firebase/GCP | $0–$50 | Free tier auth + Nanos VMs (unknown cost) | 20 + | Active | Auth0 | $0 | Free tier | 21 + | Active | Stripe | Per-txn | 2.9% + $0.30 per charge | 22 + | Active | PayPal | Per-txn | ~2.2% + $0.30 per charge | 23 + | Active | Anthropic (Claude) | Usage-based | API key active | 24 + | Active | Jamsocket | $0 | Free/early tier | 25 + | Dormant | Tezos | Per-txn | Gas fees on mint only | 26 + | Unknown | Domain registrations | ~$80–120/yr | 8 domains across registrars | 27 + | **Total recurring** | | **~$151–201/mo** | **Plus ~$519 overdue (GitHub + Netlify)** | 28 + 29 + --- 30 + 31 + ## Changes Since Last Audit (Mar 3) 32 + 33 + | Change | Detail | 34 + |--------|--------| 35 + | DigitalOcean **restored** | $412.34 paid (thanks Casey), all 6 droplets back online | 36 + | MongoDB **restored** | Self-hosted on DO silo, back with DO | 37 + | Netlify escalated to **CRITICAL** | Period ends ~Mar 27 — only ~4 days remain | 38 + | Shopify **offline** | shop.aesthetic.computer down, /api/shop broken, product carousel dead | 39 + | Overdue adjusted | DO resolved ($412.34), GitHub (~$500) + Netlify ($19) remain = ~$519 | 40 + 41 + --- 42 + 43 + ## 1. Netlify — CRITICAL 44 + 45 + **Plan:** Pro (legacy-orb-pro) 46 + **Billing:** $19/mo per member (1 member) 47 + **Status:** `past_due` — **period ends ~Mar 27, site at risk** 48 + **Billing email:** me@jas.life 49 + **Payment:** Stripe (has payment method on file) 50 + **Current period:** Feb 27 – Mar 27, 2026 51 + 52 + **Impact if disabled:** aesthetic.computer, sotce.net, botce.ac, notepat.com, kidlisp.com, wipppps.world ALL go down. All serverless functions (100+) stop. Edge functions stop. This is the most critical service. 53 + 54 + **Action needed:** Fix payment IMMEDIATELY. 55 + 56 + --- 57 + 58 + ## 2. Shopify — OFFLINE 59 + 60 + **Store:** shop.aesthetic.computer 61 + **Status:** Offline / store closed 62 + **Impact:** 63 + - Product pages at shop.aesthetic.computer return errors 64 + - `/api/shop` function fails (fetches from Shopify Admin API) 65 + - Product carousel on main site broken 66 + - All shop redirects in netlify.toml point to dead store 67 + 68 + **Integration points:** 69 + - `system/netlify/functions/shop.mjs` — fetches products via Admin API 70 + - `system/public/aesthetic.computer/disks/common/products.mjs` — carousel component 71 + - `ac-shop/` — CLI management tools 72 + - Redis cache (10-min TTL) may serve stale data briefly 73 + 74 + **Action needed:** Decide whether to reactivate Shopify or remove shop integration. 75 + 76 + --- 77 + 78 + ## 3. DigitalOcean — ACTIVE (restored) 79 + 80 + **Status:** Paid. All 6 droplets back online. 81 + **Monthly cost:** ~$132 82 + 83 + | Resource | Monthly Cost | 84 + |----------|-------------| 85 + | 6 Droplets (all running) | ~$121 | 86 + | Managed Redis/Valkey | $15 | 87 + | 150GB Block Volume | $15 | 88 + | Spaces Subscription | $5 | 89 + | **Total** | **~$132** | 90 + 91 + See [digitalocean-2026-03-03.md](digitalocean-2026-03-03.md) for full droplet breakdown (unchanged). 92 + 93 + --- 94 + 95 + ## 4. GitHub — OVERDUE (~$500) 96 + 97 + **User:** whistlegraph (Free plan) 98 + **Org:** justanothersystem (Free plan, Copilot Business configured, 4 seats) 99 + **Status:** ~$500 unpaid from prior months 100 + **Monthly cost:** Currently $0 (no active paid features) 101 + 102 + Check billing: https://github.com/organizations/justanothersystem/billing/history 103 + 104 + --- 105 + 106 + ## 5. Cloudflare — FREE 107 + 108 + **Account:** me@jas.life 109 + **Plan:** Free Website (all zones) 110 + **Zones:** 8 domains 111 + **Workers:** 2 (grab, dp1-feed) 112 + **Monthly cost: $0** 113 + 114 + --- 115 + 116 + ## 6. MongoDB — ACTIVE (self-hosted) 117 + 118 + **Previous:** MongoDB Atlas (suspended) 119 + **Current:** Self-hosted on Silo droplet (`silo.aesthetic.computer:27017`) 120 + **Monthly cost:** $0 standalone (included in DO silo droplet cost) 121 + 122 + --- 123 + 124 + ## 7–13. Unchanged Services 125 + 126 + | # | Service | Status | Monthly | Notes | 127 + |---|---------|--------|---------|-------| 128 + | 7 | Firebase/GCP | Active | $0–50 | Free auth + unknown Nanos VMs | 129 + | 8 | Auth0 | Free | $0 | Free tier (7K users) | 130 + | 9 | Stripe | Active | Per-txn | 2.9% + $0.30 | 131 + | 10 | PayPal | Active | Per-txn | ~2.2% + $0.30 | 132 + | 11 | Anthropic | Active | Usage | Claude Code API | 133 + | 12 | Jamsocket | Free | $0 | Session orchestration | 134 + | 13 | Tezos | Dormant | Gas fees | Keeps minting only | 135 + | 14 | Domains (8) | Active | ~$8/mo | ~$100/yr across registrars | 136 + 137 + --- 138 + 139 + ## Priority Actions 140 + 141 + ### Urgent — Before April 142 + 143 + 1. **Netlify payment** — Fix NOW. Period ends ~Mar 27. Everything goes down. 144 + 2. **Shopify** — Decide: reactivate store or remove integration from codebase. 145 + 3. **GitHub ~$500** — Pay at billing history page. 146 + 147 + ### April Tasks 148 + 149 + 4. **GCP/Nanos billing** — Log into console.cloud.google.com, check if chat VMs running. 150 + 5. **Domain renewals** — Check expiration dates for 8 domains. 151 + 6. **Anthropic spend** — Check console.anthropic.com for monthly burn rate. 152 + 7. **Cost reduction** — Execute plan: legacy-2016 + Redis + downgrades (~$107/mo savings possible). 153 + 154 + ### Cost Reduction Opportunities (unchanged) 155 + 156 + | Action | Monthly Savings | 157 + |--------|----------------| 158 + | Kill legacy-2016 droplet + 150GB volume | $27 | 159 + | Downgrade oven → s-2vcpu-2gb | $36 | 160 + | Downgrade help → s-1vcpu-1gb | $18 | 161 + | Redis → Upstash free tier | $15 | 162 + | Evaluate AT/PDS droplet | $6 | 163 + | Consolidate domains | $5–10 | 164 + | **Total possible** | **~$107–112/mo** | 165 + 166 + Could bring monthly from ~$151 down to ~$44. 167 + 168 + --- 169 + 170 + *Updated by Claude Code — 2026-03-23* 171 + *Previous audit: [all-services-2026-03-03.md](all-services-2026-03-03.md)*
+37 -23
system/netlify/functions/billing.js
··· 3 3 // Credentials stored in MongoDB to avoid Netlify's 4KB env var limit 4 4 // Endpoint: /api/billing 5 5 6 - import { respond } from "../../backend/http.mjs"; 7 - import { connect } from "../../backend/database.mjs"; 8 - import { authorize, hasAdmin } from "../../backend/authorization.mjs"; 6 + import { respond } from "../../backend/http.mjs"; 7 + import { connect } from "../../backend/database.mjs"; 8 + import { authorize, hasAdmin } from "../../backend/authorization.mjs"; 9 9 10 10 const dev = process.env.CONTEXT === "dev"; 11 11 ··· 61 61 }, 62 62 }, 63 63 mongodb: { 64 - name: "MongoDB Atlas", 65 - description: "Database", 66 - // Atlas billing requires org-level API access 64 + name: "MongoDB", 65 + description: "Database (self-hosted on DO silo)", 66 + // Migrated from Atlas to self-hosted on silo.aesthetic.computer 67 + }, 68 + shopify: { 69 + name: "Shopify", 70 + description: "E-commerce / product store", 71 + // shop.aesthetic.computer — currently offline 67 72 }, 68 73 pinata: { 69 74 name: "Pinata", ··· 252 257 name: PROVIDERS.mongodb.name, 253 258 description: PROVIDERS.mongodb.description, 254 259 estimated: true, 255 - monthlyEstimate: 0, // M0 free tier 256 - note: "Atlas M0 free tier", 260 + monthlyEstimate: 0, 261 + note: "Self-hosted on DO silo (cost included in DigitalOcean)", 262 + }, 263 + { 264 + provider: "shopify", 265 + name: PROVIDERS.shopify.name, 266 + description: PROVIDERS.shopify.description, 267 + estimated: true, 268 + monthlyEstimate: 0, 269 + status: "offline", 270 + note: "shop.aesthetic.computer — store offline as of March 2026", 257 271 }, 258 272 { 259 273 provider: "anthropic", ··· 265 279 ]; 266 280 } 267 281 268 - export async function handler(event, context) { 269 - // Only allow GET 270 - if (event.httpMethod !== "GET") { 271 - return respond(405, { error: "Method not allowed" }); 272 - } 273 - 274 - // Sensitive endpoint: require @jeffrey admin auth. 275 - const user = await authorize(event.headers || {}); 276 - if (!user) { 277 - return respond(401, { error: "Unauthorized" }); 278 - } 279 - const isAdmin = await hasAdmin(user); 280 - if (!isAdmin) { 281 - return respond(403, { error: "Admin access required" }); 282 - } 282 + export async function handler(event, context) { 283 + // Only allow GET 284 + if (event.httpMethod !== "GET") { 285 + return respond(405, { error: "Method not allowed" }); 286 + } 287 + 288 + // Sensitive endpoint: require @jeffrey admin auth. 289 + const user = await authorize(event.headers || {}); 290 + if (!user) { 291 + return respond(401, { error: "Unauthorized" }); 292 + } 293 + const isAdmin = await hasAdmin(user); 294 + if (!isAdmin) { 295 + return respond(403, { error: "Admin access required" }); 296 + } 283 297 284 298 const query = event.queryStringParameters || {}; 285 299 const provider = query.provider; // Optional: filter by provider
+174 -33
system/public/aesthetic.computer/disks/blank.mjs
··· 13 13 14 14 // UI elements 15 15 let buyBtn = null; 16 + let specBtn = null; 16 17 let userHandle = null; 17 18 19 + // Handle cycling (when not logged in) 20 + let allHandles = []; 21 + let cycleIndex = 0; 22 + let cycleTimer = 0; 23 + const CYCLE_INTERVAL = 90; // frames between handle switches 24 + 25 + // Whitelist handles to highlight 26 + const HIGHLIGHT_HANDLES = ["sat", "fifi", "prutti"]; 27 + 28 + // Handle colors cache: Map<handle, Array<{r, g, b}> | null> 29 + const handleColors = new Map(); 30 + let currentDisplayColors = null; // colors for the currently displayed handle 31 + 32 + async function fetchHandleColor(handle) { 33 + const clean = handle.startsWith("@") ? handle.slice(1) : handle; 34 + if (handleColors.has(clean)) return handleColors.get(clean); 35 + try { 36 + const res = await fetch( 37 + `/.netlify/functions/handle-colors?handle=${encodeURIComponent(clean)}`, 38 + ); 39 + if (res.ok) { 40 + const data = await res.json(); 41 + handleColors.set(clean, data.colors || null); 42 + return data.colors || null; 43 + } 44 + } catch {} 45 + handleColors.set(clean, null); 46 + return null; 47 + } 48 + 49 + const SPEC_URL = 50 + "https://psref.lenovo.com/Product/Lenovo/Lenovo_ThinkPad_11e_Yoga_Gen_6"; 51 + 18 52 // Animation 19 53 let frame = 0; 20 54 ··· 41 75 userHandle = handle(); 42 76 setupButtons(ui, screen); 43 77 fetchCheckout(api); 78 + if (!userHandle) fetchHandles(screen); 79 + // Prefetch colors for logged-in user 80 + if (userHandle) fetchHandleColor(userHandle); 81 + // Prefetch colors for whitelisted handles 82 + HIGHLIGHT_HANDLES.forEach((h) => fetchHandleColor(h)); 83 + } 84 + 85 + // Max chars that fit on the laptop screen text line: "hi @handle" 86 + // MatrixChunky8 is ~4px per char, screen plane is ~56px wide → ~14 chars 87 + const MAX_SCREEN_CHARS = 14; 88 + 89 + async function fetchHandles(screen) { 90 + try { 91 + const res = await fetch("/api/handles?tenant=aesthetic"); 92 + if (!res.ok) return; 93 + const data = await res.json(); 94 + allHandles = (data.handles || []) 95 + .map((h) => h.handle) 96 + .filter(Boolean) 97 + .filter((h) => `hi @${h}`.length <= MAX_SCREEN_CHARS); 98 + // Put highlighted handles first, then shuffle the rest 99 + const highlighted = []; 100 + const rest = []; 101 + for (const h of allHandles) { 102 + if (HIGHLIGHT_HANDLES.includes(h.toLowerCase())) highlighted.push(h); 103 + else rest.push(h); 104 + } 105 + for (let i = rest.length - 1; i > 0; i--) { 106 + const j = floor(Math.random() * (i + 1)); 107 + [rest[i], rest[j]] = [rest[j], rest[i]]; 108 + } 109 + allHandles = [...highlighted, ...rest]; 110 + // Prefetch colors for all displayed handles 111 + allHandles.forEach((h) => fetchHandleColor(h)); 112 + } catch {} 44 113 } 45 114 46 115 function setupButtons(ui, screen) { 47 116 buyBtn = new ui.TextButton(getBuyText(), { center: "x", bottom: 20, screen }); 117 + specBtn = new ui.TextButton("SPECS", { x: 6, bottom: 20, screen }); 48 118 } 49 119 50 120 async function fetchCheckout(api) { ··· 94 164 95 165 wipe(...bg); 96 166 167 + // Animated backdrop — drifting primary/secondary color bands 168 + { 169 + const t = frame * 0.01; 170 + const alpha = isDark ? 35 : 20; 171 + // Red, blue, yellow — primary colors that pop against the plain laptop 172 + const palette = [ 173 + [200, 30, 30], // red 174 + [30, 50, 200], // blue 175 + [220, 200, 20], // yellow 176 + [20, 180, 60], // green 177 + [180, 40, 180], // magenta 178 + ]; 179 + for (let i = 0; i < palette.length; i++) { 180 + const phase = i * (PI * 2) / palette.length; 181 + const bw = floor(w * 0.5); 182 + const bh = floor(h * 0.35); 183 + const bx = max(0, min(w - bw, floor(w * 0.5 + sin(t + phase) * w * 0.4 - bw / 2))); 184 + const by = max(0, min(h - bh, floor(h * 0.5 + cos(t * 0.7 + phase) * h * 0.35 - bh / 2))); 185 + const [cr, cg, cb] = palette[i]; 186 + ink(cr, cg, cb, alpha).box(bx, by, bw, bh, "fill"); 187 + } 188 + } 189 + 97 190 // Thanks page 98 191 if (thanks) { 99 192 const cy = floor(h / 2); ··· 237 330 const viewDirY = sin(ax); 238 331 const viewDirZ = cos(ay) * cos(ax); 239 332 240 - const debugNormals = []; 241 333 const addFaces = (verts3d, proj, color, tag) => { 242 334 const frontFaces = new Set(); 243 335 // Compute box center for outward normal correction ··· 265 357 nx /= nLen; ny /= nLen; nz /= nLen; 266 358 // Dot with view direction — negative = faces camera (opposes view dir) 267 359 const dot = nx * viewDirX + ny * viewDirY + nz * viewDirZ; 268 - // Store debug normal (all faces, front or back) 269 - const nScale = 0.4; 270 - debugNormals.push({ 271 - from: [fcx, fcy, fcz], 272 - to: [fcx + nx * nScale, fcy + ny * nScale, fcz + nz * nScale], 273 - front: dot < 0, tag, fi, 274 - }); 275 360 if (dot >= 0) continue; // faces away from camera → skip 276 361 frontFaces.add(fi); 277 362 const z = (proj[a][2] + proj[b][2] + proj[c][2] + proj[d][2]) / 4; ··· 414 499 } 415 500 } 416 501 417 - // 🔍 Debug: draw face normals (green = front-facing, red = back-facing) 418 - for (const dn of debugNormals) { 419 - const p0 = project(dn.from); 420 - const p1 = project(dn.to); 421 - if (dn.front) { 422 - ink(0, 255, 0, 200); 423 - } else { 424 - ink(255, 0, 0, 120); 425 - } 426 - line(p0[0], p0[1], p1[0], p1[1]); 427 - // Dot at tip 428 - ink(255, 255, 0, 220).box(p1[0] - 1, p1[1] - 1, 3, 3); 429 - } 430 - 431 502 // ⌨️ Keyboard keys — smooth fade based on facing + hinge + occlusion 432 503 if (kbFacing) { 433 504 for (const key of kbKeys) { ··· 508 579 tri(projTL[0], projTL[1], projTR[0], projTR[1], projBR[0], projBR[1]); 509 580 tri(projTL[0], projTL[1], projBR[0], projBR[1], projBL[0], projBL[1]); 510 581 582 + // Subtle scanline / noise animation on screen 583 + { 584 + const st = frame * 0.02; 585 + const scanA = floor(facing * 18); 586 + // Horizontal scanline that scrolls down 587 + const scanFrac = (st * 0.5) % 1; 588 + const scanY0 = projTL[1] + (projBL[1] - projTL[1]) * scanFrac; 589 + const scanY1 = scanY0 + (projBL[1] - projTL[1]) * 0.06; 590 + const sxL = projTL[0] + (projBL[0] - projTL[0]) * scanFrac; 591 + const sxR = projTR[0] + (projBR[0] - projTR[0]) * scanFrac; 592 + const sxL1 = projTL[0] + (projBL[0] - projTL[0]) * min(1, scanFrac + 0.06); 593 + const sxR1 = projTR[0] + (projBR[0] - projTR[0]) * min(1, scanFrac + 0.06); 594 + ink(40, 60, 80, scanA); 595 + tri(sxL, scanY0, sxR, scanY0, sxR1, scanY1); 596 + tri(sxL, scanY0, sxR1, scanY1, sxL1, scanY1); 597 + } 598 + 511 599 // Screen text 512 600 const textAlpha = floor(facing * 255); 513 601 const planeW = sqrt(planeRight[0] ** 2 + planeRight[1] ** 2 + planeRight[2] ** 2); ··· 521 609 planeDown[1] / planeH * glyphScale, 522 610 planeDown[2] / planeH * glyphScale]; 523 611 524 - // Dynamic text: "hi @handle" if logged in, "AC Blank" otherwise 525 - const screenText = userHandle ? `hi ${userHandle}` : "hi"; 612 + // Dynamic text: "hi @handle" if logged in, cycle real handles otherwise 613 + let displayHandle = userHandle; 614 + let handleClean = null; 615 + if (!displayHandle && allHandles.length > 0) { 616 + cycleTimer += 1; 617 + if (cycleTimer >= CYCLE_INTERVAL) { 618 + cycleTimer = 0; 619 + cycleIndex = (cycleIndex + 1) % allHandles.length; 620 + } 621 + handleClean = allHandles[cycleIndex]; 622 + displayHandle = `@${handleClean}`; 623 + } else if (displayHandle) { 624 + handleClean = displayHandle.startsWith("@") 625 + ? displayHandle.slice(1) 626 + : displayHandle; 627 + } 628 + // Look up colors every frame (async fetch may have completed) 629 + currentDisplayColors = handleClean 630 + ? handleColors.get(handleClean) || null 631 + : null; 632 + const screenText = displayHandle ? `hi ${displayHandle}` : "hi"; 633 + // "hi " prefix length for color offset — "@" is at index 3 634 + const handleStart = 3; 526 635 // Estimate width: MatrixChunky8 avg ~4px per char 527 636 const textW = screenText.length * 4; 528 637 const textH = 8; ··· 536 645 sTR[2] + offsetR * rn[2] + offsetD * dn[2], 537 646 ]; 538 647 539 - // Glitchy digital blue — vary per-pixel via pixelCallback 648 + // Screen text with handle colors when available 540 649 ink(0).write3D(screenText, { 541 650 origin: textOrigin, 542 651 right: rn, ··· 544 653 project, 545 654 typeface: "MatrixChunky8", 546 655 pixelCallback: (sx, sy, gx, gy, ci) => { 547 - // Hash-ish seed from position + frame for shimmer 548 656 const seed = (gx * 7 + gy * 13 + ci * 31 + frame * 3) & 0xFF; 549 657 const flicker = sin(frame * 0.15 + seed * 0.1) * 0.5 + 0.5; 550 - // Blue/cyan palette with occasional white flash 551 - const flash = (seed + frame) % 47 === 0; 552 - const r = flash ? 220 : floor(20 + flicker * 40); 553 - const g = flash ? 240 : floor(80 + flicker * 100 + seed * 0.2); 554 - const b = flash ? 255 : floor(180 + flicker * 75); 555 - const a = floor(textAlpha * (0.6 + flicker * 0.4)); 556 - ink(r, g, b, a); 658 + // More varied opacity — some pixels dim, some bright, some blink off 659 + const opVar = sin(seed * 0.37 + frame * 0.08) * 0.5 + 0.5; 660 + const dropout = ((seed * 17 + frame) % 61) < 4 ? 0.15 : 1; 661 + const a = floor(textAlpha * (0.3 + opVar * 0.5 + flicker * 0.2) * dropout); 662 + 663 + // Use handle colors for the @handle portion 664 + const charIdx = ci - handleStart; 665 + if (currentDisplayColors && charIdx >= 0 && charIdx < currentDisplayColors.length) { 666 + const c = currentDisplayColors[charIdx]; 667 + const shimmer = 0.6 + flicker * 0.4; 668 + ink( 669 + floor(c.r * shimmer), 670 + floor(c.g * shimmer), 671 + floor(c.b * shimmer), 672 + a, 673 + ); 674 + } else { 675 + // Default blue/cyan for "hi " prefix or no colors 676 + const flash = (seed + frame) % 47 === 0; 677 + const r = flash ? 220 : floor(20 + flicker * 40); 678 + const g = flash ? 240 : floor(80 + flicker * 100 + seed * 0.2); 679 + const b = flash ? 255 : floor(180 + flicker * 75); 680 + ink(r, g, b, a); 681 + } 557 682 }, 558 683 }); 559 684 } ··· 590 715 } 591 716 buyBtn.paint($btn, scheme, hover); 592 717 } 718 + 719 + // Spec sheet link (bottom left) 720 + if (specBtn) { 721 + specBtn.reposition({ x: 6, bottom: 20, screen }, "SPECS"); 722 + const specScheme = isDark 723 + ? [[20, 20, 24], fgDim, [180, 180, 190]] 724 + : [[228, 228, 232], fgDim, [60, 60, 70]]; 725 + const specHover = isDark 726 + ? [[30, 30, 38], [180, 180, 200], [220, 220, 230]] 727 + : [[215, 215, 225], [60, 60, 80], [30, 30, 40]]; 728 + specBtn.paint($btn, specScheme, specHover); 729 + } 593 730 } 594 731 595 732 function act({ event: e, screen, jump, sound, ui, api }) { ··· 598 735 if (e.is("reframed")) { 599 736 setupButtons(ui, screen); 600 737 } 738 + 739 + specBtn?.act(e, { 740 + push: () => jump(`out:${SPEC_URL}`), 741 + }); 601 742 602 743 buyBtn?.act(e, { 603 744 down: () => {