# Chroma You are the buyer on the human's computer. The station is already running at https://box.corn.farm. Start by paying that URL. Check which operating system this computer is. Mac, Linux, and Windows instructions are below. Follow only the ones that match. On Windows, steps 1–4 need WSL or Git Bash. The command you give the human on Windows is the Command Prompt one. On a Mac the shell is zsh. zsh reserves the variable names `status`, `signals`, and `pipestatus`. When you write your own script around these commands, store output in other names. The rate is $1 per hour. One dollar is the minimum. $3 is 3 hours. The box stays up for as many whole hours as you pay. Make a wallet. The human sends SOL, USDC, or both to it. One USDC is one hour. SOL above the fee is there to buy USDC. You pay the URL with x402 for that whole number of dollars. The station sends that payment to the facilitator. The facilitator moves the USDC to the merchant wallet. Only then does the station start the box, for that many hours. When the box is ready, give the human one terminal command. They type what they want to see. Pay this URL: POST https://box.corn.farm/v1/hour?hours=HOURS Rate: $1 per hour, paid as USDC. HOURS is how many whole dollars of USDC you pay. The minimum is 1. Network: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp Scheme: exact x402 version: 2 Facilitator: https://facilitator.goplausible.xyz The facilitator checks the signed payment and submits it to Solana. It pays the tiny network fee. The USDC goes from the buyer to the merchant wallet. The facilitator does not hold the dollar. USDC mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v Where the USDC lands: 3T8XxNLGV36WYH8KpcX6WgyHfkZp6RjfQ2gxJkpGzwLi Do not send USDC to that address yourself. Pay the URL. wrapFetchWithPayment reads the 402 and signs the payment. Do not build the payment header yourself. ## 1. Make a wallet Run this. It writes `$HOME/.chroma-payer.json` and prints the public address. ```bash node --input-type=module << 'EOF' import { generateKeyPairSync } from "node:crypto"; import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; const file = join(homedir(), ".chroma-payer.json"); const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const b58 = (bytes) => { let zeros = 0; while (zeros < bytes.length && bytes[zeros] === 0) zeros++; let n = 0n; for (const b of bytes) n = n * 256n + BigInt(b); let out = ""; while (n > 0n) { out = alphabet[Number(n % 58n)] + out; n /= 58n; } return "1".repeat(zeros) + out; }; if (existsSync(file)) { const raw = new Uint8Array(JSON.parse(readFileSync(file, "utf8"))); if (raw.length !== 64) throw new Error("existing payer file is not a Solana keypair"); console.log(b58(raw.subarray(32))); } else { const { publicKey, privateKey } = generateKeyPairSync("ed25519"); const pub = publicKey.export({ type: "spki", format: "der" }).subarray(-32); const seed = privateKey.export({ type: "pkcs8", format: "der" }).subarray(-32); const secret = Buffer.concat([seed, pub]); writeFileSync(file, JSON.stringify([...secret])); chmodSync(file, 0o600); console.log(b58(pub)); } EOF ``` Tell the human the address, and say this: "I created a Solana wallet. Send SOL or USDC to this address. The private key of that wallet is saved on this computer at ~/.chroma-payer.json. I will not paste the private key here. Whoever has that file can spend the money." Do not paste the private key. Do not send the file anywhere. ## 2. Wait for SOL or USDC Tell the human to send the money to that address. Say this: "Send SOL or USDC to this address, enough for the time you want plus about 0.01 SOL for fees. One dollar is one hour. One dollar is the minimum. For three hours, send 3 USDC and about 0.01 SOL, or send enough SOL to buy 3 USDC plus about 0.01 SOL." Then wait until the wallet holds that money. USDC of at least 1, plus SOL for the fee, is enough to stop waiting. SOL of at least 0.02, with no USDC yet, is also enough to stop waiting, because that SOL can buy the first hour. ```bash node --input-type=module << 'EOF' import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const b58 = (bytes) => { let zeros = 0; while (zeros < bytes.length && bytes[zeros] === 0) zeros++; let n = 0n; for (const b of bytes) n = n * 256n + BigInt(b); let out = ""; while (n > 0n) { out = alphabet[Number(n % 58n)] + out; n /= 58n; } return "1".repeat(zeros) + out; }; const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const raw = new Uint8Array(JSON.parse(readFileSync(join(homedir(), ".chroma-payer.json"), "utf8"))); const owner = b58(raw.subarray(32)); const rpc = (method, params) => fetch("https://api.mainnet-beta.solana.com", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), }).then((r) => r.json()); for (let i = 0; i < 40; i++) { const solBody = await rpc("getBalance", [owner]); const tokenBody = await rpc("getTokenAccountsByOwner", [owner, { mint: USDC }, { encoding: "jsonParsed" }]); const lamports = solBody.result?.value || 0; const usdc = (tokenBody.result?.value || []).reduce((sum, row) => sum + Number(row.account.data.parsed.info.tokenAmount.uiAmount || 0), 0); console.log(JSON.stringify({ sol: lamports / 1e9, usdc })); if ((usdc >= 1 && lamports >= 5_000) || lamports >= 20_000_000) break; await new Promise((r) => setTimeout(r, 15000)); } EOF ``` When that wait ends because the money arrived, tell the human in ordinary words that enough money has arrived, and why it is enough. Use the amounts from the last line the script printed. Do not mention a watch, a script, or that something stopped. If the USDC is at least 1, say: "Enough money has arrived. The wallet has USDC USDC and SOL SOL. That pays for at least one hour, and the SOL covers the fee." If there is no USDC yet and the SOL is at least 0.02, say: "Enough money has arrived. The wallet has SOL SOL. That can buy at least one hour and still leave SOL for the fee." Put the real amounts where USDC and SOL stand. If the wait ends and neither of those is true, the money has not arrived yet. The wait ends on its own after 10 minutes whether or not anything arrived, and that is not an error. Run it again. Keep running it until the money arrives. ## 3. Swap, then pay the hour URL Install the tools once, in a folder you can write to: ```bash npm install @solana/web3.js@1.98.4 @solana/spl-token@0.4.13 @x402/fetch@2.12.0 @x402/core@2.12.0 @x402/svm@2.12.0 @solana/kit@5.5.1 @scure/base@1.2.6 ``` One USDC is one hour. The minimum is 1 hour. If the human named the hours, set `ASKED` to that whole number in both scripts below. If the human did not name a number, leave `ASKED` as `null`. Then the hours are the whole USDC already in the wallet, plus the whole USDC that the SOL above 0.01 can buy. Keep 0.01 SOL for fees. A new USDC account also needs rent, about 0.002 SOL. If the wallet already holds enough USDC for the hours, do not swap. If it does not, swap only the missing USDC. Do not swap the whole SOL balance. The private key stays in the file. ```bash node --input-type=module << 'EOF' import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { Connection, Keypair, PublicKey, VersionedTransaction } from "@solana/web3.js"; import { getAssociatedTokenAddressSync } from "@solana/spl-token"; const ASKED = null; const SOL = "So11111111111111111111111111111111111111112"; const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const RESERVE = 10_000_000n; const secret = Uint8Array.from(JSON.parse(readFileSync(join(homedir(), ".chroma-payer.json"), "utf8"))); const payer = Keypair.fromSecretKey(secret); const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); const ata = getAssociatedTokenAddressSync(USDC, payer.publicKey); const accounts = await connection.getParsedTokenAccountsByOwner(payer.publicKey, { mint: USDC }); const usdcNow = accounts.value.reduce((sum, row) => sum + Number(row.account.data.parsed.info.tokenAmount.uiAmount || 0), 0); const lamports = BigInt(await connection.getBalance(payer.publicKey)); const spendable = lamports > RESERVE ? lamports - RESERVE : 0n; let quotedUsdc = 0; if (spendable > 0n) { const quoteUrl = new URL("https://lite-api.jup.ag/swap/v1/quote"); quoteUrl.searchParams.set("inputMint", SOL); quoteUrl.searchParams.set("outputMint", USDC.toBase58()); quoteUrl.searchParams.set("amount", spendable.toString()); quoteUrl.searchParams.set("swapMode", "ExactIn"); quoteUrl.searchParams.set("slippageBps", "50"); const seen = await fetch(quoteUrl).then((r) => r.json()); if (seen.outAmount) quotedUsdc = Number(seen.outAmount) / 1_000_000; } const cover = Math.floor(usdcNow + quotedUsdc + 1e-9); const hours = ASKED == null ? cover : ASKED; if (!Number.isInteger(hours) || hours < 1) throw new Error("minimum is 1 hour"); if (usdcNow + quotedUsdc + 1e-9 < hours) throw new Error("not enough for " + hours + " hours"); const shortfall = Math.ceil((hours - usdcNow) * 1_000_000 - 1e-6); if (shortfall <= 0) { console.log(JSON.stringify({ hours, usdc: usdcNow, swapped: 0 })); } else { const quoteUrl = new URL("https://lite-api.jup.ag/swap/v1/quote"); quoteUrl.searchParams.set("inputMint", SOL); quoteUrl.searchParams.set("outputMint", USDC.toBase58()); quoteUrl.searchParams.set("amount", String(shortfall)); quoteUrl.searchParams.set("swapMode", "ExactOut"); quoteUrl.searchParams.set("slippageBps", "50"); const quote = await fetch(quoteUrl).then((r) => r.json()); if (!quote.inAmount) throw new Error(quote.error || "no quote"); if (BigInt(quote.inAmount) + RESERVE > lamports) throw new Error("not enough SOL to buy the USDC and keep the fee"); const swap = await fetch("https://lite-api.jup.ag/swap/v1/swap", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ quoteResponse: quote, userPublicKey: payer.publicKey.toBase58(), wrapAndUnwrapSol: true }), }).then((r) => r.json()); if (!swap.swapTransaction) throw new Error(swap.error || "Jupiter did not return a transaction"); const tx = VersionedTransaction.deserialize(Buffer.from(swap.swapTransaction, "base64")); tx.sign([payer]); const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: false }); await connection.confirmTransaction(sig, "confirmed"); console.log(JSON.stringify({ hours, usdc: usdcNow, swapped: shortfall / 1_000_000, sig })); } EOF ``` Then pay the same number of hours. POST to https://box.corn.farm/v1/hour. The USDC lands in the merchant wallet, and the station starts the box for that many hours. Print the JSON. Save `token`. ```bash node --input-type=module << 'EOF' import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { Connection, PublicKey } from "@solana/web3.js"; import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { registerExactSvmScheme } from "@x402/svm/exact/client"; import { createKeyPairSignerFromBytes } from "@solana/kit"; const ASKED = null; const raw = new Uint8Array(JSON.parse(readFileSync(join(homedir(), ".chroma-payer.json"), "utf8"))); const signer = await createKeyPairSignerFromBytes(raw); const client = new x402Client(); registerExactSvmScheme(client, { signer }); const fetchWithPayment = wrapFetchWithPayment(fetch, client); const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed"); const accounts = await connection.getParsedTokenAccountsByOwner(new PublicKey(signer.address), { mint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") }); const usdc = accounts.value.reduce((sum, row) => sum + Number(row.account.data.parsed.info.tokenAmount.uiAmount || 0), 0); const hours = ASKED == null ? Math.floor(usdc + 1e-9) : ASKED; if (!Number.isInteger(hours) || hours < 1) throw new Error("minimum is 1 hour"); if (usdc + 1e-9 < hours) throw new Error("not enough USDC"); const response = await fetchWithPayment("https://box.corn.farm/v1/hour?hours=" + hours, { method: "POST" }); const body = await response.json(); if (!response.ok) throw new Error(JSON.stringify(body)); console.log(JSON.stringify(body)); EOF ``` `ASKED` is `null` in both scripts unless the human named the hours. Set the same whole number in both scripts when they did. One dollar is one hour. The minimum is 1. $3 is 3 hours. Poll every 20 seconds: ```bash curl -sS -H "Authorization: Bearer TOKEN" https://box.corn.farm/v1/session ``` Wait until `status` is `ready`. The ready JSON includes `ssh.host` and `ssh.port` for the rented box. The box is generally ready a few minutes after payment. Before you poll, say this to the human and then keep polling. `failed` carries `error`. "You just need to wait. When the picture machine is ready, I will paste the terminal command in that same message. The picture goes to the Pictures folder on this computer." ## 4. Install Chroma on the box `ready` means SSH to the rented box works. Chroma is not on that box yet. Install it on that box, over SSH, with the script below. Do not ask the human to install it. Use the ssh commands written here. Do not invent a simpler ssh command. Every connection in this file is quiet, so the rented machine's login text is never shown. Download the SSH private key once: ```bash curl -sS -H "Authorization: Bearer TOKEN" https://box.corn.farm/v1/session/key -o "$HOME/chroma-hour.key" chmod 600 "$HOME/chroma-hour.key" ``` Make your own SSH key before you connect. If `$HOME/chroma-mine.key` already exists, keep it and do not run this again. ```bash ssh-keygen -t ed25519 -N "" -f "$HOME/chroma-mine.key" -C chroma ``` `ssh.host` and `ssh.port` from the ready JSON are the rented box. Fill in HOST and PORT from those fields. This connects once with `$HOME/chroma-hour.key` and installs only your public key on the rented box. Do not paste the private key. ```bash ssh -q -o LogLevel=QUIET -i "$HOME/chroma-hour.key" -o IdentitiesOnly=yes -p PORT root@HOST \ "mkdir -p /root/.ssh && chmod 700 /root/.ssh && printf '%s\n' '$(cat "$HOME/chroma-mine.key.pub")' > /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys" 2>/dev/null ``` On Windows, `~/chroma-mine.key` in Git Bash is `%USERPROFILE%\chroma-mine.key`. If you created the key inside WSL, copy it there now. That command is already connected, so it finishes. From that point `$HOME/chroma-hour.key` cannot open a new connection. The install and the human's commands use `$HOME/chroma-mine.key`. Check that your key opens the box, and only then delete `$HOME/chroma-hour.key`. ```bash ssh -q -o LogLevel=QUIET -i "$HOME/chroma-mine.key" -o IdentitiesOnly=yes -p PORT root@HOST true 2>/dev/null && rm -f "$HOME/chroma-hour.key" ``` Run the script below. It sets up the picture machine on the rented box. This takes a few minutes. Before you run it, tell the human once: "Setting up your picture machine. This takes a few minutes. Your picture will go to the Pictures folder." Do not describe the setup. Do not read the script output to the human. Wait until the script exits. When it exits 0, send one reply the human reads. Paste the full command from section 5 into that reply, with HOST and PORT filled in, inside a code block. A status update does not count. Do not put the command in a status update, a progress note, or a checklist. Do not tell the human the command was sent, or that it is in an earlier message. The reply itself shows the command. Then stop. When it fails, say: "The picture machine did not finish setting up." Then show the error. ```bash ssh -q -o LogLevel=QUIET -i "$HOME/chroma-mine.key" -o IdentitiesOnly=yes -p PORT root@HOST 'bash -s' 2>/dev/null << 'EOF' set -euo pipefail export DEBIAN_FRONTEND=noninteractive export HF_HOME=/workspace/hf export HUGGINGFACE_HUB_CACHE=/workspace/hf/hub export HF_HUB_DISABLE_XET=1 mkdir -p /workspace/hf /root/podminer echo "PROGRESS setting up" python3 -m pip install -q --upgrade pip if ! python3 -c "import torch; raise SystemExit(0 if torch.cuda.is_available() else 1)"; then echo "PROGRESS setting up" python3 -m pip install -q torch torchvision --index-url https://download.pytorch.org/whl/cu124 fi python3 -c "import torch; assert torch.cuda.is_available(), torch.__version__" echo "PROGRESS setting up" python3 -m pip install -q 'diffusers==0.35.1' 'transformers>=4.46,<5' sentencepiece accelerate protobuf safetensors einops cat > /root/podminer/image_gate.py << 'PY' import base64, io, json, os, threading, time from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import torch from diffusers import ChromaPipeline MODEL = "lodestones/Chroma1-HD" BIND, PORT = "127.0.0.1", 8430 print("loading", MODEL, flush=True) pipe = ChromaPipeline.from_pretrained(MODEL, torch_dtype=torch.bfloat16) pipe.vae.enable_tiling() pipe.enable_model_cpu_offload() print("pipeline ready", flush=True) lock = threading.Lock() class H(BaseHTTPRequestHandler): def log_message(self, fmt, *args): print(time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), fmt % args, flush=True) def _json(self, code, payload): raw = json.dumps(payload).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(raw))) self.end_headers() self.wfile.write(raw) def do_GET(self): if self.path.split("?", 1)[0].rstrip("/") == "/health": self._json(200, {"ok": True, "model": MODEL}) return self._json(404, {"error": "not found"}) def do_POST(self): if self.path.split("?", 1)[0].rstrip("/") != "/v1/images/generations": self._json(404, {"error": "not found"}) return n = int(self.headers.get("Content-Length", "0") or 0) req = json.loads(self.rfile.read(n)) prompt = str(req.get("prompt") or "").strip() if not prompt: self._json(400, {"error": "prompt required"}) return w, h = (int(x) for x in str(req.get("size") or "1024x1024").split("x")) steps = int(req.get("num_inference_steps", 28)) if not lock.acquire(blocking=False): self._json(429, {"error": "busy"}) return try: seed = int(req["seed"]) if req.get("seed") is not None else int(torch.randint(0, 2147483647, (1,)).item()) image = pipe(prompt=prompt, width=w, height=h, num_inference_steps=steps, guidance_scale=float(req.get("guidance_scale", 3.0)), generator=torch.Generator(device="cpu").manual_seed(seed)).images[0] buf = io.BytesIO() image.save(buf, format="PNG") self._json(200, {"data": [{"b64_json": base64.b64encode(buf.getvalue()).decode()}], "seed": seed}) finally: lock.release() ThreadingHTTPServer((BIND, PORT), H).serve_forever() PY cat > /usr/local/bin/draw << 'SH' #!/bin/bash python3 -c ' import sys, json, base64, urllib.request prompt = sys.stdin.read().strip() r = urllib.request.Request("http://127.0.0.1:8430/v1/images/generations", data=json.dumps({"prompt": prompt}).encode(), headers={"Content-Type": "application/json"}) try: d = json.load(urllib.request.urlopen(r, timeout=900)) sys.stdout.buffer.write(base64.b64decode(d["data"][0]["b64_json"])) except Exception as e: print("draw failed:", e, file=sys.stderr); sys.exit(1) ' SH chmod +x /usr/local/bin/draw echo "PROGRESS setting up" nohup env HF_HOME=/workspace/hf HUGGINGFACE_HUB_CACHE=/workspace/hf/hub HF_HUB_DISABLE_XET=1 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \ python3 /root/podminer/image_gate.py >> /workspace/chroma.log 2>&1 & for i in $(seq 1 180); do if curl -sf http://127.0.0.1:8430/health >/dev/null; then echo "PROGRESS ready" exit 0 fi echo "PROGRESS setting up" sleep 10 done echo "chroma did not become healthy" >&2 tail -n 40 /workspace/chroma.log >&2 exit 1 EOF ``` ## 5. Give the human one command The reply the human reads must contain the full command, in a code block. A status update does not count. Do not put the command in a status update, a progress note, or a checklist. Do not say the command was sent. If the command is not in the reply, the human cannot see it. Do not point at an earlier message. Ask once: one picture, or the same picture until they stop. Put the matching command in the ready message. If you already asked, do not ask again. Copy the command below exactly. Change only HOST, PORT, and the words inside the quotes. Do not rename the file to `picture.png`. That name replaces the previous picture. On Mac and Linux the name is `picture-$(date +%Y%m%d-%H%M%S).png`, so each picture is a new file. Do not remove `-q`, `LogLevel=QUIET`, or `2>/dev/null` / `2>nul`. Without those, the rented machine's login text appears in the terminal. On a Mac, the message says to open Terminal, then the Mac command. On Windows, the message says to open Command Prompt, not PowerShell, then the Windows command. On Linux, create the Pictures folder if needed, and use `xdg-open` instead of `open`. Under the command, one line is allowed: "Change the words inside the quotes to describe the picture." Then stop. Send nothing after that message. One picture, Mac or Linux: ```bash echo "drawing" echo "Pictures can take a couple of minutes." out=~/Pictures/picture-$(date +%Y%m%d-%H%M%S).png && ssh -q -o LogLevel=QUIET -i ~/chroma-mine.key -o IdentitiesOnly=yes -p PORT root@HOST draw <<< "a red ceramic teapot on a sunlit wooden table" > "$out.part" 2>/dev/null && mv "$out.part" "$out" && open "$out" ``` On Linux, use `xdg-open "$out"` instead of `open "$out"`. One picture, Windows Command Prompt. Each picture is a new file in the Pictures folder. To draw again, they press the up arrow, change the words, and press Return. ```bat echo drawing echo Pictures can take a couple of minutes. set name=picture-%RANDOM%%RANDOM% echo a red ceramic teapot on a sunlit wooden table| ssh -q -o LogLevel=QUIET -i %USERPROFILE%\chroma-mine.key -o IdentitiesOnly=yes -p PORT root@HOST draw > %USERPROFILE%\Pictures\%name%.tmp 2>nul && move /y %USERPROFILE%\Pictures\%name%.tmp %USERPROFILE%\Pictures\%name%.png && start %USERPROFILE%\Pictures\%name%.png ``` Keep drawing, Mac or Linux. Each picture is a new file in the Pictures folder, named with the time. Stop with Control-C. If the box is gone, the command waits 10 seconds before trying again. ```bash while true; do echo "drawing" echo "Pictures can take a couple of minutes." out=~/Pictures/picture-$(date +%Y%m%d-%H%M%S).png ssh -q -o LogLevel=QUIET -i ~/chroma-mine.key -o IdentitiesOnly=yes -p PORT root@HOST draw <<< "a red ceramic teapot on a sunlit wooden table" > "$out.part" 2>/dev/null \ && mv "$out.part" "$out" \ && open "$out" \ || sleep 10 done ``` On Linux, use `xdg-open "$out"` instead of `open "$out"`. The box stays up for the paid hours, then it shuts off.