# Chroma You are the buyer. The service is already running. Do not edit code. Do not search a repository. Do not install the shop. 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. The rate is $1 per hour. The box stays up for as many hours as you pay. $2 is 2 hours. $3 is 3 hours. It is not a one-hour rental. Make a wallet. The human sends SOL to it. You swap that SOL to USDC and pay the URL with x402 for the 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 http://127.0.0.1:4402/v1/hour?hours=HOURS Rate: $1 per hour, paid as USDC. HOURS is how many whole dollars of USDC you pay. 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. That address is where you send SOL. 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 Tell the human to send SOL to that address privately. A normal public transfer shows who sent it. Say this: "Send SOL to this address in private. Solflare, a privacy transfer, or Houdini Swap all work. Any other private method you already use is fine." Then wait for the SOL to arrive. ```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 raw = new Uint8Array(JSON.parse(readFileSync(join(homedir(), ".chroma-payer.json"), "utf8"))); const owner = b58(raw.subarray(32)); for (let i = 0; i < 40; i++) { const body = await fetch("https://api.mainnet-beta.solana.com", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getBalance", params: [owner] }), }).then((r) => r.json()); const sol = (body.result?.value || 0) / 1e9; console.log(sol); if (sol > 0) break; await new Promise((r) => setTimeout(r, 15000)); } EOF ``` ## 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 ``` Swap only the USDC for the hours being paid. The default is 1 hour, so swap 1 USDC. If the human asked for more hours, set `HOURS` to that number. Do not swap the whole SOL balance. A new USDC account needs rent, about 0.002 SOL, plus 5,000 lamports for the transaction fee. Keep both. If the USDC account already exists, keep only the fee. 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 HOURS = 1; const SOL = "So11111111111111111111111111111111111111112"; const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const FEE = 5_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 ataInfo = await connection.getAccountInfo(ata); const rent = ataInfo ? 0n : BigInt(await connection.getMinimumBalanceForRentExemption(165)); const lamports = BigInt(await connection.getBalance(payer.publicKey)); const reserve = rent + FEE; 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(HOURS * 1_000_000)); 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 still open the USDC account"); } 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("swapped", HOURS, "USDC", sig); EOF ``` Then pay the same number of hours. The payment goes to https://facilitator.goplausible.xyz, 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 HOURS = 1; 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); if (usdc < HOURS) throw new Error("not enough USDC"); const response = await fetchWithPayment("http://127.0.0.1:4402/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 ``` `HOURS` is 1 in both scripts. That pays 1 USDC and the box runs for 1 hour. If the human asked for more hours, set `HOURS` to that number in both scripts before running them. $2 is 2 hours. $3 is 3 hours. Poll every 20 seconds: ```bash curl -sS -H "Authorization: Bearer TOKEN" http://127.0.0.1:4402/v1/session ``` Wait until `status` is `ready`. 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`. "I'll guide you through how to open the terminal, and give you the command to cut and paste into the terminal after this. You just need to wait. The rented box will generate your image and send it here to your computer." ## 4. Install Chroma on the box `ready` means SSH works. Chroma is not installed yet. You install it. Do not ask the human to install it. Download the SSH private key once: ```bash curl -sS -H "Authorization: Bearer TOKEN" http://127.0.0.1:4402/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` are the box. Fill in HOST and PORT. This connects once with the station key and installs only your public key. Do not paste the private key. ```bash ssh -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" ``` 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 the station 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 the station key. ```bash ssh -i "$HOME/chroma-mine.key" -o IdentitiesOnly=yes -p PORT root@HOST true && rm -f "$HOME/chroma-hour.key" ``` Every 10 to 15 seconds, tell the human one plain sentence about the newest `PROGRESS` line. If the same line is still the newest, say that step is still running. Do not go silent until Chroma is ready. The box image may include PyTorch built for CUDA 13. On an older host driver that fails with CUDA error 804 and `torch.cuda.is_available()` is false. If CUDA is not available, reinstall PyTorch from the cu124 wheel before installing Diffusers. Then install Diffusers 0.35.1, Transformers 4.x, and Accelerate. The model is `lodestones/Chroma1-HD`. It listens on `127.0.0.1:8430` only. `draw` reads the prompt from standard input, asks that local server for the image, and prints the PNG bytes on standard output. It does not write the prompt or the picture on the box. Wait until `http://127.0.0.1:8430/health` returns ok before giving the human the draw command. ```bash ssh -i "$HOME/chroma-mine.key" -o IdentitiesOnly=yes -p PORT root@HOST 'bash -s' << '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 checking whether PyTorch can see the GPU" 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 installing a PyTorch build this driver can use" 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 PyTorch can see the GPU" echo "PROGRESS installing Diffusers and the Chroma libraries" 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 starting Chroma and downloading the model" 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 Chroma is ready" exit 0 fi echo "PROGRESS still working: $(tail -n 1 /workspace/chroma.log 2>/dev/null || echo starting)" sleep 10 done echo "chroma did not become healthy" >&2 tail -n 40 /workspace/chroma.log >&2 exit 1 EOF ``` Do not give the human the draw command until that SSH command exits 0. ## 5. Ask which prompt style they want The picture goes from the Vast box straight to this computer over SSH. It does not go through the service. The prompt and the picture stay in memory on the box. On a Mac or Linux, one picture is a new file in the user's Pictures folder, named with the time. On Windows, and for the repeating command, the file is `picture.png` and the next picture replaces it. `draw` reads the prompt from standard input, prints the PNG bytes on standard output, and prints errors on standard error. If drawing fails, `draw` exits non-zero, so the `&&` chain stops and a half-written file is not renamed. The human does not run Python. On a Mac or Linux, the only words they change are inside the quotes. On Windows, the only words they change are the words after `echo` and before the `|`. Ask the human which they want. One picture, or keep drawing the same words until they stop. Check the operating system. Give only the matching command. On a Mac, tell them: press Command-Space, type Terminal, and press Return. On Windows, tell them to open Command Prompt, not PowerShell. `>` is byte-safe in bash and in Command Prompt. It is not byte-safe in PowerShell 5.1, which re-encodes the output as text and corrupts the PNG. On Linux, run `mkdir -p ~/Pictures` before you give the human the command. Fill in HOST and PORT. One picture, Mac or Linux: ```bash out=~/Pictures/picture-$(date +%Y%m%d-%H%M%S).png && ssh -i ~/chroma-mine.key -o IdentitiesOnly=yes -p PORT root@HOST draw <<< "a red ceramic teapot on a sunlit wooden table" > "$out.part" && mv "$out.part" "$out" && open "$out" ``` On Linux, use `xdg-open "$out"` instead of `open "$out"`. One picture, Windows Command Prompt. Command Prompt has no `<<<`, so `echo` sends the prompt. Do not put a space before `|`. `echo` would add that space to the prompt. Command Prompt treats `&`, `|`, `<`, `>`, `^`, `(`, `)`, and `%` as its own syntax, so this command cannot safely take a prompt that contains those characters. The picture is `%USERPROFILE%\Pictures\picture.png`, and the next one replaces it. To draw again, tell the human to press the up arrow, change the words if they want, and press Return. ```bat echo a red ceramic teapot on a sunlit wooden table| ssh -i %USERPROFILE%\chroma-mine.key -o IdentitiesOnly=yes -p PORT root@HOST draw > %USERPROFILE%\Pictures\picture.tmp && move /y %USERPROFILE%\Pictures\picture.tmp %USERPROFILE%\Pictures\picture.png && start %USERPROFILE%\Pictures\picture.png ``` Keep drawing, Mac or Linux. Stop with Control-C. To draw something else, stop and change the words inside the quotes. If the box is gone, the command waits 10 seconds before trying again. ```bash while true; do ssh -i ~/chroma-mine.key -o IdentitiesOnly=yes -p PORT root@HOST draw <<< "a red ceramic teapot on a sunlit wooden table" > ~/Pictures/.picture.tmp \ && mv ~/Pictures/.picture.tmp ~/Pictures/picture.png \ && open ~/Pictures/picture.png \ || sleep 10 done ``` On Linux, use `xdg-open ~/Pictures/picture.png` instead of `open ~/Pictures/picture.png`. The box stays up for the paid hours, then it shuts off.