Monorepo for Aesthetic.Computer
aesthetic.computer
1# KidLisp Keeps - NFT Minting System
2
3## Overview
4
5"Keeps" are KidLisp pieces preserved as NFTs on Tezos. Each `$code` can only exist once at a time, ensuring uniqueness and provenance.
6
7---
8
9## Current Contract (v3)
10
11| Network | Contract | Admin | Status |
12|---------|----------|-------|--------|
13| **Mainnet** | `KT1JEVyKjsMLts63e4CNaMUywWTPgeQ41Smi` | staging | Active (Staging) |
14| Ghostnet | `KT1StXrQNvRd9dNPpHdCGEstcGiBV6neq79K` | kidlisp | Testing |
15
16**Legacy (v2):** `KT1EcsqR69BHekYF5mDQquxrvNg5HhPFx6NM` (archived, do not use for new mints)
17
18### Storage
19| Field | Type | Description |
20|-------|------|-------------|
21| `administrator` | address | Admin wallet (can mint, burn, update, lock) |
22| `content_hashes` | big_map[bytes, nat] | Maps piece name → token_id (prevents duplicates) |
23| `contract_metadata_locked` | bool | If true, collection metadata is frozen |
24| `keep_fee` | mutez | Required fee to mint (0 = free) |
25| `ledger` | big_map[nat, address] | Token ownership (token_id → owner) |
26| `metadata` | big_map[string, bytes] | Contract-level TZIP-16 metadata |
27| `metadata_locked` | big_map[nat, bool] | Per-token metadata lock status |
28| `next_token_id` | nat | Auto-incrementing token counter |
29| `operators` | big_map | FA2 operator approvals |
30| `token_creators` | big_map[nat, address] | **v3:** Original creator for each token |
31| `token_metadata` | big_map[nat, record] | Per-token TZIP-21 metadata |
32
33### Entrypoints
34
35| Entrypoint | Access | Description |
36|------------|--------|-------------|
37| `keep` | Admin or User (with fee) | Mint new token with full TZIP-21 metadata |
38| `edit_metadata` | **Admin, Owner, or Creator** | Update token metadata (if not locked) |
39| `lock_metadata` | Admin or Owner | Permanently freeze token metadata |
40| `burn_keep` | Admin | Destroy token and free piece name for re-mint |
41| `set_contract_metadata` | Admin | Update collection metadata (if not locked) |
42| `lock_contract_metadata` | Admin | Permanently freeze collection metadata |
43| `set_keep_fee` | Admin | Set mint fee (in tez) |
44| `withdraw_fees` | Admin | Withdraw accumulated fees |
45| `transfer` | Owner/Operator | FA2 standard transfer |
46| `balance_of` | Public | FA2 standard balance query |
47| `update_operators` | Owner | FA2 operator management |
48
49### v3 Permission Model (edit_metadata)
50
51```
52edit_metadata authorization (in order of check):
531. Admin — can edit any token
542. Owner — current holder of the token
553. Creator — address stored in token_creators[token_id]
56
57⚠️ For objkt.com artist attribution:
58 The CREATOR must call edit_metadata, not admin!
59 Admin calls show admin as "updater" on objkt.
60```
61
62### Uniqueness Enforcement
63- Each piece name (e.g., "cow", "roz") can only be minted once
64- `content_hashes` big_map tracks: piece_name → token_id
65- Burning removes the entry, allowing re-mint
66- Checked client-side via TzKT API before IPFS upload
67
68---
69
70## CLI Commands (`node keeps.mjs`)
71
72| Command | Description |
73|---------|-------------|
74| `deploy` | Deploy new contract |
75| `status` | Show contract info |
76| `balance` | Check wallet balance |
77| `mint <piece> [--thumbnail]` | Mint piece with optional animated thumbnail |
78| `update <token_id> <piece>` | Update token metadata |
79| `lock <token_id>` | Permanently lock token metadata |
80| `burn <token_id>` | Destroy token (allows re-mint) |
81| `redact <token_id> [--reason="..."]` | Censor token content |
82| `set-collection-media --image=<uri>` | Set collection thumbnail |
83| `lock-collection` | Permanently lock collection metadata |
84| `fee` | Show current keep fee |
85| `set-fee <tez>` | Set keep fee (admin only) |
86| `withdraw [dest]` | Withdraw fees to wallet (admin only) |
87
88---
89
90## Fee System
91
92The contract supports configurable mint fees. See [KEEPS-FEE-SYSTEM.md](./KEEPS-FEE-SYSTEM.md) for full documentation.
93
94### Quick Reference
95
96```bash
97# Check current fee
98node keeps.mjs fee
99
100# Set fee to 5 XTZ
101node keeps.mjs set-fee 5
102
103# Withdraw accumulated fees
104node keeps.mjs withdraw
105```
106
107### Storage & Entrypoints
108- `keep_fee` (mutez) - Required payment to keep
109- `set_keep_fee(new_fee)` - Admin updates fee
110- `withdraw_fees(destination)` - Admin withdraws balance
111
112⚠️ **Note**: Fee system requires contract v2.1+ (with fee entrypoints). Existing contracts need redeployment.
113
114---
115
116## Metadata Structure
117
118### Token Metadata (TZIP-21)
119```json
120{
121 "name": "$cow",
122 "description": "(wipe \"blue\")\n(ink \"yellow\")\n...\n\nby @jeffrey\nac25namuc",
123 "artifactUri": "ipfs://Qm...",
124 "displayUri": "ipfs://Qm...",
125 "thumbnailUri": "ipfs://Qm... (animated WebP)",
126 "symbol": "KEEP",
127 "tags": ["$cow", "KidLisp", "Aesthetic.Computer", "interactive"],
128 "attributes": [
129 { "name": "Language", "value": "KidLisp" },
130 { "name": "Code", "value": "$cow" },
131 { "name": "Author", "value": "@jeffrey" },
132 { "name": "User Code", "value": "ac25namuc" },
133 { "name": "Lines of Code", "value": "3" },
134 { "name": "Dependencies", "value": "2" },
135 { "name": "Packed", "value": "2025.12.9" },
136 { "name": "Interactive", "value": "Yes" },
137 { "name": "Platform", "value": "Aesthetic Computer" }
138 ]
139}
140```
141
142### Collection Metadata
143```json
144{
145 "name": "KidLisp Keeps",
146 "version": "2.0.0",
147 "interfaces": ["TZIP-012", "TZIP-016", "TZIP-021"],
148 "imageUri": "https://oven.aesthetic.computer/keeps/latest",
149 "homepage": "https://aesthetic.computer"
150}
151```
152
153---
154
155## Infrastructure
156
157### Services
158| Service | URL | Purpose |
159|---------|-----|---------|
160| Oven | `https://oven.aesthetic.computer` | Thumbnail generation (Puppeteer + FFmpeg) |
161| Grab | `https://grab.aesthetic.computer` | Static screenshot fallback |
162| Pinata | IPFS pinning | Artifact and metadata storage |
163| TzKT | `api.ghostnet.tzkt.io` | On-chain data queries |
164
165### Thumbnail Generation
166- **Format**: Animated WebP
167- **Size**: 96x96 @ 2x density (192x192 actual)
168- **Duration**: 8 seconds capture
169- **FPS**: 10 capture → 20 playback
170- **Quality**: 70
171
172---
173
174## Phase 2: Creator Authorization (PLANNED)
175
176### Problem
177Currently only admin can mint. We want:
1781. Only **handled** users (with `@handle`) can mint
1792. Users can only mint **their own** pieces (pieces they authored)
1803. Minting should be self-service via web UI
181
182### Existing Infrastructure
183
184#### Authentication
185- **Auth0** provides JWT tokens via `/userinfo` endpoint
186- `authorize()` in `backend/authorization.mjs` validates tokens
187- Returns `{ sub, email, email_verified, ... }`
188
189#### Piece Ownership (Already Tracked!)
190The `kidlisp-codes` MongoDB collection already stores:
191```javascript
192{
193 code: "cow", // Piece name
194 source: "(wipe...)", // Source code
195 hash: "...", // SHA-256 of source
196 user: "auth0|123...", // Creator's Auth0 sub ID ✅
197 when: Date, // Created timestamp
198}
199```
200
201#### Handle Resolution
202- `handleFor(userId)` in `backend/authorization.mjs` gets `@handle` from `sub`
203- `fetchAuthorInfo(userId)` in `bundle-html.js` resolves handle + userCode
204
205### Architecture
206
207```
208┌─────────────────┐ ┌──────────────────────────────┐ ┌─────────────────┐
209│ AC Frontend │────▶│ /api/kidlisp-keep │────▶│ Tezos Contract │
210│ (user clicks │ │ (Netlify function) │ │ (SmartPy FA2) │
211│ "Keep" btn) │ │ │ │ │
212└─────────────────┘ │ 1. Validate JWT (Auth0) │ └─────────────────┘
213 │ │ 2. Check user has @handle │
214 │ JWT Bearer │ 3. Verify piece ownership │
215 │ token │ 4. Check not already minted │
216 ▼ │ 5. Generate bundle & thumb │
217 │ 6. Upload to IPFS │
218 │ 7. Sign & submit Tezos tx │
219 └──────────────────────────────┘
220 │
221 ▼
222 ┌──────────────────────────────┐
223 │ MongoDB `kidlisp-codes` │
224 │ - code → user mapping │
225 │ - piece ownership proof │
226 └──────────────────────────────┘
227```
228
229### Authorization Flow
230
2311. **User Authentication** (Auth0)
232 ```javascript
233 const user = await authorize({ authorization: req.headers.authorization });
234 if (!user) return 401 Unauthorized;
235 ```
236
2372. **Handle Requirement**
238 ```javascript
239 const handle = await handleFor(user.sub);
240 if (!handle) return 403 "You need an @handle to mint";
241 ```
242
2433. **Piece Ownership Verification**
244 ```javascript
245 const piece = await db.collection('kidlisp-codes').findOne({ code: pieceName });
246 if (!piece) return 404 "Piece not found";
247 if (piece.user !== user.sub) return 403 "You don't own this piece";
248 ```
249
2504. **Duplicate Check**
251 ```javascript
252 const duplicate = await checkDuplicatePiece(pieceName);
253 if (duplicate.exists) return 409 "Already minted as token #X";
254 ```
255
2565. **Minting**
257 - Generate bundle via existing `bundle-html.js` logic
258 - Generate thumbnail via Oven
259 - Upload to IPFS
260 - Sign transaction with server-side admin key
261 - Submit to Tezos
262
263### API Endpoints
264
265#### `POST /api/kidlisp-keep`
266Mint a new keep (requires auth)
267
268**Headers:**
269- `Authorization: Bearer <JWT>`
270
271**Body:**
272```json
273{
274 "piece": "cow",
275 "generateThumbnail": true
276}
277```
278
279**Response:**
280```json
281{
282 "success": true,
283 "tokenId": 5,
284 "txHash": "op...",
285 "artifactUri": "ipfs://...",
286 "objktUrl": "https://objkt.com/asset/KT1.../5"
287}
288```
289
290**Errors:**
291- `401` - Not authenticated
292- `403` - No @handle, or not piece owner
293- `404` - Piece not found
294- `409` - Already minted
295
296#### `GET /api/kidlisp-keep?piece=cow`
297Check piece mint status (public)
298
299**Response:**
300```json
301{
302 "piece": "cow",
303 "canMint": true,
304 "owner": "@jeffrey",
305 "minted": false
306}
307// or if minted:
308{
309 "piece": "cow",
310 "canMint": false,
311 "minted": true,
312 "tokenId": 5,
313 "objktUrl": "https://..."
314}
315```
316
317### Security
318
3191. **Admin Key Protection**
320 - Tezos private key in Netlify env: `TEZOS_KIDLISP_KEY`
321 - Never exposed to client
322 - Server signs all transactions
323
3242. **Ownership Enforcement**
325 - Only `piece.user === user.sub` can mint
326 - First saver owns the piece (existing behavior)
327 - Admin can mint any piece (bypass)
328
3293. **On-Chain Protection**
330 - SmartPy contract: `assert self.is_administrator_()` on all mutations
331 - `content_hashes` big_map prevents duplicate minting
332 - Metadata locking is permanent once applied
333
3344. **Rate Limiting** (Future)
335 - Per-user limits (e.g., 5 mints/day)
336 - Prevent spam
337
338### Scalability (Designed for Millions of Pieces)
339
3401. **Token ID Retrieval** - O(1)
341 - Uses `next_token_id - 1` from storage, not pagination
342 - Works at any scale
343
3442. **Duplicate Check** - O(1)
345 - `content_hashes` big_map lookup by key
346 - Big maps are hash tables, constant-time access
347
3483. **Status Command** - O(1) with pagination
349 - Uses TzKT API with `limit` and `sort.desc`
350 - Shows only recent tokens, total count from storage
351
3524. **Gas Costs** - Constant
353 - Big map operations don't increase with collection size
354 - ~0.05 tez per mint regardless of token count
355
3565. **IPFS Storage**
357 - ~50KB per piece average (bundle + metadata + thumb)
358 - 1M pieces ≈ 50GB = ~$7.50/month on Pinata
359
360### Implementation Steps
361
3621. [x] Document existing infrastructure
3632. [ ] Implement `/api/kidlisp-keep` GET (check status)
3643. [ ] Implement `/api/kidlisp-keep` POST (mint)
3654. [ ] Add "Keep" button to UI when viewing own piece
3665. [ ] Test locally with dev server
3676. [ ] Deploy to production
3687. [ ] Add rate limiting
369
370### Environment Variables Needed
371
372```env
373# Netlify env vars (already have most of these)
374TEZOS_KIDLISP_KEY=edsk... # Admin signing key
375TEZOS_CONTRACT_ADDRESS=KT1... # Keeps contract
376TEZOS_NETWORK=ghostnet # or mainnet
377PINATA_API_KEY=... # For IPFS uploads
378PINATA_API_SECRET=...
379OVEN_URL=https://oven.aesthetic.computer
380```
381
382---
383
384## Files
385
386| File | Purpose |
387|------|---------|
388| `tezos/keeps_fa2_v2.py` | SmartPy contract source |
389| `tezos/keeps.mjs` | CLI tool for minting/management |
390| `tezos/contract-address.txt` | Current deployed contract |
391| `oven/server.mjs` | Thumbnail generation server |
392| `oven/grabber.mjs` | Puppeteer frame capture |
393
394---
395
396## Deployment History
397
398| Date | Contract | Network | Notes |
399|------|----------|---------|-------|
400| 2025-12-09 | KT1Ah5m2kzU3GfN42hh57mVJ63kNi95XKBdM | Ghostnet | Current - with burn, redact |
401| 2025-12-09 | KT1FvJyG4e6tRHdJLTjMhvi7mMrrAGkBCdBv | Ghostnet | Added piece-name uniqueness |
402| 2025-12-09 | KT1CfExN8EcSMS5Pm2vzxpQKyzkijNHvGCdm | Ghostnet | Added content_hashes |
403| 2025-12-09 | KT1N9jz6NJaBYW4LVhccZs6ttQMvFEAmkkSM | Ghostnet | First with metadata lock |