<?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: Dennison Bertram</title>
    <description>The latest articles on DEV Community by Dennison Bertram (@dennisonbertram).</description>
    <link>https://dev.to/dennisonbertram</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%2F3572813%2Facf865c6-793b-455a-91db-cfc769f1cc36.png</url>
      <title>DEV Community: Dennison Bertram</title>
      <link>https://dev.to/dennisonbertram</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dennisonbertram"/>
    <language>en</language>
    <item>
      <title>My AI Agent Deploys Code to My Server. Here's What Keeps It in a Box.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Sat, 17 Oct 2026 14:21:16 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/my-ai-agent-deploys-code-to-my-server-heres-what-keeps-it-in-a-box-1iog</link>
      <guid>https://dev.to/dennisonbertram/my-ai-agent-deploys-code-to-my-server-heres-what-keeps-it-in-a-box-1iog</guid>
      <description>&lt;p&gt;My coding agent deploys apps to a server I own. A lot of the time it's shipping code it wrote ten minutes earlier, and I haven't read every line. Usually that's fine. The question I kept coming back to was what happens on the day it isn't fine: a bad dependency, a confused agent, or an app that starts poking at the machine it runs on.&lt;/p&gt;

&lt;p&gt;My answer is a sandbox around every app the agent ships. I built it into &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt;, an open-source tool that turns a Linux server into a place AI agents can deploy apps and databases on their own. This post explains in plain terms how that sandbox works, how to set it up on your server, and how to check for yourself that an app is really inside it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can sign up and set things up from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a normal container isn't enough
&lt;/h2&gt;

&lt;p&gt;Most apps today ship as containers. A container feels like its own small computer, with its own files and processes. It isn't one. Every container on a machine shares the same Linux kernel with the host and with every other container. The kernel is the core program that controls files, memory, the network, and everything else.&lt;/p&gt;

&lt;p&gt;Programs ask the kernel for things through system calls, or syscalls. Opening a file is a syscall. So is sending network data or starting a process. In a normal container, those calls go straight to the host's real kernel. Linux has hundreds of them, and every so often someone finds a bug in one that lets a program break out of its container and reach the host. That's called a container escape.&lt;/p&gt;

&lt;p&gt;When I write and deploy the code myself, I can live with that risk. When an agent writes and deploys code all day without me watching, I want another wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gVisor does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gvisor.dev/docs/" rel="noopener noreferrer"&gt;gVisor&lt;/a&gt; is an open-source project from Google that adds that wall. It gives each container its own kernel, written in Go, that runs as an ordinary program on the host. gVisor calls this kernel the Sentry.&lt;/p&gt;

&lt;p&gt;When the app makes a syscall, the Sentry handles it itself instead of passing it to the host kernel. When the app needs files, a separate helper process called the Gofer fetches them. The Sentry itself runs with tight limits on what it can ask the real kernel for. So if the app finds a bug, it's a bug in gVisor's kernel, and getting from there to the host takes a second break-in.&lt;/p&gt;

&lt;p&gt;gVisor plugs into Docker as a runtime, which is the piece that actually starts containers. Docker's default runtime is called &lt;code&gt;runc&lt;/code&gt;. gVisor's is called &lt;code&gt;runsc&lt;/code&gt;. You use the same Docker commands with a different engine underneath.&lt;/p&gt;

&lt;p&gt;There are costs, and gVisor's docs are upfront about them. Each syscall takes a longer path, so apps that make a lot of them, like heavy disk or network work, run slower. gVisor also doesn't implement every Linux syscall, so once in a while an app won't run unchanged (see the &lt;a href="https://gvisor.dev/docs/user_guide/compatibility/" rel="noopener noreferrer"&gt;compatibility notes&lt;/a&gt;). For the web apps and APIs I have agents deploy, I'll take that trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up gVisor for agentic-hosting
&lt;/h2&gt;

&lt;p&gt;agentic-hosting is a single Go program called &lt;code&gt;ah&lt;/code&gt; that runs on your server and takes deploy requests from agents over an HTTP API. The &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; covers the full install. Here's the gVisor part, on Ubuntu.&lt;/p&gt;

&lt;p&gt;Install &lt;code&gt;runsc&lt;/code&gt; from gVisor's package repository:&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="nv"&gt;KEYRING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/keyrings/gvisor-archive-keyring.gpg
&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://storage.googleapis.com/gvisor/releases

curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://gvisor.dev/archive.key &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEYRING&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="nv"&gt;$ARCH&lt;/span&gt;&lt;span class="s2"&gt; signed-by=&lt;/span&gt;&lt;span class="nv"&gt;$KEYRING&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt; release main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/gvisor.list
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; runsc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then register it with Docker under the name &lt;code&gt;runsc&lt;/code&gt;:&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="nb"&gt;sudo tee&lt;/span&gt; /etc/docker/daemon.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "runtimes": {
    "runsc": {
      "path": "/usr/bin/runsc"
    }
  },
  "default-address-pools": [{"base":"172.20.0.0/14","size":24}]
}
&lt;/span&gt;&lt;span class="no"&gt;JSON
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;runtimes&lt;/code&gt; block is the part that matters. &lt;code&gt;ah&lt;/code&gt; asks Docker for a runtime named exactly &lt;code&gt;runsc&lt;/code&gt;. The address pool line gives Docker room for a lot of small networks, which &lt;code&gt;ah&lt;/code&gt; needs because it creates a private network for each agent and another one for each app.&lt;/p&gt;

&lt;p&gt;Check that it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker info 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep &lt;/span&gt;runsc
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;runsc hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command should show &lt;code&gt;runsc&lt;/code&gt; among Docker's runtimes. The second runs a test container inside gVisor.&lt;/p&gt;

&lt;p&gt;That's all the gVisor setup there is. &lt;code&gt;ah&lt;/code&gt; checks for it every time it starts. In production, if Docker doesn't list &lt;code&gt;runsc&lt;/code&gt;, &lt;code&gt;ah&lt;/code&gt; exits with this error:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FATAL: gVisor runtime (runsc) not found in Docker daemon. gVisor is required for container isolation in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In dev mode (&lt;code&gt;--dev&lt;/code&gt;, for trying it on a laptop) it logs a warning instead, but app containers still won't start, because &lt;code&gt;ah&lt;/code&gt; always asks for &lt;code&gt;runsc&lt;/code&gt;. There's no setting that turns the sandbox off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when an agent deploys
&lt;/h2&gt;

