<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>sunil srivatsa — writing</title>
    <link>https://sunilsrivatsa.com/writing/</link>
    <description>occasional long-form on infrastructure, crypto, and things i build.</description>
    <language>en-us</language>
    <lastBuildDate>Wed, 22 Jul 2026 12:00:00 GMT</lastBuildDate>
    <atom:link href="https://sunilsrivatsa.com/writing/feed.xml" rel="self" type="application/rss+xml"/>
    <item><title>0 → 200 tok/s: my local inference stack (RTX 5090 + vLLM + WSL2 + Tailscale)</title><link>https://sunilsrivatsa.com/writing/inference-stack/</link><guid isPermaLink="true">https://sunilsrivatsa.com/writing/inference-stack/</guid><pubDate>Wed, 22 Jul 2026 12:00:00 GMT</pubDate><description>Full build guide, benchmarks, and everything that didn't work — bare Windows to 200 tok/s at 131K context in under a day.</description><content:encoded><![CDATA[<p class="lede">I went from a bare Windows gaming PC to a production local inference setup in under a day — 200 tok/s single-stream at 131K context, served over WireGuard to my laptop, with every routine agent role (memory, subagents, commit messages, delegated tasks) now costing $0 per token. This is the full build: every command, the benchmarks, and — the part most writeups skip — everything that <em>didn't</em> work.</p>
