Monorepo for Aesthetic.Computer aesthetic.computer

KidLisp Keeps - NFT Minting System#

Overview#

"Keeps" are KidLisp pieces preserved as NFTs on Tezos. Each $code can only exist once at a time, ensuring uniqueness and provenance.


Current Contract (v3)#

Network Contract Admin Status
Mainnet KT1JEVyKjsMLts63e4CNaMUywWTPgeQ41Smi staging Active (Staging)
Ghostnet KT1StXrQNvRd9dNPpHdCGEstcGiBV6neq79K kidlisp Testing

Legacy (v2): KT1EcsqR69BHekYF5mDQquxrvNg5HhPFx6NM (archived, do not use for new mints)

Storage#

Field Type Description
administrator address Admin wallet (can mint, burn, update, lock)
content_hashes big_map[bytes, nat] Maps piece name → token_id (prevents duplicates)
contract_metadata_locked bool If true, collection metadata is frozen
keep_fee mutez Required fee to mint (0 = free)
ledger big_map[nat, address] Token ownership (token_id → owner)
metadata big_map[string, bytes] Contract-level TZIP-16 metadata
metadata_locked big_map[nat, bool] Per-token metadata lock status
next_token_id nat Auto-incrementing token counter
operators big_map FA2 operator approvals
token_creators big_map[nat, address] v3: Original creator for each token
token_metadata big_map[nat, record] Per-token TZIP-21 metadata

Entrypoints#

Entrypoint Access Description
keep Admin or User (with fee) Mint new token with full TZIP-21 metadata
edit_metadata Admin, Owner, or Creator Update token metadata (if not locked)
lock_metadata Admin or Owner Permanently freeze token metadata
burn_keep Admin Destroy token and free piece name for re-mint
set_contract_metadata Admin Update collection metadata (if not locked)
lock_contract_metadata Admin Permanently freeze collection metadata
set_keep_fee Admin Set mint fee (in tez)
withdraw_fees Admin Withdraw accumulated fees
transfer Owner/Operator FA2 standard transfer
balance_of Public FA2 standard balance query
update_operators Owner FA2 operator management

v3 Permission Model (edit_metadata)#

edit_metadata authorization (in order of check):
1. Admin — can edit any token
2. Owner — current holder of the token  
3. Creator — address stored in token_creators[token_id]

⚠️  For objkt.com artist attribution:
   The CREATOR must call edit_metadata, not admin!
   Admin calls show admin as "updater" on objkt.

Uniqueness Enforcement#

  • Each piece name (e.g., "cow", "roz") can only be minted once
  • content_hashes big_map tracks: piece_name → token_id
  • Burning removes the entry, allowing re-mint
  • Checked client-side via TzKT API before IPFS upload

CLI Commands (node keeps.mjs)#

Command Description
deploy Deploy new contract
status Show contract info
balance Check wallet balance
mint <piece> [--thumbnail] Mint piece with optional animated thumbnail
update <token_id> <piece> Update token metadata
lock <token_id> Permanently lock token metadata
burn <token_id> Destroy token (allows re-mint)
redact <token_id> [--reason="..."] Censor token content
set-collection-media --image=<uri> Set collection thumbnail
lock-collection Permanently lock collection metadata
fee Show current keep fee
set-fee <tez> Set keep fee (admin only)
withdraw [dest] Withdraw fees to wallet (admin only)

Fee System#

The contract supports configurable mint fees. See KEEPS-FEE-SYSTEM.md for full documentation.

Quick Reference#

# Check current fee
node keeps.mjs fee

# Set fee to 5 XTZ
node keeps.mjs set-fee 5

# Withdraw accumulated fees
node keeps.mjs withdraw

Storage & Entrypoints#

  • keep_fee (mutez) - Required payment to keep
  • set_keep_fee(new_fee) - Admin updates fee
  • withdraw_fees(destination) - Admin withdraws balance

⚠️ Note: Fee system requires contract v2.1+ (with fee entrypoints). Existing contracts need redeployment.


Metadata Structure#

Token Metadata (TZIP-21)#