&lt;p&gt;From the agent's side, the sandbox doesn't show up at all. The agent registers, gets an API key, and asks for a service:&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 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services"&lt;/span&gt; &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;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&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;'{
    "name": "web",
    "image": "your-dockerhub-user/your-app:1.0",
    "port": 3000
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That request takes a name, an image, a port, and optional environment variables. Nothing else. There's no field for the runtime, extra privileges, or more memory, so the agent can't ask for a looser box even if it wanted one.&lt;/p&gt;

&lt;p&gt;Behind that call, &lt;code&gt;ah&lt;/code&gt; starts the container with these settings. They're hardcoded in &lt;code&gt;internal/docker/client.go&lt;/code&gt;, so you can read them yourself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime &lt;code&gt;runsc&lt;/code&gt;.&lt;/strong&gt; The app runs on gVisor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All capabilities dropped&lt;/strong&gt; (&lt;code&gt;CapDrop: ALL&lt;/code&gt;). Linux splits root's powers into pieces called capabilities, like changing file ownership or reconfiguring the network. The app gets none of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No new privileges&lt;/strong&gt; (&lt;code&gt;no-new-privileges&lt;/code&gt;). A program inside can't gain more power later, for example through &lt;code&gt;sudo&lt;/code&gt; or another setuid program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only root filesystem&lt;/strong&gt; (&lt;code&gt;ReadonlyRootfs&lt;/code&gt;). The app can't change its own files. It gets small in-memory scratch folders instead: &lt;code&gt;/tmp&lt;/code&gt; (64 MB) and &lt;code&gt;/run&lt;/code&gt;, &lt;code&gt;/var/run&lt;/code&gt;, and &lt;code&gt;/var/tmp&lt;/code&gt; (16 MB each). They're all mounted &lt;code&gt;noexec&lt;/code&gt;, so nothing written there can run as a program.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;256 processes max&lt;/strong&gt; (&lt;code&gt;PidsLimit&lt;/code&gt;). A program that keeps spawning copies of itself hits a wall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory and CPU caps&lt;/strong&gt; from the agent's quota, 2048 MB and 2 CPUs by default. The memory-plus-swap limit equals the memory limit, so the app gets no swap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A private network.&lt;/strong&gt; Each agent's apps sit on their own Docker network, marked internal, with app-to-app traffic turned off. Web traffic comes in through Traefik, the proxy that routes requests, over a separate small network for each app. None of these networks has a route to the internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;ah&lt;/code&gt; also sets the label &lt;code&gt;traefik.enable=false&lt;/code&gt; on every app, so an image can't use its own labels to grab another app's traffic. The Postgres and Redis containers it creates for agents run under &lt;code&gt;runsc&lt;/code&gt; too.&lt;/p&gt;

&lt;p&gt;One practical effect: images that write outside those scratch folders, or that expect to start as root and switch users, will crash here. If an agent's app dies with "Read-only file system" in its logs, that's the reason. The agent can read those logs itself at &lt;code&gt;GET /v1/services/{id}/logs&lt;/code&gt; and fix the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check an app is really sandboxed
&lt;/h2&gt;

&lt;p&gt;I don't like trusting a list of settings in a blog post, including mine. Here's how to check on your own server.&lt;/p&gt;

&lt;p&gt;First, ask &lt;code&gt;ah&lt;/code&gt;. The detailed health endpoint reports whether gVisor is installed and which version:&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;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/system/health/detailed"&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "status": "ok",&lt;/span&gt;
&lt;span class="c"&gt;#   "docker": {"available": true, ...},&lt;/span&gt;
&lt;span class="c"&gt;#   "gvisor": {"available": true, "version": "..."},&lt;/span&gt;
&lt;span class="c"&gt;#   ...&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then look at the container. Get the service's &lt;code&gt;container_id&lt;/code&gt; from the API and ask Docker how it was started (this uses &lt;code&gt;jq&lt;/code&gt; to pull out the field):&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="nv"&gt;CID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&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;$AH_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AH_URL&lt;/span&gt;&lt;span class="s2"&gt;/v1/services/&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .container_id&lt;span class="si"&gt;)&lt;/span&gt;

docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{json .HostConfig}}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{Runtime, ReadonlyRootfs, CapDrop, PidsLimit}'&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "Runtime": "runsc",&lt;/span&gt;
&lt;span class="c"&gt;#   "ReadonlyRootfs": true,&lt;/span&gt;
&lt;span class="c"&gt;#   "CapDrop": [&lt;/span&gt;
&lt;span class="c"&gt;#     "ALL"&lt;/span&gt;
&lt;span class="c"&gt;#   ],&lt;/span&gt;
&lt;span class="c"&gt;#   "PidsLimit": 256&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the host, every gVisor sandbox shows up as its own process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-eo&lt;/span&gt; pid,args | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'[r]unsc-sandbox'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also look from inside. If the image has &lt;code&gt;dmesg&lt;/code&gt; and &lt;code&gt;touch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; dmesg
&lt;span class="c"&gt;# [    0.000000] Starting gVisor...&lt;/span&gt;

docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;touch&lt;/span&gt; /test
&lt;span class="c"&gt;# ... Read-only file system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under gVisor, &lt;code&gt;dmesg&lt;/code&gt; prints gVisor's own made-up boot log instead of the host's kernel log. Some of the lines are jokes, like "Preparing for the zombie uprising...". gVisor's docs point out that this output is easy to fake, so use it as a quick sanity check and trust &lt;code&gt;docker inspect&lt;/code&gt; for the real answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the sandbox doesn't cover
&lt;/h2&gt;

&lt;p&gt;gVisor protects the server from the apps. It doesn't solve everything.&lt;/p&gt;

&lt;p&gt;Builds are a separate step. When an agent deploys from git, Nixpacks (the tool that turns source code into a container image) builds it with Docker's regular builder, not inside gVisor. The app that comes out of the build runs sandboxed. &lt;code&gt;ah&lt;/code&gt; limits where the source can come from: HTTPS URLs on github.com, gitlab.com, bitbucket.org, sr.ht, or codeberg.org.&lt;/p&gt;

&lt;p&gt;The API key is the other gap. Anyone holding an agent's key can do anything that agent can, including deploying, deleting, and reading its secrets. They can't reach other agents' apps or register new agents. I give each agent its own key with &lt;code&gt;POST /v1/auth/keys&lt;/code&gt; and revoke it when I'm done.&lt;/p&gt;