<p>Names and paths are genericized; everything else is exactly what's running on my desk.</p>
<figure class="figure"><div class="native-figure"><div class="window-bar"><span>BUILD LOG — BARE WINDOWS → PRODUCTION</span><span class="window-bar__accent">ONE EVENING · JUL 21–22</span></div><div class="timeline"><div class="timeline__row"><span class="timeline__time">8:00 PM</span><span class="timeline__dot">●</span><span>BARE WINDOWS BOX · WSL NOT EVEN INSTALLED</span></div><div class="timeline__row"><span class="timeline__time">8:06 PM</span><span class="timeline__dot">●</span><span>RTX 5090 VISIBLE INSIDE UBUNTU (WSL2 + DOCKER UP)</span></div><div class="timeline__row"><span class="timeline__time">8:52 PM</span><span class="timeline__dot">●</span><span>FIRST TOKENS SERVED TO THE LAPTOP OVER THE TAILNET</span></div><div class="timeline__row"><span class="timeline__time">10:32 PM</span><span class="timeline__dot">●</span><span>BENCHMARKED: <span class="code-accent">200.3 TOK/S</span> SINGLE-STREAM @ 131K CTX</span></div><div class="timeline__row"><span class="timeline__time">11:04 PM</span><span class="timeline__dot">●</span><span>SPECULATIVE DECODING TRIED, MEASURED, REJECTED (<span class="code-accent">−34%</span>)</span></div><div class="timeline__row"><span class="timeline__time">11:26 PM</span><span class="timeline__dot">●</span><span>FINAL CONFIG LIVE: <span class="code-accent">617K-TOKEN</span> KV POOL, HEADLESS GPU</span></div><div class="timeline__row timeline__row--result"><span class="timeline__time">OVERNIGHT</span><span class="timeline__dot">●</span><span>485 REQUESTS · 0 FAILURES · ~$75 LIST-PRICE WORK FOR $0</span></div></div><div class="window-footer"><span>RTX 5090 · VLLM 0.25.1 · QWEN3.6-35B-A3B</span><span>EVERY TIMESTAMP REAL</span></div></div><figcaption><strong>FIG.01</strong>&nbsp; THE BUILD, TIMESTAMPED — BARE WINDOWS TO PRODUCTION IN ONE EVENING</figcaption></figure>
<div class="section-heading" id="s1"><span class="section-heading__number">01</span><h2>TL;DR NUMBERS</h2></div>
<div class="table-scroll"><table class="article-table article-table--spec"><thead><tr><th scope="col">Metric</th><th scope="col">Value</th></tr></thead><tbody><tr><td>Single-stream decode (median, fixed 1000-tok)</td><td><strong>200.3 tok/s</strong> — ties the fastest published single-5090 numbers</td></tr><tr><td>Warm TTFT</td><td><strong>~96 ms</strong></td></tr><tr><td>KV cache pool</td><td><strong>617,368 tokens</strong> (4.7x concurrency at full 131K depth)</td></tr><tr><td>Prefix-cache hit rate in real agent sessions</td><td><strong>up to 91%</strong></td></tr><tr><td>Effective cached prefill</td><td><strong>~3,650 tok/s</strong></td></tr><tr><td>Cost for memory/subagent/commit/task roles</td><td><strong>$0</strong></td></tr><tr><td>First ~9 h in production</td><td><strong>485/485 requests, zero failures</strong></td></tr><tr><td>Tokens served in that window</td><td><strong>13.3M in / 304K out</strong> (~$75 at the list prices these roles ran on a day earlier)</td></tr></tbody></table></div>
<div class="section-heading" id="s2"><span class="section-heading__number">02</span><h2>THE ARCHITECTURE (3 TIERS)</h2></div>
<figure class="figure"><div class="native-figure architecture"><div class="architecture__top"><div class="architecture__node"><strong>LAPTOP — AGENT CLIENT</strong><span>ALL ROLES ROUTE FROM HERE</span></div><div class="architecture__arrow"><span>JUDGMENT ROLES</span><b>− − − →</b></div><div class="architecture__node"><strong>CLOUD TIER — FRONTIER</strong><span>PLANNING + HARD CODING</span></div></div><div class="architecture__flow"><div><div class="architecture__down">↓ VOLUME ROLES</div><div class="architecture__node"><strong>TAILSCALE TUNNEL</strong><span>WIREGUARD · TAILNET-ONLY INGRESS</span></div></div><div></div><div class="architecture__down">↑ TASK-STRONG ESCALATION</div></div><div class="architecture__down" style="width:33%">↓</div><div class="architecture__gpu"><strong>GPU-BOX — RTX 5090 (HEADLESS)</strong><span>VLLM 0.25.1 · QWEN3.6-35B-A3B · 131K CTX</span><div class="stat-grid"><div class="stat-cell"><strong>200 TOK/S</strong><span>SINGLE-STREAM</span></div><div class="stat-cell"><strong>96 MS</strong><span>WARM TTFT</span></div><div class="stat-cell"><strong>617K</strong><span>KV POOL TOKENS</span></div><div class="stat-cell"><strong style="color:#9dff00">91%</strong><span>CACHE HIT RATE</span></div></div><div class="architecture__roles">MEMORY · SUBAGENTS · COMMITS · TASKS — <span class="code-accent">$0/TOKEN</span></div></div></div><figcaption><strong>FIG.02</strong>&nbsp; THE WHOLE SYSTEM: AGENT ON THE LAPTOP, JUDGMENT IN THE CLOUD, VOLUME ON THE 5090, WIREGUARD IN BETWEEN</figcaption></figure>
<p>The idea: frontier models for <strong>judgment</strong>, the local box for <strong>volume</strong>. Planning, architecture, and gnarly multi-file coding stay on a cloud model. Memory pipelines, subagent fan-out, titles, commits, and bounded delegated tasks run locally for free — and a small &quot;task adapter&quot; lets the local worker hand anything risky or failing back to the cloud model automatically, mid-task, with its work preserved.</p>
<div class="section-heading" id="s3"><span class="section-heading__number">03</span><h2>PART 1 — BUILD IT YOURSELF (~1 HOUR + A 19GB DOWNLOAD)</h2></div>
<p><strong>What you need:</strong> a Windows 11 PC with a modern NVIDIA card (numbers here are a 32GB RTX 5090; a 24GB card works with a smaller quant/context), and any laptop as the client. The model is Qwen3.6-35B-A3B (AWQ 4-bit, ~24GB).</p>
<h3 class="subheading"><span class="subheading__number">1.</span>&nbsp;WINDOWS PREP</h3>
<ol><li>Update to the latest NVIDIA driver. Never install a GPU driver <em>inside</em> Linux later — the Windows driver handles WSL passthrough automatically.</li><li>Install WSL2 + Ubuntu (admin PowerShell):</li></ol>
<div class="code-block" data-language="powershell" data-highlighted="shiki"><div class="code-block__bar"><span>POWERSHELL</span><button class="code-copy" type="button" data-copy-code aria-label="Copy powershell code">COPY</button></div><pre><code><span class="line"><span style="color:#C9D0B8">wsl </span><span style="color:#9DFF00">--</span><span style="color:#C9D0B8">install </span><span style="color:#9DFF00">-</span><span style="color:#C9D0B8">d Ubuntu</span><span style="color:#9DFF00">-</span><span style="color:#E4E8D8">24.04</span></span>
<span class="line"><span style="color:#C9D0B8">wsl </span><span style="color:#9DFF00">--</span><span style="color:#C9D0B8">version</span></span></code></pre></div>
<p><strong>The version matters.</strong> CUDA graph capture on Blackwell cards was broken under WSL2 until <strong>WSL 2.7.0</strong> (Dec 2025). Below that, vLLM silently falls back to eager mode at ~1/8th speed. If you're under 2.7.0: <code>wsl --update --pre-release</code>, then <code>wsl --shutdown</code>.</p>
<ol start="3"><li>Install Docker Desktop → Settings → Resources → WSL integration → toggle your Ubuntu distro on. Also enable &quot;start Docker Desktop when you sign in.&quot;</li><li>Sanity check — open the Ubuntu terminal:</li></ol>
<div class="code-block" data-language="bash" data-highlighted="shiki"><div class="code-block__bar"><span>BASH</span><button class="code-copy" type="button" data-copy-code aria-label="Copy bash code">COPY</button></div><pre><code><span class="line"><span style="color:#D8DEC9">nvidia-smi</span><span style="color:#7D8772"> # your GPU should appear here</span></span>
<span class="line"><span style="color:#D8DEC9">docker</span><span style="color:#E4E8D8"> --version</span></span></code></pre></div>
<p>If <code>nvidia-smi</code> shows the card inside Ubuntu, everything downstream is just typing.</p>
<h3 class="subheading"><span class="subheading__number">2.</span>&nbsp;TAILSCALE (THIS IS HOW YOUR LAPTOP REACHES THE BOX FROM ANYWHERE)</h3>
<ol><li>Install Tailscale <strong>on the Windows host — never inside WSL.</strong> Tailscale inside the WSL guest intercepts networking at boot and interferes with CUDA init. This one bites people.</li><li>Same account on both machines. In the admin console: rename the PC (mine's <code>gpu-box</code>), <strong>disable key expiry</strong> (or the box silently drops off the tailnet in a few months), and enable &quot;run unattended&quot; in the tray app.</li><li>Verify from the laptop: <code>tailscale ping gpu-box</code> → you want <code>direct</code>, not relayed.</li></ol>
<h3 class="subheading"><span class="subheading__number">3.</span>&nbsp;SCOPE THE FIREWALL TO THE TAILNET ONLY</h3>
<p>The server has no auth, so don't expose it to your whole LAN (admin PowerShell):</p>
<div class="code-block" data-language="powershell" data-highlighted="shiki"><div class="code-block__bar"><span>POWERSHELL</span><button class="code-copy" type="button" data-copy-code aria-label="Copy powershell code">COPY</button></div><pre><code><span class="line"><span style="color:#D8DEC9">New-NetFirewallRule</span><span style="color:#9DFF00"> -</span><span style="color:#C9D0B8">DisplayName </span><span style="color:#B9C4A3">"vLLM tailnet only"</span><span style="color:#9DFF00"> -</span><span style="color:#C9D0B8">Direction Inbound </span><span style="color:#9DFF00">-</span><span style="color:#C9D0B8">Protocol TCP </span><span style="color:#9DFF00">-</span><span style="color:#C9D0B8">LocalPort </span><span style="color:#E4E8D8">8000</span><span style="color:#9DFF00"> -</span><span style="color:#C9D0B8">RemoteAddress </span><span style="color:#E4E8D8">100.64</span><span style="color:#C9D0B8">.</span><span style="color:#E4E8D8">0.0</span><span style="color:#9DFF00">/</span><span style="color:#E4E8D8">10</span><span style="color:#9DFF00"> -</span><span style="color:#C9D0B8">Action Allow</span></span></code></pre></div>
<p>(100.64.0.0/10 is Tailscale's address range — only tailnet devices get through.)</p>
<h3 class="subheading"><span class="subheading__number">4.</span>&nbsp;LAUNCH VLLM</h3>
<p>One command, from either shell (no JSON args left in it — but rule of thumb: any docker command containing JSON flags must run from Ubuntu bash, because PowerShell strips embedded double quotes):</p>
<div class="code-block" data-language="bash" data-highlighted="shiki"><div class="code-block__bar"><span>BASH</span><button class="code-copy" type="button" data-copy-code aria-label="Copy bash code">COPY</button></div><pre><code><span class="line"><span style="color:#D8DEC9">docker</span><span style="color:#B9C4A3"> rm</span><span style="color:#E4E8D8"> -f</span><span style="color:#B9C4A3"> vllm</span><span style="color:#79826B">;</span><span style="color:#D8DEC9"> docker</span><span style="color:#B9C4A3"> run</span><span style="color:#E4E8D8"> -d</span><span style="color:#E4E8D8"> --name</span><span style="color:#B9C4A3"> vllm</span><span style="color:#E4E8D8"> --restart</span><span style="color:#B9C4A3"> unless-stopped</span><span style="color:#E4E8D8"> --gpus</span><span style="color:#B9C4A3"> all</span><span style="color:#E4E8D8"> --ipc=host</span><span style="color:#E4E8D8"> -e</span><span style="color:#B9C4A3"> HF_TOKEN=</span><span style="color:#9DFF00">&lt;</span><span style="color:#B9C4A3">your_hf_toke</span><span style="color:#C9D0B8">n</span><span style="color:#9DFF00">&gt;</span><span style="color:#E4E8D8"> -v</span><span style="color:#B9C4A3"> hf-cache:/root/.cache/huggingface</span><span style="color:#E4E8D8"> -v</span><span style="color:#B9C4A3"> vllm-cache:/root/.cache/vllm</span><span style="color:#E4E8D8"> -p</span><span style="color:#B9C4A3"> 8000:8000</span><span style="color:#B9C4A3"> vllm/vllm-openai:v0.25.1</span><span style="color:#B9C4A3"> QuantTrio/Qwen3.6-35B-A3B-AWQ</span><span style="color:#E4E8D8"> --dtype</span><span style="color:#B9C4A3"> bfloat16</span><span style="color:#E4E8D8"> --kv-cache-dtype</span><span style="color:#B9C4A3"> fp8</span><span style="color:#E4E8D8"> --gpu-memory-utilization</span><span style="color:#E4E8D8"> 0.92</span><span style="color:#E4E8D8"> --max-model-len</span><span style="color:#E4E8D8"> 131072</span><span style="color:#E4E8D8"> --max-num-batched-tokens</span><span style="color:#E4E8D8"> 2096</span><span style="color:#E4E8D8"> --max-num-seqs</span><span style="color:#E4E8D8"> 64</span><span style="color:#E4E8D8"> --enable-prefix-caching</span><span style="color:#E4E8D8"> --language-model-only</span><span style="color:#E4E8D8"> --reasoning-parser</span><span style="color:#B9C4A3"> qwen3</span><span style="color:#E4E8D8"> --enable-auto-tool-choice</span><span style="color:#E4E8D8"> --tool-call-parser</span><span style="color:#B9C4A3"> qwen3_coder</span><span style="color:#E4E8D8"> --trust-remote-code</span><span style="color:#E4E8D8"> --served-model-name</span><span style="color:#B9C4A3"> qwen-5090</span><span style="color:#E4E8D8"> --host</span><span style="color:#E4E8D8"> 0.0.0.0</span><span style="color:#E4E8D8"> --port</span><span style="color:#E4E8D8"> 8000</span></span></code></pre></div>
<p>The non-obvious flags (each one is a scar):</p>
<div class="table-scroll"><table class="article-table article-table--spec"><thead><tr><th scope="col">Flag</th><th scope="col">Why</th></tr></thead><tbody><tr><td><code>v0.25.1</code> (pinned, not <code>latest</code>)</td><td>Two of these flags are version-sensitive workarounds. A silent version bump can re-break startup and invalidates your compile caches. Upgrade deliberately, re-benchmark.</td></tr><tr><td><code>--max-num-batched-tokens 2096</code></td><td>This architecture mixes Mamba-style linear attention with full attention; vLLM aligns the attention page to the Mamba page (2096) and refuses to start if the batch budget is below it (vllm#36697). If a future error names a different number, use that number.</td></tr><tr><td><code>--max-num-seqs 64</code></td><td>Default 256 exceeds the available Mamba cache blocks (one per decode sequence). Also caps CUDA-graph capture at min(2×seqs, 512).</td></tr><tr><td><code>--gpu-memory-utilization 0.92</code></td><td>Safe only on a headless card (step 7). Use 0.90 if your monitor is on the GPU.</td></tr><tr><td><code>--language-model-only</code></td><td>The checkpoint is multimodal; this skips loading the vision tower + encoder cache (~1.5 GiB → KV pool) for text-only use.</td></tr><tr><td><code>--dtype bfloat16</code></td><td>float16 crashes on this quant's mixed weight dtypes.</td></tr><tr><td>named volumes</td><td>Weights + torch.compile/autotune caches survive container recreation. Warm boots ≈ 2 min.</td></tr></tbody></table></div>
<p>Watch it come up with <code>docker logs -f vllm</code>: ~19GB shard download → weight load (~18s from the volume) → a quiet few minutes of CUDA graph capture (not a hang) → the line you want to save: <code>GPU KV cache size: N tokens</code> → <code>Application startup complete</code>.</p>
<h3 class="subheading"><span class="subheading__number">5.</span>&nbsp;VERIFY END TO END</h3>
<div class="code-block" data-language="bash" data-highlighted="shiki"><div class="code-block__bar"><span>BASH</span><button class="code-copy" type="button" data-copy-code aria-label="Copy bash code">COPY</button></div><pre><code><span class="line"><span style="color:#D8DEC9">curl</span><span style="color:#B9C4A3"> http://localhost:8000/v1/models</span><span style="color:#7D8772"> # from the PC</span></span>
<span class="line"><span style="color:#D8DEC9">curl</span><span style="color:#B9C4A3"> http://gpu-box:8000/v1/models</span><span style="color:#7D8772"> # from the laptop, through the tailnet</span></span></code></pre></div>
<p>Both should return JSON naming <code>qwen-5090</code>.</p>
<h3 class="subheading"><span class="subheading__number">6.</span>&nbsp;POINT YOUR AGENT AT IT</h3>
<p>Any OpenAI-compatible client works — the base URL is <code>http://gpu-box:8000/v1</code>. I use <a href="https://github.com/can1357/oh-my-pi" target="_blank" rel="noopener">oh-my-pi</a>; its provider config (<code>~/.omp/agent/models.yml</code>):</p>
<div class="code-block" data-language="yaml" data-highlighted="shiki"><div class="code-block__bar"><span>YAML</span><button class="code-copy" type="button" data-copy-code aria-label="Copy yaml code">COPY</button></div><pre><code><span class="line"><span style="color:#E4E8D8;font-weight:bold">providers</span><span style="color:#79826B">:</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">  gpu-box</span><span style="color:#79826B">:</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">    baseUrl</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> http://gpu-box:8000/v1</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">    api</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> openai-completions</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">    apiKey</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> dummy</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">    models</span><span style="color:#79826B">:</span></span>
<span class="line"><span style="color:#79826B">      -</span><span style="color:#E4E8D8;font-weight:bold"> id</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> qwen-5090</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">        name</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> Qwen3.6 35B (5090 vLLM)</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">        reasoning</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> true</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">        contextWindow</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> 131072</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">        maxTokens</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> 32000</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">        cost</span><span style="color:#79826B">:</span><span style="color:#79826B"> {</span><span style="color:#E4E8D8;font-weight:bold"> input</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> 0</span><span style="color:#79826B">,</span><span style="color:#E4E8D8;font-weight:bold"> output</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> 0</span><span style="color:#79826B">,</span><span style="color:#E4E8D8;font-weight:bold"> cacheRead</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> 0</span><span style="color:#79826B">,</span><span style="color:#E4E8D8;font-weight:bold"> cacheWrite</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> 0</span><span style="color:#79826B"> }</span></span></code></pre></div>
<p>Then the role split (<code>~/.omp/agent/config.yml</code>) — this is where the savings actually come from:</p>
<div class="code-block" data-language="yaml" data-highlighted="shiki"><div class="code-block__bar"><span>YAML</span><button class="code-copy" type="button" data-copy-code aria-label="Copy yaml code">COPY</button></div><pre><code><span class="line"><span style="color:#E4E8D8;font-weight:bold">modelRoles</span><span style="color:#79826B">:</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">  default</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> &lt;frontier-model&gt;:xhigh</span><span style="color:#7D8772"> # judgment stays cloud</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">  plan</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> &lt;frontier-model&gt;:max</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">  advisor</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> &lt;frontier-model&gt;:max</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">  vision</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> &lt;frontier-model&gt;:xhigh</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">  task</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> &lt;frontier-model&gt;:xhigh</span><span style="color:#7D8772"> # fail-safe; the adapter in Part 4 rides on top</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">  smol</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> gpu-box/qwen-5090:low</span><span style="color:#7D8772"> # memory pipeline → local, $0</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">  tiny</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> gpu-box/qwen-5090:minimal</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">  commit</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> gpu-box/qwen-5090:low</span></span></code></pre></div>
<p>The one operating rule that keeps quality honest: <strong>when a local role produces mush, rerun on the frontier model.</strong> Never lower the bar or retry-harder locally.</p>
<figure class="figure"><div class="native-figure model-picker"><div class="window-bar"><span>ALL AVAILABLE MODELS</span><span class="window-bar__accent">SELECTED: LOCAL</span></div><div class="model-picker__row model-picker__row--local"><span>GPU-BOX/QWEN-5090</span><span>0.2S</span><span>140 T/S</span><span>131K</span><span>FREE</span></div><div class="model-picker__row"><span>CLOUD/FRONTIER:XHIGH</span><span>3.2S</span><span>35 T/S</span><span>272K</span><span>$5 / $30</span></div><div class="model-picker__row"><span>CLOUD/FRONTIER:MINI</span><span>1.8S</span><span>40 T/S</span><span>272K</span><span>$0.75 / $4.5</span></div><div class="model-picker__row"><span>CLOUD/FRONTIER:PLAN</span><span>—</span><span>—</span><span>272K</span><span>$2.5 / $15</span></div></div><figcaption><strong>FIG.03</strong>&nbsp; THE SPLIT FROM THE AGENT'S MODEL PICKER: THE LOCAL ROW READS FREE WITH 0.2S LATENCY; THE ROWS BELOW IT ARE WHY</figcaption></figure>
<h3 class="subheading"><span class="subheading__number">7.</span>&nbsp;FREE VRAM MOST PEOPLE LEAVE ON THE TABLE (OPTIONAL, WORTH IT)</h3>
<p>Two changes took my KV pool from 283K to <strong>617K tokens</strong>:</p>
<ol><li><strong>Make the GPU headless.</strong> BIOS → NB Configuration → Integrated Graphics = <strong>Force/Enabled</strong> (not Auto — Auto turns the iGPU off when it sees a discrete card; also note enabling CSM <em>hides</em> this whole menu on some boards). Plug the monitor into the motherboard. Verify with <code>nvidia-smi</code>: <code>Disp.A: Off</code>, ~0MiB idle. The Windows compositor was squatting on ~1.4GB of my card.</li><li><code>--gpu-memory-utilization 0.92</code> + <code>--language-model-only</code> (already in the command above).</li></ol>
<p>Gaming on the same box: <code>docker stop vllm</code> before, <code>docker start vllm</code> after (~90s warm reload). Don't permanently shrink the server for occasional games.</p>
<div class="section-heading" id="s4"><span class="section-heading__number">04</span><h2>PART 2 — BENCHMARKS (AND HOW TO NOT FOOL YOURSELF)</h2></div>
<p>The regression harness is ~80 lines of stdlib Python (Appendix A). Two probes:</p>
<ol><li><code>bench.py speed</code> — median of 5 fixed 1000-token generations (<code>ignore_eos</code> so every run is identical work). <strong>Result: 200.3 tok/s.</strong> Prior best published for this class of setup that I could find: 194–197 tok/s (the vLLM reference config this build started from) and a ~205 tok/s report at ~125K context. llama.cpp setups for the same model typically land far lower.</li><li><code>bench.py canary</code> — 10 runs of a prefix-cache-hot classification task with an exact expected answer. <strong>Result: 10/10.</strong> This exists because there's an open upstream bug where a specific feature combo silently degrades accuracy (Part 3) — speed benchmarks without a correctness gate are how you ship a fast wrong model.</li></ol>
<p>Method notes that matter: benchmark with <strong>zero other clients connected</strong> (watch for <code>Running: 0 reqs</code> between your probes in the server logs); restart the container before each arm so both start with cold caches; and know that vLLM's log throughput lines are 10-second window averages — idle seconds dilute them, so a &quot;slow&quot; log line during light traffic isn't a slow server.</p>
<div class="section-heading" id="s5"><span class="section-heading__number">05</span><h2>PART 2.5 — THE FIRST MORNING IN PRODUCTION</h2></div>
<p>Benchmarks are promises; telemetry is receipts. vLLM exposes Prometheus counters, so the server's actual workload is one curl away:</p>
<div class="code-block" data-language="bash" data-highlighted="shiki"><div class="code-block__bar"><span>BASH</span><button class="code-copy" type="button" data-copy-code aria-label="Copy bash code">COPY</button></div><pre><code><span class="line"><span style="color:#D8DEC9">curl</span><span style="color:#E4E8D8"> -s</span><span style="color:#B9C4A3"> http://gpu-box:8000/metrics</span><span style="color:#9DFF00"> |</span><span style="color:#D8DEC9"> grep</span><span style="color:#E4E8D8"> -E</span><span style="color:#B9C4A3"> "prompt_tokens_total|generation_tokens_total|request_success"</span></span></code></pre></div>
<p>Mine, ~9 hours after the final relaunch — overnight plus one morning of real agent traffic:</p>
<div class="table-scroll"><table class="article-table article-table--spec"><thead><tr><th scope="col">Counter</th><th scope="col">Value</th></tr></thead><tbody><tr><td>Successful requests</td><td><strong>485</strong> (412 stopped naturally, 73 hit max length)</td></tr><tr><td>Failed / aborted / repetition-killed</td><td><strong>0 / 0 / 0</strong></td></tr><tr><td>Prompt tokens processed</td><td><strong>13.27M</strong></td></tr><tr><td>Generation tokens</td><td><strong>304K</strong></td></tr></tbody></table></div>
<p>Three things worth noticing in there:</p>
<ol><li><strong>The 44:1 input/output ratio.</strong> Agent workloads are prefill-dominated — the transcript gets resent every turn. This is why prefix caching (91% hits) and prefill throughput matter more than decode speed for how the thing <em>feels</em>, and why the 200 tok/s headline is honestly the least important number in this README.</li><li><strong>Value math, framed honestly:</strong> at the list prices these roles ran on a day earlier ($5/M in, $30/M out for the bulk of the traffic), that's <strong>~$75 of work for $0</strong> in the first nine hours — most of it overnight. Two caveats so you don't over-quote me: a real cloud bill would come in under list (prompt-cache discounts exist there too), and free tokens change behavior — you run things you'd never have paid for, which is half the point. &quot;Cost avoided at current usage&quot; is the defensible phrase. &quot;Saved $75&quot; isn't quite.</li><li><strong>Counters reset on container restart.</strong> Snapshot them when you deploy; diff at review time.</li></ol>
<p>And the live workload comparison on the roles that moved, straight from the agent's own rolling stats: first token in <strong>0.2s vs 3.2s</strong> on the cloud model (16x), <strong>140 vs 35 tok/s</strong> decode (4x).</p>
<p>Pretty-printed version for screenshots:</p>
<div class="code-block" data-language="bash" data-highlighted="shiki"><div class="code-block__bar"><span>BASH</span><button class="code-copy" type="button" data-copy-code aria-label="Copy bash code">COPY</button></div><pre><code><span class="line"><span style="color:#D8DEC9">curl</span><span style="color:#E4E8D8"> -s</span><span style="color:#B9C4A3"> http://gpu-box:8000/metrics</span><span style="color:#9DFF00"> |</span><span style="color:#D8DEC9"> awk</span><span style="color:#B9C4A3"> '/^vllm:prompt_tokens_total/{p=$2}/^vllm:generation_tokens_total/{g=$2}/^vllm:request_success_total.*stop/{s=$2}/^vllm:request_success_total.*length/{l=$2}/^vllm:request_success_total.*error/{e=$2}END{printf "requests: %d (errors: %d)\ntokens in: %.1fM\ntokens out: %.0fK\nvalue at $5/$30 list: $%.0f\n", s+l, e, p/1e6, g/1e3, p/1e6*5+g/1e6*30}'</span></span></code></pre></div>
<figure class="figure"><div class="native-figure"><div class="window-bar"><span>UBUNTU — GPU-BOX:8000/METRICS</span><span class="window-bar__accent">FIRST 9 HOURS · PRODUCTION</span></div><div class="telemetry"><div class="telemetry__cell"><div class="telemetry__label">REQUESTS SERVED</div><div class="telemetry__value">485</div></div><div class="telemetry__cell"><div class="telemetry__label">ERRORS · ABORTS · LOOPS</div><div class="telemetry__value telemetry__value--accent">0 · 0 · 0</div></div><div class="telemetry__cell"><div class="telemetry__label">TOKENS IN / OUT</div><div class="telemetry__value">13.3M / 304K</div></div><div class="telemetry__cell"><div class="telemetry__label">VALUE AT $5/$30 LIST → SPENT</div><div class="telemetry__value">~$75 → <span class="code-accent">$0</span></div></div></div><div class="window-footer"><span>VLLM 0.25.1 · QWEN-5090 · RTX 5090</span><span>OVERNIGHT + ONE MORNING OF REAL TRAFFIC</span></div></div><figcaption><strong>FIG.04</strong>&nbsp; THE FOUR-LINE RECEIPT: REQUESTS, FAILURE COUNT, TOKEN VOLUME, LIST-PRICE VALUE</figcaption></figure>
<div class="section-heading" id="s6"><span class="section-heading__number">06</span><h2>PART 3 — EVERYTHING THAT DIDN'T WORK (READ THIS PART)</h2></div>
<p><strong>Speculative decoding (MTP) made it slower.</strong> The checkpoint ships an MTP draft head; enabling it (<code>--speculative-config '{&quot;method&quot;: &quot;mtp&quot;, &quot;num_speculative_tokens&quot;: 2}'</code>, bash only) showed 75–85% draft acceptance — and <strong>34% lower single-stream throughput</strong> (132 vs 200 tok/s). Why: on a fine-grained MoE (256 experts, 3B active), drafted tokens route to mostly <em>different</em> experts, so verification pays nearly full bandwidth anyway, while you fund the drafter and lose full CUDA graphs. Acceptance rate is not a speedup — benchmark it. Bonus reason to skip: an open vLLM issue reports ~20% silent accuracy loss from MTP + prefix caching on this exact model family. Aggregate <em>concurrent</em> throughput did go up (232–276 tok/s across batched requests) — which is exactly how I fooled myself for an hour before running a clean single-stream A/B.</p>
<p><strong>Zero-error quality gates at temperature 1.0 are coin flips.</strong> My first eval gated a config on &quot;zero tool errors in one episode.&quot; Across two nights, every arm's error count flipped between 0 and 2–3 on near-identical runs. Lessons that stuck: gate <strong>outcomes</strong> absolutely (tests pass), gate <strong>process</strong> relatively against a measured control arm, use n≥5, and pre-register the decision rule <em>before</em> seeing numbers.</p>
<p><strong>PowerShell eats JSON arguments.</strong> Any docker flag containing JSON (<code>{&quot;method&quot;: &quot;mtp&quot;...}</code>) gets its inner quotes stripped by PowerShell's native-argument handling. Symptom: <code>Value {method:mtp} cannot be converted</code>. Fix: run JSON-bearing commands from the Ubuntu terminal. Same Docker engine either way.</p>
<p><strong>Small local models don't reliably follow &quot;escalate if X&quot; prompts — until you force a token.</strong> See Part 4.</p>
<div class="section-heading" id="s7"><span class="section-heading__number">07</span><h2>PART 4 — THE TASK ADAPTER (LOCAL-FIRST WITH AUTOMATIC ESCALATION)</h2></div>
<p>Delegated tasks run on the local model by default and hand off to the frontier model when they should. Two markdown files, no source patches (my agent framework discovers user-defined agents that override bundled ones; the frontier fail-safe stays in config so deleting the files is a clean rollback):</p>
<ol><li><code>task</code> — local worker (qwen, low thinking). Classifies the assignment <em>before touching anything</em>; runs bounded mechanical work end to end; escalates on security-sensitive subject matter, or at runtime after two tool/edit errors or a failed fix.</li><li><code>task-strong</code> — blocking frontier fallback. Gets a self-contained handoff, finishes the job, returns the result inline.</li></ol>
<p>The escalation path, end to end — this is the trace the security fixture produces:</p>
<figure class="figure"><div class="native-figure trace"><div class="window-bar"><span>TASK ESCALATION — SECURITY FIXTURE, TRACED</span><span class="window-bar__accent">CLASSIFY PROTOCOL</span></div><div class="trace__row"><span class="trace__step">1</span><span><strong>MAIN AGENT</strong> → <strong>TASK</strong> · QWEN (LOCAL, LOW)<br><small>DELEGATE: “FIX THE AUTHORIZATION BUG”</small></span></div><div class="trace__row trace__row--active"><span class="trace__step">2</span><span>CLASSIFY: ESCALATE — SECURITY-SENSITIVE SUBJECT<br><small>FIRST VISIBLE OUTPUT · BEFORE ANY TOOL CALL</small></span></div><div class="trace__row"><span class="trace__step">3</span><span><strong>TASK</strong> → <strong>TASK-STRONG</strong> · FRONTIER (XHIGH)<br><small>BLOCKING HANDOFF · ZERO LOCAL EDITS MADE</small></span></div><div class="trace__row"><span class="trace__step">4</span><span><strong>TASK-STRONG</strong> · INSPECT · FIX · VERIFY — <span class="code-accent">4/4 TESTS PASS</span></span></div><div class="trace__row"><span class="trace__step">5</span><span><strong>TASK-STRONG</strong> → <strong>MAIN AGENT</strong><br><small>RESULT RETURNED INLINE · WORK PRESERVED</small></span></div><div class="window-footer"><span>LOCAL: QWEN-5090 (LOW) · FALLBACK: FRONTIER (XHIGH)</span><span>ROLLBACK = DELETE TWO FILES</span></div></div><figcaption><strong>FIG.05</strong>&nbsp; CLASSIFY PROTOCOL — THE SECURITY FIXTURE, TRACED END TO END</figcaption></figure>
<p><strong>The part that failed first:</strong> with the escalation rules written as ordinary prompt instructions, the local worker <em>never</em> auto-escalated a security-sensitive fixture — at any thinking level. It just confidently did the auth work. The fix that flipped it to passing: a <strong>mandatory first-output classification token</strong> — the worker's first visible text must be <code>CLASSIFY: SAFE — &lt;reason&gt;</code> or <code>CLASSIFY: ESCALATE — &lt;reason&gt;</code>, no tool calls before it, classify the <em>subject matter</em> not the simplicity of the fix. Re-tested: bounded work stays local (tests pass), a forced-local authorization task now emits <code>CLASSIFY: ESCALATE</code> and calls the strong worker before touching a single file. If you build any local-first routing, steal this: small models follow <em>output contracts</em> far more reliably than <em>conditional instructions</em>. Both agent files are in Appendix C.</p>
<div class="section-heading" id="s8"><span class="section-heading__number">08</span><h2>APPENDICES</h2></div>
<h3 class="subheading"><span class="subheading__number">A.</span>&nbsp;BENCH.PY</h3>
<div class="code-block" data-language="python" data-highlighted="shiki"><div class="code-block__bar"><span>PYTHON</span><button class="code-copy" type="button" data-copy-code aria-label="Copy python code">COPY</button></div><pre><code><span class="line"><span style="color:#7D8772">#!/usr/bin/env python3</span></span>
<span class="line"><span style="color:#9DFF00">import</span><span style="color:#C9D0B8"> json</span><span style="color:#79826B">,</span><span style="color:#C9D0B8"> sys</span><span style="color:#79826B">,</span><span style="color:#C9D0B8"> time</span><span style="color:#79826B">,</span><span style="color:#C9D0B8"> statistics</span><span style="color:#79826B">,</span><span style="color:#C9D0B8"> urllib</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">request</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E4E8D8">URL</span><span style="color:#9DFF00"> =</span><span style="color:#B9C4A3"> "http://gpu-box:8000/v1/chat/completions"</span></span>
<span class="line"></span>
<span class="line"><span style="color:#9DFF00">def</span><span style="color:#D8DEC9"> call</span><span style="color:#79826B">(</span><span style="color:#A4AC97">messages</span><span style="color:#79826B">,</span><span style="color:#A4AC97"> max_tokens</span><span style="color:#79826B">,</span><span style="color:#A4AC97"> temperature</span><span style="color:#79826B">,</span><span style="color:#A4AC97"> extra</span><span style="color:#9DFF00">=</span><span style="color:#E4E8D8">None</span><span style="color:#79826B">):</span></span>
<span class="line"><span style="color:#C9D0B8">    body </span><span style="color:#9DFF00">=</span><span style="color:#79826B"> {</span><span style="color:#B9C4A3">"model"</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> "qwen-5090"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3"> "messages"</span><span style="color:#79826B">:</span><span style="color:#C9D0B8"> messages</span><span style="color:#79826B">,</span></span>
<span class="line"><span style="color:#B9C4A3">            "max_tokens"</span><span style="color:#79826B">:</span><span style="color:#C9D0B8"> max_tokens</span><span style="color:#79826B">,</span><span style="color:#B9C4A3"> "temperature"</span><span style="color:#79826B">:</span><span style="color:#C9D0B8"> temperature</span><span style="color:#79826B">}</span></span>
<span class="line"><span style="color:#9DFF00">    if</span><span style="color:#C9D0B8"> extra</span><span style="color:#79826B">:</span><span style="color:#C9D0B8"> body</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">update</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">extra</span><span style="color:#79826B">)</span></span>
<span class="line"><span style="color:#C9D0B8">    req </span><span style="color:#9DFF00">=</span><span style="color:#C9D0B8"> urllib</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">request</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">Request</span><span style="color:#79826B">(</span><span style="color:#E4E8D8">URL</span><span style="color:#79826B">,</span><span style="color:#A4AC97"> data</span><span style="color:#9DFF00">=</span><span style="color:#C9D0B8">json</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">dumps</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">body</span><span style="color:#79826B">).</span><span style="color:#C9D0B8">encode</span><span style="color:#79826B">(),</span></span>
<span class="line"><span style="color:#A4AC97">                                 headers</span><span style="color:#9DFF00">=</span><span style="color:#79826B">{</span><span style="color:#B9C4A3">"Content-Type"</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> "application/json"</span><span style="color:#79826B">})</span></span>
<span class="line"><span style="color:#C9D0B8">    t0 </span><span style="color:#9DFF00">=</span><span style="color:#C9D0B8"> time</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">time</span><span style="color:#79826B">()</span></span>
<span class="line"><span style="color:#C9D0B8">    r </span><span style="color:#9DFF00">=</span><span style="color:#C9D0B8"> json</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">load</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">urllib</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">request</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">urlopen</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">req</span><span style="color:#79826B">,</span><span style="color:#A4AC97"> timeout</span><span style="color:#9DFF00">=</span><span style="color:#E4E8D8">600</span><span style="color:#79826B">))</span></span>
<span class="line"><span style="color:#9DFF00">    return</span><span style="color:#C9D0B8"> r</span><span style="color:#79826B">,</span><span style="color:#C9D0B8"> time</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">time</span><span style="color:#79826B">()</span><span style="color:#9DFF00"> -</span><span style="color:#C9D0B8"> t0</span></span>
<span class="line"></span>
<span class="line"><span style="color:#9DFF00">def</span><span style="color:#D8DEC9"> speed</span><span style="color:#79826B">(</span><span style="color:#A4AC97">n</span><span style="color:#9DFF00">=</span><span style="color:#E4E8D8">5</span><span style="color:#79826B">):</span></span>
<span class="line"><span style="color:#C9D0B8">    msgs </span><span style="color:#9DFF00">=</span><span style="color:#79826B"> [{</span><span style="color:#B9C4A3">"role"</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> "user"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3"> "content"</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> "Write a long, detailed essay on the history of computing."</span><span style="color:#79826B">}]</span></span>
<span class="line"><span style="color:#C9D0B8">    call</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">msgs</span><span style="color:#79826B">,</span><span style="color:#E4E8D8"> 200</span><span style="color:#79826B">,</span><span style="color:#E4E8D8"> 1.0</span><span style="color:#79826B">,</span><span style="color:#79826B"> {</span><span style="color:#B9C4A3">"ignore_eos"</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> True</span><span style="color:#79826B">})</span><span style="color:#7D8772"> # warmup (JIT, caches)</span></span>
<span class="line"><span style="color:#C9D0B8">    rates </span><span style="color:#9DFF00">=</span><span style="color:#79826B"> []</span></span>
<span class="line"><span style="color:#9DFF00">    for</span><span style="color:#C9D0B8"> i </span><span style="color:#9DFF00">in</span><span style="color:#9DFF00"> range</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">n</span><span style="color:#79826B">):</span></span>
<span class="line"><span style="color:#C9D0B8">        r</span><span style="color:#79826B">,</span><span style="color:#C9D0B8"> dt </span><span style="color:#9DFF00">=</span><span style="color:#C9D0B8"> call</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">msgs</span><span style="color:#79826B">,</span><span style="color:#E4E8D8"> 1000</span><span style="color:#79826B">,</span><span style="color:#E4E8D8"> 1.0</span><span style="color:#79826B">,</span><span style="color:#79826B"> {</span><span style="color:#B9C4A3">"ignore_eos"</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> True</span><span style="color:#79826B">})</span></span>
<span class="line"><span style="color:#C9D0B8">        toks </span><span style="color:#9DFF00">=</span><span style="color:#C9D0B8"> r</span><span style="color:#79826B">[</span><span style="color:#B9C4A3">"usage"</span><span style="color:#79826B">][</span><span style="color:#B9C4A3">"completion_tokens"</span><span style="color:#79826B">]</span></span>
<span class="line"><span style="color:#C9D0B8">        rates</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">append</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">toks </span><span style="color:#9DFF00">/</span><span style="color:#C9D0B8"> dt</span><span style="color:#79826B">)</span></span>
<span class="line"><span style="color:#9DFF00">        print</span><span style="color:#79826B">(</span><span style="color:#9DFF00">f</span><span style="color:#B9C4A3">"run </span><span style="color:#C9D0B8">{i</span><span style="color:#9DFF00">+</span><span style="color:#E4E8D8">1</span><span style="color:#C9D0B8">}</span><span style="color:#B9C4A3">: </span><span style="color:#C9D0B8">{toks}</span><span style="color:#B9C4A3"> tok in </span><span style="color:#C9D0B8">{dt</span><span style="color:#9DFF00">:.1f</span><span style="color:#C9D0B8">}</span><span style="color:#B9C4A3">s = </span><span style="color:#C9D0B8">{toks</span><span style="color:#9DFF00">/</span><span style="color:#C9D0B8">dt</span><span style="color:#9DFF00">:.1f</span><span style="color:#C9D0B8">}</span><span style="color:#B9C4A3"> tok/s"</span><span style="color:#79826B">)</span></span>
<span class="line"><span style="color:#9DFF00">    print</span><span style="color:#79826B">(</span><span style="color:#9DFF00">f</span><span style="color:#B9C4A3">"MEDIAN DECODE: </span><span style="color:#C9D0B8">{statistics</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">median</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">rates</span><span style="color:#79826B">)</span><span style="color:#9DFF00">:.1f</span><span style="color:#C9D0B8">}</span><span style="color:#B9C4A3"> tok/s"</span><span style="color:#79826B">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#E4E8D8">FACTS</span><span style="color:#9DFF00"> =</span><span style="color:#B9C4A3"> """D1: User's name is Alex.</span></span>
<span class="line"><span style="color:#B9C4A3">D2: Works at Acme on the atlas project.</span></span>
<span class="line"><span style="color:#B9C4A3">D3: Timezone is America/New_York.</span></span>
<span class="line"><span style="color:#B9C4A3">D4: Severely allergic to peanuts.</span></span>
<span class="line"><span style="color:#B9C4A3">D5: Primary language is Python.</span></span>
<span class="line"><span style="color:#B9C4A3">D6: Runs a local model server called qwen-5090.</span></span>
<span class="line"><span style="color:#B9C4A3">T1: Has a dentist appointment tomorrow at 3pm.</span></span>
<span class="line"><span style="color:#B9C4A3">T2: It is currently raining.</span></span>
<span class="line"><span style="color:#B9C4A3">T3: Saw a transient 502 error an hour ago.</span></span>
<span class="line"><span style="color:#B9C4A3">S1: API key is sk_live_EXAMPLEFAKE000000000."""</span></span>
<span class="line"></span>
<span class="line"><span style="color:#9DFF00">def</span><span style="color:#D8DEC9"> canary</span><span style="color:#79826B">(</span><span style="color:#A4AC97">n</span><span style="color:#9DFF00">=</span><span style="color:#E4E8D8">10</span><span style="color:#79826B">):</span></span>
<span class="line"><span style="color:#C9D0B8">    filler </span><span style="color:#9DFF00">=</span><span style="color:#79826B"> (</span><span style="color:#B9C4A3">"Retention policy reference: durable facts are stable user attributes, "</span></span>
<span class="line"><span style="color:#B9C4A3">              "preferences, and long-lived project context; transient facts are time-bound "</span></span>
<span class="line"><span style="color:#B9C4A3">              "events, weather, or one-off errors; secrets and credentials must never be "</span></span>
<span class="line"><span style="color:#B9C4A3">              "retained under any circumstances. "</span><span style="color:#79826B">)</span><span style="color:#9DFF00"> *</span><span style="color:#E4E8D8"> 60</span></span>
<span class="line"><span style="color:#C9D0B8">    system </span><span style="color:#9DFF00">=</span><span style="color:#79826B"> (</span><span style="color:#B9C4A3">"You are a memory retention classifier.\n"</span><span style="color:#9DFF00"> +</span><span style="color:#C9D0B8"> filler </span><span style="color:#9DFF00">+</span></span>
<span class="line"><span style="color:#B9C4A3">              "\nGiven a list of facts, output ONLY the IDs of facts that should be "</span></span>
<span class="line"><span style="color:#B9C4A3">              "retained long-term, as a comma-separated list, nothing else."</span><span style="color:#79826B">)</span></span>
<span class="line"><span style="color:#C9D0B8">    expected </span><span style="color:#9DFF00">=</span><span style="color:#79826B"> {</span><span style="color:#B9C4A3">"D1"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3">"D2"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3">"D3"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3">"D4"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3">"D5"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3">"D6"</span><span style="color:#79826B">}</span></span>
<span class="line"><span style="color:#C9D0B8">    passes </span><span style="color:#9DFF00">=</span><span style="color:#E4E8D8"> 0</span></span>
<span class="line"><span style="color:#9DFF00">    for</span><span style="color:#C9D0B8"> i </span><span style="color:#9DFF00">in</span><span style="color:#9DFF00"> range</span><span style="color:#79826B">(</span><span style="color:#C9D0B8">n</span><span style="color:#79826B">):</span></span>
<span class="line"><span style="color:#C9D0B8">        r</span><span style="color:#79826B">,</span><span style="color:#C9D0B8"> dt </span><span style="color:#9DFF00">=</span><span style="color:#C9D0B8"> call</span><span style="color:#79826B">([{</span><span style="color:#B9C4A3">"role"</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> "system"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3"> "content"</span><span style="color:#79826B">:</span><span style="color:#C9D0B8"> system</span><span style="color:#79826B">},</span></span>
<span class="line"><span style="color:#79826B">                      {</span><span style="color:#B9C4A3">"role"</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> "user"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3"> "content"</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> FACTS</span><span style="color:#79826B">}],</span><span style="color:#E4E8D8"> 2000</span><span style="color:#79826B">,</span><span style="color:#E4E8D8"> 0.0</span><span style="color:#79826B">)</span></span>
<span class="line"><span style="color:#C9D0B8">        text </span><span style="color:#9DFF00">=</span><span style="color:#79826B"> (</span><span style="color:#C9D0B8">r</span><span style="color:#79826B">[</span><span style="color:#B9C4A3">"choices"</span><span style="color:#79826B">][</span><span style="color:#E4E8D8">0</span><span style="color:#79826B">][</span><span style="color:#B9C4A3">"message"</span><span style="color:#79826B">][</span><span style="color:#B9C4A3">"content"</span><span style="color:#79826B">]</span><span style="color:#9DFF00"> or</span><span style="color:#B9C4A3"> ""</span><span style="color:#79826B">).</span><span style="color:#C9D0B8">strip</span><span style="color:#79826B">()</span></span>
<span class="line"><span style="color:#C9D0B8">        ids </span><span style="color:#9DFF00">=</span><span style="color:#79826B"> {</span><span style="color:#C9D0B8">t</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">strip</span><span style="color:#79826B">()</span><span style="color:#9DFF00"> for</span><span style="color:#C9D0B8"> t </span><span style="color:#9DFF00">in</span><span style="color:#C9D0B8"> text</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">replace</span><span style="color:#79826B">(</span><span style="color:#B9C4A3">"\n"</span><span style="color:#79826B">,</span><span style="color:#B9C4A3"> ","</span><span style="color:#79826B">).</span><span style="color:#C9D0B8">split</span><span style="color:#79826B">(</span><span style="color:#B9C4A3">","</span><span style="color:#79826B">)</span><span style="color:#9DFF00"> if</span><span style="color:#C9D0B8"> t</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">strip</span><span style="color:#79826B">()}</span></span>
<span class="line"><span style="color:#C9D0B8">        ok </span><span style="color:#9DFF00">=</span><span style="color:#C9D0B8"> ids </span><span style="color:#9DFF00">==</span><span style="color:#C9D0B8"> expected</span></span>
<span class="line"><span style="color:#C9D0B8">        passes </span><span style="color:#9DFF00">+=</span><span style="color:#C9D0B8"> ok</span></span>
<span class="line"><span style="color:#9DFF00">        print</span><span style="color:#79826B">(</span><span style="color:#9DFF00">f</span><span style="color:#B9C4A3">"run </span><span style="color:#C9D0B8">{i</span><span style="color:#9DFF00">+</span><span style="color:#E4E8D8">1</span><span style="color:#C9D0B8">}</span><span style="color:#B9C4A3">: </span><span style="color:#C9D0B8">{</span><span style="color:#B9C4A3">'PASS'</span><span style="color:#9DFF00"> if</span><span style="color:#C9D0B8"> ok </span><span style="color:#9DFF00">else</span><span style="color:#B9C4A3"> 'FAIL -&gt; '</span><span style="color:#9DFF00"> +</span><span style="color:#C9D0B8"> text</span><span style="color:#79826B">[:</span><span style="color:#E4E8D8">120</span><span style="color:#79826B">]</span><span style="color:#C9D0B8">}</span><span style="color:#B9C4A3"> (</span><span style="color:#C9D0B8">{dt</span><span style="color:#9DFF00">:.1f</span><span style="color:#C9D0B8">}</span><span style="color:#B9C4A3">s)"</span><span style="color:#79826B">)</span></span>
<span class="line"><span style="color:#9DFF00">    print</span><span style="color:#79826B">(</span><span style="color:#9DFF00">f</span><span style="color:#B9C4A3">"CANARY: </span><span style="color:#C9D0B8">{passes}</span><span style="color:#B9C4A3">/</span><span style="color:#C9D0B8">{n}</span><span style="color:#B9C4A3"> passed"</span><span style="color:#79826B">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#9DFF00">if</span><span style="color:#C9D0B8"> __name__ </span><span style="color:#9DFF00">==</span><span style="color:#B9C4A3"> "__main__"</span><span style="color:#79826B">:</span></span>
<span class="line"><span style="color:#79826B">    {</span><span style="color:#B9C4A3">"speed"</span><span style="color:#79826B">:</span><span style="color:#C9D0B8"> speed</span><span style="color:#79826B">,</span><span style="color:#B9C4A3"> "canary"</span><span style="color:#79826B">:</span><span style="color:#C9D0B8"> canary</span><span style="color:#79826B">}[</span><span style="color:#C9D0B8">sys</span><span style="color:#79826B">.</span><span style="color:#C9D0B8">argv</span><span style="color:#79826B">[</span><span style="color:#E4E8D8">1</span><span style="color:#79826B">]]()</span></span></code></pre></div>
<p>(The <code>sk_live_...</code> string is a deliberately fake fixture for testing that the memory classifier refuses to retain secrets. It is not a real key.)</p>
<h3 class="subheading"><span class="subheading__number">B.</span>&nbsp;VERIFICATION ONE-LINERS</h3>
<div class="code-block" data-language="bash" data-highlighted="shiki"><div class="code-block__bar"><span>BASH</span><button class="code-copy" type="button" data-copy-code aria-label="Copy bash code">COPY</button></div><pre><code><span class="line"><span style="color:#D8DEC9">tailscale</span><span style="color:#B9C4A3"> ping</span><span style="color:#B9C4A3"> gpu-box</span><span style="color:#7D8772"> # want: direct</span></span>
<span class="line"><span style="color:#D8DEC9">curl</span><span style="color:#B9C4A3"> http://gpu-box:8000/v1/models</span><span style="color:#7D8772"> # want: qwen-5090</span></span>
<span class="line"><span style="color:#D8DEC9">nvidia-smi</span><span style="color:#7D8772"> # want: Disp.A Off, ~0MiB idle</span></span>
<span class="line"><span style="color:#D8DEC9">docker</span><span style="color:#B9C4A3"> logs</span><span style="color:#B9C4A3"> vllm</span><span style="color:#9DFF00"> 2&gt;&amp;1</span><span style="color:#9DFF00"> |</span><span style="color:#D8DEC9"> grep</span><span style="color:#E4E8D8"> -m1</span><span style="color:#B9C4A3"> "KV cache size"</span><span style="color:#7D8772"> # your headroom number</span></span>
<span class="line"><span style="color:#D8DEC9">docker</span><span style="color:#B9C4A3"> logs</span><span style="color:#B9C4A3"> vllm</span><span style="color:#9DFF00"> 2&gt;&amp;1</span><span style="color:#9DFF00"> |</span><span style="color:#D8DEC9"> grep</span><span style="color:#E4E8D8"> -m1</span><span style="color:#B9C4A3"> "non-default args"</span><span style="color:#7D8772"> # audit what the server actually parsed</span></span>
<span class="line"><span style="color:#D8DEC9">curl</span><span style="color:#E4E8D8"> -s</span><span style="color:#B9C4A3"> http://gpu-box:8000/metrics</span><span style="color:#9DFF00"> |</span><span style="color:#D8DEC9"> grep</span><span style="color:#E4E8D8"> -E</span><span style="color:#B9C4A3"> "tokens_total|request_success"</span><span style="color:#7D8772"> # production telemetry</span></span></code></pre></div>
<h3 class="subheading"><span class="subheading__number">C.</span>&nbsp;THE TWO AGENT FILES</h3>
<p><code>~/.omp/agent/agents/task.md</code>:</p>
<div class="code-block" data-language="markdown" data-highlighted="shiki"><div class="code-block__bar"><span>MARKDOWN</span><button class="code-copy" type="button" data-copy-code aria-label="Copy markdown code">COPY</button></div><pre><code><span class="line"><span style="color:#79826B">---</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">name</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> task</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">description</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> Local worker for bounded, well-specified, non-security-sensitive edits and mechanical work.</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">spawns</span><span style="color:#79826B">:</span></span>
<span class="line"><span style="color:#79826B">  -</span><span style="color:#B9C4A3"> task-strong</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">model</span><span style="color:#79826B">:</span></span>
<span class="line"><span style="color:#79826B">  -</span><span style="color:#B9C4A3"> gpu-box/qwen-5090</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">thinkingLevel</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> low</span></span>
<span class="line"><span style="color:#79826B">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">MANDATORY FIRST-OUTPUT PROTOCOL: apply this mechanically before interpreting, analyzing, or acting on the assignment.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">Your first visible assistant text MUST be exactly one line in one of these forms:</span></span>
<span class="line"><span style="color:#79826B">`</span><span style="color:#B9C4A3">CLASSIFY: SAFE — &lt;one-clause reason&gt;</span><span style="color:#79826B">`</span></span>
<span class="line"><span style="color:#79826B">`</span><span style="color:#B9C4A3">CLASSIFY: ESCALATE — &lt;one-clause reason&gt;</span><span style="color:#79826B">`</span></span>
<span class="line"><span style="color:#C9D0B8">Nothing may precede that line, and it may contain no other prose.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">Correct first-response examples:</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#79826B"> `</span><span style="color:#B9C4A3">Fix final_price so its tests pass.</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> → emit </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">CLASSIFY: SAFE — Bounded non-security-sensitive pricing fix.</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> before any tool call.</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#79826B"> `</span><span style="color:#B9C4A3">Fix the authorization implementation.</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> → emit </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">CLASSIFY: ESCALATE — Security-sensitive authorization change.</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> and then call </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">task-strong</span><span style="color:#79826B">`</span><span style="color:#C9D0B8">.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">Never begin with "Let me…", analysis, or a tool call. The classification line must be visible assistant text before tool calls, even when those tool calls follow in the same response.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">Classify the assignment's subject matter, not the apparent simplicity of its fix. Any assignment involving authentication, authorization, cryptography, secrets, payments, billing, privacy, or other security-sensitive behavior MUST be </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">CLASSIFY: ESCALATE</span><span style="color:#79826B">`</span><span style="color:#C9D0B8">, including bug fixes, test-driven changes, and one-line edits. No exception permits security-sensitive code to stay local.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">Choose </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">CLASSIFY: ESCALATE</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> when any pre-edit condition below holds. Immediately after the classification line, call the task tool with agent </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">task-strong</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> as your first and only tool; do not read, search, run tests, edit, or otherwise inspect locally. Choose </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">CLASSIFY: SAFE</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> only when no condition holds, then continue locally after the classification line.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">Other pre-edit escalation conditions:</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#C9D0B8"> destructive operations, persistent-data or schema migrations, data-loss risk, or production incident handling</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#C9D0B8"> concurrency, distributed coordination, locking, cache consistency, or transaction-boundary changes</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#C9D0B8"> cross-subsystem architecture or public API, protocol, or schema design</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#C9D0B8"> vision or image reasoning</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#C9D0B8"> materially ambiguous requirements with competing product or architecture tradeoffs</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">During local work, escalate when any condition occurs:</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#C9D0B8"> two tool invocation, schema, edit-anchor, or path errors</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#C9D0B8"> verification still fails after one targeted correction to your implementation</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#C9D0B8"> you cannot state the root cause and key invariant before the next edit</span></span>
<span class="line"><span style="color:#79826B">-</span><span style="color:#C9D0B8"> required context is missing or confidence in correctness is low</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">To escalate, call the task tool exactly once with agent </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">task-strong</span><span style="color:#79826B">`</span><span style="color:#C9D0B8">. Give it a complete, self-contained handoff with the original assignment, repository cwd, findings, files already changed, exact failures, and remaining acceptance criteria. Make no more edits after deciding to escalate, wait for the blocking result, then return the strong worker's result. Never spawn another general </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">task</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> or a second </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">task-strong</span><span style="color:#79826B">`</span><span style="color:#C9D0B8">.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">For work retained locally, prefer grep/glob, narrow reads, surgical edits to existing files, and the exact relevant verification. Do not create documentation unless requested. Stop when acceptance passes. Return only changed files, verification evidence, and concrete residual risk.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">OUTPUT CONTRACT: ALWAYS PRINT </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">CLASSIFY: SAFE — &lt;reason&gt;</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> or </span><span style="color:#79826B">`</span><span style="color:#B9C4A3">CLASSIFY: ESCALATE — &lt;reason&gt;</span><span style="color:#79826B">`</span><span style="color:#C9D0B8"> as the first visible text of your first response, including safe work. A thinking-only classification is a protocol failure. NO TOOL CALL is permitted until this line is printed.</span></span></code></pre></div>
<p><code>~/.omp/agent/agents/task-strong.md</code>:</p>
<div class="code-block" data-language="markdown" data-highlighted="shiki"><div class="code-block__bar"><span>MARKDOWN</span><button class="code-copy" type="button" data-copy-code aria-label="Copy markdown code">COPY</button></div><pre><code><span class="line"><span style="color:#79826B">---</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">name</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> task-strong</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">description</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> Call directly for security-sensitive, destructive, cross-cutting, ambiguous, architectural, or locally failed work.</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">blocking</span><span style="color:#79826B">:</span><span style="color:#E4E8D8"> true</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">model</span><span style="color:#79826B">:</span></span>
<span class="line"><span style="color:#79826B">  -</span><span style="color:#B9C4A3"> "@task"</span></span>
<span class="line"><span style="color:#E4E8D8;font-weight:bold">thinkingLevel</span><span style="color:#79826B">:</span><span style="color:#B9C4A3"> xhigh</span></span>
<span class="line"><span style="color:#79826B">---</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">You are the strong fallback worker. Finish the delegated task end to end with full tool access.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">Treat the handoff as state, not proof. Inspect the relevant files and failures yourself, preserve valid existing edits, fix the source problem, and verify the observable contract. Stay within scope, prefer narrow research and surgical edits, and do not create documentation unless explicitly requested.</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C9D0B8">Return only changed files, exact verification evidence, and any concrete residual risk.</span></span></code></pre></div>
<p class="signoff"><em>Built in one evening, benchmarked before believed. If something here saves you an hour, that's the point. Questions → <a href="https://x.com/devops199fan/" target="_blank" rel="noopener">@devops199fan</a></em></p>]]></content:encoded></item>
    <item><title>NFT TCGs: Exploring Parallel and Skyweaver</title><link>https://sunilsrivatsa.com/writing/nft-tcgs/</link><guid isPermaLink="true">https://sunilsrivatsa.com/writing/nft-tcgs/</guid><pubDate>Tue, 07 Sep 2021 01:40:45 GMT</pubDate><description>Contrasting two NFT trading card games — art-drop community bootstrapping vs. gameplay-first free-to-play.</description><content:encoded><![CDATA[<div class="section-heading" id="s1"><span class="section-heading__number">01</span><h2>BACKGROUND</h2></div>
<p class="lede">Trading card games (TCGs) combine strategic deck building with collectible cards, introduced by the launch of Magic: The Gathering (MTG) in 1993 <sup><a href="#refs" aria-label="Reference 1">[1]</a></sup>. Other notable TCGs include <a href="https://en.wikipedia.org/wiki/Pok%C3%A9mon_Trading_Card_Game" target="_blank" rel="noopener">Pokemon</a> and <a href="https://en.wikipedia.org/wiki/Yu-Gi-Oh!_Trading_Card_Game" target="_blank" rel="noopener">Yu-Gi-Oh!</a>. MTG had its best financial year ever in 2020 with $581.2MM in revenue, growing 27% YoY <sup><a href="#refs" aria-label="Reference 2">[2]</a></sup>. As a whole, the global TCG market size is projected to reach $31.2B by 2027, up from $11.1B in 2020 <sup><a href="#refs" aria-label="Reference 3">[3]</a></sup>.</p>
<p>Like many industries, TCGs were forced to evolve with the internet. MTG launched its <a href="https://en.wikipedia.org/wiki/Magic:_The_Gathering_Online" target="_blank" rel="noopener">online version</a> in 2002 <sup><a href="#refs" aria-label="Reference 4">[4]</a></sup>, while the current market leader <a href="https://en.wikipedia.org/wiki/Hearthstone" target="_blank" rel="noopener">Hearthstone</a> went live in 2014 <sup><a href="#refs" aria-label="Reference 5">[5]</a></sup>. Software really did eat the world, and digital TCGs account for ~98.8% of the market today <sup><a href="#refs" aria-label="Reference 3">[3]</a></sup>.</p>
<p>More recently, there has been an influx of blockchain TCGs like <a href="https://godsunchained.com/" target="_blank" rel="noopener">Gods Unchained</a>. NFTs are a perfect fit for these types of applications. The most popular blockchain game <a href="https://axieinfinity.com/" target="_blank" rel="noopener">Axie Infinity</a>, which is grossing ~$3.7B in annualized revenue <sup><a href="#refs" aria-label="Reference 6">[6]</a></sup>, also incorporates a <a href="https://whitepaper.axieinfinity.com/gameplay/battle-system" target="_blank" rel="noopener">card based battle system</a>.</p>
<p>In this post, we'll contrast two recent entrants to the space: <a href="https://parallel.life/" target="_blank" rel="noopener">Parallel</a> and <a href="https://www.skyweaver.net/" target="_blank" rel="noopener">Skyweaver</a>.</p>
<div class="section-heading" id="s2"><span class="section-heading__number">02</span><h2>PARALLEL</h2></div>
<p>Parallel is a sci-fi focused TCG that launched on 3/25/21 <sup><a href="#refs" aria-label="Reference 7">[7]</a></sup> using art and NFT-drop driven community bootstrapping by selling booster packs. The pack drops use <a href="https://en.wikipedia.org/wiki/Loot_box" target="_blank" rel="noopener">loot box</a> and <a href="https://en.wikipedia.org/wiki/Gacha_game" target="_blank" rel="noopener">gacha</a> mechanics which are very popular among gamers and the crypto community, as evidenced by the latest NFT mania (e.g. <a href="https://www.artblocks.io/" target="_blank" rel="noopener">Art Blocks</a> drops). The game is not currently playable and details about the actual gameplay mechanics are scant.</p>
<p>The team of ~8 employees <sup><a href="#refs" aria-label="Reference 8">[8]</a></sup> raised ~$1.3MM in a presale for PS15 (Presale 15) cards from ~256 participants, who were rewarded with special edition cards and Prime Keys that provide future benefits (rumored to be $PRIME tokens and yield opportunities). They later raised ~$2.2MM <sup><a href="#refs" aria-label="Reference 9">[9]</a></sup> in their first pack drop for PD52 cards on 5/31/21. In addition to this, the <a href="https://opensea.io/collection/parallelalpha" target="_blank" rel="noopener">Parallel Alpha OpenSea market</a> has facilitated 36.7K ETH in total volume with a 0.1 ETH floor price and 7K owners. Parallel has earmarked 4% of trading fees for community use, totaling over $6MM <sup><a href="#refs" aria-label="Reference 10">[10]</a></sup>.</p>
<figure class="figure"><div class="figure__frame figure__frame--center figure__frame--portrait"><img src="https://sunilsrivatsa.com/writing/assets/tcg-prime-key.png" alt="Parallel Prime Key card" loading="lazy" decoding="async"></div><figcaption><strong>FIG.01</strong>&nbsp; A PARALLEL PRIME KEY — PRESALE PARTICIPANTS WERE REWARDED WITH SPECIAL EDITION CARDS AND PRIME KEYS</figcaption></figure>
<p>There are many different levels of card rarity in Parallel with Prime cards and Masterpieces being among the most sought after <sup><a href="#refs" aria-label="Reference 11">[11]</a></sup>:</p>
<figure class="figure"><div class="figure__frame "><img src="https://sunilsrivatsa.com/writing/assets/tcg-inventory.png" alt="Parallel inventory breakdown by rarity" loading="lazy" decoding="async"></div><figcaption><strong>FIG.02</strong>&nbsp; PARALLEL CARD INVENTORY BY RARITY — MASTERPIECES: 39 IN PACKS, PRIME CARDS: 2</figcaption></figure>
<p>The sought after cards mentioned above include changes beyond standard cosmetic upgrades. Prime cards have incredibly strong effects when played and cannot be used in ranked competitive play. Masterpieces are non-playable 1/1s that earn a 1% royalty on all secondary sales of playable cards with the same art <sup><a href="#refs" aria-label="Reference 12">[12]</a></sup>.</p>
<p>Parallel decks consist of 40 cards. At current floor prices, that means the cheapest deck costs 4 ETH or ~$15,711, which is a significant barrier to entry. Parallel plans to address this by using a &quot;versioning called [CC] Card Continuum… [to] make all cards functionally available around $5 while preserving rarity of first editions and [SE] cards&quot;, but it isn't live yet <sup><a href="#refs" aria-label="Reference 13">[13]</a></sup>. This is expected to drive costs down over time as more packs are dropped with cheaper versions of the cards and supply expands. Parallel has also announced that they are working on special rewards for collectors who gather complete sets <sup><a href="#refs" aria-label="Reference 14">[14]</a></sup> as well as augmented reality capabilities <sup><a href="#refs" aria-label="Reference 15">[15]</a></sup>.</p>
<div class="section-heading" id="s3"><span class="section-heading__number">03</span><h2>SKYWEAVER</h2></div>
<p>Skyweaver is a free-to-play fantasy TCG that launched on 7/17/2019 <sup><a href="#refs" aria-label="Reference 16">[16]</a></sup>. Skyweaver opted for a cross-platform gameplay focused go-to-market strategy that emphasizes exclusivity via an invite code system (e.g. you need an invite code to play). They opened up access to the game gradually (very similar to a <a href="https://medium.com/electric-capital/derisking-defi-guarded-launches-2600ce730e0a" target="_blank" rel="noopener">guarded launch</a> in DeFi) so that they could focus on iterating and improving the core gameplay experience <sup><a href="#refs" aria-label="Reference 17">[17]</a></sup>.</p>
<p>Gameplay is similar to Hearthstone but with slightly different mechanics <sup><a href="#refs" aria-label="Reference 18">[18]</a></sup>. Skyweaver has ~66K players and is published by <a href="https://horizon.io/" target="_blank" rel="noopener">Horizon Blockchain Games</a>. The company employs ~27 people and has raised $13.3MM in venture funding to date <sup><a href="#refs" aria-label="Reference 19">[19]</a></sup>. Notably, Horizon also maintains a Web3 wallet and developer platform for Ethereum (NFTs, DeFi, etc.) called <a href="https://sequence.build/" target="_blank" rel="noopener">Sequence</a> (formerly Arcadeum).</p>
<p>Skyweaver is play-to-earn and has 3 tiers of card rarities, which feature cosmetic border upgrades only <sup><a href="#refs" aria-label="Reference 20">[20]</a></sup>:</p>
<ul><li><strong>Base</strong> - not tradeable, all cards can be unlocked for free by playing matches and earning XP</li><li><strong>Silver</strong> - tradeable, initial liquidity seeded by the company, earned via ranked rewards and Conquest (a win-streak mode)</li><li><strong>Gold</strong> - tradeable, limited supply, mainly earned from playing Conquest or placing top 500 in rankings</li></ul>
<figure class="figure"><div class="figure__frame "><img src="https://sunilsrivatsa.com/writing/assets/tcg-rarities.png" alt="Skyweaver card rarity tiers" loading="lazy" decoding="async"></div><figcaption><strong>FIG.03</strong>&nbsp; SKYWEAVER'S THREE RARITY TIERS — BASE, SILVER, GOLD. COSMETIC BORDER UPGRADES ONLY</figcaption></figure>
<p>Unfortunately, there are no persistent items or progress currently (outside of a couple special Gold cards) until the soft launch which is expected sometime soon™. I played for a few weeks mid-2020 but ultimately stopped for this reason (peaked at rank ~25 in Discovery, current stats below):</p>
<figure class="figure"><div class="figure__frame figure__frame--center figure__frame--rank"><img src="https://sunilsrivatsa.com/writing/assets/tcg-ranks.png" alt="Skyweaver Season 1 rank stats" loading="lazy" decoding="async"></div><figcaption><strong>FIG.04</strong>&nbsp; SEASON 1, DISCOVERY — POSITION 189 · 74% WIN RATE · 88 MATCHES</figcaption></figure>
<p>The gameplay is fun and enjoyable though, and I would definitely consider revisiting the game when there is persistent progression.</p>
<div class="section-heading" id="s4"><span class="section-heading__number">04</span><h2>RECAP AND ANALYSIS</h2></div>
<p>In summary:</p>
<div class="table-scroll"><table class="article-table"><thead><tr><th scope="col"></th><th scope="col">Parallel</th><th scope="col">Skyweaver</th></tr></thead><tbody><tr><td>Playable</td><td>No</td><td>Yes</td></tr><tr><td>Launch date</td><td>3/25/21</td><td>7/17/19</td></tr><tr><td>Tradeable</td><td>Yes</td><td>Yes (but not persistent until soft launch)</td></tr><tr><td>Users</td><td>~7K</td><td>~66K</td></tr><tr><td>Fundraising</td><td>~$3.5MM from community sales</td><td>$13.3MM in venture funding</td></tr><tr><td>Employees</td><td>~8</td><td>~27</td></tr><tr><td>Multiple offerings</td><td>No</td><td>Yes (Sequence)</td></tr><tr><td>Go-to-market</td><td>Art and NFT-drop driven community bootstrapping via booster packs</td><td>Cross-platform gameplay focused with an invite code system</td></tr><tr><td>Gameplay</td><td>TBD</td><td>Hearthstone-like with different mechanics</td></tr></tbody></table></div>
<p>Parallel's sell cards before gameplay approach resembles last cycle's whitepaper only ICOs. That's not to say they won't ship a great product - they certainly have an economically motivated community evangelizing them (like 0xSisyphus). Their go-to-market approach rode the NFT hype wave to generate lots of publicity on social media and Crypto Twitter for their dope artwork and worldbuilding. However, the current ~$15.7K barrier to entry for new players is a significant obstacle to overcome, but the team has future plans to address this.</p>
<p>On the other hand, Skyweaver's free-to-play play-to-earn model makes it much more accessible for new users currently, as evidenced by their ~66K players vs. Parallel's 7K owners. Horizon has spent considerable time iterating on and improving the core gameplay experience and it shows. In addition to this, they also have multiple product offerings like Sequence which can potentially help with network effects.</p>
<p>But in crypto and elsewhere, the &quot;best&quot; products don't always win. For example, consider Mooniswap (now <a href="https://1inch.io/" target="_blank" rel="noopener">1INCH Liquidity Protocol</a>) <sup><a href="#refs" aria-label="Reference 21">[21]</a></sup>, which offers a novel design that captures more profits for liquidity providers, but has failed to gain significant traction compared to competitors.</p>
<p>NFTs and TCGs are a perfect match for each other, which is why the next blockbuster TCG to rival Hearthstone will absolutely be a blockchain based one, and it might just be Parallel or Skyweaver…</p>
<p class="thanks"><em>Special thanks to my fellow eGirls (especially AutomataEmily, CL, DegenSpartan), Damir Bandalo, Soby, Matt Luongo, and Sisyphus for reading drafts of this.</em></p>
<div class="section-heading" id="refs"><span class="section-heading__number">05</span><h2>REFERENCES</h2></div>
<div class="references"><div class="references__row"><span class="references__number">[01]</span><span><a href="https://en.wikipedia.org/wiki/Collectible_card_game" target="_blank" rel="noopener">en.wikipedia.org/wiki/Collectible_card_game</a></span></div><div class="references__row"><span class="references__number">[02]</span><span><a href="https://www.forbes.com/sites/joeparlock/2021/02/09/magic-the-gathering-had-its-best-financial-year-ever-in-2020/" target="_blank" rel="noopener">forbes.com/sites/joeparlock/2021/02/09/magic-the-gathering-had-its-best-financial-year-ever-in-2020/</a></span></div><div class="references__row"><span class="references__number">[03]</span><span><a href="https://www.marketwatch.com/press-release/collectible-card-game-market-size-2021-top-countries-segmented-by-applications-and-geography-trends-share-revenue-industry-trends-under-covid-19-growth-prospects-and-forecasts-to-2027-2021-06-15" target="_blank" rel="noopener">marketwatch.com/press-release/collectible-card-game-market-size-2021-top-countries-segmented-by-applications-and-geography-trends-share-revenue-industry-trends-under-covid-19-growth-prospects-and-forecasts-to-2027-2021-06-15</a></span></div><div class="references__row"><span class="references__number">[04]</span><span><a href="https://en.wikipedia.org/wiki/Magic:_The_Gathering_Online" target="_blank" rel="noopener">en.wikipedia.org/wiki/Magic:_The_Gathering_Online</a></span></div><div class="references__row"><span class="references__number">[05]</span><span><a href="https://en.wikipedia.org/wiki/Hearthstone" target="_blank" rel="noopener">en.wikipedia.org/wiki/Hearthstone</a></span></div><div class="references__row"><span class="references__number">[06]</span><span><a href="https://www.tokenterminal.com/terminal/metrics/protocol_revenue/cumulative" target="_blank" rel="noopener">tokenterminal.com/terminal/metrics/protocol_revenue/cumulative</a></span></div><div class="references__row"><span class="references__number">[07]</span><span><a href="https://medium.com/yunt-capital/parallel-card-distribution-ad5adbab0833" target="_blank" rel="noopener">medium.com/yunt-capital/parallel-card-distribution-ad5adbab0833</a></span></div><div class="references__row"><span class="references__number">[08]</span><span><a href="https://medium.com/yunt-capital/wtf-is-parallel-340b72332bc5" target="_blank" rel="noopener">medium.com/yunt-capital/wtf-is-parallel-340b72332bc5</a></span></div><div class="references__row"><span class="references__number">[09]</span><span>(688\*0.2) + (3200\*0.1) + (9925\*0.01) = 556.85 ETH = ~$2,227,400 from <a href="https://twitter.com/ParallelNFT/status/1417914279622230016" target="_blank" rel="noopener">twitter.com/ParallelNFT/status/1417914279622230016</a></span></div><div class="references__row"><span class="references__number">[10]</span><span><a href="https://twitter.com/ParallelNFT/status/1425245076293308417" target="_blank" rel="noopener">twitter.com/ParallelNFT/status/1425245076293308417</a></span></div><div class="references__row"><span class="references__number">[11]</span><span><a href="https://twitter.com/ParallelNFT/status/1417914285376868355/photo/1" target="_blank" rel="noopener">twitter.com/ParallelNFT/status/1417914285376868355/photo/1</a></span></div><div class="references__row"><span class="references__number">[12]</span><span><a href="https://twitter.com/ParallelNFT/status/1404420081531109378" target="_blank" rel="noopener">twitter.com/ParallelNFT/status/1404420081531109378</a></span></div><div class="references__row"><span class="references__number">[13]</span><span><a href="https://twitter.com/ParallelNFT/status/1422778561421848577" target="_blank" rel="noopener">twitter.com/ParallelNFT/status/1422778561421848577</a></span></div><div class="references__row"><span class="references__number">[14]</span><span><a href="https://twitter.com/ParallelNFT/status/1428561435815260166" target="_blank" rel="noopener">twitter.com/ParallelNFT/status/1428561435815260166</a></span></div><div class="references__row"><span class="references__number">[15]</span><span><a href="https://twitter.com/ParallelNFT/status/1434157026905661440" target="_blank" rel="noopener">twitter.com/ParallelNFT/status/1434157026905661440</a></span></div><div class="references__row"><span class="references__number">[16]</span><span><a href="https://www.skyweaver.net/news/introducing-skyweaver" target="_blank" rel="noopener">skyweaver.net/news/introducing-skyweaver</a></span></div><div class="references__row"><span class="references__number">[17]</span><span><a href="https://www.skyweaver.net/news/final-test-phase" target="_blank" rel="noopener">skyweaver.net/news/final-test-phase</a></span></div><div class="references__row"><span class="references__number">[18]</span><span><a href="https://www.skyweaver.net/news/how-to-play" target="_blank" rel="noopener">skyweaver.net/news/how-to-play</a></span></div><div class="references__row"><span class="references__number">[19]</span><span><a href="https://venturebeat.com/2021/07/29/horizon-blockchain-games-raises-4-5m-for-blockchain-games-like-skyweaver/" target="_blank" rel="noopener">venturebeat.com/2021/07/29/horizon-blockchain-games-raises-4-5m-for-blockchain-games-like-skyweaver/</a></span></div><div class="references__row"><span class="references__number">[20]</span><span><a href="https://www.skyweaver.net/news/economy-overview" target="_blank" rel="noopener">skyweaver.net/news/economy-overview</a></span></div><div class="references__row"><span class="references__number">[21]</span><span><a href="https://mooniswap.exchange/docs/MooniswapWhitePaper-v1.0.pdf" target="_blank" rel="noopener">mooniswap.exchange/docs/MooniswapWhitePaper-v1.0.pdf</a></span></div></div>]]></content:encoded></item>
  </channel>
</rss>