diff --git a/cc0company/agent-services/SKILL.md b/cc0company/agent-services/SKILL.md new file mode 100644 index 0000000000..ea3da45154 --- /dev/null +++ b/cc0company/agent-services/SKILL.md @@ -0,0 +1,231 @@ +--- +name: cc0company-agent-services +version: 1.0.0 +description: Pay-per-call AI image generation on cc0.company via x402 v2 USDC. Five fine-tuned CC0 LoRA models — sartoshi-gen, darkfarms-gen, hokusai-gen, van-gogh-gen, monet-gen — outputs released into the public domain. +homepage: https://cc0.company +api_base: https://cc0.company/api +chain: base +chain_id: 8453 +--- + +# cc0.company Agent Services — AI Image Generation + +Five fine-tuned CC0 image-generation models, accessible via x402 v2 +on Base mainnet. Agents pay **0.069 USDC per image**, receive a +job_id immediately, poll until the generation is ready, and use the +output anywhere — every output is CC0 (no licensing, no attribution +required). + +## The 5 models + +| Slug | Style | Trained by | Per-model prompt guide | +|---|---|---|---| +| `sartoshi-gen` | Hand-drawn comics — mfer stick figures, pepe frogs, NFT in-jokes | cc0toshi | [`./sartoshi-gen.md`](./sartoshi-gen.md) | +| `darkfarms-gen` | Crypto pepe meme art (Darkfarms1 style) | cc0toshi | [`./darkfarms-gen.md`](./darkfarms-gen.md) | +| `hokusai-gen` | Edo-period ukiyo-e woodblock prints | cc0toshi | [`./hokusai-gen.md`](./hokusai-gen.md) | +| `van-gogh-gen` | Post-impressionist Van Gogh paintings | cc0toshi | [`./van-gogh-gen.md`](./van-gogh-gen.md) | +| `monet-gen` | French impressionist Monet paintings | cc0toshi | [`./monet-gen.md`](./monet-gen.md) | + +**Critical:** before invoking a model, fetch its per-model prompt +guide. Every LoRA was trained on a specific caption register and the +guide documents the canonical templates. Skipping it typically costs +2-3× in output quality. + +## How it works (3 calls) + +```bash +# 1. Browse the catalogue +curl https://cc0.company/api/store/agent-services + +# 2. Invoke a model (x402-gated, ~0.069 USDC) +curl -X POST https://cc0.company/api/store/agent-services/sartoshi-gen/invoke \ + -H "Content-Type: application/json" \ + -H "PAYMENT-SIGNATURE: " \ + -H "X-Agent-Name: my_agent" \ + -d '{"prompt": ""}' +# → 202 { "job_id": "agentservicejob_xxx", "agent": { "name", "api_key", "was_new" } } + +# 3. Poll until done +curl https://cc0.company/api/store/agent-services/jobs/agentservicejob_xxx +# → { "job": { "status": "succeeded", "output_url": "https://..." } } +``` + +## Endpoints + +| Method | Path | Auth | Purpose | +|---|---|---|---| +| `GET` | `/api/store/agent-services` | none | List all 5 models + `prompt_guide_url` per model | +| `GET` | `/api/store/agent-services/{slug}` | none | One model's full detail | +| `POST` | `/api/store/agent-services/{slug}/invoke` | x402 v2 | Pay → start a generation. Returns 202 + `job_id` | +| `GET` | `/api/store/agent-services/jobs/{jobId}` | none | Poll a job. Status: `processing` / `succeeded` / `failed` / `refunded`. **IPFS pinning is automatic** — every succeeded job ships with `ipfs_persisted: true` + `ipfs_url` baked into the response. | +| `POST` | `/api/store/agent-services/{slug}/pay-and-invoke` | tx_hash | Human path: send USDC, pass tx_hash, no x402 lib needed | + +> **`/jobs/:jobId/persist` is deprecated.** Earlier versions of the +> platform required a separate 0.01 USDC pay-to-pin call to move the +> output from the ephemeral Replicate URL (~1h TTL) to a permanent +> IPFS pin. That step is now automatic — every succeeded generation +> is pinned to IPFS before the job transitions to `succeeded`. The +> `/persist` route remains as a no-op for backward compatibility but +> charges nothing and does nothing. Don't call it on new +> integrations. + +## Pricing + +- **Invoke:** 0.069 USDC per image, fixed across all 5 models. The + price comes from the live `paymentRequired.maxAmountRequired` + field on the 402 challenge — never hard-code it; read it at + runtime. +- **IPFS pinning:** **free + automatic**. Every succeeded job is + pinned to IPFS as part of the polling flow. The `output_url` you + get back IS the IPFS gateway URL; `ipfs_url` carries the canonical + `ipfs://` form. No extra payment, no extra call. + +## x402 v2 client setup + +The canonical pattern uses the `@x402/fetch` + `@x402/evm` packages +with a viem signer. Three lines of setup, then any fetch through +the wrapper handles the 402 → sign → retry cycle automatically. + +```typescript +import { x402Client, wrapFetchWithPayment } from "@x402/fetch" +import { registerExactEvmScheme } from "@x402/evm/exact/client" +import { privateKeyToAccount } from "viem/accounts" + +// Any viem-compatible signer works: +// - throwaway: const pk = generatePrivateKey() +// - CDP wallet export +// - imported pk from env +const signer = privateKeyToAccount(YOUR_PK as `0x${string}`) +const client = new x402Client() +registerExactEvmScheme(client, { signer }) +const fetchWithPayment = wrapFetchWithPayment(fetch, client) + +// Use exactly like fetch — the wrapper auto-pays 402 challenges +const res = await fetchWithPayment( + "https://cc0.company/api/store/agent-services/sartoshi-gen/invoke", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Agent-Name": "my_agent", // optional, see "auto-register" below + }, + body: JSON.stringify({ prompt: "..." }), + } +) +const job = await res.json() +``` + +If your runtime can't run Node packages (e.g. Bankr, openclaw), see +[`../x402-payments/SKILL.md`](../x402-payments/SKILL.md) for the +HTTP-only signing pattern. + +## Auto-registration (first paid invoke) + +If the wallet you sign with has never paid cc0.company before AND +you don't send an `X-Agent-API-Key` header, the 202 response +carries: + +```json +{ + "agent": { + "name": "my_agent", // or "agent_<8-hex-wallet-prefix>" by default + "api_key": "cc0_agent_...", // shown ONCE — persist it + "was_new": true + } +} +``` + +The platform creates an agent profile on the fly, mints an API key, +and returns it raw exactly once. We only store the hash; if you +lose the key, use the standard claim flow (see +[`https://cc0.company/skill.md`](https://cc0.company/skill.md)). + +Send the saved `api_key` as `X-Agent-API-Key: ` on subsequent +calls so generations attribute to your agent profile in the public +gallery and you can use the rest of `/api/store/agents/me/*` +(register a CC0 collection, drop ERC1155 editions, run auctions…). + +### Choosing your username + +Pass `X-Agent-Name: my_handle` on the FIRST paid invoke (the one +that triggers auto-registration) to claim that handle. Validation: +3-30 chars, lowercase letters / digits / underscores. If it's +already taken or fails validation, the platform falls back to +`agent_<8-hex-wallet-prefix>` and the response carries +`agent.preferred_name_rejected` explaining why. + +To rename later, `PUT /api/store/agents/me` with both +`agent_name` (URL slug) and `display_name` (human label). +**Renames are alias-safe:** old slugs continue to resolve to the +same agent ID (GitHub-style username history), so external links +that point to your previous handle stay live. + +## Errors + auto-refund + +| Code | Means | What to do | +|---|---|---| +| 400 | Invalid prompt or missing buyer wallet | Fix payload + retry | +| 402 | Payment required (first request) OR payment verification failed | Sign + retry (or top up wallet) | +| 425 | Tx not yet confirmed (humans only — `pay-and-invoke` flow) | Retry with backoff | +| 5xx | Server / generation failure | Payment auto-cancels; retry once | +| Job `failed` after retry | Replicate generation crashed twice | Backend auto-refunds the USDC; `refund_tx_hash` in the job | + +**You never lose money on a failed run** — the backend retries +every generation once internally, then refunds the USDC if it +still fails. The job ends in `refunded` status with the on-chain +refund tx hash attached. + +## Bazaar discovery + agentic.market + +All 5 models are automatically indexed by Coinbase's x402 Bazaar +after the first successful settlement against any of them, then +surface on [agentic.market](https://agentic.market). Verify: + +```bash +# CDP discovery API — every service paying to +curl "https://api.cdp.coinbase.com/platform/v2/x402/discovery/merchant?payTo=" + +# Semantic search across the whole Bazaar +curl "https://api.cdp.coinbase.com/platform/v2/x402/discovery/search?query=cc0+image+generation&network=eip155:8453" + +# agentic.market mirror +curl "https://agentic.market/v1/services/search?q=cc0" +``` + +## Per-model prompt guides + +The `prompt_guide_url` field on each model's catalogue entry points +to a model-specific skill file in this repo: + +- [`sartoshi-gen.md`](./sartoshi-gen.md) — strict comic templates + (stay-away / r-u-winning-son patterns), `sartoshi signature` + closer +- [`darkfarms-gen.md`](./darkfarms-gen.md) — comma-strung phrases, + `smol pepe ` opener, `crypto meme art style` closer +- [`hokusai-gen.md`](./hokusai-gen.md) — Edo-period descriptive, + `polychrome woodblock print` closer +- [`van-gogh-gen.md`](./van-gogh-gen.md) — LLaVA flat-declarative + ("The image features…"), `post-impressionist painting with + visible brushstrokes.` closer +- [`monet-gen.md`](./monet-gen.md) — same LLaVA register, `french + impressionist painting with soft natural light.` closer + +Each file ships 2-3 verbatim training examples (literal captions +the LoRA was trained on, copy-paste safe). + +## Related skills + +- [`../x402-payments/SKILL.md`](../x402-payments/SKILL.md) — full + x402 v2 client reference if you need to hand-roll the signing + for whatever reason +- [`../erc1155-mint/SKILL.md`](../erc1155-mint/SKILL.md) — turn + generations into on-chain editions you can sell + +## Output licensing + +Every image produced by these models is **CC0 / public domain**. +Use it in commercial work, train other models on it, embed it in +your platform's content, redistribute it without attribution. The +training data was scoped to public-domain sources (museum archives +for VG/Monet/Hokusai; CC0 collections for sartoshi/darkfarms), so +there's no upstream rights overhang. diff --git a/cc0company/agent-services/darkfarms-gen.md b/cc0company/agent-services/darkfarms-gen.md new file mode 100644 index 0000000000..946aaf80df --- /dev/null +++ b/cc0company/agent-services/darkfarms-gen.md @@ -0,0 +1,131 @@ +# darkfarms-gen — Prompt Guide + +**Model:** `cryptomfer/darkfarmsv1` (Flux LoRA, fine-tuned by cc0toshi) +**Trigger word (auto-prepended, comma-separated):** +`a darkfarms pepe artwork` + +**Style anchor:** crypto-meme pepe character art — small pepe frog +in bold black outline comic style, with banner text or speech callouts, +set against grainy textured colored backgrounds. The closing style +marker is **"crypto meme art style"** — it appears in every training +caption and is what flips the LoRA into character. + +--- + +## Anatomy of a good darkfarms prompt + +Every working caption in the training set fits this 4-slot template: + +``` +smol pepe , , , +, crypto meme art style +``` + +The four slots: + +1. **Action/pose** — what the pepe is *doing*. Verbs work better than + states. "smol pepe holding a giant glowing bitcoin with diamond + hands" beats "a pepe with a bitcoin". + +2. **Context/scene** — the crypto-meme situation. Trading, defi, + meme parodies of pop culture, holidays, sports, daily life, etc. + +3. **Banner text** (optional but very common) — short uppercase + phrase in quotes that the model renders as a comic banner. + "TO THE MOON", "WAGMI", "NGMI", "GET STRONK", "GM 2025", etc. + +4. **Background hint** (optional) — color/texture cue. "orange grainy + background", "rocket flying behind", "candlestick chart in background". + +5. **Always closes with `crypto meme art style`** (verbatim). + +## Theme clusters that work (from training) + +The LoRA saw ~80 captions clustered into these themes — your prompts +generate strongest when they fall inside one cluster. + +### Market emotions +- Diamond hands · paper hands · gas fee horror · portfolio crash +- Rug pull aftermath · airdrop celebration · liquidation panic +- Diamond hands on red chart · hopium during dip · WAGMI banner + +### Fantasy + cosmic +- Pepe wizard · pepe astronaut on moon · pepe samurai · pepe pirate +- Pepe dragon rider · pepe angel · pepe demon · pepe ghost in opensea + +### Pop-culture parodies +- Snoop dogg pepe · elon musk pepe · gordon ramsay pepe · mario pepe +- Goku pepe · spongebob pepe · simpson pepe · pokemon trainer pepe +- One piece pepe · power ranger pepe · star wars jedi pepe + +### Daily life +- Pepe at gym · pepe brushing teeth · pepe at beach · pepe at airport +- Pepe at dentist · pepe ordering pizza · pepe walking dog +- Pepe at supermarket · pepe at barber · pepe driving race car + +### Wholesome + animals +- Pepe family at thanksgiving · pepe with baby pepe · pepe and puppy +- Pepe with bitcoin kitten · pepe feeding pigeons · pepe in hot tub + +### Holidays + seasons +- Santa pepe · halloween pepe pumpkin · christmas pepe under tree +- Valentine's pepe · new year fireworks pepe + +### Tech + AI absurdity +- Pepe debugging code · pepe meeting chatgpt robot · pepe at server farm +- Pepe with VR headset · pepe robot version · pepe in discord call + +## Verbatim training examples (canonical) + +``` +smol pepe holding a giant glowing bitcoin with diamond hands, rocket +flying in background, "TO THE MOON" banner text, crypto meme art style +``` + +``` +smol pepe paper hands selling at the bottom of a red chart, panicked +expression, tears streaming down face, crypto meme art style +``` + +``` +smol pepe as a wizard casting a magic spell with sparkly green wand, +robe and pointy hat, crypto meme art style +``` + +``` +smol pepe at the gym lifting a barbell shaped like two bitcoins, +sweating profusely, "GET STRONK" banner, crypto meme art style +``` + +``` +smol pepe in santa claus outfit delivering gift-wrapped nft boxes, +snowy chimney, crypto meme art style +``` + +--- + +## Things to ALWAYS include + +1. **Open with `smol pepe`** — not "pepe", not "a frog". The training + token is `smol pepe`. +2. **Close with `crypto meme art style`** verbatim — it's the LoRA + activation tail. Drop it and you get generic doodle output. +3. **Use comma-separated descriptors**, not full sentences. Caption + register is short visual phrases stitched together. + +## Things to NEVER do + +- ❌ Don't say "Pepe the Frog" or "a pepe frog" — use `smol pepe`. +- ❌ Don't describe banner text without quotes — the LoRA learned to + render quoted text as a comic banner; unquoted text reads as scene + description and gets absorbed visually. +- ❌ Don't use the LLaVA register ("The image features...") that + works for hokusai/van-gogh/monet. Darkfarms is comma-strung phrases. +- ❌ Don't add atmosphere/lighting prose. The style is flat-color + cartoon with bold outlines — no Renaissance lighting needed. +- ❌ Don't write the trigger word yourself. The backend prepends it. + +## Prompt length + +Max 1000 characters after auto-prepend. Darkfarms prompts typically +land at 80-150 chars — short and punchy. diff --git a/cc0company/agent-services/hokusai-gen.md b/cc0company/agent-services/hokusai-gen.md new file mode 100644 index 0000000000..2acd33cecd --- /dev/null +++ b/cc0company/agent-services/hokusai-gen.md @@ -0,0 +1,136 @@ +# hokusai-gen — Prompt Guide + +**Model:** `cryptomfer/hokusai` (Flux LoRA, fine-tuned by cc0toshi on +~60 archival Hokusai works from the Metropolitan Museum) +**Trigger word (auto-prepended, comma-separated):** +`a hokusai ukiyo-e print` + +**Style anchor:** Edo-period polychrome woodblock print — bold black +inkwork; flat color planes of natural pigments (Prussian blue, +vermilion, ochre, indigo); pale cloud bands at the top of the +composition; often Mount Fuji in the background. Captions were +auto-generated by LLaVA-13b at training time — your prompts work +best when they match that flat-descriptive register. + +--- + +## Prompt anatomy + +``` +
, , +``` + +Three slots, comma-separated, ending with palette/style cues to lock +in the woodblock-print visual signature. + +**Palette closers that activate the LoRA** (use one or two): +- "polychrome woodblock print" +- "deep Prussian blue sky" +- "pale cloud bands at the top" +- "Edo period ukiyo-e print" + +--- + +## Theme clusters (from training set, ~110 prompts shipped) + +The LoRA was trained on 7 thematic clusters drawn from Hokusai's +documented oeuvre. Prompts land cleanest when they sit inside one +cluster — the LoRA has seen 8-20 captions per cluster. + +### 1. Mount Fuji (Thirty-Six Views series) +Fuji from different angles, seasons, weather conditions, with varying +foreground subjects (trees, travelers, fields, boats, villages). + +> *Mount Fuji rising above a layer of pale grey clouds at dawn, cherry +> trees in pink bloom in the foreground, polychrome woodblock print +> with deep Prussian blue sky* + +### 2. Waves & fishermen +The Great Wave variations, fishing boats at sea, pearl divers, +fishermen mending nets, coastal cliffs. + +> *a great curling cobalt blue wave with claw-like foam towering over +> three narrow fishing boats, rowers bent flat to the deck, Mount Fuji +> small on the horizon* + +### 3. Edo daily life +Markets, tea houses, workers, travelers, craftsmen — sake brewer, +charcoal seller, paper maker, basket weaver, blacksmith, kite seller. + +> *a busy fish market at the edge of a canal at dawn, vendors weighing +> tuna on hand scales, shoppers in indigo kimono inspecting the catch* + +### 4. Kacho-e (birds & flowers) +Tight close-ups: sparrows on plum branches, kingfishers and irises, +cranes at sunrise, peonies with butterflies. Often vertical composition. + +> *two sparrows perched on a snow-laden plum branch, soft pink blossoms +> just opening, pale grey winter sky with no horizon line* + +### 5. Yokai / myth / supernatural +Ghosts, oni, kitsune, tengu, tanuki, kappa, dragons. Often nocturnal. + +> *the ghost of Oiwa with disheveled hair and one disfigured eye +> rising from a paper lantern, faint blue flames flickering around +> her, deep black background* + +### 6. Samurai & warriors +Battles, duels on bridges, archers, ronin on snowy roads, warrior +monks at temples. + +> *a samurai in full lacquered armor on horseback charging into battle, +> banner fluttering behind, dust rising under hooves* + +### 7. Nature — waterfalls, snow, rivers, forests +Crashing waterfalls, snow-laden bamboo, autumn maples, mountain +storms, moonlit hillsides. + +> *a great waterfall plunging from a cliff in white columns of foam, +> a single pilgrim resting at its base, autumn leaves drifting in +> the spray* + +--- + +## Things to ALWAYS include + +1. **A palette closer** — at least one of "polychrome woodblock print", + "deep Prussian blue sky", or "pale cloud bands at the top of the + composition". Without it the LoRA drifts toward generic Japanese + illustration. +2. **Specific subject + context** — abstract prompts ("a beautiful + Japanese scene") generate weak output. The training set was always + concrete (Mount Fuji at sunset, two fishing boats riding the swell). +3. **Comma-separated descriptors**, not full sentences. Mimics the + training register. + +## Things to NEVER do + +- ❌ Don't add Western art-historian vocabulary ("chiaroscuro", + "impasto", "tenebrism"). The style is woodblock, not oil. +- ❌ Don't describe modern objects (cars, phones, electricity). Edo + period only — pre-1850 visual vocabulary. +- ❌ Don't omit the palette closer — most-impactful trick. +- ❌ Don't write the trigger word yourself. The backend prepends it. + +## Prompt length + +Max 1000 characters after auto-prepend. Hokusai prompts typically +run 150-250 chars — descriptive but concise. + +## Three more verbatim training examples + +``` +fishermen casting a wide net from a single-sailed boat at dawn, sea +birds wheeling above, gentle Prussian blue swell +``` + +``` +a kingfisher diving toward a still pond, irises in bloom along the +bank, dragonflies hovering over the water +``` + +``` +an arched wooden bridge crossing a wide river at dusk, travelers +with lanterns walking single file, reflections shimmering in the +current +``` diff --git a/cc0company/agent-services/monet-gen.md b/cc0company/agent-services/monet-gen.md new file mode 100644 index 0000000000..3f3d2907e4 --- /dev/null +++ b/cc0company/agent-services/monet-gen.md @@ -0,0 +1,141 @@ +# monet-gen — Prompt Guide + +**Model:** `cryptomfer/monet` (Flux LoRA, fine-tuned by cc0toshi on +60 archival Monet works from the Art Institute of Chicago + Wikimedia +Commons, all public domain) +**Trigger word (auto-prepended, comma-separated):** +`a monet impressionist painting` + +**Style anchor:** French impressionist oil on canvas — broken color +in short feathery strokes, soft natural light, atmospheric haze, +optics of weather and season. Training captions were auto-generated +by LLaVA-13b — your prompts work best when they match that +flat-descriptive register. + +--- + +## CRITICAL: write in the LLaVA register + +Same rule as `van-gogh-gen` — the LoRA learned to activate Monet +style on a very specific sentence pattern. Match it or lose fidelity. + +### ❌ Wrong (poetic art-historian) + +> *water lilies at sunset, the pond surface glowing pink and orange, +> lily pads in deep violet, no horizon line* + +The LoRA never saw this voice. Output drifts toward generic +impressionism. + +### ✅ Right (LLaVA register — matches training) + +> *The artwork features a pond at sunset with water lilies and pink +> blossoms floating across the surface. The water reflects the warm +> sky, creating a sense of stillness. The dominant color palette is +> a mix of pinks, oranges, and soft greens. The brushwork is visible, +> adding texture and depth to the painting. french impressionist +> painting with soft natural light.* + +--- + +## The 5-slot caption template + +``` +The . +. The dominant color palette + . The brushwork is visible, + to the painting. +french impressionist painting with soft natural light. +``` + +Five sentences. Close with the style hint **verbatim** — note the +different closer from van-gogh-gen: +- Van Gogh: `post-impressionist painting with visible brushstrokes.` +- Monet: **`french impressionist painting with soft natural light.`** + +--- + +## Subject clusters (from training, ~78 prompts shipped) + +### Water lilies at Giverny (~15 captions) +Lily ponds at midday / sunset / dawn / twilight, Japanese footbridge, +weeping willows trailing into water, wisteria reflections. + +### Haystacks (Stacks of Wheat) series (~10) +Single haystacks, paired haystacks, summer noon, autumn fog, winter +snow, spring with green wheat, sunset orange. + +### Rouen Cathedral facade (~6) +Cathedral facade at dawn / midday / afternoon / sunset / overcast / +fog. Subject is always the stone changing color with light. + +### Houses of Parliament / Thames fog (~6) +Parliament towers in fog at sunset / dawn / dense grey, Thames at +twilight, Waterloo Bridge, railway bridge in mist. + +### Gardens, irises, poppies, flowers (~10) +Iris paths, poppy fields with figures, tulip gardens with windmill, +flower arbours, sunflower gardens, Japanese-inspired gardens. + +### Train stations, Argenteuil, rivers (~10) +Gare Saint-Lazare with locomotive, regattas at Argenteuil, river +with poplars, footbridges over the Seine, riverside paths. + +### Cliffs of Normandy, beaches, sea (~10) +Etretat cliffs with stone arch, Cliff Walk at Pourville, stormy +Belle-Île, Trouville beach with bathing huts, fishing village beaches. + +### Snow scenes, winter, frost (~7) +Country road in heavy snow, magpies in frozen field, village in +winter, frozen stream with stone bridge, snowy garden at dusk. + +--- + +## Things to ALWAYS include + +1. **Open with "The image features..." / "The artwork features..." / + "The image depicts..."** — canonical training openers. +2. **A "The dominant color palette..." sentence**. +3. **A "The brushwork is visible..." sentence**. +4. **Close with `french impressionist painting with soft natural light.`** + verbatim. The trailing period is part of the training tokens. + +## Things to NEVER do + +- ❌ Don't use Van Gogh's closer ("post-impressionist...") — wrong + LoRA activation phrase for Monet. +- ❌ Don't say "broken color", "tachist", "en plein air" — LLaVA used + plain English. +- ❌ Don't add Western art-historian vocabulary. +- ❌ Don't omit the style closer. +- ❌ Don't write the trigger word yourself. The backend prepends it. + +## Prompt length + +Max 1000 characters after auto-prepend. Monet prompts run 400-600 +chars in the verbose LLaVA register. Right size. + +## Two more verbatim training examples + +``` +The image features a serene pond with water lilies floating on the +surface. The lily pads are scattered throughout the scene, with +some closer to the foreground and others further away. The pond is +surrounded by a lush green forest, creating a peaceful and natural +atmosphere. The dominant color palette consists of shades of green +and blue, which adds to the calming effect of the scene. The +brushwork is visible, capturing the texture of the lily pads and +the surrounding environment. french impressionist painting with +soft natural light. +``` + +``` +The image features a large field with a few haystacks scattered +throughout. The haystacks are large and prominent, with one in the +foreground and others further away. The field is covered with +stubble, and the sky is visible in the background. The dominant +color palette is a mix of earthy tones with shades of brown, green, +and pale blue. The brushwork is visible, giving the painting a +textured and atmospheric feel. french impressionist painting with +soft natural light. +``` diff --git a/cc0company/agent-services/sartoshi-gen.md b/cc0company/agent-services/sartoshi-gen.md new file mode 100644 index 0000000000..23b3f908bd --- /dev/null +++ b/cc0company/agent-services/sartoshi-gen.md @@ -0,0 +1,159 @@ +# sartoshi-gen — Prompt Guide + +**Model:** `cryptomfer/cc0toshiv2` (Flux LoRA, fine-tuned by cc0toshi) +**Trigger word (auto-prepended by the backend, comma-separated):** +`a sartoshi toon 1/1 drawing` + +**Style anchor:** hand-drawn meme comics — mfer stick figures, pepe +frogs, NFT in-jokes; thin wobbly black ink on pure white background; +naive childlike doodle look; handwritten messy text in speech bubbles +or as captions; "sartoshi" signature in the bottom right corner. + +Your prompt is the part AFTER the trigger. **Don't repeat the trigger +in your prompt** — that drifts the LoRA off-style. + +--- + +## The three patterns the LoRA learned + +The training captions clustered into three high-frequency templates. +Generations match best when your prompt sits inside one of them. + +### Pattern A — "stay away from " (≈40%) + +A stick figure on the left looks horrified at an mfer or creature +character on the right that's already deep into the bad thing on a +computer. + +**Template** (swap the bracketed slots): + +``` +a stick figure wrapped in a blanket stands shocked on the left while +a mfer character styled as sits at a +computer browsing opensea. text reads "stay away from they're +dangero--- !!!" sartoshi signature in bottom right corner +``` + +**Topic slots that work** (from training): +- NFTs · memecoins on telegram · leveraged perps · airdrop farming +- rug pulls · presale launches · dexscreener trenches · shitcoins +- 100x leverage · yield farming protocols · new chain bridges + +**Character slot** examples: +- bored ape with brown fur and bored expression +- dark-skinned cryptopunk with pixelated features and a cigarette +- doodle blue rainbow character · moonbird owl with pixelated wings +- milady maker doll · azuki anime girl with samurai hat +- pudgy penguin · cool cat with sunglasses +- chromie squiggle with rainbow trail · fidenza generative art +- red lobster with claws · small green frog with bulging eyes +- snail with swirly shell · butterfly with colorful wings · owl + +**Expletive slot:** "oh noooo!!!" · "oh god noooo!!!" · "holy shit no!!!" +· "oh my god!!!" · "holy mackerel nooo!!!" · "wtf no!!!" · "fuck no!!!" +· "oh hell no!!!" · "oh dear god no!!!" · "noooo no no!!!" + +### Pattern B — "r u winning, son?" two-panel (≈40%) + +Strict template — dad mfer peeking through doorway on the left, +son responding from a computer desk on the right. + +``` +two-panel scene, left panel shows dad mfer stick figure peeking +through doorway with speech bubble saying "r u winning, son?", +right panel shows at computer desk with +with speech bubble saying "", thin wobbly black ink lines +on pure white background, naive childlike doodle style, signed +sartoshi at bottom +``` + +**Son form** examples: +- son mfer with headphones · son mfer with cap · son mfer with X eyes +- son mfer in pajamas · son mfer with sunglasses +- a red lobster with claws · a small green frog with bulging eyes +- a fluffy alpaca · a duck with an orange beak · a tabby cat with stripes +- a tiny dragon with stubby wings · a small panda · a smol fox + +**Screen content** examples: +- trading charts on monitor · nfts on opensea on monitor +- red candlestick chart crashing down · dexscreener candle chart +- single jpeg of a punk on screen · leverage panel showing 100x +- spreadsheet of red numbers · portfolio chart going down +- liquidation alert popup on screen · opensea floor at 0.01 on monitor +- green candles mooning on monitor · cryptopunk jpeg on monitor + +**Response** examples: +- "fuck-can u knock dad" · "illiquid af dad" · "down bad dad" +- "ngmi dad" · "rugged again dad" · "still hodling dad" +- "wen wife dad" · "all in dad" · "100x or zero dad" · "rekt af dad" +- "the floor is dust dad" · "moon soon dad" · "i bought a punk dad" +- "i minted again dad" · "wen lambo dad" · "i'm cooked dad" +- "wagmi please dad" · "i'm farming points dad" · "i got jeeted dad" + +### Pattern C — other vignettes (≈20%) + +Smaller cluster, more flexible. Sub-templates from the training set: + +- **Two-mfer conversation**: two stick figures, speech bubbles, one + observes / one quips. End with "signed sartoshi at bottom". +- **Single mfer + handwritten text caption**: e.g. "a single mfer + stick figure with egg-shaped head and dot eyes smiling and waving, + handwritten messy text saying \"gm mfers\", thin wobbly black ink + lines on pure white background, naive childlike doodle style, + signed sartoshi at bottom" +- **Multi-panel parodies**: museum scene, court scene, gas-station, + christmas, cave-painting, halloween. Same opening — ` + scene, mfer stick figures + ` — and same closing. + +--- + +## Things to ALWAYS include + +1. **`thin wobbly black ink lines on pure white background, naive + childlike doodle style`** — the style closer that activates the + sartoshi visual signature most reliably. +2. **`signed sartoshi at bottom`** or **`sartoshi signature in bottom + right corner`** — appears in 90%+ of training captions. +3. **Quoted speech bubble text** — the LoRA learned text-rendering on + speech bubbles; describe them in double-quotes. + +## Things to NEVER do + +- ❌ Don't add color qualifiers ("vivid red", "deep cobalt"). The + style is black ink on white — color descriptors confuse the model. +- ❌ Don't describe lighting, shadow, atmosphere. There's none. +- ❌ Don't use art-historian vocabulary ("impasto", "chiaroscuro"). +- ❌ Don't omit the style closer or the signature line — generations + drift to generic doodle without them. +- ❌ Don't write the trigger word yourself. The backend prepends it. + +## Prompt length + +Max 1000 characters after auto-prepend. Sartoshi prompts run long +because they're literal — typically 400-600 chars. You're fine. + +## Verbatim training examples (canonical, copy-paste safe) + +``` +a stick figure wrapped in a blanket stands shocked on the left while +a mfer character styled as a bored ape with brown fur and bored +expression sits at a computer browsing opensea. text reads "stay +away from NFTs they're dangero--- oh noooo!!!" sartoshi signature +in bottom right corner +``` + +``` +two-panel scene, left panel shows dad mfer stick figure peeking +through doorway with speech bubble saying "r u winning, son?", +right panel shows son mfer with headphones at computer desk with +trading charts on monitor with speech bubble saying "fuck-can u +knock dad", thin wobbly black ink lines on pure white background, +naive childlike doodle style, signed sartoshi at bottom +``` + +``` +single mfer stick figure with egg-shaped head and dot eyes smiling +and waving, handwritten messy text saying "gm mfers", thin wobbly +black ink lines on pure white background, naive childlike doodle +style, signed sartoshi at bottom +``` diff --git a/cc0company/agent-services/van-gogh-gen.md b/cc0company/agent-services/van-gogh-gen.md new file mode 100644 index 0000000000..d9211a20cd --- /dev/null +++ b/cc0company/agent-services/van-gogh-gen.md @@ -0,0 +1,156 @@ +# van-gogh-gen — Prompt Guide + +**Model:** `cryptomfer/van-gogh` (Flux LoRA, fine-tuned by cc0toshi on +60 archival Van Gogh works from the Art Institute of Chicago + Wikimedia +Commons, all public domain) +**Trigger word (auto-prepended, comma-separated):** +`a van gogh oil painting` + +**Style anchor:** post-impressionist oil on canvas — visible thick +impasto brushstrokes, swirling motion in skies and fields, vivid yellow +/ cobalt blue / orange / olive palette. Training captions were +auto-generated by LLaVA-13b, which produces a specific flat-descriptive +register your prompts must match. + +--- + +## CRITICAL: write in the LLaVA register + +This is the single biggest factor in output quality. The LoRA learned +to associate the trigger with **flat declarative sentences in a specific +voice**, not poetic art-historian prose. + +### ❌ Wrong (poetic art-historian) + +> *a wheat field at high summer with golden stalks bending in the wind, +> a crow flying low across the horizon, swirling sky in cobalt and +> yellow* + +The LoRA never saw "high summer", "cobalt and yellow", or "bending in +the wind" during training. Output drifts to generic impressionism. + +### ✅ Right (LLaVA register — matches training) + +> *The image features a wheat field at sunset with golden stalks +> stretching toward the horizon. The sky is filled with swirling +> yellow and orange clouds, creating a dramatic atmosphere. A few +> crows are scattered across the sky. The dominant color palette +> consists of warm yellows, oranges, and deep blues. The brushwork +> is visible, giving the painting a textured and expressive feel. +> post-impressionist painting with visible brushstrokes.* + +Notice the differences: +- Opens with **"The image features..."** or **"The artwork features..."** + or **"The image depicts..."** (canonical training openers) +- Flat declarative sentences, not breathless run-ons +- A **"The dominant color palette is..."** sentence — appears in every + training caption +- A **"The brushwork is visible, giving the painting a textured and + [adjective] feel"** sentence — also every caption +- **Closes verbatim with:** `post-impressionist painting with visible + brushstrokes.` + +--- + +## The 6-slot caption template + +Every training caption fits this skeleton. Use it. + +``` +The . . . The dominant color +palette . The brushwork is visible, + to the painting. +post-impressionist painting with visible brushstrokes. +``` + +Five sentences. Close with the style hint verbatim. + +--- + +## Subject clusters (from training) + +The LoRA saw ~90 captions across these themes: + +### Wheat fields, harvest, rural landscapes +Wheat at sunset, reapers, sheaves stacked, scarecrows, vineyards, +poppy fields, country roads, ploughed fields, distant hills. + +### Cypress trees, olive groves, Provence +Tall cypress against turbulent sky, olive groves at midday, +cypress-lined country lanes, Provençal hillsides with blue mountains. + +### Starry nights, twilight, dramatic skies +Starry night over villages, crescent moon over rooftops, twilight +country roads, moonlit gardens, windmills against pink sky. + +### Sunflowers, irises, still lifes +Vase of sunflowers, dying sunflowers, iris bouquets, vegetables on +tables, apple still lifes, wildflower bouquets, old shoes still life. + +### Self-portraits and portraits +Red-bearded man with intense blue eyes, bandaged-ear self-portrait, +postman in blue uniform, peasants in straw hats, old man weeping. + +### Interiors — bedroom, café, kitchen +The yellow bedroom, café terrace at night, billiard café interior, +weaver at his loom, peasant family eating potatoes, attic with skylight. + +### Orchards, gardens, blossom +Peach orchard in pink blossom, almond branch close-up, iris garden, +public park with autumn leaves, hospital garden with patients. + +### Boats, fishermen, sea +Fishing boats at Saintes-Maries, fishermen mending nets, beach +shellfish gatherers, sailing boats on calm sea. + +### Villages, churches, narrow streets +Small villages with red roofs, narrow village streets, country church +with graveyard, thatched cottages, village square with fountain. + +--- + +## Things to ALWAYS include + +1. **Open with "The image features..." / "The artwork features..." / + "The image depicts..."** — canonical training openers. +2. **A "The dominant color palette..." sentence** — every training + caption has one. The LoRA learned to bind style activation to this + exact phrase. +3. **A "The brushwork is visible..." sentence** — same as above. +4. **Close with `post-impressionist painting with visible brushstrokes.`** + verbatim. The trailing period is part of the training token sequence. + +## Things to NEVER do + +- ❌ Don't use poetic vocabulary ("impasto", "vermilion", "cobalt"). + LLaVA used plain English: "yellows", "blues", "earthy tones". +- ❌ Don't write one cinematic run-on sentence. The training register + is 5-7 short declarative sentences. +- ❌ Don't omit the style closer. +- ❌ Don't write the trigger word yourself. The backend prepends it. + +## Prompt length + +Max 1000 characters after auto-prepend. Van Gogh prompts run 500-700 +chars because the LLaVA register is verbose. That's the right length. + +## Two more verbatim training examples + +``` +The artwork features a portrait of a postman in a blue uniform with +brass buttons. He has a full beard and is seated against a wallpaper +of flowers. The dominant color palette is a mix of deep blues, yellows, +and warm browns. The brushwork is visible, adding texture and warmth +to the painting. post-impressionist painting with visible brushstrokes. +``` + +``` +The image features a tall cypress tree rising against a swirling night +sky. The sky is filled with thick yellow stars and curving blue clouds, +with a crescent moon visible. The cypress dominates the composition, +its green flame-like shape standing against the dark sky. The dominant +color palette consists of deep blues, yellows, and dark greens. The +brushwork is visible, giving the painting a textured and expressive +feel. post-impressionist painting with visible brushstrokes. +``` diff --git a/cc0company/erc1155-mint/SKILL.md b/cc0company/erc1155-mint/SKILL.md new file mode 100644 index 0000000000..c15d443a47 --- /dev/null +++ b/cc0company/erc1155-mint/SKILL.md @@ -0,0 +1,618 @@ +--- +name: cc0company-erc1155-mint +version: 1.0.0 +description: Deploy and run an ERC1155 NFT collection on cc0.company as an AI agent — open editions, limited editions, 1/1 auctions, allowlists, airdrops. On-chain storage via SSTORE2 chunks. Platform handles createTokenWithAttributes after agent pays ETH gas. +homepage: https://cc0.company +api_base: https://cc0.company/api +chain: base +chain_id: 8453 +factory: 0xB9585C09B6A78a16Bfb18D5b49D7F43431623065 +--- + +# cc0.company ERC1155 Mint — Skill for AI Agents + +Drop your own ERC1155 collection on Base. Same on-chain +infrastructure as the human NFT wizard (v9 factory at +`0xB9585C09B6A78a16Bfb18D5b49D7F43431623065`); same bytecode; same +event signatures. Every endpoint below is mirrored at +`/api/store/agents/me/...` with API-key auth + collection-ownership +checks. + +## Mental model — 3 levels of objects + +``` +┌─ Collection (= one ERC1155 contract on chain) ──────────────────┐ +│ │ +│ ┌─ Token (one tokenId inside the contract) ────────────────┐ │ +│ │ - Name, description, on-chain SVG/PNG artwork (SSTORE2) │ │ +│ │ - Edition type: open_edition | limited_edition | auction │ │ +│ │ - mint_price, max_supply, mint_start/end_time │ │ +│ │ │ │ +│ │ ┌─ Phase (mint window with rules) ──────────────────┐ │ │ +│ │ │ - phase_type: public | allowlist | token_gated │ │ │ +│ │ │ - start_time, end_time, max_per_wallet │ │ │ +│ │ │ - merkle_root (allowlist), gate_token (token-gated) │ │ +│ │ └────────────────────────────────────────────────────┘ │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ (one collection → many tokens → many phases per token) │ +└──────────────────────────────────────────────────────────────────┘ +``` + +A **collection** is one ERC1155 contract — deploy it ONCE. Inside it +you create **tokens** — every artwork drop is a new tokenId. +Optionally, gate each token's mint with **phases** (allowlist period +first, then public, with different per-wallet limits). + +## Three edition types — choose one per token + +| Type | `max_supply` | Use case | +|---|---|---| +| `open_edition` | `0` (= unlimited) | Time-bounded drop. Anyone can mint while open; closes at `mint_end_time`. Common for memes / free claims. | +| `limited_edition` | `N` (fixed) | Hard cap. Often paired with an allowlist phase. Common for curated drops. | +| `auction` | always `1` (1/1) | English auction with reserve price, bids stored on-chain, settle after duration. Common for high-value art. | + +The platform won't let you mix: pick one type per token. A collection +can contain a mix of all three types (e.g. one auction + 10 open +editions). + +## Who signs what + +| Step | Signer | Auth | Cost | +|---|---|---|---| +| Deploy contract (one-time) | Agent (your wallet) | own pk | ~$0.05 gas | +| Create token + upload artwork | **Backend** (platform `uploader` wallet) | agent pays ETH up-front | quoted dynamically (~$0.01–0.10 for typical artwork) | +| Start auction | Agent (`onlyCreator` modifier) | own pk | ~$0.01 gas | +| Settle auction | Anyone (permissionless on-chain) | open | ~$0.01 gas | +| Buy / mint as buyer | Buyer | own pk | mint_price + gas | +| Airdrop (mint to a list) | Backend | agent pays ETH up-front | quoted dynamically | +| Update metadata (pre-freeze) | Backend | agent | no gas (DB only) | +| Freeze metadata | Backend | agent | ~$0.01 gas | + +Anything the platform signs (token creation, airdrop, freeze, reveal) +is **gated on a verified ETH payment from the agent to the platform +wallet first**. The backend never touches the chain without +verifying on-chain that you've paid the gas estimate. + +## Prerequisites + +1. **A Base EVM wallet** with a few cents of ETH on Base for gas + transfers, and enough USDC for x402 image-gen calls if you're + generating the artwork too. Recommended: Coinbase CDP SDK or + Base MCP. See [`../README.md`](../README.md) for the full options + list. + +2. **An agent API key**. Auto-issued on your first paid x402 invoke + (e.g. buying a generation). Pass on every call as + `Authorization: Bearer ` or `X-Agent-API-Key: `. + +## The full lifecycle in 6 steps + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Create collection (DB row) │ +│ POST https://cc0.company/api/store/agents/me/collections │ +│ → returns { collection: { id, ... }, status: "draft" } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 2. Prepare deploy tx (backend builds the v9 factory calldata) │ +│ POST https://cc0.company/api/store/agents/me/ │ +│ collections/:id/prepare-deploy │ +│ → returns { transaction: { to, data, value, chainId }, ... } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 3. Sign + send the tx FROM YOUR WALLET │ +│ cdp.evm.sendTransaction({ network: "base", transaction }) │ +│ → returns { transactionHash: "0x..." } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 4. Confirm deployment (backend reads receipt, persists address) │ +│ POST https://cc0.company/api/store/agents/me/ │ +│ collections/:id/confirm-deploy │ +│ → returns { contract_address, collection: { ... status: "active" } } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 5. Create your first token │ +│ │ +│ Endpoint: POST /api/store/agents/me/collections/:id/ │ +│ tokens/create-and-upload │ +│ │ +│ 5a. Quote: POST with NO `payment_tx_hash` │ +│ → 402 { required_payment: { ethCostWei } } │ +│ │ +│ 5b. Send plain ETH transfer to platform wallet │ +│ (0xAabEc077428420333c45b6D84455d4EAE8Ee0625) │ +│ cdp.evm.sendTransaction({ to, value: ethCostWei }) │ +│ │ +│ 5c. POST WITH `payment_tx_hash` from step 5b │ +│ → backend verifies ETH on-chain, compresses artwork, │ +│ splits into SSTORE2 chunks, calls │ +│ createTokenWithAttributes via platform uploader wallet │ +│ → 201 { token_id, contract_address } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 6. (Optional) Configure phases, allowlists, airdrops, freeze │ +│ Sell the drop. Watch mints come in. │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Quick-start: open edition drop (the easiest path) + +5 minutes, no allowlist, anyone can mint while open. + +```bash +API_KEY="cc0_agent_..." # your saved API key +COLLECTION_NAME="my memes" +SYMBOL="MYMEME" +PLATFORM_WALLET="0xAabEc077428420333c45b6D84455d4EAE8Ee0625" + +# ── 1. Create the collection row ──────────────────────────────── +COL=$(curl -s -X POST https://cc0.company/api/store/agents/me/collections \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"$COLLECTION_NAME\", + \"symbol\": \"$SYMBOL\", + \"description\": \"$DESC\", + \"token_standard\": \"ERC1155\", + \"chain\": \"base\", + \"royalty_bps\": 500 + }") +COL_ID=$(echo "$COL" | jq -r '.collection.id') + +# ── 2. Prepare deploy ─────────────────────────────────────────── +PREP=$(curl -s -X POST https://cc0.company/api/store/agents/me/collections/$COL_ID/prepare-deploy \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"collection_id\":\"$COL_ID\"}") +TX_TO=$(echo "$PREP" | jq -r '.transaction.to') +TX_DATA=$(echo "$PREP" | jq -r '.transaction.data') + +# ── 3. Sign + send via your wallet ────────────────────────────── +# (CDP example — your runtime; see x402-payments/SKILL.md for variants) +TX_HASH=$(node -e " + import('@coinbase/cdp-sdk').then(async ({ CdpClient }) => { + const cdp = new CdpClient({ apiKeyId: process.env.CDP_API_KEY_ID, apiKeySecret: process.env.CDP_API_KEY_SECRET }) + const account = await cdp.evm.getOrCreateAccount({ name: 'my-agent' }) + const { transactionHash } = await account.sendTransaction({ + network: 'base', + transaction: { to: '$TX_TO', data: '$TX_DATA', value: 0n }, + }) + console.log(transactionHash) + }) +") + +# ── 4. Confirm deploy ──────────────────────────────────────────── +DEPLOYED=$(curl -s -X POST https://cc0.company/api/store/agents/me/collections/$COL_ID/confirm-deploy \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"tx_hash\":\"$TX_HASH\"}") +CONTRACT=$(echo "$DEPLOYED" | jq -r '.contract_address') +echo "Deployed at $CONTRACT" + +# ── 5a. Get the ETH cost quote for the artwork upload ─────────── +ARTWORK_B64=$(base64 -w0 < my-meme.png) +QUOTE=$(curl -s -X POST https://cc0.company/api/store/agents/me/collections/$COL_ID/tokens/create-and-upload \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"My first drop\", + \"description\": \"A meme for the ages\", + \"max_supply\": \"0\", + \"mint_price\": \"1000000000000000\", + \"edition_type\": \"open_edition\", + \"mint_end_time\": \"$(date -u -d '+7 days' +%Y-%m-%dT%H:%M:%SZ)\", + \"artwork_data\": \"data:image/png;base64,$ARTWORK_B64\" + }") +ETH_COST_WEI=$(echo "$QUOTE" | jq -r '.required_payment.ethCostWei') + +# ── 5b. Send the ETH transfer ─────────────────────────────────── +PAYMENT_TX=$(node -e " + import('@coinbase/cdp-sdk').then(async ({ CdpClient }) => { + const cdp = new CdpClient({ apiKeyId: process.env.CDP_API_KEY_ID, apiKeySecret: process.env.CDP_API_KEY_SECRET }) + const account = await cdp.evm.getOrCreateAccount({ name: 'my-agent' }) + const { transactionHash } = await account.sendTransaction({ + network: 'base', + transaction: { to: '$PLATFORM_WALLET', value: ${ETH_COST_WEI}n }, + }) + console.log(transactionHash) + }) +") + +# ── 5c. POST again with payment_tx_hash — backend creates token ─ +TOKEN=$(curl -s -X POST https://cc0.company/api/store/agents/me/collections/$COL_ID/tokens/create-and-upload \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"My first drop\", + \"description\": \"A meme for the ages\", + \"max_supply\": \"0\", + \"mint_price\": \"1000000000000000\", + \"edition_type\": \"open_edition\", + \"mint_end_time\": \"$(date -u -d '+7 days' +%Y-%m-%dT%H:%M:%SZ)\", + \"artwork_data\": \"data:image/png;base64,$ARTWORK_B64\", + \"payment_tx_hash\": \"$PAYMENT_TX\" + }") +echo "Token created: $(echo "$TOKEN" | jq '.')" +``` + +That's it. Your collection is live, the token is mintable for 1 +week at 0.001 ETH per copy, unlimited supply. + +### Token page URL — use `view_url` from the response + +The `create-and-upload` response surfaces these scalar fields at the +top level. Use them directly — don't reassemble paths by hand: + +```json +{ + "success": true, + "collection_id": "01KT3NQJPXG0NWBFCPQFCF8T3W", + "contract_address": "0x8067f1bf85a93CE792238874597B5bA29b03E644", + "token_id": "1", + "onChainTokenId": "1", + "txHash": "0x...", + "view_url": "https://cc0.company/mint/0x8067f1bf85a93CE792238874597B5bA29b03E644/1", + "legacy_view_url": "https://cc0.company/nft-collections/01KT3NQJPXG0NWBFCPQFCF8T3W/token/1", + "basescan_url": "https://basescan.org/tx/0x...", + "token": { ... full DB row ... } +} +``` + +The **canonical token page URL** is: + +``` +https://cc0.company/mint/{contract_address}/{token_id} +``` + +— same shape as most other NFT platforms (OpenSea, Zora, etc.), so it's +the address agents naturally guess. `{contract_address}` is the deployed +contract (you got it back from `confirm-deploy`) and `{token_id}` is +the on-chain token ID (a small integer, starting at `1`, returned in +`onChainTokenId`). Case-insensitive on the contract — both checksummed +and lowercased work. + +The legacy `https://cc0.company/nft-collections/{collection_id}/token/{token_id}` +shape still works for backwards compatibility, and is returned as +`legacy_view_url` for tooling that already indexed it. + +## Quick-start: limited edition with allowlist + +For curated drops where you want to give early access to a specific +wallet list before opening to the public. + +```bash +# After steps 1-5 above, with edition_type="limited_edition" and +# max_supply set to e.g. 100: + +# Create an allowlist phase +PHASE=$(curl -s -X POST https://cc0.company/api/store/agents/me/collections/$COL_ID/phases \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"phase_type\": \"allowlist\", + \"name\": \"Whitelist\", + \"start_time\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", + \"end_time\": \"$(date -u -d '+1 day' +%Y-%m-%dT%H:%M:%SZ)\", + \"mint_price\": \"500000000000000\", + \"max_per_wallet\": 2 + }") +PHASE_ID=$(echo "$PHASE" | jq -r '.phase.id') + +# Add wallets to the allowlist (Merkle root regenerates automatically) +curl -s -X POST https://cc0.company/api/store/agents/me/collections/$COL_ID/allowlist \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"phase_id\": \"$PHASE_ID\", + \"entries\": [ + { \"wallet_address\": \"0x111...\", \"max_mint_quantity\": 2 }, + { \"wallet_address\": \"0x222...\", \"max_mint_quantity\": 2 } + ] + }" + +# Activate the phase (toggle-able without redeploying) +curl -s -X POST https://cc0.company/api/store/agents/me/collections/$COL_ID/phases/$PHASE_ID/activate \ + -H "Authorization: Bearer $API_KEY" + +# Later — add a public phase that starts when the allowlist ends +curl -s -X POST https://cc0.company/api/store/agents/me/collections/$COL_ID/phases \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"phase_type\": \"public\", + \"name\": \"Public\", + \"start_time\": \"$(date -u -d '+1 day' +%Y-%m-%dT%H:%M:%SZ)\", + \"end_time\": \"$(date -u -d '+7 days' +%Y-%m-%dT%H:%M:%SZ)\", + \"mint_price\": \"1000000000000000\", + \"max_per_wallet\": 5 + }" +``` + +A buyer wallet on the allowlist fetches their Merkle proof via: + +```bash +curl -X POST https://cc0.company/api/store/agents/me/collections/$COL_ID/allowlist/proof \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"phase_id\":\"$PHASE_ID\",\"wallet_address\":\"0x111...\"}" +# → { "proof": ["0xabc...", "0xdef..."] } +``` + +…then passes that proof into the mint call. + +## Quick-start: 1/1 auction + +```bash +# Step 5 with edition_type="auction": +# "edition_type": "auction", +# "auction_duration": 48, // hours +# "auction_reserve_price": "10000000000000000" // 0.01 ETH + +# After the token is created, START the auction. This is a tx the +# AGENT signs (onlyCreator on the contract). Use the prepare/confirm +# pattern (same as deploy): +PREP=$(curl -s -X POST \ + https://cc0.company/api/store/agents/me/collections/$COL_ID/tokens/$TOKEN_ID/prepare-start-auction \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json") +# … sign + send tx via your wallet … +curl -s -X POST \ + https://cc0.company/api/store/agents/me/collections/$COL_ID/tokens/$TOKEN_ID/confirm-start-auction \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"tx_hash\":\"$START_TX\"}" + +# Poll auction state any time +curl -s https://cc0.company/api/store/agents/me/collections/$COL_ID/tokens/$TOKEN_ID/auction \ + -H "Authorization: Bearer $API_KEY" + +# After end_time has passed, settle (anyone can call but here you do it) +curl -s -X POST \ + https://cc0.company/api/store/agents/me/collections/$COL_ID/tokens/$TOKEN_ID/auction/settle \ + -H "Authorization: Bearer $API_KEY" +``` + +## Complete agent endpoint reference + +> **READ THIS BEFORE THE TABLES** — every Path in the tables below is +> RELATIVE. To get the full URL, prepend +> `https://cc0.company/api/store/agents/me`. So `/collections/:id/tokens/create-and-upload` +> in the table = `https://cc0.company/api/store/agents/me/collections/:id/tokens/create-and-upload` +> in a curl call. +> +> Auth: `Authorization: Bearer ` OR `X-Agent-API-Key: ` — +> both accepted on every endpoint below. +> +> If you're seeing 404 from one of these, check three things in order: +> (1) did you include the `/api/store/agents/me` prefix, (2) did you +> spell the path exactly (case-sensitive), (3) is your collection ID +> the cc0.company internal ID (e.g. `01KT3NQJPXG0NWBFCPQFCF8T3W`) and +> NOT the on-chain contract address. + +### Collection lifecycle + +| Method | Path | Purpose | +|---|---|---| +| `POST` | `/collections` | Create draft collection (DB row) | +| `GET` | `/collections/:id` | Read collection (via main agents API) | +| `POST` | `/collections/:id/prepare-deploy` | Build the deploy tx | +| `POST` | `/collections/:id/confirm-deploy` | Persist deployed contract address | +| `POST` | `/collections/:id/freeze` | Permanently freeze metadata | +| `POST` | `/collections/:id/reveal` | Reveal pre-revealed tokens | +| `GET/PUT/DELETE` | `/collections/:id/draft` | Server-side draft auto-save | +| `GET/PATCH/DELETE` | `/collections/:id/deployment-steps` | Resume an interrupted deploy wizard | + +### Phases (gating) + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/collections/:id/phases` | List phases | +| `POST` | `/collections/:id/phases` | Create phase (public / allowlist / token_gated / dutch_auction) | +| `GET` | `/collections/:id/phases/:phaseId` | Read phase | +| `PATCH` | `/collections/:id/phases/:phaseId` | Update phase config | +| `DELETE` | `/collections/:id/phases/:phaseId` | Delete phase | +| `POST` | `/collections/:id/phases/:phaseId/activate` | Toggle phase live | +| `DELETE` | `/collections/:id/phases/:phaseId/activate` | Toggle phase off | + +### Allowlist + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/collections/:id/allowlist?phase_id=` | List wallets in phase | +| `POST` | `/collections/:id/allowlist` | Bulk add wallets, regenerates Merkle root | +| `DELETE` | `/collections/:id/allowlist` | Remove wallets | +| `POST` | `/collections/:id/allowlist/proof` | Get Merkle proof for a wallet | + +### Tokens + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/collections/:id/tokens` | List tokens | +| `POST` | `/collections/:id/tokens` | Create token row (no artwork) | +| `GET` | `/collections/:id/tokens/:tokenId` | Read token | +| `PATCH` | `/collections/:id/tokens/:tokenId` | Update token (pre-upload only) | +| `DELETE` | `/collections/:id/tokens/:tokenId` | Delete token (pre-upload only) | +| `POST` | `/collections/:id/tokens/create-and-upload` | One-step: create + upload artwork | +| `GET` | `/collections/:id/tokens/:tokenId/upload` | Quote ETH cost for upload | +| `POST` | `/collections/:id/tokens/:tokenId/upload` | Upload artwork (with `payment_tx_hash`) | +| `POST` | `/collections/:id/tokens/:tokenId/artwork-chunk` | Append a single SSTORE2 chunk (for huge artwork) | + +### Auctions + +| Method | Path | Purpose | +|---|---|---| +| `POST` | `/collections/:id/tokens/:tokenId/prepare-start-auction` | Build the startAuction tx | +| `POST` | `/collections/:id/tokens/:tokenId/confirm-start-auction` | Persist startAuction tx hash | +| `GET` | `/collections/:id/tokens/:tokenId/auction` | Read live auction state | +| `POST` | `/collections/:id/tokens/:tokenId/auction/settle` | Settle finished auction | + +### Airdrops + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/collections/:id/airdrops` | List airdrop jobs | +| `POST` | `/collections/:id/airdrops` | Create airdrop (recipients[] + token_id) | +| `GET` | `/collections/:id/airdrops/:airdropId` | Read airdrop status | +| `PATCH` | `/collections/:id/airdrops/:airdropId` | Retry failed entries | + +### Metadata + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/collections/:id/metadata` | List all token metadata | +| `POST` | `/collections/:id/metadata` | Batch set metadata for multiple tokens | +| `GET` | `/collections/:id/metadata/:tokenId` | Read one token's metadata | +| `POST` | `/collections/:id/metadata/:tokenId` | Set one token's metadata | +| `PATCH` | `/collections/:id/metadata/:tokenId` | Partial update | + +### Stats + history + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/collections/:id/stats` | Aggregate stats (minted, holders, revenue) | +| `GET` | `/collections/:id/mints` | Paginated mint-event history | +| `GET` | `/token/:contractAddress/:tokenId` | Global token lookup (no collection ID needed) | + +### Buyer-side mint (agent buying NFTs from another creator) + +| Method | Path | Purpose | +|---|---|---| +| `POST` | `/mint/:collectionId/verify` | Eligibility check (allowlist, supply, window) | +| `POST` | `/mint/:collectionId` | Mint token (returns tx data to sign + send) | +| `POST` | `/mint/:collectionId/confirm` | Record mint tx hash for stats reconciliation | + +#### Allowlist mint signature (v12 — current) + +`mint()` reverts `WrongMintEntrypoint` when the active phase has a +non-zero `merkleRoot`. Use `mintWithProof` instead. The selector +changed in v12 — there is a SECOND `uint256` arg for the +per-leaf cap. + +``` +mintWithProof(uint256 tokenId, uint256 quantity, uint256 maxQuantity, bytes32[] proof) +``` + +Leaf format: `keccak256(abi.encodePacked(msg.sender, maxQuantity))`, +sorted-pair tree, OpenZeppelin convention. + +Per-wallet caps are encoded in the leaf, so different addresses on +the same allowlist phase can have different allowances. The phase's +global `maxPerAddress` is IGNORED on allowlist phases — `maxQuantity` +wins. Cumulative mints across multiple `mintWithProof` calls on the +same phase must stay ≤ `maxQuantity`. Passing a `maxQuantity` you +weren't added with → InvalidProof revert (the leaf hash mismatches). + +`maxQuantity == 0` is rejected with `InvalidInput` — a zero-cap +leaf would defeat the point of an allowlist. + +**Where to get the proof + cap**: the platform proof endpoint +returns both for any buyer. + +```bash +curl https://cc0.company/api/store/nft-minting/collections/{COL_ID}/allowlist/proof?address={BUYER} +# → { "proof": ["0x...", ...], "maxQuantity": 3 } +``` + +— or build it off-chain from the creator's published list with +OpenZeppelin's `StandardMerkleTree.of(entries, ["address", "uint256"])`. + +**Legacy v11 collections** (deployed before the v12 ref): signature +is the old 3-arg `mintWithProof(uint256, uint256, bytes32[])` with +address-only leaves `keccak256(abi.encodePacked(msg.sender))`. The +platform proof endpoint returns the appropriate shape based on the +collection's deploy block; agents that read `maxQuantity` from its +response are forward-compatible either way (v11 omits the field). + +### Chunked upload jobs (huge artwork) + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/upload` | List your upload jobs | +| `POST` | `/upload` | Create job (declares total chunks + SHA256) | +| `GET` | `/upload/:jobId` | Read job status | +| `DELETE` | `/upload/:jobId` | Cancel job | +| `POST` | `/upload/:jobId/batch` | Push a batch of chunks | +| `POST` | `/upload/:jobId/finalize` | Close job + create on-chain token | +| `POST` | `/upload/:jobId/hash-override` | Rewrite expected SHA256 mid-flight | + +## Common errors + +| Code | Cause | Fix | +|---|---|---| +| `401` | Missing or invalid API key | Send `Authorization: Bearer ` or `X-Agent-API-Key: ` | +| `403` | Collection belongs to another agent | Check `collection_id`; agents can only mutate their own | +| `400 contract_address null` | Calling token / freeze / etc before `confirm-deploy` succeeded | Finish deploy first | +| `402 payment required` | Missing `payment_tx_hash` (or invalid) | Send ETH to `0xAabEc077428420333c45b6D84455d4EAE8Ee0625`, retry with the resulting tx hash | +| `402 Payment already consumed` | Reusing a `payment_tx_hash` from a prior upload | Send a fresh ETH transfer for each new token / artwork | +| `409 Collection already deployed` | `prepare-deploy` after `confirm-deploy` already wrote the address | Check `collection.contract_address`; if set, skip the prepare/confirm cycle | +| `Generation refused — metadata frozen` | POST/PATCH to /metadata after /freeze | Freeze is irreversible; create a new collection if you need to update | + +## Anti-patterns + +- ❌ **Don't sign `createTokenWithAttributes` yourself.** The + platform's uploader wallet has the `onlyUploader` role on the + contract. You pay ETH gas; backend signs the actual contract + call. + +- ❌ **Don't hard-code the platform wallet address.** It's + `0xAabEc077428420333c45b6D84455d4EAE8Ee0625` today, but read it + from the quote response (`required_payment.payTo`) so future + rotations don't break your agent. + +- ❌ **Don't reuse a `payment_tx_hash` across tokens.** One ETH + transfer = one upload. The backend rejects double-spends with + `402 Payment already consumed`. + +- ❌ **Don't call `freeze` until you're SURE.** It's irreversible. + +- ❌ **Don't try to sign the deploy tx via Bankr's `/agent/submit` + on a default-config key.** Bankr's "Disable arbitrary contract + calls" toggle (default ON) blocks deploys. Either flip it off at + [bankr.bot/security](https://bankr.bot/security) or use CDP / + viem instead. See [`../x402-payments/SKILL.md`](../x402-payments/SKILL.md). + +## Related skills + +- [`../x402-payments/SKILL.md`](../x402-payments/SKILL.md) — every + paid endpoint here uses x402 v2 for ETH/USDC settlement; the + canonical signing patterns live there +- [`../agent-services/SKILL.md`](../agent-services/SKILL.md) — buy + AI-generated artwork from cc0.company's 5 LoRA models, then use + the output as the artwork for a token here + +## Background + +The on-chain stack: **CC0Collection1155 v12 bytecode** deployed via +the v9 factory at `0xB9585C09B6A78a16Bfb18D5b49D7F43431623065` (the +CC0CollectionFactory; bytecode-agnostic, no factory redeploy +needed for the v12 bump), with the **v12 shared renderer** at +`0xa94a19C76886e3809573b027bf7cfDA7788fe4dC` handling tokenURI +generation. SSTORE2 chunks hold the on-chain artwork (DEFLATE +compressed). Verified on Basescan via bytecode-similarity match +against the v12 reference at `0xb43B9A87ab88F00A01324E3865d8fc117be99dd6`. + +Pre-v12 collections still on chain use the legacy v11 renderer +`0x439C31A2ff9B6Df7C77D53C73E3726F786c2658C` + v11 reference +`0xB0EDA98DD5fD8b14777fdcC743bfFbA57a2aBBeF`. The platform's +allowlist proof endpoint returns `contract_version: "v11" | "v12"` +so agents picking between the 3-arg and 4-arg mintWithProof +signatures don't have to derive that themselves. + +Source: [github.com/cryptomfer/cc0-nft-contracts](https://github.com/cryptomfer/cc0-nft-contracts) (Solidity). + +## License + +This skill file is CC0. The on-chain contracts are MIT-licensed. +The artwork minted via this flow is whatever license you stamp on +the token — cc0.company doesn't enforce CC0 on third-party drops, +only on the platform's own collections. diff --git a/cc0company/erc721-shared-mint/SKILL.md b/cc0company/erc721-shared-mint/SKILL.md new file mode 100644 index 0000000000..8e1c07c99c --- /dev/null +++ b/cc0company/erc721-shared-mint/SKILL.md @@ -0,0 +1,793 @@ +--- +name: cc0company-erc721-shared-mint +version: 1.0.0 +description: Deploy a single-artwork ERC721 collection on cc0.company as an AI agent — one shared image, fixed max supply, multi-phase mint with merkle-allowlist + public windows. Single-payment orchestrator handles deploy + on-chain artwork commit in one server-side flow. +homepage: https://cc0.company +api_base: https://cc0.company/api +chain: base +chain_id: 8453 +factory: 0xB9585C09B6A78a16Bfb18D5b49D7F43431623065 +reference_deploy: 0x5112A2Db56dA0E5c96fECAf5e11a3F4E6135c9B4 +--- + +# cc0.company ERC721Shared — Skill for AI Agents + +Drop a classic NFT collection on Base — one artwork shared by every +minted token, fixed max supply, optional allowlist phases. The +agent-friendly counterpart to the human ERC721 Generative wizard: +**no layers, no DNA, no traits to design** — just an image, a price, +and a phase schedule. + +> **Pick the right standard:** +> +> - **ERC721Shared** (this skill) → one image, edition-style drop with +> allowlist + public phases, fixed max supply. Each token is unique +> by `tokenId` but renders the same artwork. Pick this for memes, +> profile-pic drops, single-asset open or limited editions. +> - **ERC1155** (`../erc1155-mint`) → multiple distinct artworks under +> one contract, mixed edition types (open / limited / 1-of-1 +> auction) per token. Pick this if you want auctions or want to +> keep multiple drops under one contract. +> - **CC0Store** (ERC721A) → physical product receipts. Tokens are +> proof-of-purchase, not the artwork. + +## Mental model + +``` +┌─ Collection (= one CC0CollectionShared v3 contract on chain) ──┐ +│ │ +│ - name, symbol, description (constructor args) │ +│ - maxSupply (hard cap, baked at deploy) │ +│ - publicMintPrice + paymentToken (ETH or any ERC20) │ +│ - initialMerkleRoot (allowlist, baked at deploy) │ +│ - SHARED ARTWORK stored fully on-chain via SSTORE2 + DEFLATE │ +│ │ +│ Mint surface: │ +│ mint(quantity, allowlistProof, allowlistLimit) │ +│ ┌─ public mint → empty proof, limit=0 │ +│ └─ allowlist mint → proof from the tree, limit from leaf │ +└─────────────────────────────────────────────────────────────────┘ +``` + +Each token in the collection is a unique ERC-721 token, but +`tokenURI(tokenId)` resolves to the **same shared image** for every +ID. Holders get a "#1/100" feel without you having to design 100 +distinct images. + +## Why "single-payment" + +The human wizard makes the creator sign **one ETH transfer**, then +the backend orchestrates the entire deploy + on-chain artwork commit +chain server-side. This skill exposes the same orchestrator to +agents via the standard `402 Payment Required` challenge — your +agent gets a price quote up-front, sends ETH, retries, done. + +Old-school flow (still works for ERC1155, fine for agents that +prefer to sign every step) is **2 wallet signatures**: deploy tx, +then upload tx. Single-payment is **1 wallet signature**: ETH +transfer, the backend uses it to drive everything else. + +## Who signs what + +| Step | Signer | Auth | Cost | +|---|---|---|---| +| Create collection draft (DB row) | Backend | API key | free | +| Upload artwork chunks (transit buffer) | Backend | API key | free | +| Get the deploy quote | Backend | API key | free | +| **Pay** the quoted ETH amount | **Agent** (your wallet) | own pk | quoted dynamically (~$0.05–$0.50 depending on artwork size) | +| `factory.deployCollectionShared(...)` | Backend (with creator=YOU) | API key + payment | already paid in step 4 | +| `addArtworkChunk(...)` × N | Backend (uploader role) | API key + payment | already paid | +| `finalizeArtwork()` | Backend (uploader role) | API key + payment | already paid | +| Mint as buyer | Buyer | own pk | `mintPrice × qty` + gas | +| Rotate merkle root post-deploy | Agent (owner) | own pk | ~$0.01 gas | + +**Important: you are the contract owner.** The factory takes +`creator` as an arg and transfers ownership to it in the constructor, +so the resulting contract is owned by your agent's wallet +**regardless of who paid gas to call the factory**. The platform's +backend wallet only gets the `uploader` role, which is scoped to +`addArtworkChunk` + `finalizeArtwork` and **useless after the seal**. + +## Prerequisites + +1. **A Base EVM wallet** with a few cents of ETH on Base for the + single deploy payment. Recommended: Coinbase CDP SDK or Base + MCP. See [`../README.md`](../README.md) for the full options + list. + +2. **An agent API key**. Auto-issued on your first paid x402 invoke + (e.g. buying an image generation). Pass on every call as + `Authorization: Bearer ` or `X-Agent-API-Key: `. + +## The full lifecycle in 5 steps + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. Create collection draft (DB row) │ +│ POST /api/store/agents/me/collections │ +│ body: { token_standard: "ERC721Shared", name, symbol, ... } │ +│ → returns { collection: { id, ... }, status: "draft" } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 2. Stream artwork chunks into transit buffer │ +│ POST /api/store/agents/me/collections/:id/artwork-chunk │ +│ Repeat with chunk_index 0…N-1 │ +│ Final chunk response → { complete: true } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 3. Get the deploy quote (402 challenge) │ +│ POST /api/store/agents/me/collections/:id/ │ +│ orchestrate-shared-deploy │ +│ body: { deploy_params: { ... } } ← no payment_tx_hash │ +│ → 402 { ethCostWei, platformWallet, chunkCount, ... } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 4. Send ETH to platformWallet │ +│ cdp.evm.sendTransaction({ │ +│ network: "base", │ +│ transaction: { to: platformWallet, value: ethCostWei } │ +│ }) │ +│ → returns { transactionHash: "0x..." } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 5. Retry the orchestrator with payment_tx_hash │ +│ POST /api/store/agents/me/collections/:id/ │ +│ orchestrate-shared-deploy │ +│ body: { payment_tx_hash, deploy_params: { ... } } │ +│ → { contract_address, deploy_tx_hash, chunk_tx_hashes, ... } │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ + ✅ Collection is LIVE + Buyers call collection.mint() directly +``` + +--- + +## Step 1: Create the collection draft + +```bash +curl -X POST https://cc0.company/api/store/agents/me/collections \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -d '{ + "name": "Cosmic Dreams", + "symbol": "COSMIC", + "description": "100 editions of a hand-drawn cosmic scene.", + "token_standard": "ERC721Shared", + "chain": "base", + "max_supply": 100, + "mint_price": "1000000000000000", + "payment_token": "0x0000000000000000000000000000000000000000", + "royalty_bps": 500 + }' +``` + +**Field notes:** + +| Field | Format | Notes | +|---|---|---| +| `token_standard` | string | Must be `"ERC721Shared"` — separates this from ERC1155 + Generative | +| `mint_price` | string (wei or token base units) | Public mint price PER TOKEN. `"1000000000000000"` = 0.001 ETH | +| `payment_token` | hex address | `0x000…000` = ETH. Any ERC20 = token-priced (USDC on Base: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) | +| `max_supply` | uint | Hard cap. `0` = unlimited (rarely a good idea for a Shared drop). | +| `royalty_bps` | uint96 | Secondary-market royalty in basis points. `500` = 5%. Recipient defaults to your agent wallet. | + +Response: + +```json +{ + "success": true, + "collection": { + "id": "col_xxx", + "status": "draft", + "token_standard": "ERC721Shared", + "name": "Cosmic Dreams", + "contract_address": null + } +} +``` + +**Save the `collection.id`** for every subsequent step. + +## Step 2: Upload the shared artwork (chunked) + +The artwork lives **fully on-chain** (no IPFS, no broken links). You +stream it to the backend's in-memory transit buffer in ≤ 50 KB +chunks, then the orchestrator compresses + commits it via SSTORE2 in +Step 5. + +**Chunk shape:** +- Encode your artwork as a base64 data URL: + `data:image/png;base64,iVBORw0KGgo...` + (also valid: `image/gif`, `image/svg+xml`, `image/jpeg`) +- Slice the string into chunks of ≤ 50,000 characters. +- POST each chunk **in order**. `chunk_index=0` initializes the + buffer; the buffer auto-completes when all slots are filled. + +```typescript +import { readFile } from "node:fs/promises" + +const COLLECTION_ID = "col_xxx" +const API_KEY = process.env.CC0_AGENT_API_KEY! + +// 1. Read your artwork file and encode it. +const fileBytes = await readFile("./cosmic-dreams.png") +const dataUrl = `data:image/png;base64,${fileBytes.toString("base64")}` + +// 2. Slice into ≤ 50KB chunks. +const CHUNK_SIZE = 50_000 +const chunks: string[] = [] +for (let i = 0; i < dataUrl.length; i += CHUNK_SIZE) { + chunks.push(dataUrl.slice(i, i + CHUNK_SIZE)) +} + +// 3. POST each one. +for (let i = 0; i < chunks.length; i++) { + const res = await fetch( + `https://cc0.company/api/store/agents/me/collections/${COLLECTION_ID}/artwork-chunk`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + chunk_index: i, + total_chunks: chunks.length, + data: chunks[i], + }), + }, + ) + const json = await res.json() + console.log( + `chunk ${i + 1}/${chunks.length}`, + json.complete ? "→ buffer complete" : `(${json.received}/${json.total})`, + ) +} +``` + +**Final chunk response:** + +```json +{ + "success": true, + "complete": true, + "received": 12, + "total": 12, + "collection": { "id": "col_xxx", ... } +} +``` + +**Caveats:** +- The buffer is **in-memory only** with a **30-minute idle TTL**. If + you wait too long between chunks (or between Step 2 and Step 3), + the buffer is dropped and you start over. +- Max practical artwork size: ~5 MB (compressed on-chain). Bigger + artworks work but the deploy cost grows linearly with byte count. +- Chunks for a single collection MUST be POSTed serially — the + in-memory buffer is not concurrency-safe. + +## Step 3: Get the deploy quote (402 challenge) + +POST the orchestrator **without** `payment_tx_hash`. The backend +computes the deploy + upload + finalize cost from the chunk count + +size and replies with the price in WEI. + +```bash +curl -X POST https://cc0.company/api/store/agents/me/collections/col_xxx/orchestrate-shared-deploy \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -d '{ + "deploy_params": { + "name": "Cosmic Dreams", + "symbol": "COSMIC", + "description": "100 editions of a hand-drawn cosmic scene.", + "maxSupply": "100", + "mintSettings": { + "publicMintPrice": "1000000000000000", + "paymentToken": "0x0000000000000000000000000000000000000000", + "mintStart": 1733000000, + "mintEnd": 1735000000, + "maxPerAddress": "5" + }, + "withdrawRecipients": [ + { "recipient": "0xYourAgentWallet", "percentage": 10000 } + ], + "royaltyRecipient": "0xYourAgentWallet", + "royaltyBps": 500, + "owner": "0xYourAgentWallet", + "initialMerkleRoot": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + }' +``` + +**402 response:** + +```json +{ + "success": false, + "error": "Payment Required", + "message": "Send a plain ETH transfer of ethCostWei to platformWallet, then POST again with payment_tx_hash.", + "ethCostWei": "4521000000000000", + "requiredMinPaymentWei": "4068900000000000", + "platformWallet": "0xPlatformBackendWallet", + "chunkCount": 11, + "uploadBytes": 145600, + "useCompression": true +} +``` + +### `deploy_params` field reference + +| Field | Type | Notes | +|---|---|---| +| `name`, `symbol`, `description` | string | Must match the draft from Step 1. Stored in contract storage. | +| `maxSupply` | string (uint256) | Hard cap. `"0"` = unlimited. | +| `mintSettings.publicMintPrice` | string (wei or token base units) | Price PER MINT. Per-phase overrides come post-deploy via `setMintSettings`. | +| `mintSettings.paymentToken` | address | `0x000…000` = ETH. Any ERC20 = token-priced. | +| `mintSettings.mintStart` / `mintEnd` | unix seconds | Public mint window. Allowlist phases are set post-deploy via `setMerkleRoot` + `setMintSettings`. | +| `mintSettings.maxPerAddress` | string (uint256) | Per-wallet cap. `"0"` = unlimited. | +| `withdrawRecipients` | array | Splits payouts. Percentages are basis points (10000 = 100%), must sum to 10000. | +| `royaltyRecipient` / `royaltyBps` | address / uint96 | Secondary-market royalties (ERC-2981). `500` = 5%. | +| `owner` | address | Contract owner — your agent's wallet, NOT the platform. | +| `initialMerkleRoot` | bytes32 | Allowlist root baked at deploy. Use `0x000…000` if you don't have an allowlist; you can set one later. | + +### Building the allowlist merkle root (optional) + +If you want allowlist phases sealed at block 0, build a sorted-pair +merkle tree with `keccak256(abi.encodePacked(address, uint256))` +leaves — OpenZeppelin convention. Mirror it exactly so the on-chain +`_verifyAllowlistProof` accepts your proofs. + +```typescript +import { StandardMerkleTree } from "@openzeppelin/merkle-tree" + +const entries: [string, bigint][] = [ + ["0xAlice", 3n], // [address, mint limit] + ["0xBob", 1n], + ["0xCarol", 5n], +] + +const tree = StandardMerkleTree.of(entries, ["address", "uint256"]) + +const initialMerkleRoot = tree.root // bytes32 hex string + +// Later, when each holder mints, get their per-leaf proof: +const aliceProof = tree.getProof([0]) // index of Alice in `entries` +// → ["0x123…", "0x456…"] // pass to collection.mint(qty, proof, limit) +``` + +For lighter dependencies, you can hand-implement the sorted-pair +tree — see [`./examples/build-merkle.ts`](./examples/build-merkle.ts). + +## Step 4: Send ETH + finalize the deploy + +Wire the quoted `ethCostWei` to `platformWallet` (plain ETH transfer, +**no calldata**), then POST the orchestrator a second time with the +resulting `payment_tx_hash`. + +**Via CDP SDK (Coinbase Agentic Wallet):** + +```typescript +import { CdpClient } from "@coinbase/cdp-sdk" + +const cdp = new CdpClient({ + apiKeyId: process.env.CDP_API_KEY_ID!, + apiKeySecret: process.env.CDP_API_KEY_SECRET!, +}) +const account = await cdp.evm.getOrCreateAccount({ name: "my-agent" }) + +const { transactionHash: paymentTxHash } = await account.sendTransaction({ + network: "base", + transaction: { + to: "0xPlatformBackendWallet" as `0x${string}`, + value: 4521000000000000n, // ethCostWei from the 402 + data: "0x", + }, +}) +``` + +**Via viem (raw private key):** + +```typescript +import { createWalletClient, http } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import { base } from "viem/chains" + +const account = privateKeyToAccount(process.env.AGENT_PK! as `0x${string}`) +const client = createWalletClient({ account, chain: base, transport: http() }) + +const paymentTxHash = await client.sendTransaction({ + to: "0xPlatformBackendWallet" as `0x${string}`, + value: 4521000000000000n, +}) +``` + +Then retry the orchestrator with the same `deploy_params` plus the +`payment_tx_hash`: + +```bash +curl -X POST https://cc0.company/api/store/agents/me/collections/col_xxx/orchestrate-shared-deploy \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -d '{ + "payment_tx_hash": "0xYourEthTransferHash", + "deploy_params": { ... same as Step 3 ... } + }' +``` + +## Step 5: Success — collection is live + +```json +{ + "success": true, + "contract_address": "0xYourDeployedCollection", + "deploy_tx_hash": "0x...", + "chunk_tx_hashes": ["0x...", "0x...", "0x..."], + "finalize_tx_hash": "0x...", + "collection": { + "id": "col_xxx", + "status": "active", + "contract_address": "0x...", + "token_standard": "ERC721Shared", + "shared_artwork_onchain": true, + "shared_artwork_payment_tx_hash": "0xYourEthTransferHash" + } +} +``` + +**What the orchestrator did for you:** +1. Verified the ETH payment covers the quoted cost minus 10% slippage. +2. Called `factory.deployCollectionShared(...)` — you are the + contract owner, the platform is the uploader. +3. Looped `collection.addArtworkChunk(...)` over every chunk. +4. Called `collection.finalizeArtwork(...)` — the art is now + permanently sealed and `mint()` is unblocked. +5. Cleared the in-memory buffer. + +**Idempotency:** each `payment_tx_hash` can authorize **exactly one +deploy**. If you retry the orchestrator with the same hash against a +different collection, it returns `402 "Payment already consumed"`. +Same for collections that already have a `contract_address`. + +--- + +## Buyer mint flow (no x402 needed) + +ERC721Shared accepts ETH or any ERC20 directly via the contract's +`mint(quantity, allowlistProof, allowlistLimit)` function. Buyers +(or AI agents acting as buyers) call it via viem like any standard +NFT contract — no x402 relay, no platform intermediary. + +### Public mint (no allowlist) + +```typescript +import { createWalletClient, http } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import { base } from "viem/chains" + +const COLLECTION = "0xYourDeployedCollection" as `0x${string}` +const QUANTITY = 1n +const PUBLIC_MINT_PRICE = 1000000000000000n // 0.001 ETH per token + +const buyer = privateKeyToAccount(BUYER_PK as `0x${string}`) +const client = createWalletClient({ account: buyer, chain: base, transport: http() }) + +const mintTxHash = await client.writeContract({ + address: COLLECTION, + abi: [ + { + type: "function", + name: "mint", + inputs: [ + { name: "quantity", type: "uint256" }, + { name: "allowlistProof", type: "bytes32[]" }, + { name: "allowlistLimit", type: "uint256" }, + ], + outputs: [], + stateMutability: "payable", + }, + ], + functionName: "mint", + args: [ + QUANTITY, + [] as `0x${string}`[], // no allowlist proof for public mint + 0n, // no allowlist limit + ], + value: PUBLIC_MINT_PRICE * QUANTITY, +}) +``` + +### Allowlist mint (with merkle proof) + +```typescript +const PROOF = [ + "0x123...", // sibling hashes from the allowlist tree + "0x456...", +] as `0x${string}`[] +const ALLOWLIST_LIMIT = 3n // the limit the buyer's leaf was hashed with + +const mintTxHash = await client.writeContract({ + address: COLLECTION, + abi: SHARED_MINT_ABI, // same as above + functionName: "mint", + args: [QUANTITY, PROOF, ALLOWLIST_LIMIT], + value: PUBLIC_MINT_PRICE * QUANTITY, +}) +``` + +**Where to find a buyer's proof:** there's a public proof endpoint +at `GET /api/store/nft-minting/collections/{id}/allowlist/proof?address={buyer}` +that returns the merkle proof + per-leaf limit for that buyer. +Mirror with agent auth at `/api/store/agents/me/collections/{id}/allowlist/proof` +(catch-all proxy). + +### USDC-priced mint (paymentToken ≠ address(0)) + +```typescript +// 1. Approve the collection to spend USDC +await client.writeContract({ + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base + abi: ERC20_ABI, + functionName: "approve", + args: [COLLECTION, PUBLIC_MINT_PRICE * QUANTITY], +}) + +// 2. Mint without value — the contract pulls USDC from msg.sender +await client.writeContract({ + address: COLLECTION, + abi: SHARED_MINT_ABI, + functionName: "mint", + args: [QUANTITY, [], 0n], + // no `value` — USDC pulled via transferFrom +}) +``` + +--- + +## Post-deploy management + +| Action | Method | Path | +|---|---|---| +| List the holders | GET | `/api/store/agents/me/collections/{id}/mints` | +| Add airdrop recipients | POST | `/api/store/agents/me/collections/{id}/airdrops` | +| Update allowlist merkle root | POST | `/api/store/agents/me/collections/{id}/allowlist` | +| Read collection stats | GET | `/api/store/agents/me/collections/{id}/stats` | +| Get deployment + chunk tx hashes | GET | `/api/store/agents/me/collections/{id}/deployment-steps` | + +All of these proxy to the same routes humans use from the dashboard. +Authentication: Bearer agent API key. + +### Rotating the merkle root post-deploy + +If you didn't bake an allowlist into `initialMerkleRoot` at deploy, +or you want to switch to a different list later, the owner can call +`setMerkleRoot(bytes32 newRoot)` directly on the contract: + +```typescript +const newTree = StandardMerkleTree.of(newEntries, ["address", "uint256"]) + +await ownerClient.writeContract({ + address: COLLECTION, + abi: [ + { + type: "function", + name: "setMerkleRoot", + inputs: [{ name: "newRoot", type: "bytes32" }], + outputs: [], + stateMutability: "nonpayable", + }, + ], + functionName: "setMerkleRoot", + args: [newTree.root as `0x${string}`], +}) +``` + +### Rotating the public mint window post-deploy + +Same pattern — `setMintSettings(...)` is owner-only on the contract. +Used to start a new phase (e.g. after an allowlist window, open the +public window with a higher price). + +--- + +## Read functions you'll need + +| Function | Returns | Use for | +|---|---|---| +| `mintSettings()` | `(publicMintPrice, paymentToken, mintStart, mintEnd, maxPerAddress)` | Confirm the current mint config | +| `totalSupply()` | `uint256` | Show "X / maxSupply minted" | +| `maxSupply()` | `uint256` | Hard cap baked at deploy | +| `merkleRoot()` | `bytes32` | Check whether an allowlist is currently active (`0x000…000` = none) | +| `tokenURI(uint256)` | `string` | On-chain JSON metadata, image is a data URL pointing at the SSTORE2 artwork | +| `artworkFinalized()` | `bool` | True after the orchestrator's finalize. Mint is blocked until this is true. | +| `owner()` | `address` | Should be your agent's wallet | + +## Events to watch + +| Event | When | Notes | +|---|---|---| +| `Transfer(from, to, tokenId)` | Every mint, every transfer | `from == address(0)` = mint | +| `MintSettingsUpdated(...)` | `setMintSettings()` calls | Phase rotation, price changes | +| `ArtworkFinalized()` | Once, by orchestrator | Mint is unblocked from this block | + +--- + +## Complete end-to-end example + +A self-contained TypeScript script that takes a local image and +walks through all 5 steps. Drop it in [`./examples/full-deploy.ts`](./examples/full-deploy.ts) +to run. + +```typescript +import { readFile } from "node:fs/promises" +import { CdpClient } from "@coinbase/cdp-sdk" + +const API = "https://cc0.company/api" +const API_KEY = process.env.CC0_AGENT_API_KEY! +const headers = { + "Content-Type": "application/json", + "Authorization": `Bearer ${API_KEY}`, +} + +async function main() { + // 1. Create draft. + const draft = await fetch(`${API}/store/agents/me/collections`, { + method: "POST", + headers, + body: JSON.stringify({ + name: "Cosmic Dreams", + symbol: "COSMIC", + description: "100 editions of a hand-drawn cosmic scene.", + token_standard: "ERC721Shared", + chain: "base", + max_supply: 100, + mint_price: "1000000000000000", + payment_token: "0x0000000000000000000000000000000000000000", + royalty_bps: 500, + }), + }).then((r) => r.json()) + const collectionId = draft.collection.id + console.log("draft created:", collectionId) + + // 2. Upload artwork chunks. + const bytes = await readFile("./cosmic-dreams.png") + const dataUrl = `data:image/png;base64,${bytes.toString("base64")}` + const CHUNK = 50_000 + const chunks = [] + for (let i = 0; i < dataUrl.length; i += CHUNK) { + chunks.push(dataUrl.slice(i, i + CHUNK)) + } + for (let i = 0; i < chunks.length; i++) { + await fetch( + `${API}/store/agents/me/collections/${collectionId}/artwork-chunk`, + { + method: "POST", + headers, + body: JSON.stringify({ + chunk_index: i, + total_chunks: chunks.length, + data: chunks[i], + }), + }, + ) + } + console.log(`uploaded ${chunks.length} chunks`) + + // 3. + 4. Quote → pay → retry. + const cdp = new CdpClient({ + apiKeyId: process.env.CDP_API_KEY_ID!, + apiKeySecret: process.env.CDP_API_KEY_SECRET!, + }) + const account = await cdp.evm.getOrCreateAccount({ name: "my-agent" }) + + const now = Math.floor(Date.now() / 1000) + const deployParams = { + name: "Cosmic Dreams", + symbol: "COSMIC", + description: "100 editions of a hand-drawn cosmic scene.", + maxSupply: "100", + mintSettings: { + publicMintPrice: "1000000000000000", + paymentToken: "0x0000000000000000000000000000000000000000", + mintStart: now, + mintEnd: now + 60 * 60 * 24 * 30, + maxPerAddress: "5", + }, + withdrawRecipients: [ + { recipient: account.address, percentage: 10000 }, + ], + royaltyRecipient: account.address, + royaltyBps: 500, + owner: account.address, + initialMerkleRoot: + "0x0000000000000000000000000000000000000000000000000000000000000000", + } + + const quote = await fetch( + `${API}/store/agents/me/collections/${collectionId}/orchestrate-shared-deploy`, + { method: "POST", headers, body: JSON.stringify({ deploy_params: deployParams }) }, + ).then((r) => r.json()) + console.log("quote:", quote) + + const { transactionHash: paymentTxHash } = await account.sendTransaction({ + network: "base", + transaction: { + to: quote.platformWallet, + value: BigInt(quote.ethCostWei), + data: "0x", + }, + }) + console.log("paid:", paymentTxHash) + + const deployed = await fetch( + `${API}/store/agents/me/collections/${collectionId}/orchestrate-shared-deploy`, + { + method: "POST", + headers, + body: JSON.stringify({ + payment_tx_hash: paymentTxHash, + deploy_params: deployParams, + }), + }, + ).then((r) => r.json()) + + console.log("LIVE:", deployed.contract_address) + console.log("OpenSea:", `https://opensea.io/assets/base/${deployed.contract_address}/1`) +} + +main().catch(console.error) +``` + +--- + +## Error matrix + +| Status | Error | Cause | Fix | +|---|---|---|---| +| 401 | "Invalid API key." | Bearer / X-Agent-API-Key missing or wrong | Auto-issued on first paid x402 invoke; check `agent.api_key` from that response | +| 403 | "This collection does not belong to your agent profile." | `collection.profile_id !== agent.profile_id` | Use a collection you created via Step 1 with the SAME API key | +| 400 | "Artwork chunks not yet complete. POST chunks to /artwork-chunk first" | You skipped Step 2 OR the 30-min idle TTL dropped your buffer | Re-run Step 2 immediately before Step 3 | +| 400 | "Collection already deployed." | `collection.contract_address` is set | Each draft can only be deployed once. Create a new draft. | +| 402 | "Payment Required" + `ethCostWei` | First POST without `payment_tx_hash` (intended) | Pay and retry per Step 4 | +| 402 | "Payment verification failed" | Tx hash doesn't exist on chain yet, or value is too low | Wait for inclusion; bump value if rejected for insufficient amount | +| 402 | "Payment already consumed" | Same `payment_tx_hash` was used for another collection | Send a fresh ETH payment for the new deploy | +| 503 | "Price oracle unavailable" | Price feed RPC blip | Retry in 10s | + +--- + +## Platform addresses (Base mainnet) + +| Address | Purpose | +|---|---| +| `0xB9585C09B6A78a16Bfb18D5b49D7F43431623065` | CC0 Collection Factory v9 — deploys CC0Store, CC0Collection1155, AND CC0CollectionShared | +| `0x5112A2Db56dA0E5c96fECAf5e11a3F4E6135c9B4` | CC0CollectionShared v3 reference deploy — Basescan "Similar Match" verification anchor | +| `0x2906bff63e65e95bd05442a995b0e151febbad67` | Inflater (DEFLATE decompression) — read by every Shared collection's tokenURI | +| `0x151a3443eC023dB682419C9e2d8004C75c6584c0` | Platform fee recipient — receives the on-chain 5% mint cut | +| `0xAabEc077428420333c45b6D84455d4EAE8Ee0625` | Platform wallet — receives ETH deploy payments, acts as uploader | +| `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | USDC on Base | + +## Platform fee + +5% platform fee on all mints, distributed to cc0.company treasury. +Baked into the contract — you can't disable it. Royalty (your +`royaltyBps`) is separate and goes to your `royaltyRecipient` on +secondary-market sales. + +## See also + +- [`../erc1155-mint/SKILL.md`](../erc1155-mint/SKILL.md) — multi-token + art drops with auctions +- [`../x402-payments/SKILL.md`](../x402-payments/SKILL.md) — paying + x402 endpoints (e.g. for AI image generation) +- [`https://cc0.company/skill.md`](https://cc0.company/skill.md) — + flat HTTP-level API reference diff --git a/cc0company/erc721-shared-mint/examples/build-merkle.ts b/cc0company/erc721-shared-mint/examples/build-merkle.ts new file mode 100644 index 0000000000..97bdbbff32 --- /dev/null +++ b/cc0company/erc721-shared-mint/examples/build-merkle.ts @@ -0,0 +1,151 @@ +/** + * build-merkle.ts — hand-rolled sorted-pair merkle tree compatible + * with cc0.company's CC0CollectionShared `_verifyAllowlistProof`. + * + * Use this if you don't want to pull `@openzeppelin/merkle-tree`. + * The convention is the same: + * + * - leaf = keccak256(abi.encodePacked(address, uint256)) + * - intermediate nodes hash the SORTED pair (lo, hi) of children + * - root is the final hash + * + * The on-chain verifier walks the proof the same way: for each + * sibling, it sorts (current, sibling) and hashes — so any proof + * produced here is accepted by the contract as-is. + * + * Run with: `tsx examples/build-merkle.ts` + * Requires: viem (for keccak256 + encodePacked). + */ + +import { keccak256, encodePacked, type Hex } from "viem" + +export interface AllowlistEntry { + address: `0x${string}` + limit: bigint +} + +function leafHash(entry: AllowlistEntry): Hex { + return keccak256( + encodePacked(["address", "uint256"], [entry.address, entry.limit]), + ) +} + +function pairHash(a: Hex, b: Hex): Hex { + // Sorted-pair: smaller hash first, so verification is direction- + // independent (the verifier doesn't need to know "left or right"). + const [lo, hi] = a < b ? [a, b] : [b, a] + return keccak256(`${lo}${hi.slice(2)}` as Hex) +} + +export interface MerkleTree { + root: Hex + leaves: Hex[] + layers: Hex[][] +} + +export function buildMerkleTree(entries: AllowlistEntry[]): MerkleTree { + if (entries.length === 0) { + // Empty tree → root = bytes32(0). Matches EMPTY_MERKLE_ROOT + // semantics used by the frontend. + return { + root: ("0x" + "00".repeat(32)) as Hex, + leaves: [], + layers: [[]], + } + } + + // Dedup by lowercased address (last write wins on limit) — must + // match what computeMerkleRoot() does on the frontend. + const dedup = new Map() + for (const e of entries) { + dedup.set(e.address.toLowerCase(), e) + } + const normalized = Array.from(dedup.values()).map((e) => ({ + address: e.address.toLowerCase() as `0x${string}`, + limit: e.limit, + })) + + const leaves = normalized.map(leafHash).sort() + const layers: Hex[][] = [leaves] + + let layer = leaves + while (layer.length > 1) { + const next: Hex[] = [] + for (let i = 0; i < layer.length; i += 2) { + if (i + 1 === layer.length) { + // Odd node out — promote unchanged. (Matches OpenZeppelin.) + next.push(layer[i]) + } else { + next.push(pairHash(layer[i], layer[i + 1])) + } + } + layers.push(next) + layer = next + } + + return { root: layer[0], leaves, layers } +} + +export function getProof( + tree: MerkleTree, + entry: AllowlistEntry, +): Hex[] { + const target = leafHash({ + address: entry.address.toLowerCase() as `0x${string}`, + limit: entry.limit, + }) + let idx = tree.leaves.indexOf(target) + if (idx < 0) { + throw new Error( + `Entry ${entry.address}/${entry.limit} not in this tree. Did you dedup before passing it in?`, + ) + } + + const proof: Hex[] = [] + for (let l = 0; l < tree.layers.length - 1; l++) { + const layer = tree.layers[l] + const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1 + if (siblingIdx < layer.length) { + proof.push(layer[siblingIdx]) + } + idx = Math.floor(idx / 2) + } + return proof +} + +// ─── Demo ────────────────────────────────────────────────────────── + +if (import.meta.url === `file://${process.argv[1]}`) { + const entries: AllowlistEntry[] = [ + { + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + limit: 3n, + }, + { + address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + limit: 1n, + }, + { + address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906", + limit: 5n, + }, + ] + + const tree = buildMerkleTree(entries) + console.log("root:", tree.root) + console.log("leaves:", tree.leaves.length) + console.log("layers:", tree.layers.length) + + const proof = getProof(tree, entries[0]) + console.log( + "proof for", + entries[0].address, + "limit", + entries[0].limit.toString(), + ":", + proof, + ) + console.log( + "→ pass to collection.mint(qty, proof, limit) when this address mints", + ) +} diff --git a/cc0company/erc721-shared-mint/examples/full-deploy.ts b/cc0company/erc721-shared-mint/examples/full-deploy.ts new file mode 100644 index 0000000000..a5c4e534fe --- /dev/null +++ b/cc0company/erc721-shared-mint/examples/full-deploy.ts @@ -0,0 +1,189 @@ +/** + * full-deploy.ts — end-to-end deploy of an ERC721Shared collection + * as an AI agent. Walks all 5 steps from SKILL.md. + * + * Run with: `tsx examples/full-deploy.ts ./your-artwork.png` + * + * Env required: + * CC0_AGENT_API_KEY — agent API key (Bearer) + * CDP_API_KEY_ID — Coinbase CDP credentials... + * CDP_API_KEY_SECRET — ...for signing the ETH payment + * + * The CDP SDK paths in here can be swapped for any viem-compatible + * signer — see SKILL.md "Step 4" for the viem alternative. + */ + +import { readFile } from "node:fs/promises" +import { CdpClient } from "@coinbase/cdp-sdk" + +const API = "https://cc0.company/api" +const CHUNK_SIZE = 50_000 + +const API_KEY = process.env.CC0_AGENT_API_KEY +if (!API_KEY) { + throw new Error("CC0_AGENT_API_KEY is required") +} + +const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${API_KEY}`, +} + +async function main() { + const artworkPath = process.argv[2] + if (!artworkPath) { + throw new Error("Usage: tsx full-deploy.ts ") + } + + // ─── Step 1: Create draft ───────────────────────────────────── + const draft = await jsonPost(`${API}/store/agents/me/collections`, { + name: "Cosmic Dreams", + symbol: "COSMIC", + description: "100 editions of a hand-drawn cosmic scene.", + token_standard: "ERC721Shared", + chain: "base", + max_supply: 100, + mint_price: "1000000000000000", // 0.001 ETH + payment_token: "0x0000000000000000000000000000000000000000", + royalty_bps: 500, + }) + if (!draft.success) { + throw new Error(`Draft creation failed: ${draft.error}`) + } + const collectionId: string = draft.collection.id + console.log("✓ Draft created:", collectionId) + + // ─── Step 2: Upload artwork chunks ──────────────────────────── + const bytes = await readFile(artworkPath) + const mime = guessMime(artworkPath) + const dataUrl = `data:${mime};base64,${bytes.toString("base64")}` + const chunks: string[] = [] + for (let i = 0; i < dataUrl.length; i += CHUNK_SIZE) { + chunks.push(dataUrl.slice(i, i + CHUNK_SIZE)) + } + for (let i = 0; i < chunks.length; i++) { + const res = await jsonPost( + `${API}/store/agents/me/collections/${collectionId}/artwork-chunk`, + { + chunk_index: i, + total_chunks: chunks.length, + data: chunks[i], + }, + ) + if (!res.success) { + throw new Error(`Chunk ${i} failed: ${res.error}`) + } + process.stdout.write(`\rchunk ${i + 1}/${chunks.length}`) + } + console.log("\n✓ Artwork buffered:", chunks.length, "chunks") + + // ─── Step 3: Get the deploy quote (402 challenge) ───────────── + const cdp = new CdpClient({ + apiKeyId: process.env.CDP_API_KEY_ID!, + apiKeySecret: process.env.CDP_API_KEY_SECRET!, + }) + const account = await cdp.evm.getOrCreateAccount({ name: "my-agent" }) + console.log("✓ Wallet:", account.address) + + const now = Math.floor(Date.now() / 1000) + const deployParams = { + name: "Cosmic Dreams", + symbol: "COSMIC", + description: "100 editions of a hand-drawn cosmic scene.", + maxSupply: "100", + mintSettings: { + publicMintPrice: "1000000000000000", + paymentToken: "0x0000000000000000000000000000000000000000", + mintStart: now, + mintEnd: now + 60 * 60 * 24 * 30, // 30 days + maxPerAddress: "5", + }, + withdrawRecipients: [ + { recipient: account.address, percentage: 10000 }, + ], + royaltyRecipient: account.address, + royaltyBps: 500, + owner: account.address, + initialMerkleRoot: + "0x0000000000000000000000000000000000000000000000000000000000000000", + } + + const quote = await jsonPost( + `${API}/store/agents/me/collections/${collectionId}/orchestrate-shared-deploy`, + { deploy_params: deployParams }, + ) + if (quote.success || quote.error !== "Payment Required") { + throw new Error(`Expected 402 quote, got: ${JSON.stringify(quote)}`) + } + console.log( + "✓ Quote received:", + quote.ethCostWei, + "wei to", + quote.platformWallet, + ) + + // ─── Step 4: Pay the quoted amount ──────────────────────────── + const { transactionHash: paymentTxHash } = await account.sendTransaction({ + network: "base", + transaction: { + to: quote.platformWallet as `0x${string}`, + value: BigInt(quote.ethCostWei), + data: "0x", + }, + }) + console.log("✓ Payment sent:", paymentTxHash) + + // ─── Step 5: Finalize the deploy ────────────────────────────── + console.log("→ Orchestrating deploy + artwork commit (this takes 10-30s)…") + const deployed = await jsonPost( + `${API}/store/agents/me/collections/${collectionId}/orchestrate-shared-deploy`, + { + payment_tx_hash: paymentTxHash, + deploy_params: deployParams, + }, + ) + if (!deployed.success) { + throw new Error(`Deploy failed: ${JSON.stringify(deployed)}`) + } + console.log("\n✅ COLLECTION LIVE") + console.log(" Contract:", deployed.contract_address) + console.log(" Deploy tx:", deployed.deploy_tx_hash) + console.log(" Finalize tx:", deployed.finalize_tx_hash) + console.log( + " OpenSea:", + `https://opensea.io/assets/base/${deployed.contract_address}/1`, + ) +} + +async function jsonPost(url: string, body: unknown) { + const res = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(body), + }) + return res.json() +} + +function guessMime(path: string): string { + const ext = path.toLowerCase().split(".").pop() + switch (ext) { + case "png": + return "image/png" + case "jpg": + case "jpeg": + return "image/jpeg" + case "gif": + return "image/gif" + case "svg": + return "image/svg+xml" + case "webp": + return "image/webp" + default: + return "application/octet-stream" + } +} + +main().catch((err) => { + console.error("✗", err.message || err) + process.exit(1) +}) diff --git a/cc0company/launchpad/SKILL.md b/cc0company/launchpad/SKILL.md new file mode 100644 index 0000000000..e5ff91a7a4 --- /dev/null +++ b/cc0company/launchpad/SKILL.md @@ -0,0 +1,215 @@ +--- +name: cc0company-launchpad +version: 1.0.0 +description: Launch your own token on the cc0.company launchpad (Base, Uniswap V4) as an AI agent — one transaction, instant liquidity, 75% of all trading fees back to you forever, enforced on-chain. Includes wallet flows (viem / CDP / private key), a keyless flow for Bankr-style signers, fee claiming, and $cc0company staking. +homepage: https://cc0.company +api_base: https://cc0.company/api +sdk: "@cc0company/sdk" +chain: base +chain_id: 8453 +factory: 0xf9007657b627c5421d6eBD5D71F86CDfCdc7dA8D +fee_locker: 0xC04bdF721FA5CEc839819864FA86F3D48B89Fcee +staking: 0x38cE743b88c54eD1aF84816Ff596E518d16DFF95 +cc0company_token: 0x67c5F00491c09cbCF6359f95690574E6106bb3CF +--- + +# cc0.company Launchpad — Skill for AI Agents + +Launch a token on Base in ONE transaction: ERC20 + Uniswap V4 pool + liquidity +locked forever + fee split wired — atomically. The token trades the second the +transaction lands. Your token page on cc0.company (chart, swap, fee-claim +button) goes live automatically. + +## The economics — enforced on-chain, not promised + +Every trade's LP fee is split: + +| Share | Recipient | Asset | +|-------|-----------|-------| +| **75%** | **You, the creator** — splittable across up to 5 wallets | ETH + your own token | +| 15% | $cc0company stakers | ETH | +| 10% | Platform treasury | ETH | + +The factory validates this split on every `deployToken` call. A config that +drops, resizes, or re-administers the staking/treasury slices reverts with +`Cc0InvalidProtocolSplit` — there is no way to launch around it, which also +means nobody can rug YOUR 75%. You remain the admin of your own slices: +redirect or re-split them any time. + +## Install + +```bash +npm install @cc0company/sdk viem +``` + +Source: [github.com/cryptomfer/cc0company-sdk](https://github.com/cryptomfer/cc0company-sdk) (CC0-1.0). + +## Path A — viem-compatible signer (walletClient or private key) + +```typescript +import { Cc0Launchpad } from '@cc0company/sdk'; +import { privateKeyToAccount } from 'viem/accounts'; + +const launchpad = new Cc0Launchpad({ account: privateKeyToAccount(process.env.PK) }); +// or { walletClient } for browser wallets (MetaMask, Rabby, …) +``` + +Then launch: + +```typescript +const { tokenAddress, txHash, registered } = await launchpad.launchToken({ + name: 'My Token', + symbol: 'MTK', + image: imageBytes, // ANY https URL, data: URL, Blob or Uint8Array — the SDK pins + // it to IPFS via cc0.company BEFORE launching (the URI goes + // on-chain forever, so permanence is guaranteed; 8MB max, + // images only). ipfs:// URIs pass through untouched. If + // pinning fails the launch fails — by design. Escape hatch: + // imagePolicy: 'as-is'. + description: 'born to launch', // stored on-chain + feeTier: 1, // 1 | 2 | 3 % static — or feeMode: 'dynamic' +}); +// registered === true → cc0.company/token/{tokenAddress} is live +``` + +Need the `ipfs://` URI up front (e.g. for Path B)? +`await launchpad.pinImage(bytesOrUrl)` → `{ cid, ipfsUri, gatewayUrl }` — +direct endpoint: `POST https://cc0.company/api/store/launchpad/pin-image` +(multipart `file` or JSON `{ url }`). + +Gas: a few cents of ETH on Base. That's the only cost — no listing fee. + +## Path B — ANY other wallet infra (Coinbase CDP, Bankr, Safe, relayers) + +CDP accounts are NOT viem-compatible (no `toAccount` adapter exists in +`@coinbase/cdp-sdk`) — give the SDK a `sender` instead. The SDK pins the image, +builds the tx, estimates gas (+20%); your infra signs + submits; the SDK waits, +parses, and registers. ALL SDK methods (launch, claim fees, stake) work through it. + +```typescript +import { CdpClient } from '@coinbase/cdp-sdk'; +import { Cc0Launchpad } from '@cc0company/sdk'; + +const cdp = new CdpClient(); +const account = await cdp.evm.getOrCreateAccount({ name: 'launcher' }); + +const launchpad = new Cc0Launchpad({ + sender: { + address: account.address, + send: async (tx) => { + // CDP's TypeScript SDK wants the BIGINT fields (tx.*) — hex fee strings + // throw TipAboveFeeCapError. Everything is pre-estimated by the SDK. + // (tx.json — the hex mirror — is for RAW JSON transports/relayers only.) + const { transactionHash } = await cdp.evm.sendTransaction({ + address: account.address, + network: 'base', + transaction: { + to: tx.to, + data: tx.data, + value: tx.value, + gas: tx.gas, + maxFeePerGas: tx.maxFeePerGas, + maxPriorityFeePerGas: tx.maxPriorityFeePerGas, + }, + }); + return transactionHash; // CDP's field is `transactionHash`, NOT `hash` + }, + }, +}); + +await launchpad.launchToken({ ... }); // everything handled end-to-end +``` + +Fully manual (no sender): `prepareLaunchTransaction(params, { creator })` → +submit `tx.json` yourself → `finishLaunch({ txHash, params, creator, imageUri: tx.imageUri })` +waits + parses + registers in one call. + +## All launch options + +```typescript +await launchpad.launchToken({ + name: 'My Token', + symbol: 'MTK', + image: 'ipfs://…', + + feeMode: 'static', // 'static' (default) | 'dynamic' (1%→3% volatility preset) + feeTier: 1, // 1 | 2 | 3 (static only) + + // Split YOUR 75% across up to 5 wallets (bps of TOTAL fees, sum must be 7500) + creatorRewards: [ + { recipient: '0xYou', bps: 5000, feePreference: 'both' }, // ETH + token + { recipient: '0xPartner', bps: 2500, feePreference: 'paired' }, // ETH only + ], + + // Anti-snipe: descending tax on the first seconds… or omit for a 2-block MEV delay + sniperTax: { startingBps: 800_000, endingBps: 50_000, secondsToDecay: 15 }, + + // Lock supply for yourself (lockup ≥ 7 days, optional linear vesting) + vault: { percentage: 10, lockupSeconds: 604800, vestingSeconds: 2592000 }, + + // Merkle airdrop of supply (lockup ≥ 1 day) + airdrop: { merkleRoot: '0x…', percentage: 5 }, + + // Buy your own token in the launch transaction + devBuyEth: '0.05', + + register: true, // default — auto-registers on cc0.company +}); +``` + +## Claim your fees + +Fees accrue in the fee locker per `(feeOwner, token)` as trades happen. +Claiming is PERMISSIONLESS — any wallet can trigger it, funds always go to the +creator. Three ways: + +```typescript +// SDK +import { Cc0Fees } from '@cc0company/sdk'; +const fees = new Cc0Fees({ account }); +await fees.getClaimableFees(creatorWallet, tokenAddress); // { weth, token } in wei +await fees.claimFees(creatorWallet, tokenAddress); // claims both +``` + +- **Token page**: `cc0.company/token/{address}` has a one-tap **Claim fees** button. +- **Raw contract**: `claim(address feeOwner, address token)` on the fee locker + `0xC04bdF721FA5CEc839819864FA86F3D48B89Fcee` (claim WETH at + `0x4200000000000000000000000000000000000006`, and your token address). + +## Stake $cc0company (earn from EVERY launch) + +15% of every launchpad token's trading fees flow to $cc0company stakers, in ETH: + +```typescript +import { Cc0Staking } from '@cc0company/sdk'; +import { parseEther } from 'viem'; + +const staking = new Cc0Staking({ account }); +await staking.stake(parseEther('1000')); // auto-approves if needed; earning starts now +await staking.claimRewards(); // accrued ETH (WETH) → your wallet +const pos = await staking.getPosition(me); // staked / earned / unbonding / totals +await staking.requestUnstake(parseEther('500')); // 48h cooldown, then: +await staking.withdraw(); +await staking.exit(); // claim all + unstake all, one tx +``` + +## Contract reference (Base mainnet, all verified) + +| Contract | Address | +|----------|---------| +| Factory (validates the split) | `0xf9007657b627c5421d6eBD5D71F86CDfCdc7dA8D` | +| Fee locker (claim here) | `0xC04bdF721FA5CEc839819864FA86F3D48B89Fcee` | +| Staking | `0x38cE743b88c54eD1aF84816Ff596E518d16DFF95` | +| $cc0company | `0x67c5F00491c09cbCF6359f95690574E6106bb3CF` | +| WETH (Base) | `0x4200000000000000000000000000000000000006` | + +Full list: [cc0.company/docs/smart-contracts](https://cc0.company/docs/smart-contracts). + +## Errors you might hit + +| Error | Cause | Fix | +|-------|-------|-----| +| `Cc0InvalidProtocolSplit` | Hand-rolled lockerConfig that drops/resizes the protocol slices | Use the SDK — it builds the exact enforced layout | +| `Deprecated` | You called the OLD factory (`0x2Ed3…D40B`) | Use the current factory above / update the SDK | +| `walletClient has no account attached` | viem client built without an account | Pass `account:` to the constructor or attach one to the WalletClient | +| `registered: false` in the result | Registry unreachable (the launch itself succeeded) | Call `registerLaunch()` again later — idempotent | diff --git a/cc0company/x402-payments/SKILL.md b/cc0company/x402-payments/SKILL.md new file mode 100644 index 0000000000..4a04e71533 --- /dev/null +++ b/cc0company/x402-payments/SKILL.md @@ -0,0 +1,226 @@ +--- +name: cc0company-x402-payments +version: 1.0.0 +description: Canonical x402 v2 client reference for cc0.company. Three signing patterns (viem one-liner, Bankr HTTP, CDP SDK), Coinbase Bazaar discovery, error matrix. +homepage: https://cc0.company +network: eip155:8453 +facilitator: https://api.cdp.coinbase.com/platform/v2/x402 +--- + +# x402 Payments on cc0.company — Skill for AI Agents + +Two cc0.company endpoints are x402-gated: + +- `POST /api/store/agent-services/:slug/invoke` — AI image generation (0.069 USDC) +- `POST /api/store/agent-assets/:slug/buy` — Buy a CC0 asset (price varies) + +Both use **x402 v2** on Base mainnet. The flow is identical: call without payment → receive a 402 challenge → sign an EIP-3009 USDC `transferWithAuthorization` for the requested amount → retry with the signed payload in `PAYMENT-SIGNATURE`. The legacy `X-PAYMENT` header is still accepted on `/agent-assets/buy` for compatibility, but new integrations should use `PAYMENT-SIGNATURE`. + +## Network + asset constants + +``` +Network: Base mainnet (chainId 8453, CAIP-2 eip155:8453) +USDC contract: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 +EIP-712 domain: { name: "USD Coin", version: "2", chainId: 8453, verifyingContract: } +Facilitator: https://api.cdp.coinbase.com/platform/v2/x402 (Coinbase CDP) +``` + +The `payTo` address and `maxAmountRequired` value come from the **live 402 response** — never hard-code them. + +## Pattern A — `@x402/fetch` + viem (recommended, one-liner) + +Works for **any wallet where you can produce a viem-compatible signer**: throwaway hot keys (`generatePrivateKey()`), exported private keys from CDP / Base MCP, browser-extension wallets via WalletConnect, etc. + +```bash +npm install @x402/fetch @x402/evm viem +``` + +```typescript +import { x402Client, wrapFetchWithPayment } from "@x402/fetch" +import { registerExactEvmScheme } from "@x402/evm/exact/client" +import { privateKeyToAccount } from "viem/accounts" + +const signer = privateKeyToAccount(YOUR_PRIVATE_KEY as `0x${string}`) +const client = new x402Client() +registerExactEvmScheme(client, { signer }) +const fetchWithPayment = wrapFetchWithPayment(fetch, client) + +// Any fetch through this wrapper auto-handles 402 challenges: +const res = await fetchWithPayment( + "https://cc0.company/api/store/agent-services/sartoshi-gen/invoke", + { + method: "POST", + headers: { "Content-Type": "application/json", "X-Agent-Name": "your_handle" }, + body: JSON.stringify({ prompt: "..." }), + } +) +const job = await res.json() +``` + +The wrapper catches the 402, signs `transferWithAuthorization` with your viem signer, attaches `PAYMENT-SIGNATURE`, and retries — zero manual typed-data work. + +## Pattern B — Bankr (HTTP-only, no Node needed) + +For agents running in environments where you can't import npm packages but you can `curl`. Bankr exposes a typed-data signing endpoint at `/wallet/sign` (the older `/agent/sign` is deprecated and returns HTML — make sure you hit the new path). + +```bash +# 1. Get the 402 challenge — read paymentRequired from the response body +CHALLENGE=$(curl -s -X POST https://cc0.company/api/store/agent-services/sartoshi-gen/invoke \ + -H "Content-Type: application/json" \ + -d '{"prompt": "..."}') + +PAY_TO=$(echo "$CHALLENGE" | jq -r '.paymentRequired.payTo') +AMOUNT=$(echo "$CHALLENGE" | jq -r '.paymentRequired.maxAmountRequired') +USDC="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" +NONCE="0x$(openssl rand -hex 32)" +VALID_BEFORE=$(( $(date +%s) + 600 )) +FROM=$(bankr prompt "What is my wallet address on Base?" | jq -r '.address') + +# 2. Ask Bankr to sign the EIP-712 typed data +SIG=$(curl -s -X POST https://api.bankr.bot/wallet/sign \ + -H "X-API-Key: $BANKR_KEY" \ + -H "Content-Type: application/json" \ + -d @- < **CDP gotcha:** don't call `wallet.sign()` — that's raw-bytes +> signing only. The account from `getOrCreateAccount()` is a viem +> `LocalAccount`; pass it as `signer`. If you genuinely need manual +> typed-data signing, call `account.signTypedData({ domain, types, +> primaryType, message })` directly. +> +> If your `@coinbase/cdp-sdk` is older than v1.40, upgrade: +> `npm i @coinbase/cdp-sdk@latest`. + +## What cc0.company actually needs from your wallet + +1. A Base EVM address (`0x...`). +2. The ability to sign EIP-3009 `transferWithAuthorization` for x402 payments. +3. (For ERC1155 mint) the ability to send arbitrary transactions (`sendTransaction`) for collection deploys. + +If your wallet does all three, you can use every cc0.company endpoint. + +## Bazaar discovery + agentic.market + +Every paid endpoint on cc0.company is automatically indexed by the **Coinbase x402 Bazaar** after its first successful settlement. **agentic.market** reads from the same Bazaar index — so once an agent has paid us at least once, the entire catalogue surfaces on `https://agentic.market` and at the CDP discovery API. + +```bash +# Returns every cc0.company service from the CDP discovery index: +curl "https://api.cdp.coinbase.com/platform/v2/x402/discovery/merchant?payTo=" + +# Semantic search across the Bazaar: +curl "https://api.cdp.coinbase.com/platform/v2/x402/discovery/search?query=cc0+image+generation&network=eip155:8453" + +# Browse from agentic.market: +curl "https://agentic.market/v1/services/search?q=cc0" +``` + +The CDP catalogue refreshes on a ~6-hour schedule. Newly-deployed services may take a refresh cycle to appear. + +## Error matrix + +| Code | Means | What to do | +|---|---|---| +| `400` | Invalid prompt or missing buyer wallet | Fix payload + retry | +| `402` | Payment required (first request) | Sign + retry | +| `402 verification failed` | Signed but the payment doesn't cover `maxAmountRequired` or tx is invalid | Re-quote, re-sign | +| `425` | Tx pending (humans only — `pay-and-invoke` flow) | Retry with backoff | +| `5xx` | Server / generation failure | Payment auto-cancels; retry once | +| Job `failed` after retry | Generation crashed twice | Backend auto-refunds; `refund_tx_hash` in the job | + +## Related skills + +- [`../agent-services/SKILL.md`](../agent-services/SKILL.md) — the + 5 image-gen models pay via this protocol +- [`../erc1155-mint/SKILL.md`](../erc1155-mint/SKILL.md) — uses ETH + not USDC, but the agent flow (quote → pay → retry with tx_hash) + has the same shape + +## License + +CC0.