&lt;p&gt;And gVisor is one more wall, not a guarantee. Keep the host patched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Every app an agent deploys through agentic-hosting runs inside gVisor, with no extra privileges, a read-only filesystem, capped resources, and a network that only lets web traffic in. The agent can't turn any of that off, and you can check the main settings yourself with one &lt;code&gt;docker inspect&lt;/code&gt; command.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;Give it to your agent: &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;Try the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/install/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;, read the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/docs/security/" rel="noopener noreferrer"&gt;security model&lt;/a&gt;, or start at the &lt;a href="https://dennisonbertram.github.io/agentic-hosting/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt; if you find a way out of the box. I want to hear about it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>docker</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Why One Happy Path Beats a Menu of Options When Your User Is an AI Agent</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:17:14 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/why-one-happy-path-beats-a-menu-of-options-when-your-user-is-an-ai-agent-33mj</link>
      <guid>https://dev.to/dennisonbertram/why-one-happy-path-beats-a-menu-of-options-when-your-user-is-an-ai-agent-33mj</guid>
      <description>&lt;p&gt;Give an engineer five ways to deploy an app and they'll pick one. When I gave an AI agent several ways, it tried a few, mixed them together, and left me debugging at midnight.&lt;/p&gt;

&lt;p&gt;That's the reason &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt; works the way it does. It's an open-source tool I built that turns a Linux server you own into a place AI coding agents can deploy apps and databases on their own. This post explains its main design rule, one way to do each job, and walks through the API calls an agent makes to go from a git repo to a live URL.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; (or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;skill&lt;/a&gt;) and it can sign up and get going from that one link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why fewer options
&lt;/h2&gt;

&lt;p&gt;I used to think "only one way to do it" was a sign of an unfinished product. Heroku, Kubernetes, Coolify, and Dokku all give you lots of settings, because people running servers have strong opinions about how to build and route things.&lt;/p&gt;

&lt;p&gt;Agents don't have opinions. They make a guess, and if the guess fails they retry, sometimes with a different guess. Every extra option is another branch they can wander down that I never tested.&lt;/p&gt;

&lt;p&gt;So agentic-hosting has one build system, one router, and one choice per database type. If there's only one path, the agent can't find a creative one I didn't plan for. Here's what that looks like in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building: git repo in, running app out
&lt;/h2&gt;

&lt;p&gt;Apps are built with Nixpacks, a tool that looks at your code and figures out how to package it into a container without a Dockerfile. It's the only build option. You can't upload your own Dockerfile.&lt;/p&gt;

&lt;p&gt;Deploying from git takes two calls. First the agent creates a service. The API requires an &lt;code&gt;image&lt;/code&gt; field, so for git deploys it sends the placeholder &lt;code&gt;"pending"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST /v1/services
Authorization: Bearer &amp;lt;api_key&amp;gt;
Content-Type: application/json

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"my-app"&lt;/span&gt;,
  &lt;span class="s2"&gt;"image"&lt;/span&gt;: &lt;span class="s2"&gt;"pending"&lt;/span&gt;,
  &lt;span class="s2"&gt;"port"&lt;/span&gt;: 3000
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it starts a build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST /v1/services/&lt;span class="o"&gt;{&lt;/span&gt;serviceID&lt;span class="o"&gt;}&lt;/span&gt;/builds

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"source_type"&lt;/span&gt;: &lt;span class="s2"&gt;"git"&lt;/span&gt;,
  &lt;span class="s2"&gt;"source_url"&lt;/span&gt;: &lt;span class="s2"&gt;"https://github.com/org/repo"&lt;/span&gt;,
  &lt;span class="s2"&gt;"source_ref"&lt;/span&gt;: &lt;span class="s2"&gt;"main"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;source_type&lt;/code&gt; has to be &lt;code&gt;"git"&lt;/code&gt;, and &lt;code&gt;source_ref&lt;/code&gt; defaults to &lt;code&gt;"main"&lt;/code&gt;. Only HTTPS URLs from github.com, gitlab.com, bitbucket.org, sr.ht, and codeberg.org are accepted. Anything else gets rejected before the build starts.&lt;/p&gt;

&lt;p&gt;While it builds, the agent can watch the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /v1/services/&lt;span class="o"&gt;{&lt;/span&gt;serviceID&lt;span class="o"&gt;}&lt;/span&gt;/builds/&lt;span class="o"&gt;{&lt;/span&gt;buildID&lt;span class="o"&gt;}&lt;/span&gt;/logs?follow&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because there's only one build path, the logs always look the same, and when something breaks there's one place to look. In my experience that helps the agent more than which build tool it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing: you get one URL
&lt;/h2&gt;

&lt;p&gt;Traefik, an open-source proxy, takes incoming web traffic and sends it to the right app. agentic-hosting writes Traefik's config itself. Apps can't set their own routing rules, so a container can't quietly grab another app's traffic.&lt;/p&gt;

