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_hashesbig_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 keepset_keep_fee(new_fee)- Admin updates feewithdraw_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:
- Only handled users (with
@handle) can mint - Users can only mint their own pieces (pieces they authored)
- Minting should be self-service via web UI
Existing Infrastructure#
Authentication#
- Auth0 provides JWT tokens via
/userinfoendpoint authorize()inbackend/authorization.mjsvalidates 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)inbackend/authorization.mjsgets@handlefromsubfetchAuthorInfo(userId)inbundle-html.jsresolves 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#
-
User Authentication (Auth0)
const user = await authorize({ authorization: req.headers.authorization }); if (!user) return 401 Unauthorized; -
Handle Requirement
const handle = await handleFor(user.sub); if (!handle) return 403 "You need an @handle to mint"; -
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"; -
Duplicate Check
const duplicate = await checkDuplicatePiece(pieceName); if (duplicate.exists) return 409 "Already minted as token #X"; -
Minting
- Generate bundle via existing
bundle-html.jslogic - Generate thumbnail via Oven
- Upload to IPFS
- Sign transaction with server-side admin key
- Submit to Tezos
- Generate bundle via existing
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 authenticated403- No @handle, or not piece owner404- Piece not found409- 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#
-
Admin Key Protection
- Tezos private key in Netlify env:
TEZOS_KIDLISP_KEY - Never exposed to client
- Server signs all transactions
- Tezos private key in Netlify env:
-
Ownership Enforcement
- Only
piece.user === user.subcan mint - First saver owns the piece (existing behavior)
- Admin can mint any piece (bypass)
- Only
-
On-Chain Protection
- SmartPy contract:
assert self.is_administrator_()on all mutations content_hashesbig_map prevents duplicate minting- Metadata locking is permanent once applied
- SmartPy contract:
-
Rate Limiting (Future)
- Per-user limits (e.g., 5 mints/day)
- Prevent spam
Scalability (Designed for Millions of Pieces)#
-
Token ID Retrieval - O(1)
- Uses
next_token_id - 1from storage, not pagination - Works at any scale
- Uses
-
Duplicate Check - O(1)
content_hashesbig_map lookup by key- Big maps are hash tables, constant-time access
-
Status Command - O(1) with pagination
- Uses TzKT API with
limitandsort.desc - Shows only recent tokens, total count from storage
- Uses TzKT API with
-
Gas Costs - Constant
- Big map operations don't increase with collection size
- ~0.05 tez per mint regardless of token count
-
IPFS Storage
- ~50KB per piece average (bundle + metadata + thumb)
- 1M pieces ≈ 50GB = ~$7.50/month on Pinata
Implementation Steps#
- Document existing infrastructure
- Implement
/api/kidlisp-keepGET (check status) - Implement
/api/kidlisp-keepPOST (mint) - Add "Keep" button to UI when viewing own piece
- Test locally with dev server
- Deploy to production
- 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 |