{
  "name": "$cow",
  "description": "(wipe \"blue\")\n(ink \"yellow\")\n...\n\nby @jeffrey\nac25namuc",
  "artifactUri": "ipfs://Qm...",
  "displayUri": "ipfs://Qm...",
  "thumbnailUri": "ipfs://Qm... (animated WebP)",
  "symbol": "KEEP",
  "tags": ["$cow", "KidLisp", "Aesthetic.Computer", "interactive"],
  "attributes": [
    { "name": "Language", "value": "KidLisp" },
    { "name": "Code", "value": "$cow" },
    { "name": "Author", "value": "@jeffrey" },
    { "name": "User Code", "value": "ac25namuc" },
    { "name": "Lines of Code", "value": "3" },
    { "name": "Dependencies", "value": "2" },
    { "name": "Packed", "value": "2025.12.9" },
    { "name": "Interactive", "value": "Yes" },
    { "name": "Platform", "value": "Aesthetic Computer" }
  ]
}

Collection Metadata#

{
  "name": "KidLisp Keeps",
  "version": "2.0.0",
  "interfaces": ["TZIP-012", "TZIP-016", "TZIP-021"],
  "imageUri": "https://oven.aesthetic.computer/keeps/latest",
  "homepage": "https://aesthetic.computer"
}

Infrastructure#

Services#

Service URL Purpose
Oven https://oven.aesthetic.computer Thumbnail generation (Puppeteer + FFmpeg)
Grab https://grab.aesthetic.computer Static screenshot fallback
Pinata IPFS pinning Artifact and metadata storage
TzKT api.ghostnet.tzkt.io On-chain data queries

Thumbnail Generation#

  • Format: Animated WebP
  • Size: 96x96 @ 2x density (192x192 actual)
  • Duration: 8 seconds capture
  • FPS: 10 capture → 20 playback
  • Quality: 70

Phase 2: Creator Authorization (PLANNED)#

Problem#

Currently only admin can mint. We want:

  1. Only handled users (with @handle) can mint
  2. Users can only mint their own pieces (pieces they authored)
  3. Minting should be self-service via web UI

Existing Infrastructure#

Authentication#

  • Auth0 provides JWT tokens via /userinfo endpoint
  • authorize() in backend/authorization.mjs validates tokens
  • Returns { sub, email, email_verified, ... }

Piece Ownership (Already Tracked!)#

The kidlisp-codes MongoDB collection already stores:

{
  code: "cow",           // Piece name
  source: "(wipe...)",   // Source code
  hash: "...",           // SHA-256 of source
  user: "auth0|123...",  // Creator's Auth0 sub ID ✅
  when: Date,            // Created timestamp
}

Handle Resolution#

  • handleFor(userId) in backend/authorization.mjs gets @handle from sub
  • fetchAuthorInfo(userId) in bundle-html.js resolves handle + userCode

Architecture#

┌─────────────────┐     ┌──────────────────────────────┐     ┌─────────────────┐
│   AC Frontend   │────▶│  /api/kidlisp-keep           │────▶│  Tezos Contract │
│   (user clicks  │     │  (Netlify function)          │     │  (SmartPy FA2)  │
│    "Keep" btn)  │     │                              │     │                 │
└─────────────────┘     │  1. Validate JWT (Auth0)     │     └─────────────────┘
        │               │  2. Check user has @handle   │
        │ JWT Bearer    │  3. Verify piece ownership   │
        │ token         │  4. Check not already minted │
        ▼               │  5. Generate bundle & thumb  │
                        │  6. Upload to IPFS           │
                        │  7. Sign & submit Tezos tx   │
                        └──────────────────────────────┘
                                      │
                                      ▼
                        ┌──────────────────────────────┐
                        │  MongoDB `kidlisp-codes`     │
                        │  - code → user mapping       │
                        │  - piece ownership proof     │
                        └──────────────────────────────┘

Authorization Flow#

  1. User Authentication (Auth0)

    const user = await authorize({ authorization: req.headers.authorization });
    if (!user) return 401 Unauthorized;
    
  2. Handle Requirement

    const handle = await handleFor(user.sub);
    if (!handle) return 403 "You need an @handle to mint";
    
  3. Piece Ownership Verification

    const piece = await db.collection('kidlisp-codes').findOne({ code: pieceName });
    if (!piece) return 404 "Piece not found";
    if (piece.user !== user.sub) return 403 "You don't own this piece";
    
  4. Duplicate Check

    const duplicate = await checkDuplicatePiece(pieceName);
    if (duplicate.exists) return 409 "Already minted as token #X";
    
  5. Minting

    • Generate bundle via existing bundle-html.js logic
    • Generate thumbnail via Oven
    • Upload to IPFS
    • Sign transaction with server-side admin key
    • Submit to Tezos