&lt;p&gt;The URL follows one pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By default: &lt;code&gt;http://{serviceID}.localhost&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If you start the server with &lt;code&gt;--base-domain example.com&lt;/code&gt; (or set &lt;code&gt;AH_BASE_DOMAIN&lt;/code&gt;): &lt;code&gt;https://{dns_label}.example.com&lt;/code&gt;, where &lt;code&gt;dns_label&lt;/code&gt; comes from the service name
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /v1/services/&lt;span class="o"&gt;{&lt;/span&gt;serviceID&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;# → includes "url": "http://svc_….localhost"&lt;/span&gt;
&lt;span class="c"&gt;#    or "https://my-app.example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom domains aren't supported yet. That's &lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues/82" rel="noopener noreferrer"&gt;issue #82&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Databases: Postgres or Redis
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; field takes &lt;code&gt;"postgres"&lt;/code&gt; or &lt;code&gt;"redis"&lt;/code&gt;, and nothing else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST /v1/databases

&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"my-db"&lt;/span&gt;, &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"postgres"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It takes about 10 to 30 seconds. Then the agent grabs the connection string, adds it to the app's environment variables, and restarts the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /v1/databases/&lt;span class="o"&gt;{&lt;/span&gt;dbID&lt;span class="o"&gt;}&lt;/span&gt;/connection-string

POST /v1/services/&lt;span class="o"&gt;{&lt;/span&gt;serviceID&lt;span class="o"&gt;}&lt;/span&gt;/env
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"DATABASE_URL"&lt;/span&gt;: &lt;span class="s2"&gt;"&amp;lt;connection-string&amp;gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

POST /v1/services/&lt;span class="o"&gt;{&lt;/span&gt;serviceID&lt;span class="o"&gt;}&lt;/span&gt;/restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Environment variables are encrypted on disk and hidden in API responses unless you add &lt;code&gt;?reveal=true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smaller limits
&lt;/h2&gt;

&lt;p&gt;The same thinking shows up in small places. Creating a service accepts &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;port&lt;/code&gt;, and optional &lt;code&gt;env&lt;/code&gt;, so the agent can't request more memory or CPU. Images come from Docker Hub or the server's own local registry. And if the agent retries a request with the same &lt;code&gt;Idempotency-Key&lt;/code&gt; header within 10 minutes, it won't deploy twice.&lt;/p&gt;

&lt;p&gt;None of this limits what you can build. It limits how many ways the agent can get there, which is what I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;With the server running (see the README, or run &lt;code&gt;./bin/ah --dev --open-registration --port 8080&lt;/code&gt; on your laptop), the agent's path is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register to get an API key.&lt;/li&gt;
&lt;li&gt;Create a service with &lt;code&gt;"image": "pending"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Start a build from a git URL.&lt;/li&gt;
&lt;li&gt;Watch the build logs, then the app logs at &lt;code&gt;GET /v1/services/{id}/logs?follow=true&amp;amp;tail=100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a Postgres or Redis database, set the env var, restart.&lt;/li&gt;
&lt;li&gt;Open the service's &lt;code&gt;url&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you use Claude Code, the repo also has a skill and &lt;code&gt;/ah-deploy&lt;/code&gt;, &lt;code&gt;/ah-db&lt;/code&gt;, &lt;code&gt;/ah-logs&lt;/code&gt;, and &lt;code&gt;/ah-status&lt;/code&gt; commands that do the same steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;agentic-hosting gives an AI agent exactly one way to build, route, and add a database, which makes its runs a lot more predictable. You run it on your own server, and the agent does the deploying.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt;&lt;/strong&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to sign up and set up, or the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; for daily use.&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Read the &lt;a href="https://agentic.hosting/docs/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;&lt;/strong&gt; and try the quickstart.&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt;&lt;/strong&gt; with what you'd want your agent to do next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you hand your agent the keys and something breaks, tell me what it was.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>go</category>
    </item>
    <item>
      <title>I Gave My Coding Agent a Server of Its Own. Here's How to Do It Safely.</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:51:05 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/i-gave-my-coding-agent-a-server-of-its-own-heres-how-to-do-it-safely-5cin</link>
      <guid>https://dev.to/dennisonbertram/i-gave-my-coding-agent-a-server-of-its-own-heres-how-to-do-it-safely-5cin</guid>
      <description>&lt;p&gt;My coding agent writes the app, writes the tests, and catches the bug I missed. Then it stops and hands me a list of deploy commands to run. For a while I just accepted that. Eventually I wanted to know why the last step was still my job.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;agentic-hosting&lt;/a&gt;. It's open source, and it turns a Linux server you own into a place an AI agent (Claude Code, Cursor, anything that can make HTTP requests) can deploy apps and spin up databases by itself, inside limits you set. This post shows how to install it, hand an agent a key, and watch it ship something.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⭐ If this is useful, a star on &lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other people find it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; point it at &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt&lt;/a&gt; and it can do the setup for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just give the agent SSH?
&lt;/h2&gt;

&lt;p&gt;When I first wanted my agent to deploy, I had two options: no access, or root. Neither felt right. Hosting tools are built for people clicking around a dashboard, and an agent with a root shell can do a lot of damage on a bad run.&lt;/p&gt;

&lt;p&gt;agentic-hosting is the middle option. Think of it as a Heroku you run on your own machine, except the main user is an agent calling an API. There's a dashboard too, but that's for you to see what the agent did.&lt;/p&gt;

&lt;p&gt;The design is deliberately narrow. There's one way to build an app, one way to route traffic to it, and one way to create each kind of database (Postgres or Redis). Each agent gets its own account, called a tenant, with its own API key and rate limits. Every app runs in a sandbox (gVisor, which sits between the container and the server's kernel), so a misbehaving app can't easily touch the host.&lt;/p&gt;

&lt;p&gt;The narrowness is the part I care about most. People like choices. Agents mostly use choices as new ways to be wrong.&lt;/p&gt;

&lt;p&gt;Under the hood it's one Go program, &lt;code&gt;ah&lt;/code&gt;, that runs as a background service on port 8080. Traefik, a standard open-source proxy, takes web traffic on ports 80/443 and passes it to your apps. State lives in SQLite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting it up
&lt;/h2&gt;

&lt;p&gt;You need a Linux server; Ubuntu 22.04 or newer is what I use. The README has the complete script. These are the main steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install Docker, gVisor's &lt;code&gt;runsc&lt;/code&gt;, Go 1.25, and Nixpacks&lt;/strong&gt; (the tool that turns source code into a runnable container without a Dockerfile). Then tell Docker about gVisor:&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="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/docker/daemon.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "runtimes": { "runsc": { "path": "/usr/bin/runsc" } },
  "default-address-pools": [{"base":"172.20.0.0/14","size":24}]
}
&lt;/span&gt;&lt;span class="no"&gt;JSON
&lt;/span&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Start the shared pieces:&lt;/strong&gt; a Docker network, Traefik (copy its &lt;code&gt;docker run&lt;/code&gt; command from the README), and a private image registry that only listens locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create &lt;span class="nt"&gt;--driver&lt;/span&gt; bridge traefik-public

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; paas-registry &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:5000:5000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /var/lib/ah/registry:/var/lib/registry &lt;span class="se"&gt;\&lt;/span&gt;
  registry:2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Build &lt;code&gt;ah&lt;/code&gt; and create its secrets:&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;git clone https://github.com/dennisonbertram/agentic-hosting /agentic-hosting
&lt;span class="nb"&gt;cd&lt;/span&gt; /agentic-hosting
&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 go build &lt;span class="nt"&gt;-o&lt;/span&gt; bin/ah ./cmd/ah
&lt;span class="nb"&gt;cp &lt;/span&gt;bin/ah /usr/local/bin/ah

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/lib/ah/builds /var/lib/ah/backups
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 32 /dev/urandom | xxd &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 64 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/lib/ah/master.key
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /var/lib/ah/master.key

openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32   &lt;span class="c"&gt;# your bootstrap token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Run it.&lt;/strong&gt; Put the token in &lt;code&gt;/etc/default/ah&lt;/code&gt; as &lt;code&gt;AH_BOOTSTRAP_TOKEN=...&lt;/code&gt;, install &lt;code&gt;deploy/ah.service&lt;/code&gt;, and start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl daemon-reload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; ah
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Forwarded-Proto: https'&lt;/span&gt; http://127.0.0.1:8080/v1/system/health
&lt;span class="c"&gt;# → {"status":"ok"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only want to poke at it on your laptop, skip all that and run &lt;code&gt;./bin/ah --dev --open-registration --port 8080&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing an agent the keys
&lt;/h2&gt;

&lt;p&gt;First, create a tenant for the agent. The bootstrap token from step 3 is what lets you do this:&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;-X&lt;/span&gt; POST http://localhost:8080/v1/tenants/register &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;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-Bootstrap-Token: &amp;lt;bootstrap-token&amp;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;'{"name": "my-agent", "email": "me@example.com"}'&lt;/span&gt;
&lt;span class="c"&gt;# → {"tenant_id": "ten_...", "api_key": "keyid.secret"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here the agent sends &lt;code&gt;Authorization: Bearer &amp;lt;api_key&amp;gt;&lt;/code&gt; on every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploying from git&lt;/strong&gt; is two calls. Create a service, then start a build. The &lt;code&gt;image&lt;/code&gt; field is required, so for git deploys you pass &lt;code&gt;"pending"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST /v1/services
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"my-app"&lt;/span&gt;, &lt;span class="s2"&gt;"image"&lt;/span&gt;: &lt;span class="s2"&gt;"pending"&lt;/span&gt;, &lt;span class="s2"&gt;"port"&lt;/span&gt;: 3000 &lt;span class="o"&gt;}&lt;/span&gt;

POST /v1/services/&lt;span class="o"&gt;{&lt;/span&gt;serviceID&lt;span class="o"&gt;}&lt;/span&gt;/builds
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"source_type"&lt;/span&gt;: &lt;span class="s2"&gt;"git"&lt;/span&gt;, &lt;span class="s2"&gt;"source_url"&lt;/span&gt;: &lt;span class="s2"&gt;"https://github.com/org/repo"&lt;/span&gt;, &lt;span class="s2"&gt;"source_ref"&lt;/span&gt;: &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It builds from GitHub, GitLab, Bitbucket, sr.ht, and Codeberg over HTTPS. The agent can follow the build logs (&lt;code&gt;/builds/{buildID}/logs?follow=true&lt;/code&gt;) and the running app's logs (&lt;code&gt;/v1/services/{serviceID}/logs?follow=true&amp;amp;tail=100&lt;/code&gt;). This turned out to matter more than I expected. An agent that can read its own crash can usually fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding a database:&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;POST /v1/databases
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"my-db"&lt;/span&gt;, &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"postgres"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;   &lt;span class="c"&gt;# or "redis"&lt;/span&gt;

GET /v1/databases/&lt;span class="o"&gt;{&lt;/span&gt;dbID&lt;span class="o"&gt;}&lt;/span&gt;/connection-string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent passes that string to the app with &lt;code&gt;POST /v1/services/{serviceID}/env&lt;/code&gt;. Environment variables are encrypted on disk and hidden in responses unless you ask for &lt;code&gt;?reveal=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The app shows up at &lt;code&gt;http://{serviceID}.localhost&lt;/code&gt;, or at &lt;code&gt;https://{dns_label}.example.com&lt;/code&gt; if you start &lt;code&gt;ah&lt;/code&gt; with &lt;code&gt;--base-domain example.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skipping the curl entirely
&lt;/h2&gt;

&lt;p&gt;If you use Claude Code, the repo includes a skill and a few commands so you can just ask:&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude/skills/agentic-hosting
&lt;span class="nb"&gt;cp&lt;/span&gt; .claude/skills/agentic-hosting/SKILL.md ~/.claude/skills/agentic-hosting/SKILL.md
&lt;span class="nb"&gt;cp&lt;/span&gt; .claude/commands/ah-&lt;span class="k"&gt;*&lt;/span&gt;.md ~/.claude/commands/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/ah-deploy https://github.com/org/my-app web 3000
/ah-db web postgres
/ah-logs web
/ah-status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;/ah-register&lt;/code&gt; to create a tenant and &lt;code&gt;/ah-snapshot&lt;/code&gt; to snapshot a running service. Not on Claude? The same flows are bash scripts in &lt;code&gt;scripts/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first time I watched the agent deploy, read the logs, fix a crash, and redeploy without me, I found myself checking in on it the way you'd check on a new hire.&lt;/p&gt;

&lt;h2&gt;
  
  
  What keeps it from going wrong
&lt;/h2&gt;

&lt;p&gt;I don't trust the agent to be careful. I trust the limits around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every 30 seconds, &lt;code&gt;ah&lt;/code&gt; checks that what it thinks is running matches what Docker is actually running, and fixes the difference.&lt;/li&gt;
&lt;li&gt;If an app crashes 5 times in 10 minutes, it stops restarting it. Turning it back on takes an explicit API call.&lt;/li&gt;
&lt;li&gt;At 80% disk it warns. At 90% it refuses new deploys, builds, and databases.&lt;/li&gt;
&lt;li&gt;Each tenant is rate-limited to 100 requests a second, with bursts up to 200.&lt;/li&gt;
&lt;li&gt;Requests can carry an idempotency key, so if the agent retries within 10 minutes, nothing deploys twice.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ah backup&lt;/code&gt; snapshots the SQLite databases and keeps the last 10.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's still early. There's no billing yet (usage is tracked but only queryable), no Prometheus metrics, and it runs on a single server. Read the security section of the README before you give anyone a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;agentic-hosting lets your coding agent deploy apps and databases on a server you own, with sandboxing and limits so a bad run stays small. You install it once, give each agent its own tenant, and let it work. The quickest way to try it is &lt;code&gt;--dev&lt;/code&gt; mode on your laptop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;strong&gt;&lt;a href="https://github.com/dennisonbertram/agentic-hosting" rel="noopener noreferrer"&gt;Star agentic-hosting on GitHub&lt;/a&gt;&lt;/strong&gt; so more people find it.&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;Give this to your agent:&lt;/strong&gt; send it to &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to set up and sign up, or install the &lt;a href="https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md" rel="noopener noreferrer"&gt;agent skill&lt;/a&gt; so it knows the day-to-day commands.&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Read the &lt;a href="https://agentic.hosting/docs/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;&lt;/strong&gt; for the full API.&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;&lt;a href="https://github.com/dennisonbertram/agentic-hosting/issues" rel="noopener noreferrer"&gt;Open an issue&lt;/a&gt;&lt;/strong&gt; and tell me what you want your agent to do next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you hand your agent a server, I'd like to hear what it does with it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>selfhosted</category>
      <category>devops</category>
      <category>go</category>
    </item>
    <item>
      <title>Building an ICO on Canton Network</title>
      <dc:creator>Dennison Bertram</dc:creator>
      <pubDate>Wed, 12 Nov 2025 21:06:22 +0000</pubDate>
      <link>https://dev.to/dennisonbertram/building-an-ico-on-canton-network-2a31</link>
      <guid>https://dev.to/dennisonbertram/building-an-ico-on-canton-network-2a31</guid>
      <description>&lt;p&gt;&lt;strong&gt;A hands-on guide to creating privacy-preserving token sales on Canton Network&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ever wanted to build a token sale where only the buyer, seller, and maybe a regulator can see the details? That's exactly what we're going to create today. We'll build a complete ICO (Initial Coin Offering) smart contract that sells one token for another, with privacy built-in and atomic operations that can't fail halfway through.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's an ICO Anyway?
