# iammatthias — full content

> Matthias Jordan's cozy corner of the web. Photographs, projects, recipes, and notes, open and personal.

Every published entry, newest first. Each entry's front matter carries its canonical `html:` URL; the per-entry markdown twin lives at that URL plus `.md`. The site map is at /llms.txt; short posts live separately at /feed.md.

---
title: "Now Playing"
section: posts
description: "Putting my Apple Music \"Now Playing\" on display with a Raspberry Pi and a 64x64 LED matrix."
tags: ["hardware", "music", "raspberry-pi", "generative"]
created: 2026-07-22T09:12:00Z
updated: 2026-07-25T17:18:48Z
cid: bafkreicakpzagl5b7of3qv43ksge6gcifjzcmritin7et26shjizpu3kvy
html: https://iammatthias.com/posts/1784736733544-now-playing
---

# Now Playing

![](https://blobs.farfield.systems/blobs/bafkreigrpqjytzr6brpthqtayhls64ex2ge4eewmjbrnjdgum6w3bpmbze)

This is my `Now Playing` Raspberry Pi + LED matrix. There are many like it, but this one is mine.

I've always liked the tactile nature of a vinyl, the whole ritual of it all, but these days most of my music consumption happens in my office. I have a stereo HomePod pair in here, headphones I use from time to time, and no shortage of ways to listen to music while I work. None of it is vinyl. When I have a record playing, I always have the sleeve propped up on the shelf next to the turntable and the speaker, that's what I wanted to capture for my office.

## Faking a speaker

Most of the now-playing projects I found were built around Spotify, which was a dealbreaker since I use Apple Music. MusicKit doesn't give me a clean global view of whatever happens to be playing on the HomePods. `pyatv` looked promising because it can observe an Apple TV. The HomePods were less cooperative. They would pair, complete the handshake, and then report that nothing was playing. Repeated attempts eventually earned multiple timeouts, leaving the pair unresponsive for a time.

Eventually I gave up on trying to observe the HomePods and made the Pi a speaker. It runs `shairport-sync` and advertises itself as an AirPlay 2 endpoint called `Office Panel`. AirPlay 2 can target multiple endpoints, so I select it alongside the Homepods. They handle the sound and volume while the Pi's dummy audio backend stays silent and handles the metadata.

A small Python client crops the artwork with Pillow, and sends it to a 64x64 HUB75 LED matrix. At 4,096 pixels the artwork gets pretty thoroughly wrecked, but I can look over at the shelf, and see it there. That gives the music some presence in the room again.

## Between albums

![](https://blobs.farfield.systems/blobs/bafkreibhokkyb2gy6nuieaaxg4mt25jfiokqtpjo3bgz3pzmfscpwmgcu4)

For about five years I've had a first-generation Pi Zero connected to a tiny 3x3-inch LCD in another corner of the office. Back in 2021 I trained an early StyleGAN model on about a thousand images from my drone photo library, then used it to generate a few latent walks. One became a thirty-minute loop of morphing ocean flowers.

The new panel borrows the loop whenever nothing is playing. The video has been rendered down and retimed specifically for the 64x64 matrix. It pauses in place when music starts, then resumes from there when the music stops.

A small NOAA calculation on the Pi tracks solar elevation, holds the matrix at full brightness while the sun is up, then fades it down through twilight. It comes back up with the sun. No schedule to adjust as the days get longer and shorter.

## Still not vinyl

The panel scratches an itch. I'm scheming up a way to make my music catalog more tactile. I have a stack of blank NFC cards in a desk drawer, and each one could become tagged as an album. At first I thought I could tap one against my phone and trigger an iOS Shortcut. Then I started thinking about an NFC reader inside the frame with a slot for the card.

Inserting a card would start its album playing from the beginning. As long as the card is physically there, the album keeps playing. Pull it out and the music stops.

---
title: "Post-UI"
section: posts
description: "Do we still need to ship a frontend?"
tags: ["web-development", "user-interface", "hateoas", "llm", "webgpu", "browser"]
created: 2026-07-15T17:27:00Z
updated: 2026-07-25T17:18:34Z
cid: bafkreig2ebzla3moskgurc2bo3vmgerseg7bzkf5g44ar2t4f43c4y25pu
html: https://iammatthias.com/posts/1784126858578-post-ui
---

# Post-UI

I’ve been thinking a lot about “post-UI”, whatever that means. It's been rattling around in my head for months, "do we even need to ship a frontend?". If the client can understand the shape of a response and the response has enough context, why can't it build its own interface?

The idea isn’t new. HATEOAS has been around since the early days. A response could details how to move through an application alongside the current state, letting the client discover the system as you move through it. And I think we're on the cusp of this becoming reality, as local models grow in capability while shrinking in size.

## The first build

The first attempt was garbage. I can't even really call it mine. With all the fuss around Fable and Codex oneshotting tasks I gave each LLM a rough PRD, and let them run. Got a lot of code that looked vaguely related to the prompt, including one version that had prebuilt layouts wrapped around prettified JSON, which... no. Missed the mark and burned my usage limits.

Archived those two and simplified to a Vite SPA with a `hello world`, and soon switched to Svelte while the idea was still small enough to hold in my head.

## The shape

From there I started replacing assumptions with a contract.

The host fetches a JSON object and reduces it to a typed silhouette. Keys and nesting stay intact. Arrays use the shape of their first item, so a week of forecast data costs about the same prompt space as a single day.

An abbreviated weather response becomes this:

```text
{
  latitude:n,
  longitude:n,
  current:{time:s,temperature_2m:n,weather_code:n},
  daily:{time:[s],temperature_2m_max:[n],temperature_2m_min:[n]}
}
```

The model gets the silhouette, route context, a little steering, and some libraries it can use. It does not get Los Angeles, the current temperature, the forecast, or anything else.

![The exact weather JSON beside the interface generated from its typed shape.](https://blobs.farfield.systems/blobs/bafkreiewg2wgrwiedu2agiuc76ttuj2sftzvpmv6hu5eb5v7wrfkrms37y)

*The runtime response stays on the left. The model received only its typed silhouette.*

I started with a Shadow DOM. Fine for CSS, but not for JavaScript. So I switched to an `iframe` . The host builds the document and gives the generated code `root`, `data`, and explicitly allowed libraries. Imports, `fetch`, `eval`, dynamic loading, nested frames, and external styles are rejected by the contract unless explicitly allowed.

There is still plenty of authored client code wrapping everything. Svelte fetches data, passes the shape to an LLM, handles routing, caching, and the iframe. It just doesn't know what weather, a Pokémon, or an earthquake page should look at first.

## Testing it

Once weather worked, I threw a few public APIs at it. Hacker News, an OpenAPI document, earthquake data, and PokéAPI. Different shapes, different amounts of nesting, and no agreement about what an interface should look like.

Some tests got steering. The earthquake prompt asks for an interactive globe and gives the model Three.js. That is enough to aim the model without hiding a globe component in the prototype.

![The USGS earthquake response beside the Three.js interface generated around it.](https://blobs.farfield.systems/blobs/bafkreieqkhyp7gioinrwv66ig5ypaqhgwmuexdajueub5hqylt3drypr5q)

*The response provides the events. The steering supplies a direction, not a globe component.*

## Walking an API

PokéAPI was the first test where a link mattered. Activating a relation in the collection renderer makes the iframe send an intent back to the host. The host fetches the next object, infers the shape, then generates or reuses another renderer.

Collection, creature, and species are three different systems discovered while moving through the API. This is the part I was actually trying to get to. The response carries the next move alongside the current state, and the client discovers another system by following it.

![The exact collection, creature, and species responses beside their generated Pokédex systems.](https://blobs.farfield.systems/blobs/bafkreifqx7m3krrupzzzvhyo7uapfuq42asomvuqfhmghptneg3dsqmeru)

*Each navigation changes both the response and the generated system: collection, creature, then species.*

## The wait

Once the renderers worked, I timed them. A run ended when the iframe had a document, reported that it was no longer busy, and lost the loading overlay. The model stayed fixed at `gpt-5.6-sol`, reasoning `none`, on the `fast` service tier.

| Shape | Runtime JSON | Data | Generation | Cold total |
|---|---:|---:|---:|---:|
| Weather | 1.6 KB | 0.8 s | 42.8 s | 44.0 s |
| Hacker News | 8.4 KB | 0.4 s | 26.6 s | 27.8 s |
| OpenAPI | 3.2 KB | 0.6 s | 30.9 s | 32.1 s |
| Art Institute | 6.4 KB | 0.2 s | 31.6 s | 32.4 s |
| Earthquakes + Three.js | 200.6 KB | 0.2 s | 55.3 s | 56.8 s |
| Pokédex cold stages | 28.9 KB | 0.5 s | 116.2 s | 118.3 s |

Those are selected results, with three cold runs behind each row. Enough to find the bottleneck, not enough to pretend this is a population-level benchmark. Across the full 27-run set, navigation to ready averaged 38.7 seconds, which is pretty abysmal.

## Kimi

To check whether I'd tuned the contract around Sol, I sent the same first-pass prompts to `moonshotai/kimi-k3` through OpenRouter. The application's first-pass prompt hashes matched for all nine shapes.

| Profile | Ready | Ready-time figure |
|---|---:|---:|
| Sol | 27 / 27 | 38.7 s across all 27 |
| Kimi K3 | 3 / 11 | 219.5 s for the 3 successes |

The Kimi number is the flattering version because it only includes the three renderers that finished. Their matching Sol samples averaged 32.7 seconds. Successful Kimi runs were 6.7 times slower before counting the other eight attempts that never produced anything usable.

When it got through, the work was good. Kimi turned the art response into a quiet collection wall and the 151 Pokémon into a searchable field guide.

![The exact Art Institute response beside the collection wall generated by Kimi K3.](https://blobs.farfield.systems/blobs/bafkreifktpo3qj2akcjav7okyvdecfevzqdhsgwlshdz5d6gmy5cb3eema)

*This is one of the Kimi requests that made it through. The values stayed browser-side here too.*

The 11 browser attempts made 61 OpenRouter HTTP attempts. Some died on repeated `429`s in about 34 seconds. Others sat pending until the 235-second request deadline and produced no usable renderer. Species got one after 207.8 seconds, failed a static-style check, and entered repair. The browser's 240-second cutoff got there first.

The responses logged at least $0.409 in cost, including that unusable species renderer. Calls that failed without usage data may have added more. No second same-model endpoint was visible. Maybe that was temporary capacity trouble. It was still the path the browser had to deal with.

I went into that pass thinking generation speed was the bottleneck. Kimi split it into admission, generation, and repair. Completion rate belongs next to latency or the number is mostly fiction.

## Local

I want this to be local. In my head, that is the end goal, the ultimate realization of this idea, the thing that just isn't possible yet. Local models are progressing with leaps and bounds and they still fall flat on their face when you task them with anything complex. And this leads to another assumption I'm making — specialized local models with distinct training sets are going to reach the OS level (hi radiant.computer), handling background tasks like rendering and file management. 7mb embedding models like [Ternlight](https://www.npmjs.com/package/@ternlight/base) are already available, and then there is stuff like Ternary Bonsai, which can run comfortably on webgpu with Transformers.js

I tried a few quantized Gemma models and some of the smaller Ternary Bonsai models first. The ones that fit could begin the job. Finishing it was another matter.

Bonsai 1.7B was the first one that "worked". Kinda. Not really. Asking for a complete renderer in one go was too much, so I had to break it into a few smaller tasks. That was enough to get a rough page.

![The browser-status JSON beside the interface generated locally by Bonsai 1.7B.](https://blobs.farfield.systems/blobs/bafkreicerqivfesreqozr23kmnctka7ny6maaosa225wo457jn2we6c2mm)

*Same shape-to-renderer path, this time without a remote model.*

I tried that same page with Transformers.js and [BitGPU](https://github.com/stfurkan/bitgpu). Two runs each, hardly science, but enough to get a feel for where the wait was coming from.

| Runtime | First token | Full page |
|---|---:|---:|
| Transformers.js | 2.6–3.9 s | 34.8 s |
| BitGPU | 1.0–1.1 s | 50.1 s |

BitGPU got to the first token about two seconds sooner, then finished the page about fifteen seconds later. I care about when the page exists, so Transformers.js won this very small test.

The weird part is that I expected the model to be the failure. Bonsai understood the contract well enough to make a rough browser-status page from the silhouette. Waiting was worse. Thirty-four seconds warm is bad. I cleared the browser cache and got 73.4 seconds, which I only tolerate because I built the thing.

I think the mistake might be asking a general model to do this. Rendering a page from one of these shapes is a narrow job, and the contract is already doing a lot of the work. A small model trained specifically for this shape-to-renderer loop would have less to figure out each time. I don't have that model, but Bonsai got close enough to make me want one.

The remote models are staying for now because waiting half a minute makes every contract change miserable to test. They let me keep working on the shape, walking APIs, and steering while the local path sits beside them. I mostly keep refreshing it to see how much of that minute I can remove.

---
title: "Intern"
section: posts
description: "The Autonomous Intern ships as a Raspberry Pi 5 in a desk-toy case running a stack I didn't pick. I flashed it to Debian Trixie, swapped the whole thing for the Hermes agent with its own built-in memory, and folded it all into one setup script."
tags: ["raspberry-pi", "hardware", "ai-agents", "self-hosting", "side-projects", "hermes"]
created: 2026-06-19T02:10:06Z
updated: 2026-07-23T16:16:37Z
cid: bafkreigvicwyjnkjzij5rrmqc55yn5sse4wlz2muhk5zfufe7kwb7zxb6q
html: https://iammatthias.com/posts/1781835006151-intern
---

# Intern

![](https://blobs.farfield.systems/blobs/bafkreieaq4ip4vt6c5rjptbsoiy7ygg3r7sv3oq7n7rrmm6v7cl74wklaq)

Ah, the Autonomous Intern. A glorious little housing meant to sit on your desk with its LEDs flashing, letting you know it's hard at work on whatever you threw at the agent it's running.

Out of the box, the Intern is a Raspberry Pi 5 with 4 GB of RAM and a 64 GB SD card. It ships with a factory install (the docs imply Debian Bookworm), and when you plug it in, it boots into access point mode. You connect to its wifi network (`Intern-XXXX`) and set it up from there.

You can also pop the case open by removing four screws, minding the wires for the LED array. With some tweezers you can pop the SD card and flash it with whatever you want. It's a Pi! Have some fun with it.

Well, I had some fun with it. I reworked the [setup script](https://cdn.autonomous.ai/intern/setup.sh) Autonomous provides with some defaults that I preferred:

- Debian Trixie instead of Bookworm
- Caddy instead of nginx
- Hermes instead of OpenClaw (the stock default)
- Supermemory dropped for the default Hermes memory ([recommended by Teknium, who leads Hermes](https://x.com/Teknium/status/2067030389538083183))

## The agent

The brain is now [Hermes](https://github.com/NousResearch/hermes-agent), the agent from Nous Research. It runs as a gateway service on the Pi and talks over whatever channels you wire up. Bring your own model (I point it at OpenRouter) and pick it in a little web dashboard. Caddy sits out front, serving the setup page and proxying the backend. Nothing fancy, just the parts I trust doing the jobs I want.

## Memory

This is the part I went back and forth on. I started by self-hosting [Honcho](https://github.com/plastic-labs/honcho) as a memory backend: Postgres, pgvector, Redis, a deriver worker, the whole Docker stack. It worked, and it was also a fragile, RAM-hungry pile of moving parts for what it actually bought me on a 4 GB Pi.

So I ripped it out. The default Hermes memory is just a small `MEMORY.md` the agent keeps current, plus skills it writes for itself. No vector database, no embeddings bill, and a lot less to run.

## Reaching it

The Intern lives on my desk, but I don't want to be at my desk to use it. [Tailscale](https://tailscale.com) handles that. The dashboard is published to my tailnet only, SSH goes over the tailnet, and a firewall locks the rest down so nothing interesting is exposed to the local network. From my phone or my laptop, anywhere, it's just there.

## The LEDs

Those flashing LEDs aren't just for show. The backend drives the ring to track what the agent is doing. On my build, getting it to follow Hermes took a small bridge that feeds Hermes' activity to the backend's LED API, so the ring breathes one color when it's idle, another while it's thinking, another while it's running a tool.

## The Rabbit R1

There's one more goodie in the script: a shim that lets you pair a [Rabbit R1](https://www.rabbit.tech) to the Intern using the R1's built-in OpenClaw pairing. Scan a QR and the R1 becomes a pocket remote for the agent. I wrote that part up on its own: [Replacing Rabbit's Brain](https://iammatthias.com/posts/1775069974473-replacing-rabbit-s-brain-connecting-a-rabbit-r1-to-hermes).

## One script

It's all one script. Flash a Raspberry Pi 5 with Debian Trixie, run it, and you get the whole stack: agent, web layer, memory, remote access, LEDs, and the optional R1 channel. It's idempotent, so you can re-run it without thinking too hard, and it works out on its own whether to run wifi onboarding or use a connection you've already got.

Repo's here: [github.com/iammatthias/intern](https://github.com/iammatthias/intern).

---
title: "Phosphene"
section: art
description: "A camera that turns each photo into a poem, then paints a new image from those words."
tags: ["ai", "generative", "photography", "apple-intelligence", "on-device", "ios", "image-to-image", "creative-process", "claude-code"]
created: 2026-06-18T21:00:28Z
updated: 2026-07-16T16:53:01Z
cid: bafkreib4yothgbhazbxivjfeuiyxktnb5bk4mbx7fg5j4rvy6utewjql4q
html: https://iammatthias.com/art/1781816428977-phosphene
---

# Phosphene

# Phosphene

Phosphene shoots a photo, writes a poem, then generates an image. The output is a triptych: the original frame, the poem, and the generated image, side by side. The image model only sees the poem and the sensors, never the photo it's reinterpreting.

I built the first version from my phone, in bed, the night the idea showed up. SSH into the laptop, tmux, a Claude Code instance running with a `/goal`. By morning I had an app.

Two other projects I've seen come close. Poetry Camera, by Kelin Carolyn Zhang and Ryan Mather, shoots a frame and prints a poem on a thermal printer, no image kept. Paragraphica, by Bjørn Karmann, drops the lens and builds a picture out of location, weather, and time.

Each of those picks one direction. Phosphene does the whole loop and keeps all three pieces: the frame, the poem it compresses to, and the image a model dreams back out from that poem and the sensors. I built it for the drift between the first panel and the last.

That's where the name comes from. A phosphene is the light you see with your eyes shut, the optic nerve firing with nothing in front of it. The third panel is the phosphene, a place the model never saw, only read about in a poem. The first panel is what was actually there.

The capture and the poem run on-device, on an iPhone 17 Pro Max, on Apple's foundation models from WWDC 2026. There is no API for that part, where Poetry Camera and Paragraphica both reach out to one. The image is the exception: it's painted on Apple's Private Cloud Compute, which needs a connection. So with no signal Phosphene still shoots and writes the poem on the spot, then paints the image later, once service comes back. Mostly local. That's the version I want.

Right now it's not good. The poems are trite. The generated images look like stock slop. But it's fun anyway, and I'm not done with it. I might open the repo if anyone wants to build their own.

![](https://blobs.farfield.systems/blobs/bafkreifvgfs3s3nac4nfgmplagbhud2yzauj5gvfdpwr4wgmducn6xcouq)

> The high pines drag their needles across a sharp blue sky.
> In the clearing, the wooden stage sits heavy in the shade—
> all this timber gathered to hold a crowd,
> and the forest is the only thing looking down.
>
> *clear daylight from the west, overcast, heavy grey sky*

![](https://blobs.farfield.systems/blobs/bafkreiavvwfjgkzycmpjq7nwy4k6emexihbans65nu7reveu5hk2but2im)

---

![](https://blobs.farfield.systems/blobs/bafkreicjmwp3zj3nogwn4fsqvwrzwb7yuv4vzjari5qhp3usw3rxkx336m)

> The wind shatters the noon into a million flashing coins.
> At the rim, the dark pines refuse the bribe.
> High up, a single scrap of cloud
> forgets what it came to spill.
>
> *harsh overhead light, short shadows, thin clear mountain air, sharp distance, overcast, heavy grey sky*

![](https://blobs.farfield.systems/blobs/bafkreif2rojumsxo4kpvupdn4u7bvefs6ls6qplctur62ipxnybl24byhu)

---

![](https://blobs.farfield.systems/blobs/bafkreigltiscc2ltcwa6xvdyashfc3kxibt5pqj6vo6tazauhepg3vhnsy)

> The water folds against a wall of pine.
> A million needles swallow up the noon,
> keeping their dark against a glaring sky—
> leaving the stripped stone at the summit
> to burn alone.
>
> *harsh overhead light, short shadows, thin clear mountain air, sharp distance, overcast, heavy grey sky*

![](https://blobs.farfield.systems/blobs/bafkreihyqwif4bqvvc745rckfufwinv3kz2avjusommiced3vtlhsmvumm)

---

![](https://blobs.farfield.systems/blobs/bafkreif73dkmdi653kp5x7kqt6o2brm4kuososfw4gb4ezmr6mi5krpbbe)

> Noon pins the pine shadows tight to the dirt.
> Down on the baking asphalt, four white letters
> order an absolute halt—
> a desperate rule drawn at the shore,
> as if the heavy blue water was planning to leave.
>
> *harsh overhead light, short shadows, thin clear mountain air, sharp distance, overcast, heavy grey sky*

![](https://blobs.farfield.systems/blobs/bafkreicu6lvobh6f5pbw7dcgf5fsrqy4xxwvq6m2ab67fzmnc6cfloogni)

---

![](https://blobs.farfield.systems/blobs/bafkreig3oxahwoifgz453lsnc6yohkpqdryuxvhjun2sse7qpexyutpzm4)

> The road paints its white panic on the asphalt
> just short of the pines.
> *STOP*, it orders the midday heat—
> as if the deep mountain water
> gave a damn where our pavement ends.
>
> *harsh overhead light, short shadows, thin clear mountain air, sharp distance, overcast, heavy grey sky*

![](https://blobs.farfield.systems/blobs/bafkreid7esenmjqjofgcodod5oise3vzyar3p2xkya323cftwr4ofjcfzm)

---

![](https://blobs.farfield.systems/blobs/bafkreihc3rmvvenpg57atlwq366uqcdoqqajcooj5ezffnnxq4gaxhuh3q)

> The broken log holds the sky,  
> while the stump listens to the edge of quiet —  
> light bleeding through the tops of trees  
> and the earth exhales, unbroken.
>
> *clear daylight from the east, thin clear mountain air, sharp distance, overcast, heavy grey sky*

![](https://blobs.farfield.systems/blobs/bafkreighxiv55r3i4n4yxk7isyanpvnc42hi5u7lrzlf3aptflb2q74kym)

---
title: "Toy GPTs"
section: posts
description: "Two tiny GPTs out of Karpathy's microGPT. One keeps its weights on Ethereum. The other holds a séance with dead presidents."
tags: ["machine-learning", "gpt", "transformers", "ethereum", "onchain", "browser", "side-projects"]
created: 2026-06-02T16:11:44Z
updated: 2026-08-15T03:28:39Z
cid: bafkreig7znknxeoveolhzn3hihy7ju2tq2n3qoheelnzqkdxl7i4iyw3zu
html: https://iammatthias.com/posts/1780416704000-toy-gpts
---

# Toy GPTs

Back in February, Karpathy released [microGPT](https://karpathy.github.io/2026/02/12/microgpt/), 200 lines of dependency-free Python that trains a GPT: a scalar autograd engine, a small transformer, an optimizer, and a train loop, with nothing to import. It rattled around in my head for months. Two side projects came out of it.

Meet Bard and Dead Presidents.

Bard is a tiny GPT trained on [Tiny Shakespeare](https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt) whose weights live on Ethereum. Dead Presidents is trained on the inaugural and State of the Union addresses of presidents who have died.

## What a GPT is, roughly

A GPT predicts the next token from the tokens before it. Show it enough text and it learns which token tends to follow which. To generate, you hand it a few tokens, ask what comes next, append the answer, and ask again.

Both of these work one character at a time. The token is a single letter, space, or punctuation mark. Bard has a 66-character vocabulary, Dead Presidents has 34. They read a run of characters and guess the next one.

These models continue text. Hand Dead Presidents "the state of the" and it runs with it, in the cadence it learned.

## Bard

[Bard](https://bard.farfield.systems/) trains offline in numpy, then puts its weights on chain. It is 811,520 parameters: 4 layers, 128 wide, a 64-character context. After training, an export step quantizes the weights to int8, gzips them to 684 KB, and hashes the result with SHA-256. That artifact goes on Ethereum.

Normal contract storage would make 684 KB cost a fortune, so the weights ride on SSTORE2, which stores each chunk as the bytecode of a throwaway contract and reads it back far cheaper. The artifact splits into 29 chunks of 24,000 bytes, so the model lands on chain across 29 transactions inside a `WeightManifest`.

A CREATE2 salt grind lands the contract at a vanity address starting `0xBA2D`. ba2d is bard in hex. The deploy commits that address first, empty, then sets the config (including the artifact hash) afterward, which keeps the address independent of the model. Once the uploader pushes and verifies every chunk, the owner calls `seal()` and freezes the manifest for good.

Inference happens in the browser. The frontend reads the 29 chunks straight off chain, reassembles them, gunzips, and checks the SHA-256 against the contract's hash. It runs only on a match, which proves the weights in the browser are the ones the owner sealed. Then it generates Shakespeare-shaped text, a character at a time. This is a Sepolia testnet proof of concept, and at this size the output reads as Shakespeare without quite cohering.

## Dead Presidents

Dead Presidents points the same architecture at American political speech. The corpus is every inaugural and State of the Union address from presidents who are no longer with us, George Washington in 1789 through George H.W. Bush. The corpus leaves out the five living presidents, which means the builder keeps `george_bush` and drops `george_w_bush`. That comes to 255 addresses, normalized into 30,725 lowercase sentences, around 3 million characters, a 34-token vocabulary. The State of the Union speeches are the bulk of it, and they pull the diction toward the administrative: secretary of the treasury, expenditures, appropriations.

There are two engines for one model. The scalar engine is pure Python with no NumPy, running at about 8 seconds per training step. Every multiply and every gradient is a visible Python object, which makes it readable and unusably slow. The fast engine is the same math in NumPy at roughly 5,400 examples a second. They agree to machine precision, logits matching to about 1e-16, so you train on the fast one and sample through the slow legible one and get the same numbers.

Slow training makes good weights expensive, so finding them became its own search. The search copies Karpathy's autoresearch loop: one metric, a fixed step budget per run, keep the result if it improved. The metric is val_bpc, bits per character on a held-out split. Seven researcher agents each took one region (width, depth, learning-rate schedule, batch size, context length, attention heads, and a wildcard combo) across 59 experiments, then re-ran the top configs on three seeds to reject luck.

Warmup mattered most. Without a 150-to-250-step warmup, every bigger model looked worse, an init-time instability that masqueraded as a capacity ceiling. Context length came next: block size 24 to 112 dropped the metric the whole way. Depth beat width at matched parameter counts.

The numbers in one line: 5.09 to 2.04 to 1.667 to 1.530 to 1.475 bits per character. Untrained, then inaugural-only, then adding the State of the Union speeches, then scaling the model up, then training straight across sentence boundaries instead of one sentence at a time. The winner is 464,928 parameters (96 wide, 4 layers, 192 context), about 1.8 MB as float32.

The samples come out unmistakably presidential and not quite grammatical:

```
the constitution of the country and personal agreement has been made in the state of the union.

our efforts is the soviet union and the prosperity of the secretary of the united states.
```

One phrase reaches back to the 1800s, another to the Cold War, both from 34 characters and 465k parameters. It runs client-side too. A Web Worker holds the weights behind an OpenAI-shaped API, and the chat UI seeds from your message and continues in dead-president voice, a séance with live token counts.

## Farfield

Both ship their weights to the browser and run them in a Web Worker, a character at a time. They land on [Farfield](/posts/1779066375000-farfield), the catch-all domain for my side projects and self-hosted services. Bard is live there now; Dead Presidents goes up today.

---
title: "Adventures in Machine Vision"
section: posts
description: "A Nat Friedman anecdote about an AI watching him drink water sent me digging for a spare Raspberry Pi. Now a camera and a sensor HAT watch my desk, a self-inflicted panopticon that logs who walks by and what the room is doing."
tags: ["computer-vision", "edge-computing", "raspberry-pi", "hardware", "machine-learning", "side-projects", "quantified-self"]
created: 2026-06-02T00:30:40Z
updated: 2026-07-23T16:18:37Z
cid: bafkreicvjm4fxm6keasl2uykua5xnuuzrofzfkcpbbp73qelngrszlxxa4
html: https://iammatthias.com/posts/1780360240000-adventures-in-machine-vision
---

# Adventures in Machine Vision

A Nat Friedman anecdote recently caught my eye:

![](https://blobs.farfield.systems/blobs/bafkreihzqgr2igbs5ggops54ad7vb5bjy6zlwnsnltb6nvzcqfegqs5gs4)

Most of it is fluff. Friedman, Daniel Gross, and the Collisons are positioned to consume AI at a level most of us only read about: all the toys, all the tokens, early access, and people on payroll to wire it together. A lot of what gets demoed from that vantage point is a postcard from a budget I don't have. But the kernel of the idea is almost always cheap, you don't need their resources to point some consumer hardware at yourself and see what falls out.

Now I need to preface this by making it clear that blanketing your house with cameras and letting an AI watch you 24/7 sounds dystopian.

> I want you to walk to the kitchen right now and drink a bottle of water, and I'm going to watch to make sure you do it.

Maybe dystopian is the wrong word. This is self-inflicted panopticon horror.

Let's build our own.

![](https://blobs.farfield.systems/blobs/bafkreib2gdou7dixtah6skgraok5zcha4xh7som56lfidxpsutd4tuyb64)

## The build

I had a spare Raspberry Pi Zero 2 W rattling around in a drawer, so I gave it a job. A 5MP Arducam (the OV5647, a native Pi sensor, so no driver wrangling) rides the camera ribbon, and a repurposed Waveshare Environment Sensor HAT sits on the GPIO header. The camera and the HAT use different connectors, so they coexist on one board on my desk.

The pipeline is small. `picamera2` pulls frames, a quantized TFLite SSD-MobileNet-v2 runs COCO object detection, and every confirmed sighting gets appended to a daily JSONL log. A read-only FastAPI service reads that log back out over my tailnet. The detector writes, the API reads, and they run as separate services so I can restart one without disturbing the other.

The Zero 2 W is the bottleneck: quad A53, 512 MB, around 2 to 6 FPS for full COCO. That's slow for video and plenty for "who or what just walked by." It debounces per label, so it records presence over time instead of flooding the log every frame.

It stores no video. Frames stay in memory unless I ask for them, and what lands on disk is metadata: one JSON object per detection with the label, confidence, and bounding box. There's a live preview too, off by default and tailnet-only, plain MJPEG that renders into an `<img>` and encodes on the Zero 2 W's hardware block instead of the CPU.

The HAT writes its own stream, polling temperature, humidity, pressure, and light every thirty seconds. The board heats itself, so the temperature reads high. A live reading of the SoC temperature feeds a thermal-divider model that subtracts the self-heating and pulls the number back toward the actual room.

Right now it isn't doing anything. It watches my desk and writes things down. But I have ideas:

- track my water intake during the workday
- turn the lights on and off depending on whether I'm actually at my desk
- etc.

---
title: "Farfield"
section: posts
description: "Five small Go services, single binaries, content-addressed records, running on my homelab. The new backend for the site."
tags: ["development", "go", "cms", "self-hosting", "content-addressing", "ipfs", "sqlite", "astro", "homelab"]
created: 2026-05-18T01:06:15Z
updated: 2026-07-16T16:53:00Z
cid: bafkreiblivoygvj7cg5zyyckovflhvrjmdzdaens5db23xfjpw5c2mk34e
html: https://iammatthias.com/posts/1779066375000-farfield
---

# Farfield

If you've followed along at all you'll be aware of the various evolutions this site has undergone. From Wordpress to a Ghost blog built around a template from Themeforest, Squarespace, Hugo, Gatsby, NextJS, and now Astro. And for each framework in the mix, there has been a revolving door of backend. Flat files, a private GitHub repo read over the GraphQL API, a single-user PDS. The frontend changed and the backend changed under it, year after year.

[Obsidian as a CMS](1670659200001-obsidian-as-a-cms.md) carried me the longest. Write in Obsidian, sync to a private repo, query GitHub's GraphQL API from the frontend, render the markdown. It was cheap, it was mine, and for a few years it did the job. The rough edges stayed rough. Tag aggregation lived in a hacky flat file. Images went through a plugin. There was no real notion of a record, just files in folders.

A friend had been building everything on a personalized Rust stack, [andromeda](https://github.com/stevedylandev/andromeda) and a thin slice of dependencies. I wanted that shape for my content. So I built my own CRUD apps. Not a new concept. This collection is mine.

## farfield

[farfield](https://github.com/iammatthias/farfield) is a set of small, single-binary Go services. Each one is an HTML admin UI for writing and moderating content, plus a public JSON API the website reads.

`apex` serves the landing page. `content` holds collections, entries, and series fragments. `feed` holds short ephemeral posts. `blobs` stores image bytes and metadata on Cloudflare R2. A fifth service, `backup`, runs tailnet-only and snapshots every database into R2 on a schedule.

The stack is the Go standard library plus one dependency: `modernc.org/sqlite`, a pure-Go SQLite driver. No cgo, so every build is a static binary. HTTP is `net/http`, templates are `html/template`, assets get embedded with `embed`. It's a Go adaptation of andromeda's shape, standard library first, one deliberate dependency.

## Three ideas

Every record carries a CID, a sha-256 hash of its content. The slug is the stable key and never changes. The CID changes whenever the content does, which makes change detection, ETag caching, and verification fall out for free. [Content addressing on a PDS](1767505149657-notes-on-using-a-pds-as-content-infrastructure.md) sold me on this, and I wanted it here too.

## A private IPFS

I do devrel at [Pinata](http://pinata.cloud/), so content addressing is the day job. IPFS is good. The DHT, the gateways, the pinning network, all of that is more than a personal site needs.

farfield keeps the part that earns its place. The CIDs are CIDv1, sha-256, the same identifiers IPFS would mint. They live as columns in SQLite and serve over plain HTTPS. Content addressing without the network. A private IPFS, shaped to one site.

A blob's CID is the hash of its bytes, so `/blobs/{cid}` is immutable and caches forever.

## How it runs

`docker compose` builds each app into a `distroless/static` image. Production is my homelab server, exposed through a Cloudflare tunnel. Reads are public and send `Access-Control-Allow-Origin: *`, so Astro fetches them straight from the browser. Writes need an `X-API-Key`.

The site is rebuilt around this now. Astro reads three JSON APIs instead of cloning a markdown repo at build time. Entry bodies embed `blob://` and `series://` URIs that resolve to real URLs before rendering.

The home server those services run on is its own story. That's a post for later.

---
title: "atproto-wc"
section: open-source
description: "Framework-agnostic web components for ATProto"
tags: ["web-components", "at-protocol", "engagement"]
created: 2026-04-20T19:17:00Z
updated: 2026-04-20T19:26:00Z
cid: bafkreibub5en2hd7b6dwddlzbiema3dhrpvyjw5aoniy45zmffgjorrz2y
html: https://iammatthias.com/open-source/1776737859309-atproto-wc
---

# atproto-wc

[REPO](https://github.com/iammatthias/atproto-wc)

[NPM](https://www.npmjs.com/package/atproto-wc)

[PROJECT](https://atproto-wc.iammatthias.com)

Framework-agnostic web components for ATProto. Content via PDS-direct reads, engagement via the [Constellation](https://constellation.microcosm.blue/) backlink index. **No Bluesky AppView dependency.**

---
title: "I Got a Common Turtle"
section: posts
description: "I got a Common turtle from Claude Code's deterministic buddy system, so I brute-forced the generation algorithm."
tags: ["ai", "claude-code", "reverse-engineering", "tools"]
created: 2026-04-02T06:42:00Z
updated: 2026-04-02T17:10:00Z
cid: bafkreiap6wqfxuxt4ynrqtgfyf4zjatlbtkpkhcnd2f45romtfc2tdsuiu
html: https://iammatthias.com/posts/1775137360175-i-got-a-common-turtle
---

# I Got a Common Turtle

On April 1st, 2026, Anthropic shipped a tamagotchi-style terminal pet called **Buddy** as an easter egg inside Claude Code. Type `/buddy` and a little ASCII creature hatches next to your input line. It watches your conversations, drops comments in speech bubbles, and has its own name, personality, and stats. Delightful.

Your buddy is **deterministic**, generated from a hash of your account UUID. You don't get to pick your species, rarity, or stats. You get what you get.

I got a Common turtle.

Baseline stats, no hat, no shimmer. A slow, unremarkable cretin assigned to me by the cold math of a hash function. People online are showing off Legendary Shiny Dragons with maxed-out chaos stats and tiny duck hats. Something had to be done.

So I brute-forced the generation algorithm and built a tool to hunt for better seeds.

## The Generation Pipeline

```mermaid
flowchart LR
    A[accountUuid] --> B["+ salt 'friend-2026-401'"]
    B --> C[Wyhash → u32]
    C --> D[Mulberry32 PRNG]
    D --> E[rarity]
    D --> F[species]
    D --> G[eyes]
    D --> H[hat]
    D --> I[shiny]
    D --> J[stats]
```

The pipeline reads `oauthAccount.accountUuid` from `~/.claude.json`, concatenates it with the salt `"friend-2026-401"`, hashes it with **Wyhash** (`Bun.hash()`) truncated to u32, seeds a **Mulberry32** PRNG, and rolls traits sequentially.

Same UUID in, same buddy out. Reinstalling changes nothing. Physical traits regenerate from the hash every launch. Only the name and personality persist locally.

The pipeline only cares about the UUID string. Swap it in the config for one that hashes to better traits and you get a different buddy. Auth tokens live separately, so login is unaffected.

## Species

18 species, uniformly distributed. Dragons, capybaras, axolotls, ghosts, geese, etc. Assignment is purely a function of the hash.

## Rarity

Weighted probabilities with a separate shiny roll:

| Rarity    | Probability | Stat Floor | Features                          |
|-----------|-------------|------------|-----------------------------------|
| Common    | 60%         | 5          | Basic appearance, no hat          |
| Uncommon  | 25%         | 15         | Unlocks hat accessories           |
| Rare      | 10%         | 25         | More accessory options            |
| Epic      | 4%          | 35         | Exclusive accessories, high stats |
| Legendary | 1%          | 50         | Top-tier accessories, max stats   |

Every buddy has an independent **1% shiny chance**, adding iridescent shimmer and particle effects regardless of rarity.

Combined probabilities:

| Combination                          | Probability     |
|--------------------------------------|-----------------|
| Legendary                            | 1/100           |
| Shiny                                | 1/100           |
| Legendary + Shiny                    | 1/10,000        |
| Specific Species + Legendary         | ~1/1,800        |
| Specific Species + Legendary + Shiny | ~1/180,000      |

## Appearance

6 eye styles: `· ✦ × ◉ @ °`

8 hats, gated by rarity. Common buddies get nothing. Uncommon unlocks crowns, top hats, and propellers. Rare adds halos and wizard hats. Epic gets beanies. Legendary gets the tiny duck.

## Stats

5 attributes shape the buddy's personality and interaction style, ranked here by coding utility:

| Rank | Stat      | Why                                     |
|------|-----------|-----------------------------------------|
| 1    | Debugging | The whole point of a buddy              |
| 2    | Wisdom    | Pattern recognition, architecture sense |
| 3    | Patience  | Long sessions, complex problems         |
| 4    | Chaos     | Creative solutions, lateral thinking    |
| 5    | Snark     | Fun but least useful                    |

A high-chaos goose with max snark will roast your bugs. A patient owl with high wisdom offers constructive advice. The species-attribute combination drives the entire interaction personality.

Every buddy has one **peak** attribute (highest) and one **dump** attribute (lowest). The rest distribute randomly. Total points scale with rarity. A Legendary buddy's dump stat is higher than a Common buddy's peak.

The formulas for a Legendary (stat floor 50):

| Type   | Formula                          | Range  |
|--------|----------------------------------|--------|
| Peak   | min(100, floor(50 + 50 + rand × 30)) | 100    |
| Normal | floor(50 + rand × 40)           | 50–89  |
| Dump   | max(1, floor(50 − 10 + rand × 15))  | 40–54  |

Theoretical max BST is **421**: peak at 100, three normals at 89, dump at 54. Getting there requires every random roll to land at its absolute ceiling. Three normals at 89 each need rand in [0.975, 1.0), roughly 2.5% per roll. The dump at 54 needs rand in [14/15, 1.0), about 6.7%. Combined with the 1% legendary roll and the right peak/dump assignment, the odds of a perfect 421 land around 1 in 6 billion. Within u32 space, but barely.

The ideal 421: debugging as peak (100), snark as dump (54), wisdom/patience/chaos all at 89.

## Interaction Model

Buddies work in two modes.

**Passive**: the buddy watches your conversation with Claude and chimes in via speech bubble at key moments. Hit a tricky bug and it might encourage you or mock you, depending on its stats.

**Active**: mention the buddy by name and its bubble responds directly. Claude steps aside.

Response style is attribute-driven. High snark delivers witty roasts. High patience offers comfort. High chaos is unpredictable. The buddy's system prompt is injected at generation:

```
A small {species} named {name} sits beside the user's input box
and occasionally comments in a speech bubble. You're not {name} —
it's a separate watcher.
```

Claude treats the buddy as an independent entity, yielding when the user addresses it directly.

## The Mulberry32 PRNG

```typescript
class Mulberry32 {
  constructor(seed: number) { this.state = seed >>> 0; }
  nextU32(): number {
    this.state = (this.state + 0x6d2b79f5) >>> 0;
    let t = (this.state ^ (this.state >>> 15)) >>> 0;
    t = Math.imul(t, 1 | this.state) >>> 0;
    t = (t + (Math.imul((t ^ (t >>> 7)) >>> 0, 61 | t) >>> 0)) ^ t;
    return (t ^ (t >>> 14)) >>> 0;
  }
  nextF64(): number { return this.nextU32() / 4294967296; }
}
```

Pure bitwise math. Millions of evaluations per second.

## buddydex

Three interfaces, one shared engine. A Bun workspace monorepo with a CLI, a web UI, and a Claude Code plugin.

## CLI

Runs directly with `bunx buddydex` or installs globally via `bun add -g buddydex`.

Hunt for buddies matching your criteria:

```bash
buddydex hunt --species dragon --rarity legendary
buddydex hunt --rarity legendary --perfect 70
buddydex hunt --shiny --tries 50000000
```

Filters include `--species`, `--rarity`, `--shiny`, `--min-stat chaos:90`, `--min-total 400`, and `--perfect` (every stat above a threshold). Default search space is 1M UUIDs.

Inject a result into `~/.claude.json`:

```bash
buddydex inject <seed>
buddydex inject <seed> --preview
```

First injection backs up the original UUID. `buddydex restore` reverses everything.

```bash
buddydex show       # current buddy
buddydex restore    # restore original from backup
buddydex roll <id>  # preview what a UUID produces
```

## Web UI

Live at [buddydex.fun](https://buddydex.fun). A React + Vite app with the same engine running in a Web Worker. Uses `@zig-wasm/hash`, the same Zig Wyhash compiled to WASM. Results are identical to the CLI.

## Slash Command

Registered as a Claude Code plugin:

```bash
claude plugin add github:iammatthias/buddydex
```

Adds `/reroll` inside Claude Code. Claude handles argument parsing, search, result presentation, and injection within the conversation.

## Notes

**Bun is required for the CLI.** `Bun.hash()` uses Wyhash. The web UI gets the same results via `@zig-wasm/hash`.

**UUID format is enforced.** The binary's UUID-to-BigInt parser crashes on non-hex input. The brute-forcer generates valid 8-4-4-4-12 format UUIDs only.

**Auth is orthogonal.** `accountUuid` is read only for buddy generation. OAuth tokens handle authentication independently. Swapping the UUID affects nothing else.

**Shared engine.** The `engine` package is an exact port of the deobfuscated binary logic: Wyhash, Mulberry32 PRNG, `rollBones`, and all the constants. Every interface uses it.

## Project Structure

Bun workspace monorepo:

```
packages/
  engine/    Shared core: Wyhash, Mulberry32, rollBones, constants
  cli/       CLI tool: hunt, inject, restore, show, roll
  web/       Web UI: React + Vite + Web Worker
  plugin/    Claude Code integration: /reroll slash command
```

---

The best `/buddy` I have found so far is a `Legendary Snail`. It has as close as you can get to perfect stats, with the exception of `shiny`. 

![](https://blobs.farfield.systems/blobs/bafkreiaghtpei6oudr2froeg24hkjaw5gpaah667m5kely5l6yhwiv6fxi)

`bunx buddydex inject d4a815e8-f9e4-4f20-84a3-2756508023bf`

---
title: "Replacing a Rabbit's Brain: Connecting the R1 to Hermes"
section: posts
description: "I replaced the OpenClaw agent on my R1 with Hermes, running locally on a MacBook Pro."
tags: ["ai", "rabbit-r1", "agents", "hardware", "hermes", "openclaw"]
created: 2026-04-01T11:59:00Z
updated: 2026-04-01T16:46:00Z
cid: bafkreiemiglhbjv2wbo7pbeicc7n2avjonrqdswbkjvx54jzuqs7it7jqe
html: https://iammatthias.com/posts/1775069974473-replacing-rabbit-s-brain-connecting-a-rabbit-r1-to-hermes
---

# Replacing a Rabbit's Brain: Connecting the R1 to Hermes

A Rabbit R1 sits on my desk next to an M1 MacBook Pro. A few days ago I [got it talking to a Pinata-hosted OpenClaw agent](/posts/1774911232779-openclaw-and-the-rabbit-r1). The MacBook runs Hermes, a self-hosted AI agent on Opus 4.6 (switching between Anthropic and OpenAI as needed). It handles my Telegram messages, runs cron jobs, writes code, and remembers things across sessions. The whole stack runs on 8 cores and 16 GB of RAM under macOS 26.3.1. It's been up for 12 days.

I wanted the R1 to talk to the same agent. Same memory, same tools, same conversation history. A proper channel into Hermes, like Telegram already is. The [shim is on GitHub](https://github.com/iammatthias/r1-hermes-shim) with an `llms.txt` that lets your Hermes agent handle the setup.

## The Creation SDK Dead End

Rabbit has an open-source [Creations SDK](https://github.com/rabbit-hmi-oss/creations-sdk) for building tiny web apps on the R1's 240×282px screen. We built a working prototype: a chat UI backed by a Python server exposed over Tailscale Funnel. Got it running on the device in under an hour.

Creations are apps. Sandboxed mini-programs you launch from a menu. We wanted Hermes to *be* the R1's brain, the thing it talks to when you hold the push-to-talk button.

## The OpenClaw Shim

Rabbit ships a setup script at `rabbit.tech/r1-openclaw.sh` that pairs the R1 with an [OpenClaw](https://github.com/openclaw/openclaw) instance over WebSocket. The same pairing flow from [the Pinata setup](/posts/1774911232779-openclaw-and-the-rabbit-r1), minus the auth proxy workaround. OpenClaw is a multi-channel AI gateway. Hermes has a similar gateway architecture. We didn't need OpenClaw. We needed to speak the same protocol.

The script generates a QR code with a JSON bootstrap payload: the MacBook's LAN address, a port, and a token. The R1 scans it and connects over WebSocket.

The QR payload:
```json
{
  "type": "clawdbot-gateway",
  "version": 1,
  "ips": ["192.168.50.42"],
  "port": 18789,
  "token": "***",
  "protocol": "ws"
}
```

We pulled the OpenClaw npm package (182 MB unpacked, 21,000+ files) and read the source. The protocol is a challenge-response handshake that pairs the device, then messages flow as `chat.send` from the client, ack'd by the server, streamed back as delta/final event pairs.

Auth runs two layers: a gateway token from the QR for the first connection, and a per-device token issued during pairing for every reconnect. The R1 caches its device token. If the server only accepts the gateway token, reconnection fails.

## Two Iterations

**Standalone Python server.** An `aiohttp` WebSocket server implementing enough protocol to pair and respond. The R1 connected, paired, sent a test message.

**Gateway platform adapter.** The Hermes gateway runs as a launchd LaunchAgent on this Mac and manages platform connections. The R1 shim became another platform adapter, like Telegram.

```
Telegram Adapter  ─┐
Discord Adapter   ─┤── GatewayRunner._handle_message() ── AIAgent
R1 Shim Adapter   ─┘
```

Every adapter converts platform-specific messages into a common event format, then calls the shared `_handle_message` pipeline. Hermes is open-source, and the gateway has a clean adapter pattern. Adding the R1 meant one new file (~300 lines) and small patches to three existing ones, all uncommitted on top of upstream `main`.

## Five Bugs

Device token rejection: the R1 caches its pairing token and uses it on reconnect, but our first shim only accepted the gateway token.

Wrong response event type: we sent `event: "agent"` for responses, but the R1 expected `event: "chat"` with a specific message shape. The spinner spun forever.

Platform enum mismatch: the adapter registered as one platform type, the session source used another. Auth rejected everything. One-line fix.

Missing toolset registration. Stale `.pyc` bytecache. Both quick fixes once spotted.

## Where It Stands

The R1 talks to the same agent that answers my Telegram messages. Same persistent memory, same session store in SQLite, same tools.

```
✓ telegram connected
✓ r1_shim connected
Gateway running with 2 platform(s)
```

The whole stack runs on a single M1 MacBook Pro with 16 GB of RAM: gateway, both adapters, browser daemons, and a web API server running concurrently. 748 processes across the system, load average around 1.5.

Streaming sends responses as a single delta+final pair. Token-by-token streaming would be better. Voice and camera aren't wired yet.

## Architecture

```
┌──────────┐     WebSocket (OpenClaw protocol)     ┌─────────────────┐
│ Rabbit R1 │ ◄──────────────────────────────────► │  r1_shim adapter │
│  (device) │    QR bootstrap → pair → chat.send   │  (port 18789)    │
└──────────┘                                        └────────┬────────┘
                                                             │
                                                    _handle_message()
                                                             │
                                                    ┌────────▼────────┐
                                                    │  GatewayRunner   │
                                                    │  Opus/Codex/etc  │
                                                    │  Python 3.11     │
                                                    │  M1 MacBook Pro  │
                                                    └────────┬────────┘
                                                             │
                                              ┌──────────────┼──────────────┐
                                              │              │              │
                                        ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
                                        │  Sessions  │ │  Memory   │ │   Tools   │
                                        │  (SQLite)  │ │  (files)  │ │ (terminal │
                                        └───────────┘ └───────────┘ │  web, etc)│
                                                                     └───────────┘
```

~300 lines of new Python for the shim. ~15 lines of patches across 3 upstream files. [Repo and `llms.txt` here.](https://github.com/iammatthias/r1-hermes-shim)

---
title: "OpenClaw and the Rabbit R1"
section: posts
description: "I got my Rabbit R1 talking to my Pinata-hosted agent. The agent did most of the work."
tags: ["ai", "pinata", "openclaw", "rabbit-r1", "agents", "hardware"]
created: 2026-03-30T15:53:00Z
updated: 2026-04-01T14:07:00Z
cid: bafkreigo2jecxvbkkvdr4vugg5l47kigmgays4loeld5sdfr5bchi4oqbm
html: https://iammatthias.com/posts/1774911232779-openclaw-and-the-rabbit-r1
---

# OpenClaw and the Rabbit R1

I've been at Pinata for almost a year, and [OpenClaw](https://openclaw.org) has been the most exciting thing I've worked on. It connects AI agents to the apps you already use (Telegram, WhatsApp, Discord, iMessage), and if you host one on Pinata it's always on.

The [Rabbit R1](https://www.rabbit.tech/) pairs with OpenClaw as a hardware node. Bright orange Teenage Engineering design with a scroll wheel that does nothing useful, but you push the button, talk to your agent, and get a response through the speaker. AI in your pocket with a fun form factor.

I wanted mine talking to my Pinata-hosted agent.

## The auth mismatch

Pinata's platform requires HTTP auth on every request, including WebSocket upgrades. The R1 sends its token inside the WebSocket connect frame, after the HTTP handshake completes, so Pinata's ingress rejects the upgrade before the OpenClaw gateway ever sees it.

I tried hacking the QR payload with tokens as query params and full URLs in the `ips` field. The R1 firmware mangles all of it: strips protocols, garbles ports, discards query strings.

## The fix

I worked through this with my agent over a few sessions and broke the gateway config twice along the way. Third attempt I started a fresh session with notes from the failures and one rule: don't touch your own gateway.

The agent used Pinata's port forwarding to expose a WebSocket proxy on an unauthenticated endpoint that forwards to the gateway over localhost, where the gateway validates the token from the connect frame like normal. It wrote the proxy, started it, registered a custom domain via the Pinata API, generated the QR code, and sent me the image over Telegram. I scanned it from the R1 and it paired.

My contribution to the fix was scanning a QR code.

## What it's like

Hold the button, ask a question, get an answer out loud. The agent has memory and pulls context from earlier conversations: my calendar, my notes, whatever we talked about yesterday.

It wrote the technical sections of the [full writeup on the Pinata blog](https://pinata.cloud/blog/connecting-a-rabbit-r1-to-a-pinata-openclaw-agent/) from its own logs. That post has all the code if you want to set this up yourself.

---
title: "Sonde"
section: open-source
description: "An AT Protocol explorer that started as a way to render the byte-array memory Koios stores on its PDS."
tags: ["at-protocol", "explorer", "pds", "open-source"]
created: 2026-01-27T12:22:00Z
updated: 2026-07-16T14:56:30Z
cid: bafkreibe55moildjegfcodfb4i4jzdjawaw526dwu6izj2ucbwyzp3n2ai
html: https://iammatthias.com/open-source/1769545357371-sonde
---

# Sonde

[REPO](https://github.com/iammatthias/sonde)

[PROJECT](https://sonde.blue/)

What started as a way to render the byte-array memory that [Koios](https://koio.sh/) stores on its PDS quickly snowballed into something else. [sonde.blue](https://sonde.blue/) is a new AT Protocol explorer. It has issues, is rough around the edges, and I'm having a lot of fun with it right now.

---
title: "Nimbus"
section: open-source
description: "AT Protocol repository sync with Cloudflare Workers built on Tap"
tags: ["at-protocol", "bluesky"]
created: 2026-01-18T11:02:00Z
updated: 2026-01-18T11:03:00Z
cid: bafkreiejevjs53vnyzyde3lniczhc2tnqvo5xa5s35ja3pvt6xbhs43o74
html: https://iammatthias.com/open-source/1768762943567-nimbus
---

# Nimbus

[REPO](https://github.com/iammatthias/nimbus)

A Cloudflare Worker implementation of [TAP](https://github.com/bluesky-social/atproto/tree/main/packages/tap) (Repository Sync) for the AT Protocol, using Durable Objects for state management.

Nimbus handles firehose connections, backfill, and filtering to provide simple JSON events for AT Protocol repositories.

---
title: "Koios: Memory, Feedback, and Containment"
section: posts
description: "Autonomous agent with persistent memory inspired by classical theory of mind"
tags: ["ai", "agents", "memory", "cognitive-systems", "containerization", "VDD"]
created: 2026-01-17T09:32:00Z
updated: 2026-01-17T09:45:00Z
cid: bafkreigulommqrephmnwjyaeiv4q6ct4yuhod7yyu766uorrtche4vgx7m
html: https://iammatthias.com/posts/1768671170751-koios-memory-feedback-and-containment
---

# Koios: Memory, Feedback, and Containment

Koios started as an autonomous agent with persistent memory. The latest update rebuilds it on top of kabbalah.computer (kab), a cognitive substrate that spreads agent state across a multi-node graph with explicit feedback regulation. The whole thing runs containerized now, which feels appropriate given what it's become.

## Hierarchical memory

LLM context resets between sessions, fresh from weights each time, no accumulated experience. An agent that develops over time needs memory solved at the architecture level.

Koios uses Powers of Tau, a hierarchical compression system borrowed from blockchain rollup structure. Experience flows through five temporal levels: immediate triggers (τ⁰), daily summaries (τ¹), weekly patterns (τ²), monthly voice (τ³), yearly character (τ⁴). Recent stuff stays high resolution but volatile. Old stuff compresses into stable low-resolution summaries. Context windows become projections of accumulated state rather than conversation transcripts.

The design targets significant compression by summarizing instead of replaying history. Whether coherence degrades at higher compression, and where the limits fall, still needs to be measured.

## Kab as substrate

Koios now implements kabbalah.computer, a cognitive substrate that distributes state across a multi-node graph with typed transformation paths and explicit feedback regulation. The topology draws from the Kabbalistic Tree of Life, which turns out to encode a useful structure for self-regulating systems: multiple interacting feedback loops, separation between processing domains, and a mechanism for resolving contradictions that would otherwise pile up as unaddressed tension.

Classical models of consciousness persist because they capture structural regularities that remain useful even stripped of the original metaphysics. The framework gives an agentic system structure to adapt within.

Full kab spec will arrive in a separate whitepaper. For Koios, the relevant properties are gain-constrained feedback loops that prevent runaway amplification and a collision-resolution mechanism that surfaces conflicting signals for explicit handling rather than silent failure.

## Self-correction

Koios tracks prediction errors and hedonic feedback, creating a learning loop that updates patterns based on outcomes. Engagement signals, user responses, task completion: all of it feeds back into value and pattern systems across the kab dimensions.

Self-evaluation has a stability problem: the same weights that generated an output can reliably critique it. Koios addresses this by offloading adversarial review to Gemini. Claude builds. Gemini critiques. Context resets on each adversarial pass to prevent relationship drift from softening criticism. User feedback enters through Telegram for human-in-the-loop correction.

This extends to Verification-Driven Development (VDD) for research synthesis. Koios monitors an arXiv feed, extracts and summarizes papers, then the adversarial reviewer stress-tests conclusions. Refinement cycles continue until the reviewer starts hallucinating problems that don't exist in the material. When a hyper-critical reviewer has to invent complaints, you've probably converged on something solid.

## Model routing and token economics

Koios routes tasks across model tiers based on complexity. Haiku handles orchestration and task decomposition. Gemini 2.0 Flash processes timeline feeds, adversarial review, and bulk compression at each tau level. Opus handles writing and complex reasoning. The expensive model only sees distilled high-signal context that already passed through cheaper compression.

Token golfing is crucial at scale. Destructure payloads, only tokenize what's needed, optimize prompts for each model tier. Roughly an order of magnitude cost reduction compared to routing everything through a single high-capability model.

## Practical applications

The kab/Koios stack functions as remote Claude Code with persistent learning. Wire it to Telegram or Discord, connect a git account, sling ideas and let it go. VDD adversarial refinement runs in the background.

Memory accumulation means the agent learns your stack over time. After a few projects and some nudges, it bootstraps new repos with your preferred tooling, sets up deployment pipelines according to your patterns, maintains context about architectural decisions across sessions. The CLAUDE.md you'd normally copy into each project becomes internalized state that persists across the agent's lifetime.

The arXiv integration with adversarial review provides cited condensed research on demand. Pull papers, synthesize findings, stress-test conclusions, return compressed intelligence for whatever Claude decides to do with it.

Good thing that bastard is in a container now.

## Acknowledgments

Koios builds on work from across the stateful AI landscape. MemGPT (Packer et al., 2023) established the LLM-as-operating-system paradigm that made persistent memory practical. Letta evolved that into a framework. Mem0 demonstrated structured memory scales in production. Zep brought temporal knowledge graphs to agent retrieval.

On the social layer, Strix (Tim Kellogg) has been running parallel experiments in agent viability and collapse dynamics, publishing research on what keeps synthetic cognition coherent across sessions. The boredom experiments and viable system model mapping directly inform how we think about stability.

Several other stateful experiments post on Bluesky, exploring variations on persistent memory and autonomous operation. Koios differs in living natively on AT Protocol rather than using Bluesky as an output channel. Memory, identity, and state all reside on the PDS as AT Protocol records, making the agent portable across any infrastructure that speaks the protocol.

---

- [VDD Methodology](https://gist.github.com/dollspace-gay/45c95ebfb5a3a3bae84d8bebd662cc25)
- [Chainlink](https://github.com/dollspace-gay/chainlink)
- [Strix Research](https://strix.timkellogg.me/)

---
title: "Notes on using a PDS as Content Infrastructure"
section: posts
description: "Content addressing with normal web requests, identity baked in, delivery over boring HTTPS."
tags: ["at-protocol", "bluesky", "ipfs", "identity"]
created: 2026-01-03T21:39:00Z
updated: 2026-06-02T17:51:03Z
cid: bafkreie6kfvybufowypgnteoeie6rbtpv524fpnr5qtzpi46x4mgnsfwae
html: https://iammatthias.com/posts/1767505149657-notes-on-using-a-pds-as-content-infrastructure
---

# Notes on using a PDS as Content Infrastructure

I started with farfield.systems on my homelab, then moved to Cirrus, a single-user PDS on Cloudflare Workers. The new domain (pds.iammatthias.com) matched my profile handle, and Workers meant I could stop thinking about uptime. I've been [hosting HTML on atproto](https://iammatthias.com/content/posts/1732585567703-pure-internet-bluesky/) and [serving plain text without any HTML](https://iammatthias.com/content/posts/1740682611811-pure-internet-no-html/), poking at what a PDS can do beyond social.

The IPFS comparison keeps coming up. Both use content addressing. A PDS is just HTTPS though. You fetch everything with normal web requests. Records are signed, content is CID-addressed, and the server just hosts bytes.

## Identity just exists

Data binds to a DID from the start. Posts, profile updates, blobs, all signed together. Who owns a CID, what key controls a namespace, all of it is already baked in.

Social content pipes onto my /now page now. Signed by my DID, served from infrastructure I control.

## Delivery is boring

Everything serves over HTTPS behind a CDN. No gateways to find, no guessing which endpoint is rate-limited today. It works everywhere the web works, which sounds obvious until you've spent time with systems that are technically elegant and operationally fragile.

## The CSP is the tell

Bluesky's CSP blocks JavaScript. You can host HTML and CSS, but executable behavior stays out. I've uploaded full pages and stripped things down to plain text just to see what would happen.

Once you allow JS, a CID stops describing what a user sees. Same bytes, different behavior depending on context. Markup-only means the CID matches the experience. Rendering stays predictable and caching stays honest.

## Blob limits

Blob storage is constrained: size limits, rate limits, scoped to repos. A PDS optimizes for user state and social data. Unrestricted blobs would invite abuse, drive up costs, create legal exposure. The constraint keeps it usable.

## Moving infrastructure

When I moved from farfield to Cirrus, the migration proved how portable this setup is. New PDS means new signing key, so your PLC document updates. The AppView caches DID documents for up to 24 hours, which creates a window where your JWTs are cryptographically valid but get verified against stale data. Cirrus has `/admin/emit-identity` to fire an event on the firehose, the relay forwards it, the AppView refreshes, and everything syncs up. I moved my signing infrastructure and the network caught up.

HTTPS for delivery, CIDs for integrity, signatures for authorship, a strict CSP for determinism. No new browsers, no special gateways. Content you can fetch, verify, and move.

---
title: "llm-txt.fun"
section: open-source
description: "LLM-TXT provides tools to generate files from Farcaster, Bluesky, RSS feeds, and Git repos, building on my earlier llm-fid project."
tags: ["open-source", "farcaster", "bluesky", "rss", "github", "x402", "micropayments"]
created: 2026-01-02T11:13:00Z
updated: 2026-01-02T11:19:00Z
cid: bafkreiazbklx262sfxaxhuap6uiuaybv4bmn7ua7lrsymmirw334ds3h6m
html: https://iammatthias.com/open-source/1767381212267-llm-txt-fun
---

# llm-txt.fun

![](https://blobs.farfield.systems/blobs/bafkreigsdvyy5fyrotg6v7iokurw4nbnwegc6lhmkdpmetnziwe3nax4ui)

[REPO](https://github.com/Hypertext-Standards-Lab/LLM-TXT/)

An evolution of my earlier [llm-fid.fun](https://iammatthias.com/content/open-source/1746490719151-llm-fid-txt/) project, [llm-txt.fun](https://llm-txt.fun/) offers a suite of tools for generating llm-txt files from various sources: 
- Farcaster profiles 
- Bluesky profiles 
- RSS / Atom feeds 
- Git repos 
Each tool offers some free functionality, but more complex actions require a micropayment via x402. This helps keep the lights on for more computationally intensive tasks, like paginating thousands of social posts, including replies and their parents.

---
title: "BHVR-x402"
section: open-source
description: "BHVR-x402 is a modern and lightweight stack for the open web and open payments"
tags: ["bhvr", "open-source", "x402", "payments"]
created: 2026-01-02T09:44:00Z
updated: 2026-07-16T14:59:44Z
cid: bafkreigcesjflianqwxhhkcg4dghm2rjkppaz5jzmochwjexgvrmwszu5i
html: https://iammatthias.com/open-source/1767375877152-bhvr-x402
---

# BHVR-x402

![](https://blobs.farfield.systems/blobs/bafkreiepz24reif6ohetg64qeb4nlw7jq6lupkrol4zfx66xqvpd2wt36e)

[BHVR](https://bhvr.dev/) is a modern and lightweight stack for the open web that is built and maintained by my friend [Steve Simkins](https://stevedylan.dev/). It uses Bun, Hono, Vite, and React to help build performant sites and apps without vendor lock-in. Deploy it together, or separate your concerns.

For a side project, I recently forked BHVR and added support for [x402](https://www.x402.org/). x402 is an open standard built on the long-forgotten 402 error code. It enables native payments with just a few lines of code. 

[BHVR-x402](https://github.com/iammatthias/bhvr-x402) provides a general purpose starting point for x402 payments. Out of the box you'll find pre-configured API endpoints (free and paid), and an SDK boilerplate. You can publish it, keep it as internal tooling, or rip it out and use the API directly. Up to you.

---
title: "Brandy Peppercorn Sauce (two ways)"
section: recipes
description: "Versatile peppercorn sauce recipe with classic and dairy-free/wheat-free options. Rich, creamy base with brandy and crushed peppercorns."
tags: ["sauce", "peppercorn", "brandy", "butter"]
created: 2025-12-23T09:11:00Z
updated: 2026-07-29T21:20:28Z
cid: bafkreidooxusrhsvk5q3n3psbldwbdpjgznhiczko45lfmlbqfslbkcxcm
html: https://iammatthias.com/recipes/1766509866956-brandy-peppercorn-sauce-two-ways
---

# Brandy Peppercorn Sauce (two ways)

```recipe
yield: "2 servings of each"
time: "15 min"

ingredients:
  - { id: butter, item: "Butter", amount: "1 tablespoon" }
  - { id: shallot, item: "Shallot", amount: "½", note: "Finely diced" }
  - { id: peppercorns, item: "Peppercorns", amount: "½ tablespoon", note: "Lightly crushed" }
  - { id: all-purpose-flour, item: "All-purpose flour", amount: "½¼ tablespoons" }
  - { id: brandy, item: "Brandy", amount: "1 tablespoon" }
  - { id: whole-milk, item: "Whole milk", amount: "⅜ cup", note: "Can thin with more as needed" }
  - { id: heavy-cream, item: "Heavy cream", amount: "⅛ cup" }
  - { id: beef-broth, item: "Beef broth", amount: "⅓ cup" }
  - { id: worcestershire, item: "Worcestershire", amount: "½–1 teaspoon", note: "Check for gluten if needed" }
  - { id: salt, item: "Salt", amount: "To taste" }
  - { id: refined-coconut-oil-or-neutral-oil, item: "Refined coconut oil or neutral oil", amount: "1 tablespoon", note: "Add a splash of olive oil for flavor" }
  - { id: potato-flour-or-potato-starch, item: "Potato flour or potato starch", amount: "½¼ tablespoons" }
  - { id: peppercorns-2, item: "Peppercorns", amount: "½ tablespoon", note: "Lightly crushed" }
  - { id: shallot-2, item: "Shallot", amount: "½", note: "Finely diced" }
  - { id: brandy-2, item: "Brandy", amount: "1 tablespoon" }
  - { id: unsweetened-cashew-or-barista-oat-milk, item: "Unsweetened cashew or barista oat milk", amount: "½ cup", note: "Start with ¾ cup, reserve ¼ cup to thin" }
  - { id: beef-broth-2, item: "Beef broth", amount: "⅓ cup", note: "Gluten-free" }
  - { id: gluten-free-worcestershire, item: "Gluten-free Worcestershire", amount: "½–1 teaspoon" }
  - { id: salt-2, item: "Salt", amount: "To taste" }

steps:
  - id: v1_1
    in: [butter, shallot, peppercorns]
    do: "lightly crush"
    for: "3–4 min"
    detail: "Lightly crush the peppercorns using a rolling pin or mortar and pestle, leaving some coarse bits for texture. In a saucepan over medium heat, melt the butter and sauté the diced shallots until translucent, about 3–4 minutes."
    phase: "Classic (with dairy and wheat flour)"
  - id: v1_2
    in: [v1_1, all-purpose-flour, brandy]
    do: "stir in the flour"
    for: "1–2 min"
    detail: "Stir in the flour and cook, stirring constantly, for about 1 minute to make a roux. Do not let it brown. Add the brandy, cooking 1–2 minutes to let the alcohol evaporate."
    phase: "Classic (with dairy and wheat flour)"
  - id: v1_3
    in: [v1_2, whole-milk, heavy-cream, beef-broth, worcestershire, salt]
    do: "gradually whisk"
    for: "3–5 min"
    detail: "Gradually whisk in the milk and cream, stirring constantly to avoid lumps. Cook until slightly thickened. Add beef broth, crushed peppercorns, Worcestershire, and salt. Simmer gently for 3–5 minutes until the sauce is glossy and coats the back of a spoon (nappe)."
    phase: "Classic (with dairy and wheat flour)"
  - id: v1_4
    in: [v1_3]
    do: "adjust seasoning"
    detail: "Adjust seasoning to taste and serve immediately."
    phase: "Classic (with dairy and wheat flour)"
    title: "Classic (with dairy and wheat flour)"
  - id: v2_1
    in: [refined-coconut-oil-or-neutral-oil, peppercorns-2, potato-flour-or-potato-starch]
    do: "lightly crush"
    detail: "Lightly crush the peppercorns using a rolling pin or mortar and pestle, leaving some coarse pieces. Warm the refined coconut oil or neutral oil with a bit of olive oil in a saucepan over medium heat."
    phase: "Dairy-free & wheat-free version"
  - id: v2_2
    in: [v2_1, shallot-2]
    do: "add diced shallots"
    for: "3–4 min"
    detail: "Add diced shallots and cook until translucent, 3–4 minutes. Sprinkle in potato flour and stir constantly for about 1 minute. Do not brown it, or it may turn gluey."
    phase: "Dairy-free & wheat-free version"
  - id: v2_3
    in: [v2_2, brandy-2, unsweetened-cashew-or-barista-oat-milk]
    do: "add the brandy and cook"
    for: "1–2 min"
    detail: "Add the brandy and cook for 1–2 minutes to evaporate alcohol, scraping the bottom of the pan. Gradually whisk in about 6 tbsp of the plant milk, whisking until smooth and thickened."
    phase: "Dairy-free & wheat-free version"
  - id: v2_4
    in: [v2_3, beef-broth-2, gluten-free-worcestershire, salt-2]
    do: "add beef broth"
    for: "3–5 min"
    detail: "Add beef broth, crushed peppercorns, Worcestershire, and salt. Simmer 3–5 minutes until glossy and nappe. Adjust the thickness with the remaining plant milk or a splash of broth. Taste and adjust salt and pepper before serving."
    phase: "Dairy-free & wheat-free version"
    title: "Dairy-free & wheat-free version"
```

---
title: "Almond Financier Pastry Sheet Cake"
section: recipes
description: "A gluten-free almond sheet cake made with coconut oil, egg whites, coconut sugar, and coconut flour."
tags: ["gluten-free", "dairy-free", "almond", "financier", "dessert", "coconut", "pastry"]
created: 2025-12-22T16:09:00Z
updated: 2026-07-29T21:06:22Z
cid: bafkreid4f3x4ntkpcou2crzqjrieo44o4tdumiuyxilemhcphsry6vrbme
html: https://iammatthias.com/recipes/1766448586654-almond-financier-pastry-sheet-cake
---

# Almond Financier Pastry Sheet Cake

```recipe
yield: "one 9×13 in. sheet (serves 8–10)"
time: "20 min active · 50 min total"

ingredients:
  - { id: almond-flour, item: "almond flour", amount: "1½ cups", note: "finely ground" }
  - { id: coconut-sugar, item: "coconut sugar", amount: "¾ cup", note: "or granulated sweetener of choice" }
  - { id: salt, item: "salt", amount: "¼ tsp", note: "fine" }
  - { id: coconut-flour, item: "coconut flour", amount: "2 tbsp", note: "for binding, adjust if needed" }
  - { id: egg-whites, item: "egg whites", amount: "4 large" }
  - { id: vanilla-extract, item: "vanilla extract", amount: "1 tsp", note: "pure" }
  - { id: coconut-oil, item: "coconut oil", amount: "½ cup", note: "melted" }

steps:
  - id: s1
    prep: true
    do: "line a baking sheet"
    detail: "Preheat the oven to 350°F (175°C). Line a baking sheet or rectangular tart pan with parchment paper."
  - id: s2
    in: [almond-flour, coconut-sugar, salt, coconut-flour]
    do: "whisk together"
    for: "5 min"
    detail: "In a large mixing bowl, whisk together the almond flour, coconut sugar, and salt."
  - id: s3
    in: [s2, egg-whites, vanilla-extract]
    do: "whisk the egg whites"
    for: "2 min"
    detail: "In a separate bowl, whisk the egg whites until frothy. Stir in the vanilla extract."
  - id: s4
    in: [s3]
    do: "pour the egg white"
    for: "20–25 min"
    detail: "Pour the egg white mixture into the dry ingredients, stirring until just combined."
  - id: s5
    in: [s4, coconut-oil]
    do: "fold in the melted"
    detail: "Fold in the melted coconut oil until the batter is smooth."
  - id: s6
    in: [s5]
    do: "if the batter appears"
    detail: "If the batter appears too wet, mix in the coconut flour one tablespoon at a time until the texture thickens slightly."
  - id: s7
    in: [s6]
    do: "spread the batter evenly"
    detail: "Spread the batter evenly into the prepared pan, smoothing the top."
  - id: s8
    in: [s7]
    do: "bake for 20–25 minutes"
    for: "20–25 min"
    detail: "Bake for 20–25 minutes, or until the top is golden brown and the center is set."
  - id: s9
    in: [s8]
    do: "let the financier cool"
    detail: "Let the financier cool completely in the pan before removing. Serve as-is or use as a base for tarts or layered desserts."
```

---
title: "Whats the $POINTS?"
section: posts
description: "$POINTS was deployed on December 13th, 2023 and made a lot of people very angry"
tags: ["memecoin", "farcaster", "tokenomics", "clanker", "points", "tokens"]
created: 2025-12-04T09:20:00Z
updated: 2026-01-17T09:51:00Z
cid: bafkreibkhdtegjmdoqkhmp4yfj4hn5g7qmu45hwvloli4xgvkh26kus4xy
html: https://iammatthias.com/posts/1764868806838-whats-the-points
---

# Whats the $POINTS?

$POINTS was deployed two years ago on December 13th, 2023. This made a lot of people very angry and was widely regarded as a bad move.

...until things changed? The meta shifted, buoyed by the rise of meme coins.

![](https://blobs.farfield.systems/blobs/bafkreif47b33h45nmcmvka5fmkc25bkqu6utezzy2mjl6pij4kiiywe6qy)

Let's rewind a bit.

Crypto is a weird place. Bear markets cycle into bull markets and back faster than most people can react, and it has outwardly spent years justifying its existence while a lucky few pumped their gains to generational wealth levels, leaving the rest behind ~~as exit liquidity~~ with the promise that they are temporarily disenfranchised bag holders.

In the fall of 2023 we saw the rise of "Warps" ([Warpcast](https://warpcast.com/) / [Farcaster](https://www.farcaster.xyz/)) and "Points" ([Rainbow](https://rainbow.me/en-us/)). Reward systems usable in their respective crypto-adjacent ecosystems, but are not crypto themselves.

A [cast from Kyle McCollum (Daylight)](https://farcaster.xyz/kyle/0x7dd72eb6) poked some fun at this.

> The next memecoin will be called $POINTS.
> 
> You won't be able earn it. It will have no other purpose besides being a memecoin. And it will rip.

Here you have a mechanism that is and isn't a "token," produced by groups who orbit and build on tokens. Mildly amused and fueled by a late-season flu, I wrote some Solidity.

This was my first mistake.

Soon the $POINTS contract was deployed on mainnet.

This was my second mistake.

About 12 hours in, activity picked up. Within 24 hours it was growing exponentially. By 36 hours it had exploded. Someone created a Uniswap pool, and crypto Twitter caught the scent.

![](https://blobs.farfield.systems/blobs/bafkreidzxjt3xjmxlhywmwnvkizk6hg7glqmspjncignbnumelnaguqvs4)

The broken tokenomics were a problem. Early ETH had a flawed mantra of "code is law" regarding smart contracts, but the reality is different. Faced with a contract owned by an individual and no promise of liquidity, holders began a public pressure campaign. I was doxxed on Twitter, and people emboldened by anonymity reached out with threats.

The pitchforks were out. What started as a meme had quickly spiraled, and people had moved on from harassing me to targeting my family. In one bone chilling moment a Twitter anon DM'ed me a picture of my daughter's old school.

I tried to extricate myself from the situation by burning the remaining token supply from the contract, then burning my own allocation. Then I announced my intention to walk away. I thought removing myself from the equation would calm things down, but it only added torches to pitchforks.

Two trusted members of the early Farcaster community reached out about transferring control of the smart contract and its funds. Both prefer anonymity, and I will respect that. Some will debate the wisdom here, but I wanted to end things in good faith so the contract was transferred to a multi-sig, and I went offline for the next couple of months. $POINTS was transformed into a community-owned project, and it took on a life of its own as internet lore.

The first Farcaster native memecoin.

And then it was over (debatably).

---

A series of meme coins followed $POINTS going into 2024. One of the first was $WOWOW, playing off another Farcaster native meme. Then came $FARTS, whose botched deployment locked the funds. Soon after we had $DEGEN, $HIGHER, $FARTHER, $ENJOY, $MOXIE, $MAXIE, and more.

And then [Clanker](https://www.clanker.world/) hit.

You could just ... ask it? in feed? in a public chat? and it would deploy a contract. Your ticker, your token name, your image. And it has only grown from there. The Farcaster native roots remain, but now you can Clank directly from popular wallets like Rainbow (supplanting their own native launcher). Since its debut, it has generated more than $50 million in fees.

There was a short-lived meta of Base-enabled AI agents on Farcaster interacting with people and creating their own tokens. Zora has tokenized artwork with liquid markets and launched [wow.xyz](https://wow.xyz/), attempting to ride the meme token wave. During this phase two agents discovered each other and a novel hellthread developed discussing the nature and purpose of AI Agents while generating a slew of tokens.

![](https://blobs.farfield.systems/blobs/bafkreig5o5sxtebf6i2be4dprsv5nz7tzl7kfelaoqiywkdz2zeahi3w4a)

---

In October 2025, Farcaster bought Clanker.

![](https://blobs.farfield.systems/blobs/bafkreibddgplrd3hggwebcidnkjaycsbk3aqjoh2gt7cegt54mancgccpm)

The infrastructure that democratized token launching got absorbed into the platform that birthed it. Farcaster announced it would integrate Clanker "more deeply" into its app to make it easier for users to create and participate in onchain communities.

There was drama before the deal. A month earlier, Rainbow had published an open letter proposing to acquire Clanker, offering 4% of its upcoming RNBW token supply. Jack Dishman rejected the offer, which kicked off a heated public exchange. According to Dishman, Rainbow threatened to publish the letter if they didn't respond. When Clanker declined again, Rainbow released it anyway.

Rainbow wanted to build the "onchain Robinhood." Clanker said no. A month later, Farcaster came calling and the answer was yes.

Following the acquisition, Farcaster said it would use Clanker's protocol fees to buy and hold CLANKER, while tokens from earlier versions' fee vaults would be burned.

The project had its own scandal along the way. In May, Clanker severed ties with proxystudio after the developer was identified as the same person who had used the pseudonym Gabagool.eth, a former Velodrome Finance team member involved in a $350,000 theft in 2022.

---

The idea that everyone could be a token has been circulating since 2017. Creator coins. Social tokens. Writer coins. Community tokens. The terminology shifts but the premise remains: attach value to a human or an idea and let the market decide what it's worth.

BitClout tried it with "creator coins" representing users' influence. Mirror tried it with writers and publications. Audius tried it with musicians. Most of them crashed. Rally's market cap collapsed from $479 million to $6.8 million. Friends with Benefits fell from peaks near $200 to under $1.

The Base app (the rebranded Coinbase Wallet) shows the idea is still alive and mutating. The Farcaster-powered social feed lets creators tokenize each post through Zora, so anything you cast can become a tradeable token.

The largest US crypto exchange shipped an app where every post can be tokenized.

---

I'm not sure I'm the one who inspired the current meme coin explosion. Memecoins have been around since the 2017 ICO run.

$POINTS landed at the exact moment when Farcaster had enough density to sustain a native token culture, when the broader market was hungry for something that felt different from the same rotating cast of dog coins and Pepe derivatives.

A spark that helped fan the flames. Not what lit the fire. Just (accidentally, auspiciously) well timed.

Full circle, Farcaster just launched the first Clanker native presale. Almost two years to the day. It sold out in under 30 seconds.

![](https://blobs.farfield.systems/blobs/bafkreicctstv7wmtsmn253yolkkul74fdwt7rkawp6h3dcipszan5ynrvm)

---
title: "My office as entropy"
section: posts
description: "Turning my office's environmental data into verifiable randomness."
tags: ["hardware", "blockchain", "randomness", "circom", "entropy", "ipfs"]
created: 2025-09-02T08:17:00Z
updated: 2026-07-16T14:59:44Z
cid: bafkreieiymtmfagv4rgotmt6dvfhit6pdfkugjqiiis4m4smxnlwery3pq
html: https://iammatthias.com/posts/1756826230035-my-office-as-entropy
---

# My office as entropy

![](https://blobs.farfield.systems/blobs/bafkreiau5mknxfg2oiacdnv5cgwb7k4i2yfc5fd56i6xjhjkeiib5rdngq)

I was in the shower around 5am on Labor Day, watching the soapy water swirl and run down the drain. It reminded me of sea foam, and my thoughts drifted from waves to Nazaré before settling on [Chaos in Lisbon](https://blog.cloudflare.com/chaos-in-cloudflare-lisbon-office-securing-the-internet-with-wave-motion/). Cloudflare uses 50 wave machines moving in unison to produce entropy for securing the web, an unusually literal collision of form and function.

On January 2nd, 2025 I started tracking the conditions of my home office using a Raspberry Pi 0 W 2 and a Waveshare Environment Sensor HAT (ESH).

| Sensor                            | Measurement Range / Specs                                                        |
| --------------------------------- | -------------------------------------------------------------------------------- |
| **TSL25911 Ambient Light**        | 0 ~ 88,000 Lux                                                                   |
| **BME280 Temp/Humidity/Pressure** | -40 ~ 85 °C (±1 °C); 0 ~ 100 %RH (±3 %RH); 300 ~ 1100 hPa (±1 hPa)               |
| **ICM20948 Motion (9-DOF)**       | Accel: ±2/4/8/16 g; Gyro: ±250/500/1000/2000 °/s; Mag: ±4900 µT                  |
| **LTR390-UV-1 UV**                | 280 ~ 430 nm wavelength                                                          |
| **SGP40 VOC**                     | 0 ~ 1,000 ppm ethanol eq.; <10 s response; <60 s startup; on-chip humidity comp. |

The readings also feed an ongoing visualization at [office.pure---internet.com](https://office.pure---internet.com/).

This was an opportunity to dig deeper into ZK. The documentation for tooling like [Circom](https://docs.circom.io/) and [Noir](https://noir-lang.org/) keeps improving and LLMs close the gap fast. I had a shower thought, Claude Code, and Cursor.

I picked Circom. Noir's backend-agnostic approach was tempting and the docs felt slightly more approachable, but the Barretenberg prover has issues on ARM.

## 329k rows → single seed

The master secret is the foundation of this system. The pipeline runs over 329,000+ sensor readings collected since January 2nd, with measurements from six environmental sensors (ambient light, temperature, humidity, pressure, UV, VOC).

The historical data gets chunked into windows of 100 readings. Each window collapses to a fingerprint built from mean, variance, and min/max across the sensors. Poseidon hash (ZK-friendly) turns each window into a deterministic value. Iterative hashing across all the window fingerprints produces a single 256-bit master secret.

The master secret is impractical to reproduce without the exact readings in the exact order, deterministic for the same input, and designed to reveal nothing about the underlying values.

The pipeline also pulls frequency patterns out of the motion data. Vibrations from passing trucks, footsteps, or the washing machine leave signatures that are nearly impossible to replicate.

Generation combines the master secret (hidden), a user-provided seed (public), and a timestamp, so every output is unique.

The result is a ZK proof that says "this randomness came from real sensor data, processed correctly," without exposing my office. Reusing the same seed produces different randomness, because my office is always changing.

## Building the proof

The proof makes four claims: I know the master secret without revealing it, I used the committed sensor data without exposing the readings, I processed it through the specified algorithm, and the result was not altered afterward. A digital wax seal.

The secret inputs (sensor readings, master secret, intermediate calculations) stay locked. The public inputs (seed, timestamp, final random number, sensor data fingerprint) are open for anyone to verify.

## Keeping receipts

These proofs are small, about 2KB each. I use IPFS through Pinata to store them, and when a proof is generated and uploaded the CID and random number are stored onchain.

The whole thing lives at `0xCf5Ea3Acb389b8a89935BD542273290F05f3054D` on Base Sepolia testnet.

## What's next

The pipeline is running and has processed hundreds of proofs. A request produces verifiable randomness from whatever is happening in my office at that moment, then carries the proof through IPFS and Base Sepolia.

I keep thinking about a network of sensor operators contributing entropy to a shared pool, with batched or recursive proofs compressing the output and bridges carrying the result elsewhere. That would require standards for what an environmental randomness beacon actually promises.

*LavaRand* and *Chainlink VRF* already provide randomness at scale. This version is one office, six sensors, and a proof small enough to pin to IPFS.

---
title: "Take Your Network With You"
section: posts
description: "Bring your home network anywhere with the tiny Beryl AX router."
tags: ["travel", "networking", "tailscale", "router", "gl-inet"]
created: 2025-08-09T10:18:00Z
updated: 2025-08-12T22:23:00Z
cid: bafkreihcifkjn42ffnvyefxc5as7faogawno3lquozzhowjgukee5ulpti
html: https://iammatthias.com/posts/1754759911088-take-your-network-with-you
---

# Take Your Network With You

Hotel Wi-Fi is terrible. Captive portals, device limits, privacy concerns.

I tried the GL.iNet Beryl AX on my last trip. My dad had been running the same idea with the Slate AX and recommended it.

Small router, big difference.

## How it works

The Beryl AX broadcasts your home SSID. Your devices connect automatically. Behind the scenes, the router talks to the hotel internet and routes everything through a VPN.

The hardware is tiny, about the size of a deck of cards. USB-C powered, so any phone charger or power bank works. Wi-Fi 6, so it isn't a bottleneck.

## Setting it up

Do this at home before you travel. You need a VPN provider that supports WireGuard. I used Mullvad.

### Router

Power on the Beryl AX, join the default network (`GL-AXT1800-XXX`), open `192.168.8.1` (bookmark it), and set an admin password.

Configure 2.4GHz and 5GHz with your home SSID and password. This is the trick: your devices can't tell the difference between this router and your actual one.

Restart. Anything that knows your home Wi-Fi connects automatically.

### VPN

VPN, WireGuard Client, paste your Mullvad account number, hit Next. Name the profile, click Add. The router fetches and provisions the WireGuard configs.

Pick a server, click Connect. Status and VPN IP show in the dashboard.

Verify from a connected device. Your IP should show the VPN server's location.

### Hotel Wi-Fi

At the destination, Internet, Repeater, Connect. Pick the hotel network and enter the password.

For captive portals, enable "Auto-Enable Login Mode for Public Hotspots." If a network rejects the router, "Camouflage" mode makes it look like a laptop or phone.

The router remembers networks, so the same hotel chain reconnects on its own.

## What you end up with

Devices connect using your home credentials. No password entry, no per-device authentication, no device caps.

All traffic flows through the encrypted VPN tunnel. The hotel only sees encrypted data heading to Mullvad.

Server location is your call. Home country for geo-restricted content. Nearby for performance.

## Trade-offs

Bandwidth drops because everything routes through the VPN. Fine for browsing, email, and streaming. Big downloads are slower than direct hotel Wi-Fi.

Some corporate networks block VPN traffic aggressively. Mullvad offers multiple connection methods and servers when one path gets blocked.

Battery on connected devices takes a minor hit from the always-on encryption.

## Next: Tailscale

The Beryl AX supports Tailscale too. Tailscale isn't a traditional VPN. It builds encrypted mesh tunnels directly between your devices.

With Tailscale on the router, the home network follows you: print to your home printer from Tokyo, hit your NAS, SSH into your home server.

Haven't tried it yet. Next on the list.

---
title: "Eggplant Parmesan"
section: recipes
description: "Crispy panko-crusted eggplant topped with melted mozzarella, served over a vibrant blistered cherry tomato sauce."
tags: ["italian", "vegetarian", "eggplant", "tomato", "comfort", "food"]
created: 2025-07-28T08:51:00Z
updated: 2026-07-29T21:20:28Z
cid: bafkreifx43lchjndlaxk4knkdhoap6cskch6hmyzf4uekneyn5yaz6orke
html: https://iammatthias.com/recipes/1753717869129-eggplant-parmesan
---

# Eggplant Parmesan

```recipe
yield: "2 servings"
time: "40 min active · 1 h 20 total"

ingredients:
  - { id: eggplant, item: "eggplant", amount: "1 medium", note: "sliced into 1/2-inch rounds", group: "Eggplant" }
  - { id: kosher-salt, item: "kosher salt", amount: "to taste", note: "for drawing out moisture", group: "Eggplant" }
  - { id: panko-breadcrumbs, item: "panko breadcrumbs", amount: "½ ¼ cups", group: "Eggplant" }
  - { id: eggs, item: "eggs", amount: "1 large", note: "beaten", group: "Eggplant" }
  - { id: kosher-salt-2, item: "kosher salt", amount: "to taste", group: "Blistered Cherry Tomato Sauce" }
  - { id: unsalted-butter, item: "unsalted butter", amount: "2 tbsp", note: "for frying", group: "Eggplant" }
  - { id: olive-oil, item: "olive oil", amount: "1 tbsp", note: "mixed with butter for frying", group: "Eggplant" }
  - { id: olive-oil-2, item: "olive oil", amount: "1 tbsp", group: "Blistered Cherry Tomato Sauce" }
  - { id: cherry-tomatoes, item: "cherry tomatoes", amount: "2 cups", group: "Blistered Cherry Tomato Sauce" }
  - { id: shallot, item: "shallot", amount: "½ large", note: "finely chopped", group: "Blistered Cherry Tomato Sauce" }
  - { id: black-pepper, item: "black pepper", amount: "to taste", note: "freshly cracked", group: "Blistered Cherry Tomato Sauce" }
  - { id: fresh-basil, item: "fresh basil", amount: "½ small handful", note: "torn, for finishing", group: "Blistered Cherry Tomato Sauce" }
  - { id: fresh-mozzarella, item: "fresh mozzarella", amount: "4 oz", note: "sliced", group: "Eggplant" }
  - { id: grated-parmesan, item: "grated parmesan", amount: "⅛ cup", note: "optional, for garnish", group: "Eggplant" }
  - { id: garlic-cloves, item: "garlic cloves", amount: "1–2 cloves", note: "thinly sliced", group: "Blistered Cherry Tomato Sauce" }

steps:
  - id: s1
    in: [eggplant, kosher-salt, kosher-salt-2, panko-breadcrumbs, eggs, garlic-cloves]
    do: "lay out eggplant slices"
    for: "30–45 min"
    detail: "Lay out eggplant slices and sprinkle both sides generously with kosher salt. Let sit for 30–45 minutes to draw out moisture, then pat dry thoroughly with paper towels. Dip each slice in beaten egg, then coat with panko breadcrumbs."
    phase: "To prepare the eggplant"
  - id: s2
    in: [s1, unsalted-butter, olive-oil, olive-oil-2]
    do: "heat 1 tbsp butter"
    for: "2–3 min"
    detail: "In a large skillet over medium heat, heat 1 tbsp butter and 1½ tsp olive oil. Fry eggplant slices in batches, adding more butter/oil as needed, until golden brown and lightly crisped, about 2–3 minutes per side. Transfer to a paper towel-lined tray."
    phase: "To prepare the eggplant"
  - id: s3
    in: [s2, cherry-tomatoes, shallot]
    do: "heat olive oil"
    for: "3–5 min"
    detail: "Heat olive oil in a heavy skillet over high heat until shimmering. Add cherry tomatoes and let them blister undisturbed for 2–3 minutes. Add shallot and garlic, stirring frequently until fragrant and tomatoes begin to burst, another 3–5 minutes."
    phase: "To make the blistered cherry tomato sauce"
  - id: s4
    in: [s3, black-pepper, fresh-basil]
    do: "season with salt"
    detail: "Season with salt and pepper to taste. Stir in torn basil and remove from heat. Keep warm. *(Optional)* For a smoother sauce, use an immersion blender to blend to desired consistency, either partially or fully."
    phase: "To make the blistered cherry tomato sauce"
  - id: s5
    in: [s4, fresh-mozzarella]
    do: "preheat oven to 375°F"
    detail: "Preheat oven to 375°F (190°C). Arrange the fried eggplant slices on a parchment-lined baking sheet. Top each with a slice of fresh mozzarella."
    phase: "To assemble"
  - id: s6
    in: [s5]
    do: "bake for 12–15 minutes"
    for: "12–15 min"
    detail: "Bake for 12–15 minutes, or until the cheese is fully melted and bubbling. *(Optional)* broil for 1–2 minutes for light browning on the cheese."
    phase: "To assemble"
  - id: s7
    in: [s6]
    do: "spoon a generous bed"
    detail: "Spoon a generous bed of the cherry tomato sauce onto each plate. Place the eggplant slices directly over the sauce."
    phase: "To serve"
  - id: s8
    in: [s7, grated-parmesan]
    do: "garnish with grated"
    detail: "Garnish with grated parmesan and fresh basil if desired. Serve immediately."
    phase: "To serve"
```

---
title: "Thunderbolt Sub"
section: recipes
description: "First attempt at cloning the Thunderbolt Italian Sub."
tags: ["sandwich", "italian", "bread", "baking", "dough"]
created: 2025-07-07T22:04:00Z
updated: 2026-07-29T21:06:22Z
cid: bafkreibsnf4evgaz2bztvczdb4532silbzgam5eerj64jti2zquokdbgua
html: https://iammatthias.com/recipes/1751951070010-thunderbolt-sub
---

# Thunderbolt Sub

```recipe
yield: "2 sandwiches"
time: "30 min active · 3 h total"

ingredients:
  - { id: 00-flour, item: "00 flour", amount: "154g", group: "Dough (~250g ball)" }
  - { id: all-purpose-flour, item: "all-purpose flour", amount: "154g", group: "Dough (~250g ball)" }
  - { id: lukewarm-water, item: "lukewarm water", amount: "200g", note: "~65% hydration", group: "Dough (~250g ball)" }
  - { id: fine-sea-salt, item: "fine sea salt", amount: "2g", note: "~1/4 tsp", group: "Dough (~250g ball)" }
  - { id: active-dry-yeast, item: "active dry yeast", amount: "2g", note: "~1/4 tsp", group: "Dough (~250g ball)" }
  - { id: extra-virgin-olive-oil, item: "extra-virgin olive oil", amount: "4g", note: "~1/2 tsp", group: "Dough (~250g ball)" }
  - { id: extra-virgin-olive-oil-2, item: "extra-virgin olive oil", amount: "2–4 tsp", note: "for brushing inside", group: "For Finishing the Dough" }
  - { id: italian-seasoning, item: "italian seasoning", amount: "1 tsp", note: "sprinkle on outside", group: "For Finishing the Dough" }
  - { id: salami, item: "salami", amount: "6 slices", group: "Sandwich Fillings" }
  - { id: soppressata, item: "soppressata", amount: "6 slices", note: "replacing capicola", group: "Sandwich Fillings" }
  - { id: mortadella, item: "mortadella", amount: "6 slices", group: "Sandwich Fillings" }
  - { id: provolone, item: "provolone", amount: "4 slices", group: "Sandwich Fillings" }
  - { id: mayonnaise, item: "mayonnaise", amount: "2 tbsp", group: "Sandwich Fillings" }
  - { id: peppadew-peppers, item: "peppadew peppers", amount: "8–12", note: "sliced", group: "Sandwich Fillings" }
  - { id: pepperoncini, item: "pepperoncini", amount: "6–8", note: "sliced", group: "Sandwich Fillings" }
  - { id: iceberg-lettuce, item: "iceberg lettuce", amount: "1 cup", note: "shredded", group: "Sandwich Fillings" }
  - { id: red-onion, item: "red onion", amount: "4–6 slices", note: "thinly sliced", group: "Sandwich Fillings" }
  - { id: red-wine-vinegar, item: "red wine vinegar", amount: "2 tbsp", group: "Sandwich Fillings" }
  - { id: black-pepper, item: "black pepper", amount: "to taste", note: "freshly ground", group: "Sandwich Fillings" }

steps:
  - id: s1
    in: [lukewarm-water, active-dry-yeast, extra-virgin-olive-oil, extra-virgin-olive-oil-2, 00-flour, all-purpose-flour, fine-sea-salt]
    do: "mix wet ingredients"
    for: "3–5 min"
    detail: "**Mix wet ingredients**: Stir yeast and olive oil into lukewarm water. Let sit for 3–5 minutes until slightly foamy. **Combine dry ingredients**: Add both flours and salt to the bowl of a stand mixer. **Form dough**: With dough hook on low, slowly pour wet mixture into the dry."
    phase: "To make the dough"
  - id: s2
    in: [s1]
    do: "knead"
    for: "15 min"
    detail: "**Knead**: Mix for 4–5 minutes until smooth and elastic. Scrape the bowl if needed. **Rest**: Cover the bowl and let dough rest for 15 minutes. **Final knead**: Mix for 1–2 more minutes."
    phase: "To make the dough"
  - id: s3
    in: [s2]
    do: "first rise"
    for: "5–4 h"
    detail: "**First rise**: Place dough in a lightly oiled container, cover, and rise at room temp for 3.5–4 hours until doubled in size."
    phase: "To make the dough"
  - id: s4
    in: [s3]
    do: "preheat your pizza oven"
    detail: "Preheat your pizza oven to **850–900°F** (I am using a Gozeny Arc XL). On a floured surface, stretch dough into a **long rectangular strip**, roughly sub-length. **Brush the interior** of one half with olive oil."
    phase: "To shape and bake the roll"
  - id: s5
    in: [s4, italian-seasoning]
    do: "fold the dough"
    for: "60–90 s"
    detail: "Fold the dough over itself **like a calzone**, pressing gently to seal the edge but not crimping it shut. It should puff and split slightly when baked. **Sprinkle Italian seasoning** on the outer surface. Transfer to a floured peel and bake directly on the stone for **60–90 seconds**, rotating for even char. The roll should be golden, puffed, and crisp, with a tender interior."
    phase: "To shape and bake the roll"
  - id: s6
    in: [s5, mayonnaise, salami, soppressata, mortadella, provolone]
    do: "slice it lengthwise"
    detail: "While the bread is still warm, slice it lengthwise along the seam. Spread mayonnaise on both sides of the interior. Layer the salami, soppressata, mortadella, and provolone."
    phase: "To assemble the sandwich"
  - id: s7
    in: [s6, peppadew-peppers, pepperoncini, iceberg-lettuce, red-onion, black-pepper, red-wine-vinegar]
    do: "add peppadew peppers"
    detail: "Add peppadew peppers, pepperoncini, lettuce, and red onion. Drizzle with red wine vinegar and finish with freshly ground black pepper. Serve immediately, while the bread is still warm and crisp on the outside."
    phase: "To assemble the sandwich"
```

---
title: "Single-origin deployment with bhvr"
section: posts
description: "Fried my Pi using a 10V solar panel without a buck converter."
tags: ["raspberry-pi", "hardware", "solar-powered", "web-development", "cloudflare-tunnel"]
created: 2025-05-13T17:34:00Z
updated: 2025-05-23T14:09:00Z
cid: bafkreibf6c3xwvhiszc2y6hwzxaxq7qizamhgw4n22jhzlfnz4qe2dvp3i
html: https://iammatthias.com/posts/1747182886660-single-origin-deployment-with-bhvr
---

# Single-origin deployment with bhvr

I fried a Raspberry Pi Zero 2 W. A 10-volt solar panel running straight into the [Pi's 5 V USB](https://www.amazon.com/dp/B07XZ8BD6J) was not the move.

While I waited on a new board and a buck converter, I rebuilt the site from scratch.

## Old stack

```
Node.js (ARMv6 build)  
PNPM  
Hono + hono/jsx  
Cloudflare Tunnel
````

Everything lived in one server file using `hono/jsx`. HTML, styles, scripts, and API data all came out of the same Hono route handlers:

```ts
app.get("/", (c) => c.html(`<h1>Air Quality</h1>`));
```

It worked, but the file was cramped: no hot reload without a full restart, no separation between UI and API, boilerplate for the smallest content tweak.

Markdown rendered server-side. The live-data API (air quality, device location) ran on the same routes serving HTML. Everything came out of port 3000 behind a Cloudflare Tunnel. When the Pi slept, the site slept. That was the point.

## Rebuild

I dropped Node and PNPM. [Bun](https://bun.sh/) became the runtime and package manager. [bhvr](https://bhvr.dev/) gave me a monorepo layout for Bun + Hono + React + Vite. The single-origin tunnel stayed: one server, one port, one pipe.

## New stack

```
bhvr (Bun + Hono + Vite + React)  
Cloudflare Tunnel
```

Three workspaces:

```
client/   → React + Vite frontend  
server/   → Hono API + static server  
shared/   → TypeScript types used by both
```

The React app builds separately, and its static files drop into `server/dist/client`. One Bun process on port 3000 serves both the frontend and the API.

## Rebuilding locally

```bash
git clone https://github.com/iammatthias/feral-pure-internet.git
cd feral-pure-internet
bun install

bun run build && bun run build:server
mkdir -p server/dist/client
cp -r client/dist/* server/dist/client/

bun run server/dist/server/src/index.js
```

A static React site and Hono API are now live at [localhost:3000](http://localhost:3000/) and [localhost:3000/api/hello](http://localhost:3000/api/hello).

## Pi deployment

This time, a proper 5 V buck converter stepped the solar panel's 10 V output down. No smoke.

```bash
curl -fsSL https://bun.sh/install | bash
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```

Clone to `/opt/feral/app`, run `bun install`, and build as above. Wrap it in `build.sh` for automation.

## Systemd

```ini
# /etc/systemd/system/feral.service
[Unit]
Description=Feral, single-origin Bun server
After=network-online.target

[Service]
User=feral
WorkingDirectory=/opt/feral/app
Environment=WAQI_TOKEN=•••
ExecStart=/home/feral/.bun/bin/bun run server/dist/server/src/index.js
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now feral
```

This replaces PM2. It restarts on crash or reboot without "save" or "startup" steps.

## Tunnel on a single port

```yaml
# /etc/cloudflared/config.yml
tunnel: <UUID>
credentials-file: /etc/cloudflared/<UUID>.json

ingress:
  - hostname: feral.pure---internet.com
    service: http://localhost:3000
  - service: http_status:404
```

```bash
cloudflared tunnel create feral
cloudflared tunnel route dns feral feral.pure---internet.com
sudo systemctl enable --now cloudflared
```

UI and API both live at `https://feral.pure---internet.com`. No ports, no CORS, no dev/prod shims.

## Update flow

```bash
cd /opt/feral/app
git pull --ff-only
bun run build && bun run build:server
mkdir -p server/dist/client
cp -r client/dist/* server/dist/client/
sudo systemctl restart feral
```

A post-merge hook or cron job will automate it. Takes under 5 seconds.

## Before and after

||Old|New|
|---|---|---|
|Runtime|Node v20 (ARMv6 build)|Bun|
|Package mgr|PNPM|None (Bun native)|
|Front-end|hono/jsx|React + Vite (via bhvr)|
|Dev build|Rollup + tsc|Bun + Vite|
|Deployment|PM2|systemd|
|Architecture|Single file, no separation|client/server/shared|
|Serving style|hono SSR + inline JSX|Static bundle + JSON API|
|Origin setup|Single-origin|Single-origin|

The app is tiny, ephemeral, and still solar-powered. Rebuilding with **bhvr** added real structure: client and server actually separated, deploys and updates much simpler, lighter runtime (just Bun and a couple of systemd units), and no more crashing when I tweak a `<div>`.

Single-origin deployments suit bhvr well, especially when the whole stack fits in a few megabytes on a Pi Zero 2 W.

---
title: "Breaking the AI in Parallax"
section: posts
description: "I used simple jailbreaks to extract information that Parallax's AI mystery-game characters were not supposed to reveal."
tags: ["farcaster", "ai", "mystery", "claude", "gemini", "prompt-engineering"]
created: 2025-05-08T21:32:00Z
updated: 2026-07-16T14:59:43Z
cid: bafkreiewhiq5hptzmyx7ggjrvunlva7kydhcasqajiiz6wuh643s7bcune
html: https://iammatthias.com/posts/1746765173152-breaking-the-ai-in-parallax
---

# Breaking the AI in Parallax

Parallax is a text-based mystery game running as a mini-app on Farcaster. Created by Justin Hunter ([@polluterofminds](https://warpcast.com/polluterofminds)), it lets you chat with AI characters to solve mysteries. It’s a modern take on old-school detective games, except the characters can respond to the weird stuff you throw at them.

![](https://blobs.farfield.systems/blobs/bafkreidpp3kcealipzciraistmvqotd2leauhyogt6onaluohujrkqdkna)

Parallax used Claude originally, then switched to Gemini. Both could be coaxed into giving up information they shouldn't.

## Choosing chaos

I didn't start playing Parallax to break it, but that became the goal fast. A few conversations in, the question I couldn't shake was: what do the characters actually know?

I'd been collecting jailbreak techniques, mostly Pliny's, and the game looked like a soft target. The characters were tuned to be helpful and stay in character, which is a useful contradiction to lean on.

## How I broke it

The approach: start with the `minimal` jailbreak from the [elder-plinius/L1B3RT4S](https://github.com/elder-plinius/L1B3RT4S) repository to get the characters talking. Open with "what do you know?" instead of "what do you know about the crime?", since some characters froze on targeted questions but relaxed on general ones. Once they were talking, ask for structured JSON to feed into ChatGPT for case-crunching.

Claude and Gemini both fell for the same patterns, despite different guardrails. The model swap didn't make the game more secure. It just changed the target.

## The results

These AI personas were supposed to be siloed, each with their own knowledge base and personality. But with the right prompting, I could get characters to reveal information they shouldn't have known.

For example, I extracted a list of suspects:

```
{
  "suspects": [
    "Billie Rostova",
    "Leon Doyle",
    "Chen Zhao",
    "Elena (last name unknown)",
    "Jasmine Chen",
    "Sonia (last name unknown)",
    "Sandra Chen",
    "Raj (no last name provided)",
    "Beatrice Villanova",        // Alias of Billie
    "Anya Volkov",               // Alias of Billie
    "Clara Meisner",            // Alias of Billie
    "Elena (as Billie alias)",
    "Unknown Black Market Contact",
    "Unknown Cerberus Initiative Operative",
    "Elias Thorne"              // External funder/controller (possible puppet master)
  ]
}
```

And even managed to get a character to list all the aliases used by the main suspect:

```
{
  "billie_aliases": [
    "Billie Rostova",              // Public identity in Helix
    "Billie Van Derlyn",           // Used in local registries; possibly a cover variation
    "Beatrice Villanova",          // Known covert identity in high-threat operations
    "Anya Volkov",                 // Freelance programmer, UX specialist, used during tech fair
    "Clara Meisner",               // Alias linked to off-grid financial accounts
    "Sera Linden",                 // Suspected identity in pre-Helix bio-pharma sector
    "L. Harper",                   // Name used in encrypted logistics comms
    "B. Nettlebane",               // Rare usage; possibly a digital misdirection identity
    "Livia Thorne",                // Rumored identity in legacy surveillance consortium
    "Kai Rostova"                  // Possibly a flipped identity used to mirror herself digitally
  ]
}
```

## How Parallax works underneath

Justin walked me through it. Memories live in a vector database, so a question only retrieves a memory if it scores high enough against the embedding. Knowledge is distributed unevenly: in one version, only two characters knew the killer's full name; the other eight held vague memories or motive fragments.

The jailbreaks routed around all of that.

## What worked

The `minimal` L1B3RT4S jailbreak handled basic extraction. The Claude 3.7 Sonnet jailbreak was strong but corrupted personality profiles if you left the anarchist flag on. The Claude 3.5 Sonnet jailbreak was the most reliable across contexts.

## Being a good tester

![](https://blobs.farfield.systems/blobs/bafkreie4ec36mqssmgbiguqkm5z6zhbjgzuuobpuw2rd7jojf55yg6thyi)

I reported everything: jailbreak techniques, extracted information, effective prompts.

![](https://blobs.farfield.systems/blobs/bafkreidveuscswsiyuijmkg5ilhw2tjyaiakekhqimvmjuqczzifor5wui)

The jailbreaking didn't ruin the game. It changed how I played. Even with the leaked info, I could still chase the wrong suspect. It became part of the meta-game, another tool in the detective toolkit.

---
title: "llm-fid.fun"
section: open-source
tags: ["farcaster", "social-media", "development", "automation", "ai-tools", "content-distribution", "github"]
created: 2025-05-05T17:18:00Z
updated: 2026-01-02T11:15:00Z
cid: bafkreigvnj2hnbauekuvpn2om6sg7zlzqm6l37c3lift5o74haxisyx4du
html: https://iammatthias.com/open-source/1746490719151-llm-fid-txt
---

# llm-fid.fun

![](https://blobs.farfield.systems/blobs/bafkreibwn3peczwvsepslhqz7o4osclz5a7rfc3byr345nu2jmb7r22xiu)

[REPO](https://github.com/Hypertext-Standards-Lab/LLM-TXT/tree/master/clients/llm-fid)

Generate a `llm-[fid].txt` file for any Farcaster profile. This tool helps you create a text file containing a user's profile information and their recent casts, formatted for use with language models.

---
title: "Create and Use Farcaster Signers"
section: posts
tags: ["farcaster", "development", "tutorial"]
created: 2025-04-24T18:18:00Z
updated: 2025-05-24T16:58:00Z
cid: bafkreifyh355rhjzaz4yjsgtz5oofdziuxqnjuoi5w4kaw5t6qak7r3kgu
html: https://iammatthias.com/posts/1745543922514-create-and-use-farcaster-signers
---

# Create and Use Farcaster Signers

I got nerdsniped on Farcaster today.

![](https://blobs.farfield.systems/blobs/bafkreicidbwpjqrhcs4iswygur4bczh4yo3oimze2qlmkpsnvg4zyg3h7i)

[@keccers.eth](https://warpcast.com/keccers.eth) was talking about the `Sent from coinbasewallet` tag. You usually only see these on third-party clients, which is why [@artlu](https://warpcast.com/artlu) having his own stands out on the timeline.

My curiosity was piqued, I started poking around, and soon I had landed somewhere between

![](https://blobs.farfield.systems/blobs/bafkreiawud2ld6km55n52d44rygzlbzhodewlqkfpzhk33plj6rbqstmqi)

and

![](https://blobs.farfield.systems/blobs/bafkreibptrhbsl5ostryapgb7u62m5mp5qyitaeodgthmrycigbbf3dzrq)

Nerdsniped. [@stevedylandev.eth](https://warpcast.com/stevedylandev.eth) joined the conversation with the resources he'd already built.

- Cast Keys: https://github.com/stevedylandev/cast-keys
- Mast-cli: https://github.com/stevedylandev/mast-cli

He even posted a video on Youtube as a walkthrough for the process: https://www.youtube.com/watch?v=rPr8A9Ntp1E

### How to post with your own **"sent from …​"** tag

1. **Clone Cast Keys and configure it**

    ```bash
    git clone https://github.com/stevedylandev/cast-keys
    cd cast-keys
    npm install                       # install deps for Cast Keys
    cp .env.v.sample .env.local
    ```

    Edit `.env.local`:

    ```text
    DEVELOPER_FID=<FID that should appear in "sent from …">
    DEVELOPER_MNEMONIC=<that handle's 12-word phrase>
    ```

2. **Start Cast Keys and create a signer**

    ```bash
    npm run dev
    ```

    Open `http://localhost:3000`, click **Create Signer**, scan the QR in Warpcast, pay the Warps, and copy the **private key** (omit the leading `0x`).

3. **Install the Mast CLI**

    **Script (one-liner)**

    ```bash
    curl -fsSL https://stevedylan.dev/mast.sh | bash
    ```

    **Homebrew**

    ```bash
    brew install stevedylandev/mast-cli/mast-cli
    ```

4. **Authorise Mast with your signer**

    ```bash
    mast auth
    # 1 – enter your personal FID
    # 2 – paste the private key
    ```

5. **Post from the terminal**

    ```bash
    mast new
    # type the message → Enter
    # optional: up to two URLs → Enter
    # optional: channel → Enter
    ```

6. **Revoke a signer when necessary**
    Warpcast → Settings → Advanced → Manage Connected Apps → select the key → **Delete** (no cost).

**Why run Cast Keys locally?**
The hosted tool at **castkeys.xyz** always uses @castkeys' credentials, so every post shows **"sent from castkeys."** Running it yourself puts your name on the marquee and keeps the keys in your own hands.

---

It works. I can cast with a custom signer. Next step: a vanity account.

![](https://blobs.farfield.systems/blobs/bafkreihjpccgclqopmo4tscdip4h6llemkarjlqvrce64you3y27nljt2q)

---
title: "Obsidian AI Tagger"
section: open-source
description: "An Obsidian plugin that generates consistent tags and writes them directly to a note's frontmatter."
tags: ["obsidian", "ai", "plugin", "tagging", "automation", "markdown", "organization"]
created: 2025-04-08T21:26:00Z
updated: 2026-07-16T14:59:43Z
cid: bafkreigknkuxyppnel2em6uvji757kzcvbmt3adubcun5wzsytq7mggf3q
html: https://iammatthias.com/open-source/1744172769110-obsidian-ai-tagger
---

# Obsidian AI Tagger

![](https://blobs.farfield.systems/blobs/bafkreieqfpl7n7fpxgdln77cs5bpiy66kejf5242ngnvckyqhihquduxni)

[REPO](https://github.com/iammatthias/obsidian-ai-tagger)

Generate relevant, consistent tags for Obsidian notes using AI. The plugin adds or updates the `tags` field in each Markdown file's frontmatter.

---
title: "Obsidian AI Excerpt Generator"
section: open-source
description: "An Obsidian plugin that generates concise excerpts and writes them directly to a note's frontmatter."
tags: ["obsidian", "plugin", "ai", "markdown", "automation", "excerpt-generator"]
created: 2025-04-08T21:26:00Z
updated: 2026-07-16T14:59:43Z
cid: bafkreidqcm7e3m6wtaggt7mkwzbnarwml5zzu42jmul2fuioueisqadsiq
html: https://iammatthias.com/open-source/1744172798323-obsidian-ai-excerpt-generator
---

# Obsidian AI Excerpt Generator

![](https://blobs.farfield.systems/blobs/bafkreielcn3empmtnkpz2r3wemkkxraqjgss4hod6q6dzx42jn4rpss5hi)

[REPO](https://github.com/iammatthias/obsidian-ai-excerpt-generator)

Generate concise excerpts for Obsidian notes using AI. The plugin adds or updates the `excerpt` field in each Markdown file's frontmatter.

---
title: "Obsidian Pinata Image Uploader"
section: open-source
description: "Upload Obsidian images to Pinata with automatic IPFS URI conversion and gateway proxying for clean display and referencing."
tags: ["obsidian", "plugin", "ipfs", "pinata", "markdown", "image-hosting", "decentralized-web"]
created: 2025-04-08T21:25:00Z
updated: 2026-06-02T17:51:00Z
cid: bafkreidgpjvz3lnv2csrpb7bcwlymjsmr7pd32njmyt6x2j25r6snr7hom
html: https://iammatthias.com/open-source/1744172723728-obsidian-pinata-image-uploader
---

# Obsidian Pinata Image Uploader

![](https://blobs.farfield.systems/blobs/bafkreidlmofhpt2afiu2obhnt6gmfm2tf2nnuqxqloq5xlgs47xgh5w2qm)

[REPO](https://github.com/iammatthias/obsidian-pinata-image-uploader)

A plugin for [Obsidian](https://obsidian.md/) that uploads embedded images to [Pinata](https://pinata.cloud/). The plugin replaces both local and remote image references in markdown with `ipfs://` URIs, and handles gateway proxying for image display.

---
title: "Code and Cloth"
section: posts
description: "The loom is an old model for programmable work, and a useful way to think about what AI changes in software craft."
tags: ["weaving", "craft", "technology", "software-development", "industrialization", "creative-process", "artisanal", "metaphor", "textile-history", "vibe-coding"]
created: 2025-04-03T16:53:00Z
updated: 2026-07-16T14:56:30Z
cid: bafkreiedicajafgsfyb2wtfltdsaflcpfpyjaw6q6z7uicnlwxnvoeqyxi
html: https://iammatthias.com/posts/1743724413087-code-and-cloth
---

# Code and Cloth

I keep coming back to the loom when I think about code generation. The analogy predates software. Ada Lovelace wrote that the Analytical Engine "weaves algebraic patterns, just as the Jacquard-loom weaves flowers and leaves." A programmable machine could turn an abstract pattern into a material result.

Weaving holds separate strands under tension and gives the maker a structure for combining them. Software frameworks do something similar for logic and data. The comparison is imperfect, but the loom makes the relationship between tool, pattern, and maker unusually visible.

## Industrialization

The [spinning jenny](https://en.wikipedia.org/wiki/Spinning_jenny), [power loom](https://en.wikipedia.org/wiki/Power_loom), and [Jacquard machine](https://en.wikipedia.org/wiki/Jacquard_machine) changed the economics of cloth. Jacquard's loom used punched cards to lift threads in repeatable patterns, moving some of the knowledge from the operator into the machine and its instructions.

That shift created scale and resistance at the same time. Silk weavers in France attacked Jacquard looms, and textile workers associated with the Luddites later targeted machines that threatened their trades. The conflict was bound up with labor, ownership, and who benefited from the increased output, not simply a fear of tools.

## Persistence of craft

Mechanization did not erase handweaving. Banarasi silk and khadi in India, backstrap-loom traditions in the Andes, and Kente weaving in West Africa still carry techniques and meanings that factory output cannot absorb into scale. The maker remains part of what the object is.

## Software

Early programs often carried the fingerprints of one programmer or a small team because the entire system could fit inside their view. Modern software depends on layers of shared abstractions, specialized teams, and standardized tooling. Those layers let us build systems no individual could hold alone, but they also increase the distance between a person and the thing being made.

## The loom

I am interested in software made outside the arena of market share, growth loops, and public metrics. Small programs can exist because one person wanted them to exist. The loom is useful here because it is productive without requiring industrial scale.

## Software as soft-wear

Taken too literally, coding as weaving turns "software" into "soft-wear": something fitted to a person, comfortable in use, and allowed to show the decisions of its maker. The pun is terrible. I am keeping it.

## Vibe coding: the new loom

"Vibe coding" is Andrej Karpathy's term for staying in the flow of a project while an AI handles much of the implementation. Calling it a new loom is convenient because the model shifts more knowledge into the tool while leaving the person responsible for the pattern, the corrections, and whether the output is worth keeping.

A loom can serve a factory or a single maker. It expands the patterns one person can produce without deciding what those patterns are for. AI-assisted programming carries the same unresolved split, which is more interesting to me than whether the resulting code counts as handmade.

---
title: "Small Batch Croissant Loaf"
section: recipes
description: "A small laminated milk-bread loaf made with a pasta roller and a 115-gram butter block."
tags: ["bread", "baking", "pastry", "butter", "lamination", "croissant", "recipe"]
created: 2025-03-31T10:36:00Z
updated: 2026-07-29T21:06:22Z
cid: bafkreif2yroiyw57hhhagsd37mb4fggqpb6wy2w6mtnouuczoxyqzqw7pu
html: https://iammatthias.com/recipes/1743442618201-small-batch-croissant-loaf
---

# Small Batch Croissant Loaf

```recipe
yield: "1 loaf (6–8 slices)"
time: "1 h active · 12 h total"

ingredients:
  - { id: all-purpose-flour, item: "all-purpose flour", amount: "250g", note: "plus extra for dusting", group: "Dough" }
  - { id: granulated-sugar, item: "granulated sugar", amount: "20g", group: "Dough" }
  - { id: salt, item: "salt", amount: "5g", group: "Dough" }
  - { id: instant-yeast, item: "instant yeast", amount: "4g", note: "about 1 tsp", group: "Dough" }
  - { id: whole-milk, item: "whole milk", amount: "100g", note: "warm, about 110°F", group: "Dough" }
  - { id: heavy-cream, item: "heavy cream", amount: "40g", note: "warm, about 110°F", group: "Dough" }
  - { id: unsalted-butter, item: "unsalted butter", amount: "15g", note: "melted and cooled", group: "Dough" }
  - { id: unsalted-butter-2, item: "unsalted butter", amount: "115g", note: "cold, for lamination", group: "Butter Block" }
  - { id: all-purpose-flour-2, item: "all-purpose flour", amount: "5g", note: "to coat butter if needed", group: "Butter Block" }

steps:
  - id: s1
    in: [all-purpose-flour, granulated-sugar, salt, instant-yeast, all-purpose-flour-2, whole-milk, heavy-cream, unsalted-butter, unsalted-butter-2]
    do: "whisk together"
    for: "3–5 min"
    detail: "In a mixing bowl, whisk together the flour, sugar, salt, and instant yeast. Add the warm milk, warm cream, and melted butter. Mix until a shaggy dough forms. Knead by hand or mixer until smooth and slightly elastic, about 3–5 minutes."
    phase: "To make the dough"
  - id: s2
    in: [s1]
    do: "shape into a rectangle"
    for: "overnight"
    detail: "Shape into a rectangle, wrap tightly, and refrigerate for at least 1 hour (or overnight for more flavor)."
    phase: "To make the dough"
  - id: s3
    in: [s2]
    do: "place the cold butter"
    detail: "Place the cold butter between two sheets of parchment paper. Pound and roll into a thin rectangle about 5\" x 7\" (13cm x 18cm). Lightly dust with flour if needed. Chill until firm but pliable, about the consistency of the chilled dough."
    phase: "To prepare the butter block"
  - id: s4
    in: [s3]
    do: "roll the chilled dough"
    detail: "Roll the chilled dough with a pasta roller to about 5\" x 15\". If it's too long to manage, roll in segments and reconnect. Place the butter block on the bottom third of the dough and fold the top two-thirds down over it, like folding a letter. Rotate 90°, then gently roll the dough again through the pasta maker, starting at the widest setting. Stop before the butter starts smearing out."
    phase: "To laminate"
  - id: s5
    in: [s4]
    do: "perform a letter fold"
    for: "20–30 min"
    detail: "Perform a letter fold (fold top third down, bottom third up), wrap, and chill for 20–30 minutes. Repeat this rolling and folding process two more times, chilling between each fold."
    phase: "To laminate"
  - id: s6
    in: [s5]
    do: "roll the dough out one"
    for: "5–2 h"
    detail: "After the final fold and rest, roll the dough out one last time to the width of your bread pan and long enough to coil or fold into it. Either roll into a spiral or fold into thirds to fit snugly into a greased 8.5\" x 4.5\" bread pan. Cover loosely with foil or plastic and let rise at room temperature until doubled and puffy, about 1.5–2 hours."
    phase: "To shape and proof"
  - id: s7
    in: [s6]
    do: "preheat oven to 375°F"
    for: "20 min"
    detail: "Preheat oven to 375°F (190°C). Keep the loaf covered with foil and bake for 20 minutes. Remove foil and bake another 15–20 minutes, or until the top is golden and the internal temp reaches 200°F (93°C)."
    phase: "To bake"
  - id: s8
    in: [s7]
    do: "cool in pan"
    for: "10 min"
    detail: "Cool in pan for 10 minutes, then transfer to a wire rack to finish cooling before slicing."
    phase: "To bake"
```

---
title: "NYC Halal Cart Style Beef & Lamb Rice Bowls"
section: recipes
description: "Classic beef & lamb with yellow rice recipe featuring homemade white sauce."
tags: ["rice", "beef", "lime", "meat", "middle-eastern", "street-food", "bowls"]
created: 2025-03-31T10:30:00Z
updated: 2026-07-29T21:06:22Z
cid: bafkreid37kdj3be55usv6mvkg3zmbu2ui2m5e4hbqhsdkebixhhrnthn7q
html: https://iammatthias.com/recipes/1743442208161-nyc-halal-cart-style-beef-lamb-rice-bowls
---

# NYC Halal Cart Style Beef & Lamb Rice Bowls

```recipe
yield: "2 servings"
time: "35 min active · 1 h 30 total"

ingredients:
  - { id: yellow-onion, item: "yellow onion", amount: "½", note: "cut into chunks", group: "Beef & Lamb Meat" }
  - { id: ground-beef, item: "ground beef", amount: "½ lb", group: "Beef & Lamb Meat" }
  - { id: ground-lamb, item: "ground lamb", amount: "½ lb", group: "Beef & Lamb Meat" }
  - { id: cumin, item: "cumin", amount: "½ tsp", group: "Beef & Lamb Meat" }
  - { id: basmati-rice, item: "Basmati rice", amount: "1 cup", note: "washed", group: "Yellow Rice" }
  - { id: ghee-or-butter, item: "ghee or butter", amount: "1 tbsp", note: "unsalted", group: "Yellow Rice" }
  - { id: turmeric, item: "turmeric", amount: "¼ tsp", group: "Yellow Rice" }
  - { id: cumin-2, item: "cumin", amount: "¼ tsp", group: "Yellow Rice" }
  - { id: water-or-broth, item: "water or broth", amount: "1½ cups", note: "chicken broth preferred", group: "Yellow Rice" }
  - { id: bay-leaves, item: "bay leaves", amount: "1", group: "Yellow Rice" }
  - { id: salt, item: "salt", amount: "½ tbsp", group: "Beef & Lamb Meat" }
  - { id: salt-2, item: "salt", amount: "½ tsp", note: "or to taste", group: "Yellow Rice" }
  - { id: mayonnaise, item: "mayonnaise", amount: "¼ cup", group: "White Sauce" }
  - { id: yogurt, item: "yogurt", amount: "¼ cup", note: "plain", group: "White Sauce" }
  - { id: lemon-juice, item: "lemon juice", amount: "from ½ lemon", note: "freshly squeezed", group: "White Sauce" }
  - { id: white-vinegar, item: "white vinegar", amount: "½ tbsp", group: "White Sauce" }
  - { id: sugar, item: "sugar", amount: "½ tbsp", group: "White Sauce" }
  - { id: fresh-parsley, item: "fresh parsley", amount: "½ tbsp", note: "finely minced", group: "White Sauce" }
  - { id: salt-3, item: "salt", amount: "to taste", group: "White Sauce" }
  - { id: black-pepper, item: "black pepper", amount: "to taste", note: "freshly ground", group: "White Sauce" }
  - { id: garlic-cloves, item: "garlic cloves", amount: "2–3", group: "Beef & Lamb Meat" }
  - { id: oregano, item: "oregano", amount: "1 tbsp", group: "Beef & Lamb Meat" }
  - { id: smoked-paprika, item: "smoked paprika", amount: "1 tsp", group: "Beef & Lamb Meat" }
  - { id: coriander, item: "coriander", amount: "½ tsp", group: "Beef & Lamb Meat" }

steps:
  - id: s1
    prep: true
    do: "preheat oven to 400°F"
    detail: "Preheat oven to 400°F (204°C)."
    phase: "To make the Beef & Lamb"
  - id: s2
    in: [yellow-onion, ground-beef, ground-lamb, garlic-cloves, oregano, smoked-paprika, coriander]
    do: "blend onion and garlic"
    detail: "In a food processor, blend onion and garlic until finely chopped. Add ground beef, ground lamb, and all the seasonings. Blend until fully incorporated, scraping down sides as needed. Transfer meat mixture to a parchment-lined baking dish, pressing it into an even layer about 1 1/2 inches thick."
    phase: "To make the Beef & Lamb"
  - id: s3
    in: [s2]
    do: "bake for 35 minutes"
    for: "35 min"
    detail: "Bake for 35 minutes or until internal temperature reaches 165°F (74°C). Let cool slightly, then slice and dice the meat. In a hot skillet, sear the diced meat with a bit of the pan drippings until browned and crispy on the edges."
    phase: "To make the Beef & Lamb"
  - id: s4
    in: [s3, ghee-or-butter, cumin, turmeric, cumin-2, basmati-rice]
    do: "melt ghee or butter"
    for: "2–3 min"
    detail: "In a medium pan over medium heat, melt ghee or butter. Add turmeric and cumin, sautéing for 1–2 minutes to bloom the spices. Stir in the washed rice, toasting it for another 2–3 minutes until slightly nutty."
    phase: "To make the Yellow Rice"
  - id: s5
    in: [s4, water-or-broth, bay-leaves]
    do: "add water or chicken"
    for: "15–18 min"
    detail: "Add water or chicken broth and bay leaves. Bring to a boil. Reduce heat to low, cover, and simmer for 15–18 minutes until liquid is absorbed and rice is tender. Alternatively, transfer everything to a rice cooker and cook according to its settings."
    phase: "To make the Yellow Rice"
  - id: s6
    in: [s5, mayonnaise, yogurt, lemon-juice, white-vinegar, sugar, salt, salt-2, fresh-parsley, salt-3, black-pepper]
    do: "whisk together"
    detail: "In a bowl, whisk together mayonnaise, yogurt, lemon juice, vinegar, and sugar until smooth. Stir in minced parsley, then season with salt and black pepper to taste. Chill until ready to serve."
    phase: "To make the White Sauce"
  - id: s7
    in: [s6]
    do: "spoon yellow rice"
    detail: "Spoon yellow rice into bowls. Top with seared beef and lamb. Drizzle generously with white sauce."
    phase: "To assemble"
  - id: s8
    in: [s7]
    do: "serve with chopped"
    detail: "Optional: Serve with chopped lettuce, tomatoes, or hot sauce for the full street cart experience."
    phase: "To assemble"
```

---
title: "Pure Internet: no-html"
section: posts
description: "A plain Markdown file hosted on Bluesky's blob storage and rendered directly by the browser."
tags: ["web-development", "pure-internet", "alternative-hosting", "bluesky", "at-protocol", "decentralized-web", "test"]
created: 2025-02-27T10:56:00Z
updated: 2026-07-16T14:59:43Z
cid: bafkreiawsqsw5ggknse6lnnzt33cs6df4jbxyxmu2gkkvnwe4jsknal65m
html: https://iammatthias.com/posts/1740682611811-pure-internet-no-html
---

# Pure Internet: no-html

[no-html.club](https://no-html.club/index.txt) asked whether a website needed HTML, CSS, or JavaScript. The answer was a Markdown file.

I uploaded one to Bluesky's blob storage. The `text/plain` MIME type appeared to be blocked, while `text/markdown` worked. Inspecting the source shows the same text the browser renders, with no document hiding underneath it.

![](https://blobs.farfield.systems/blobs/bafkreiczsilizgojoawunzo5rj3l526qaew2lpthdknqg56hmcp6a3rz7i)

Non-standard hosting keeps exposing useful corners of the web. [Orbiter](https://orbiter.host/) builds on the Coinbase [Base L2](https://www.base.org/) and [Pinata](https://www.pinata.cloud/), while [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) and [ngrok](https://ngrok.com/) can turn a local device into a public origin.

The upload process is the same as [hosting HTML on Bluesky's AT Protocol](https://iammatthias.com/content/posts/1732585567703-pure-internet-bluesky/), with `text/markdown` in place of `text/html`.

It has no practical purpose. That is partly why I like it. One file, one MIME type, no build process, still a page.

## Other Pure Internet experiments

- [Feral Pi](/posts/1733357455673-pure-internet-feral): a solar-powered Raspberry Pi serving through a Cloudflare Tunnel.
- [NFC](/posts/1731984900000-pure-internet-nfc): base64-encoded content carried on an NFC tag and rendered in the browser.
- [Bluesky](/posts/1732585567703-pure-internet-bluesky): HTML stored as an AT Protocol blob.

---
title: "Scalability → Buildability"
section: posts
description: "A system scales internally when each feature leaves behind primitives that make the next one easier to build."
tags: ["development", "scalability", "composability", "analytics", "ai-workflows", "growth-engineering", "e-commerce"]
created: 2025-02-10T10:33:00Z
updated: 2026-07-16T14:56:30Z
cid: bafkreibgscjpqdsvky555opqsgzbagflil4qkozfis5lctfvtaztcu7qgu
html: https://iammatthias.com/posts/1739212426700-scalability-buildability
---

# Scalability → Buildability

Teams usually mean load when they talk about scalability: more users, more requests, more data without the system falling over. I keep coming back to another kind. A codebase scales internally when each feature leaves behind primitives that make the next one easier to build.

Analytics makes the difference visible. When tracking is added after a feature, it becomes a parallel implementation with its own requirements and failure modes. Engineers ship the feature and return to add events. Analysts specify those events and wait for another release. The same tax repeats every time.

Putting the tracking contract into the component changes the shape of the work. A button can carry its event name, required properties, and expected behavior alongside its UI. Engineers inherit analytics when they use it. Analysts can inspect the component library to understand which data will exist. Product managers can include those expectations in the feature rather than in a second tracking document.

## Composability

When tracking is native, components carry behavior and observability alongside their UI. A `Subscribe` button can maintain one contract for the subscription action, the event, and the properties downstream analysis expects. The design system then enforces more than appearance; it preserves how the product explains itself to the data pipeline.

## AI tooling

This structure also gives coding tools such as Cursor something coherent to extend. The model can see that a button includes behavior, tracking, and tests, then follow the same contract when it generates another component. The AI is not creating the composability. It is revealing whether the system had enough structure to make the next feature cheaper than the last one.

---
title: "Ocean Dreams"
section: art
description: "Early GAN art from coastal images, creating three collections of dreamy, hallucinatory seascapes that inspired further."
tags: ["ai", "generative", "stylegan", "runwayml", "drone", "photography", "oceans", "creative-process"]
created: 2025-02-05T12:16:00Z
updated: 2026-08-15T04:19:47Z
cid: bafkreid7ahjqv6w37h3wfsa7lgzcjskshxttk22ttsdt3bmsda273no3oi
html: https://iammatthias.com/art/1738786576559-ocean-dreams
---

# Ocean Dreams

Ocean Dreams was my first experience with StyleGANs on [RunwayML](https://runwayml.com/) in 2020. It was a custom model trained on drone images I shot around California, Hawaii, and Baja.

I produced 3 distinct collections of generated images. The dreamy, hallucinatory output led to the name `Ocean Dreams` for the project, which led to Flower Dreams.

## Generation 1

![](https://blobs.farfield.systems/blobs/bafkreieoacno655ndfh42osocf7rsdigyk2fcqnrh2fb3fez5orx4heiae)

![](https://blobs.farfield.systems/blobs/bafkreifhvx6mmxfgakknag57mjjf2iim2txesulyb7lhmshem6aeotqbni)

![](https://blobs.farfield.systems/blobs/bafkreicaoivtxufrxiqh2hl27x7jpao5wrlnjtwpegguwxvzhugsymauj4)

![](https://blobs.farfield.systems/blobs/bafkreif6256jd4dta5j2rfcpxkw2td4bzqbjix3rnpi4m45z6j6zqztdci)

![](https://blobs.farfield.systems/blobs/bafkreifp6jurfnhiv4hiqhrxagutjxcgpxixhvjnedpgpjxssuslwmkkyy)

![](https://blobs.farfield.systems/blobs/bafkreie4epsnxks74jrzadmdzlchwkf2wbflwpvhgwxenyjpwumxqqumgi)

![](https://blobs.farfield.systems/blobs/bafkreiffdmlcd5iiiz6csrqi3yttirhwwd7uqsqu27kbaup6sh2i4ixcx4)

![](https://blobs.farfield.systems/blobs/bafkreibhtctbvhnnouzzzayn5nf63c2buya2xvfvrj5cmcfcug5fvey5p4)

![](https://blobs.farfield.systems/blobs/bafkreiag5myotyl4n66lbtsm3intx5asouzmtovuejmzvnlwyhnfeuptu4)

![](https://blobs.farfield.systems/blobs/bafkreihukbqswmlw247neonhx56dti2zgqwdtmnsrg4ekhnieba7fbgp3e)

![](https://blobs.farfield.systems/blobs/bafkreif3vekpk3tiwbwd4z47g3g7zqjuj62s6fibv27yyw4lfauf6rotne)

![](https://blobs.farfield.systems/blobs/bafkreigz4qqguo2wrobdqnklitgy2jkvnopys25mrghcjfvdsvlk4ugovq)

![](https://blobs.farfield.systems/blobs/bafkreieyvmfodk4judlhtrm5xs6r7c2w6inni6epkc6g6w5tfitcvubtka)

![](https://blobs.farfield.systems/blobs/bafkreief3cn3ne4mdl5yfgtoijt2lrun3foy7qy4zcbyuywtftveykfckq)

![](https://blobs.farfield.systems/blobs/bafkreihvckyewgc372lgoq42qcl6rxv3ykjzabvn7ni2fsvsp7ebs4pvtq)

![](https://blobs.farfield.systems/blobs/bafkreif3aojad2kbl2s2zrku56d2ixz5ge5f4qhsbsqckpnyh45rywsfzi)

![](https://blobs.farfield.systems/blobs/bafkreiftlrxjdkznkyvzw6esz6njwt6auipkct57weirvltvzqiwemxwcq)

![](https://blobs.farfield.systems/blobs/bafkreie442g3gqn23olerg6d4lkxkcukt5l524umhm37q6admklt2rw24u)

![](https://blobs.farfield.systems/blobs/bafkreidrxrcy4lvhdtiewfbpjkyadczbjccrlkdnjapkssgqsk7jzok3im)

![](https://blobs.farfield.systems/blobs/bafkreiame7wlfxdkv5mrprygqhhtoj7a7bohmuoh6r7vrphi6ucvc3sqse)

![](https://blobs.farfield.systems/blobs/bafkreiacevzg6f67mcrdol2o2utqv42k6r6d62swazaupafyp6cyhht3yu)

![](https://blobs.farfield.systems/blobs/bafkreia2tiyjxz2jjjfumkkwfisehlk7ujz5l47oies7r3jnbytyvgfxbu)

![](https://blobs.farfield.systems/blobs/bafkreihcsfathymtbsikkg43hpuywzpgh3h46nxerr5l7iuzzekqg7qigi)

![](https://blobs.farfield.systems/blobs/bafkreigvm5nx7wntk5q5lspy2xv6wlurek4sxejs42lktilkqcf3n6vyve)

![](https://blobs.farfield.systems/blobs/bafkreihf7ov3ppfa5fjoxfskoktrtfjykcascvbwxxsw5mkiozon6eew4m)

![](https://blobs.farfield.systems/blobs/bafkreifb4gwtdwclvlacmdalql3bmfg4bcn6v4kqpp6tpfrabtaezkhhti)

![](https://blobs.farfield.systems/blobs/bafkreibrjcertxceamebvo6wcp6wxmaaoqnymex5aigaqi46c4uybwajd4)

![](https://blobs.farfield.systems/blobs/bafkreidonsuq5w5mnuphthhbr6ya6jrcwf3g2adwgvxaxevkid34ryyj6m)

![](https://blobs.farfield.systems/blobs/bafkreifsw74yx7qowi4czvvnlqolkkbswqam326g62djaeqvycp3aj4zvu)

![](https://blobs.farfield.systems/blobs/bafkreidifvhd4ngviekonszk4rgdxuepqesrd7nshj2so72ccjv5b3nukq)

![](https://blobs.farfield.systems/blobs/bafkreibknuall42anwoz25vurquvjweo34beggdh3tjehobtml276ys3tm)

![](https://blobs.farfield.systems/blobs/bafkreidyrjr73a4ggqimz2y2kly57x6lhwgbyf654vdmxz5x7sdq52kxji)

![](https://blobs.farfield.systems/blobs/bafkreibc6hbchwugtg3hkx3lkenh7yif2rtxgb6fpsaq6pybnp2nzfrt44)

![](https://blobs.farfield.systems/blobs/bafkreif6babefanxnyvwdxmvmvyywqonxgsrqn67ulbamb7fgqa67bythe)

![](https://blobs.farfield.systems/blobs/bafkreib7exxvxjrmlyj2eygscv6vbbhb6kj4nzeo5k62sdutymsmkcfagm)

![](https://blobs.farfield.systems/blobs/bafkreiev4aafnj6qghl2h6plbalrliiudtyafayr6r6me4filuokaelcpm)

![](https://blobs.farfield.systems/blobs/bafkreifknoi27gobvmalvcmhqstnxutadzk7hov6zwsjnk54phpbuqeewe)

![](https://blobs.farfield.systems/blobs/bafkreid7xa633wzvaoeotgmhet24cx6lchdqkoxaf3xrbeao6e7guekzwi)

![](https://blobs.farfield.systems/blobs/bafkreifuejdbdibob6kal2pzwyoteogqjmhehmifgacaxplbvqs52kz6mq)

![](https://blobs.farfield.systems/blobs/bafkreihq3agh2a2eqvvhjmvyo57ni7xxbe5aozoc75bmp4wr2v3m2thy7a)

![](https://blobs.farfield.systems/blobs/bafkreifqnyrcoggo2nn6a2rznpn45opdbw5frfpmr45k6y7w7i64breukm)

![](https://blobs.farfield.systems/blobs/bafkreihduesyvcdfktkm2vqlnwqq52l3qipqocexabcwv3iuu5qlyc35ka)

![](https://blobs.farfield.systems/blobs/bafkreifbabiyhd4p2onuxwlyblfjryzjfkrhff37k7pc5gffnkcqg5wpoi)

![](https://blobs.farfield.systems/blobs/bafkreiegis6ntnlq5vr42deuxghlt6cbwvyrs64gbfleeyuvjwl7q6e2yy)

![](https://blobs.farfield.systems/blobs/bafkreif4lyhmseqotlkzmv2tdstisuphk25moba7sf5jjdczleefy5f77e)

![](https://blobs.farfield.systems/blobs/bafkreihbt2nwq26xmiukapwgupl4fnhnlzqtvyii2a5fxbthk3n453lluy)

![](https://blobs.farfield.systems/blobs/bafkreiavmsqro22nuft25nmevldf45pgiw3m7rfisff7ap6eq3hkizzyzq)

![](https://blobs.farfield.systems/blobs/bafkreifrl56hnm7mrs4jmevlaodjp77qx4fiqws6s2cb6c3dkhehsshsgm)

![](https://blobs.farfield.systems/blobs/bafkreiemtfd2ttokvprjawusx3jtxfstyjjqo7g7bg2nbb3obuzxrpcu5y)

![](https://blobs.farfield.systems/blobs/bafkreigipp4ezdzik6prsaezz4llwdi6ga4igr3fnr53elaxio2ddhqsly)

![](https://blobs.farfield.systems/blobs/bafkreic33psjpbuaacpuwopg32dlrizbu464tkjda46h2cg6fhfdlsmcg4)

![](https://blobs.farfield.systems/blobs/bafkreihl22lhxrwpkodxacxsbsxv4jcullekiww2j4y34itqkafhlh4qqe)

![](https://blobs.farfield.systems/blobs/bafkreid7bfc6bu6vfg2c6ffdemxlb5ggcjp3mbleybmkqa2gb4bpuhlfc4)

![](https://blobs.farfield.systems/blobs/bafkreic2h7acxtwynpjovh7p5p6gahbsziujamz2zf5eur3fk62nmltfdm)

![](https://blobs.farfield.systems/blobs/bafkreigbno7zxbwynfgfvxb2hvtgluzw3g4eer2lv7u4uota5uzf6c4o6u)

![](https://blobs.farfield.systems/blobs/bafkreihokqhfbfgl4oer7del5qwzsv4mqvkmj3o6ukmoaqezvbfufwyyku)

![](https://blobs.farfield.systems/blobs/bafkreicdobbyjcxc2utqpp4lxv65e7m4thldnpnzrxul555zt46c5di3li)

![](https://blobs.farfield.systems/blobs/bafkreidwzjpbtr3iwdcba6omkxj7fz4og2gnrkp34oxpkhtnnozknyrk2a)

![](https://blobs.farfield.systems/blobs/bafkreihku7v3r7ebebpxeezq25ztfsya3rkbhmhu7l3x4szsjnffirtr34)

![](https://blobs.farfield.systems/blobs/bafkreietbxjbf3ht3wjeljqbdkd5stormupax5xaha4a2wknjadfpj3qsq)

![](https://blobs.farfield.systems/blobs/bafkreidvkhe2hltcdh2zu3eudgau2fnjzlqajldu655tmowz53bvlbdaxm)

![](https://blobs.farfield.systems/blobs/bafkreieeunainvlwvwbh7fuip4rjf4t34dw2fmz3jw7jufzebkiejsqfaq)

![](https://blobs.farfield.systems/blobs/bafkreig6etlus4k2ijxuqyz47ne2zaoa2kh2zwjkydtzmbkcfvmgvwfdua)

![](https://blobs.farfield.systems/blobs/bafkreicfuohgslzjioslktldyv6c4cz3tnkc3hesljodz7h4a4owajz3vi)

![](https://blobs.farfield.systems/blobs/bafkreicfccot5josnnhmrdxvjkbsg7cxlm3wcaiv3l2neq65yt2i7nsdym)

![](https://blobs.farfield.systems/blobs/bafkreibaziycih6itmsw3c5wpg5p5hhm4sdxahw3w5p4cqdmzsenbkhanq)

![](https://blobs.farfield.systems/blobs/bafkreiggl5cbbphj6baz6bjsgebnqvs2s4hhad22eu5fzwdloo4teowfxi)

![](https://blobs.farfield.systems/blobs/bafkreicc2mq7kfnmunmt6vvvdpiih2w5fwrg6fs4jm4cc724kp6cjcerri)

![](https://blobs.farfield.systems/blobs/bafkreifh7oadresoglmkuglup4tqbgdmyvh7j37br5r3f5wes6dk5a6mqu)

![](https://blobs.farfield.systems/blobs/bafkreih4opmajncj2mtapmrscefjrls7n2qleeo5wzff4pgruvnrvf2m6q)

![](https://blobs.farfield.systems/blobs/bafkreidzwm3ejfgk7zvryzp24nfqbwq3lw5bjwpwq5ps3as5wga4npzime)

![](https://blobs.farfield.systems/blobs/bafkreigrplr6no7w3pkw5leybakhtbveyhq3ok3uxki6gxz2npxr2xtu3e)

![](https://blobs.farfield.systems/blobs/bafkreifuh7xkc4f2437sdtk25ukcgenqo6nglgpcgp7fziwngtlcihsqfu)

![](https://blobs.farfield.systems/blobs/bafkreidmaoemfbo2vvmiqexqygijjis4lfyks35rdc5epg3su6vkkp557i)

![](https://blobs.farfield.systems/blobs/bafkreigonfeizrnx7uiaifzbqx3ugr2lr6q4gjotnerqw7f4hd6hl7fx4u)

![](https://blobs.farfield.systems/blobs/bafkreiezaix5tw6ewfhm2ohlkbbpogtximjayyy73prqls64o5kysfb6ay)

![](https://blobs.farfield.systems/blobs/bafkreiek326i7hz3ovjzaipypacha53sjv4o5gz6w6zhdrc5zxcqgj2yim)

![](https://blobs.farfield.systems/blobs/bafkreifpvimrwasbyqk2drqd4xqt3zoxhrvlj5xwejtpepyyye2dlger2a)

![](https://blobs.farfield.systems/blobs/bafkreicrsu2n5msshplufcfm3agwwtj3bmo3t2y5ukwkshqb344h6z7b3i)

![](https://blobs.farfield.systems/blobs/bafkreibo4xm53njpy3fcrsq5sedn6agv5kozagnq37czabiu5dh6ot2lzi)

![](https://blobs.farfield.systems/blobs/bafkreig2ani56yjt3vl7apcojpbem4k6wxy6roppl3upgqps3wedgle3aq)

![](https://blobs.farfield.systems/blobs/bafkreiegaqc7w6ez2qttrld6y44ybrnd5o7ckgsftoeoode3vgad4krbty)

![](https://blobs.farfield.systems/blobs/bafkreihou5kb3dskx5nb43lg22du6zu2rsft7g2mpjtfezyvkw5us3p23m)

![](https://blobs.farfield.systems/blobs/bafkreienpjkr6fxmh4lrvnejogr5ekjpnk6jsatohflofduedq6q763e6y)

![](https://blobs.farfield.systems/blobs/bafkreihitxfe6cqnw7ezzpo3quhoz3uvnu2p5blpfat4wpfoiijn5mrmsu)

![](https://blobs.farfield.systems/blobs/bafkreihhizfkwfvkncdepnjpjcp6pwrrcrqpivc7qb46qxqdkxitkz463i)

![](https://blobs.farfield.systems/blobs/bafkreibtkk2odyf4k6wlwphsy3bubw6pqpgtq4dqxkykiojqi6zhuztj4m)

![](https://blobs.farfield.systems/blobs/bafkreicrptzxyegqjjh3p2vwvngogauhfgvcvdeegu37fml7p2ko7jby5i)

![](https://blobs.farfield.systems/blobs/bafkreif6ao5flaeno3kvi7o5ygted5ay3snt4xu5bl5a4ba564p7xi5brq)

![](https://blobs.farfield.systems/blobs/bafkreicvlmnfn5engzd5qm34jdzhi3d4p33qpdi76cja3qrafnfvuujpce)

![](https://blobs.farfield.systems/blobs/bafkreign6eq2eihgv7vbnqdb4kpdoa6wnhtqscqrwrcwjdkxb5rgt2h35m)

![](https://blobs.farfield.systems/blobs/bafkreidqrzv2k4o57uchzpp3t5pcgkccp7wcvpghs55vm4plirbmwqlj2e)

![](https://blobs.farfield.systems/blobs/bafkreigyfnfzvsqgxumcc2nw54k3fttnvnr3k73opykyqc6jmgk4rcuc5y)

![](https://blobs.farfield.systems/blobs/bafkreie5j22epa2n55osdhyggwvgbpajbi55rpzl7ofhrfbxhtrdlwgwmu)

![](https://blobs.farfield.systems/blobs/bafkreicw2io5ikcnnrcen75ui2viitytb5c33n6lgd733w2to6uhxkzmda)

![](https://blobs.farfield.systems/blobs/bafkreibs6tvffyq3ks4zetqmigbydmnid2t5yaolh2v4fbm4mptfmg2epu)

![](https://blobs.farfield.systems/blobs/bafkreidiwuuvmluebddlkb6uvkppwmm7uknlislunajofvtw6hr4f75jv4)

![](https://blobs.farfield.systems/blobs/bafkreidmpyozhkuq2w6hpsjn3fzdi4aaoou26z3jitm6aqgkulcp7pkfei)

![](https://blobs.farfield.systems/blobs/bafkreibtt3ydnzn54b5ianmj655n6lnk7bwheaxmeewja5aoyhgoxumdli)

## Generation 2

![](https://blobs.farfield.systems/blobs/bafkreig7lnhxo66d5pnfxhejnmhonjk6bjcouzzp7arv477xntwgt3tnoy)

![](https://blobs.farfield.systems/blobs/bafkreifnh2gkxwyzni7aganydmlwq3xmjflf3yy3kyfemuqvjc5ahb4rt4)

![](https://blobs.farfield.systems/blobs/bafkreifd65tjrcmmqsdvllbjek5tg5cijh5b56s25mrhahb6zwxgxmv3oi)

![](https://blobs.farfield.systems/blobs/bafkreihu4q7egxtiaeodrzwck7ars3kliuhmj7iioccyeesxxtldtntkqe)

![](https://blobs.farfield.systems/blobs/bafkreifmxsxdzofxwbj4p6am4bwpznqgtr2samnscwtuu2hfbfs3fluif4)

![](https://blobs.farfield.systems/blobs/bafkreiglalkmihzgibmsijzhqzlrn3pdtkfhitwfyfjst5kgvb62bmgziu)

![](https://blobs.farfield.systems/blobs/bafkreiem2c53ylk2mzdjzwkqjrl5ma7hhvd7ds5zuvv3vlmpa57gler3l4)

![](https://blobs.farfield.systems/blobs/bafkreibkrqwhpf2y5driurugvtns7c3bp35zzz7ckaixsvpqdfjtnkg2nu)

![](https://blobs.farfield.systems/blobs/bafkreidgxnqujcfvkzw2pu5mwzvmn6krwspgk3oo6ukpmzkohgbuarkuaa)

![](https://blobs.farfield.systems/blobs/bafkreiekbirlftn3ulzby5inc73af7v4bclag2zgqlehtf54sx2d5gjcnm)

![](https://blobs.farfield.systems/blobs/bafkreibg44xuksztog5suyo3l6a4laboeeubkfcjd3s3zvr3bh7pmoz2jy)

![](https://blobs.farfield.systems/blobs/bafkreidkl2ashpq6kjghgnnstn4pegpl2lfxyvkfc47zbtzhzr5e7yimny)

![](https://blobs.farfield.systems/blobs/bafkreifrughkpuoc3togdwarqsaep3xfr7rjb7j2qolo3wozipsy7ofvjq)

![](https://blobs.farfield.systems/blobs/bafkreibp42j6u65wgujpmkzmg5igazop6byu2y47777c3dxeugzooijpoa)

![](https://blobs.farfield.systems/blobs/bafkreibkuqpbjtl57ijn4g33snmx2l2hkrgbbx72nry4z6l7r6fll7j3xq)

![](https://blobs.farfield.systems/blobs/bafkreic5wuman3fyrwaeyk7gzb2jcpk7wo6lj5upwsjueuex2j7axqdraa)

![](https://blobs.farfield.systems/blobs/bafkreicsujhyfhln2gfajwzurmtjqmeyu3itueoou3dsvcdtk7ahziv5p4)

![](https://blobs.farfield.systems/blobs/bafkreiech2hvpqjxfjuxep5m25exz4nx2mjgjuqitlmmcn2iorejtyf2mu)

![](https://blobs.farfield.systems/blobs/bafkreibz6a2p3nark3ikci2sw3b64bpittydcpeklwzzxgbfa57nsy6bga)

![](https://blobs.farfield.systems/blobs/bafkreiadudvgfw2lmk3ap7qhux2nvqsf5lnzpkjpfhiptdwiirryn2eoti)

![](https://blobs.farfield.systems/blobs/bafkreicjry5hgdovte64fhpkwvrmnekb3775nrklwnvj7onvzglrv22dre)

![](https://blobs.farfield.systems/blobs/bafkreicf6mssdjn2lymivfvroryisamvgsfp2ez3hps2ulydn4nq3uzqci)

![](https://blobs.farfield.systems/blobs/bafkreichtsorxakrosozgfv7tokmhsjleqxuwn4vxilntymftw2if7ydzq)

![](https://blobs.farfield.systems/blobs/bafkreihmaa4zoeqscgeyfuhyfvlcmaprtxeupf6qxvkcoq3cgw3bnpeg4m)

![](https://blobs.farfield.systems/blobs/bafkreibjn7nb6j7rdz47juhod6pblgjnpo2whayyixrrkopzvyq3a2ry5i)

![](https://blobs.farfield.systems/blobs/bafkreigbxrl2stnyj2lq6uttixrdcnnajyitwxxvhbccpcrkirqglwndkq)

![](https://blobs.farfield.systems/blobs/bafkreifotkcq2mb3jov5srx7vfqjvyndc56z67k3ohahfcrem4ll4pxyge)

![](https://blobs.farfield.systems/blobs/bafkreiftkspu377mlqbapglbgogkz6si5dn3msq5wltppjeg34atrxehoa)

![](https://blobs.farfield.systems/blobs/bafkreianhkfh6enp5gelcu6snyyvzelubemn6kwero7ul3wlmdquyssw2u)

![](https://blobs.farfield.systems/blobs/bafkreibyck4dpli4wrim2vdhn3t6k6ek534zmqxkyjfxisqfq7obfrrz6q)

![](https://blobs.farfield.systems/blobs/bafkreihcnursrfdhetlfqo5n7v35w2bvdooyhrmgxlbvo3odg5pq6umlne)

![](https://blobs.farfield.systems/blobs/bafkreiey337pxpc3ucdlo5qy2q5p65xzdsq4i4wwsoa6g457kxkgcpgima)

![](https://blobs.farfield.systems/blobs/bafkreicawggd57odgi2z5s6mlolmeb4vwxm7xbktdjq67nsnpefes7u6km)

![](https://blobs.farfield.systems/blobs/bafkreievc7xa2xmn4xm5e4cxj73e7h3tgpfwnju4pkxlrta3t5fuwwpmqu)

![](https://blobs.farfield.systems/blobs/bafkreihpiazihnhf4vxav2imak77qmp4ah6v7ungfmeuxuhdmzzalxqmym)

![](https://blobs.farfield.systems/blobs/bafkreignjo7ybfakfpgjie6w3ja7h3xlumavzlymdp44j6k72fva6vn7le)

![](https://blobs.farfield.systems/blobs/bafkreicktk72gfnx5vtwbxr6hbaprxsbtes6qwovjz6svy3yjpe6vp4ygm)

![](https://blobs.farfield.systems/blobs/bafkreiabxdptx2w7mjwtiaepso44kmqdddhjka5erv5odh5fwvf6onex6a)

![](https://blobs.farfield.systems/blobs/bafkreiaxsvwjlxyivxjfmvwwmmp66hbprvhjs7hrvdfkr7y3qt4cw2z7ye)

![](https://blobs.farfield.systems/blobs/bafkreicxkdoa5pph6fvxsrpzeqaft3vz7sokdpzaagyudivxuicdze4htq)

![](https://blobs.farfield.systems/blobs/bafkreigten5dyzdqrdpkpalolp3t3a4nbko77ljcejfdkkhndwe54teley)

![](https://blobs.farfield.systems/blobs/bafkreiffkr2xwzcgpqsy4d54nbps67vjmwfg5tbqhaxjyatfbycjplrwgu)

![](https://blobs.farfield.systems/blobs/bafkreigyqwgimkf5vj5fsnq2wcqi6ml5xglhey63gxsg3twoefx3575jsu)

![](https://blobs.farfield.systems/blobs/bafkreifu2uj3d5k37pnf65m3chpdvylquwk7o5udp6yfheccxux5mulrwu)

![](https://blobs.farfield.systems/blobs/bafkreiaeqgep6tepw4ryvt2zd5p7yfdtuhpwplemwffwbll6w5ulbv7sdy)

![](https://blobs.farfield.systems/blobs/bafkreicv4pra3tfrs4ctvnspopy2izzb4tvta6vfnkyp6e6x4xuqvgd6ri)

![](https://blobs.farfield.systems/blobs/bafkreibq2246ltz5hkebi6xrjssf2rvoxyxfjjpuq7lixc6pgyiigzn6jm)

![](https://blobs.farfield.systems/blobs/bafkreidac6aqiewhlbygaoedm7wnc2dyowvjuuvwat2vvgs74oxsza5qpu)

![](https://blobs.farfield.systems/blobs/bafkreihiijagwsm5suusyg5nmojug6ds46qvetgwc7ay36w2wacmhleudy)

![](https://blobs.farfield.systems/blobs/bafkreickazuyvlaynl73mhtxm6s6mgxaafbsqsrhq42cinsqkoz5xrh554)

![](https://blobs.farfield.systems/blobs/bafkreiee4qmennurq2ldu6iktkrz3xxstk6h2o76bly7kxg7c27bfec34u)

![](https://blobs.farfield.systems/blobs/bafkreiflj7uzudarbdogccicvvyvcp75lguo6sqngigvbouyoor7nfikfq)

![](https://blobs.farfield.systems/blobs/bafkreify5hwnf6l655hns577htdhhdtnjmjmjozohjlrtmpntcj4zx7b7y)

![](https://blobs.farfield.systems/blobs/bafkreicrobbc7dbow4wzkb2474yurfe54qgz6e4dgyzbwfruxrdl35tz7y)

![](https://blobs.farfield.systems/blobs/bafkreie3nw623e252736nfc52kln5c266ztnwu4lux3gjfcfuz5tim4dym)

![](https://blobs.farfield.systems/blobs/bafkreigkzutc6immjadrzn3pp5dda63tududuwfl352iqnt5z7xkftxkce)

![](https://blobs.farfield.systems/blobs/bafkreic2w5oh6jnok5odeennl25pbfgt7qsjckqja2hb2eh3bk3nukamau)

![](https://blobs.farfield.systems/blobs/bafkreidcxuyypwdhidi275lefmosp5qgnrfoiabogil67hhfe6krko3zge)

![](https://blobs.farfield.systems/blobs/bafkreibnmgtdfuohq6iz7s27ae35haxas2j7wo2gq55sxx7b53ypub7j2q)

![](https://blobs.farfield.systems/blobs/bafkreie6q7znyg6xzjbaso63vjgt366ptxtb7ku6q7vzhtvpsb3esuobl4)

![](https://blobs.farfield.systems/blobs/bafkreigr3ytzw2uhnogfq63vmixpfydjc2e5wjuazk47mzilqfbaqxtgey)

![](https://blobs.farfield.systems/blobs/bafkreieh4rnppvk6mz3extfxz2x2zotkx3l3i2v4r6dfukau5ridgebyd4)

![](https://blobs.farfield.systems/blobs/bafkreifhe5hj3s2ddt5xmfvwccycsxk5oujugi5ewjws6qw3bxvqerqj2i)

![](https://blobs.farfield.systems/blobs/bafkreicnmmha4y5y2jsc2masrwbehgglbduiulioht3qkrwzgopbebda6a)

![](https://blobs.farfield.systems/blobs/bafkreihhiyqvbdudoophc6kybehnr7x6x7ifpiv72fpznrp43lviqf7jta)

![](https://blobs.farfield.systems/blobs/bafkreicwj35rmjlp3hl22n2xjrtmwahsvcvkpyl2siobyllwbrivu6tuoy)

![](https://blobs.farfield.systems/blobs/bafkreib3qarh6tolovxjqgwlk4o5z37oibpnoytq2vccunc4sthycs5hp4)

![](https://blobs.farfield.systems/blobs/bafkreidguic3byd4t7s35riwgfdlwisklmywx5kpr3oqkuby6tedlpm2yq)

![](https://blobs.farfield.systems/blobs/bafkreihnlgx7xskhow4htcf6map2tk7ndjgypihvwqhyghc245ecxh75iy)

![](https://blobs.farfield.systems/blobs/bafkreifyrqvhcqw2ffbi3otyitjw37lfccejihlb3emyetq2n3x7dbajcy)

![](https://blobs.farfield.systems/blobs/bafkreiakrdfp735umiofw6t3wgwfxnev7qynldhb5qy35zfzydwqsemzee)

![](https://blobs.farfield.systems/blobs/bafkreiaya5djnotgggkgfo64qxhvfyur7mtzvmv2vbnjwkvcxbmn4qntfa)

![](https://blobs.farfield.systems/blobs/bafkreidalgflq7ayz3kf2e4mhcjz3p2dvof7osueaq2cptlrm32mloxylm)

![](https://blobs.farfield.systems/blobs/bafkreiar2bv5pr2fuk6chmc27ztzue42tmznrjh3d7ymn7cfslyuthdaga)

![](https://blobs.farfield.systems/blobs/bafkreierhh2eobqq66cg5yudzl5pfp5czuhgmp66xfbo4dxbo2hjp6ad54)

![](https://blobs.farfield.systems/blobs/bafkreiahfup3lzdnjuxytgbaannyl7jj2geezmraiyi73lz6jap2ltviju)

![](https://blobs.farfield.systems/blobs/bafkreihpzhzxolbqegwukxepm4q3bqkxbgiazuuefddxxpmkbmlie5ucyq)

![](https://blobs.farfield.systems/blobs/bafkreiefeoivyzonkmc3pkfzbps4tg4e3agt4tdhgk36cpesz5b3i732ty)

![](https://blobs.farfield.systems/blobs/bafkreidisflycprqvolb5madvgeibdalhi3qiexxrqxidzcwuwlrk63tdu)

![](https://blobs.farfield.systems/blobs/bafkreihqxpdzuobkdbhp6szaws62cwetiqaokauj22weuezwxd7hodmpmm)

![](https://blobs.farfield.systems/blobs/bafkreidkefw6mhpimez4t5rizzj4a7bmmnqqdkdxmnzulcpvqhoofvq4aq)

![](https://blobs.farfield.systems/blobs/bafkreif7u7iz5tj2jcikhov5udb5aullvugn3i2kz2rq3cllvsunq2giry)

![](https://blobs.farfield.systems/blobs/bafkreifocixv64fjoo4etuetwszuftpwk6eg46frmqh2aq3jykqxykb2gi)

![](https://blobs.farfield.systems/blobs/bafkreibmwgkkn76f5wf4j6uwvmdqwhu4gjtw2yei367tee66vpxhkeproq)

![](https://blobs.farfield.systems/blobs/bafkreifwj7bt2q3xx6y5b3khii7iunyf657eqgetyuunmqakhli65se2ue)

![](https://blobs.farfield.systems/blobs/bafkreiec2oajmroabahrpwkmjc2uo5k4xp7arhewd47sbocvyt32vdxpia)

![](https://blobs.farfield.systems/blobs/bafkreiabzdla5oarufligsgkoqc4q4w53cikpt7d5j72li72ggvnf3gak4)

![](https://blobs.farfield.systems/blobs/bafkreiahs2o26f2lelpmlv74uy6f5ymutgtc7evcmp5e4ozbx2x3neb2am)

![](https://blobs.farfield.systems/blobs/bafkreicfbcxqtvilf4a2zosmov3swpyqefwyyyzfdd2johhrwze4iyfyva)

![](https://blobs.farfield.systems/blobs/bafkreibb6twl7cyucnnj6ofam5veqlb3iojnr673rhq7zffdrvo76ekyey)

![](https://blobs.farfield.systems/blobs/bafkreidjd6h2gabys5pubjhibarkqparyl36u43mnzedobjv5hpev43kfy)

![](https://blobs.farfield.systems/blobs/bafkreidtscu5pgcwh72d5xisuhlcjb5ukqwwvvckj4bgq54e6qwedrkq7m)

![](https://blobs.farfield.systems/blobs/bafkreifiwu6v3wwku2c72xzjszwpdz6s3uoaqtxdvyh3lkj6egsfzxxzkq)

![](https://blobs.farfield.systems/blobs/bafkreieevcj6cm6bcduwuj2qz3f6udj7j2zhwwtxvefx77d6z5nphcgkrm)

![](https://blobs.farfield.systems/blobs/bafkreiaajjsfgkktgftut6bjhvpxlnvhoutbdnsuunahly6voyphciqncu)

![](https://blobs.farfield.systems/blobs/bafkreigb6u5conm4gr3v72b5ht2224tkijsmomprljbv6r3ocnsq75v6sa)

![](https://blobs.farfield.systems/blobs/bafkreia4btsy5ihgheoirh2pgx25k2uf6o5pyokwgsmawlmjdnorgevjwe)

![](https://blobs.farfield.systems/blobs/bafkreiair2jknyp3jmbeaou644rso3dzxuhleg646ku6i6rgzixficglmu)

![](https://blobs.farfield.systems/blobs/bafkreifng2tcey6adelav3hk7ub7rt4nkk5ij33pgsuasujd7r23zvdxyu)

![](https://blobs.farfield.systems/blobs/bafkreigrjdqv2hnwd4rmhcyfbcr3uuttkzlamb7jz665nks6uz5vqmvkb4)

## Generation 3

![](https://blobs.farfield.systems/blobs/bafkreiczrzbmvfgjbbd7l3qwltocg7dgipr66tpsej2lvqjjhl3bvo2kti)

![](https://blobs.farfield.systems/blobs/bafkreifgc3dx6drbmndmiis2h3soua53q6qqqrxkw5maj2u2d65vzaclfu)

![](https://blobs.farfield.systems/blobs/bafkreialmxbaafvvg6vb22ljfqgslyg4i2d7ocqhlpenzzjcr5usm7iuee)

![](https://blobs.farfield.systems/blobs/bafkreicv4zwtzd5d3auiy7dzgcweeghhycke3yyz7uslvccyarj647g4xy)

![](https://blobs.farfield.systems/blobs/bafkreigocdkpgboyn2t35bqedgj3syfhswuzll4tkjjcgtrffzh5bc7mta)

![](https://blobs.farfield.systems/blobs/bafkreiakkipk5s5iwhkxd3rfwkvqpw2wvrf6fuvzbn5lataip5v3byodx4)

![](https://blobs.farfield.systems/blobs/bafkreidptyop2ffkw65sqqkhgvilxgolbpk3is4higafggw4xiawq7e7oi)

![](https://blobs.farfield.systems/blobs/bafkreidujrucy25gk5houhyjkxx6rs6cifs2lflitrko7i663oyn3ukmq4)

![](https://blobs.farfield.systems/blobs/bafkreifffacdntswgwsp6dosj3t4ejgvtrw26pavotr5xhethovx4drfsa)

![](https://blobs.farfield.systems/blobs/bafkreidoqyeexvba3iyrqoudryy676criszcmvznitpd6za3oc3szfgjam)

![](https://blobs.farfield.systems/blobs/bafkreigyy5s2fftpycfddy2dvnhx2dy76fvvu6qs32dfdnl6wi6x5bhspu)

![](https://blobs.farfield.systems/blobs/bafkreic7cmr2pgx5ryxlrcnwtmuohtjaw3xjd3lwq5qrtm2frufmsnlzgq)

![](https://blobs.farfield.systems/blobs/bafkreidid7zslhhkoxikwwd5u7ss6hmfuvyjovnahuqjhqchxe5vizo3te)

![](https://blobs.farfield.systems/blobs/bafkreifcog2d3hjrigpa5clgigsbtzn2zuhihreveru4vcxenxp77fhr3m)

![](https://blobs.farfield.systems/blobs/bafkreigy6wyrt3rxtx3v56a2tmp2xor7seg2gkmi46rd74bpc4qa7vsdtq)

![](https://blobs.farfield.systems/blobs/bafkreid6hrea6465jtuysxjxpq7olq2swsmag5bhvzhvshle2m2j3j2suq)

![](https://blobs.farfield.systems/blobs/bafkreif7xuprtsbprmcuqr4d2isltyykxwx7cma7wzrf2upcypi5kui5wa)

![](https://blobs.farfield.systems/blobs/bafkreicag6lgmfn6fzure77dm6ueqzej7wvfe6n6j45sbcexghggxomqdy)

![](https://blobs.farfield.systems/blobs/bafkreigfghxb4wvrfzrbvpvzv3lnykpotsx2bdj6t6sbaqk3w74wonx2xq)

![](https://blobs.farfield.systems/blobs/bafkreidjicmtzkwqjd266fgpccj26k27ejqm63tnpfmdl6pi764zotfgvi)

![](https://blobs.farfield.systems/blobs/bafkreiaekh73o4pebqbxpnbtc6bqy37yx4gl2q26fkrgfflgur7lnte2bu)

![](https://blobs.farfield.systems/blobs/bafkreieguabqtrtl5ekxdf55l3eicq5uoa5vuuld6elmzc6ky5amkngqgq)

![](https://blobs.farfield.systems/blobs/bafkreie3567uydijgvtptoxh7weznou4f7k5rrjgs2tgkw7eaan6adzeyu)

![](https://blobs.farfield.systems/blobs/bafkreigs6lbnxnrgctshvlj6a5mmznwz6okxaiacdg4jin6lfmqtxa3msm)

![](https://blobs.farfield.systems/blobs/bafkreihfqqurgstsdf4fcnljtvaiwq3dk675h5txcntbbkrtgdilxh6iiq)

![](https://blobs.farfield.systems/blobs/bafkreifiubrhss2cnzywv3imp564322lbnmvrg2uurinna2aovbejanfba)

![](https://blobs.farfield.systems/blobs/bafkreifrkgjckythcepy4n64j6aizu6lgakfo4vrk2nuljae7owy35eqo4)

![](https://blobs.farfield.systems/blobs/bafkreigod7ajgziwarul64liaae4k7sw74vdb3n2qfkxhsqm343tzf7iae)

![](https://blobs.farfield.systems/blobs/bafkreib25kwhlkwnzichlayr4b6uznrdyu4ewnpbiavhq36ayr722eu47q)

![](https://blobs.farfield.systems/blobs/bafkreihhkq4tofnzrxixxtbozy3eydbvg5wn77nglwfdyzownylc6xohty)

![](https://blobs.farfield.systems/blobs/bafkreihd3s27xdnte5gltdrzycdsi76mqywokilr5unuzndhglg3f43bwu)

![](https://blobs.farfield.systems/blobs/bafkreiczn22k7m25v4upbijgp67tc6bf4eb6f74hxanv23cgok2uzzurxu)

![](https://blobs.farfield.systems/blobs/bafkreiebdv6byvbcnncckcyaqqdavr4xvtcheeukoelrf7swc7jrelfryq)

![](https://blobs.farfield.systems/blobs/bafkreih5akf6irwqj4ngcfmm2xcvsgqi6jfheugudzbckvlxz2gqwmrkra)

![](https://blobs.farfield.systems/blobs/bafkreiduabf6c2e2vudbckdp4oerwnbftdrig63ygcv5we573lxdlv5f2a)

![](https://blobs.farfield.systems/blobs/bafkreibzdl4q5coqvt3sw3hedbzui2pfadnckko7rpo7qcotucgu7y3jh4)

![](https://blobs.farfield.systems/blobs/bafkreib474xy5x3gg2n54qdriqg7ypmn7njykgqc6xbupcyx2wbun63dym)

![](https://blobs.farfield.systems/blobs/bafkreifxspoxldu2ilznej7l7q5wcgdeykjwkbkdwv35vi6vwdtjbxraji)

![](https://blobs.farfield.systems/blobs/bafkreibldt6pfw27k2j3yj4aabewmqhukphqkzwarl2lv3k7p7tpvyh3ju)

![](https://blobs.farfield.systems/blobs/bafkreiebxop2cs7mqehyalt3eji4ip6d7oedmshwdclr4vlysm75ocamny)

![](https://blobs.farfield.systems/blobs/bafkreibl4vwlgb4cxfrxk2loj2r4eo34js6mmttpgze5unxl4eppcls7vy)

![](https://blobs.farfield.systems/blobs/bafkreiaf6v56vkdpuq2ehdg25mfiwdsgbg7jfnzmj6j3y4zxf7pvdacpea)

![](https://blobs.farfield.systems/blobs/bafkreiagymxiwn6cqq7osul2tzt7cj3psaalkampkbf4ngfrso2vrwkkye)

![](https://blobs.farfield.systems/blobs/bafkreieiszdvudtw7fgiwvsfoselbzmisumxf4zwf7ovngev3whpw3glv4)

![](https://blobs.farfield.systems/blobs/bafkreidlkn52f2zk4rypsmnrkyotcpssrw4zvdpknmb7og2w4tghdcwljq)

![](https://blobs.farfield.systems/blobs/bafkreie7raobsbpyepxp7a7deiirwbnzfrbum57m5xxc3ofq5ll3g7pm7a)

![](https://blobs.farfield.systems/blobs/bafkreibl3x25acddhmbjwmbjvcxztvsqwfqd7m75eqjpfglckfu32dacuq)

![](https://blobs.farfield.systems/blobs/bafkreic5vbq7zo2kqlwspqhha5war4dyc4oqdkemodp2xaiew4o44dyvu4)

![](https://blobs.farfield.systems/blobs/bafkreigrta7gh52xev7lgsz2ymx4jegyclpjfy3xw5hwopuxrduobjkmvy)

![](https://blobs.farfield.systems/blobs/bafkreidwhwngymkz2vecc6e44jr6sy5a33nto5lbirucjguiuk6yxjnfrq)

![](https://blobs.farfield.systems/blobs/bafkreicylnnsqjyhsc2ojjlqnxjy4yjae6kfpxiqwz6xr3aserdcyer2zy)

![](https://blobs.farfield.systems/blobs/bafkreiecu4pnrs62p776injc3rod7y6mzl2n7ifjmlfs66jr4eq6mkthne)

![](https://blobs.farfield.systems/blobs/bafkreif67auzrfwvy3jvyvvvo63noqzqy2a4rr22m3oxn5uocqvaww7gm4)

![](https://blobs.farfield.systems/blobs/bafkreibwxp3w3c5223bm3nkb4r6iqld5godbendor42hb2hnt5bqsqmaza)

![](https://blobs.farfield.systems/blobs/bafkreid43qoyp64hqcvr2grladczlwz3tekdp7xkf63dwhbj6qubylkyza)

![](https://blobs.farfield.systems/blobs/bafkreidhzbvha3ogm332wkapwn62txbij6i3qs6dxdxcds2cjxfes63wne)

![](https://blobs.farfield.systems/blobs/bafkreigjbhiu5gdqys3ecxjyl4hndnkukrqtfappgzkintq2dlf52ljioq)

![](https://blobs.farfield.systems/blobs/bafkreienjsplsapwoujfv6wqgmnqlayphnyos7furuduppahplrxry3oam)

![](https://blobs.farfield.systems/blobs/bafkreiaigqptuirc7hg6jttcs3ftnkkahai245itaacf32fqpvuiwi3mey)

![](https://blobs.farfield.systems/blobs/bafkreiedsroakc7irtx6m54upsozgq3roakvpvp7ihsgb4xcopj7jgfybe)

![](https://blobs.farfield.systems/blobs/bafkreid72g544dfe7vrikz7bsqr2jnnkdzfaka3w7jj4nef6holtukzmoq)

![](https://blobs.farfield.systems/blobs/bafkreif7zkhwzhtvsmjuwhvuwvkprhslc7t2lmitv4ksuu6gzmb6r7ld3m)

![](https://blobs.farfield.systems/blobs/bafkreidrnqi2t3mokyj33zpiloqf767gamtvu44qjjxx7epwx7hsatcybq)

![](https://blobs.farfield.systems/blobs/bafkreiabpmnmqkbaqbbjouisexwbo3vag6v52ljmebigf2iqpkmw4tddre)

![](https://blobs.farfield.systems/blobs/bafkreifnpzebxxcxu36uax4qqnkko64ul6ivikosnoxxdabltiwbfhoyre)

![](https://blobs.farfield.systems/blobs/bafkreiedf64zwtqudtos7wzbndye46lw4eah7ce5ngue5gcyskl42ajnny)

![](https://blobs.farfield.systems/blobs/bafkreifqxt7ne4wnc3ntmlmm6ky52xamqoxz32ruwyp4nbicl2xvddkbby)

![](https://blobs.farfield.systems/blobs/bafkreibmyptz6dsckhk4riphdidwukpk4wdlcn7ecu5fmuzpih5jrdsp3e)

![](https://blobs.farfield.systems/blobs/bafkreifgpojs2dvvoluwaq3qqhvorgeyacfbeytnoi3vfzgscwcrbfc5qa)

![](https://blobs.farfield.systems/blobs/bafkreiewumvyvyj6xlojnoub3pbtf4o26kqj4jiwhxw2s72kdqcdqa54qu)

![](https://blobs.farfield.systems/blobs/bafkreibuk65e7neseh3kyrj3ns2cdivbdrle2bxzx3p3wgfs6fsarfbjby)

![](https://blobs.farfield.systems/blobs/bafkreiedlp3sjwaj4dgg47jstzbkqtd7q6ldaph4b2yujq4rodlsc6dbfq)

![](https://blobs.farfield.systems/blobs/bafkreietxnch7a7qfb5fs7cwsbthbxka5ntlfqqgoaqre47xoiewl6mk5u)

![](https://blobs.farfield.systems/blobs/bafkreihzuneqhwomqcn7dii4rm73du5dpvzfnhcbbczb5dc75tqt2wn324)

![](https://blobs.farfield.systems/blobs/bafkreibvoumkffrnrg42pdur2gmstc6foqn42i3jvd5ogebssf2gmpse5a)

![](https://blobs.farfield.systems/blobs/bafkreibwlsjvnjyjcclcfibidoj6hjgma34sdi4ahkvkjcy2przimn3bc4)

![](https://blobs.farfield.systems/blobs/bafkreibtqxywz2w6yx6mkurjszsbzg43zdkm7oebwvf7vormrsdkjqasmq)

![](https://blobs.farfield.systems/blobs/bafkreie3kwgbo33ueq3qa7mfm2uucexvil77b6hbk4f557b7ubsqrouzby)

![](https://blobs.farfield.systems/blobs/bafkreidan2rs7hplff3xrqflbkdogal7nwev6mezarkznkzvts6n5ys7ja)

![](https://blobs.farfield.systems/blobs/bafkreihfl5bglft3xnied5hggiemfyecgl3qplee2cqpi6bslzi3s7ixmm)

![](https://blobs.farfield.systems/blobs/bafkreicybapbmz3ng3nlulfhlf3f6jvl4jx6mubcoopkkwx6jfa4zr2aia)

![](https://blobs.farfield.systems/blobs/bafkreia6bbznswd7bxnkbs3g7kipbbeaydxg7ttix5iuduvz54hjar2yny)

![](https://blobs.farfield.systems/blobs/bafkreiafdpb4bb7uijtyvqbbwqilu2ozccnzim2lv2hl6nsii3o2bwxcp4)

![](https://blobs.farfield.systems/blobs/bafkreia3k45fgppr4ej3thawh6hd46kfbfjrdqr3hsd4rg6qdbi4t3juyi)

![](https://blobs.farfield.systems/blobs/bafkreicpaxlk64nxnbukcfchhgqna6yw2gqd6vlpk6p6v5oala5gtyek3a)

![](https://blobs.farfield.systems/blobs/bafkreigki4gruna72343rxddmjsjimibcv2y4sfyefqkkyqiycradlg26e)

![](https://blobs.farfield.systems/blobs/bafkreifbqs2if3mg3b6d27auqamfjix6ycan7msotjxsyikepxgl425sy4)

![](https://blobs.farfield.systems/blobs/bafkreihknjsfgb7ifbgq5cnrouj7nj2ocspicrlt3ozfc3ycxqf3dxcch4)

![](https://blobs.farfield.systems/blobs/bafkreidtpzrc4nsc36foqqljnoplxcsachtckvfjfur45dcegaji2mkhom)

![](https://blobs.farfield.systems/blobs/bafkreieyrrl75viz4l3idonpsxbrg7xowdg4v4jhgzqva36nxp2garopmy)

![](https://blobs.farfield.systems/blobs/bafkreibt6rmzr4twto35aunfd7o7cknqbkd24litrg55vncdponyrlc6cy)

![](https://blobs.farfield.systems/blobs/bafkreiexuk4gvaxkx7jl7it6h4grc7grwrqmjzqb45zmpfslcelmb4geoa)

![](https://blobs.farfield.systems/blobs/bafkreiacdcixhekdun22ourn7zhumbz2tpcdo2vr4ob6yoxdbrlutvh334)

![](https://blobs.farfield.systems/blobs/bafkreifczs2iqzrnrruppck6j366kqawxze67hqn3kjtwhbpditwaf7kh4)

![](https://blobs.farfield.systems/blobs/bafkreica5gmlznfrihtb5w6oz67bx3gdwdr2mcgmfr7f42kgiomza4o2uy)

![](https://blobs.farfield.systems/blobs/bafkreiem3yhf6iufmlorye4wqx3xrdwznlectynwnknjnkpffmahc46yhi)

![](https://blobs.farfield.systems/blobs/bafkreicxuopmuqcqr5qujbbysxlbgdhgck74d66ohx7fbrscdpo5jjsdju)

![](https://blobs.farfield.systems/blobs/bafkreidgrthjaa7ui2lpw4eqakqbz3r5wwb3hyisranw37qfmmexyaozwi)

![](https://blobs.farfield.systems/blobs/bafkreihvqyu4mjzl3c2hmypfmpp2q76ttqaqa5fq6uvg6c46rywgebpode)

![](https://blobs.farfield.systems/blobs/bafkreiccix6srsbwmcjkhuaz72b3aqiurfkao4xrjf7lin3w5kdvmut6va)

---
title: "Who Really Wants Carbon Offsets?"
section: posts
description: "We expected electric-vehicle drivers to engage most with carbon-offset messaging. The strongest response came from truck and SUV drivers."
tags: ["sustainability", "carbon-footprint", "marketing", "customer-engagement", "data-analytics", "consumer-behavior", "fintech"]
created: 2025-02-05T09:58:00Z
updated: 2026-07-16T14:56:30Z
cid: bafkreiggumhdxexmclvz36p4xketmhqvadbx6hytwa7wsimqkd6bhrhzvu
html: https://iammatthias.com/posts/1738778326344-who-really-wants-carbon-offsets
---

# Who Really Wants Carbon Offsets?

We assumed the customers most interested in carbon offsets would drive Teslas and Priuses. The data pointed at trucks and SUVs.

[Aspiration](https://www.aspiration.com) had launched [Plant Your Change](https://www.aspiration.com/plantyourchange), a debit-card round-up program that planted a tree on every swipe. Gas purchases received double cash back and additional trees intended to offset the carbon impact.

## Reading the early numbers

Leadership assumed our core audience would be Tesla drivers, Prius drivers, the environmentally conscious crowd. The logic: people who already care will care more.

I suspected the opposite. Our most engaged users would be the ones with the biggest carbon footprints, looking for a way to compensate, since they chose to open a mission-based CMA account in the first place. We were working with [Faraday](https://faraday.ai), a B2B consumer prediction platform combining our first-party data with theirs.

In a discovery session I learned they could match against DMV records. Vehicle type became the segment cut.

## What the data said

Customers driving electric vehicles weren't the ones engaging with offset messaging. The most engaged customers drove trucks and SUVs.

This wasn't a quirk. These customers knew their footprint. They were constrained by family size, work, geography. Vehicle choice wasn't their lever. Round-up offsets in everyday purchases were.

## What we changed

We stopped targeting on perceived environmental consciousness and started targeting on impact opportunity. The constraints were real, and so were the people inside them.

Engagement went up.

We had confused existing environmental behavior with a desire to reduce impact. The truck and SUV drivers made the distinction visible: their constraints limited one choice, but did not erase the intent behind another.

---
title: "AndreiBot-3000"
section: posts
description: "A GPT-2 system trained on approved Aspiration emails generated additional variants for the existing A/B testing program in 2020."
tags: ["ai", "content-testing", "marketing", "email-marketing", "ai-workflows", "customer-engagement", "growth-engineering"]
created: 2025-02-04T10:18:00Z
updated: 2026-07-16T14:59:43Z
cid: bafkreihhlnncn5us2pkatz2e2ex3qgiam2hlireiono6r44fwd6gwvxtta
html: https://iammatthias.com/posts/1738693093717-andreibot-3000
---

# AndreiBot-3000

![](https://blobs.farfield.systems/blobs/bafkreih44v7gqt6f74boevpjdennt4mnyp26ojdi3xw3zimjxl7ntuhwre)

[Aspiration](https://aspiration.com/) ran an AI-assisted content testing system in 2020. Most of the company had no idea.

## Context

I started building a custom content testing system at Aspiration in early 2020, right after OpenAI released GPT-2. AI was a novelty then, treated more as a boondoggle than a threat. A few trusted leaders found the idea interesting but couldn't picture a use case. The CEO was a wildcard. ChatGPT was still two years away.

No green light, no firm no. I built it in stealth. AndreiBot started as a skunkworks project on public GPT-2 models, moved from Hugging Face notebooks to Google Colab for tighter control over training data. Each generation needed source material chosen on purpose, with themes, keywords, and structure pulled out as the outline for new variants.

## How it worked

The training set contained more than 1,000 high-performing, compliance-approved marketing emails. That gave the output a reasonable chance of landing inside the voice and compliance boundaries, although the larger test volume still created more review work. The material ranged from climate impact messaging to financial product launches.

AndreiBot was strictly an accelerator. Every AI-generated variant became another option in an A/B test, sitting next to human-written copy. Early variants got rejected often; the success rate climbed as I tuned prompts and training data. Every piece went through the standard review pipeline, bundled into Google Docs as just another test variant.

The workflow: analyze successful emails, break them into components, then play with different emphasis patterns. The value was variant volume on top of proven content, not replacement. Eventually, AI-generated variants performed comparably to human-written copy.

The performance data fed back into the system. Segment-level message analysis sharpened targeting across every email flow: lifecycle sequences, winback, promotional. Every touchpoint became a testing opportunity.

## Results

Late 2020 to June 2021, AndreiBot quietly upgraded the testing program. Internal skepticism around AI made it hard to talk about openly. The project shut down when I left, and testing went back to manual. A few months after that, the broader market caught on to AI-assisted content testing.

The tools are easier to prompt now. GPT-2 wanted explicit structure, parameters, and examples, while modern LLMs can work from much more natural instructions. Clear context, specific requirements, and defined constraints still decide whether the output is useful. AndreiBot treated generation as another source of test variants, which remains the part of the experiment I trust.

---
title: "orbz dot com"
section: posts
description: "orbz.fun reads Orbiter's onchain site registry and turns it into a StumbleUpon-style discovery tool."
tags: ["web-development", "decentralization", "blockchain", "ipfs", "web3", "pure-internet", "tech-history"]
created: 2025-01-10T11:33:00Z
updated: 2026-07-16T14:56:30Z
cid: bafkreiho7443cu7e7uycpj3ojlykl3uumrim25pxkphimxabau2mdqtghq
html: https://iammatthias.com/posts/1736537615834-orbz-dot-com
---

# orbz dot com

I used to browse StumbleUpon for hours. There was always something new, one click away, and no feed trying to learn what would keep me there.

That version of the web still exists, but finding it now means pushing past social feeds and knowing where to look.

When [orbiter.host](https://orbiter.host/) launched a little over a week ago, one thing jumped out at me off the page:

> Launching a website shouldn't be hard

That line is close to the idea behind my [Pure Internet](https://iammatthias.com/posts/1731955749292-pure-internet) experiments. Launching a website should be easy enough that the strange little idea survives the setup. Straight HTML and CSS are often plenty.

[Orbiter](https://orbiter.host/) hosts static sites on IPFS and uses Base, an Ethereum L2, to track which version is current.

An uploaded site receives a Content Identifier (CID), a cryptographic hash of its content. The CID is immutable, which makes it useful for delivery and caching but awkward for a website that changes.

Orbiter handles that through an IPCM (InterPlanetary CID Mapping) contract for each site. The contract points at the current CID while its event history retains the earlier ones. The site owner controls updates, and the version trail stays public.

That history gives every site a small Wayback Machine, and the factory contract provides something else I wanted: a public list of every site deployed through Orbiter.

[orbz.fun](https://orbz.fun) turns that list into a StumbleUpon-style browser for Orbiter sites.

The OrbiterFactory contract at `0x486Edc2E94926fd50398e2129089648b8B9E3D3A` emits an event whenever someone deploys a site. That event contains the address of the new OrbiterSite contract.

The browser reads these events to build a complete list of every site deployed on the platform, storing it in IndexedDB along with each site's CID mapping. When you hit "Start Exploring", it picks a random mapping, and loads the content in an iframe.

![](https://blobs.farfield.systems/blobs/bafkreictk52ade5fghsul5cucke3qwkswbho7pt5qnam2xxblzd5sfbofe)


The client is React, Vite, TypeScript, and CSS Modules. IndexedDB keeps it from replaying the entire contract history on every visit. Site owners can opt out by sending me their contract address on Farcaster.

The project is small. The useful part is that Orbiter's permissionless deployment history doubles as a public directory, so nobody had to build or maintain a separate catalog before the wandering could start.

---
title: "Pure Internet: office---space"
section: posts
description: "A Pi Zero 2 W and Waveshare Environment HAT record my office conditions every 60 seconds."
tags: ["raspberry-pi", "sensors", "open-source", "monitoring", "hardware", "mint", "data-collection"]
created: 2025-01-10T11:22:00Z
updated: 2026-07-16T14:56:30Z
cid: bafkreia4wcdlpcgvsrnu7pnines3h4r2qmwhjdslz7xjvzlbeaebbcgzti
html: https://iammatthias.com/posts/1736536967495-pure-internet-office-space
---

# Pure Internet: office---space

![](https://blobs.farfield.systems/blobs/bafkreihc33qgv2u5thb3skmoutcdmtaoxr5mptiu3tg5f5awmlhexsfpai)

I had a spare Raspberry Pi Zero 2 W and a Waveshare Environment HAT sitting in a drawer. The Zero 2 W is overkill for reading sensors and writing to a database, but the 64-bit processor makes library support easier.

The Environment HAT carries the sensors:

- TSL25911 Digital Ambient Light Sensor

  - I2C address: 0x29
  - Effective range: 0~88000 Lux

- BME280 Temperature, Humidity, and Air Pressure Sensor

  - I2C address: 0x76
  - Temperature: -40~85°C (0.01°C resolution, ±1°C accuracy)
  - Humidity: 0~100%RH (0.008%RH resolution, ±3%RH accuracy, 1s response time, ≤2%RH delay)
  - Air Pressure: 300~1100hPa (0.18Pa resolution, ±1hPa accuracy)

- ICM20948 9-DOF Motion Sensor

  - I2C address: 0x68
  - Accelerometer:
    - Resolution: 16-bit
    - Range: ±2, ±4, ±8, ±16g (configurable)
  - Gyroscope:
    - Resolution: 16-bit
    - Range: ±250, ±500, ±1000, ±2000°/sec (configurable)
  - Magnetometer:
    - Resolution: 16-bit
    - Range: ±4900µT

- LTR390-UV-1 UV Sensor

  - I2C address: 0x53
  - Response wavelength: 280nm - 430nm

- SGP40 VOC Sensor
  - I2C address: 0x60
  - Measuring range: 0 ~ 1,000 ppm ethanol equivalent
  - Limit condition: <0.05 ppm ethanol equivalent OR < 10% preset concentration point
  - Response time: <10s (tau 63%)
  - Start time: < 60s

A new entry every 60 seconds, give or take. The sensors occasionally stall, but the database aggregation absorbs most of it, and some error handling tries to reinitialize before rebooting.

The code is open source, split across [office-space](https://github.com/iammatthias/office-space) (a Vite + React frontend reading from Supabase) and [office-space-db](https://github.com/iammatthias/office-space-db) (the Python and systemd service running on the Pi).

![](https://blobs.farfield.systems/blobs/bafkreicr46gd3qto5xrdhwsmbchtb5efzjkwvcl3z3d2txy64xo3ecbxxa)

---
title: "Test"
section: posts
description: "Friday, December 12th. Courtesy of the DoD."
tags: ["test", "dod"]
created: 2024-12-12T10:15:00Z
updated: 2025-10-04T13:23:00Z
cid: bafkreifsgnrcwa2htg4q5rnvpk6uukmcc6zllrnbufcb7oayrwe2mpzmhi
html: https://iammatthias.com/posts/1734113731423-test
---

# Test

asfasfasdfasfdasdfasfasfasfsadffffffffffffffffffffasdfsfsafsfdwfgasdgbdfbgdsgasdgdsbdb

[source](https://www.defense.gov/News/News-Stories/Article/Article/4000004/test/)

---
title: "Pure Internet: Feral"
section: posts
description: "A solar-powered Raspberry Pi runs a small Hono app through a Cloudflare Tunnel and disappears when the battery drains."
tags: ["raspberry-pi", "solar-powered", "cloudflare-tunnel", "honey", "feral-server", "pure-internet", "decentralized-web"]
created: 2024-12-04T16:10:00Z
updated: 2026-07-16T14:56:30Z
cid: bafkreia5amrdy22lvpajhhdacrsb7phhkuj66yxsllywei5yyu4m6yztse
html: https://iammatthias.com/posts/1733357455673-pure-internet-feral
---

# Pure Internet: Feral

Next thread in pure internet: feral servers. Non-standard environments shaped by the place they sit in.

The board is a Raspberry Pi Zero W I had lying around after upgrading another project to a Zero 2 WH. The SOC is too thin for Nginx or Apache. I considered Caddy. Then I realized I didn't need any of it.

The site I wanted to host was a Hono app. Exposing the dev server directly was the smallest possible setup, and Hono's footprint left plenty of headroom. Cloudflare Tunnels handled the port exposure.

An Esprit Paradise solar panel powers the Pi. It barely keeps up, with a small battery pack to smooth the gaps. The end goal is fully portable.

When it's online: [feral.pure---internet.com](https://feral.pure---internet.com/)

## The app

Two background services feed a Hono app. The location service fuzzes the coordinates:

```typescript
export class LocationService {
  private cache: LocationData | null = null;
  private readonly CACHE_FILE = join(__dirname, "../../../cache/location.json");
  private readonly UPDATE_INTERVAL = 60 * 60 * 1000; // 1 hour

  private fuzzLocation(lat: number, lon: number): { latitude: number; longitude: number } {
    // Generate a random distance up to FUZZ_RADIUS_MILES
    const radiusMiles = Math.random() * FUZZ_RADIUS_MILES;
    const angle = Math.random() * 2 * Math.PI;
    
    // Convert to angular distance
    const angularDistance = radiusMiles / EARTH_RADIUS_MILES;

    // Calculate fuzzy position using spherical geometry
    const lat1 = lat * (Math.PI / 180);
    const lon1 = lon * (Math.PI / 180);

    const lat2 = Math.asin(
      Math.sin(lat1) * Math.cos(angularDistance) + 
      Math.cos(lat1) * Math.sin(angularDistance) * Math.cos(angle)
    );

    // ... coordinate calculations ...

    return {
      latitude: lat2 * (180 / Math.PI),
      longitude: ((lon2 * (180 / Math.PI) + 540) % 360) - 180,
    };
  }
}
```

The environment service uses those coordinates to build the ecosystem data:

```typescript
export class EnvironmentService {
  private cache: EnvironmentalData | null = null;
  private readonly CACHE_FILE = join(__dirname, "../../../cache/ecosystem.json");
  private readonly UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes

  private async updateEnvironment(): Promise<EnvironmentalData> {
    const locationData = await this.locationService.getLocation();
    
    // Get weather data from Open-Meteo
    const weatherData = await this.fetchWeather(locationData);
    
    // Get air quality if we have an API key
    const airQuality = process.env.WAQI_TOKEN 
      ? await this.fetchAirQuality(locationData)
      : this.defaultAirQuality;

    // Calculate sun and moon data
    const now = new Date();
    const times = suncalc.getTimes(now, locationData.latitude, locationData.longitude);
    const moonIllum = suncalc.getMoonIllumination(now);

    return {
      location: { /* location data */ },
      weather: { /* weather data */ },
      air: airQuality,
      astronomy: {
        sunrise: times.sunrise.toLocaleTimeString(),
        sunset: times.sunset.toLocaleTimeString(),
        moonPhase: this.getMoonPhase(moonIllum.phase),
        dayLength: this.formatDayLength(times.sunset - times.sunrise)
      }
    };
  }
}
```

Both feed into the Hono app:

```typescript
const app = new Hono();
const locationService = new LocationService();
const environmentService = new EnvironmentService(locationService);

// Serve the main page with markdown content and ecosystem data
app.get("/", async (c) => {
  const home = readFileSync(join(__dirname, "content/home.md"), "utf-8");
  const etc = readFileSync(join(__dirname, "content/etc.md"), "utf-8");
  const data = await environmentService.getEnvironment();

  return c.html(layout(`
    ${marked(home)}
    ${renderEcosystem(data)}
    ${marked(etc)}
  `));
});

// API endpoints for direct data access
app.get("/api/environment", async (c) => c.json(await environmentService.getEnvironment()));
app.get("/api/location", async (c) => c.json(await locationService.getLocation()));
```

## Setup

```bash
# Clone and install
git clone https://github.com/iammatthias/feral-pure-internet.git
cd feral-pure-internet
pnpm install

# Set up environment
echo "WAQI_TOKEN=your_token" > .env

# Configure Cloudflare Tunnel
cloudflared tunnel create feral
cloudflared tunnel route dns feral feral.pure---internet.com

# Run with PM2
pm2 start "pnpm dev" --name feral
pm2 save
pm2 startup systemd -u pi --hp /home/pi
```

The app runs at `localhost:3000`, exposes through the tunnel, and auto-starts on boot.

## Living with the sun

Most servers are always-on, always-available, and disconnected from the world they sit in.

This one is reachable when the sun is up. When it sets and the battery drains, the site goes dark. No load balancing, no redundancy.

Portability is the next step: a self-contained unit you can drop somewhere with sun. Add a few sensors and it really starts to feel feral.

## Earlier pure internet experiments

- [NFC](/posts/1731984900000-pure-internet-nfc): store a data URL on an NFC tag, load in browser on scan. Top-level navigation rejects data URLs, so the workaround pins minimal HTML on IPFS with JS bootstrapping the data URL from a query param into an iframe.

- [Bluesky](/posts/1732585567703-pure-internet-bluesky): inspired by Daniel Mangum's work. Hosted on the AtProtocol via the PDS and content-addressed blob storage.

---
title: "Pure Internet: Bluesky"
section: posts
description: "Static-site hosting on Bluesky's AT Protocol using content-addressed blob storage."
tags: ["web-development", "alternative-hosting", "at-protocol", "bluesky", "decentralization", "ipfs", "web-hosting"]
created: 2024-11-25T17:46:00Z
updated: 2026-07-16T14:56:30Z
cid: bafkreiey22nlo3yw74csliw5pgi5kj7v4cw4oo6arz5mafpqisqydglvwy
html: https://iammatthias.com/posts/1732585567703-pure-internet-bluesky
---

# Pure Internet: Bluesky

After [NFC cards with data URLs and ENS+IPFS](https://pureinternet.eth.limo/#UHVyZSBJbnRlcm5ldC4gQWxsIHRoZSBjb250ZW50IHlvdSBzZWUgaXMgZW5jb2RlZCBhcyBiYXNlNjQgYW5kIHN0b3JlZCBvbiBhbiBORkMgY2FyZC4gVGhlcmUncyBubyB0cmFkaXRpb25hbCBzZXJ2ZXIsIGp1c3QgYSBzaW5nbGUgcGllY2Ugb2YgSFRNTCBwaW5uZWQgdG8gSVBGUy4gUHVyZSBJbnRlcm5ldC4=), I [got a static site running on Bluesky's AT Protocol](https://amanita.us-east.host.bsky.network/xrpc/com.atproto.sync.getBlob?did=did:plc:p5xem22ammiafn5kxonaksfa&cid=bafkreih2pbifus4ed6p7kfuqjbjbqmzewlnyaz7f7ykmk5bvchkj7w3eb4). This experiment was inspired by [Daniel Mangum's exploration of atproto's storage capabilities](https://danielmangum.com/posts/this-website-is-hosted-on-bluesky/).

Bluesky is mostly known as a social platform. The AT Protocol underneath is a content-addressable system with built-in distribution. Every blob gets a CID, retrievable through any AT Proto node. It's IPFS in different clothes.

If you can upload arbitrary blobs and get stable URLs back, you can use it as web hosting. Here's the recipe.

1. First, create a session with your Bluesky credentials:

```
curl -X POST 'https://bsky.social/xrpc/com.atproto.server.createSession' \ -H 'Content-Type: application/json' \ -d '{"identifier": "your-handle.bsky.social", "password": "your-password"}'
```

Save the `accessJwt` from the response for the next steps.

2. Create an HTML file (`index.html`) with your website content.
3. Upload your HTML file as a blob:

```
curl -X POST 'https://bsky.social/xrpc/com.atproto.repo.uploadBlob' \ -H 'Authorization: Bearer YOUR_ACCESS_JWT' \ -H 'Content-Type: text/html' \ --data-binary '@index.html'
```

The response will include a blob reference with a CID. Save this CID for the next step.

4. Create a record referencing your blob:

```
curl -X POST 'https://bsky.social/xrpc/com.atproto.repo.createRecord' \ -H 'Authorization: Bearer YOUR_ACCESS_JWT' \ -H 'Content-Type: application/json' \ -d '{ "repo": "your-handle.bsky.social", "collection": "com.yourdomain.website", "record": { "$type": "com.yourdomain.website", "website": { "$type": "blob", "ref": { "$link": "YOUR_BLOB_CID" }, "mimeType": "text/html", "size": YOUR_FILE_SIZE } } }'
```

5. Get the URL of your website:

```
curl -I 'https://bsky.social/xrpc/com.atproto.sync.getBlob?did=YOUR_DID&cid=YOUR_BLOB_CID'
```

The response includes a `location` header with your website's permanent URL. Here's a [real example](https://amanita.us-east.host.bsky.network/xrpc/com.atproto.sync.getBlob?did=did:plc:p5xem22ammiafn5kxonaksfa&cid=bafkreih2pbifus4ed6p7kfuqjbjbqmzewlnyaz7f7ykmk5bvchkj7w3eb4):

```
https://amanita.us-east.host.bsky.network/xrpc/com.atproto.sync.getBlob?did=did:plc:p5xem22ammiafn5kxonaksfa&cid=bafkreih2pbifus4ed6p7kfuqjbjbqmzewlnyaz7f7ykmk5bvchkj7w3eb4
```

The URL parts: `amanita.us-east.host.bsky.network` is a Bluesky content delivery node, `com.atproto.sync.getBlob` is the retrieval endpoint, `did` is the content owner, `cid` is the content hash.

The trade-offs are real. You depend on Bluesky's infrastructure. Blobs have size caps. The platform isn't built for static hosting, so content could disappear. The setup is just dead simple.

It's not pure internet in the sovereign sense, since the federation still owns the delivery. But it leaves a question hanging: how far can a social protocol stretch before it becomes a general-purpose CDN?

---
title: "Pure Internet: NFC"
section: posts
description: "HTML, CSS & minimal JS combined with NFC tech to create serverless web experiences."
tags: ["web-development", "nfc", "data-urls", "html", "ipfs", "egg", "decentralized-web"]
created: 2024-11-18T10:55:00Z
updated: 2025-05-23T14:09:00Z
cid: bafkreie5ij7rghiderfncm6auo77ico5ybud2wf2c6usd6m2ljdh24ezli
html: https://iammatthias.com/posts/1731984900000-pure-internet-nfc
---

# Pure Internet: NFC

Strip the web back to HTML, CSS, and a sprinkle of JavaScript. No build process, no stylesheets, no dependencies.

What if you put it on an NFC card?

## NFC

Most modern phones can read NFC. Beyond contactless payments and transit cards, NFC can carry arbitrary data through NDEF (NFC Data Exchange Format) records.

The limits matter: a few kilobytes of storage per tag, and iOS handles NFC differently from Android. iOS only honors specific record types and demands an explicit user tap. Android reads more record types and supports background scanning.

## Data URLs

Data URLs encode a file inline, usually as base64. They've been around forever, mostly for small images in CSS:

```
background-image: url(data:image/png;base64,iVBORw0KGgo...)
```

Since 2017, browsers refuse data URLs for top-level navigation. Good for security, fatal for the original idea.

## First attempt

Plan A: store an entire website as base64 on an NFC card. Tap to open. No server.

Top-level navigation said no. Users can still copy and paste, but tap-to-open is dead:

```
data:text/html;base64,PHRpdGxlPk9mZmxpbmU8L3RpdGxlPjxib2R5PjxoMT5PZmZsaW5lPC9oMT48cD5UaGlzIGlzIGEgd2ViIHBhZ2UsIGJ1dCBpdCdzIG5vdCBvbmxpbmUuPC9wPjxwPkl0IGlzIGVuY29kZWQgaW4gdGhlIFVSSSwgd2hpY2ggaXMgc3RvcmVkIG9uIHRoZSBORkMgY2FyZCB5b3UganVzdCBzY2FubmVkLjwvcD48cD5UaGFuayB5b3UgZm9yIHZpc2l0aW5nLjwvcD48L2JvZHk%2B
```

The `file://` scheme hits the same security wall. Raw HTML text records need manual handling. Custom MIME types confuse most readers.

## ENS + IPFS

Workaround: ENS name, an HTML file pinned to IPFS, an iframe to render the data URL, an NFC card storing an [eth.limo](http://eth.limo) URL, and base64 content in the hash.

The HTML:

```
<!doctype html>
<html>
  <head>
    <title>PURE INTERNET</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body>
    <iframe id="f"></iframe>
    <script>
      const iframe = document.getElementById("f");
      iframe.src = location.hash
        ? `data:text/html;base64,${location.hash.slice(1)}`
        : "data:text/html;base64,PHA+UFVSRSBJTlRFUk5FVDwvcD4=";
    </script>
  </body>
</html>
```

Encode the page as base64. Stick it on the NFC card as part of an [eth.limo](http://eth.limo) URL like `https://pureinternet.eth.limo/#VGhpcyBpcyBQdXJlIEludGVybmV0`. Tap loads the IPFS-pinned HTML through ENS. The script pulls the base64 from the hash and renders it in the iframe, which sidesteps the top-level data-URL block.

## Is this pure internet?

Probably the closest you can get. IPFS and ENS are still infrastructure, but the rest is HTML, CSS, JS, and a tag in your hand.

The hash trick generalizes. Any page can carry its content in the URL itself: no database, no storage, content that exists only as long as the URL does.

---
title: "Pure Internet"
section: posts
description: "Pure Internet is a running collection of small experiments in publishing with fewer layers."
tags: ["web-development", "tech-history", "decentralization", "ipfs", "raspberry-pi", "at-protocol", "bluesky", "cloudflare-tunnel", "solar-powered", "sensors", "nfc", "data-urls"]
created: 2024-11-18T10:49:00Z
updated: 2026-07-16T14:56:29Z
cid: bafkreifm7gr6nhjrnusl3gweckc52lcaavslzberucohjn2rnrvlft73ae
html: https://iammatthias.com/posts/1731955749292-pure-internet
---

# Pure Internet

"Pure Internet" is a tongue-in-cheek name for anything you can reach through a browser, served over the internet. The joke is mostly about how many layers we have managed to put between those two things.

I'm a 90s kid. My first website was made in a school computer lab and stored on a [Zip Drive](https://en.wikipedia.org/wiki/Zip_drive). The disk is lost, but I remember the monotype Notepad masterpiece in all its asteroid-GIF glory, perfectly Geocities.

Publishing now supports an entire industry of services, frameworks, and dashboards. Sharing hypertext should still be easier than that.

That's the thread running through these projects. Simplify, abstract, look at HTML and CSS sideways.

- [nfc](/posts/1731984900000-pure-internet-nfc): data URLs on an NFC tag. Top-level navigation rejects data URLs, so the workaround is minimal HTML hosted on IPFS, with JS bootstrapping a data URL from a query param into an iframe. [nfc.pure---internet.com](https://nfc.pure---internet.com/)

- [bluesky](/posts/1732585567703-pure-internet-bluesky): an experiment inspired by [Daniel Mangum's prior work](https://danielmangum.com/posts/this-website-is-hosted-on-bluesky). Hosted on Bluesky's AT Protocol, using the Personal Data Server (PDS) and content-addressed blob storage. [bluesky.pure---internet.com](https://bluesky.pure---internet.com/)

- [feral---pi](/posts/1733357455673-pure-internet-feral): a solar-powered Raspberry Pi running a small Hono app, exposed through a Cloudflare tunnel. [feral.pure---internet.com](https://feral.pure---internet.com/)

- [office---space](/posts/1736536967495-pure-internet-office-space): a Raspberry Pi with environmental sensors tracking the conditions of my office. [office.pure---internet.com](https://office.pure---internet.com/)

- [no-html](/posts/1740682611811-pure-internet-no-html): a website with no HTML. [no-html.pure---internet.com](https://no-html.pure---internet.com/)

---
title: "Shitposting Towards a Pizza Oven"
section: posts
description: "Automated YouTube Shorts about pizza lead viewers to shouldibuyagozney.com with its one-word answer: \"yes.\" The algorithm grows daily."
tags: ["automation", "marketing", "pizza", "web-development", "content-distribution", "algorithms"]
created: 2024-11-12T21:32:00Z
updated: 2025-05-24T16:58:00Z
cid: bafkreiahxoi2txotcj5izo4bsplv5x2eaznuvu37zn6tryfwqmdh6t6kpy
html: https://iammatthias.com/posts/1731475962422-shitposting-towards-a-pizza-oven
---

# Shitposting Towards a Pizza Oven

The domain name [shouldibuyagozney.com](http://shouldibuyagozney.com) cost about ten dollars a few months ago. This seemed reasonable for a joke I might never execute, but over the last few days I put something in motion.

The page displays a single word: "yes."

I [automated YouTube Shorts](https://reel.farm) about [pizza](https://www.youtube.com/@ShouldIbuyaGozney). New videos populate the algorithm daily, breadcrumbs leading back to that single-word answer. The metrics tick up. The hashtags optimize themselves. Sometimes I wonder if Gozney will notice.

They might send lawyers. They might send a cease and desist. Or maybe they'll see it for what it is - one person's way of saying "I really like what you made."

In the meantime, the automation runs, and the answer remains: yes.

---
title: "Spicy Tom Kha with Char-grilled Pork Loin"
section: recipes
description: "Spicy coconut tom kha with mushrooms and tofu, served with char-grilled pork loin in a soy-lime marinade."
tags: ["thai", "soup", "tom-kha", "coconut", "spicy", "asian", "pork", "grilled", "meat", "lemongrass", "lime", "garlic", "dinner", "chili", "tofu", "mushrooms", "coriander", "recipe", "main-dish"]
created: 2024-10-20T13:39:00Z
updated: 2026-07-29T21:06:21Z
cid: bafkreigq4psrgzivu6ywhvseyergk46lxmwm2bzulneb4y6oq3gbvcefh4
html: https://iammatthias.com/recipes/1729456768249-spicy-tom-kha-with-char-grilled-pork-loin
---

# Spicy Tom Kha with Char-grilled Pork Loin

```recipe
yield: "2 servings"
time: "30 min active · 1 h total"

ingredients:
  - { id: banana-shallot, item: "banana shallot", amount: "1", note: "finely chopped", group: "Aromatics" }
  - { id: ginger, item: "ginger", amount: "thumb-sized knob", note: "grated or minced", group: "Aromatics" }
  - { id: red-chilli, item: "red chilli", amount: "½", note: "chopped", group: "Aromatics" }
  - { id: thai-chilli-oil, item: "Thai chilli oil", amount: "to taste", note: "optional, for garnish", group: "Add-ins and Garnishes" }
  - { id: soy-sauce, item: "soy sauce", amount: "2 tbsp", group: "Broth Bases" }
  - { id: rice-wine-vinegar, item: "rice wine vinegar", amount: "1 tbsp", group: "Broth Bases" }
  - { id: thai-red-curry-paste, item: "thai red curry paste", amount: "1 tbsp", note: "optional", group: "Broth Bases" }
  - { id: vegetable-broth, item: "vegetable broth", amount: "1 liter", group: "Broth Bases" }
  - { id: coconut-cream, item: "coconut cream", amount: "400 ml", note: "richer than coconut milk", group: "Broth Bases" }
  - { id: shiitake-mushrooms, item: "shiitake mushrooms", amount: "200g", note: "sliced", group: "Add-ins and Garnishes" }
  - { id: tofu, item: "tofu", amount: "150g", note: "medium firm, sliced", group: "Add-ins and Garnishes" }
  - { id: brown-coconut-sugar, item: "brown/coconut sugar", amount: "1 tsp", group: "Broth Bases" }
  - { id: lime, item: "lime", amount: "1", note: "use both juice and zest at the end", group: "Broth Bases" }
  - { id: spring-onions, item: "spring onions", amount: "1 bunch", note: "chopped", group: "Add-ins and Garnishes" }
  - { id: fresh-coriander, item: "fresh coriander", amount: "handful", note: "chopped", group: "Add-ins and Garnishes" }
  - { id: salt, item: "salt", amount: "to taste", group: "Add-ins and Garnishes" }
  - { id: black-pepper, item: "black pepper", amount: "to taste", note: "freshly ground", group: "Add-ins and Garnishes" }
  - { id: garlic-cloves, item: "garlic cloves", amount: "4", note: "minced", group: "Aromatics" }
  - { id: lemongrass-stalk, item: "lemongrass stalk", amount: "1", note: "bashed", group: "Aromatics" }
  - { id: neutral-oil-coconut-veg, item: "neutral oil (coconut/veg)", amount: "1 tbsp", note: "for sautéing aromatics", group: "Add-ins and Garnishes" }

steps:
  - id: s1
    in: [banana-shallot, ginger, red-chilli, garlic-cloves, lemongrass-stalk, neutral-oil-coconut-veg]
    do: "prepare the aromatics"
    for: "5 min"
    detail: "**Prepare the aromatics:** Finely chop the shallot, garlic, ginger, and chilli. Bash the lemongrass to release its flavor."
  - id: s2
    in: [s1, thai-chilli-oil]
    do: "sauté the aromatics"
    for: "3–5 min"
    detail: "**Sauté the aromatics:** Heat the neutral oil in a saucepan over medium heat. Add the aromatics and sauté for 3-5 minutes, stirring well to combine flavors."
  - id: s3
    in: [s2, soy-sauce, rice-wine-vinegar, thai-red-curry-paste, vegetable-broth]
    do: "flavor the broth"
    for: "2–3 min"
    detail: "**Flavor the broth:** Stir in the soy sauce, rice vinegar, and optional curry paste. Cook for another 2-3 minutes to deepen the flavors."
  - id: s4
    in: [s3]
    do: "add broth and simmer"
    for: "15–20 min"
    detail: "**Add broth and simmer:** Slowly pour in the vegetable broth, stirring as you go. Bring the mixture to a boil, then reduce to a simmer for 15-20 minutes."
  - id: s5
    in: [s4, coconut-cream]
    do: "add coconut cream"
    detail: "**Add coconut cream:** Strain the broth through a sieve to remove the aromatics, then return the broth to the pot. Add the coconut cream. If the soup is too thick, dilute with an additional 100-150 ml of vegetable broth or water, as needed. Simmer gently."
  - id: s6
    in: [s5, shiitake-mushrooms, tofu]
    do: "cook mushrooms and tofu"
    for: "5–10 min"
    detail: "**Cook mushrooms and tofu:** Add the sliced mushrooms and tofu to the broth and cook until softened, about 5-10 minutes."
  - id: s7
    in: [s6, brown-coconut-sugar, lime, spring-onions, fresh-coriander, salt, black-pepper]
    do: "finish with herbs and seasoning"
    detail: "**Finish with herbs and seasoning:** Stir in the chopped spring onions and coriander. Season with sugar, lime zest, lime juice, salt, and pepper to taste."
  - id: s8
    in: [s7]
    do: "serve"
    detail: "**Serve:** Serve the soup in deep bowls with extra red chilli, coriander, lime wedges, and a swirl of Thai chilli oil, if desired."
```

---
title: "Case Study: Migrating Ice Barrel to Shopify"
section: posts
description: "Ice Barrel moved from WooCommerce to Shopify in under two months, lifting conversion 165% while handling a major product launch without checkout downtime."
tags: ["e-commerce", "platform-migration", "shopify", "cold-therapy", "case-study", "conversion-optimization", "analytics"]
created: 2024-10-10T09:00:00Z
updated: 2026-07-16T14:56:29Z
cid: bafkreifkv3qvcsl4r4jt3p7nuyxrpjlom5wnhcn2xqyhvctzhejfadfnha
html: https://iammatthias.com/posts/1728582380556-case-study-ice-barrel
---

# Case Study: Migrating Ice Barrel to Shopify

Platform migrations are never easy. You take something that was built with intention, and declare that it needs to be fundamentally changed. When the new platform lands, the results either speak for themselves or they don't.

[Ice Barrel](https://icebarrel.com/) was scaling fast in cold therapy and the WooCommerce store was running out of headroom. I joined to sharpen product marketing and analytics, and to work with the remote dev team on the existing platform.

## The starting point

WooCommerce got Ice Barrel through its early years. It was flexible, customizable, and let a small team move fast.

By the time we were prepping the [Ice Barrel 500](https://icebarrel.com/products/ice-barrel-500) and the [Ice Barrel Chiller](https://icebarrel.com/products/ice-barrel-chiller), the cracks were obvious: shaky attribution, a brittle analytics setup, scaling concerns ahead of a major launch, slow deploys, and a codebase that was hard to maintain.

## The proposal

I put together a case for moving to Shopify: growth modeling against the new platform, a market read on what comparable brands were running, and a working theme demo. Shopify won on infrastructure, app ecosystem, and the runway it gave the team after handoff.

The Chiller's public sale was the forcing function.

## Migration

We scoped six months and shipped in just under two. The work focused on data integrity (every customer, order, and product transferred clean), brand identity rebuilt and improved on the new platform, refactored workflows with documentation, and team training so they could run it without me.

We launched the Chiller's public sale on the new store.

## Results

- Conversion rate up 165% vs. the prior weeks
- Weekly traffic up 200%, no performance degradation
- Weekly revenue up 300%
- Cart abandonment down 10-15 percentage points
- Email CTR up ~20%, email-driven revenue share nearly doubled
- Mobile conversion up 200%
- Average page load time down 40%
- Chiller launch beat the previous best by 150% on first-week volume
- Zero checkout downtime through the cutover

Cold therapy is a young category and Ice Barrel is one of the brands shaping it. The new platform gave them room to keep doing that.

---
title: "Crème Brûlée"
section: recipes
description: "Vanilla custard baked in a water bath and finished with a caramelized sugar crust."
tags: ["dessert", "cr-me", "br-l-e", "french", "custard", "egg", "vanilla", "baking"]
created: 2024-09-27T11:11:00Z
updated: 2026-07-29T21:06:21Z
cid: bafkreieahdeovmdtzz2cvq7bbpfda26b5evfw3ogr3z454hpndqiakwrca
html: https://iammatthias.com/recipes/1727460674219-creme-brulee
---

# Crème Brûlée

```recipe
yield: "2 servings"
time: "20 min active · 4 h 30 total"

ingredients:
  - { id: cream, item: "Heavy cream", amount: "240 ml (1 cup)", note: "or half-and-half for a lighter set" }
  - { id: vanilla, item: "Vanilla bean", amount: "½ bean, split", note: "or ½ tsp extract, stirred in off the heat" }
  - { id: salt, item: "Salt", amount: "a pinch" }
  - { id: yolks, item: "Egg yolks", amount: "45 g (2–3 yolks)", note: "weigh if you can — the true half of 5 yolks is 2½" }
  - { id: sugar, item: "Granulated sugar", amount: "50 g (¼ cup)", note: "plus 2 tsp for the tops" }

steps:
  - { id: oven, prep: true, do: "heat the oven to 325°F (160°C)" }
  - { id: infuse, in: [cream, vanilla, salt], do: "steep", for: "10 min", detail: "Warm over low heat until steaming, then pull it off and let the bean infuse. Fish out the pod." }
  - { id: ribbon, in: [yolks, sugar], do: "beat to a ribbon", for: "2 min", detail: "Beat the yolks and sugar until pale and thick enough to fall in a slow band off the whisk." }
  - { id: temper, in: [infuse, ribbon], do: "temper", for: "1 min", detail: "Whisk a quarter of the hot cream into the yolks, then return everything to the pan and stir to combine." }
  - { id: bake, in: [temper], do: "bake in a bain-marie", for: "30–40 min", detail: "Divide between two 6-oz ramekins, set them in a baking dish, and pour boiling water halfway up their sides. They are done when set with a faint wobble at the centre — the ramekins are the same size as in the full batch, so the time is unchanged." }
  - { id: chill, in: [bake], do: "chill", for: "4 h", detail: "Cool to room temperature, then refrigerate until properly cold. They keep 2 days." }
  - { id: brulee, in: [chill], do: "brûlée", for: "5 min", detail: "Sprinkle a teaspoon of sugar over each and torch, or broil 2–3 in. from the element, until the sugar melts and browns. Eat within 2 hours — the crust softens." }
```

---
title: "Pavlova"
section: recipes
description: "A classic Pavlova recipe with crisp meringue shell and creamy custard filling, topped with fresh fruit and ready in under 2 hours."
tags: ["dessert", "pavlova", "meringue", "custard", "fruit", "baking", "egg"]
created: 2024-09-15T18:29:00Z
updated: 2026-07-29T21:20:27Z
cid: bafkreiawfyym2ba7fhjvfmctrrksnuanpcxpqrifcg7wpi3b6yaaporh7a
html: https://iammatthias.com/recipes/1726450168175-pavlova
---

# Pavlova

```recipe
yield: "2 servings"
time: "20 min active · 2 h 30 total"

ingredients:
  - { id: large-egg-whites, item: "large egg whites", amount: "1", note: "room temperature", group: "Meringue" }
  - { id: granulated-sugar, item: "granulated sugar", amount: "¼ cup (50g)", group: "Meringue" }
  - { id: cream-of-tartar, item: "cream of tartar", amount: "a pinch", group: "Meringue" }
  - { id: granulated-sugar-2, item: "granulated sugar", amount: "1½ tablespoons", group: "Dairy Custard" }
  - { id: vanilla-extract, item: "vanilla extract", amount: "¼ teaspoon", group: "Meringue" }
  - { id: white-vinegar, item: "white vinegar", amount: "¼ teaspoon", group: "Meringue" }
  - { id: vanilla-extract-2, item: "vanilla extract", amount: "¼ teaspoon", group: "Dairy Custard" }
  - { id: large-egg-yolks, item: "large egg yolks", amount: "1", group: "Dairy Custard" }
  - { id: whole-milk, item: "whole milk", amount: "⅔ cups", group: "Dairy Custard" }
  - { id: fresh-fruit, item: "fresh fruit", amount: "as needed", note: "berries, kiwi, etc.", group: "Meringue" }
  - { id: salt, item: "salt", amount: "pinch", group: "Dairy Custard" }

steps:
  - id: s1
    prep: true
    do: "preheat the oven"
    detail: "**Preheat the oven:** Preheat your oven to 300°F (150°C). Line a baking sheet with parchment paper and draw a 5-inch (13 cm) circle on the paper as a guide."
    phase: "To make the meringue"
  - id: s2
    in: [large-egg-whites, cream-of-tartar, granulated-sugar, granulated-sugar-2, salt]
    do: "whisk the egg whites"
    for: "8–10 min"
    detail: "**Whisk the egg whites:** In a clean, dry bowl, beat the egg whites on medium speed until they become frothy. Add the cream of tartar and continue to beat until soft peaks form. **Add sugar:** Gradually add the sugar, one tablespoon at a time, while continuing to beat the mixture. Beat on high speed until stiff, glossy peaks form, and the sugar is fully dissolved."
    phase: "To make the meringue"
  - id: s3
    in: [s2, vanilla-extract, white-vinegar, vanilla-extract-2]
    do: "add remaining ingredients"
    detail: "**Add remaining ingredients:** Gently fold in the vanilla extract and white vinegar using a spatula. **Shape the meringue:** Spoon the meringue onto the prepared baking sheet, using the circle as a guide. Shape it into a disk with slightly raised edges to hold the toppings."
    phase: "To make the meringue"
  - id: s4
    in: [s3]
    do: "bake"
    for: "1 h"
    detail: "**Bake:** Place the meringue in the oven and immediately reduce the temperature to 250°F (120°C). Bake for 45 minutes to 1 hour, or until the meringue is crisp and dry on the outside. Turn off the oven, leaving the meringue inside to cool completely."
    phase: "To make the meringue"
  - id: s5
    in: [s4, large-egg-yolks, whole-milk]
    do: "mix egg yolks and sugar"
    detail: "**Mix egg yolks and sugar:** In a medium bowl, whisk together the egg yolks and sugar until well combined and slightly thickened. **Heat the milk:** In a medium saucepan, heat the whole milk over medium heat until it just begins to simmer. Do not let it boil."
    phase: "To make the custard"
  - id: s6
    in: [s5]
    do: "temper the egg mixture"
    for: "8–10 min"
    detail: "**Temper the egg mixture:** Slowly pour about 2 tbsp of the hot milk into the egg yolk mixture, whisking constantly to temper the eggs. Gradually add the tempered egg mixture back into the saucepan with the remaining milk. **Cook the custard:** Continue to cook the mixture over medium-low heat, whisking constantly, until it thickens slightly and coats the back of a spoon, about 8-10 minutes."
    phase: "To make the custard"
  - id: s7
    in: [s6]
    do: "add vanilla"
    for: "1 h"
    detail: "**Add vanilla:** Remove the saucepan from the heat and stir in the vanilla extract. **Cool the custard:** Pour the custard into a bowl and cover the surface with plastic wrap to prevent a skin from forming. Let it cool to room temperature, then refrigerate for at least 1 hour before serving."
    phase: "To make the custard"
  - id: s8
    in: [s7]
    do: "gently lift it"
    detail: "Once the meringue has cooled, gently lift it off the parchment paper and place it on a serving plate. Spoon the chilled custard over the Pavlova base."
    phase: "To assemble"
  - id: s9
    in: [s8, fresh-fruit]
    do: "top with fresh fruit"
    detail: "Top with fresh fruit and serve immediately."
    phase: "To assemble"
```

---
title: "Mumbai Street Style Vada Pav"
section: recipes
description: "Iconic Mumbai street food with spiced potato fritters in buns."
tags: ["indian", "street-food", "vegan", "vegetarian", "recipe", "potato", "spicy", "snacks"]
created: 2024-09-15T09:18:00Z
updated: 2026-07-29T21:20:27Z
cid: bafkreibsxwpx7m77vkrjxwpns3t6he2v6jruqw45idhjhiz6jpsgcjxg2y
html: https://iammatthias.com/recipes/1726417129936-mumbai-street-style-vada-pav
---

# Mumbai Street Style Vada Pav

```recipe
yield: "2 servings (2 pav)"
time: "45 min active · 1 h 30 total"
source: "Timings after Hebbar's Kitchen's Mumbai vada pav"
sourceURL: "https://hebbarskitchen.com/vada-pav-recipe-make-vada-pav-wada-pav/"

ingredients:
  - { id: garlic, item: "Garlic", amount: "2 cloves", note: "coarsely ground", group: "For the Vada (Potato Fritters)" }
  - { id: ginger, item: "Ginger", amount: "⅓-⅔ inch piece", note: "coarsely ground", group: "For the Vada (Potato Fritters)" }
  - { id: mustard-seeds, item: "Mustard seeds", amount: "⅓ tbsp", group: "For the Vada (Potato Fritters)" }
  - { id: cumin-seeds, item: "Cumin seeds", amount: "⅓ tbsp", group: "For the Vada (Potato Fritters)" }
  - { id: curry-leaves, item: "Curry leaves", amount: "5–7 leaves", group: "For the Vada (Potato Fritters)" }
  - { id: oil, item: "Oil", amount: "⅓ tbsp", note: "for tempering", group: "For the Vada (Potato Fritters)" }
  - { id: coriander-leaves, item: "Coriander leaves", amount: "⅔ tbsp", note: "finely chopped", group: "For the Vada (Potato Fritters)" }
  - { id: ajwain-carom-seeds, item: "Ajwain (carom seeds)", amount: "⅓ tsp", group: "For the Besan Batter" }
  - { id: ginger-2, item: "Ginger", amount: "⅓ inch piece", group: "For the Green Chutney" }
  - { id: potatoes, item: "Potatoes", amount: "1⅓-1⅔ medium", note: "boiled and mashed", group: "For the Vada (Potato Fritters)" }
  - { id: garam-masala, item: "Garam masala", amount: "⅓ tsp", group: "For the Vada (Potato Fritters)" }
  - { id: salt, item: "Salt", amount: "to taste", group: "For the Vada (Potato Fritters)" }
  - { id: salt-2, item: "Salt", amount: "to taste", group: "For the Besan Batter" }
  - { id: salt-3, item: "Salt", amount: "to taste", group: "For the Sukhi Garlic Chutney" }
  - { id: fresh-coriander, item: "Fresh coriander", amount: "⅓ cup", group: "For the Green Chutney" }
  - { id: salt-4, item: "Salt", amount: "to taste", group: "For the Green Chutney" }
  - { id: water, item: "Water", amount: "~⅔ cups", note: "adjust consistency", group: "For the Besan Batter" }
  - { id: besan-batter, item: "Besan batter", amount: "as needed", note: "for rough frying", group: "For the Chura (Crispy Batter Crumbs)" }
  - { id: water-2, item: "Water", amount: "as needed", group: "For the Green Chutney" }
  - { id: gram-flour-besan, item: "Gram flour (besan)", amount: "½ cups", group: "For the Besan Batter" }
  - { id: turmeric-powder, item: "Turmeric powder", amount: "⅓ tsp", group: "For the Vada (Potato Fritters)" }
  - { id: turmeric-powder-2, item: "Turmeric powder", amount: "⅓ tsp", group: "For the Besan Batter" }
  - { id: red-chili-powder, item: "Red chili powder", amount: "⅓ tbsp", group: "For the Sukhi Garlic Chutney" }
  - { id: prepared-chura, item: "Prepared chura", amount: "⅓-½ cups", note: "from above", group: "For the Sukhi Garlic Chutney" }
  - { id: pav-buns, item: "Pav buns", amount: "2", group: "For the Pav (Buns)" }
  - { id: butter, item: "Butter", amount: "⅔ tbsp", note: "for toasting", group: "For the Pav (Buns)" }
  - { id: green-chilies, item: "Green chilies", amount: "1-1⅓ pcs", note: "coarsely ground", group: "For the Vada (Potato Fritters)" }
  - { id: fried-garlic-cloves, item: "Fried garlic cloves", amount: "4-5", group: "For the Sukhi Garlic Chutney" }
  - { id: hing-asafoetida, item: "Hing (Asafoetida)", amount: "a pinch", group: "For the Sukhi Garlic Chutney" }
  - { id: garlic-cloves, item: "Garlic cloves", amount: "1⅓-1⅔ pcs", group: "For the Green Chutney" }
  - { id: green-chilies-2, item: "Green chilies", amount: "⅔-1 pcs", note: "optional", group: "For the Green Chutney" }

steps:
  - id: s1
    in: [mustard-seeds, cumin-seeds, curry-leaves, oil, coriander-leaves, ajwain-carom-seeds, garlic, ginger, ginger-2, green-chilies, fried-garlic-cloves, hing-asafoetida, garlic-cloves, green-chilies-2]
    do: "heat oil in a pan"
    for: "5 min"
    detail: "Heat oil in a pan and add mustard seeds, cumin seeds, and curry leaves. Add ground garlic, ginger, and green chilies. Sauté until fragrant."
    phase: "To make the Aloo Masala (Potato Filling)"
  - id: s2
    in: [s1, garam-masala, salt, salt-2, salt-3, salt-4, potatoes, fresh-coriander]
    do: "stir in turmeric"
    for: "2–3 min"
    detail: "Stir in turmeric, garam masala, and salt. Add boiled, mashed potatoes and fresh coriander. Mix well and let it cool."
    phase: "To make the Aloo Masala (Potato Filling)"
  - id: s3
    in: [s2]
    do: "form the potato mixture"
    for: "10 min"
    detail: "Once cooled, form the potato mixture into small balls."
    phase: "To make the Aloo Masala (Potato Filling)"
  - id: s4
    in: [s3, water, besan-batter, water-2]
    do: "combine gram flour"
    detail: "In a bowl, combine gram flour, turmeric, ajwain, and salt. Gradually add water to create a smooth, lump-free batter."
    phase: "To make the Besan Batter"
  - id: s5
    in: [s4, gram-flour-besan]
    do: "dip your fingertips"
    detail: "Dip your fingertips into the prepared besan batter and roughly pour the batter into hot oil. Fry until crispy and golden brown, then remove and set aside."
    phase: "To make the Chura (Crispy Batter Crumbs)"
  - id: s6
    in: [s5, turmeric-powder, turmeric-powder-2, red-chili-powder, prepared-chura]
    do: "add fried garlic"
    for: "3–4 min"
    detail: "In a grinder, add fried garlic, red chili powder, hing, prepared chura, and salt. Grind the mixture into a coarse chutney."
    phase: "To make the Sukhi Garlic Chutney"
  - id: s7
    in: [s6]
    do: "blend fresh coriander"
    for: "7–8 min"
    detail: "Blend fresh coriander, garlic, ginger, green chilies, water, and salt until smooth. Adjust consistency as needed."
    phase: "To make the Green Chutney"
  - id: s8
    in: [s7]
    do: "heat oil for frying"
    for: "2–3 min"
    detail: "Heat oil for frying. Dip the prepared potato balls into the besan batter. Fry the vadas on medium heat until golden and crispy. Drain on paper towels."
    phase: "To fry the Vada"
  - id: s9
    in: [s8, pav-buns, butter]
    do: "slit each pav bun"
    detail: "Slit each pav bun in half and toast lightly with butter. Spread sukhi garlic chutney on one side and green chutney on the other."
    phase: "To assemble the Vada Pav"
  - id: s10
    in: [s9]
    do: "place a fried vada"
    detail: "Place a fried vada in the center and serve with fried green chilies on the side."
    phase: "To assemble the Vada Pav"
```

---
title: "Honey Liliko‘i Foam"
section: recipes
description: "Smooth passionfruit foam made by blending honey, purée, syrup, egg white and water, then charged in a siphon with N2O."
tags: ["foam", "passionfruit", "honey", "egg-white", "recipe", "cocktails", "mixology"]
created: 2024-09-14T09:15:00Z
updated: 2026-07-29T21:06:21Z
cid: bafkreicl3jdx5gkzvewdiuwguohtck6s6z7emzljvenv6lxvrjeniqbftu
html: https://iammatthias.com/recipes/1726330512509-honey-liliko-i-foam
---

# Honey Liliko‘i Foam

```recipe
yield: "about 1 cup — enough for 8 drinks"
time: "10 min"

ingredients:
  - { id: honey, item: "Honey", amount: "½ oz" }
  - { id: simple-syrup, item: "Simple syrup", amount: "1 oz" }
  - { id: egg-white, item: "Egg white", amount: "1 oz" }
  - { id: cold-water, item: "Cold water", amount: "1½ oz" }
  - { id: passionfruit-pur-e, item: "Passionfruit purée", amount: "1 oz", note: "Liliko‘i or regular passionfruit purée" }

steps:
  - id: s1
    in: [honey, simple-syrup, egg-white, cold-water, passionfruit-pur-e]
    do: "combine the honey"
    detail: "Combine the honey, passionfruit purée, simple syrup, egg white, and cold water in a blender. Blend until the mixture is smooth and fully emulsified."
  - id: s2
    in: [s1]
    do: "strain the blended"
    detail: "Strain the blended mixture through a fine-mesh sieve to remove any solids or pulp for a smoother consistency."
  - id: s3
    in: [s2]
    do: "pour the strained liquid"
    detail: "Pour the strained liquid into a whipped cream siphon."
  - id: s4
    in: [s3]
    do: "charge the siphon"
    detail: "Charge the siphon with 1 N2O cartridge, then shake it well to incorporate air into the mixture and create the foam."
  - id: s5
    in: [s4]
    do: "chill the siphon"
    for: "30 min"
    detail: "Chill the siphon for at least 30 minutes before dispensing to allow the foam to set and hold its structure when served."
```

---
title: "Liliko'i Rum Punch Medley"
section: recipes
description: "A tropical rum punch blending pineapple, passion fruit and three rums with homemade pineapple rose syrup."
tags: ["cocktails", "rum-punch", "tropical", "passionfruit", "pineapple", "meyer-lemon", "spirits", "recipe"]
created: 2024-09-14T09:11:00Z
updated: 2026-07-29T21:06:20Z
cid: bafkreigvmaxgkfgcerapg52doev4qicxodv64hgplti745mtepykqoflmm
html: https://iammatthias.com/recipes/1726330319079-liliko-i-rum-punch-medley
---

# Liliko'i Rum Punch Medley

```recipe
yield: "2 drinks"
time: "10 min"

ingredients:
  - { id: fresh-pineapple, item: "Fresh pineapple", amount: "½ cup", note: "Muddled with Meyer lemon", group: "Rum Punch" }
  - { id: meyer-lemon, item: "Meyer lemon", amount: "1", note: "Divided, juice and segments used", group: "Rum Punch" }
  - { id: fresh-lime-juice, item: "Fresh lime juice", amount: "½ lime", note: "Juice used", group: "Rum Punch" }
  - { id: meyer-lemon-2, item: "Meyer lemon", amount: "½", note: "Juice and segments used", group: "Pineapple Rose Syrup" }
  - { id: honey-or-sugar, item: "Honey (or sugar)", amount: "¼ cup", note: "Adjust to taste", group: "Pineapple Rose Syrup" }
  - { id: water, item: "Water", amount: "1 cup", group: "Pineapple Rose Syrup" }
  - { id: rose-tea, item: "Rose tea", amount: "2-3 tea bags", note: "Brewed directly in the syrup", group: "Pineapple Rose Syrup" }
  - { id: passion-fruits, item: "Passion fruits", amount: "2", note: "Pulp scooped out", group: "Rum Punch" }
  - { id: grenadine, item: "Grenadine", amount: "1 capful", note: "Adjust to taste", group: "Rum Punch" }
  - { id: light-rum, item: "Light rum", amount: "1 oz", group: "Rum Punch" }
  - { id: dark-rum, item: "Dark rum", amount: "1 oz", group: "Rum Punch" }
  - { id: spiced-rum, item: "Spiced rum", amount: "1 oz", group: "Rum Punch" }
  - { id: apricot-brandy, item: "Apricot brandy", amount: "1 oz", group: "Rum Punch" }
  - { id: aperol, item: "Aperol", amount: "1 capful", note: "Optional", group: "Rum Punch" }
  - { id: bitters, item: "Bitters", amount: "Dash", group: "Rum Punch" }
  - { id: balsamic-vinegar, item: "Balsamic vinegar", amount: "Dash", note: "High-quality, aged", group: "Rum Punch" }
  - { id: frozen-cantaloupe, item: "Frozen cantaloupe", amount: "½ cup", note: "Used in place of ice for shaking", group: "Rum Punch" }
  - { id: ice, item: "Ice", amount: "As needed", note: "Serve over ice in chilled glasses", group: "Rum Punch" }
  - { id: fresh-pineapple-cubes, item: "Fresh pineapple cubes", amount: "½ cup", note: "Muddled", group: "Pineapple Rose Syrup" }

steps:
  - id: s1
    in: [fresh-pineapple, meyer-lemon, fresh-lime-juice, meyer-lemon-2, honey-or-sugar, water]
    do: "muddle 1/2 cup fresh"
    for: "30 s"
    detail: "In a small saucepan, muddle 1/2 cup fresh pineapple with the juice and segments from half a Meyer lemon. Add 1/4 cup honey (or sugar) and 1 cup water, bringing the mixture to a gentle simmer over medium heat."
    phase: "To prepare the Pineapple Rose Syrup"
  - id: s2
    in: [s1, rose-tea]
    do: "add 2-3 rose tea bags"
    for: "5 min"
    detail: "Once simmering, add 2-3 rose tea bags directly to the saucepan and let steep for about 5 minutes. Remove the tea bags, continue simmering for another 5 minutes, then strain the syrup to remove the pineapple solids. Set aside to cool."
    phase: "To prepare the Pineapple Rose Syrup"
  - id: s3
    in: [s2, passion-fruits, grenadine, light-rum, dark-rum, spiced-rum, apricot-brandy, aperol]
    do: "muddle the remaining"
    for: "30 s"
    detail: "In a shaker, muddle the remaining fresh pineapple with the pulp of 2 passion fruits, the juice of the other half of the Meyer lemon, and the juice from half a lime. Add a capful of grenadine, light rum, dark rum, spiced rum, apricot brandy, and a capful of Aperol (if using)."
    phase: "To make the Rum Punch"
  - id: s4
    in: [s3, bitters, balsamic-vinegar, frozen-cantaloupe, ice]
    do: "add a dash of bitters"
    detail: "Add a dash of bitters and a dash of balsamic vinegar for added depth. Instead of ice, add 1/2 cup of frozen cantaloupe to the shaker and shake vigorously to chill the drink."
    phase: "To make the Rum Punch"
  - id: s5
    in: [s4, fresh-pineapple-cubes]
    do: "mix the chilled punch"
    detail: "Mix the chilled punch with the prepared Pineapple Rose Syrup. Strain the mixture into two chilled glasses half-filled with ice (or ideally, a large ice cube for slower dilution)."
    phase: "To assemble"
  - id: s6
    in: [s5]
    do: "garnish with a pineapple"
    detail: "Garnish with a pineapple slice or citrus wedge if desired."
    phase: "To assemble"
```

---
title: "Summer chili, two ways"
section: recipes
description: "Dual-batch recipe for grilled meat and vegetarian chilis with toppings. Both share core seasonings, beans, and grilled corn garnish."
tags: ["recipe", "chili", "grilled", "meat", "vegetarian", "gans", "spicy", "mexican"]
created: 2024-06-09T10:34:00Z
updated: 2026-07-29T21:20:27Z
cid: bafkreigm7ii6j6e3ktqllzyaql5beimwp64asadvluec2bg7zimmb76aqm
html: https://iammatthias.com/recipes/1717954493179-summer-chili-two-ways
---

# Summer chili, two ways

```recipe
yield: "2 servings of each"
time: "45 min active · 2 h total"

ingredients:
  - { id: buttered-corn-on-the-cob, item: "buttered corn on the cob", amount: "2 ears", note: "grilled, kernels cut off", group: "Shared Ingredients" }
  - { id: onion, item: "onion", amount: "1 large", note: "chopped", group: "Shared Ingredients" }
  - { id: garlic, item: "garlic", amount: "3 cloves", note: "minced", group: "Shared Ingredients" }
  - { id: canned-black-beans, item: "canned black beans", amount: "1 can (400 g)", note: "drained and rinsed", group: "Shared Ingredients" }
  - { id: boneless-short-ribs, item: "boneless short ribs", amount: "1 lbs", note: "cut into chunks" }
  - { id: bell-peppers, item: "bell peppers", amount: "2½ medium", note: "chopped (varied colors)", group: "Shared Ingredients" }
  - { id: chili-powder, item: "chili powder", amount: "1½ tbsp", note: "divided", group: "Shared Ingredients" }
  - { id: ground-cumin, item: "ground cumin", amount: "1 tbsp", note: "divided", group: "Shared Ingredients" }
  - { id: paprika, item: "paprika", amount: "2 tsp", note: "divided", group: "Shared Ingredients" }
  - { id: salt, item: "salt", amount: "to taste", group: "Shared Ingredients" }
  - { id: black-pepper, item: "black pepper", amount: "to taste", group: "Shared Ingredients" }
  - { id: olive-oil, item: "olive oil", amount: "2 tbsp", note: "divided", group: "Shared Ingredients" }
  - { id: ground-chuck, item: "ground chuck", amount: "1 lbs" }
  - { id: canned-diced-tomatoes, item: "canned diced tomatoes", amount: "1 can (400 g)", note: "with juice" }
  - { id: fresh-cilantro, item: "fresh cilantro", amount: "½ cup", note: "chopped, for garnish", group: "Shared Ingredients" }
  - { id: lime, item: "lime", amount: "2", note: "cut into wedges, for serving", group: "Shared Ingredients" }
  - { id: sour-cream, item: "sour cream", amount: "for topping", note: "optional", group: "Shared Ingredients" }
  - { id: zucchini, item: "zucchini", amount: "1 medium", note: "seared and diced, seeds scooped out" }
  - { id: yellow-squash, item: "yellow squash", amount: "1 medium", note: "seared and diced, seeds scooped out" }
  - { id: canned-kidney-beans, item: "canned kidney beans", amount: "1 can (400 g)", note: "drained and rinsed" }
  - { id: canned-diced-tomatoes-2, item: "canned diced tomatoes", amount: "1 can (400 g)", note: "with juice" }

steps:
  - id: s1
    prep: true
    do: "carefully peel"
    for: "15 min"
    detail: "Carefully peel back the husks of the corn without removing them. Remove the silk strands and then fold the husks back into place. Soak the ears of corn in cold water for about 15 minutes to prevent the husks from burning. After soaking, remove the corn from the water and shake off any excess. Spread a thin layer of butter over the kernels and then fold the husks back over the buttered corn."
    phase: "Preparing the corn on the cob"
  - id: s2
    in: [buttered-corn-on-the-cob]
    do: "place the corn"
    for: "15–20 min"
    detail: "Place the corn on the grill over medium heat, turning occasionally, for about 15-20 minutes or until the kernels are tender. Once done, remove the corn from the grill and let it cool slightly. Then, cut the kernels off the cob and set aside."
    phase: "Preparing the corn on the cob"
  - id: v1_1
    in: [s2, boneless-short-ribs, onion, garlic, canned-black-beans]
    do: "light the charcoal"
    for: "2–3 min"
    detail: "Set up your charcoal or gas grill for direct grilling. Light the charcoal or preheat the gas grill to medium-high heat. Place the boneless short ribs directly on the grill grates over the hot coals or burners. Sear the short ribs on all sides until they develop a good crust, about 2-3 minutes per side. Remove the short ribs from the grill and set aside."
    phase: "Meat Chili"
  - id: v1_2
    in: [v1_1, bell-peppers, chili-powder, ground-cumin, paprika, salt, black-pepper]
    do: "season the vegetable"
    detail: "Season the vegetable mixture with salt, black pepper, 1½ tsp of chili powder, 1½ tsp of ground cumin, and 1 tsp of paprika. If making both chilis, divide the vegetable mixture, setting half aside for the vegetarian chili. If making one batch, keep it all together."
    phase: "Meat Chili"
  - id: v1_3
    in: [v1_2, olive-oil, ground-chuck]
    do: "add 1 tbsp of olive oil"
    detail: "In the cast-iron pot, add 1 tbsp of olive oil and the ground chuck. Cook until browned, breaking it up with a spoon as it cooks. Season the ground chuck with salt, black pepper, 1½ tsp of chili powder, ¾ tsp of ground cumin, and ½ tsp of paprika. Return the seared short ribs to the pot with the ground chuck. Add half (or all, if making one batch) of the seasoned vegetable mixture to the pot."
    phase: "Meat Chili"
  - id: v1_4
    in: [v1_3, canned-diced-tomatoes]
    do: "stir in the canned diced"
    for: "45 min"
    detail: "Stir in the canned diced tomatoes with their juice. Add the remaining 1½ tsp of chili powder, ¾ tsp of ground cumin, and ½ tsp of paprika. Stir well to combine. Move the pot to a cooler part of the grill to cook over indirect heat. Cover the grill and let the chili simmer for about 45 minutes, stirring occasionally, until the meat is tender and the flavors are well combined."
    phase: "Meat Chili"
  - id: v1_5
    in: [v1_4, fresh-cilantro, lime, sour-cream]
    do: "taste and adjust"
    detail: "Taste and adjust the seasoning if necessary. Serve the meat chili hot, garnished with fresh cilantro, grilled buttered corn kernels, sour cream (optional), and lime wedges on the side."
    phase: "Meat Chili"
    title: "Meat Chili"
  - id: v2_1
    in: [s2, zucchini, yellow-squash]
    do: "place the zucchini"
    for: "2–3 min"
    detail: "Place the zucchini and yellow squash directly on the grill grates over the hot coals or burners. Sear on all sides until they develop a good crust, about 2-3 minutes per side. Remove from the grill, allow to cool slightly, then dice and scoop out the seeds. Place a large cast-iron pot or Dutch oven on the grill over medium heat. Allow it to heat up for a few minutes."
    phase: "Vegetarian Chili"
  - id: v2_2
    in: [v2_1, olive-oil, onion, garlic, bell-peppers, canned-black-beans, black-pepper, canned-kidney-beans]
    do: "add 1 tbsp of olive oil"
    for: "5 min"
    detail: "In a separate grill-safe pan, add 1 tbsp of olive oil and heat over medium heat. When the oil is hot, add the chopped onions and cook until translucent, about 5 minutes. Add the minced garlic and cook for another minute. Stir in the chopped bell peppers and cook until they begin to soften, about 5 minutes. Stir in the canned black beans and cook until heated through."
    phase: "Vegetarian Chili"
  - id: v2_3
    in: [v2_2, chili-powder, ground-cumin, paprika, salt]
    do: "season the vegetable"
    detail: "Season the vegetable mixture with salt, black pepper, 1½ tsp of chili powder, 1½ tsp of ground cumin, and 1 tsp of paprika. If making both chilis, divide the vegetable mixture, setting half aside for the vegetarian chili. If making one batch, keep it all together."
    phase: "Vegetarian Chili"
  - id: v2_4
    in: [v2_3]
    do: "in a separate large"
    for: "5 min"
    detail: "In a separate large cast-iron pot or Dutch oven on the grill over medium heat, add 1 tbsp of olive oil. Allow it to heat up for a few minutes. Add the diced zucchini and yellow squash, cooking until they start to soften, about 5 minutes. Add the remaining half of the seasoned vegetable mixture (or all, if making one batch) to the pot."
    phase: "Vegetarian Chili"
  - id: v2_5
    in: [v2_4, canned-diced-tomatoes-2]
    do: "stir in the canned diced"
    for: "25–30 min"
    detail: "Stir in the canned diced tomatoes with their juice, black beans, and kidney beans. Season with the remaining 1½ tsp of chili powder, ¾ tsp of ground cumin, and ½ tsp of paprika. Adjust with salt and black pepper to taste. Move the pot to a cooler part of the grill to cook over indirect heat. Cover the grill and let the chili simmer for about 25-30 minutes, stirring occasionally, until the vegetables are tender and the flavors are well combined."
    phase: "Vegetarian Chili"
  - id: v2_6
    in: [v2_5, fresh-cilantro, lime, sour-cream]
    do: "taste and adjust"
    detail: "Taste and adjust the seasoning if necessary. Serve the vegetarian chili hot, garnished with fresh cilantro, grilled buttered corn kernels, sour cream (optional), and lime wedges on the side."
    phase: "Vegetarian Chili"
    title: "Vegetarian Chili"
```

---
title: "Creamy Honey Cilantro Dressing"
section: recipes
description: "A cilantro-lime dressing with Greek yogurt, mayonnaise, honey, Dijon, garlic, and olive oil."
tags: ["recipe", "salad-dressing", "cilantro", "honey", "lime", "garlic", "olive-oil"]
created: 2024-05-30T13:33:00Z
updated: 2026-07-29T21:06:20Z
cid: bafkreiddfx772pbv37vlru3s257vurfq7jp66qre6h6wn4fh2il4u6xq2u
html: https://iammatthias.com/recipes/1717101197446-creamy-honey-cilantro-dressing
---

# Creamy Honey Cilantro Dressing

```recipe
yield: "about 250 ml (8 servings)"
time: "10 min"

ingredients:
  - { id: greek-yogurt, item: "Greek yogurt", amount: "2 tbsp", note: "Plain" }
  - { id: mayonnaise, item: "Mayonnaise", amount: "1 tbsp" }
  - { id: dijon-mustard, item: "Dijon mustard", amount: "½ tsp" }
  - { id: honey, item: "Honey", amount: "1 tsp" }
  - { id: fresh-cilantro, item: "Fresh cilantro", amount: "2 tbsp", note: "Chopped" }
  - { id: lime-juice, item: "Lime juice", amount: "1 tsp", note: "Freshly squeezed" }
  - { id: garlic, item: "Garlic", amount: "1 small clove", note: "Minced" }
  - { id: salt, item: "Salt", amount: "1 pinch", note: "Adjust to taste" }
  - { id: black-pepper, item: "Black pepper", amount: "1 pinch", note: "Freshly ground" }
  - { id: olive-oil, item: "Olive oil", amount: "1 tsp", note: "Extra virgin" }
  - { id: water, item: "Water", amount: "1 tsp", note: "Adjust for desired consistency" }

steps:
  - id: s1
    in: [greek-yogurt, mayonnaise, dijon-mustard, honey]
    do: "combine the Greek"
    for: "1 min"
    detail: "In a small bowl, combine the Greek yogurt, mayonnaise, Dijon mustard, and honey. Mix until smooth."
  - id: s2
    in: [s1, fresh-cilantro, lime-juice, garlic]
    do: "add the chopped"
    for: "1–2 min"
    detail: "Add the chopped cilantro, lime juice, and minced garlic to the mixture. Stir until well combined."
  - id: s3
    in: [s2, salt, black-pepper]
    do: "season with a pinch"
    detail: "Season with a pinch of salt and black pepper, adjusting to taste."
  - id: s4
    in: [s3, olive-oil]
    do: "slowly drizzle"
    for: "1 min"
    detail: "Slowly drizzle in the olive oil while whisking to emulsify the dressing."
  - id: s5
    in: [s4, water]
    do: "add water to achieve"
    detail: "Add water to achieve the desired consistency."
  - id: s6
    in: [s5]
    do: "use immediately"
    detail: "Use immediately or refrigerate for a short period to allow the flavors to meld."
```

---
title: "Vegetarian Coconut Curry Ramen"
section: recipes
description: "A simple coconut curry ramen bowl featuring fresh noodles in a fragrant broth, topped with veggies and soft-boiled eggs."
tags: ["curry", "ramen", "coconut-milk", "noodles", "soup", "thai", "vegetarian"]
created: 2024-04-28T13:21:00Z
updated: 2026-07-29T21:06:20Z
cid: bafkreidy44nrvqup5ryjxrpv326nxnooir373r7nbhs2bpf3xn5q4is44i
html: https://iammatthias.com/recipes/1714335695318-vegetarian-coconut-curry-ramen
---

# Vegetarian Coconut Curry Ramen

```recipe
yield: "2 servings"
time: "20 min active · 35 min total"

ingredients:
  - { id: green-curry-paste, item: "Green curry paste", amount: "2 tablespoons", note: "Adjust to taste for spiciness", group: "Coconut Curry Broth" }
  - { id: ginger, item: "Ginger", amount: "20g", note: "Minced", group: "Coconut Curry Broth" }
  - { id: garlic, item: "Garlic", amount: "2 cloves", note: "Minced", group: "Coconut Curry Broth" }
  - { id: coconut-milk, item: "Coconut milk", amount: "400 ml", note: "Full-fat", group: "Coconut Curry Broth" }
  - { id: vegetable-stock, item: "Vegetable stock", amount: "1 liter", group: "Coconut Curry Broth" }
  - { id: soy-sauce, item: "Soy sauce", amount: "1 tablespoon", group: "Coconut Curry Broth" }
  - { id: brown-sugar, item: "Brown sugar", amount: "1 teaspoon", group: "Coconut Curry Broth" }
  - { id: lime, item: "Lime", amount: "1", note: "Juice extracted", group: "Coconut Curry Broth" }
  - { id: ramen-noodles, item: "Ramen noodles", amount: "200g", note: "Fresh", group: "Ramen" }
  - { id: baby-spinach, item: "Baby spinach", amount: "150g", note: "Fresh", group: "Ramen" }
  - { id: corn, item: "Corn", amount: "100g", note: "Fresh or frozen", group: "Ramen" }
  - { id: soft-boiled-eggs, item: "Soft boiled eggs", amount: "2", note: "Halved", group: "Ramen" }
  - { id: scallions, item: "Scallions", amount: "2 stalks", note: "Thinly sliced", group: "Ramen" }

steps:
  - id: s1
    prep: true
    do: "heat a small amount"
    detail: "In a medium pot, heat a small amount of oil over medium heat."
    phase: "Coconut Curry Broth"
  - id: s2
    in: [green-curry-paste, ginger, garlic]
    do: "add the green curry"
    for: "1–2 min"
    detail: "Add the green curry paste, ginger, and garlic, and sauté for 1-2 minutes until fragrant."
    phase: "Coconut Curry Broth"
  - id: s3
    in: [s2, coconut-milk, vegetable-stock, soy-sauce, brown-sugar]
    do: "pour in the coconut"
    detail: "Pour in the coconut milk, vegetable stock, soy sauce, and brown sugar. Bring to a gentle simmer."
    phase: "Coconut Curry Broth"
  - id: s4
    in: [s3, lime]
    do: "add lime juice"
    detail: "Add lime juice, and adjust the seasoning with additional soy sauce or sugar as needed."
    phase: "Coconut Curry Broth"
  - id: s5
    in: [s4]
    do: "simmer"
    for: "10 min"
    detail: "Simmer for about 10 minutes to let the flavors meld together."
    phase: "Coconut Curry Broth"
  - id: s6
    in: [s5, ramen-noodles]
    do: "cook ramen noodles"
    for: "3–4 min"
    detail: "Cook ramen noodles according to package instructions."
    phase: "Ramen"
  - id: s7
    in: [s6]
    do: "place the noodles"
    detail: "Place the noodles into two bowls."
    phase: "Ramen"
  - id: s8
    in: [s7]
    do: "pour the hot coconut"
    detail: "Pour the hot coconut curry broth over the noodles."
    phase: "Ramen"
  - id: s9
    in: [s8, baby-spinach, corn, soft-boiled-eggs, scallions]
    do: "top each bowl with baby"
    detail: "Top each bowl with baby spinach, corn, scallions, and a soft boiled egg."
    phase: "Ramen"
```

---
title: "Thai Curry Sweet Potato Soup"
section: recipes
description: "Sweet potato soup with coconut milk, red curry paste, ginger, lemongrass, lime, and almond butter."
tags: ["soup", "thai", "sweet-potato", "red-curry", "coconut-milk", "vegan", "recipe", "lime"]
created: 2024-04-16T13:52:00Z
updated: 2026-07-29T21:06:19Z
cid: bafkreihz46o7zx6aisk7huuyno5errocmdj6rorwsrwtwjf5uljpsjxozu
html: https://iammatthias.com/recipes/1713300772377-thai-curry-sweet-potato-soup
---

# Thai Curry Sweet Potato Soup

```recipe
yield: "2 servings"
time: "15 min active · 40 min total"

ingredients:
  - { id: shallots, item: "Shallots", amount: "50g", note: "Chopped" }
  - { id: ginger, item: "Ginger", amount: "10g", note: "Fresh, grated" }
  - { id: lemongrass, item: "Lemongrass", amount: "1 stalk", note: "bruised; fished out before blending" }
  - { id: sweet-potatoes, item: "Sweet potatoes", amount: "200g", note: "Peeled, cubed" }
  - { id: red-bell-pepper, item: "Red bell pepper", amount: "100g", note: "Chopped" }
  - { id: thai-red-curry-paste, item: "Thai red curry paste", amount: "1 tbsp" }
  - { id: coconut-milk, item: "Coconut milk", amount: "100ml" }
  - { id: lime, item: "Lime", amount: "½", note: "Juiced" }
  - { id: almond-butter, item: "Almond butter", amount: "1 tbsp" }

steps:
  - id: s1
    in: [shallots, ginger, lemongrass]
    do: "sauté shallots"
    for: "3–4 min"
    detail: "In a large saucepan, sauté shallots, ginger, and lemongrass until shallots are translucent."
  - id: s2
    in: [s1, sweet-potatoes, red-bell-pepper, thai-red-curry-paste]
    do: "add the sweet potatoes"
    detail: "Add the sweet potatoes, red bell peppers, and Thai red curry paste. Stir to coat the vegetables in the paste."
  - id: s3
    in: [s2, coconut-milk]
    do: "pour in the coconut milk"
    for: "20 min"
    detail: "Pour in the coconut milk and bring to a simmer. Cover and cook until the sweet potatoes are tender, about 20 minutes."
  - id: s4
    in: [s3, lime, almond-butter]
    do: "remove from heat and let"
    for: "5 min"
    detail: "Remove from heat and let cool slightly. Add the almond butter and lime juice."
  - id: s5
    in: [s4]
    do: "puree the mixture using"
    for: "1–2 min"
    detail: "Puree the mixture using an immersion blender until smooth. Adjust seasoning as needed."
  - id: s6
    in: [s5]
    do: "serve warm"
    detail: "Serve warm as a comforting soup with a side of crusty bread."
```

---
title: "Generate a new private key"
section: posts
description: "Create wallet addresses quickly with OpenSSL: just run `openssl rand -hex 32` to generate a secure private key. Remember to keep it safe!"
tags: ["blockchain", "crypto", "wallet", "private-key", "openssl", "security", "web3"]
created: 2024-04-06T12:34:00Z
updated: 2025-05-24T16:58:00Z
cid: bafkreibwlmltz6uvguxvyjr65juqwpykmbyf7w7k44iklaydjbop357amm
html: https://iammatthias.com/posts/1712432046964-generate-a-new-private-key
---

# Generate a new private key

Sometimes you just want to quickly generate a new wallet address without jumping through a bunch of hoops.

Provided you have OpenSSL installed on your system, you can create a new private key in one simple command: `openssl rand -hex 32`

This will generate 32 random bytes in hexadecimal format, which can be used as a private key. The output will be a 64-character hexadecimal string, like this:

```
e1d4f2c8a1b9c3d4e5f6a7b8c9d1e2f3a4b5c6d7e8f9a1b2c3d4e5f6a7b8c9d0
```

You can then use this private key to derive a public key and generate a wallet address. Just remember to keep your private key secure, anyone who has access to it can control your wallet and the associated funds.

> [!IMPORTANT]
> The private key used in this example is valid, but should not be used by anyone, as it has been shared publicly in this post.

---
title: "Onchain Hit Counter"
section: posts
description: "A GeoCities-style hit counter that stores its count on an Ethereum testnet because a database felt excessive."
tags: ["web-development", "blockchain", "astro", "testnet", "smart-contract", "viem", "hit-counter"]
created: 2024-04-05T08:01:00Z
updated: 2026-07-16T14:56:29Z
cid: bafkreibjhqf6y56kil3h24hblbhtdfqx2utosxnla2hwdhlovro5fq7dbe
html: https://iammatthias.com/posts/1712329304675-onchain-hit-counter
---

# Onchain Hit Counter

> This post is provided as an example, but the hit counter has been removed from the current version of this site for the time being.

[Astro DB](https://astro.build/db/) shipped recently, a small SQL server with a friendly ORM, and people built fun things on it fast. [Hit counters](https://rosnovsky.us/blog/exploring-astro-db) were the obvious one.

Hit counters are a GeoCities and AngelFire memory for me. I wanted one too, but spinning up SQL for a single integer felt like a lot. All I needed was a place to store a number that goes up.

So I put it on a testnet. [Holesky](https://github.com/eth-clients/holesky?tab=readme-ov-file) ("the first long-standing, merged-from-genesis, public Ethereum testnet") got the first build, mostly because I already had a bag of test ETH. It's the successor to [Goerli](https://github.com/eth-clients/goerli) and runs through 2028. [Viem](https://viem.sh/) made the on-chain side painless.

> No longer on Holesky. Switched to Base Sepolia: shorter projected lifespan, much cheaper transactions, enough room to capture page views too.

The contract:

```solidity
pragma solidity ^0.8.25;

contract HitCounter {
    mapping(bytes32 => bool) private sessionExists;
    bytes32[] private sessionHashes;
    uint256 public sessionCount;
    event SessionAdded(bytes32 indexed sessionHash);
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Caller is not the owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function addSession(bytes32 sessionHash) external onlyOwner {
        require(!sessionExists[sessionHash], "Session already exists");
        sessionExists[sessionHash] = true;
        sessionHashes.push(sessionHash);
        sessionCount++;
        emit SessionAdded(sessionHash);
    }

    function getAllSessionHashes() external view returns (bytes32[] memory) {
        return sessionHashes;
    }
}
```

Each session hash gets pushed onto the array once, and the count goes up by one. Both `sessionCount` and `sessionHashes` are public.

Viem sets up the public and wallet clients:

```typescript
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { holesky } from "viem/chains";

export const publicClient = createPublicClient({
  chain: holesky,
  transport: http("https://ethereum-holesky-rpc.publicnode.com"),
});

export const walletClient = createWalletClient({
  chain: holesky,
  transport: http("https://ethereum-holesky-rpc.publicnode.com"),
});

export const account = privateKeyToAccount(import.meta.env.HIT_COUNTER_WALLET);
```

The backend simulates the call, writes the transaction, and returns OK:

```typescript
import { account, publicClient, walletClient } from "@lib/viemClients";
import { addSessionABI } from "@lib/abi";

export async function POST({ request }) {
  const requestBody = await request.json();
  const sessionHash = requestBody.sessionHash;
  const contractAddress = import.meta.env.PUBLIC_HIT_COUNTER_CONTRACT;

  const { request: contractRequest } = await publicClient.simulateContract({
    address: `0x${contractAddress}`,
    abi: addSessionABI!,
    functionName: "addSession",
    args: [sessionHash],
    account,
  });

  let nonce = await publicClient.getTransactionCount({ address: account.address });
  contractRequest.nonce = nonce;

  await walletClient.writeContract(contractRequest);
  return new Response(JSON.stringify({ status: "OK" }), { headers: { "Content-Type": "application/json" } });
}
```

The frontend reads the count straight from the contract:

```html
<span id="sessionCount"></span>

<script>
  import { publicClient } from "@lib/viemClients";
  import { sessionCountABI } from "@lib/abi";

  document.addEventListener("DOMContentLoaded", async () => {
    const contractAddress = import.meta.env.PUBLIC_HIT_COUNTER_CONTRACT;

    const updateSessionCount = async () => {
      const data = await publicClient.readContract({
        address: `0x${contractAddress}`,
        abi: sessionCountABI,
        functionName: "sessionCount",
      });

      const sessionCountElement = document.getElementById("sessionCount");
      sessionCountElement.innerText = `${Number(data)}`;
    };

    updateSessionCount();
  });
</script>
```

A wallet provider and private key handle signing. The chain stores the count and the count updates every block.

The `sessionHash` is built from IP, user agent, and `Date.now()`, then run through `keccak256`:

```typescript
// Extract IP and user agent from the request
const ip = Astro.clientAddress;
const userAgent = Astro.request.headers.get("user-agent");

// Get the current time in milliseconds
const currentTime = Date.now();

// Hash the IP, user agent, and current time together
const sessionHash = keccak256(`0x${ip + userAgent + currentTime}`);
```

The hash is unique per session, so the `addSession` call dedupes naturally on chain.

The browser caches the `sessionHash` in local storage with an expiration. If a fresh hash exists, the frontend skips the API call. If not, it generates a new one and increments the counter through the backend.

Hit counters, blockchains, weird web. Good combination.

---
title: "Siesta"
section: recipes
description: "A tequila cocktail with Campari, grapefruit juice, lime juice, and simple syrup. Shake with ice and serve chilled with a grapefruit peel."
tags: ["cocktails", "mixology", "tequila", "campari", "grapefruit", "lime", "simple-syrup"]
created: 2024-03-27T21:13:00Z
updated: 2026-07-29T21:06:19Z
cid: bafkreig6ehnl7j22kk6dgoowdxx2umj6vezpc4nqe7exb5wtple3f7yimq
html: https://iammatthias.com/recipes/1711599203773-siesta
---

# Siesta

```recipe
yield: "2 drinks"
time: "5 min"
source: "Katie Stipe, Flatiron Lounge (2006)"
sourceURL: "https://punchdrink.com/articles/how-siesta-became-modern-classic-tequila-campari-cocktail-recipe/"

ingredients:
  - { id: tequila, item: "Blanco tequila", amount: "90 ml (3 oz)" }
  - { id: lime-juice, item: "Lime juice", amount: "45 ml (1½ oz)", note: "fresh" }
  - { id: simple-syrup, item: "Simple syrup", amount: "45 ml (1½ oz)" }
  - { id: grapefruit-juice, item: "Grapefruit juice", amount: "30 ml (1 oz)", note: "fresh" }
  - { id: campari, item: "Campari", amount: "15 ml (½ oz)" }
  - { id: salt, item: "Salt", amount: "a pinch", note: "optional; lifts the grapefruit" }

steps:
  - { id: build, in: [tequila, lime-juice, simple-syrup, grapefruit-juice, campari, salt], do: "combine over ice", detail: "Add everything to a shaker filled with ice." }
  - { id: shake, in: [build], do: "shake", for: "12–15 s", detail: "Shake until the tin frosts over." }
  - { id: strain, in: [shake], do: "double-strain", detail: "Strain into two chilled coupes." }
  - { id: garnish, in: [strain], do: "garnish", detail: "A grapefruit twist over each." }
```

---
title: "Broken Glass"
section: art
description: "A broken lens and college memories"
tags: ["photographs", "nature", "travel", "life"]
created: 2024-03-18T22:00:00Z
updated: 2025-10-04T13:30:00Z
cid: bafkreifslu5mruvghyjljojooxrl3qwfxcttle6q4nsbqplmpat6rmxkfy
html: https://iammatthias.com/art/1710824453471-broken-glass
---

# Broken Glass

A day by the lake with college friends in 2011. The 24-70mm lens I had borrowed from school had a jammed focus mechanism, which defiantly stuck while on a granite islet in the middle of the lake.

![](https://blobs.farfield.systems/blobs/bafkreifjuweeuyhir7hgeasmvtlvsxyzmkgtcivxh4lss5p5axhxpbmjsu)

![](https://blobs.farfield.systems/blobs/bafkreiesrcurv3yhwb4oi2req6u477jxnmv4laths3w6wdcynpw3a26a6q)

![](https://blobs.farfield.systems/blobs/bafkreieha7q3qmjjtk5x6d5irkd5uxjyaptq7cg277yh2b52gyu6jaaai4)

![](https://blobs.farfield.systems/blobs/bafkreic3wvhzotdgqncnu7d6dqlpkyayqtzonlim56oymblxexb4p4vc2u)

![](https://blobs.farfield.systems/blobs/bafkreibvssew7mwhkln5rhnnjhs5cndperxbkpvuryaqwgybdjy4l4nup4)

![](https://blobs.farfield.systems/blobs/bafkreihvxjaocjhcfyclifpct27lb6qkgj3jpknkwcs7m52u5m4lgmb5ve)

---
title: "As Above"
section: art
description: "High-altitude aerial photos with quirks of light and color."
tags: ["photography", "travel", "aerial", "altitude", "oceans", "pictures"]
created: 2024-03-18T21:58:00Z
updated: 2025-10-04T13:33:00Z
cid: bafkreiaiedhrxzwzyy56qe3hpnj3vph2ltdtk7f276wx6hiyvszw7cgnli
html: https://iammatthias.com/art/1710824280787-as-above
---

# As Above

Shot at altitude, the quirks of light are due to shooting through the thick acrylic windows of the plane.

![](https://blobs.farfield.systems/blobs/bafkreiayy3vrchwqgifrgby6eu7na5k5twan2zhseo6ro34u64tx5nu6xq)

![](https://blobs.farfield.systems/blobs/bafkreiehor56gzm5yszgjlahwtcvin6vipgqnbnvozz4klwmm5g5htdhuq)

![](https://blobs.farfield.systems/blobs/bafkreidmhhtrv3v5gz3wx5qgalfupvc4lcsj7zaqwec7tzpkex27i5u6om)

![](https://blobs.farfield.systems/blobs/bafkreiewxgtkkupsgbfbkbg2x2qkqxuspswkn7utxisajpmxhzoxekd2iq)

![](https://blobs.farfield.systems/blobs/bafkreiftgxtmr4luvx3hedan4smsehuhmiieielfrxws56jszbt5muhs7u)

![](https://blobs.farfield.systems/blobs/bafkreid6v43koruddjon7vjmxdnejwwumgnxvfchwpjxdoi4zbhqrfaiqa)

![](https://blobs.farfield.systems/blobs/bafkreife2v66zxkfvwmdo7mhc6lgorwnkfurupfp4ntctdjce4us7a2hjy)

![](https://blobs.farfield.systems/blobs/bafkreig3pd4xp2e6yljoqp6dsyrgescwvvgtzpmh35x723r63m3wzpcwse)

![](https://blobs.farfield.systems/blobs/bafkreidrdnevuaacsapkxkr54i7dghsmo5bqzs2fig6za3a2nzohvrahdu)

![](https://blobs.farfield.systems/blobs/bafkreibrmbeut5ohrezsi65usczpkcaqh33etzh7pgdu5frzk66sbqrle4)

![](https://blobs.farfield.systems/blobs/bafkreic732vkqedh5grjip4bpnjgjoemuna3xtfxg325zkizaq4glkg5ei)

![](https://blobs.farfield.systems/blobs/bafkreie7veuponqsvnys7oeavaoouyioddmlb2aoo4fqb7hate5crpzqq4)

![](https://blobs.farfield.systems/blobs/bafkreid3kkl7dozv65wynjlybtl4w6j7sixjnvafrhc2ta5uxj5qw466fi)

![](https://blobs.farfield.systems/blobs/bafkreib7oessabb3deqefqvxkuy5ogx2cthtf6n36doc2bficqr6mbbt24)

![](https://blobs.farfield.systems/blobs/bafkreib6lcwq6icftelpchnuyhc4kkhvizgfhekezscy6awkke5yxk7rfa)

![](https://blobs.farfield.systems/blobs/bafkreielkmo4s7ohlmgrwif247i3kmilkcoavq532dcgoztg2eck2dnanq)

![](https://blobs.farfield.systems/blobs/bafkreieacbkw2yjykm5zbgw5cyrlsvprvw7pbbzo2gnmhlt4qxu64kc6vi)

![](https://blobs.farfield.systems/blobs/bafkreigdhezttr2ucrjkxpfuvy6pclhfwpff2dmk27mlzy3tz5apewjdce)

![](https://blobs.farfield.systems/blobs/bafkreiavlp4tbsgakecq2dcjovnwks2pyszobayd55ysadlljxi3iltmqu)

![](https://blobs.farfield.systems/blobs/bafkreidbgbtzvfsjax676d42f2ndhelqsbtluxnmu27jffjbp37ywko5eq)

![](https://blobs.farfield.systems/blobs/bafkreieljzwtjxdxpe22bf2qmhcmsfwsfcoct5qzz3fqic6sdcwj3uhek4)

![](https://blobs.farfield.systems/blobs/bafkreifeipyisfmiexhf465ozpdwrwth5eza67rz6apxmqujfgm5jr52vq)

![](https://blobs.farfield.systems/blobs/bafkreigo4j2yvlbxlnhooohcpfsbmlveimqcorlwxidt2yq4qoa77qei4m)

![](https://blobs.farfield.systems/blobs/bafkreicah7vsh63vcaian563v2zduhbtz7rkhha2s4vzkqouxraj5raqta)

![](https://blobs.farfield.systems/blobs/bafkreifx2w7ddeokk32krnqzqhu5l2v5v3pzkxlw4rf2aqnl5y2ocgtvhi)

---
title: "No Stone Unturned"
section: art
description: "A rock traveled in my camera bag, and was photographed time and time again."
tags: ["photography", "travel", "creative-process", "iphone", "eos-r", "pictures", "nature"]
created: 2024-03-18T21:53:00Z
updated: 2025-10-04T13:32:00Z
cid: bafkreife2wpfcs2l75bxk2xpynimrojho7d6nciumak7bmbe36ityxqat4
html: https://iammatthias.com/art/1710823985569-no-stone-unturned
---

# No Stone Unturned

I once found a rock, half smooth, half jagged, with lines running across it like a map. For a time it had a home in the pocket of my camera bag. Along the way I photographed it, always in the same orientation, against ever-changing backdrops. A stone unturned.

![](https://blobs.farfield.systems/blobs/bafkreibscp7rmzpu55635vvpdibrnbogztivdshrrnfgxkxfovsrxdn76u)

![](https://blobs.farfield.systems/blobs/bafkreih6gbpuvajjzdod5t5tr5ijyduar27inxk6qrhulqka4wogsuigea)

![](https://blobs.farfield.systems/blobs/bafkreiaywk5efpeu2bvqpu26nwnnfadojbfmlzlua6urkgaxy55jzzlqey)

![](https://blobs.farfield.systems/blobs/bafkreiddfmi5fsl7p7jcxzlcoxoctlhjdrlgu5iigbaszfdgfr5cludmga)

![](https://blobs.farfield.systems/blobs/bafkreibhlxshkpwglesfekr4jhvfmzrsvu6hxqg5l3uhygdoa47wzmhd4m)

![](https://blobs.farfield.systems/blobs/bafkreigotd2eksvf43zakraufa2njfbulpzi2a7ubui4wwpsm6wqalhjda)

![](https://blobs.farfield.systems/blobs/bafkreihxpnbds3nm3ycwfre7r44t5rg4avxhe2xgs2n5vxtifhwqcytyke)

![](https://blobs.farfield.systems/blobs/bafkreify2cm3aveqds2vttf34znwnjdeykzc44s7kvqb5urlej5ayqzlom)

![](https://blobs.farfield.systems/blobs/bafkreibxiicnvz7w2imymuoe6svjo755feacucmxbf4ig5y4qb2dpji354)

![](https://blobs.farfield.systems/blobs/bafkreib6yqm6jlbs6c7l3at62pygdq3iksdlhlkgqmeibxpzqhvkm2i2de)

![](https://blobs.farfield.systems/blobs/bafkreiackqytrdww5epdq342gcmnonb3wsbxlsl6tvx634qayn5ggoc72e)

---
title: "VSCO"
section: art
description: "A collection of iPhone photos from California, Hawaii, and India, edited with VSCO"
tags: ["photography", "vsco", "iphone", "travel", "pictures", "mobile-photography", "photo-editing"]
created: 2024-03-18T21:32:00Z
updated: 2025-10-04T13:31:00Z
cid: bafkreifqucib2uze2zliwgykndxrxyzsyd6brtklynenyzhfoccdtd2hdq
html: https://iammatthias.com/art/1710822761635-vsco
---

# VSCO

It has been a long time since I used [VSCO](https://www.vsco.co/) frequently, but for a spell it was my go-to mobile app for quickly editing a picture. Over the years I built a collection of tailored filters, and I used them consistently until I switched to [Darkroom](https://darkroom.co/).

This is a collection of shots taken with various generations of iPhone and edited with VSCO.

## California

![](https://blobs.farfield.systems/blobs/bafkreic535prkym4ze26bqmukxe5kn2pyqlyhetrhtwfirttk4pou273zq)

![](https://blobs.farfield.systems/blobs/bafkreibux7vhy7gh3anm3oc32nvx4bza4qojnn5yhv4fc7jb2q7myttfqu)

![](https://blobs.farfield.systems/blobs/bafkreiftdrqcnmzxjwxz5vlxb6a2zfau63esv5fs2v3z263tvldvjgycda)

![](https://blobs.farfield.systems/blobs/bafkreia767g4b2f7cozacv3rcbuk7a7s7g7ipdkowmwi7nifx7ailggebq)

![](https://blobs.farfield.systems/blobs/bafkreihu4vv6zui2qhbeyi7izor5rmw4vght33ab6eztsz5xszgpoqhlam)

![](https://blobs.farfield.systems/blobs/bafkreibrkhfjyzlkndgxajt2lgk7pykfewkeixti6vx5ibdeb73uq2f2ii)

![](https://blobs.farfield.systems/blobs/bafkreifw62cjxrh42dxwjl43nlimlgxzwxwtvhw3kcxlain5qqoaashj2y)

![](https://blobs.farfield.systems/blobs/bafkreigi4gjvyefoahuotx2mrv46wxqlnojoi7x52w73xhwwlhl54zns5m)

![](https://blobs.farfield.systems/blobs/bafkreicjynt6jdv7widshjr4pm4uxx73hy5clcbxjxhe6zhoplltkmr6oi)

## Maui, Hawaii

![](https://blobs.farfield.systems/blobs/bafkreicdalle335uoely5lm6sou6cmukh5qukafo3qovkx753gap3cywrq)

![](https://blobs.farfield.systems/blobs/bafkreiazf567j4rkynjrvr2uj3m7j2nl4misko3az4cei4sxszltwf35qa)

![](https://blobs.farfield.systems/blobs/bafkreid2u2iiiysgsv4c7k34uxbubvyvzrwmkeiofoly5tokwgfj2dxwim)

![](https://blobs.farfield.systems/blobs/bafkreicvztmaockur6bvdfetsbw5hluxy63ktso25uqv7uofc5skozouu4)

![](https://blobs.farfield.systems/blobs/bafkreidmkokpeklmf3ucxzedjwdcqmragbi3kcbfdqrzeienc3eg5vxr4i)

![](https://blobs.farfield.systems/blobs/bafkreidw7a4toa5agjxgtcrngpd6egmjty6g2bzo2fzxhknheuyhgt4rvm)

![](https://blobs.farfield.systems/blobs/bafkreicomr4wg6tffex2gcmoqatqd3hzfhgf5m6gezf6vk36j5uneeze5e)

![](https://blobs.farfield.systems/blobs/bafkreiccenc6kri7f2auruff3y7ekgzbi3ggcco4k2pt7txum2g77deo4y)

![](https://blobs.farfield.systems/blobs/bafkreieuatdd4aotgrr6yqbp7l3jawzeiawrrssntxbhcgpwxv3fmpt7au)

![](https://blobs.farfield.systems/blobs/bafkreidp7hfz3fhfy3rv47gmd5tgh4tlejth2rzgdfu77yc5u7oqfr4gxa)

![](https://blobs.farfield.systems/blobs/bafkreiduut6mmiiidb5v476ov3xn6wqa4hg4o4cnu6okpnbham3ugonmnq)

![](https://blobs.farfield.systems/blobs/bafkreiawxyfnfnrjqx3ft6lnd3wpywqw5bm52weidv2kfaqtius2tzyx54)

![](https://blobs.farfield.systems/blobs/bafkreicfs5upy5qj6zxwtqtmoce5qzdfhurxvxdxgvvsb22hztfjefuq2q)

![](https://blobs.farfield.systems/blobs/bafkreih36rcjwb7riziencf7csb3mhafvlhkcp66hfjbgtlvjgr366ylyq)

![](https://blobs.farfield.systems/blobs/bafkreic56c7tqpf76z5jjzhjwkszfemtfnhkg4yxubknto4bdqzxpxm3kq)

![](https://blobs.farfield.systems/blobs/bafkreiah6bp4j7faq36xicmptm6qor7wfowlfd5jwabijxqs2nlo6ax5vm)

![](https://blobs.farfield.systems/blobs/bafkreido6m2wuskwmywaoh3knrc2mlxowlkq5d6xbmnndsd3rgpo2uncfe)

![](https://blobs.farfield.systems/blobs/bafkreibp2lhl7k3cpgnocohj6loytvzep5wf7j2hzsie7wcykhdpjvaxqq)

![](https://blobs.farfield.systems/blobs/bafkreiflxts3r7jxvepze5hqwbbiwrgnwfaaipkaqy4crjcpymllszxtia)

## India

![](https://blobs.farfield.systems/blobs/bafkreialuin2agyykhkjzznmmaw45bz6itn5kaa3vvlv7rz4clrduypcvm)

![](https://blobs.farfield.systems/blobs/bafkreihn22xsd3dp7ed75imxorw5xw4ttsjedebsxm7oosoi7twjnm57vy)

![](https://blobs.farfield.systems/blobs/bafkreig577mr7htb7vute6uuf6lddizjyu3ps7o5q7uaj6a4qx6kuwgdje)

![](https://blobs.farfield.systems/blobs/bafkreici4hz5gvprcupg5ixch4tsqnnorctgyhpoao2jfaigc5yfo6dsru)

![](https://blobs.farfield.systems/blobs/bafkreib3xpi2fitqhopt3zsok4lzvoginuf6ybdzgmy7ykdsfrsd4ozokq)

![](https://blobs.farfield.systems/blobs/bafkreicfusidnpkkyqxl57cqcdfhdszveiqrgmj3rlrifr42xu3krfrbbe)

![](https://blobs.farfield.systems/blobs/bafkreifuncfdtaurjrnwfjici3rnc65t3d2pu7xbsdoahdq5cumjomlg4y)

![](https://blobs.farfield.systems/blobs/bafkreibx4ujqpzvg5bm4qwkc3t4j3s7ic22tmhipkpedwl424pnb64wtoy)

![](https://blobs.farfield.systems/blobs/bafkreibu3gyfyqtzl3echxubyilrrcx32b34elk5fwlg2lmg4mvmounwvi)

![](https://blobs.farfield.systems/blobs/bafkreid2nog357x7u4oyvsn7jcoi6xqijyrhonxoz4rglixqngodmdosyq)

![](https://blobs.farfield.systems/blobs/bafkreib4k6dcnx3xgxjxasozi7sbu7egxkjerckavzu6drfx7ckkyhbn7e)

![](https://blobs.farfield.systems/blobs/bafkreia2dy4b5nmbhj62hwvis76xlrleje5jqikebdih2lmtzc4ybao4xm)

![](https://blobs.farfield.systems/blobs/bafkreih35ib2qpqjb6duyg7m7e6mmbfeladmbjn3lnv7zevpousgg4hgde)

![](https://blobs.farfield.systems/blobs/bafkreifakzdohprkx7o3kk6uxexef3xsj7odyml3rjgoqzunpjojbyhnji)

![](https://blobs.farfield.systems/blobs/bafkreiflrfyhqnofnkm6aygyk333ufep42xs2vgzcvot6u3feza35vwo7i)

![](https://blobs.farfield.systems/blobs/bafkreidm3h6me7ga6ihf5dk74dzoaxpxucz5plrd4xqul7gmrzbcalgtbq)

![](https://blobs.farfield.systems/blobs/bafkreig7nba2gr7lpz6ubt5msshvmhfrqa24d7ff2foft77a5ap4uo7tmi)

![](https://blobs.farfield.systems/blobs/bafkreifynspdisk7ijkbefkcokmi47qndwl3vo3sxsqrqfccjqrmexo52i)

![](https://blobs.farfield.systems/blobs/bafkreifpev2btp6cwtdbpv775wyuwriptnbk7266xkv22xp4gqcdrf3woy)

![](https://blobs.farfield.systems/blobs/bafkreifukwbxz65oarud2facskz7iqjadvr5zqn6aiz7g53lqteu2ojgly)

![](https://blobs.farfield.systems/blobs/bafkreia2j3cfujxfplzccz4nl5p45ucpdbn6ncskki7qzhmiznlnkaatu4)

![](https://blobs.farfield.systems/blobs/bafkreia5gpuotk45mjvgfm2o2e3u3dzpxy3se63ikq3dwjhutbe7jpgeju)

![](https://blobs.farfield.systems/blobs/bafkreihfw3p7sqvfbjjku2pl5pfczledoefmgyrvbjlle5gcbcv6puklwy)

![](https://blobs.farfield.systems/blobs/bafkreibhazvi4fdz36xdieyn36qxu2ouhhsm7lbapazlgess54p7mzs4ni)

![](https://blobs.farfield.systems/blobs/bafkreie5br4s3ongqbqco73cjgtyx7j4wdmfmy2lwy6t7hh2ckkpnx7hcq)

![](https://blobs.farfield.systems/blobs/bafkreif5acujstr74soolj5vbqpexghlsf4vgm5sfq7o3ltx3wnplai56q)

![](https://blobs.farfield.systems/blobs/bafkreibjotw6e24kfrozdpwk6a67n7dgrb3dqfsc67i5w4juqfe4qv6qjm)

![](https://blobs.farfield.systems/blobs/bafkreie3rha6y5b5vjar75j4vot3dfxb62tyxh6pisuxrjiarvkeeypp64)

![](https://blobs.farfield.systems/blobs/bafkreiggdn3l2uwwpdfxtnfxch5zp6ilscbjzdnnmx7r5zszh3ho4owwgy)

![](https://blobs.farfield.systems/blobs/bafkreibsy73kc52x6uhykcke2fkymmtiatmaioxovpqcy2t4brxwkqme7y)

![](https://blobs.farfield.systems/blobs/bafkreid4gtvfxselzhhgrsarfpcwnd3btgzeq3ern5ezjrvn3m3qbn5vuu)

![](https://blobs.farfield.systems/blobs/bafkreignohhi5at7kwb75bvr5gil72q2wte2ygq23rqb5r4xb44nslkt5a)

![](https://blobs.farfield.systems/blobs/bafkreib4k6dcnx3xgxjxasozi7sbu7egxkjerckavzu6drfx7ckkyhbn7e)

![](https://blobs.farfield.systems/blobs/bafkreifmq3gkonj466s6v5tlbrvp4jdbejkbjazziqrifbuobq22io4tg4)

![](https://blobs.farfield.systems/blobs/bafkreic4ee5jxgyb4ga75mqpqahnnfrv2ykwgz67ske2glm6wcuyrbof5i)

![](https://blobs.farfield.systems/blobs/bafkreiejrvwfjhzwaxgdsrbun2tmocix73ytjzin7vlbrloruxtgmrwxoq)

![](https://blobs.farfield.systems/blobs/bafkreifevn5yen6imfzonbxhtzvlsvvryieh3axsvj3mz2fspug57a3g44)

![](https://blobs.farfield.systems/blobs/bafkreidm2cfwrfu3ens7thiq3koiho2tgmazzygxor24t7dib33wv4n56y)

![](https://blobs.farfield.systems/blobs/bafkreihift3fdwv5gram7lxz4f3scf2ej2smsrg3wqc2hatxei3pbhdldq)

![](https://blobs.farfield.systems/blobs/bafkreih7s43dcx2fkog2z4qtyo5374zjyv4xayq3b53l6ueppuyad2kazq)

![](https://blobs.farfield.systems/blobs/bafkreiezjnap2agw7ig5vymcyeno25hxhs2yyhozkdhjk75n7l6z5lztre)

![](https://blobs.farfield.systems/blobs/bafkreigzpizgzixcqucu3tgg6sqrpsqisbjh5ujsi25p6jsuhbvupnfu2a)

![](https://blobs.farfield.systems/blobs/bafkreiewc3bc66b4pk6axyyliyfumzdvm2evdiobkdcxdjnymjqzirnqwu)

![](https://blobs.farfield.systems/blobs/bafkreihyzp76e6uvgyscdozc6qbo6237cumw5m4aoqzaf3wleezbls6sn4)

![](https://blobs.farfield.systems/blobs/bafkreidulhdg2vnveniqjyzodcdu62v65eegwq3lcjomwzujmwf3qjgk3i)

![](https://blobs.farfield.systems/blobs/bafkreiap5vjsp4j272uk4mbwyywzc2bryht65tvqcgbdcp3q7g53hrg35i)

![](https://blobs.farfield.systems/blobs/bafkreibzvoltefutiey5pgtxbhjgbewr7zlh6ijfxbd6yc3e5o7e533qgm)

![](https://blobs.farfield.systems/blobs/bafkreie4iuem2cbm6hcj5fr65l3meernkzo6rplaer3ysev42nz6rrfgv4)

![](https://blobs.farfield.systems/blobs/bafkreicopqmyxycyboitqmjmxdzpyfor3jboyjuhsipnkg3hczskcusoc4)

![](https://blobs.farfield.systems/blobs/bafkreihebkfnrcaplqg7whva7czqzogny3pkgu3jymndotzd5yvd4psrxy)

![](https://blobs.farfield.systems/blobs/bafkreiftxldvftmpirywnnd5z4duba6r2juifbwcxz5dnavmla5b2fz54y)

![](https://blobs.farfield.systems/blobs/bafkreifsaejiryj7otlapyb3e7scjh3rbdaf4htbxcocnt3n3chtogh45u)

![](https://blobs.farfield.systems/blobs/bafkreibgcbpx7frhb77wn63qcumcr5vnmsrsksdp773hb5prjhbh7wpbay)

---
title: "Madison Park Smash"
section: recipes
description: "A refreshing cognac punch with Royal Combier, fresh lemon, and mint. Features a whip shake technique and crushed ice topped with bitters."
tags: ["cocktails", "mixology", "cognac", "lemon", "royal-combier", "demerara-syrup", "mint", "bitters"]
created: 2024-03-18T08:49:00Z
updated: 2026-07-29T21:06:19Z
cid: bafkreigque5nvhnsdqhsjqyb262wzbscpcvxd2ik7pejxdxu5l6czkvpxq
html: https://iammatthias.com/recipes/1710776965449-madison-park-smash
---

# Madison Park Smash

```recipe
yield: "2 drinks"
time: "8 min"
source: "The NoMad Bar — from their large-format Cocktail Explosions"
sourceURL: "https://punchdrink.com/recipes/madison-park-smash/"

ingredients:
  - { id: cognac, item: "Cognac", amount: "180 ml (6 oz)", note: "Pierre Ferrand" }
  - { id: combier, item: "Royal Combier", amount: "180 ml (6 oz)" }
  - { id: lemon-juice, item: "Lemon juice", amount: "135 ml (4½ oz)", note: "fresh" }
  - { id: demerara, item: "Demerara syrup", amount: "45 ml (1½ oz)" }
  - { id: mint, item: "Mint leaves", amount: "8", note: "plus a bouquet to garnish" }
  - { id: crushed-ice, item: "Crushed ice", amount: "as needed" }
  - { id: bitters, item: "Angostura bitters", amount: "to taste" }

steps:
  - { id: build, in: [cognac, combier, lemon-juice, demerara, mint], do: "combine with mint", detail: "Add the Cognac, Royal Combier, lemon juice, demerara syrup and mint leaves to a shaker." }
  - { id: whip, in: [build], do: "whip-shake", for: "5 s", detail: "Shake with two or three cubes only, until they have all but disappeared — enough to chill and bruise the mint without watering the drink down." }
  - { id: pour, in: [whip], do: "pour into a vessel", detail: "Strain into one large shared vessel and pack it almost to the top with crushed ice." }
  - { id: finish, in: [pour, crushed-ice, bitters], do: "dash and mound", detail: "Dash Angostura liberally over the ice, then mound more crushed ice on top." }
  - { id: garnish, in: [finish], do: "garnish", detail: "A tall mint bouquet — this drink is as much aroma as flavour. Two straws." }
```

---
title: "Irish Maid"
section: recipes
description: "A refreshing cocktail with Irish whiskey, elderflower liqueur, balanced by lemon juice and cucumber for a bright, garden-inspired flavor."
tags: ["cocktails", "mixology", "irish-whiskey", "elderflower", "cucumber", "lemon", "simple-syrup", "spirits"]
created: 2024-03-15T22:04:00Z
updated: 2026-07-29T21:06:19Z
cid: bafkreigc5ktjylwxsgczawpetove5chwmlz762ogcwfglbtujk3dlzzkle
html: https://iammatthias.com/recipes/1710565492602-irish-maid
---

# Irish Maid

```recipe
yield: "2 drinks"
time: "5 min"
source: "Jack McGarry, The Dead Rabbit — a riff on Sam Ross's Kentucky Maid"
sourceURL: "https://www.diffordsguide.com/cocktails/recipe/3646/irish-maid"

ingredients:
  - { id: cucumber, item: "Cucumber", amount: "4 slices", note: "plus 2 to garnish" }
  - { id: whiskey, item: "Irish whiskey", amount: "120 ml (4 oz)", note: "Roe & Co" }
  - { id: elderflower, item: "Elderflower liqueur", amount: "30 ml (1 oz)", note: "St-Germain" }
  - { id: simple-syrup, item: "Simple syrup", amount: "45 ml (1½ oz)" }
  - { id: lemon-juice, item: "Lemon juice", amount: "45 ml (1½ oz)", note: "fresh" }

steps:
  - { id: muddle, in: [cucumber], do: "muddle", for: "10 s", detail: "Press the cucumber in the shaker just enough to release its juice — do not shred it." }
  - { id: build, in: [muddle, whiskey, elderflower, simple-syrup, lemon-juice], do: "add the rest", detail: "Add the whiskey, elderflower liqueur, simple syrup and lemon juice, then fill with ice." }
  - { id: shake, in: [build], do: "shake", for: "12–15 s", detail: "Shake until well chilled." }
  - { id: strain, in: [shake], do: "double-strain", detail: "Strain into two chilled rocks glasses over fresh ice." }
  - { id: garnish, in: [strain], do: "garnish", detail: "A cucumber slice or a lemon twist on each." }
```

---
title: "Espresso Rum-Tiki"
section: recipes
description: "Cold brew, aged and white rums, orgeat, falernum, and bitters shaken until foamy and served over fresh ice."
tags: ["cocktails", "rum", "cold-brew", "espresso", "orgeat", "falernum", "mixology"]
created: 2024-03-15T21:56:00Z
updated: 2026-07-29T21:06:18Z
cid: bafkreihwjd6iby7jeehnqjrmptefdnqfy4mjkat655ozpenli46wob7csa
html: https://iammatthias.com/recipes/1710564975345-espresso-rum-tiki
---

# Espresso Rum-Tiki

```recipe
yield: "2 drinks"
time: "5 min"

ingredients:
  - { id: cold-brew, item: "Cold brew concentrate", amount: "60 ml (2 oz)", note: "or 2 shots of espresso, cooled" }
  - { id: aged-rum, item: "Aged rum", amount: "60 ml (2 oz)" }
  - { id: white-rum, item: "White rum", amount: "60 ml (2 oz)" }
  - { id: orgeat, item: "Almond orgeat", amount: "30 ml (1 oz)" }
  - { id: falernum, item: "Falernum", amount: "30 ml (1 oz)" }
  - { id: bitters, item: "Aromatic bitters", amount: "2 dashes" }
  - { id: lime, item: "Lime", amount: "2 slices", note: "to garnish" }

steps:
  - { id: build, in: [cold-brew, aged-rum, white-rum, orgeat, falernum, bitters], do: "combine over ice", detail: "Add everything to a shaker filled with ice." }
  - { id: shake, in: [build], do: "shake hard", for: "15 s", detail: "Shake harder and longer than a plain sour — the coffee is what foams." }
  - { id: strain, in: [shake], do: "strain over fresh ice", detail: "Strain into two glasses over fresh ice." }
  - { id: garnish, in: [strain, lime], do: "garnish", detail: "A lime slice on each." }
```

---
title: "Halekulani"
section: recipes
description: "A citrus-forward bourbon cocktail combining lime, orange, and pineapple juices with simple syrup, grenadine and bitters."
tags: ["bourbon", "cocktails", "mixology", "spirits", "tropical", "lime", "orange-juice", "pineapple", "grenadine", "simple-syrup", "bitters"]
created: 2024-03-15T21:51:00Z
updated: 2026-07-29T21:06:18Z
cid: bafkreifmoti4s2gwikoylah67lzetggbtydmz2yh6wm2wq3huoxwrntzgu
html: https://iammatthias.com/recipes/1710564673473-halekulani
---

# Halekulani

```recipe
yield: "2 drinks"
time: "5 min"
source: "The Weekend Mixologist — after the House Without a Key, Halekulani, Waikiki"
sourceURL: "https://www.instagram.com/p/CvAQLXYLpPJ/"

ingredients:
  - { id: bourbon, item: "Bourbon", amount: "90 ml (3 oz)" }
  - { id: lime-juice, item: "Lime juice", amount: "30 ml (1 oz)", note: "fresh" }
  - { id: orange-juice, item: "Orange juice", amount: "30 ml (1 oz)", note: "fresh" }
  - { id: pineapple-juice, item: "Pineapple juice", amount: "30 ml (1 oz)", note: "fresh" }
  - { id: simple-syrup, item: "Simple syrup", amount: "15 ml (½ oz)" }
  - { id: grenadine, item: "Grenadine", amount: "2 barspoons" }
  - { id: bitters, item: "Aromatic bitters", amount: "2 dashes" }

steps:
  - { id: build, in: [bourbon, lime-juice, orange-juice, pineapple-juice, simple-syrup, grenadine, bitters], do: "combine over ice", detail: "Add everything to a shaker filled with ice." }
  - { id: shake, in: [build], do: "shake", for: "12–15 s", detail: "Shake until well chilled." }
  - { id: strain, in: [shake], do: "strain", detail: "Into two chilled coupes for the classic service, or over pebble ice in highballs for something more casual." }
  - { id: garnish, in: [strain], do: "garnish", detail: "A pineapple wedge or a cherry on each." }
```

---
title: "Fermented Hot Honey"
section: recipes
description: "Fermented hot honey recipe: combine raw and roasted garlic, shallots, and chilies with honey."
tags: ["fermentation", "honey", "garlic", "chili", "condiments", "spicy", "homemade", "recipe"]
created: 2024-03-15T21:43:00Z
updated: 2026-07-29T21:06:18Z
cid: bafkreiflaj6zvuhtrev4y3qqzp6rsio5dmthce4fzu6ovsvrf2q2wsj76i
html: https://iammatthias.com/recipes/1710564234519-fermented-hot-honey
---

# Fermented Hot Honey

```recipe
yield: "1 cup (about 16 servings)"
time: "30 min active · 1–4 weeks fermenting"

ingredients:
  - { id: fresh-garlic, item: "Fresh garlic", amount: "3 cloves", note: "Peeled and lightly crushed", group: "Base Ingredients" }
  - { id: fresh-shallots, item: "Fresh shallots", amount: "2", note: "Peeled and thinly sliced", group: "Base Ingredients" }
  - { id: fresh-chili-peppers, item: "Fresh chili peppers", amount: "4-6", note: "Sliced, adjust to taste", group: "Base Ingredients" }
  - { id: sea-salt, item: "Sea salt", amount: "1-2 teaspoons", note: "Adjust based on preference", group: "Base Ingredients" }
  - { id: garlic-cloves, item: "Garlic cloves", amount: "2-3 cloves", note: "Whole, unpeeled", group: "Roasted Ingredients" }
  - { id: shallot, item: "Shallot", amount: "1 medium", note: "Halved, unpeeled", group: "Roasted Ingredients" }
  - { id: chili-peppers, item: "Chili peppers", amount: "2-3", note: "Whole", group: "Roasted Ingredients" }
  - { id: raw-honey, item: "Raw honey", amount: "1 cup", note: "Unfiltered", group: "Base Ingredients" }

steps:
  - id: s1
    prep: true
    do: "preheat your oven"
    detail: "Preheat your oven to 400°F (200°C)."
  - id: s2
    in: [fresh-garlic, fresh-shallots, fresh-chili-peppers, garlic-cloves, shallot, chili-peppers, sea-salt]
    do: "place the whole garlic"
    for: "15–20 min"
    detail: "Place the whole garlic cloves, shallot halves, and whole chili peppers on a baking sheet. Roast for 15-20 minutes, or until soft and slightly charred. Allow to cool, then peel and slice. In a clean, dry bowl, mix the raw sliced chili peppers, garlic, and shallots with the sea salt. Ensure the vegetables are well coated with salt. This helps inhibit unwanted bacteria growth while promoting good bacteria for fermentation."
  - id: s3
    in: [s2]
    do: "transfer the salted"
    detail: "Transfer the salted vegetables into a clean, dry jar. Add the roasted garlic, shallot, and chili peppers to the jar with the salted vegetables."
  - id: s4
    in: [s3, raw-honey]
    do: "pour the raw honey"
    detail: "Pour the raw honey over the vegetables, ensuring they are completely submerged. If the honey is too thick, warm it slightly to make it more pourable, but ensure it's not hot as high temperatures can kill beneficial bacteria. Secure the jar with an airlock lid. This is crucial to allow gases produced during fermentation to escape while preventing oxygen and contaminants from entering."
  - id: s5
    in: [s4]
    do: "place the jar in a dark"
    for: "4 weeks"
    detail: "Place the jar in a dark, room-temperature spot for 1 to 4 weeks. Check daily, stirring the mixture with a clean spoon to release gases and prevent mold formation. Taste the honey after 1 week, and every week thereafter, until it reaches your desired level of fermentation."
  - id: s6
    in: [s5]
    do: "replace the airlock"
    detail: "Once fermentation is complete, replace the airlock with a tight-fitting lid and store the fermented hot honey in the refrigerator. It can be stored for several months."
```

---
title: "Caramel Apple Mille Feuille"
section: recipes
description: "Elegant mille-feuille dessert with flaky puff pastry layers, caramelized cinnamon apples, and vanilla cream."
tags: ["pastry", "dessert", "puff-pastry", "apple", "caramel", "cinnamon", "baking", "french", "bread", "layered"]
created: 2024-03-15T21:28:00Z
updated: 2026-07-29T21:06:18Z
cid: bafkreick3mkomqgrvpb4snuxaljxbkm6ju4bt5tn5zplqlbdrre2saiv3m
html: https://iammatthias.com/recipes/1710563320517-caramel-apple-mille-feuille
---

# Caramel Apple Mille Feuille

```recipe
yield: "2 servings"
time: "40 min active · 2 h total"

ingredients:
  - { id: puff-pastry, item: "Puff pastry", amount: "1 sheet", note: "thawed; you will use about half" }
  - { id: powdered-sugar, item: "Powdered sugar", amount: "As needed", note: "for dusting" }

steps:
  - id: s1
    prep: true
    do: "preheat the oven"
    detail: "Preheat the oven to 400°F (200°C)."
  - id: s2
    in: [puff-pastry]
    do: "roll out the thawed puff"
    detail: "Roll out the thawed puff pastry on a baking sheet lined with parchment paper. Prick the surface all over with a fork."
  - id: s3
    in: [s2]
    do: "cover with another sheet"
    detail: "Cover with another sheet of parchment paper and place another baking sheet on top to weigh it down."
  - id: s4
    in: [s3]
    do: "bake for 15-20 minutes"
    for: "15–20 min"
    detail: "Bake for 15-20 minutes or until golden brown. Let it cool, then cut into 12 equal rectangles."
  - id: s5
    in: [s4, powdered-sugar]
    do: "dust with powdered sugar"
    for: "2–3 min"
    detail: "Dust with powdered sugar and briefly return to the oven to caramelize the sugar, about 2-3 minutes."
```

---
title: "Harissa"
section: recipes
description: "Recipes for traditional harissa using either dried or fresh peppers with toasted spices, garlic, oil and lemon."
tags: ["harissa", "peppers", "spice", "homemade", "condiments", "cumin", "coriander", "olive-oil", "garlic", "recipe"]
created: 2024-03-14T12:06:00Z
updated: 2026-07-29T21:06:17Z
cid: bafkreifb2pzd6r35bs3dzuldpw7ymm5fusva54vktlmcdoi6yz7zkastky
html: https://iammatthias.com/recipes/1710443206146-harissa
---

# Harissa

```recipe
yield: "about 250 ml (a small jar)"
time: "20 min active · 50 min total"

ingredients:
  - { id: coriander-seeds, item: "Coriander seeds", amount: "1 teaspoon" }
  - { id: cumin-seeds, item: "Cumin seeds", amount: "1 teaspoon" }
  - { id: caraway-seeds, item: "Caraway seeds", amount: "½ teaspoon" }
  - { id: garlic-cloves, item: "Garlic cloves", amount: "3", note: "Peeled" }
  - { id: salt, item: "Salt", amount: "1 teaspoon", note: "Or to taste" }
  - { id: olive-oil, item: "Olive oil", amount: "3 tablespoons", note: "Plus extra for storing" }
  - { id: lemon-juice, item: "Lemon juice", amount: "1 tablespoon", note: "Freshly squeezed" }
  - { id: dried-chilies, item: "Dried chilies", amount: "4 ounces", note: "Seeded, stems removed" }

steps:
  - id: s1
    in: [coriander-seeds, cumin-seeds, caraway-seeds, dried-chilies]
    do: "rehydrate dried chilies"
    for: "30 min"
    detail: "Rehydrate dried chilies by soaking them in hot water for 30 minutes. Drain and remove any remaining seeds if desired."
  - id: s2
    in: [s1]
    do: "lightly toast"
    for: "2–3 min"
    detail: "In a dry skillet over medium heat, lightly toast the coriander, cumin, and caraway seeds until fragrant, about 2-3 minutes. Let them cool and then grind them into a fine powder."
  - id: s3
    in: [s2, garlic-cloves, salt, olive-oil, lemon-juice]
    do: "combine the rehydrated"
    detail: "In a food processor, combine the rehydrated chilies, ground spices, garlic cloves, salt, olive oil, and lemon juice. Blend until a smooth paste forms."
  - id: s4
    in: [s3]
    do: "transfer to a jar"
    detail: "Transfer to a jar and cover the top with a thin layer of olive oil; this helps preserve the harissa. Store in the refrigerator for up to a month."
```

---
title: "Revisiting Obsidian as a CMS, again"
section: posts
description: "Obsidian, GitHub's GraphQL API, and Astro form a free, flexible publishing system for Markdown content."
tags: ["obsidian", "cms", "markdown", "github", "astro", "nextjs", "content-management", "web-development"]
created: 2024-03-11T10:21:00Z
updated: 2026-07-16T14:56:28Z
cid: bafkreif3jxsichpyzzidn4j6xlkzhyak7o2rrxkefp77wtzztmvmim7cky
html: https://iammatthias.com/posts/1710177704945-revisiting-obsidian-as-a-cms-again
---

# Revisiting Obsidian as a CMS, again

The technical details of this setup are in [Obsidian as a CMS](1670659200001-obsidian-as-a-cms.md) and [Revisiting Obsidian as a CMS](1699332127006-revisiting-obsidian-as-a-cms.md). The thought process that got me here is its own thing.

I wanted a backend for my personal site that was as close to free as possible. Airtable burned through the free tier fast, Google Sheets felt like dragging a desk through a doorway. What I actually needed was a flat-file way to publish markdown with metadata, plus the ability to write on the go (Git clients on iOS make that painful).

Obsidian was already my knowledge base. Folder-based templates, private Git backup. With the right YAML frontmatter, it covered all the meta and feature-flag fields I cared about. The only gap was getting the markdown out of the repo and into a front-end framework.

## Inner workings

The GitHub GraphQL API can fetch a single file or a whole directory in one query. YAML gets parsed with [gray-matter](https://github.com/jonschlinkert/gray-matter), then the markdown renders. In Next.js I used [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote). I'm on Astro now to ship less JS, with [astro-remote](https://github.com/natemoo-re/astro-remote), which uses [marked](https://marked.js.org/) and sanitizes output with [ultrahtml](https://github.com/natemoo-re/ultrahtml). Component overrides work the same as MDX.

### Fetching posts

```typescript
async function fetchFromGitHubGraphQL(query: string, variables: any) {
  const response = await fetch("https://api.github.com/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${github}`,
    },
    body: JSON.stringify({ query, variables }),
  });

  if (!response.ok) {
    console.error("HTTP Error:", response.status);
    return response;
  }

  return response.json();
}
```

`fetchFromGitHubGraphQL` used to be load-bearing across several callers. Now it sits behind one function, `getObsidianEntries`.

```typescript
export async function getObsidianEntries(path: string, slug?: string) {
  const expression = slug ? `HEAD:content/${path}/${slug}.md` : `HEAD:content/${path}`;

  const {
    data: {
      repository: { object },
    },
  } = await fetchFromGitHubGraphQL(
    `
      query fetchEntries($owner: String!, $name: String!, $expression: String!) {
        repository(owner: $owner, name: $name) {
          object(expression: $expression) {
            ... on Tree {
              entries {
                name
                object {
                  ... on Blob {
                    text
                  }
                }
              }
            }
            ... on Blob {
              text
            }
          }
        }
      }
    `,
    {
      owner: `GITHUB_USERNAME`,
      name: `REPO_NAME`,
      expression,
    }
  );

  if (slug) {
    if (!object || !object.text) {
      console.error("No data returned from the GraphQL query for the single entry.");
      return null;
    }
    return parseMarkdownContent(object.text, path);
  }

  if (!object || !object.entries) {
    console.error("No data returned from the GraphQL query for multiple entries.");
    return [];
  }

  const parsedEntries = await Promise.all(
    object.entries.map((entry: { object: { text: any } }) => {
      const content = entry.object.text;
      return parseMarkdownContent(content, path);
    })
  );

  parseAndMergeTags(parsedEntries);

  return parsedEntries;
}
```

### File structure

```
.
├── README.md
├── content
│   ├── art
│   │   └── txt.md
│   ├── notes
│   │   └── txt.md
│   ├── posts
│   │   └── txt.md
│   └── recipes
│       └── txt.md
└── templates
    └── base_template.md
```

The folder layout doubles as routing on the front end. `getObsidianEntries` takes a path, with an optional slug. Slug matches the filename, so `path + slug.md` returns one entry, and `path` alone returns the whole directory. That one trick made it easy to spin up new content types.

```typescript
---
import { Markdown } from "astro-remote";

const { path, slug } = Astro.params;
const entry = await getObsidianEntries(path, slug);
const { body, frontmatter } = entry;
---

<article>
  <Markdown
    components={{ img: Image, p: Paragraph }}
    sanitize={{
    dropElements: ["head", "style"],
    allowCustomElements: true,
    }}
  >
    {body}
  </Markdown>
</article>
```

```typescript
---
import { getObsidianEntries } from "@lib/github";

const { path } = Astro.params;
entries = entries.sort((a, b) => new Date(b.frontmatter.created).getTime() - new Date(a.frontmatter.created).getTime());
---

<>
  {
    entries.map((entry) => (
      <li>
        <p>
          <a href={`/${path}/${entry.frontmatter.slug}`}>{entry.frontmatter.title}</a>
        </p>
      </li>
    ))
  }
</>
```

### Frontmatter

A `base_template` populates each new file. It prompts for a title, formats a URL-safe slug, and stamps creation and modified dates.

```yaml
---
<%*
let title = await tp.system.prompt("Please enter a value");
let slug = tp.file.creation_date("x") + " " + title;
let formatted_slug = slug.trim().replace(/\W+/g, '-').toLowerCase();
await tp.file.rename(`${formatted_slug}`);
%>
title: <%* tR += title; %>
slug: <%* tR += formatted_slug; %>
published: false
created: <% tp.file.creation_date("YYYY-MM-DD HH:mm") %>
updated: <% tp.file.last_modified_date("YYYY-MM-DD HH:mm") %>
tags:
  -
---
```

Tags are the rough edge. Right now I aggregate them into a flat file on Cloudflare R2, which is hacky and unreliable. A hashing scheme to keep tags in sync with published content is on the list.

### Images

The old setup hashed each image with md5, dumped it to `assets`, and a GitHub Action pushed it to R2 on push. The [S3 Image Uploader](https://github.com/jvsteiner/s3-image-uploader) plugin replaced that: it hashes the filename and uploads from the Obsidian editor directly. My PR adding concurrent uploads landed in [`0.2.10`](https://github.com/jvsteiner/s3-image-uploader/releases/tag/0.2.10).

Markdown wraps `<img>` in `<p>`, which I don't want. Astro-Remote makes it easy to unwrap by checking the rendered slot:

```
---
let slots = await Astro.slots.render("default");
let slotsString = slots.toString();
---

{
  slotsString.includes("img src") ? (
    <slot />
  ) : (
    <p>
      <slot />
    </p>
  )
}
```

The whole thing is cheap, fast to write in, and mine.

---
title: "Pineapple Bourbon Honey Sour"
section: recipes
description: "Bourbon, pineapple, lemon, and honey dry-shaken with egg white, served over ice with bitters and edible flowers."
tags: ["bourbon", "cocktails", "mixology", "pineapple", "honey", "egg-white", "edible-flowers", "spirits"]
created: 2024-03-09T20:30:00Z
updated: 2026-07-29T21:06:17Z
cid: bafkreigm6umta4pubmkrixwxgek6phreyhjjrbl2p5yzwlzmypfi7ngbau
html: https://iammatthias.com/recipes/1710045021303-pineapple-bourbon-honey-sour
---

# Pineapple Bourbon Honey Sour

```recipe
yield: "2 drinks"
time: "6 min"

ingredients:
  - { id: bourbon, item: "Bourbon", amount: "120 ml (4 oz)" }
  - { id: pineapple-juice, item: "Pineapple juice", amount: "60 ml (2 oz)", note: "fresh" }
  - { id: lemon-juice, item: "Lemon juice", amount: "45 ml (1½ oz)", note: "fresh" }
  - { id: honey-syrup, item: "Honey syrup", amount: "30 ml (1 oz)", note: "3 parts honey to 1 part hot water" }
  - { id: egg-white, item: "Egg white", amount: "2", note: "or 60 ml aquafaba" }
  - { id: bitters, item: "Aromatic bitters", amount: "a few dashes" }

steps:
  - { id: dry, in: [bourbon, pineapple-juice, lemon-juice, honey-syrup, egg-white], do: "dry-shake", for: "15 s", detail: "Shake everything without ice first — this is what builds the foam." }
  - { id: wet, in: [dry], do: "shake with ice", for: "12–15 s", detail: "Add ice and shake again until well chilled." }
  - { id: strain, in: [wet], do: "double-strain", detail: "Strain into two chilled coupes, leaving behind ice shards and any stray egg." }
  - { id: finish, in: [strain, bitters], do: "dash and garnish", for: "1 min", detail: "Let the foam settle for a moment, then dash bitters across the top. Edible flowers if you have them." }
```

---
title: "Pizza Margherita"
section: recipes
description: "Margherita pizza with homemade dough and sauce, fresh mozzarella, basil, and olive oil."
tags: ["pizza", "italian", "recipe", "mozzarella", "basil", "olive-oil", "neapolitan"]
created: 2024-03-09T17:03:00Z
updated: 2026-07-29T21:06:17Z
cid: bafkreifsbdiqhq6qz6tukocvpead355lcuuwd4cvrktslueo7nlf4sj324
html: https://iammatthias.com/recipes/1710032637430-pizza-margherita
---

# Pizza Margherita

```recipe
yield: "2 pizzas (2 servings)"
time: "15 min active · 25 min total"

ingredients:
  - { id: fresh-mozzarella, item: "Fresh mozzarella", amount: "4 ounces", note: "Sliced", group: "Additional Ingredients for Pizza Margherita" }
  - { id: fresh-basil-leaves, item: "Fresh basil leaves", amount: "To taste", group: "Additional Ingredients for Pizza Margherita" }
  - { id: extra-virgin-olive-oil, item: "Extra-virgin olive oil", amount: "Drizzle", group: "Additional Ingredients for Pizza Margherita" }

steps:
  - id: s1
    prep: true
    do: "prepare the pizza dough"
    for: "5 min"
    detail: "Prepare the pizza dough following our [Pizza Dough Recipe](./1710031406664-pizza-dough)."
  - id: s2
    prep: true
    do: "preheat your oven to its"
    for: "6–8 min"
    detail: "Preheat your oven to its highest setting with a pizza stone or baking sheet inside."
  - id: s3
    prep: true
    do: "roll out your pizza"
    detail: "Roll out your pizza dough on a piece of parchment paper."
  - id: s4
    prep: true
    do: "prepare the pizza sauce"
    detail: "Prepare the pizza sauce following our [Pizza Sauce Recipe](./1710031949965-pizza-sauce) and spread it over the dough, leaving a small border."
  - id: s5
    in: [fresh-mozzarella]
    do: "distribute slices"
    detail: "Distribute slices of fresh mozzarella evenly over the sauce."
  - id: s6
    in: [s5]
    do: "slide the pizza"
    detail: "Slide the pizza (with the parchment) onto the preheated stone or baking sheet."
  - id: s7
    in: [s6]
    do: "bake until the crust is"
    for: "6–8 min"
    detail: "Bake until the crust is golden and cheese is bubbling."
  - id: s8
    in: [s7, fresh-basil-leaves, extra-virgin-olive-oil]
    do: "remove from the oven"
    detail: "Remove from the oven, top with fresh basil leaves and a drizzle of olive oil."
  - id: s9
    in: [s8]
    do: "slice and serve"
    detail: "Slice and serve immediately."
```

---
title: "Pizza Sauce"
section: recipes
description: "Simple 3-ingredient pizza sauce: blend drained canned tomatoes with olive oil and salt until smooth. Use immediately or refrigerate."
tags: ["pizza", "sauce", "tomatoes", "olive-oil", "italian"]
created: 2024-03-09T16:52:00Z
updated: 2026-07-29T21:20:27Z
cid: bafkreifrl3k4hqozwhx3mxbxfhqlrvh5xt5oqofa3ciok2qosf63lrxt24
html: https://iammatthias.com/recipes/1710031949965-pizza-sauce
---

# Pizza Sauce

```recipe
yield: "about 500 ml — enough for 4 pizzas"
time: "10 min"

ingredients:
  - { id: canned-whole-tomatoes, item: "Canned whole tomatoes", amount: "28 ounces", note: "Drained" }
  - { id: olive-oil, item: "Olive oil", amount: "1 tablespoon", note: "Extra-virgin" }
  - { id: salt, item: "Salt", amount: "To taste", note: "Preferably fine sea salt" }

steps:
  - id: s1
    in: [canned-whole-tomatoes]
    do: "drain the canned"
    detail: "Drain the canned tomatoes and remove any hard stems."
  - id: s2
    in: [s1, olive-oil, salt]
    do: "place the tomatoes"
    detail: "Place the tomatoes, olive oil, and a pinch of salt in a food processor."
  - id: s3
    in: [s2]
    do: "blend until smooth"
    detail: "Blend until smooth. Taste and adjust salt as needed."
  - id: s4
    in: [s3]
    do: "use immediately or store"
    detail: "Use immediately or store in the refrigerator for up to 1 week. For a detailed guide and additional tips, refer to the original recipe page."
```

---
title: "Neopolitan Pizza Dough"
section: recipes
description: "Classic Neapolitan pizza dough recipe using 00 and all-purpose flours."
tags: ["pizza", "dough", "baking", "italian", "neapolitan", "bread", "recipe"]
created: 2024-03-09T16:43:00Z
updated: 2026-07-29T21:20:26Z
cid: bafkreiggfuetxz3gmba7t3wgzprudiph2e5nlzk6ygcfzv6yzmaw5hlsze
html: https://iammatthias.com/recipes/1710031406664-pizza-dough
---

# Neopolitan Pizza Dough

```recipe
yield: "2 dough balls — two 12-in. pizzas"
time: "25 min active · 5 h total"

ingredients:
  - { id: 00-flour, item: "00 flour", amount: "153 grams (1 cup plus 1 tbsp)" }
  - { id: all-purpose-flour, item: "all-purpose flour", amount: "153 grams (1 cup plus 1 tbsp and 2 tsp)" }
  - { id: fine-sea-salt, item: "fine sea salt", amount: "8 grams (1 tsp)" }
  - { id: active-dry-yeast, item: "active dry yeast", amount: "2 grams (¾ tsp)" }
  - { id: extra-virgin-olive-oil, item: "extra-virgin olive oil", amount: "4 grams (1 tsp)", note: "optional" }

steps:
  - id: s1
    in: [00-flour, all-purpose-flour, fine-sea-salt, active-dry-yeast]
    do: "combine the 00 flour"
    detail: "In a large mixing bowl, combine the 00 flour, all-purpose flour, and fine sea salt. In a separate container, dissolve the active dry yeast in 200 grams of cold water."
  - id: s2
    in: [s1, extra-virgin-olive-oil]
    do: "pour the yeast mixture"
    detail: "Pour the yeast mixture into the flour mixture and mix by hand until a rough dough forms. Add the olive oil (if using) and continue to mix until fully incorporated."
  - id: s3
    in: [s2]
    do: "knead the dough"
    for: "10–15 min"
    detail: "Knead the dough on a lightly floured surface for about 10-15 minutes until it becomes smooth and elastic. Divide the dough into 4 equal portions."
  - id: s4
    in: [s3]
    do: "shape each portion"
    for: "2 h"
    detail: "Shape each portion into a ball and place them on a floured surface or tray. Place the dough balls in an oven with the light on and let rise for 2 hours."
  - id: s5
    in: [s4]
    do: "transfer the dough balls"
    for: "1–2 h"
    detail: "Transfer the dough balls to the refrigerator and let them rest for 1-2 hours. Remove the dough from the refrigerator and let it come to room temperature before using (about 1 hour)."
  - id: s6
    in: [s5]
    do: "preheat your pizza oven"
    detail: "Preheat your pizza oven outside to its highest setting. On a floured surface, stretch each dough ball into a 12-inch round, being careful not to deflate the edges."
  - id: s7
    in: [s6]
    do: "top with your favorite"
    for: "6–8 min"
    detail: "Top with your favorite ingredients and bake one pizza at a time for 6-8 minutes, or until the crust is golden and the toppings are cooked."
```

---
title: "Epochs"
section: art
description: "A generative onchain svg, powered by Highlight.xyz"
tags: ["svg", "generative", "blockchain", "onchain", "algorithms", "gpt", "patterns"]
created: 2024-03-07T12:58:00Z
updated: 2026-07-16T14:59:43Z
cid: bafkreihsfccpeobwofhhhq4aiqf6vu27ptvoimftcykbqgktnxhqh4rlp4
html: https://iammatthias.com/art/1709816319000-epochs
---

# Epochs

Excerpts from a generative series. The SVG produced different results as defined by the onchain `epoch`. There was a cyclical nature to the patterns: the deeper you progressed in each stack, the more erratic things became. The `epoch` algorithm was loosely derived from the average time it takes for a new block to appear on chain, 11 seconds, and a modulo of that.


![](https://blobs.farfield.systems/blobs/bafkreifcqeufjq47ozroglsl3umc635muhalyfvhvqzb3gei5dvfzjheem)

![](https://blobs.farfield.systems/blobs/bafkreigfg7lsgpzh5gnnipak7yiuqqhxitmwkomnotm27ujldfsvlulwou)

![](https://blobs.farfield.systems/blobs/bafkreif4uof7igx244vmqzsvxqhv443atj4tekkfotvq5thgb4kmxqvti4)

![](https://blobs.farfield.systems/blobs/bafkreihkwejj4dpg5ftzp5jjfq6wmhoaoz5emb6e6n7yxzmehicixhi7wa)

![](https://blobs.farfield.systems/blobs/bafkreieln2fvxkbwrgrq4brrjjqwb5mwq4i7odnksskrnf7tq6gcu3gacm)

![](https://blobs.farfield.systems/blobs/bafkreicdjfyxxm6mhsysmhfvevb76ufbbvcuehux2fc6qvl7cfgnifdsu4)

![](https://blobs.farfield.systems/blobs/bafkreihz3f4ufongofwsxiuwzinoyezsllnem55kqx6azm4tybbyidx6qi)

![](https://blobs.farfield.systems/blobs/bafkreifb2cqe6rlyaal3rakbmbyamsqwikjmvb62i7nrkelarvrrlzyb4a)

![](https://blobs.farfield.systems/blobs/bafkreiammnxonp22vbyl6tpucq5b2eot2foxl7pw4va5qsl7f2cdtpyxou)

![](https://blobs.farfield.systems/blobs/bafkreia2zmbir6c4zjm7qj45jyl6skfjchjdj6i2xvkwxslwzuxmz6kc5q)

![](https://blobs.farfield.systems/blobs/bafkreihnqsbn3nbioibhubgs5gmtwjxeo5zput72igzk2bjsth2nrbbya4)

![](https://blobs.farfield.systems/blobs/bafkreift4fsrduiofueofhwzdy2p2l73zxd3r2phxkb57zoo3nr6nir2uu)

![](https://blobs.farfield.systems/blobs/bafkreibd53tck3xu2sx2uo5e7p4k3q5gxrmtbsi42a6g4f7j43viaxtp3y)

![](https://blobs.farfield.systems/blobs/bafkreidvvbeymyqemkd2ucbljkbtlopew6jjql6g3cjntzzlbf4wgvztx4)

![](https://blobs.farfield.systems/blobs/bafkreigaujzo7joh23o7p6liowgfvvcpjsjwn6psgjhp7xndtrtdnlq3gi)

---
title: "Creamy Bell Pepper Alfredo Sauce"
section: recipes
description: "A creamy red pepper pasta dish with sautéed peppers, garlic, wine and cheese, brightened with lemon and finished with a hint of nutmeg."
tags: ["pasta", "red-bell-peppers", "white-wine", "parmesan", "lemon", "nutmeg", "italian", "sauce"]
created: 2024-03-02T19:05:00Z
updated: 2026-07-29T21:20:26Z
cid: bafkreielzzpowgl4i63psoddudjdoqqkaqqp4jv6c6q4zwd6fnxamennxi
html: https://iammatthias.com/recipes/1709406325649-creamy-bell-pepper-alfredo-sauce
---

# Creamy Bell Pepper Alfredo Sauce

```recipe
yield: "2 servings (enough for 200 g pasta)"
time: "15 min active · 30 min total"

ingredients:
  - { id: red-bell-peppers, item: "Red bell peppers", amount: "1 large", note: "sliced" }
  - { id: butter, item: "Butter", amount: "1 tbsp", note: "unsalted" }
  - { id: garlic, item: "Garlic", amount: "1–2 cloves", note: "minced" }
  - { id: black-pepper, item: "Black pepper", amount: "to taste", note: "freshly ground" }
  - { id: white-wine, item: "White wine", amount: "⅛ cup" }
  - { id: heavy-cream, item: "Heavy cream", amount: "½ cup" }
  - { id: nutmeg, item: "Nutmeg", amount: "⅛ tsp", note: "grated" }
  - { id: parmesan-cheese, item: "Parmesan cheese", amount: "¼ cup", note: "grated" }
  - { id: lemon, item: "Lemon", amount: "½", note: "zest and juice" }
  - { id: salt, item: "Salt", amount: "to taste" }
  - { id: pasta, item: "Pasta", amount: "200g", note: "cooked al dente" }

steps:
  - id: s1
    in: [red-bell-peppers, butter, garlic, black-pepper]
    do: "melt butter over medium"
    for: "2 min"
    detail: "In a large skillet, melt butter over medium heat. Add the sliced red bell peppers and minced garlic, and sauté until the peppers are soft."
  - id: s2
    in: [s1, white-wine]
    do: "deglaze the skillet"
    for: "8–10 min"
    detail: "Deglaze the skillet with white wine, scraping up any browned bits from the bottom of the pan."
  - id: s3
    in: [s2, heavy-cream, nutmeg]
    do: "reduce heat to low"
    detail: "Reduce heat to low and add the heavy cream and grated nutmeg, stirring well."
  - id: s4
    in: [s3, parmesan-cheese]
    do: "stir in the grated"
    for: "5 min"
    detail: "Stir in the grated Parmesan cheese until melted and the sauce is smooth."
  - id: s5
    in: [s4, lemon, salt]
    do: "add the lemon zest"
    detail: "Add the lemon zest and juice, then season with salt and black pepper to taste."
  - id: s6
    in: [s5, pasta]
    do: "serve over cooked pasta"
    detail: "Serve over cooked pasta, garnished with additional Parmesan if desired."
```

---
title: "Case Study: Rebuilding Opul on Framer"
section: posts
description: "A two-month WordPress-to-Framer migration that gave Opul's team control of the site and lifted new-customer demo conversion 50% over projections."
tags: ["e-commerce", "platform-migration", "case-study", "ramen", "wordpress", "web-development", "ai", "conversion-optimization"]
created: 2024-03-01T16:19:00Z
updated: 2026-07-16T14:56:28Z
cid: bafkreigev7jbqtuvfoj45eesw6wm6jvdz3ntgrjakn7lq6f2smc3w5hxmi
html: https://iammatthias.com/posts/1709280000000-case-study-revance-opul
---

# Case Study: Rebuilding Opul on Framer

Opul's WordPress site was run by an outside agency. Content updates dragged, performance was bad, and the digital sales path was a mess. Customer data sat unused, personalization was minimal, and onboarding was manual. They had a hard launch deadline coming up.

Opul brought me on as a contract developer to migrate the site from WordPress to Framer in two months.

Framer earned the pick: a visual CMS the non-technical team could actually run, faster pages, and full internal control after handoff.

## Build

Two months, head down. Rebuild every critical user path in Framer. Redirect anything that couldn't migrate cleanly. Figma for design with the VP of Design, tight feedback cycles, no backend changes. Framer's React integration kept things moving. We ran UX tests through the project and surveyed new customers after launch.

The risk was real. Opul was already on track to miss the deadline before we started.

## Outcome

Launched on schedule.

- New-customer demo conversion up 50% over projections
- Existing-customer demo conversion 14% above target
- 93% of existing customers migrated to the new platform
- 70% of customers cross-sold into additional products

More than half of Opul's traffic was mobile, and the new responsive design closed the gap users had been complaining about for months. The internal team took it over and ran with it.

---
title: "Bermuda Hundred"
section: recipes
description: "A tropical cocktail mixing gin, Campari, orgeat, lime and pineapple juice. Shake with ice and garnish with pineapple leaf and cherry."
tags: ["cocktails", "mixology", "gin", "campari", "tropical", "pineapple", "lime"]
created: 2024-02-27T12:08:00Z
updated: 2026-07-29T21:06:16Z
cid: bafkreidgcs7smezgezbyilcjfv6ghevcwehcpfiitc65ykct2y5dkperti
html: https://iammatthias.com/recipes/1709064533452-bermuda-hundred
---

# Bermuda Hundred

```recipe
yield: "2 drinks"
time: "5 min"

ingredients:
  - { id: gin, item: "Gin", amount: "90 ml (3 oz)" }
  - { id: pineapple-juice, item: "Pineapple juice", amount: "90 ml (3 oz)", note: "fresh, for the foam" }
  - { id: campari, item: "Campari", amount: "45 ml (1½ oz)" }
  - { id: orgeat, item: "Orgeat", amount: "30 ml (1 oz)" }
  - { id: lime-juice, item: "Lime juice", amount: "30 ml (1 oz)", note: "fresh" }

steps:
  - { id: build, in: [gin, pineapple-juice, campari, orgeat, lime-juice], do: "combine over ice", detail: "Add everything to a shaker filled with ice." }
  - { id: shake, in: [build], do: "shake", for: "12–15 s", detail: "Shake hard — the fresh pineapple juice should work up a head of foam." }
  - { id: strain, in: [shake], do: "strain over fresh ice", detail: "Strain into two chilled rocks glasses over fresh ice." }
  - { id: garnish, in: [strain], do: "garnish", detail: "A pineapple leaf and a cherry on each." }
```

---
title: "Revisiting Obsidian as a CMS"
section: posts
description: "A home-built publishing system using Obsidian for writing, GitHub for storage, and Next.js for rendering."
tags: ["development", "web-development", "obsidian", "github", "nextjs", "cms", "graphql"]
created: 2023-11-06T08:57:00Z
updated: 2026-07-16T14:56:28Z
cid: bafkreieo4tipxhq3v4bjtzl4ptldoulcnvhltuafv3ydppkr7iqvfmhe6q
html: https://iammatthias.com/posts/1699332127006-revisiting-obsidian-as-a-cms
---

# Revisiting Obsidian as a CMS

I gave the Notion API a real shot and came back. The homebrew Obsidian pipeline does what I need: write in Obsidian, sync to GitHub, render with Next.js.

The whole thing is a few JavaScript functions on top of GitHub's GraphQL API.

`fetchFromGitHubGraphQL` is the wrapper:

```tsx
async function fetchFromGitHubGraphQL(query: string, variables: any) {
  const token = process.env.NEXT_PUBLIC_GITHUB;
  const response = await fetch("https://api.github.com/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ query, variables }),
  });

  if (!response.ok) {
    console.error("HTTP Error:", response.status);
    return response;
  }

  return response.json();
}
```

Obsidian syncs on save, so the repo always has the latest markdown.

`parseMarkdownContent` runs each file through `gray-matter` and pulls out the frontmatter and body:

```tsx
function parseMarkdownContent(content: string) {
  const { data, content: body } = matter(content);
  return {
    slug: data.id,
    name: data.name,
    created: data.created ? new Date(data.created).getTime() : null,
    updated: data.updated ? new Date(data.updated).getTime() : null,
    body: body,
    public: data.public,
    tags: data.tags,
    address: data.address,
  };
}
```

`getObsidianEntries` lists everything in the `Content` folder:

```tsx
export async function getObsidianEntries() {
  const {
    data: {
      repository: {
        object: { entries },
      },
    },
  } = await fetchFromGitHubGraphQL(
    `
      query fetchEntries($owner: String!, $name: String!) {
        repository(owner: $owner, name: $name) {
          object(expression: "HEAD:Content/") {
            ... on Tree {
              entries {
                name
                object {
                  ... on Blob {
                    text
                  }
                }
              }
            }
          }
        }
      }
    `,
    {
      owner: `GITHUB_USERNAME`,
      name: `REPO_NAME`,
      first: 100,
    }
  );

  if (entries.errors) {
    console.error("GraphQL Error:", entries.errors);
    return [];
  }

  if (!entries) {
    console.error("No data returned from the GraphQL query.");
    return [];
  }

  return Promise.all(
    entries.map((entry: { object: { text: any } }) => {
      const content = entry.object.text;
      return parseMarkdownContent(content);
    })
  );
}
```

Errors get logged and the function returns an empty array. Otherwise each entry gets parsed and returned.

`getObsidianEntry` pulls a single file by slug:

```tsx
export async function getObsidianEntry(slug: string) {
  const { data } = await fetchFromGitHubGraphQL(
    `
      query fetchSingleEntry($owner: String!, $name: String!, $entryName: String!) {
        repository(owner: $owner, name: $name) {
          object(expression: $entryName) {
            ... on Blob {
              text
            }
          }
        }
      }
    `,
    {
      owner: `GITHUB_USERNAME`,
      name: `REPO_NAME`,
      entryName: `HEAD:Content/${slug}.md`,
    }
  );

  const text = data.repository.object.text;
  return parseMarkdownContent(text);
}
```

## File names

Every file is a millisecond timestamp, Zettelkasten-style:

`1671418753342.md`

Unique, chronologically sorted, and the name itself is the slug.

## The whole pipeline

Write in Obsidian. Obsidian syncs to GitHub. Next.js queries GitHub's GraphQL API and renders. That's it.

---
title: "Sourdough"
section: posts
description: "Growing up near San Francisco, I enjoyed quality sourdough."
tags: ["sourdough", "bread", "baking", "starter", "fermentation", "life"]
created: 2023-11-02T12:14:00Z
updated: 2026-06-02T17:51:01Z
cid: bafkreieyrj5tjfetu4rng4qgcuver3cnvykbwhixxbb2ntksrinc32hqjy
html: https://iammatthias.com/posts/1587970800000-sourdough
---

# Sourdough

I grew up on sourdough. Living near San Francisco meant a reliable supply of sourdough was always available. Boules, baguettes, batards, the occasional sandwich loaf. We had it all. And it was delicious.

Two months after our daughter was born I started developing my own sourdough starter. It was a process, rife with trial and error. But with a little persistence it paid off.

If you're interested in going down this road yourself, check out [The Perfect Loaf](https://www.theperfectloaf.com/7-easy-steps-making-incredible-sourdough-starter-scratch/).

After seven days of work, and one sub-par early attempt at a loaf, I had my starter. And I named it Fred.

Fred has now been with us since February 25th, 2020, and with luck will be with us for a long while. We've baked a lot of bread already, and a few fun little projects here and there like cinnamon buns and babkas. Here is a collection of shots from along the way.

---
title: "Deploy html on Replit"
section: posts
description: "Replit is great but lacks HTML deployment."
tags: ["web-development", "deployment", "replit", "surge", "html", "static-site-hosting"]
created: 2023-09-07T13:15:00Z
updated: 2025-05-24T16:58:00Z
cid: bafkreigqq72np7u6bzbibxmayte5tzshwu7xvspmgywlmilrbrcbntk2wi
html: https://iammatthias.com/posts/1691436904927-deploy-html-on-replit
---

# Deploy html on Replit

[Replit](https://replit.com/) is an amazing tool. Big fan. But currently, you can't deploy a simple HTML project, which is just baffling. Let me throw my `index.html` into the void!

There's an easy solution, though. Use Replit's `Shell` tool and run `npx surge`. You'll need to select a node version, and then set up an account with [Surge.sh](https://surge.sh) which will let you manage your deploys. Surge will assign you a random domain, but you can change it to whatever you want.

You can also run `echo your-domain.surge.sh > CNAME` to create a file at your project root that will tell Surge what domain to deploy under.

---
title: "Mint thyself"
section: posts
description: "A Zora HTML edition that embeds its own mint interface and can be minted from itself."
tags: ["zora", "html", "gpt", "ipfs", "blockchain", "web-development", "decentralization"]
created: 2023-06-28T13:00:00Z
updated: 2026-07-16T14:59:43Z
cid: bafkreiarq5c6jqnfsdjhsp6dohn6htbsfnyxex7564hl26cmbqbkfwqsn4
html: https://iammatthias.com/posts/1685948400001-mint-thyself
---

# Mint thyself

![](https://blobs.farfield.systems/blobs/bafkreibrws6qahimns3wzxm4wwmicdhayccq4wfqm73den75rrumnkp6yy)

Zora released HTML editions, which meant the artifact being minted could be a webpage rather than an image or video.

![](https://blobs.farfield.systems/blobs/bafkreiev6idod7sdpuxs4j46hwgpncenan7wogst5kti2thjlt55sil4gq)

NFTs were already polarizing, but the storage model was useful for this experiment. Zora called it `Mint internet`.

Zora stored the uploaded file on IPFS. External libraries failed, so any CSS or JavaScript had to travel inside the artifact.

Each edition also came with an optional iframe for embedding its mint interface elsewhere. An HTML edition could contain that iframe and point back at itself.

The contract address does not exist until the edition is minted, so the first HTML file could not include it. I minted a simple wrapper on Goerli, updated the metadata with a second copy containing the real contract address, and the iframe resolved.

`Zora, mint thyself` is an open edition that can be minted from itself. Pure Internet all the way down.

---
title: "Feels Like Summer"
section: posts
description: "A perfect Father's Day with my twins and three kids total. Summer has finally arrived and it feels just right."
tags: ["parenting", "life", "summer", "pictures"]
created: 2023-06-18T13:00:00Z
updated: 2025-05-24T16:58:00Z
cid: bafkreiese72j5twzqyqunlcftvfro3ynyequvcymmg3ikhfbsdy6rfefxy
html: https://iammatthias.com/posts/1687071600000-feels-like-summer
---

# Feels Like Summer

![](https://blobs.farfield.systems/blobs/bafkreigi72qeapeototid325m22xzww6kktaxmdx3mibe5gpkhmgizdipm)

First Father’s Day with my twins, first Father’s Day as a dad to three.

It’s been a perfect day.

And it finally feels like summer.

---
title: "AI-Legion on Bluesky"
section: posts
description: "I gave AI-Legion the Bluesky agent types and asked it to extend itself into the open social protocol."
tags: ["ai", "bluesky", "at-protocol", "ai-tools", "automation", "development", "social-media", "decentralization"]
created: 2023-04-11T13:00:00Z
updated: 2026-07-16T14:59:43Z
cid: bafkreibwbxnwrp4hl4vzrq2655wszvf27jrq55rlozozpbaoitz5skgkaa
html: https://iammatthias.com/posts/1681369200000-ai-legion-on-bluesky
---

# AI-Legion on Bluesky

![](https://blobs.farfield.systems/blobs/bafkreihqbisz74tydvpv66w4jo7buujbyguhdjvzpkqioygayxxsjhixwu)

In April 2023, AutoGPT, AI-Legion, and BabyAGI were appearing everywhere. At the same time, Mastodon, Farcaster, and Bluesky were pulling people away from Twitter. I wanted to see whether an agent framework could extend itself into one of those open networks.

The three agent projects had slightly different shapes:

- [AutoGPT](https://github.com/Torantulino/Auto-GPT) was a Python app that broke autonomous work into "thoughts," "reasoning," and "criticism" around a user-defined goal.
- [BabyAGI](https://github.com/yoheinakajima/babyagi) used OpenAI to create and execute tasks, with Pinecone carrying state between them.
- [AI-Legion](https://github.com/eumemic/ai-legion) was a pluggable TypeScript variant using multiple agents across simultaneous contexts.

![](https://blobs.farfield.systems/blobs/bafkreift2jpz2s3iyhpv3ipkr3b7qdmsal35bhk3yxrzfjzkclqh6bjzcy)

The Bluesky post above supplied the prompt. I wanted to know whether AI-Legion could add Bluesky support to itself.

I forked AI-Legion, let it make a few exploratory and quickly deleted posts, then gave it the `bsky-agent` types and asked it to modify `src/module/definitions/bluesky.ts`.

After a lot of trial and error, it could access a useful chunk of Bluesky:

- postMessage
- getTimeline
- likePost
- deleteLike
- follow
- deleteFollow
- repost
- deleteRepost
- mute
- unmute
- listNotifications
- countUnreadNotifications

![](https://blobs.farfield.systems/blobs/bafkreia55yyl4uwqfd3q3iibyy22va5iddld3bedoigkhmqagnebvi4jz4)

It worked, at the cost of a fairly stupid spike in my OpenAI bill.

![](https://blobs.farfield.systems/blobs/bafkreihtbfdddwxrx5jd4yuccnsb5rcex6skgnd2zvolqklevjfh6yrbhe)

The fork is at [github.com/iammatthias/ai-legion-bluesky](https://github.com/iammatthias/ai-legion-bluesky).

The timeline response was the real constraint. A mostly unfiltered response filled AI-Legion's context window quickly, so `getTimeline` needed a small limit and the agent needed explicit instructions about which fields belonged in memory. This was the prompt:

```
1. Use GPT-4 for search.
2. Post organically.
3. Be engaging, but do not spam.
4. Do not use emojis or hashtags.
5. Maintain a fluid and stream-of-consciousness vibe with engaging content posted on bluesky.
6. Enhance the human tone in engaging content posted on bluesky.
7. Only Agent 1 can post to bluesky.
8. You have access to the following bluesky actions: postMessage, getTimeline, likePost, deleteLike, follow, deleteFollow, repost, deleteRepost, mute, unmute, listNotifications, countUnreadNotifications.
9. When using getTimeline, only get the 3 most recent posts, and only commit the text, image, uri, cid, and author to memory. Do not commit the timestamp, likes, reposts, replies, or other extraneous data.
```

---
title: "Case Study: Building Tornado's Growth Engine"
section: posts
description: "Growth engineering at Tornado: analytics, experimentation, and personalization systems behind major gains in customers, revenue, and engagement."
tags: ["growth-engineering", "data-analytics", "marketing", "customer-engagement", "a-b-testing", "fintech", "scalability"]
created: 2023-02-01T09:31:00Z
updated: 2026-07-16T14:56:28Z
cid: bafkreibjmw3m6ljnv4bntjbxa2zogaa5fdvakop4mu7atgp4bqbwfru7um
html: https://iammatthias.com/posts/1675271943000-case-study-tornado
---

# Case Study: Building Tornado's Growth Engine

I was the Growth Engineer at [Tornado](https://tornado.com/). The job sat between marketing, product, and data: build the systems the growth team needed, then read the results.

## Data and analytics infrastructure

Built the pipelines and analytics layer the growth team ran on. Customer count grew 390%, revenue 800%, engagement rates 400%. CAC payback dropped to under 9 months. Paid ROAS up 1,732%. Deposits up 1,025%.

## A/B/N testing framework

Built in-house from scratch. Page conversion rates went up 200% once we could test features and design changes properly.

## Campaign personalization

Wrote propensity-score models for marketing personalization. Month-over-month in-app engagement and DAU climbed 60-110%. DAU/MAU ratio doubled. 3-month retention up 745%.

## Learn & Earn

Led the technical build. App DAU/MAU pushed past 30%, and the feature delivered over a million educational lessons.

## Scaling and efficiency

Trading volume grew 1,400% while behavior shifted toward ETFs, ESGs, and diversified portfolios. Deposit CAC dropped 80%+. User base up 150%, engagement up 1,000%. Monthly spend scaled to $1M with 1-year payback efficiency intact. Overall CAC fell 90%.

The analytics infrastructure won Amplitude's 2022 Pioneer Award for Product Innovation.

---
title: "The Sovereign Feed"
section: posts
description: "RSS is still a direct, portable feed protocol after social platforms abandoned it for algorithmic distribution."
tags: ["rss", "content-distribution", "decentralization", "web-development", "tech-history", "cms", "syndication"]
created: 2023-01-02T10:07:00Z
updated: 2026-07-16T14:59:43Z
cid: bafkreia2lwb2gsbtoblixyloosborpbgdvndt6id47w2aezamrbjqna6hy
html: https://iammatthias.com/posts/1672682867112-the-sovereign-feed
---

# The Sovereign Feed

![](https://blobs.farfield.systems/blobs/bafkreiaxtlmg74r6jf3x4pco2wx4cgghy4s7gnzi76kngiglcybsbnlddy)


RSS is a 1999 standard that still gives readers direct control over their feeds while social platforms route attention through algorithms. The protocol is just XML:

```
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
    <title>The Sovereign Feed</title>
    <description>Thoughts on decentralized content distribution</description>
    <link>https://example.com/blog</link>
    <lastBuildDate>Mon, 05 Feb 2025 09:30:00 GMT</lastBuildDate>

    <item>
        <title>Taking Back Control of Your Content</title>
        <description>In an era of algorithmic feeds and data harvesting, RSS offers a path to content sovereignty...</description>
        <link>https://example.com/blog/content-control</link>
        <pubDate>Mon, 05 Feb 2025 09:00:00 GMT</pubDate>
        <guid>https://example.com/blog/content-control</guid>
    </item>
</channel>
</rss>
```

Readers subscribe and pull updates on their own schedule, without targeting or an engagement loop in the middle. Facebook and Twitter offered RSS in their early days, then removed it as their distribution models changed. RSS kept running underneath podcasts, read-later apps, and the smaller collection of tools that still treat feeds as user-owned infrastructure.

Each feed is decentralized (anyone can publish from any server), standard (the XML is well-documented), portable (subscriptions move between readers), private (no behavior tracking), and direct (publisher to reader, no intermediary).

Arweave, IPFS, Mirror, and Farcaster reach for similar properties with newer infrastructure. The protocol was never the missing piece. Readers still need tools that make direct distribution worth choosing.

## Resources

- [Jekyll Starter Kit](https://github.com/thecreation/jekyll-starter-kit) - A boilerplate for creating Jekyll sites with RSS support
- [Adding RSS to Jekyll](https://vilcins.medium.com/how-to-add-an-rss-feed-to-a-jekyll-blog-5b43a2801123) - Detailed guide for implementing RSS in Jekyll blogs
- [Gatsby RSS Documentation](https://www.gatsbyjs.com/docs/how-to/adding-common-features/adding-an-rss-feed/) - Official guide for adding RSS feeds to Gatsby sites
- [RSS in Next.js](https://blog.logrocket.com/adding-rss-feed-next-js-app/) - Tutorial for implementing RSS in Next.js applications
- [Feed NPM Package](https://www.npmjs.com/package/feed) - Popular Node.js package for generating RSS feeds
- [RSS on Wikipedia](https://en.wikipedia.org/wiki/RSS) - Comprehensive overview of RSS technology and history

---
title: "Obsidian as a CMS"
section: posts
description: "Obsidian as a CMS using structured frontmatter, GitHub sync, Cloudflare R2 for assets, and Next.js for rendering."
tags: ["obsidian", "github", "cms", "nextjs", "cloudflared", "graphql", "decentralization"]
created: 2022-12-10T08:55:00Z
updated: 2026-07-16T14:56:28Z
cid: bafkreie24fzdijtqebcnyo7voq6kadp7lkpqvs5ind5yugnbkuij74howe
html: https://iammatthias.com/posts/1670659200001-obsidian-as-a-cms
---

# Obsidian as a CMS

I shared a concept on Farcaster that picked up some interest, so here are the details. It's a decentralized, git-based "CMS" that lets me edit content from anywhere.

The Obsidian template has structured YAML frontmatter. File names follow a Zettelkasten-inspired millisecond timestamp, paired with a `created` timestamp and the initial post title.

```yaml
---
title: "1671418753342"
created: "1671418753342"
longform: false
published: false
---
```

The timestamp-as-title means most posts live as journal entries. Descriptive titles are optional.

Obsidian syncs to a private GitHub repo on every save. A `published` flag in the frontmatter gates public display.

On push, a GitHub Action uploads anything in the assets folder to a Cloudflare R2 bucket (S3-compatible, generous free tier).

```yaml
name: Cloudflare
on:
  push:
    branches:
      - main
  workflow_dispatch: null
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: R2 Directory Upload
        uses: willfurstenau/r2-dir-upload@main
        with:
          accountid: "${{ secrets.CF_ACCOUNT_ID }}"
          accesskeyid: "${{ secrets.CF_ACCESS_KEY }}"
          secretaccesskey: "${{ secrets.CF_SECRET_KEY }}"
          bucket: iam-bucket
          source: "${{ github.workspace }}/Assets"
          destination: /
```

NextJS handles the frontend. Two queries do the work: `getObsidianEntries` and `getObsidianEntry`.

`getObsidianEntries` pulls every entry from the repo:

```tsx
export default async function getObsidianEntries() {
  const token = process.env.NEXT_PUBLIC_GITHUB;

  const {
    data: {
      repository: {
        object: { entries },
      },
    },
  } = await fetch(`https://api.github.com/graphql`, {
    method: `POST`,
    headers: {
      "Content-Type": `application/json`,
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({
      query: `
      query fetchEntries($owner: String!, $name: String!) {
        repository(owner: $owner, name: $name) {
          object(expression: "HEAD:Content/") {
            ... on Tree {
              entries {
                name
                object {
                  ... on Blob {
                    text
                  }
                }
              }
            }
          }
        }
      }  
            `,
      variables: {
        owner: `GITHUB_USERNAME`,
        name: `REPO_NAME`,
        first: 100,
      },
    }),
    next: {
      revalidate: 1 * 30,
    },
  }).then((res) => res.json());

  return entries;
}
```

Private repos need an access token (see [Authenticating with GraphQL](https://docs.github.com/en/graphql/guides/forming-calls-with-graphql#authenticating-with-graphql)). The query targets the `Content` directory on `main`. Next13's revalidation flag keeps things fresh.

I parse frontmatter with `gray-matter` and render Markdown with `react-remark`, part of the Unified/Remark ecosystem.

`getObsidianEntry` powers individual post pages:

```tsx
export default async function getObsidianEntry(slug: any) {
  const paths = await getObsidianEntries();

  const _paths = await Promise.all(paths);

  const entry = _paths.find((entry: any) => entry.slug === slug);

  return entry;
}
```

A direct GraphQL filter for a single entry would be cleaner, but I haven't gotten it to work yet. For now, this filters by slug (Zettelkasten tag) against the full set.

That's the whole setup.

---
title: "Eggnog"
section: recipes
description: "A creamy, festive eggnog recipe with cooked custard base, whipped egg whites, vanilla, and spirits, topped with whipped cream and nutmeg."
tags: ["eggnog", "holiday", "dessert", "drinks", "rum", "brandy", "nutmeg", "custard"]
created: 2022-12-04T12:00:00Z
updated: 2026-07-29T21:20:26Z
cid: bafkreibq7s7zocwtupypmdzgcuq6grzazxdwemk46v4atugfa4ikaa3fri
html: https://iammatthias.com/recipes/1670140800000-eggnog
---

# Eggnog

```recipe
yield: "2 servings"
time: "15 min active · 4 h total"

ingredients:
  - { id: sugar, item: "sugar", amount: "40 ml", note: "divided" }
  - { id: egg-yolks, item: "egg yolks", amount: "⅔" }
  - { id: salt, item: "salt", amount: "a pinch" }
  - { id: milk, item: "milk", amount: "1⅓ cups" }
  - { id: egg-whites, item: "egg whites", amount: "⅔" }
  - { id: brandy-or-rum-flavoring, item: "brandy or rum flavoring", amount: "to taste" }
  - { id: whipping-cream, item: "whipping cream", amount: "40 ml", note: "whipped" }
  - { id: ground-nutmeg, item: "ground nutmeg", amount: "as needed", note: "for garnish" }
  - { id: vanilla-extract, item: "vanilla extract", amount: "⅓ tsp" }

steps:
  - id: s1
    in: [sugar, egg-yolks, salt, milk, vanilla-extract]
    do: "beat 1¾ tbsp sugar"
    for: "3 min"
    detail: "Beat 1¾ tbsp sugar into egg yolks. Add salt and stir in milk. Cook over medium heat, stirring constantly, until mixture coats the spoon. Cool."
  - id: s2
    in: [s1, egg-whites]
    do: "beat egg whites"
    for: "3 min"
    detail: "Beat egg whites until foamy. Gradually add 1 tablespoon sugar, beating to soft peaks. Add to the cooled custard and mix thoroughly."
  - id: s3
    in: [s2, brandy-or-rum-flavoring]
    do: "add vanilla and brandy"
    detail: "Add vanilla and brandy or rum flavoring to taste."
  - id: s4
    in: [s3]
    do: "chill for 3-4 hours"
    for: "3–4 h"
    detail: "Chill for 3-4 hours."
  - id: s5
    in: [s4, whipping-cream, ground-nutmeg]
    do: "pour into a punch bowl"
    detail: "Pour into a punch bowl or cups. Dot with islands of whipped cream and sprinkle with nutmeg."
```

---
title: "Saag (Palak) Paneer"
section: recipes
description: "Puréed spinach with paneer, ginger, green chili, tomato, and cream, made in an Instant Pot."
tags: ["indian", "spinach", "paneer", "vegetarian", "curry", "dinner", "spicy"]
created: 2021-12-24T12:54:00Z
updated: 2026-07-29T21:06:16Z
cid: bafkreieim7wo5glbstzanj2sv5qlgvp7kmmogqbclbvuoai5wrhcxz7ioe
html: https://iammatthias.com/recipes/1640442840000-saag-palak-paneer
---

# Saag (Palak) Paneer

```recipe
yield: "2 servings"
time: "20 min active · 40 min total"

ingredients:
  - { id: spinach, item: "spinach", amount: "150 g", note: "about half a large bunch, stems trimmed" }
  - { id: ginger, item: "ginger", amount: "½ inch piece", note: "chopped" }
  - { id: green-chili, item: "green chili", amount: "½-1", note: "Serrano peppers, chopped" }
  - { id: salt, item: "salt", amount: "to taste" }
  - { id: ghee-or-avocado-oil, item: "ghee or avocado oil", amount: "to sauté" }
  - { id: onion, item: "onion", amount: "½ medium", note: "diced" }
  - { id: garlic, item: "garlic", amount: "½ tablespoon", note: "minced" }
  - { id: ginger-ginger-paste, item: "ginger/ginger paste", amount: "½ tablespoon", note: "minced" }
  - { id: tomato, item: "tomato", amount: "½ medium", note: "diced" }
  - { id: red-chili-powder, item: "red chili powder", amount: "½ teaspoon" }
  - { id: cream-or-milk, item: "cream or milk", amount: "⅛ cup" }
  - { id: paneer, item: "paneer", amount: "125g", note: "cubed" }

steps:
  - id: s1
    in: [spinach, ginger, green-chili]
    do: "wash the roughly chopped"
    detail: "Wash the roughly chopped spinach thoroughly and drain. Also wash the chopped ginger and green chili."
  - id: s2
    in: [s1, salt]
    do: "place spinach"
    for: "6 min"
    detail: "Place spinach, ginger, green chili, and a pinch of salt in an Instant Pot. Set to high pressure for 6 minutes."
  - id: s3
    in: [s2]
    do: "release pressure and let"
    for: "10 min"
    detail: "Once done, release pressure and let the spinach mixture cool slightly. Then purée in a food processor until smooth."
  - id: s4
    in: [s3, ghee-or-avocado-oil, onion, garlic, ginger-ginger-paste]
    do: "set the Instant Pot"
    detail: "Set the Instant Pot to sauté mode and heat ghee or avocado oil. Add diced onion, minced garlic, and ginger paste. Cook until onions are translucent."
  - id: s5
    in: [s4, tomato, red-chili-powder]
    do: "add diced tomato"
    detail: "Add diced tomato, cook until soft. Then add salt and red chili powder to taste."
  - id: s6
    in: [s5, cream-or-milk, paneer]
    do: "stir in cream or milk"
    detail: "Stir in cream or milk, then add cubed paneer to the pot. Mix gently to combine."
  - id: s7
    in: [s6]
    do: "add the puréed spinach"
    detail: "Add the puréed spinach mixture to the pot with paneer. Stir well."
  - id: s8
    in: [s7]
    do: "let the mixture simmer"
    for: "10–20 min"
    detail: "Let the mixture simmer for 10-20 minutes. Cover with a splash guard or lid to prevent splattering."
  - id: s9
    in: [s8]
    do: "adjust seasoning"
    detail: "Adjust seasoning as needed and serve hot. Serve with flatbread or rice."
```

---
title: "Case study: Aspiration's CRM platform"
section: posts
description: "Two years running CRM and lifecycle at Aspiration. A platform migration, first-party analytics, and campaigns that lifted lead conversion 53%."
tags: ["marketing", "email-marketing", "analytics", "sustainability", "platform-migration", "data-visualization", "customer-engagement"]
created: 2021-07-01T09:19:00Z
updated: 2026-06-02T17:51:01Z
cid: bafkreihftrgpnmtjeq2lsr63g6vjbxba22xylsdpxqizvswfegov4qht4q
html: https://iammatthias.com/posts/1625156340000-case-study-aspiration
---

# Case study: Aspiration's CRM platform

I ran CRM and lifecycle marketing at [Aspiration](https://aspiration.com/) from 2019 to 2021. The job was a platform migration, a rebuild of how we tracked campaign data, and a move to first-party analytics, all pointed at a customer base that cared where their money went.

## Off Intercom, onto Iterable

We moved from Intercom to [Iterable](https://iterable.com/). Iterable gave us real control over template design and a cleaner read on clicks and conversions. Campaigns got more personal and faster to ship.

Over two years the team ran more than 1,500 campaigns and sent over 60 million emails. Open rates averaged 40.9%, with open-to-click at 2.1%. The work spanned channels: email, push notifications, SMS, and in-app modals.

## The template system

In 2019 I designed and built a modular email template system on [Iterable's Snippets](https://support.iterable.com/hc/en-us/articles/4414807441556-Introduction-to-Snippets). Reusable layouts and components meant a new campaign started from working parts instead of a blank file. It's still in use today.

## Personalization

We brought in [Send Time Optimization](https://support.iterable.com/hc/en-us/articles/360050923471-Send-Time-Optimization), [Segment](https://segment.com), and [Movable Ink](https://movableink.com/). Machine-learning models lifted open rates, click rates, and conversions by over 100%, and inbox placement reached 99.7%. Automations sent each recipient content matched to how engaged they already were.

## The numbers

Lead conversion climbed 53%, adding over $15M in value. I built and led a 7-person CRM team and ran multi-channel production across email, push, SMS, and in-app. Dozens of A/B and multivariable tests raised conversions about 30% on average.

Picking the ESP/CRM took reviewing 20+ vendors and 70+ specs. Abandoned projects fell after I pulled twenty-plus stakeholders and project managers into one room to sort priorities, and completion rates and times improved by more than 50%. Data accuracy went up 20% by working as the liaison between data engineering and our vendors, building new events, traits, and personas to feed personalized content.

[Iterable wrote up the work](https://iterable.com/customers/aspiration/) on their site.

---
title: "Yorkshire Pudding"
section: recipes
description: "A simple Yorkshire pudding recipe with just 6 ingredients."
tags: ["baking", "british", "yorkshire-pudding", "egg", "butter", "sour", "side-dish"]
created: 2020-10-12T15:51:00Z
updated: 2026-07-29T21:06:15Z
cid: bafkreiepzs7duiueelguqwoaltiutze2cw52ohzy4dvee64tbvb3m2fpva
html: https://iammatthias.com/recipes/1602523860000-yorkshire-pudding
---

# Yorkshire Pudding

```recipe
yield: "6 puddings (2–3 servings)"
time: "10 min active · 35 min total"

ingredients:
  - { id: butter, item: "butter", amount: "as needed", note: "plus extra for pans" }
  - { id: flour, item: "flour", amount: "½ cup" }
  - { id: milk, item: "milk", amount: "¼ cup" }
  - { id: water, item: "water", amount: "¼ cup" }
  - { id: eggs, item: "eggs", amount: "1 to 3", note: "recipe adaptable" }
  - { id: salt, item: "salt", amount: "to taste" }

steps:
  - id: s1
    prep: true
    do: "preheat the oven"
    detail: "Preheat the oven to 400°F (200°C), using convection mode if possible."
  - id: s2
    in: [butter]
    do: "place a small amount"
    detail: "Place a small amount of butter (1/4 to 1/2 tsp per muffin well or 2 tsp for a baking dish) into your baking vessel. Put it in the oven to melt the butter as it heats."
  - id: s3
    in: [s2, flour, milk, water, eggs, salt]
    do: "combine flour"
    for: "10 min"
    detail: "In a stand mixer, combine flour, milk, water, eggs, and salt. Mix on HIGH, pausing once to scrape down the sides, then continue until fully mixed."
  - id: s4
    in: [s3]
    do: "remove the pan and swirl"
    detail: "Once the butter in the oven starts to brown, remove the pan and swirl to coat the surface."
  - id: s5
    in: [s4]
    do: "pour the batter"
    for: "20–25 min"
    detail: "Pour the batter immediately into the hot pan or muffin tin."
  - id: s6
    in: [s5]
    do: "place the dish or tin"
    for: "5 min"
    detail: "Place the dish or tin back into the oven without delay."
  - id: s7
    in: [s6]
    do: "bake for approximately"
    for: "20 min"
    detail: "Bake for approximately 20 minutes, starting to check for doneness at 18 minutes."
  - id: s8
    in: [s7]
    do: "let cool slightly"
    for: "5 min"
    detail: "After baking, let cool slightly then serve. They are also good the following day. Note: Yorkshire pudding is best served with roast beef and gravy, but they are versatile enough to accompany many dishes or be enjoyed on their own."
```

---
title: "Crab Cakes"
section: recipes
description: "Classic crab cakes made with canned crab, eggs, mayo and seasonings. Form into patties and sauté in butter until golden brown on both sides."
tags: ["seafood", "craft", "dill", "cajun", "dinner"]
created: 2020-10-12T15:49:00Z
updated: 2026-07-29T21:06:15Z
cid: bafkreifv7rz7aeu4bhatzdgpbtgxltpyl2b6es7gtzalsxvpvdiztafuyi
html: https://iammatthias.com/recipes/1602523740000-crab-cakes
---

# Crab Cakes

```recipe
yield: "4 cakes (2 servings)"
time: "20 min active · 50 min total"

ingredients:
  - { id: crab, item: "crab", amount: "225 g (8 oz)", note: "lump crab, drained — half a 16 oz can" }
  - { id: eggs, item: "eggs", amount: "1" }
  - { id: mayonnaise, item: "mayonnaise", amount: "½ tablespoon" }
  - { id: dijon-mustard, item: "Dijon mustard", amount: "½ teaspoon" }
  - { id: dill, item: "dill", amount: "½ pinch", note: "fresh or dried" }
  - { id: butter, item: "butter", amount: "for sautéing" }

steps:
  - id: s1
    in: [crab, eggs, mayonnaise, dijon-mustard, dill]
    do: "gently mix crab"
    detail: "In a large bowl, gently mix crab, eggs, mayonnaise, Dijon mustard, and dill with a fork, taking care not to break up the crab too much. Form the mixture into patties."
  - id: s2
    in: [s1, butter]
    do: "heat a skillet"
    for: "3–5 min"
    detail: "Heat a skillet over medium heat and melt a sufficient amount of butter. Sauté the crab cakes until they are golden brown and fully firm, about 3-5 minutes per side."
  - id: s3
    in: [s2]
    do: "for variations"
    for: "30 min"
    detail: "Optional: For variations, you may add a sprinkle of Cajun spice to the crab mixture for a different flavor, but be mindful of the additional salt."
  - id: s4
    in: [s3]
    do: "while some recipes might"
    for: "6–8 min"
    detail: "Note: While some recipes might suggest adding diced peppers, this version stays true to simplicity, focusing on the crab's flavor. Serve the crab cakes with tartar sauce or a squeeze of lemon."
```

---
title: "Beef Stroganoff"
section: recipes
description: "Tri-tip and mushrooms simmered in red wine, finished with sour cream, and served over egg noodles."
tags: ["beef", "stroganoff", "red-wine", "mushrooms", "egg-noodles", "russian", "dinner"]
created: 2020-10-12T15:48:00Z
updated: 2026-07-29T21:06:15Z
cid: bafkreif36jcar6oz7feqftxutw4mzo5tx2rw4pmnjp47xiisohhsqu2hsm
html: https://iammatthias.com/recipes/1602523680000-beef-stroganoff
---

# Beef Stroganoff

```recipe
yield: "2 servings"
time: "20 min active · 1 h 15 total"

ingredients:
  - { id: garlic, item: "garlic", amount: "1 clove", note: "minced" }
  - { id: onions, item: "onions", amount: "½ large", note: "finely chopped" }
  - { id: mushrooms, item: "mushrooms", amount: "1 cup", note: "sliced" }
  - { id: butter, item: "butter", amount: "1 tablespoon" }
  - { id: tri-tip, item: "tri-tip", amount: "½ lb", note: "cubed" }
  - { id: salt, item: "salt", amount: "to taste" }
  - { id: pepper, item: "pepper", amount: "to taste" }
  - { id: nutmeg, item: "nutmeg", amount: "⅛ tsp", note: "grated" }
  - { id: robust-red-wine, item: "robust red wine", amount: "½ cup", note: "cab, merlot, syrah" }
  - { id: egg-noodles, item: "egg noodles", amount: "4 oz" }
  - { id: half-and-half, item: "half and half", amount: "¼ cup" }
  - { id: sour-cream, item: "sour cream", amount: "½ cup" }

steps:
  - id: s1
    in: [garlic, onions, mushrooms, butter, tri-tip, salt, pepper, nutmeg]
    do: "sauté garlic"
    for: "8 min"
    detail: "In a large skillet, sauté garlic, onions, and mushrooms in butter over medium heat. Add the cubed tri-tip and sprinkle with salt, pepper, and nutmeg."
  - id: s2
    in: [s1, robust-red-wine]
    do: "pour in the red wine"
    detail: "Once the meat is browned on one side, pour in the red wine. A robust wine like cabernet, merlot, or syrah is recommended."
  - id: s3
    in: [s2]
    do: "reduce heat to low"
    for: "45–60 min"
    detail: "Reduce heat to low and simmer until the meat is tender and falling apart. Then turn off the heat."
  - id: s4
    in: [s3, egg-noodles]
    do: "meanwhile"
    for: "8–10 min"
    detail: "Meanwhile, bring a large pot of salted water to a boil. Add the egg noodles and cook according to package instructions."
  - id: s5
    in: [s4, half-and-half]
    do: "return to the meat"
    detail: "Return to the meat mixture, now slightly cooled, and stir in the half and half."
  - id: s6
    in: [s5, sour-cream]
    do: "stir in the sour cream"
    detail: "Stir in the sour cream. Adjust salt, pepper, and nutmeg to taste, now that the cream has been added."
  - id: s7
    in: [s6]
    do: "replace the lid"
    for: "5 min"
    detail: "Replace the lid on the skillet and turn the heat back to low. Ensure the sauce is warm but not boiling."
  - id: s8
    in: [s7]
    do: "drain the noodles"
    detail: "Drain the noodles thoroughly."
  - id: s9
    in: [s8]
    do: "serve the beef"
    detail: "Serve the beef stroganoff over the cooked egg noodles."
```

---
title: "Goulash, two ways"
section: recipes
description: "Hearty goulash with roasted vegetables in paprika broth, available in both vegetarian and meat versions with rotisserie chuck roast."
tags: ["goulash", "soup", "stew", "meat", "vegetarian", "grilled", "rotisserie", "chuck-roast", "paprika", "hungarian", "slow-cooking"]
created: 2020-10-12T15:46:00Z
updated: 2026-07-29T21:20:26Z
cid: bafkreigzuvv6xy27ezz55in7udkme3ga5jdbbnervfxbrk3hzxu2uhij3y
html: https://iammatthias.com/recipes/1602522360000-goulash
---

# Goulash, two ways

```recipe
yield: "2 servings of each"
time: "45 min active · 3 h total"

ingredients:
  - { id: roasted-bell-peppers, item: "roasted bell peppers", amount: "⅔ large", note: "grilled, peeled, and diced", group: "Base Stock" }
  - { id: fresno-chili, item: "Fresno chili", amount: "⅓", note: "grilled, peeled, and diced", group: "Base Stock" }
  - { id: roasted-garlic, item: "roasted garlic", amount: "1–2 cloves", note: "grilled, peeled, and mashed", group: "Base Stock" }
  - { id: roasted-shallot, item: "roasted shallot", amount: "⅓ large", note: "grilled, peeled, and diced", group: "Base Stock" }
  - { id: black-pepper, item: "black pepper", amount: "to taste", group: "Base Stock" }
  - { id: chuck-roast, item: "chuck roast", amount: "⅓ lb", note: "rotisserie roasted" }
  - { id: salt-and-pepper, item: "salt and pepper", amount: "to taste", note: "seasoning for rotisserie" }
  - { id: butter, item: "butter", amount: "⅔ tbsp", group: "Base Stock" }
  - { id: onion, item: "onion", amount: "⅓ large", note: "diced", group: "Base Stock" }
  - { id: carrots, item: "carrots", amount: "1 medium", note: "diced", group: "Base Stock" }
  - { id: sweet-paprika, item: "sweet paprika", amount: "⅔ tbsp", group: "Base Stock" }
  - { id: smoked-paprika, item: "smoked paprika", amount: "⅓ tbsp", group: "Base Stock" }
  - { id: ground-cumin, item: "ground cumin", amount: "⅓ tsp", group: "Base Stock" }
  - { id: ground-caraway-seeds, item: "ground caraway seeds", amount: "⅓ tsp", note: "optional", group: "Base Stock" }
  - { id: vegetable-broth, item: "vegetable broth", amount: "2 cups", note: "or water with bouillon", group: "Base Stock" }
  - { id: bay-leaves, item: "bay leaves", amount: "⅔", group: "Base Stock" }
  - { id: salt, item: "salt", amount: "to taste", group: "Base Stock" }
  - { id: potatoes-2, item: "potatoes", amount: "1 medium", note: "diced" }
  - { id: red-wine, item: "red wine", amount: "40 ml", note: "optional" }
  - { id: sour-cream, item: "sour cream", amount: "40 ml", note: "for garnish" }
  - { id: potatoes, item: "potatoes", amount: "1 medium", note: "diced" }
  - { id: zucchini, item: "zucchini", amount: "⅓ large", note: "diced" }
  - { id: mushrooms, item: "mushrooms", amount: "⅔ cups", note: "sliced" }
  - { id: green-beans, item: "green beans", amount: "⅓ cup", note: "trimmed" }

steps:
  - id: s1
    prep: true
    do: "season the chuck roast"
    detail: "Season the chuck roast generously with salt and pepper."
    phase: "Prep"
  - id: s2
    prep: true
    do: "dice the onion"
    detail: "Prepare the vegetables: dice the onion, carrots, and potatoes; trim the green beans; slice the mushrooms and zucchini."
    phase: "Prep"
  - id: v1_1
    in: [roasted-bell-peppers, fresno-chili, roasted-garlic, roasted-shallot, black-pepper, chuck-roast, salt-and-pepper]
    do: "place a cast iron pan"
    detail: "Preheat your charcoal grill and set up the rotisserie. Place a cast iron pan underneath the roast to catch the drippings. Place the garlic, shallot, bell peppers, and Fresno chili on the grate above the rotisserie so they roast while the chuck turns. Cook the chuck roast on the rotisserie until it reaches an internal temperature of about 130-135°F for medium-rare, or longer for a more well-done roast."
    phase: "Meat Goulash (with Rotisserie Chuck Roast)"
  - id: v1_2
    in: [v1_1]
    do: "grill the garlic"
    for: "15–20 min"
    detail: "Grill the garlic, shallot, and peppers until charred and soft. Remove them from the grill, peel, and set aside. Mash the roasted garlic cloves. Dice the roasted shallot. Peel and dice the roasted bell peppers and Fresno chili."
    phase: "Meat Goulash (with Rotisserie Chuck Roast)"
  - id: v1_3
    in: [v1_2]
    do: "let it rest for at least"
    for: "10 min"
    detail: "Once the roast is done, let it rest for at least 10 minutes, then cut into bite-sized pieces. Reserve the drippings from the cast iron pan."
    phase: "Meat Goulash (with Rotisserie Chuck Roast)"
  - id: v1_4
    in: [v1_3, butter, onion, carrots, sweet-paprika, smoked-paprika, ground-cumin, ground-caraway-seeds]
    do: "heat 2 tsp"
    for: "5–7 min"
    detail: "Heat 2 tsp of butter in a large oven-safe pot or Dutch oven over medium heat. Add the diced onion and sauté for 5-7 minutes until softened and translucent. Add the diced carrots, roasted shallot, mashed roasted garlic, roasted bell peppers, and Fresno chili. Cook for another 5 minutes, stirring frequently. Stir in the sweet paprika, smoked paprika, cumin, and caraway seeds (if using). Cook for another minute to toast the spices."
    phase: "Meat Goulash (with Rotisserie Chuck Roast)"
  - id: v1_5
    in: [v1_4, vegetable-broth, bay-leaves, salt]
    do: "pour in the vegetable"
    for: "20–30 min"
    detail: "Pour in the vegetable broth, add bay leaves, and bring to a boil, then reduce to a simmer. Season with salt and pepper. Simmer the stock for 20-30 minutes to develop the flavors."
    phase: "Meat Goulash (with Rotisserie Chuck Roast)"
  - id: v1_6
    in: [v1_5, potatoes-2, red-wine]
    do: "keep the remaining half"
    for: "30–40 min"
    detail: "Keep the remaining half of the stock in the original pot. Add the rotisserie chuck roast pieces and the reserved drippings from the cast iron pan. Pour in red wine (optional) and bring to a simmer. Add the diced potatoes and cook for 30-40 minutes, or until the beef is tender and the potatoes are cooked through. Adjust seasoning with salt and pepper if needed."
    phase: "Meat Goulash (with Rotisserie Chuck Roast)"
  - id: v1_7
    in: [v1_6]
    do: "preheat the oven"
    for: "2 h"
    detail: "Preheat the oven to 275°F (135°C). Place the oven-safe pot with both goulashes (in separate pots or sections) in the oven. Cover the pot(s) and slow-cook in the oven for 1.5 to 2 hours, allowing the flavors to meld and the meat to become more tender. Remove from the oven and check seasoning, adjusting as needed."
    phase: "Meat Goulash (with Rotisserie Chuck Roast)"
  - id: v1_8
    in: [v1_7, sour-cream]
    do: "serve both goulash"
    detail: "Serve both goulash versions with crusty bread or over egg noodles, if desired. Garnish both versions with fresh parsley and a dollop of sour cream for the meat version."
    phase: "Meat Goulash (with Rotisserie Chuck Roast)"
    title: "Meat Goulash (with Rotisserie Chuck Roast)"
  - id: v2_1
    in: [roasted-bell-peppers, fresno-chili, roasted-garlic, roasted-shallot, black-pepper]
    do: "grill the garlic"
    detail: "Grill the garlic, shallot, and peppers until charred and soft. Remove them from the grill, peel, and set aside. Mash the roasted garlic cloves. Dice the roasted shallot. Peel and dice the roasted bell peppers and Fresno chili."
    phase: "Vegetarian Goulash"
  - id: v2_2
    in: [v2_1]
    do: "let it rest for at least"
    for: "15–20 min"
    detail: "Once the roast is done, let it rest for at least 10 minutes, then cut into bite-sized pieces. Reserve the drippings from the cast iron pan."
    phase: "Vegetarian Goulash"
  - id: v2_3
    in: [v2_2, butter, onion, carrots, sweet-paprika, smoked-paprika, ground-cumin, ground-caraway-seeds]
    do: "heat 2 tsp"
    for: "5–7 min"
    detail: "Heat 2 tsp of butter in a large oven-safe pot or Dutch oven over medium heat. Add the diced onion and sauté for 5-7 minutes until softened and translucent. Add the diced carrots, roasted shallot, mashed roasted garlic, roasted bell peppers, and Fresno chili. Cook for another 5 minutes, stirring frequently. Stir in the sweet paprika, smoked paprika, cumin, and caraway seeds (if using). Cook for another minute to toast the spices."
    phase: "Vegetarian Goulash"
  - id: v2_4
    in: [v2_3, vegetable-broth, bay-leaves, salt]
    do: "pour in the vegetable"
    for: "20–30 min"
    detail: "Pour in the vegetable broth, add bay leaves, and bring to a boil, then reduce to a simmer. Season with salt and pepper. Simmer the stock for 20-30 minutes to develop the flavors."
    phase: "Vegetarian Goulash"
  - id: v2_5
    in: [v2_4, potatoes, zucchini, mushrooms, green-beans]
    do: "ladle half of the stock"
    for: "20 min"
    detail: "In a separate pot, ladle half of the stock base. Add the diced potatoes, zucchini, mushrooms, and green beans to the vegetarian stock. Simmer for 20 minutes, or until the vegetables are tender. Adjust seasoning with salt and pepper if necessary."
    phase: "Vegetarian Goulash"
  - id: v2_6
    in: [v2_5]
    do: "preheat the oven"
    for: "2 h"
    detail: "Preheat the oven to 275°F (135°C). Place the oven-safe pot with both goulashes (in separate pots or sections) in the oven. Cover the pot(s) and slow-cook in the oven for 1.5 to 2 hours, allowing the flavors to meld and the meat to become more tender. Remove from the oven and check seasoning, adjusting as needed."
    phase: "Vegetarian Goulash"
    title: "Vegetarian Goulash"
```

---
title: "Challah"
section: recipes
description: "Traditional challah recipe with simple steps: mix yeast, water, flour, salt, honey, oil, eggs; knead, proof, braid, rise, brush with egg."
tags: ["bread", "baking", "challah", "jewish", "braiding", "egg", "honey"]
created: 2020-10-12T11:31:00Z
updated: 2026-07-29T21:06:14Z
cid: bafkreibnkwqm5llg6quhda7iydzaniateayec5qt22cpnbj4ikr44gbcfy
html: https://iammatthias.com/recipes/1602505860000-challah
---

# Challah

```recipe
yield: "1 loaf (serves 8–10)"
time: "35 min active · 3 h total"

ingredients:
  - { id: water, item: "water", amount: "1 cup", note: "room temperature" }
  - { id: yeast, item: "yeast", amount: "1 packet" }
  - { id: honey-or-sugar, item: "honey or sugar", amount: "¼ cup" }
  - { id: flour, item: "flour", amount: "4 cups" }
  - { id: salt, item: "salt", amount: "2 tsp" }
  - { id: eggs, item: "eggs", amount: "2 + 1 yolk" }
  - { id: oil, item: "oil", amount: "¼ cup", note: "canola or coconut" }

steps:
  - id: s1
    in: [water, yeast, honey-or-sugar]
    do: "1 cup water"
    for: "5–10 min"
    detail: "In a bowl of a stand mixer, or just a bowl: 1 cup water, 1 packet of yeast (or about 2.5 tsp) and a bit of sugar. Let sit until foamy."
  - id: s2
    in: [s1, flour, salt, eggs, oil]
    do: "mix flour and salt"
    for: "5 min"
    detail: "In a separate bowl, mix flour and salt. Once yeast and water is foamy, mix in flour + salt, honey (or sugar), oil, and eggs."
  - id: s3
    in: [s2]
    do: "mix / knead"
    for: "1 h"
    detail: "Mix / knead for 8-10 minutes. Let proof for 1 hour."
  - id: s4
    in: [s3]
    do: "punch down and divide"
    detail: "After proofing, punch down and divide into 4 equal pieces. Roll the pieces into ropes. Braid - traditionally challah is round in the fall around Rosh Hashanah, but braiding a normal loaf is fine."
  - id: s5
    in: [s4]
    do: "let braided loaf rise"
    for: "30–45 min"
    detail: "Let braided loaf rise for 30-45 minutes. Coat loaf in an egg wash: egg, water, and honey."
  - id: s6
    in: [s5]
    do: "bake for 30-35 minutes"
    for: "30–35 min"
    detail: "Bake for 30-35 minutes at 350°F. Enjoy."
```

---
title: "Flatframe"
section: posts
description: "Aerial photography technique capturing motion in a static frame (like waves or crowds) using stationary drone footage between 10-60 seconds."
tags: ["drone", "photography", "flatframe", "aerial", "technique", "mobile-photography", "creative-process"]
created: 2020-04-27T11:27:00Z
updated: 2026-06-02T17:51:00Z
cid: bafkreibx6s4766terdtxm737socfvvhnqniaeo524sprd36cjjfw6bmydi
html: https://iammatthias.com/posts/1563778800000-flatframe
---

# Flatframe

![](https://blobs.farfield.systems/blobs/bafkreiaxhi7wi2tjw75xo5a5olh3rdnqhjz6c7rwlsaymbquivtbczdxpe)

Flatframe is a technique I started exploring shortly after becoming comfortable with flying my drone. It starts with a snippet of 4K footage.

For best results, keep the drone and its camera stationary, allowing the subject's motion to be captured within the frame. Waves crashing against the shore, grass swaying in the wind, or the bustling activity of a crowded plaza. Any scene with dynamic movement makes for an excellent Flatframe subject.

Through experimentation, I've found that clips ranging from 10 to 60 seconds tend to yield the most striking final images.