API Endpoints#

POST /api/kidlisp-keep#

Mint a new keep (requires auth)

Headers:

  • Authorization: Bearer <JWT>

Body:

{
  "piece": "cow",
  "generateThumbnail": true
}

Response:

{
  "success": true,
  "tokenId": 5,
  "txHash": "op...",
  "artifactUri": "ipfs://...",
  "objktUrl": "https://objkt.com/asset/KT1.../5"
}

Errors:

  • 401 - Not authenticated
  • 403 - No @handle, or not piece owner
  • 404 - Piece not found
  • 409 - Already minted

GET /api/kidlisp-keep?piece=cow#

Check piece mint status (public)

Response:

{
  "piece": "cow",
  "canMint": true,
  "owner": "@jeffrey",
  "minted": false
}
// or if minted:
{
  "piece": "cow",
  "canMint": false,
  "minted": true,
  "tokenId": 5,
  "objktUrl": "https://..."
}

Security#

  1. Admin Key Protection

    • Tezos private key in Netlify env: TEZOS_KIDLISP_KEY
    • Never exposed to client
    • Server signs all transactions
  2. Ownership Enforcement

    • Only piece.user === user.sub can mint
    • First saver owns the piece (existing behavior)
    • Admin can mint any piece (bypass)
  3. On-Chain Protection

    • SmartPy contract: assert self.is_administrator_() on all mutations
    • content_hashes big_map prevents duplicate minting
    • Metadata locking is permanent once applied
  4. Rate Limiting (Future)

    • Per-user limits (e.g., 5 mints/day)
    • Prevent spam

Scalability (Designed for Millions of Pieces)#

  1. Token ID Retrieval - O(1)

    • Uses next_token_id - 1 from storage, not pagination
    • Works at any scale
  2. Duplicate Check - O(1)

    • content_hashes big_map lookup by key
    • Big maps are hash tables, constant-time access
  3. Status Command - O(1) with pagination

    • Uses TzKT API with limit and sort.desc
    • Shows only recent tokens, total count from storage
  4. Gas Costs - Constant

    • Big map operations don't increase with collection size
    • ~0.05 tez per mint regardless of token count
  5. IPFS Storage

    • ~50KB per piece average (bundle + metadata + thumb)
    • 1M pieces ≈ 50GB = ~$7.50/month on Pinata

Implementation Steps#

  1. Document existing infrastructure
  2. Implement /api/kidlisp-keep GET (check status)
  3. Implement /api/kidlisp-keep POST (mint)
  4. Add "Keep" button to UI when viewing own piece
  5. Test locally with dev server
  6. Deploy to production
  7. Add rate limiting

Environment Variables Needed#

# Netlify env vars (already have most of these)
TEZOS_KIDLISP_KEY=edsk...       # Admin signing key
TEZOS_CONTRACT_ADDRESS=KT1...   # Keeps contract
TEZOS_NETWORK=ghostnet          # or mainnet
PINATA_API_KEY=...              # For IPFS uploads
PINATA_API_SECRET=...
OVEN_URL=https://oven.aesthetic.computer

Files#

File Purpose
tezos/keeps_fa2_v2.py SmartPy contract source
tezos/keeps.mjs CLI tool for minting/management
tezos/contract-address.txt Current deployed contract
oven/server.mjs Thumbnail generation server
oven/grabber.mjs Puppeteer frame capture

Deployment History#

Date Contract Network Notes
2025-12-09 KT1Ah5m2kzU3GfN42hh57mVJ63kNi95XKBdM Ghostnet Current - with burn, redact
2025-12-09 KT1FvJyG4e6tRHdJLTjMhvi7mMrrAGkBCdBv Ghostnet Added piece-name uniqueness
2025-12-09 KT1CfExN8EcSMS5Pm2vzxpQKyzkijNHvGCdm Ghostnet Added content_hashes
2025-12-09 KT1N9jz6NJaBYW4LVhccZs6ttQMvFEAmkkSM Ghostnet First with metadata lock