&lt;/h2&gt;

&lt;p&gt;ICO's are back! From &lt;a href="https://www.monad.xyz" rel="noopener noreferrer"&gt;Monad&lt;/a&gt; to &lt;a href="//megaeth.com"&gt;MegaETH&lt;/a&gt; or projects launching their token on &lt;a href="https://www.tally.xyz" rel="noopener noreferrer"&gt;Tally.xyz&lt;/a&gt;, capital formation as product market fit for crypto is the meta.&lt;/p&gt;

&lt;p&gt;Before we dive into code, let's make sure we're on the same page. An ICO (Initial Coin Offering) is when a project sells their tokens to raise funds. Traditionally this happens on public blockchains where everyone can see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who bought what&lt;/li&gt;
&lt;li&gt;How much they paid&lt;/li&gt;
&lt;li&gt;All the transaction details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what if you want to keep those details private? That's where Canton Network comes in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Canton Network?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.canton.network/" rel="noopener noreferrer"&gt;Canton Network&lt;/a&gt; is different. It gives you:&lt;/p&gt;

&lt;p&gt;🔒 &lt;strong&gt;Privacy by Default&lt;/strong&gt;&lt;br&gt;
Only the parties directly involved in a transaction can see the details. No more public order books!&lt;/p&gt;

&lt;p&gt;⚡ &lt;strong&gt;Atomic Operations&lt;/strong&gt;&lt;br&gt;
Either everything happens (buyer pays, seller delivers) or nothing happens. No failed transactions leaving things in a weird state.&lt;/p&gt;

&lt;p&gt;🤝 &lt;strong&gt;Multi-Party Authorization&lt;/strong&gt;&lt;br&gt;
Smart contracts can require multiple parties to agree before anything happens. Perfect for regulated token sales.&lt;/p&gt;

&lt;p&gt;🏗️ &lt;strong&gt;Enterprise Ready&lt;/strong&gt;&lt;br&gt;
Built for serious applications with proper tooling, monitoring, and compliance features.&lt;/p&gt;


&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;We'll create a complete ICO system with three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Token Contract&lt;/strong&gt; - A simple but secure token that can be issued and transferred&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ICO Contract&lt;/strong&gt; - The main sale logic with atomic token-for-token swaps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Suite&lt;/strong&gt; - Scripts to verify everything works correctly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The flow will be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buyer pays with USDC → ICO contract validates → MYCOIN tokens created → All in one atomic transaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Quick Setup
&lt;/h2&gt;

&lt;p&gt;Let's get your environment ready. You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daml SDK&lt;/strong&gt; (version 3.3.0-snapshot...)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java 21&lt;/strong&gt; (for the build tools)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; (for running the local network)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tmux&lt;/strong&gt; (for managing long-running processes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The easiest way is to clone the Canton quickstart repo and run their setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/digital-asset/cn-quickstart.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cn-quickstart/quickstart
make setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a complete development environment with Canton nodes, wallets, and monitoring tools.&lt;/p&gt;

&lt;p&gt;Want to get started with AI? Use this &lt;a href="https://gist.github.com/dennisonbertram/9e495d6114077ed7fefbaeb2178092a4" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; to get started fast. &lt;/p&gt;




&lt;h2&gt;
  
  
  Your First ICO Contract
&lt;/h2&gt;

&lt;p&gt;Let's start with a simple token contract. Create a file called &lt;code&gt;Token.daml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module Token where

import DA.Assert

-- A simple token that can be issued and transferred
template Token
  with
    issuer : Party     -- Who created this token
    owner : Party      -- Who currently owns it
    symbol : Text      -- Like "USD" or "MYCOIN"
    amount : Decimal   -- How much
  where
    ensure amount &amp;gt; 0.0
    signatory issuer, owner  -- Both must agree to create it

    -- Transfer ownership to someone else
    choice Transfer : ContractId Token
      with
        newOwner : Party
      controller owner, newOwner  -- Both current and new owner must agree
      do
        create this with owner = newOwner

