# 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 by `renderMintPieceInfo()` at line 4472
- `mint-preview-frame` (line 2894): Black box, 4:3 aspect ratio, live iframe
- `mint-result-area` (line 2896): Where `renderMintArtifactPanel()` (line 4522) appends artifact boxes with thumbnail (100x100) + HTML iframe + HTML/THUMB/META/OBJKT link buttons
- `mint-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
1. **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.
2. **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.
3. **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.
4. **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.
5. **Buy tab has no modal at all.** External redirect to objkt only. No preview, no context.
6. **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/artist
- `Upgrade 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) — or `View on objkt ↗` if sold
- `View Seller ↗`
- Price display: `X.XX ꜩ`
- **Info:** Listing status, seller, price
### Consolidation: What Gets Removed
1. **Kill the Preview Modal** (`.keeps-modal-overlay`, lines 2866–2883, CSS 1675–1887, JS `openModal()` at 3546–3648). Merge its sidebar content (source, actions, info) into the unified modal.
2. **Kill separate artifact preview boxes** inside mint modal (`.mint-artifact-preview` 100×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.
3. **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).
4. **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.html` lines 2866–2908
#### Phase 2: Unify the CSS
- **Remove** `.keeps-modal-*` styles (lines 1675–1887)
- **Simplify** `.mint-artifact-*` styles — keep only `.mint-artifact-links` for the button row
- **Remove** `.mint-artifact-preview` (100×100 box) and `.mint-artifact-iframe` (inline iframe) — lines 2394–2425
- **Keep** `.mint-preview-frame` as the single preview (already 4:3 aspect, black bg) — lines 2172–2186
#### Phase 3: Unify the JavaScript
- **Replace** `openModal(entry)` (line 3546) with a unified `openKeepModal(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
- **Simplify** `renderMintPieceInfo()` (line 4472) to a one-line info bar
#### Phase 4: Market Tab Integration
- **Change** market cards from `` (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 `` 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:**
1. Validates piece exists in `kidlisp` collection
2. Checks mint status via TzKT: `GET /v1/tokens?contract=...&metadata.symbol=...`
3. Validates auth (login required for new mints; optional for rebake if on-chain owner)
4. Checks wallet matches user's linked Tezos address (`users` collection)
5. Checks for existing active job (<5min old → reuse, unless `force=true`)
6. Creates job in `keep-jobs` collection via `upsertJob()`
7. 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:**
```javascript
// 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:**
```json
{
"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:**
1. Verify auth (user must own piece)
2. Check tx status via TzKT: `GET /v1/operations/transactions/{hash}`
3. 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)
4. Update `kidlisp` record with full keep data:
- `tezos.contracts[CONTRACT].{ txHash, tokenId, mintedAt, owner, minter, artifactUri, thumbnailUri, metadataUri }`
- `kept.{ tokenId, txHash, walletAddress, network, contractAddress, keptAt }`
5. Clear `pendingKeep` if 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:**
1. Admin — always allowed
2. Original minter/creator — always allowed (preserves objkt "Created by" attribution)
3. Token owner — only if `allowOwnerEdit: true` (changes objkt attribution)
**Prepare mode flow:**
1. Build new FA2 metadata JSON
2. Upload to Pinata
3. Build `edit_metadata` Michelson params with MichelsonMap
4. Send "prepared" SSE event → client signs via Beacon
5. 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:**
```json
{
"piece": "$cow",
"format": "webp",
"width": 256, "height": 256,
"density": 2,
"duration": 4000,
"fps": 8,
"quality": 70,
"pinataKey": "...",
"pinataSecret": "..."
}
```
**Internal Pipeline:**
1. Launch Puppeteer headless browser
2. Navigate to `https://aesthetic.computer/$piece`
3. Wait `duration` ms, capture frames at `fps`
4. Encode WebP (or animated WebP/GIF) via Sharp
5. Upload to Pinata: `POST https://api.pinata.cloud/pinning/pinFileToIPFS`
6. Cache to DigitalOcean Spaces CDN (7-day TTL)
7. Broadcast via WebSocket to oven dashboard
**Returns:**
```json
{
"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:**
1. Load piece source (KidLisp from AC filesystem or fetch from API)
2. Parse imports/dependencies
3. Bundle with minimal runtime: boot.mjs → bios.mjs → disk.mjs → kidlisp.mjs
4. Minify with SWC
5. Compress with Brotli
6. Embed WASM Brotli decoder for self-extraction
7. Generate box art (unless `noboxart=1`)
8. Return as base64 HTML
**Returns (format=json):**
```json
{
"content": "",
"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` — Counter
- `content_hashes` — Bigmap: bytes → token_id (dedup check)
- `token_metadata` — Bigmap: token_id → token_info
- `keep_fee` — Mint fee in mutez (2.5 XTZ = 2,500,000 mutez)
- `administrator` — Admin address
- `artist_royalty_bps`, `platform_royalty_bps` — Royalty splits
**FA2 Metadata Structure (uploaded to IPFS):**
```json
{
"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`:
```javascript
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:
```json
{
"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`:
```json
{
"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 `stageTiming` from previous jobs for this piece
#### 4. Historical Timing for Estimates
Store completed job timings in `kidlisp.keepHistory`:
```javascript
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:**
1. Capture frame at t=500ms (after piece boot)
2. Capture frame at t=1500ms
3. Compare raw pixel buffers (Sharp `.raw()` → `Buffer.compare()`)
4. If identical → static: output single WebP from frame 1, skip remaining duration
5. 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:
```bash
# 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 |