Keep Modal Consolidation Report #
Date: 2026-03-10
File: system/public/kidlisp.com/keeps.html
Current State: Three Separate Modal Systems #
The keeps.html page currently has two distinct modal overlays plus an artifact sub-panel, creating a fragmented, confusing UX:
1. Preview Modal (.keeps-modal-overlay, lines 2866–2883) #
- Purpose: View a piece's source code + live iframe preview when clicking a card
- Layout: Split — left iframe preview + right sidebar (source, actions, info)
- CSS: lines 1675–1887
- JS:
openModal(entry)at line 3546 - Actions shown: Edit, Download HTML, Keep on Tezos / Rekeep Media / Upgrade Legacy
- Info shown: Author, views, created date, contract address, network, legacy runtime note
- Problem: Clicking "Keep on Tezos" or "Rekeep Media" closes this modal and opens the Mint Modal — jarring two-step flow
2. Mint Modal (.keeps-mint-overlay, lines 2885–2908) #
- Purpose: The actual keep/regenerate/upgrade flow
- Layout: Single column — piece info → live preview (4:3) → timeline → artifacts → action buttons
- CSS: lines 2093–2509
- JS:
openMintModal(entry, options)at line 5025 - Sub-components inside:
mint-piece-info(line 2893): Shows$code, author, "Edit ↗", "HTML ↗", on-chain link group, latest prepared link group, legacy warnings — rendered byrenderMintPieceInfo()at line 4472mint-preview-frame(line 2894): Black box, 4:3 aspect ratio, live iframemint-result-area(line 2896): WhererenderMintArtifactPanel()(line 4522) appends artifact boxes with thumbnail (100x100) + HTML iframe + HTML/THUMB/META/OBJKT link buttonsmint-track-log(line 2897): Timestamped activity log (max 120px)- Two action bars: start actions (line 2898) and confirm actions (line 2902)
3. Artifact Panel (inside Mint Modal, rendered dynamically) #
- JS:
renderMintArtifactPanel()at line 4522 - Shows: thumbnail (100×100), HTML iframe preview, HTML/THUMB/META/OBJKT links, Rekeep button
- Can be appended multiple times (on-chain vs latest prepared)
- Problem: These are extra preview boxes below the main preview, showing nearly the same thing in smaller boxes
Market/Buy Tab (lines 4032–4109) #
- Cards link directly to objkt.com — no modal at all
- No preview, no regenerate, no owner info
renderMarket()at line 4032
Kept Tab (lines 3340–3381) #
- Row items click →
openMintModal(entry)at line 3379 - Problem: Opens the full mint modal even just to view a kept piece — wrong affordance
What's Wrong #
-
Two modals for one thing. Preview modal shows the piece, then you close it and open the mint modal to act on it. Should be one modal with contextual actions.
-
Three preview boxes in the mint flow. The mint modal has:
- The live preview iframe (4:3, line 2894)
- The artifact thumbnail (100×100, line 4558)
- The artifact HTML iframe (line 4561)
All three show variations of the same piece. Overwhelming.
-
Info overload in
renderMintPieceInfo. Shows on-chain URI group + latest prepared URI group + legacy warnings + author + edit/HTML links. Each URI group has up to 3 sub-links (HTML ↗, THUMB ↗, META ↗). That's up to 8+ tiny links in a dense info block. -
Kept tab opens the wrong modal. Clicking a kept piece opens the mint/regenerate modal, but most users just want to view it — see the piece, see who owns it, maybe jump to objkt.
-
Buy tab has no modal at all. External redirect to objkt only. No preview, no context.
-
No unified preview component. Each context (preview, mint, kept, buy) builds its own ad-hoc layout.
Proposed Consolidation: One Modal, One Preview Box #
Design: Single KeepModal with a fixed-aspect-ratio black preview box #
┌─────────────────────────────────────┐
│ $code [×] │
├─────────────────────────────────────┤
│ │
│ ┌───────────────────────────┐ │
│ │ │ │
│ │ Black Preview Box │ │
│ │ (4:3 aspect) │ │
│ │ │ │
│ │ [progress overlay if │ │
│ │ baking/minting] │ │
│ │ │ │
│ └───────────────────────────┘ │
│ │
│ by @handle Edit ↗ │
│ │
│ ┌─────────────────────────────┐ │
│ │ [Context-specific actions] │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ [Activity log if active] │ │
│ └─────────────────────────────┘ │
│ │
│ [Cancel] [Primary] │
└─────────────────────────────────────┘
Preview Box States #
The single black preview box transitions between states:
| State | What's in the box |
|---|---|
| Live | iframe running the piece from AC |
| Baking | Thumbnail/progress overlay on top of live piece |
| Artifact | Shows the baked HTML bundle iframe (replacing live) |
| Static | Thumbnail image only (for items you don't own) |
Context-Specific Action Sections #
One modal, but actions change based on context:
A. "My Unkept Piece" (from Unkept tab) #
- Preview: Live iframe
- Actions:
Keep on Tezos ꜩ(primary, green)Edit in KidLisp.com(purple)Download HTML(secondary)
B. "My Kept Piece" (from Kept tab, or Unkept if already kept) #
- Preview: Live iframe
- Actions:
Regenerate Media(primary, orange) — if owner/artistUpgrade Legacy Bundle(shown only if legacy runtime detected)View on objkt ↗(if token exists)Edit in KidLisp.com(purple)
- Info: Token ID, contract, owner address
C. "Someone Else's Kept Piece" (from Kept tab, not owner) #
- Preview: Live iframe or static thumbnail
- Actions:
View on objkt ↗(primary)View Owner ↗(links to tzkt/objkt profile)Edit in KidLisp.com(fork/view source)
- Info: Owner address, token ID, kept date
D. "Buy/Market Listing" (from Buy tab) #
- Preview: Static thumbnail or live iframe
- Actions:
Buy on objkt ↗(primary, gold/amber) — orView on objkt ↗if soldView Seller ↗- Price display:
X.XX ꜩ
- Info: Listing status, seller, price
Consolidation: What Gets Removed #
-
Kill the Preview Modal (
.keeps-modal-overlay, lines 2866–2883, CSS 1675–1887, JSopenModal()at 3546–3648). Merge its sidebar content (source, actions, info) into the unified modal. -
Kill separate artifact preview boxes inside mint modal (
.mint-artifact-preview100×100 thumbnail +.mint-artifact-iframe). Instead, after baking completes, swap the main preview box content from live iframe → baked artifact iframe. The thumbnail is already visible during the bake progress overlay. -
Kill
renderMintPieceInfo()URI link dump (lines 4472–4520). Replace with a simple info line:$code by @handle+ a collapsible "Details" section for on-chain/IPFS links (only shown on demand). -
Kill
buildMintHeaderLinkGroup()(lines 4447–4469). The on-chain / latest-prepared dual-group is the main source of confusion. Move HTML/THUMB/META links to a simple row of small buttons below the preview box, shown only after artifacts exist.
Implementation Plan #
Phase 1: Unify the HTML Structure #
- Remove
.keeps-modal-overlay+.keeps-modal(lines 2866–2883) - Rename
.keeps-mint-overlay→.keep-modal-overlay(lines 2885–2908) - Restructure mint modal body to: preview box → info bar → actions → log → footer buttons
- Files:
keeps.htmllines 2866–2908
Phase 2: Unify the CSS #
- Remove
.keeps-modal-*styles (lines 1675–1887) - Simplify
.mint-artifact-*styles — keep only.mint-artifact-linksfor the button row - Remove
.mint-artifact-preview(100×100 box) and.mint-artifact-iframe(inline iframe) — lines 2394–2425 - Keep
.mint-preview-frameas the single preview (already 4:3 aspect, black bg) — lines 2172–2186
Phase 3: Unify the JavaScript #
- Replace
openModal(entry)(line 3546) with a unifiedopenKeepModal(entry, context)where context ='view' | 'keep' | 'regenerate' | 'buy' - Merge
openMintModal()(line 5025) into the unified function - Refactor
renderMintArtifactPanel()(line 4522) to update the main preview box instead of appending sub-panels - Update card click handlers:
- Unkept cards (line 3438):
openKeepModal(entry, 'keep') - Kept rows (line 3379):
openKeepModal(entry, 'view') - Market cards:
openKeepModal(entry, 'buy')instead of external link
- Unkept cards (line 3438):
- Simplify
renderMintPieceInfo()(line 4472) to a one-line info bar
Phase 4: Market Tab Integration #
- Change market cards from
<a href="objkt">(line 4102) to clickable cards that open the unified modal in "buy" context - Add live preview or thumbnail in the modal
- Keep "Buy on objkt ↗" as the primary action button
Key Line References #
| Component | Lines | Action |
|---|---|---|
| Preview modal HTML | 2866–2883 | Remove |
| Mint modal HTML | 2885–2908 | Keep & rename |
| Preview modal CSS | 1675–1887 | Remove |
| Mint modal CSS | 2093–2509 | Simplify |
| Artifact sub-panel CSS | 2363–2457 | Simplify (remove media boxes) |
openModal() |
3546–3648 | Remove, merge into unified |
openMintModal() |
5025–5107 | Refactor as unified entry point |
renderMintPieceInfo() |
4472–4520 | Simplify to info bar |
renderMintArtifactPanel() |
4522–4581 | Refactor to swap main preview |
buildMintHeaderLinkGroup() |
4447–4469 | Remove or make collapsible |
| Kept row click handler | 3379 | Change to openKeepModal(entry, 'view') |
| Card click handler | 3438 | Change to openKeepModal(entry, 'keep') |
| Market card render | 4102–4109 | Change from <a> to modal opener |
Summary #
Before: 2 modal overlays + dynamic artifact sub-panels = 3+ preview boxes, 8+ URI links, confusing state transitions, no modal for buy items, wrong modal for kept items.
After: 1 modal, 1 black preview box (4:3), context-aware actions, clean info bar, activity log only when active. Same modal opens from all tabs with appropriate affordances.
Part 2: Backend & Oven Dependency Map #
End-to-End Pipeline Diagram #
┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌──────────┐
│ Browser │────▶│ Netlify Funcs │────▶│ Oven Service │────▶│ Pinata │
│ (keeps.html)│◀────│ (system/) │◀────│ (oven/) │◀────│ (IPFS) │
└──────┬───────┘ └────────┬─────────┘ └────────┬────────┘ └──────────┘
│ │ │
│ Beacon SDK │ MongoDB │ Puppeteer
▼ ▼ │ Sharp
┌──────────────┐ ┌──────────────────┐ │ ffmpeg
│ Tezos Wallet │ │ MongoDB │ ▼
│ (Temple etc) │ │ (kidlisp, jobs, │ ┌─────────────────┐
└──────────────┘ │ users, secrets) │ │ DO Spaces CDN │
└────────┬─────────┘ │ (art.aesthetic │
│ │ .computer) │
┌────────▼─────────┐ └─────────────────┘
│ TzKT API │
│ (indexer) │
└──────────────────┘
Netlify Functions — Full Endpoint Map #
1. POST /api/keep-prepare — Job Creation #
File: system/netlify/functions/keep-prepare.mjs (221 lines)
What it does:
- Validates piece exists in
kidlispcollection - Checks mint status via TzKT:
GET /v1/tokens?contract=...&metadata.symbol=... - Validates auth (login required for new mints; optional for rebake if on-chain owner)
- Checks wallet matches user's linked Tezos address (
userscollection) - Checks for existing active job (<5min old → reuse, unless
force=true) - Creates job in
keep-jobscollection viaupsertJob() - Fires off background function:
POST /.netlify/functions/keep-prepare-background
Returns: { jobId, status: "preparing", alreadyMinted?, tokenId?, resuming? }
Telemetry opportunity: Currently only returns jobId. Could return estimated stage durations based on historical data.
2. POST /api/keep-prepare-background — Async Pipeline (Background Function) #
File: system/netlify/functions/keep-prepare-background.mjs (801 lines)
Runtime: Netlify Background Function — runs up to 15 minutes, returns 202 immediately.
This is where all the heavy lifting happens. Updates keep-jobs MongoDB record at each stage so the client can poll.
Pipeline Stages with Timing:
| # | Stage | What Happens | Calls | Typical Duration |
|---|---|---|---|---|
| 1 | validate |
Load piece from kidlisp collection |
MongoDB | <1s |
| 2 | analyze |
analyzeKidLisp() on source code — extract traits, deps |
Local | <1s |
| 3 | cache-check |
Check kidlisp.ipfsMedia — if source hash unchanged & <30 days, reuse IPFS URIs |
MongoDB | <1s |
| 4 | thumbnail |
POST oven/grab-ipfs — Puppeteer capture + Pinata upload |
Oven → Pinata | 10–60s |
| 5 | bundle |
GET oven/pack-html?code=...&format=json — minify, brotli, base64 |
Oven | 5–30s |
| 6 | ipfs-bundle |
Upload HTML to Pinata: POST pinning/pinFileToIPFS |
Pinata | 5–30s |
| 7 | metadata |
Build FA2 JSON, upload to Pinata: POST pinning/pinJSONToIPFS |
Pinata | 2–10s |
| 8 | security |
runContractPreflight() — verify contract hash, admin, fee, permit signer via TzKT |
TzKT + RPC | 2–5s |
| 9 | permit |
buildKeepPermit() — sign permit with Ed25519 server key |
Local (Taquito) | <1s |
| 10 | ready |
markJobReady() — store all URIs + Michelson params in job |
MongoDB | <1s |
Total typical: 30–120 seconds
MongoDB updates at each stage:
// After each stage:
await jobs.updateOne({ _id: jobId }, {
$set: {
stage: "thumbnail",
stageMessage: "Generating preview thumbnail...",
progress: 35,
updatedAt: new Date(),
// Partial results accumulate:
thumbnailUri: "ipfs://Qm...", // after stage 4
artifactUri: "ipfs://Qm...", // after stage 6
metadataUri: "ipfs://Qm...", // after stage 7
}
});
Error handling: Any stage failure → markJobFailed(jobId, error, stage) → job status = "failed"
3. GET /api/keep-status — Lightweight Poll #
File: system/netlify/functions/keep-status.mjs (52 lines)
What it does: Reads keep-jobs collection, returns current state. No external calls.
Params: ?jobId=xxx OR ?piece=xxx&wallet=tz1xxx
Returns:
{
"stage": "thumbnail",
"status": "processing",
"stageMessage": "Generating preview thumbnail...",
"progress": 35,
"artifactUri": null,
"thumbnailUri": "ipfs://QmXxx...",
"metadataUri": null,
"preparedData": null,
"error": null
}
Response time: ~100ms (MongoDB read only) Poll interval: Client polls every 2 seconds, max 450 polls (15 min timeout)
Telemetry opportunity: Could add stageDurations: { validate: 800, analyze: 200, ... } for granular timing feedback.
4. POST /api/keep-confirm — Record Mint #
File: system/netlify/functions/keep-confirm.mjs (379 lines)
What it does after user signs tx in wallet:
- Verify auth (user must own piece)
- Check tx status via TzKT:
GET /v1/operations/transactions/{hash} - Resolve token ID from bigmap:
GET /v1/contracts/{addr}/bigmaps/content_hashes/keys/{key}- Retry: 20 attempts × 1.5s delay (handles TzKT indexer lag — up to ~30s)
- Update
kidlisprecord with full keep data:tezos.contracts[CONTRACT].{ txHash, tokenId, mintedAt, owner, minter, artifactUri, thumbnailUri, metadataUri }kept.{ tokenId, txHash, walletAddress, network, contractAddress, keptAt }
- Clear
pendingKeepif token confirmed
Telemetry opportunity: Return indexer wait time, block confirmation count.
5. POST /api/keep-mint — Legacy SSE Streaming Endpoint #
File: system/netlify/functions/keep-mint.mjs (1149 lines)
Architecture: Uses @netlify/functions stream() for TransformStream SSE.
SSE Event Types:
| Event | When | Data |
|---|---|---|
progress |
Each stage transition | { stage, message } |
details |
After validation | { piece, source, author, createdAt } |
prepared |
Ready for signing | { michelsonParams, entrypoint, mintFee, contractAddress, network, rpcUrl } |
complete |
Mint confirmed | { success, piece, tokenId, txHash, objktUrl } |
error |
Any failure | { error: "message" } |
Note: The job-based polling flow (keep-prepare → keep-status) has largely replaced this. But the SSE event structure is still the reference for what telemetry to surface.
6. POST /api/keep-update — Metadata Sync (SSE) #
File: system/netlify/functions/keep-update.mjs (562 lines)
Purpose: Regenerate + sync on-chain metadata for existing keeps.
Authorization hierarchy:
- Admin — always allowed
- Original minter/creator — always allowed (preserves objkt "Created by" attribution)
- Token owner — only if
allowOwnerEdit: true(changes objkt attribution)
Prepare mode flow:
- Build new FA2 metadata JSON
- Upload to Pinata
- Build
edit_metadataMichelson params with MichelsonMap - Send "prepared" SSE event → client signs via Beacon
- Store pending URIs in MongoDB
7. POST /api/keep-update-confirm — Confirm Metadata Update #
File: system/netlify/functions/keep-update-confirm.mjs (263 lines)
Moves pending URIs to actual URIs after client-side tx confirmation.
8. GET /api/keeps-config — Contract Configuration #
File: system/netlify/functions/keeps-config.mjs (152 lines)
Returns: { contractAddress, network, profile, version, rpcUrl, tzktExplorer, objktBase }
Source: MongoDB secrets collection (_id: "tezos-kidlisp")
Oven Service — Endpoint Map #
Server: oven/server.mjs (3,020 lines)
Host: oven.aesthetic.computer (137.184.237.166)
Process manager: PM2 / systemd
Keep-Critical Endpoints #
POST /grab-ipfs — Thumbnail Capture + IPFS Upload #
File: oven/grabber.mjs line 2702
Request:
{
"piece": "$cow",
"format": "webp",
"width": 256, "height": 256,
"density": 2,
"duration": 4000,
"fps": 8,
"quality": 70,
"pinataKey": "...",
"pinataSecret": "..."
}
Internal Pipeline:
- Launch Puppeteer headless browser
- Navigate to
https://aesthetic.computer/$piece - Wait
durationms, capture frames atfps - Encode WebP (or animated WebP/GIF) via Sharp
- Upload to Pinata:
POST https://api.pinata.cloud/pinning/pinFileToIPFS - Cache to DigitalOcean Spaces CDN (7-day TTL)
- Broadcast via WebSocket to oven dashboard
Returns:
{
"success": true,
"ipfsUri": "ipfs://QmXxx...",
"ipfsCid": "QmXxx...",
"piece": "$cow",
"format": "webp",
"mimeType": "image/webp",
"size": 45230
}
Timeout: Configurable — keep-prepare-background sets 150s (KEEP_MINT_THUMBNAIL_TIMEOUT_MS)
Fallback: If primary oven fails, retries OVEN_FALLBACK_URL (always https://oven.aesthetic.computer)
Telemetry available but not surfaced:
- Puppeteer launch time
- Page load time
- Frame capture time per frame
- Sharp encoding time
- Pinata upload time + response
- Total elapsed
GET /pack-html or /bundle-html — HTML Bundle Generation #
File: oven/bundler.mjs (1,507 lines), entry: createBundle() at line 673
Query params: ?code=$cow&format=json&noboxart=1&rebake=1&nocache=1
Internal Pipeline:
- Load piece source (KidLisp from AC filesystem or fetch from API)
- Parse imports/dependencies
- Bundle with minimal runtime: boot.mjs → bios.mjs → disk.mjs → kidlisp.mjs
- Minify with SWC
- Compress with Brotli
- Embed WASM Brotli decoder for self-extraction
- Generate box art (unless
noboxart=1) - Return as base64 HTML
Returns (format=json):
{
"content": "<base64 HTML>",
"filename": "$cow.lisp.html",
"authorHandle": "@jeffrey",
"userCode": "...",
"packDate": "2026-03-10",
"depCount": 15
}
Caching: In-memory per git commit + file-level caching. nocache=1 bypasses.
Timeout: 60s from keep-prepare-background
Telemetry available but not surfaced:
- Source fetch time
- Dependency resolution time
- SWC minification time
- Brotli compression time + ratio
- Total bundle size (compressed vs raw)
GET /grab/:format/:width/:height/:piece — Preview Image (Non-IPFS) #
File: oven/grabber.mjs line 2566
Used during polling to show a live preview thumbnail while baking:
GET /grab/webp/480/360/$cow?duration=3000&fps=8&quality=75&density=2&source=keep
Cached to DO Spaces CDN. Client fetches this as a "baking in progress" image.
GET /keeps/latest/:piece — Latest IPFS Thumbnail Redirect #
File: oven/server.mjs line 1376
Returns: 302 redirect to https://ipfs.aesthetic.computer/ipfs/{cid}
MongoDB Collections #
keep-jobs — Async Job Tracking #
TTL: 30 minutes (auto-expires via MongoDB TTL index on expiresAt)
{
_id: ObjectId,
piece: string, // "$cow"
wallet: string, // "tz1..."
status: "preparing" | "ready" | "failed" | "confirmed" | "expired",
stage: "validate" | "analyze" | "thumbnail" | "bundle" | "ipfs" | "metadata" | "security" | "ready",
stageMessage: string, // Human-readable progress
progress: 0-100, // Percentage
isRebake: boolean,
regenerate: boolean,
user: string, // auth0 sub
handle: string, // @handle
artifactUri: "ipfs://...", // Accumulates as stages complete
thumbnailUri: "ipfs://...",
metadataUri: "ipfs://...",
preparedData: { // Ready for wallet signing
michelsonParams: {...},
entrypoint: "keep",
mintFee: 2.5,
contractAddress: "KT1...",
network: "mainnet",
rpcUrl: "https://mainnet.ecadinfra.com"
},
error: string,
errorStage: string,
createdAt: Date,
updatedAt: Date,
expiresAt: Date // TTL index target
}
kidlisp — Piece Records with Keep Metadata #
{
code: string, // Unique piece name
source: string, // KidLisp source code
user: string, // auth0 sub (creator)
ipfsMedia: { // Bundle cache
artifactUri: "ipfs://...",
thumbnailUri: "ipfs://...",
sourceHash: "sha256...",
packDate: "YYYY-MM-DD",
depCount: number,
createdAt: Date
},
mediaHistory: [...], // Previous IPFS media (last 20)
pendingRebake: { // During regeneration
artifactUri, thumbnailUri, sourceHash, createdAt,
network, contractAddress, contractProfile, contractVersion
},
tezos: {
contracts: {
"KT1...": { // Keyed by contract address
minted: boolean,
tokenId: number,
txHash: string,
network: "mainnet",
mintedAt: Date,
owner: "tz1...",
minter: "tz1...",
artifactUri: "ipfs://...",
thumbnailUri: "ipfs://...",
metadataUri: "ipfs://...",
lastUpdatedAt: Date,
lastUpdateTxHash: string
}
},
kept: { // Flat record (backward compat)
tokenId, txHash, walletAddress, network,
contractAddress, keptAt, keptBy
}
}
}
users — Wallet Linking #
{ _id: "auth0|sub", tezos: { address: "tz1..." } }
secrets — Credentials #
_id: "pinata"→{ apiKey, apiSecret, jwt }_id: "tezos-kidlisp"→{ address, publicKey, privateKey, network, keepsContract, currentKeepsProfile, currentKeepsVersion }
Tezos Smart Contract Interface #
Contract: KT1J15kADMuRWh9kJZzosBeRBYPjYr7RvhoN (mainnet)
Entrypoints:
| Entrypoint | Purpose | Params |
|---|---|---|
keep |
Mint new token | name, symbol, description, artifactUri, displayUri, thumbnailUri, decimals, creators, royalties, content_hash, metadata_uri, owner |
edit_metadata |
Update token metadata | token_id, token_info (MichelsonMap with "" key → metadata URI) |
Storage:
next_token_id— Countercontent_hashes— Bigmap: bytes → token_id (dedup check)token_metadata— Bigmap: token_id → token_infokeep_fee— Mint fee in mutez (2.5 XTZ = 2,500,000 mutez)administrator— Admin addressartist_royalty_bps,platform_royalty_bps— Royalty splits
FA2 Metadata Structure (uploaded to IPFS):
{
"name": "$cow",
"description": "(def cow ...source code...)",
"artifactUri": "ipfs://QmBundle...",
"displayUri": "ipfs://QmThumb...",
"thumbnailUri": "ipfs://QmThumb...",
"decimals": 0,
"symbol": "cow",
"creators": ["tz1creator..."],
"royalties": { "decimals": 4, "shares": { "tz1creator...": "1000" } },
"tags": ["$cow", "KidLisp", "Aesthetic Computer", "@jeffrey"],
"formats": [{ "uri": "ipfs://QmBundle...", "mimeType": "text/html", "dimensions": { "value": "512x384" } }]
}
Frontend Flow — Complete Telemetry Path #
Current Client-Side State Machine #
openMintModal()
│
├─ renderMintPieceInfo() ← piece info bar
├─ renderTimeline() ← progress overlay in preview box
│
└─ startMintFlow()
│
├─ STEP: wallet ← connectWallet() via Beacon SDK
│ └─ setMintStep('wallet', 'active'|'done', address)
│
├─ STEP: validate ← POST /api/keep-prepare → get jobId
│ └─ setMintStep('validate', 'active'|'done'|'error')
│
└─ pollJobStatus(piece, jobId) ← GET /api/keep-status every 2s
│
│ Each poll maps server stage → client steps:
│
├─ stage: "analyze" → setMintStep('analyze', 'active')
├─ stage: "thumbnail" → setMintStep('thumbnail', 'active')
│ └─ Fetch oven preview: GET /grab/webp/480/360/$piece
│ └─ Display in preview box as "baking" image
├─ stage: "bundle" → setMintStep('bundle', 'active')
├─ stage: "ipfs" → setMintStep('ipfs', 'active')
│ └─ When thumbnailUri arrives → swap preview to IPFS thumb
├─ stage: "metadata" → setMintStep('metadata', 'active')
│
└─ status: "ready" → STOP POLLING
│
├─ If rebake + autoSync:
│ └─ syncRebakeMetadataOnChain()
│ ├─ POST /api/keep-update (SSE)
│ └─ beaconClient.requestOperation()
│
├─ If rebake (manual):
│ └─ Show "Media regenerated" + artifact links
│
└─ If initial mint:
├─ renderMintArtifactPanel() ← extra preview boxes (REMOVE)
├─ Show "Confirm & Sign" button
│
└─ confirmMint() ← user clicks
├─ beaconClient.requestOperation() ← wallet popup
├─ POST /api/keep-confirm ← record on-chain
└─ Show success + objkt link
Mint Step IDs (10 steps, defined in keeps/constants.mjs) #
| # | Step ID | Label | Progress % |
|---|---|---|---|
| 1 | wallet |
Connect Wallet | 5% |
| 2 | validate |
Validate Piece | 10% |
| 3 | analyze |
Analyze Source | 20% |
| 4 | thumbnail |
Generate Preview | 35% |
| 5 | bundle |
Pack HTML Bundle | 50% |
| 6 | ipfs |
Pin to IPFS | 65% |
| 7 | metadata |
Build Metadata | 75% |
| 8 | review |
Review & Pay | 85% |
| 9 | sign |
Sign Transaction | 92% |
| 10 | complete |
Keep Complete! | 100% |
setMintStep() — Progress Overlay Renderer (line 4982) #
Updates #mint-progress-stage (label + detail text) and #mint-progress-bar-fill (width %).
Status classes: done (green), error (red), default (purple).
On complete → hides overlay after 1.5s.
addTrackEntry() — Activity Log (line 5960) #
Appends timestamped entries to #mint-track-log (max-height 120px, scrollable):
04:43:50 PM Starting keep flow...
04:43:51 PM Wallet connected: tz1gkf8E...KwBE
04:44:04 PM Artifacts prepared, ready for signing
Telemetry Gaps — What's Missing for Granular UI Feedback #
The backend tracks stage transitions but does not surface sub-stage timing. Here's what could be added:
1. Sub-Stage Timing in keep-jobs #
Add stageTiming object to job record, updated by keep-prepare-background:
stageTiming: {
validate: { startedAt, completedAt, durationMs: 800 },
analyze: { startedAt, completedAt, durationMs: 200 },
thumbnail: {
startedAt, completedAt, durationMs: 42000,
sub: {
puppeteerLaunch: 3200,
pageLoad: 8500,
frameCapture: 12000,
sharpEncode: 2100,
pinataUpload: 16200
}
},
bundle: {
startedAt, completedAt, durationMs: 18000,
sub: {
sourceFetch: 500,
depResolve: 2000,
swcMinify: 8000,
brotliCompress: 3500,
boxArt: 0,
bundleSize: 245000,
compressedSize: 62000,
compressionRatio: 0.25
}
},
ipfs: {
startedAt, completedAt, durationMs: 12000,
sub: {
pinataUpload: 11500,
cid: "QmXxx..."
}
},
metadata: { startedAt, completedAt, durationMs: 3000 },
security: {
startedAt, completedAt, durationMs: 2500,
alerts: [],
preflightPassed: true
},
permit: { startedAt, completedAt, durationMs: 150 }
}
Implementation: The background function already has console.log timing — just capture those values into the job record.
2. Oven Telemetry Response Enhancement #
Currently /grab-ipfs returns only the final result. Enhance to include:
{
"success": true,
"ipfsUri": "ipfs://Qm...",
"timing": {
"puppeteerMs": 3200,
"pageLoadMs": 8500,
"captureMs": 12000,
"encodeMs": 2100,
"uploadMs": 16200,
"totalMs": 42000
},
"meta": {
"framesCaptured": 32,
"outputSize": 45230,
"format": "webp",
"dimensions": "512x512"
}
}
Similarly for /pack-html:
{
"content": "...",
"timing": {
"sourceFetchMs": 500,
"depResolveMs": 2000,
"minifyMs": 8000,
"compressMs": 3500,
"totalMs": 14000
},
"meta": {
"rawSize": 245000,
"compressedSize": 62000,
"depCount": 15,
"compressionRatio": 0.25
}
}
3. Client-Side Granular Progress #
With sub-stage timing from the backend, the progress overlay can show:
┌───────────────────────────────┐
│ │
│ [piece running live] │
│ │
│ ┌─────────────────────────┐ │
│ │ ▶ Generating Preview │ │
│ │ Capturing frames... │ │
│ │ ████████░░ 12/32 │ │
│ │ ~18s remaining │ │
│ └─────────────────────────┘ │
└───────────────────────────────┘
Instead of the current coarse 10-step progress bar, show:
- Sub-step label (e.g., "Capturing frames...", "Compressing bundle...", "Uploading to IPFS...")
- Sub-progress within each stage (frame count, bytes uploaded, etc.)
- Time estimate based on
stageTimingfrom previous jobs for this piece
4. Historical Timing for Estimates #
Store completed job timings in kidlisp.keepHistory:
keepHistory: [{
when: Date,
totalMs: 85000,
stageTiming: { ... },
bundleSize: 62000,
depCount: 15
}]
Use the average of last 3 jobs to estimate "~Xs remaining" during each stage.
External Service Dependency Map #
| Service | Used By | Endpoint | Timeout | Fallback |
|---|---|---|---|---|
| Oven (grab-ipfs) | keep-prepare-bg | POST oven.ac/grab-ipfs |
150s | OVEN_FALLBACK_URL |
| Oven (pack-html) | keep-prepare-bg | GET oven.ac/pack-html |
60s | None |
| Oven (grab preview) | Client JS | GET oven.ac/grab/webp/... |
N/A | Noise placeholder |
| Pinata (file) | keep-prepare-bg | POST api.pinata.cloud/pinning/pinFileToIPFS |
90s | None |
| Pinata (json) | keep-prepare-bg | POST api.pinata.cloud/pinning/pinJSONToIPFS |
30s | None |
| TzKT (tokens) | keep-prepare, keep-confirm | GET api.tzkt.io/v1/tokens?... |
10s | None |
| TzKT (bigmap) | keep-confirm | GET .../bigmaps/content_hashes/keys/... |
30s (20×1.5s retry) | None |
| TzKT (contract) | keep-prepare-bg (preflight) | GET .../contracts/{addr} |
10s | None |
| Tezos RPC | keep-prepare-bg, keep-mint | POST mainnet.ecadinfra.com |
30s | None |
| MongoDB | All functions | Atlas connection | 5s | None |
| Beacon SDK | Client JS | Wallet popup | User-dependent | None |
| DO Spaces CDN | Oven | art.aesthetic.computer |
10s | None |
Security Preflight Checks (keep-prepare-background lines 264–346) #
| Check | What | Source | Strict? |
|---|---|---|---|
| Contract address | Matches KEEP_MINT_EXPECTED_CONTRACT |
Env var | Yes |
| Admin address | Matches KEEP_MINT_EXPECTED_ADMIN |
TzKT | Yes |
| Code hash | Matches KEEP_MINT_EXPECTED_CODE_HASH |
TzKT | Yes |
| Type hash | Matches KEEP_MINT_EXPECTED_TYPE_HASH |
TzKT | Yes |
| Keep fee | >= KEEP_MINT_MIN_EXPECTED_FEE_MUTEZ |
RPC storage | Yes |
| Permit signer | Matches KEEP_MINT_EXPECTED_PERMIT_SIGNER |
RPC storage | Yes |
| Recent operations | Scan for critical admin entrypoints | TzKT | Alert only |
Violations logged but only block if KEEP_MINT_STRICT_PREFLIGHT=true or KEEP_MINT_BLOCK_ON_ALERT=true.
Key Environment Variables #
| Variable | Default | Purpose |
|---|---|---|
TEZOS_NETWORK |
mainnet |
Active network |
OVEN_URL |
https://oven.aesthetic.computer |
Oven service |
OVEN_FALLBACK_URL |
https://oven.aesthetic.computer |
Oven fallback |
KEEP_MINT_THUMBNAIL_TIMEOUT_MS |
150000 |
Thumbnail timeout |
KEEP_MINT_PERMIT_TTL_MS |
1200000 (20min) |
Permit expiry |
KEEP_MINT_SIGNER_CACHE_TTL_MS |
30000 |
Signer credential cache |
KEEP_MINT_EXPECTED_CONTRACT |
— | Security check |
KEEP_MINT_EXPECTED_ADMIN |
— | Security check |
KEEP_MINT_STRICT_PREFLIGHT |
false |
Block on violations |
USE_IPFS_GATEWAY_URLS |
false |
Use gateway vs ipfs:// |
IPFS_GATEWAY |
https://ipfs.aesthetic.computer |
Gateway URL |
Part 3: Speed Optimizations #
Current Pipeline Timing (Sequential) #
thumbnail (fire async) ──────────────────────────────────┐
bundle (await) ──────────────────┐ │
├─ ipfs upload (async) ──┤
│ ├─ await both
│ │
metadata ─────────────────────── │ ←── needs artifact + │
│ thumbnail URIs │
security preflight ──────────────┤ ←── sequential, after │
│ metadata! │
permit ──────────────────────────┘ │
Problem: Security preflight (2–5s) runs after metadata upload, but it doesn't depend on any IPFS URIs — it only checks the contract. It could run much earlier.
Optimization 1: Parallelize Preflight with IPFS Uploads #
File: system/netlify/functions/keep-prepare-background.mjs
Saves: 2–5s
Move runContractPreflight() to fire in parallel with the thumbnail/bundle/IPFS work. It only needs the contract address, which is known from the start.
Current flow (lines 678–686):
... await ipfs upload ...
... await thumbnail ...
security preflight ← SEQUENTIAL, blocks metadata
metadata upload
permit
ready
New flow:
preflight (fire async) ─────────────────────────────┐
thumbnail (fire async) ─────────────────────────┐ │
bundle (await) → ipfs upload (async) ───────────┤ │
await all ──┼───┘
metadata upload (needs URIs + preflight.royalties)
permit
ready
Optimization 2: Adaptive Poll Interval #
File: system/public/kidlisp.com/keeps.html, pollJobStatus() at line 5243
Saves: ~1–2s perceived latency
Currently polls every 2000ms fixed. Change to:
- First 10 polls (0–20s): 800ms — fast feedback during validate/analyze/early thumbnail
- Polls 11–30 (20–60s): 1500ms — thumbnail/bundle usually in progress
- Polls 31+ (60s+): 3000ms — long-running IPFS uploads, less frequent is fine
Optimization 3: Frame-Diff Static Detection in Grabber #
File: oven/grabber.mjs, inside grabPiece()
Saves: 2–4s on static pieces (shorter capture + single-frame WebP)
After capturing the first 2–3 frames, compare pixel data. If frames are identical → piece is static → stop capturing early, output single-frame WebP instead of animated.
Algorithm:
- Capture frame at t=500ms (after piece boot)
- Capture frame at t=1500ms
- Compare raw pixel buffers (Sharp
.raw()→Buffer.compare()) - If identical → static: output single WebP from frame 1, skip remaining duration
- If different → animated: continue full capture at configured fps/duration
No need to cache the static flag — just detect it each time during the grab. The comparison adds ~50ms but can save 2–4s of capture + encoding time.
Optimization 4: Pre-warm Bundles on Deploy #
File: oven deploy script / post-deploy hook Saves: 5–30s on first keep after deploy (eliminates cold bundle cache)
The oven already has POST /pack-prewarm. After deploy, trigger it for all kept pieces:
# In deploy script, after restart:
curl -X POST https://oven.aesthetic.computer/pack-prewarm \
-H "Content-Type: application/json" \
-d '{"pieces": ["$kl1", "$cow", "$fib", ...]}'
Could also auto-populate the list from MongoDB kidlisp collection where tezos.kept exists.
Implementation Order #
| # | Optimization | Files | Effort | Impact |
|---|---|---|---|---|
| 1 | Parallelize preflight | keep-prepare-background.mjs | Low | 2–5s saved |
| 2 | Adaptive polling | keeps.html | Low | ~1–2s perceived |
| 3 | Frame-diff static detection | oven/grabber.mjs | Medium | 2–4s on static pieces |
| 4 | Pre-warm on deploy | oven deploy script | Low | 5–30s on first keep post-deploy |