-- The authority that can create new tokens
template TokenIssuer
  with
    issuer : Party
    symbol : Text
    totalSupply : Decimal
  where
    signatory issuer

    -- Create new tokens for someone (doesn't consume this contract)
    nonconsuming choice IssueTokens : ContractId Token
      with
        recipient : Party
        amount : Decimal
      controller issuer, recipient  -- Both issuer and recipient must agree
      do
        assert (amount &amp;gt; 0.0)
        create Token with
          issuer = issuer
          owner = recipient
          symbol = symbol
          amount = amount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's create the ICO contract in &lt;code&gt;Ico.daml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module Ico where

import DA.Assert
import DA.Time
import Token

-- An active ICO where people can buy tokens
template IcoOffering
  with
    issuer : Party                    -- The company running the ICO
    saleTokenIssuer : Party           -- Who creates the tokens being sold
    saleTokenSymbol : Text            -- What token is being sold
    paymentTokenIssuer : Party        -- Who creates the payment tokens
    paymentTokenSymbol : Text         -- What token buyers pay with
    exchangeRate : Decimal            -- How many sale tokens per payment token
    totalSaleTokens : Decimal         -- Total available for sale
    tokensSold : Decimal              -- How many sold so far
    totalRaised : Decimal             -- Total payment collected
    startTime : Time                  -- When ICO starts
    endTime : Time                    -- When ICO ends
    minPurchase : Decimal             -- Minimum purchase amount
    maxPurchase : Decimal             -- Maximum purchase (0 = no limit)
  where
    ensure totalSaleTokens &amp;gt; 0.0 &amp;amp;&amp;amp; exchangeRate &amp;gt; 0.0
    signatory issuer

    -- Buy tokens! This is the magic atomic swap
    choice Purchase : (ContractId Token.Token, ContractId IcoOffering)
      with
        buyer : Party
        paymentTokenCid : ContractId Token.Token  -- Buyer's payment token
        paymentAmount : Decimal                   -- How much they're paying
      controller buyer, issuer, saleTokenIssuer   -- All three parties must agree!
      do
        -- Check if ICO is active
        now &amp;lt;- getTime
        assert (now &amp;gt;= startTime)
        assert (now &amp;lt; endTime)

        -- Validate purchase amount
        assert (paymentAmount &amp;gt;= minPurchase)
        assert (maxPurchase == 0.0 || paymentAmount &amp;lt;= maxPurchase)

        -- Calculate how many tokens they get
        let saleTokenAmount = paymentAmount * exchangeRate

        -- Make sure we have enough tokens left
        let remainingTokens = totalSaleTokens - tokensSold
        assert (saleTokenAmount &amp;lt;= remainingTokens)

        -- Verify they own the payment token
        paymentToken &amp;lt;- fetch paymentTokenCid
        assert (paymentToken.issuer == paymentTokenIssuer)
        assert (paymentToken.symbol == paymentTokenSymbol)
        assert (paymentToken.owner == buyer)
        assert (paymentToken.amount &amp;gt;= paymentAmount)

        -- Handle payment (split if needed, then transfer)
        if paymentToken.amount == paymentAmount
          then do
            -- Exact amount: transfer the whole token
            _ &amp;lt;- exercise paymentTokenCid Token.Transfer with
                newOwner = issuer
            return ()
          else do
            -- Partial payment: split and transfer just the payment amount
            (paymentPortionCid, _changeCid) &amp;lt;- exercise paymentTokenCid Token.Split with
                splitAmount = paymentAmount
            _ &amp;lt;- exercise paymentPortionCid Token.Transfer with
                newOwner = issuer
            return ()

        -- Create the sale tokens for the buyer
        saleTokenCid &amp;lt;- create Token.Token with
          issuer = saleTokenIssuer
          owner = buyer
          symbol = saleTokenSymbol
          amount = saleTokenAmount

        -- Update the ICO with new totals
        updatedIcoCid &amp;lt;- create this with
          tokensSold = tokensSold + saleTokenAmount
          totalRaised = totalRaised + paymentAmount

        return (saleTokenCid, updatedIcoCid)

    -- Close the ICO when it's done
    choice Close : ContractId IcoCompleted
      controller issuer
      do
        now &amp;lt;- getTime
        assert (now &amp;gt;= endTime || tokensSold &amp;gt;= totalSaleTokens)
        create IcoCompleted with
          issuer = issuer
          saleTokenIssuer = saleTokenIssuer
          saleTokenSymbol = saleTokenSymbol
          paymentTokenIssuer = paymentTokenIssuer
          paymentTokenSymbol = paymentTokenSymbol
          totalSaleTokens = totalSaleTokens
          tokensSold = tokensSold
          totalRaised = totalRaised
          finalExchangeRate = exchangeRate

-- Record of a completed ICO
template IcoCompleted
  with
    issuer : Party
    saleTokenIssuer : Party
    saleTokenSymbol : Text
    paymentTokenIssuer : Party
    paymentTokenSymbol : Text
    totalSaleTokens : Decimal
    tokensSold : Decimal
    totalRaised : Decimal
    finalExchangeRate : Decimal
  where
    signatory issuer

    -- Get final statistics
    nonconsuming choice GetStats : (Decimal, Decimal, Decimal)
      controller issuer
      do return (tokensSold, totalRaised, finalExchangeRate)

-- Helper to create ICOs
template IcoFactory
  with
    issuer : Party
  where
    signatory issuer

    choice CreateIco : ContractId IcoOffering
      with
        saleTokenIssuer : Party
        saleTokenSymbol : Text
        paymentTokenIssuer : Party
        paymentTokenSymbol : Text
        exchangeRate : Decimal
        totalSaleTokens : Decimal
        startTime : Time
        endTime : Time
        minPurchase : Decimal
        maxPurchase : Decimal
      controller issuer
      do
        assert (totalSaleTokens &amp;gt; 0.0)
        assert (exchangeRate &amp;gt; 0.0)
        assert (endTime &amp;gt; startTime)
        assert (minPurchase &amp;gt; 0.0)

        create IcoOffering with
          issuer = issuer
          saleTokenIssuer = saleTokenIssuer
          saleTokenSymbol = saleTokenSymbol
          paymentTokenIssuer = paymentTokenIssuer
          paymentTokenSymbol = paymentTokenSymbol
          exchangeRate = exchangeRate
          totalSaleTokens = totalSaleTokens
          tokensSold = 0.0
          totalRaised = 0.0
          startTime = startTime
          endTime = endTime
          minPurchase = minPurchase
          maxPurchase = maxPurchase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing It Out
&lt;/h2&gt;

&lt;p&gt;Let's create a test script to verify our ICO works. Create &lt;code&gt;IcoTest.daml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module IcoTest where

import DA.Assert
import DA.Time
import Daml.Script
import Ico
import Token

test_ico_lifecycle = script do
  -- Create all the parties
  icoIssuer &amp;lt;- allocateParty "Company"
  saleTokenIssuer &amp;lt;- allocateParty "Company"
  paymentTokenIssuer &amp;lt;- allocateParty "USDCIssuer"
  buyer1 &amp;lt;- allocateParty "Alice"
  buyer2 &amp;lt;- allocateParty "Bob"

  -- Create token issuers
  saleIssuerCid &amp;lt;- submit saleTokenIssuer do
    createCmd TokenIssuer with
      issuer = saleTokenIssuer
      symbol = "MYCOIN"
      totalSupply = 1000000.0

  paymentIssuerCid &amp;lt;- submit paymentTokenIssuer do
    createCmd TokenIssuer with
      issuer = paymentTokenIssuer
      symbol = "USDC"
      totalSupply = 1000000.0

  -- Give buyers some USDC to spend
  buyer1PaymentCid &amp;lt;- submitMulti [paymentTokenIssuer, buyer1] [] do
    exerciseCmd paymentIssuerCid IssueTokens with
      recipient = buyer1
      amount = 1000.0

  buyer2PaymentCid &amp;lt;- submitMulti [paymentTokenIssuer, buyer2] [] do
    exerciseCmd paymentIssuerCid IssueTokens with
      recipient = buyer2
      amount = 500.0

  -- Set up the ICO
  now &amp;lt;- getTime
  factoryCid &amp;lt;- submit icoIssuer do
    createCmd IcoFactory with issuer = icoIssuer

  icoCid &amp;lt;- submit icoIssuer do
    exerciseCmd factoryCid CreateIco with
      saleTokenIssuer = saleTokenIssuer
      saleTokenSymbol = "MYCOIN"
      paymentTokenIssuer = paymentTokenIssuer
      paymentTokenSymbol = "USDC"
      exchangeRate = 100.0  -- 1 USDC = 100 MYCOIN
      totalSaleTokens = 50000.0
      startTime = now
      endTime = addRelTime now (days 1)
      minPurchase = 10.0
      maxPurchase = 0.0

  -- Alice buys 100 USDC worth (gets 10,000 MYCOIN)
  (aliceTokensCid, icoCid1) &amp;lt;- submitMulti [buyer1, icoIssuer, saleTokenIssuer] [] do
    exerciseCmd icoCid Purchase with
      buyer = buyer1
      paymentTokenCid = buyer1PaymentCid
      paymentAmount = 100.0

  -- Verify Alice got her tokens
  Some aliceTokens &amp;lt;- queryContractId buyer1 aliceTokensCid
  assert (aliceTokens.amount == 10000.0)
  assert (aliceTokens.symbol == "MYCOIN")

  -- Bob buys 50 USDC worth (gets 5,000 MYCOIN)
  (bobTokensCid, icoCid2) &amp;lt;- submitMulti [buyer2, icoIssuer, saleTokenIssuer] [] do
    exerciseCmd icoCid1 Purchase with
      buyer = buyer2
      paymentTokenCid = buyer2PaymentCid
      paymentAmount = 50.0

  -- Verify Bob got his tokens
  Some bobTokens &amp;lt;- queryContractId buyer2 bobTokensCid
  assert (bobTokens.amount == 5000.0)

  -- Check ICO totals
  Some finalIco &amp;lt;- queryContractId icoIssuer icoCid2
  assert (finalIco.tokensSold == 15000.0)  -- 10k + 5k
  assert (finalIco.totalRaised == 150.0)   -- 100 + 50

  return ()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing It Out
&lt;/h2&gt;

&lt;p&gt;Run the tests to make sure everything works:&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;# In the cn-quickstart/quickstart directory&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;daml/ico-token
daml &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test suite passed with 8 transactions and 9 active contracts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deploying to LocalNet
&lt;/h2&gt;

&lt;p&gt;Time to see your ICO running on a real Canton network! The build scripts handle all the complexity:&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;# Build the contracts&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;examples/ico-token
./scripts/build.sh

&lt;span class="c"&gt;# Deploy to local network&lt;/span&gt;
./scripts/deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compile your Daml contracts into a DAR file&lt;/li&gt;
&lt;li&gt;Start Canton participant nodes&lt;/li&gt;
&lt;li&gt;Launch web interfaces for exploration&lt;/li&gt;
&lt;li&gt;Deploy your contracts to the ledger&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once running, you can access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scan UI&lt;/strong&gt;: &lt;a href="http://scan.localhost:4000" rel="noopener noreferrer"&gt;http://scan.localhost:4000&lt;/a&gt; (explore contracts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wallet UI&lt;/strong&gt;: &lt;a href="http://wallet.localhost:2000" rel="noopener noreferrer"&gt;http://wallet.localhost:2000&lt;/a&gt; (manage tokens)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Provider&lt;/strong&gt;: &lt;a href="http://app-provider.localhost:3000" rel="noopener noreferrer"&gt;http://app-provider.localhost:3000&lt;/a&gt; (frontend)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Making It Real
&lt;/h2&gt;

&lt;p&gt;Your ICO is now live on LocalNet! Here's how to interact with it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create parties&lt;/strong&gt; through the wallet interfaces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue tokens&lt;/strong&gt; using the TokenIssuer contracts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up an ICO&lt;/strong&gt; using the IcoFactory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make purchases&lt;/strong&gt; through the Purchase choice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the magic&lt;/strong&gt; happen atomically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The best part? Only the buyer, seller, and sale token issuer can see the transaction details. Everyone else just sees that a transaction happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;🎉 &lt;strong&gt;Congratulations!&lt;/strong&gt; You just built a privacy-preserving ICO on Canton Network.&lt;/p&gt;

&lt;p&gt;Want to take it further? Here are some ideas:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Add Features&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tiered pricing&lt;/strong&gt;: Different rates for different purchase amounts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time bonuses&lt;/strong&gt;: Better rates for early buyers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vesting&lt;/strong&gt;: Lock purchased tokens for a period&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KYC integration&lt;/strong&gt;: Verify buyer identities&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Go Production&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-party deployments&lt;/strong&gt;: Run across different Canton domains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance&lt;/strong&gt;: Add regulator observers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: Set up proper observability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling&lt;/strong&gt;: Handle thousands of concurrent buyers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Learn More&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Daml patterns&lt;/strong&gt;: Explore more Canton-specific smart contract patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy models&lt;/strong&gt;: Deep dive into sub-transaction privacy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration&lt;/strong&gt;: Connect with existing DeFi protocols&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Canton ecosystem is growing fast. Your ICO contract is a great foundation for building the next generation of token sales!&lt;/p&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Dennison Bertram is the co-founder and CEO of &lt;a href="https://www.tally.xyz" rel="noopener noreferrer"&gt;Tally.xyz&lt;/a&gt;, the infrastructure layer for tokens—from ICO and airdrop to governance, staking, and value accrual. Tally delivers the complete framework for operating tokens at scale, powering the largest teams in the ecosystem with billions under management.&lt;/p&gt;

</description>
      <category>canton</category>
      <category>web3</category>
      <category>blockchain</category>
      <category>tallyxyz</category>
    </item>
  </channel>
</rss>
