<?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: MUHAMMAD ZAIN UL ABIDIN</title>
    <description>The latest articles on DEV Community by MUHAMMAD ZAIN UL ABIDIN (@zforzain2000).</description>
    <link>https://dev.to/zforzain2000</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%2F2867509%2F69a6aab1-b9d6-4e31-bb93-685ce7b2042d.jpg</url>
      <title>DEV Community: MUHAMMAD ZAIN UL ABIDIN</title>
      <link>https://dev.to/zforzain2000</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zforzain2000"/>
    <language>en</language>
    <item>
      <title>Scaling Socket.io Rooms in NestJS With the Redis Adapter</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Tue, 03 Nov 2026 05:23:28 +0000</pubDate>
      <link>https://dev.to/zforzain2000/scaling-socketio-rooms-in-nestjs-with-the-redis-adapter-3pd4</link>
      <guid>https://dev.to/zforzain2000/scaling-socketio-rooms-in-nestjs-with-the-redis-adapter-3pd4</guid>
      <description>&lt;p&gt;I have watched the same failure play out in real-time features more than once. One server, one room, everything works. You add a second instance behind a load balancer to handle more connections, and half your users stop receiving messages. Nobody changed the room code. Nobody changed the gateway. The system just got bigger, and the thing that worked at one instance quietly stopped working at two.&lt;/p&gt;

&lt;p&gt;The cause is simple, and that is what makes it dangerous. Socket.io rooms live in memory by default. Instance A knows its own sockets. Instance B knows its own sockets. Neither knows the other's. When instance A broadcasts to a room, it only talks to the sockets it can see.&lt;/p&gt;

&lt;p&gt;This post is the fix I reach for in NestJS: the Redis adapter, the sticky-session rule that makes it actually work, and a room pattern that stops you from rebuilding the same in-memory trap in a new place. It is written for a NestJS app that already has a working gateway on one instance and now needs a second one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a second instance breaks your rooms
&lt;/h2&gt;

&lt;p&gt;A single Socket.io server holds every room, every socket, and every membership in its own process memory, so a broadcast to a room is a local loop over local sockets. Add a second instance and that loop still only covers local sockets, which means a message sent from instance A never reaches a room member connected to instance B. Nothing errors, no exception is thrown, and the sender's own test client often passes because it happens to be connected to the sending instance.&lt;/p&gt;

&lt;p&gt;That silent half-delivery is the signature of this bug. If your chat, live feed, or collaboration room works for some users and not others, and the split lines up with which instance they landed on, you are almost certainly looking at in-memory rooms behind a load balancer rather than a bug in your room logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Redis adapter, in one picture
&lt;/h2&gt;

&lt;p&gt;The Redis adapter replaces the in-memory room store with a shared channel that every instance listens to, so a broadcast becomes a publish step plus a local delivery step on each instance. When instance A emits to a room, it publishes that event to Redis; instance B receives it through its subscription and delivers it to its own local sockets in that room, and instance A does the same for its locals. Your gateway code does not change shape: &lt;code&gt;server.to(room).emit(...)&lt;/code&gt; keeps working, and the adapter changes where that call reaches.&lt;/p&gt;

&lt;p&gt;Two details in that picture matter for everything that follows. First, the adapter moves messages between servers, but it does not move your application state between servers. A map you built in instance A's memory is still invisible to instance B. Second, Redis becomes part of your real-time path, so a Redis outage is now a real-time outage unless you have planned for it. I cover both failure modes below, because the adapter solves the broadcast problem and creates two smaller problems you need to handle on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire it in NestJS
&lt;/h2&gt;

&lt;p&gt;Start from the documented NestJS shape and do not improvise the wiring. Install the three packages, extend &lt;code&gt;IoAdapter&lt;/code&gt;, connect two Redis clients, and register the adapter before the app starts listening. The two-client split is not optional decoration: a Redis connection in subscriber mode cannot issue regular commands, so the publishing client and the subscribing client must be separate connections, and &lt;code&gt;duplicate()&lt;/code&gt; gives the subscriber the same configuration on its own connection.&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;redis socket.io @socket.io/redis-adapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// redis-io.adapter.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IoAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/platform-socket.io&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ServerOptions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;socket.io&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@socket.io/redis-adapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redis&lt;/span&gt;&lt;span class="dl"&gt;'&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;class&lt;/span&gt; &lt;span class="nc"&gt;RedisIoAdapter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;IoAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;adapterConstructor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;createAdapter&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;async&lt;/span&gt; &lt;span class="nf"&gt;connectToRedis&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="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;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;pubClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&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;REDIS_URL&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;subClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pubClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duplicate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;pubClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;subClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adapterConstructor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pubClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;createIOServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&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;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ServerOptions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;any&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;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIOServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adapterConstructor&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;server&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./app.module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RedisIoAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./redis-io.adapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;bootstrap&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;app&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;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppModule&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;redisIoAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RedisIoAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redisIoAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connectToRedis&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useWebSocketAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redisIoAdapter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in that code deserve a second look before you ship it. Connect both clients before the app listens, or early broadcasts can run before the adapter is ready. Read the Redis URL from the environment, never hardcode &lt;code&gt;localhost&lt;/code&gt;, or your staging and production setups will diverge in exactly the way that wastes a debugging afternoon. And keep the adapter construction in one place: every gateway in the app shares this adapter through &lt;code&gt;useWebSocketAdapter&lt;/code&gt;, so a second copy of the wiring in another module is a second chance to drift. This is the same production NestJS stack I describe on my &lt;a href="https://dev.to/blog/hire-senior-nextjs-nestjs-developer-pakistan"&gt;hiring page&lt;/a&gt;, where Socket.io and BullMQ on Redis are the real-time and background pair I ship with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sticky sessions: the step everyone skips
&lt;/h2&gt;

&lt;p&gt;The Redis adapter moves broadcasts between instances, but it does not fix the handshake, and this is the step that sinks most first attempts at scaling Socket.io. A Socket.io client starts with HTTP long polling and then upgrades to a WebSocket connection, and those polling requests must reach the same instance that owns the session. If your load balancer round-robins a polling request to a different instance, that instance does not know the session, and the client gets errors and reconnect loops instead of a working socket.&lt;/p&gt;

&lt;p&gt;The NestJS docs state the rule plainly: with multiple load-balanced instances you must either disable polling by setting &lt;code&gt;transports: ['websocket']&lt;/code&gt; in the client configuration, or enable cookie-based routing (sticky sessions) in your load balancer, because Redis alone is not enough. Pick one deliberately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What it costs you&lt;/th&gt;
&lt;th&gt;When I pick it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket-only transport&lt;/td&gt;
&lt;td&gt;You lose the polling fallback, so clients behind strict proxies that block WebSocket upgrades cannot connect at all&lt;/td&gt;
&lt;td&gt;Internal tools, controlled clients, or a first scaled deployment where you can measure how many clients actually need polling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookie-based sticky sessions&lt;/td&gt;
&lt;td&gt;Your load balancer must support it and you must test it, since a misconfigured cookie rule fails exactly like no rule at all&lt;/td&gt;
&lt;td&gt;Any public-facing app, because it keeps the polling fallback working while still pinning a session to one instance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not substitute IP-based hashing and call it done without thinking. Users behind corporate NAT, mobile networks, and shared Wi-Fi can share an IP or change it mid-session, and IP hashing pins them by an address that is not really theirs. A cookie set by the load balancer tracks the session, not the network path, and that is the property you actually need. Whichever option you pick, test it with polling enabled and with two instances running, because a sticky rule that only works in a single-instance test is not a sticky rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Address users and tenants with rooms, not in-memory maps
&lt;/h2&gt;

&lt;p&gt;Once broadcasts cross instances, the next trap is addressing. The question stops being "how do I emit to a room" and becomes "how do I reach this one user, or this one tenant's room, when I have no idea which instance holds their socket". The answer is to stop asking which instance holds them. Address the room, and let the adapter find the instance.&lt;/p&gt;

&lt;p&gt;The pattern I use has two rules. First, every socket joins a room named for its user immediately after authentication, before it joins anything else. Second, every shared room is named for the thing it belongs to, not for a server or a process, so a tenant room looks like &lt;code&gt;tenant:9:project:41&lt;/code&gt; and a user room looks like &lt;code&gt;user:42&lt;/code&gt;. A service that needs to notify one user then emits to &lt;code&gt;user:42&lt;/code&gt; from any instance, and the adapter delivers it to whichever instance holds that socket.&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;// rooms.gateway.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;WebSocketGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;WebSocketServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SubscribeMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;OnGatewayConnection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;MessageBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ConnectedSocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/websockets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Socket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;socket.io&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="nd"&gt;WebSocketGateway&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;origin&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;CLIENT_ORIGIN&lt;/span&gt; &lt;span class="p"&gt;}&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;class&lt;/span&gt; &lt;span class="nc"&gt;RoomsGateway&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;OnGatewayConnection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;WebSocketServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handleConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Authenticate first. Only join rooms for a verified user id.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&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;userId&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;SubscribeMessage&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-project&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;onJoinProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;MessageBody&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;projectId&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;ConnectedSocket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Socket&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="c1"&gt;// Check membership in your database here, then join.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`project:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectId&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;notifyProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectId&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="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Callable from any instance. The adapter routes it.&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server&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;`project:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectId&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;project-updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&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;That gateway sketch leaves two things deliberately out, and both are worth naming. Authentication is shown as a placeholder because token validation belongs in your own guard or handshake check, not in a copy-pasted snippet, and a socket that joins a room before authentication is a socket that can listen to a room it should never see. Membership checks are also a placeholder: joining &lt;code&gt;project:41&lt;/code&gt; must be gated on a database check that this user belongs to project 41, because a room name is not a permission. Validate the join payload before trusting it too; I use Zod at every boundary, as I wrote up in &lt;a href="https://dev.to/blog/zod-as-a-contract"&gt;Zod as a contract&lt;/a&gt;. The adapter will faithfully deliver messages to anyone in the room, so the join step is where your access control lives.&lt;/p&gt;

&lt;p&gt;One more habit pays off here. When you need to know who is actually connected, use the adapter's own tools, such as &lt;code&gt;fetchSockets&lt;/code&gt;, instead of a local map. A local map answers "who is connected to this instance". The adapter answers "who is connected to this system", and at two or more instances only the second question is useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pub/sub vs Redis Streams: my pick
&lt;/h2&gt;

&lt;p&gt;Socket.io now ships two Redis-backed adapters, and the choice between them is a real decision rather than a version detail. The classic adapter forwards packets through Redis pub/sub. The newer Redis Streams adapter forwards them through a Redis stream, and its docs give it one clear advantage: it handles a temporary disconnection from Redis and resumes the stream without losing packets, where the classic pub/sub adapter does not offer that resume behavior. Both still need sticky sessions, and the Streams docs say so directly, so the new adapter does not remove the handshake work from the previous section.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Adapter&lt;/th&gt;
&lt;th&gt;How messages move&lt;/th&gt;
&lt;th&gt;Disconnect behavior&lt;/th&gt;
&lt;th&gt;NestJS wiring&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Classic &lt;code&gt;@socket.io/redis-adapter&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Redis pub/sub, two clients&lt;/td&gt;
&lt;td&gt;A gap during a Redis outage can lose packets&lt;/td&gt;
&lt;td&gt;Documented in the NestJS adapter guide, shown above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@socket.io/redis-streams-adapter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One Redis stream, one client&lt;/td&gt;
&lt;td&gt;Resumes the stream after a temporary disconnect without losing packets&lt;/td&gt;
&lt;td&gt;Custom wiring through the same &lt;code&gt;IoAdapter&lt;/code&gt; extension point, not the documented NestJS example&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My pick is boring on purpose: start with the classic adapter from the NestJS docs. It is the documented path, it matches the examples other NestJS developers will recognize when they read your code, and its failure mode is one you can plan around with reconnect handling and client-side resync. Move to the Streams adapter when temporary Redis disconnects are a real problem in your deployment, not a theoretical one, and when you have time to verify the custom wiring against your own gateway tests. A newer adapter is not automatically the right adapter for a system that is already working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it with two instances on your machine
&lt;/h2&gt;

&lt;p&gt;A scaling change that has never been observed across two instances is a guess, and this one is cheap to observe. Run Redis locally, start your NestJS app twice on two ports, connect one client to each instance, and then trigger a broadcast from a service running on only one of them. If both clients receive it, the adapter is working. If only the client on the sending instance receives it, check the adapter registration first, then the Redis connection, then the room name, in that order, because those three cover most wiring mistakes.&lt;/p&gt;

&lt;p&gt;Make the test adversarial in one specific way: connect with polling enabled, not websocket-only, and watch what happens when the load balancer is in the path. A setup that passes with a direct connection to one instance and fails behind the balancer has a sticky-session problem, not an adapter problem, and no amount of adapter debugging will fix it. The same test with websocket-only transport passing and polling failing points at the same place. Name the failure before you fix it, or you will fix the wrong layer twice.&lt;/p&gt;

&lt;p&gt;Also verify the negative case. Disconnect a client, then broadcast to its room, and confirm nothing crashes and no stale delivery appears when it reconnects. Room membership is tied to the socket, not the user, so a reconnected client must rejoin its rooms after authentication. If your client code assumes rooms survive a reconnect, your users will silently stop receiving messages after every network drop, and that failure looks exactly like the original scaling bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Redis is down, and other failure modes
&lt;/h2&gt;

&lt;p&gt;Putting Redis in the middle of your real-time path changes what an outage means, so plan for it before it happens. The classic adapter has no stream to resume from, so messages published while Redis is unreachable do not wait politely for it to come back. Your clients need a resync path for exactly this case: on reconnect, the client asks for current state over HTTP or replays its room joins, and the server answers with the latest state rather than assuming the client saw every event. That resync endpoint is not extra polish. It is the thing that turns a Redis blip into a brief catch-up instead of a permanently stale screen.&lt;/p&gt;

&lt;p&gt;Three more failure modes deserve a line each. First, keep payloads small. Every broadcast is published to Redis and then fanned out to every instance, so a large payload is multiplied by your instance count before it reaches a single client. Send ids and small deltas, and let clients fetch full objects over HTTP when they need them. Second, treat Redis as trusted internal infrastructure. The Socket.io docs warn that adapter messages are not signed or encrypted by the adapter, so do not expose Redis to untrusted networks, use authentication and TLS where your setup supports them, and never put long-lived secrets in event payloads. Third, watch the fan-out itself: a room with thousands of members across many instances turns one emit into thousands of deliveries, and that is a product decision as much as an infrastructure one. If a room grows that large, split it by topic or shard it by tenant before you scale the instance count to compensate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools I would actually use
&lt;/h2&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;What it is for&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;The shared channel between Socket.io instances&lt;/td&gt;
&lt;td&gt;Run it as managed infrastructure in production, not as a container you forget about on a single host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@socket.io/redis-adapter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The documented NestJS scaling path&lt;/td&gt;
&lt;td&gt;The two-client setup above; keep its version aligned with your Socket.io version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@socket.io/redis-streams-adapter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The disconnect-resilient alternative&lt;/td&gt;
&lt;td&gt;Evaluate it when Redis gaps become a measured problem, not before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A load balancer with cookie stickiness&lt;/td&gt;
&lt;td&gt;Pinning polling sessions to one instance&lt;/td&gt;
&lt;td&gt;Test it with polling enabled; IP hashing is a weaker substitute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker Compose&lt;/td&gt;
&lt;td&gt;Running Redis plus two app instances locally&lt;/td&gt;
&lt;td&gt;The cheapest way to observe the two-instance behavior before deploying it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;{/* MONETIZATION NOTE: the tools table above is affiliate-ready. Swap the Redis and load-balancer rows for affiliate links only when a real program is joined; no affiliate claim is made here. */}&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict: what I would pick, and why
&lt;/h2&gt;

&lt;p&gt;If your NestJS app has one instance today and needs two tomorrow, I would pick the classic Redis adapter from the NestJS docs, cookie-based sticky sessions at the load balancer, and room-per-user addressing from day one. That combination is documented, it is easy for the next developer to reason about, and it fixes the actual failure: broadcasts that only reach local sockets.&lt;/p&gt;

&lt;p&gt;I would not pick the Streams adapter first, even though its disconnect behavior is better, because the documented path matters more than the newer feature when you are changing a live real-time system. I would not pick websocket-only transport for a public app, because giving up the polling fallback trades one visible failure for a quieter one on restrictive networks. And I would not ship any of it without the two-instance test above, because a scaling setup you have not watched fail and recover is a setup you do not understand yet.&lt;/p&gt;

&lt;p&gt;The answer flips in two cases. If you already measure packet loss during Redis disconnects and users notice stale screens after every blip, the Streams adapter earns its custom wiring. If your clients are all internal and controlled, websocket-only transport is simpler than sticky sessions and the tradeoff disappears. Outside those cases, the boring documented combination is the right default, and boring is a compliment when the system in question is the one your users stare at all day.&lt;/p&gt;

&lt;p&gt;If you are building a multi-tenant real-time system and want a second pair of eyes on the room design before it ships, that is the kind of production NestJS work I do. &lt;a href="https://dev.to/#contact"&gt;Reach me through the contact section on the homepage&lt;/a&gt; with the room model, the instance count, and the failure you are seeing, and we can walk through it in a first call.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why do Socket.io rooms break when I add a second NestJS instance?
&lt;/h3&gt;

&lt;p&gt;By default each Socket.io server keeps its rooms in its own memory. A broadcast from instance A only reaches sockets connected to A, so a user on instance B never sees it. The Redis adapter fixes this by forwarding broadcasts through Redis, so every instance delivers the message to its own local sockets in that room.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I still need sticky sessions if I use the Redis adapter?
&lt;/h3&gt;

&lt;p&gt;Yes. The NestJS docs warn about this directly: Redis alone is not enough. Socket.io starts with HTTP long polling and then upgrades to WebSocket, and the polling requests must reach the same instance that owns the session. Either force websocket-only transport or turn on cookie-based routing at your load balancer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the Redis adapter need two Redis clients?
&lt;/h3&gt;

&lt;p&gt;A Redis connection in subscriber mode cannot issue regular commands, so one connection cannot both publish and subscribe. The standard setup creates a publishing client and then duplicates it for subscribing. That split is in the NestJS docs example and it is the shape to copy, not a detail to simplify away.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use the classic Redis adapter or the Redis Streams adapter?
&lt;/h3&gt;

&lt;p&gt;Start with the classic Redis adapter if you are following the NestJS docs, because that is the documented NestJS path. Consider the Redis Streams adapter when temporary Redis disconnects matter to you, because it resumes the stream without losing packets where the classic pub/sub adapter does not. Either way you still need sticky sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I send a message to one user when I do not know which instance they are on?
&lt;/h3&gt;

&lt;p&gt;Put every socket in a room named for its user, such as user:42, right after authentication. Then any instance can emit to that room and the adapter routes it to the instance holding that socket. Never keep a socketId to userId map in one instance's memory and expect other instances to read it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What breaks first when a Redis-backed Socket.io setup grows?
&lt;/h3&gt;

&lt;p&gt;Usually one of three things: the load balancer sends polling requests to the wrong instance, one instance holds state the others cannot see, or a broadcast payload grows until Redis fan-out becomes the bottleneck. Fix them in that order: sticky sessions first, room-based addressing second, and smaller payloads plus fewer broadcast rooms third.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>redis</category>
      <category>websockets</category>
      <category>typescript</category>
    </item>
    <item>
      <title>TanStack AI vs Vercel AI SDK: The One I Would Ship</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:32:29 +0000</pubDate>
      <link>https://dev.to/zforzain2000/tanstack-ai-vs-vercel-ai-sdk-the-one-i-would-ship-4n2d</link>
      <guid>https://dev.to/zforzain2000/tanstack-ai-vs-vercel-ai-sdk-the-one-i-would-ship-4n2d</guid>
      <description>&lt;p&gt;I installed both SDKs this morning and read what they actually export. The &lt;code&gt;ai&lt;/code&gt; package is at 7.0.159 and exposes 181 top-level exports; &lt;code&gt;@tanstack/ai&lt;/code&gt; is at 1.0.1 with 205. Under the marketing, both do the same job: one typed API over every model provider. What differs is three architectural bets - the wire protocol, how far the types reach, and what happens to a run when your server restarts. Those bets, not the feature lists, are what you will live with. So I compared those.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;For a production Next.js app shipping this quarter, I would still start with the Vercel AI SDK - version 7, released June 25, 2026. It is the more battle-tested system: more than 16 million weekly downloads by Vercel's own count, a deeper observability stack, first-class timeouts, and the only GA durable-agent story of the two. TanStack AI 1.0, released October 23, 2026, is the better-designed abstraction in several places I care about - per-model typing, isomorphic tools, an open wire protocol - and it is the pick when your app is not Next.js-shaped or when owning the protocol matters more than ecosystem gravity. The rest of this post is the evidence, axis by axis, so you can overrule me with your own constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked this
&lt;/h2&gt;

&lt;p&gt;I did not compare landing pages. I pulled both packages from npm on November 2, 2026, installed them side by side, and inspected the installed artifacts: version numbers, &lt;code&gt;engines&lt;/code&gt; fields, dependency trees, and full export surfaces. I read Vercel's AI SDK 7 announcement and TanStack's 1.0 post end to end, plus TanStack's own comparison and migration docs. Where a claim below comes from a vendor's own comparison page, I treat it as their positioning, not neutral fact, and I say so. Where it comes from the installed packages, you can reproduce it with one &lt;code&gt;npm i&lt;/code&gt; and a text editor. One limit, stated plainly: I did not run live model calls for this comparison, because the differences that matter here are architectural, and architecture is visible in the type surface before the first token streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core APIs look alike. The bets underneath do not.
&lt;/h2&gt;

&lt;p&gt;At the call site, the two SDKs are nearly interchangeable, and TanStack's migration doc maps them line for line. &lt;code&gt;streamText()&lt;/code&gt; becomes &lt;code&gt;chat()&lt;/code&gt;. &lt;code&gt;generateText()&lt;/code&gt; becomes &lt;code&gt;chat({ stream: false })&lt;/code&gt;. &lt;code&gt;generateObject()&lt;/code&gt; becomes &lt;code&gt;chat({ outputSchema })&lt;/code&gt;. If the APIs rhyme this closely, why does the choice matter? Because everything around the call site diverges: what travels over the wire, what the compiler knows, and which platform, if any, the SDK is quietly recruiting you into.&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;// Vercel AI SDK 7: the model is a value, the loop is an option&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stepCountIs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ai-sdk/openai&lt;/span&gt;&lt;span class="dl"&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;stopWhen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stepCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TanStack AI 1.0: the adapter is the value, the loop is a strategy&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toServerSentEventsResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openaiText&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/ai-openai&lt;/span&gt;&lt;span class="dl"&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;openaiText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-6.1-sol&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;threadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;agentLoopStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;maxIterations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="nf"&gt;toServerSentEventsResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in that small diff tell you a lot. The AI SDK's core package depends on &lt;code&gt;@ai-sdk/gateway&lt;/code&gt; directly - the Vercel AI Gateway ships inside the core's dependency tree, and a bare string model id like &lt;code&gt;'openai/gpt-5'&lt;/code&gt; resolves through it. The default path and the platform path are the same path. TanStack's core instead depends on &lt;code&gt;@ag-ui/core&lt;/code&gt;: the protocol is the dependency, not a gateway. Neither is sinister. Both are honest signals about what each project optimizes for when you are not looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wire protocol is the real fork
&lt;/h2&gt;

&lt;p&gt;Your server and your client have to agree on a stream format, and that agreement outlives every other decision in this post. TanStack AI streams AG-UI, an open protocol with typed events for text deltas, tool calls, state patches, and run lifecycle, running in both directions. Any AG-UI client can sit in front of your backend, and any AG-UI agent can sit behind your UI. The AI SDK streams its own UI Message Stream protocol: mature, well-tooled, and spoken natively by exactly one ecosystem - &lt;code&gt;@ai-sdk/react&lt;/code&gt;, AI Elements, and a large pile of tutorials and templates. AG-UI reaches the AI SDK only through an external bridge package.&lt;/p&gt;

&lt;p&gt;Here is how I think about the cost. Provider abstractions are cheap to swap because they sit behind your own route handler. Protocols are expensive to swap because they leak into the client: message part rendering, tool approval UI, resumable stream handling, even your persisted chat history format. TanStack's own docs make the honest version of this point: stay on the AI SDK if your app already speaks its protocol, because TanStack AI does not read or write it. I agree with their framing, and it cuts both ways. Picking the AI SDK today means your frontend is written against a vendor protocol. Picking TanStack means betting that an open protocol with a smaller current ecosystem keeps growing. For a greenfield app, I price the open protocol as worth real money. For an app with a working AI SDK frontend, the migration math in the section below usually ends the discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type safety: per provider versus per model
&lt;/h2&gt;

&lt;p&gt;Both SDKs take TypeScript seriously, which is rarer than it should be in this space. The AI SDK types your tool inputs from a Zod schema (v7 accepts Zod 3.25+ or Zod 4, or any Standard Schema validator), types UI message parts so &lt;code&gt;isToolUIPart(part)&lt;/code&gt; narrows rendering, and gives each provider package its own option types. That is a strong surface, and I have no complaints about it at the level most apps touch.&lt;/p&gt;

&lt;p&gt;TanStack goes one level deeper, at the model. Pick a model and the adapter narrows your options to what that model supports: reasoning controls, content types, and generation parameters are typed per model, so passing an option the model does not have is a compile error rather than a silent no-op or a runtime rejection. They also type the client side harder than most: &lt;code&gt;InferChatMessages&lt;/code&gt; derives your &lt;code&gt;useChat&lt;/code&gt; message types from your server definition, so a renamed tool output field breaks the UI build instead of rendering undefined at runtime. The AI SDK gets you partway there with generic &lt;code&gt;UIMessage&lt;/code&gt; types you parameterize by hand.&lt;/p&gt;

&lt;p&gt;My judgment: for tool inputs and message shapes, call it even. For model options - the surface where provider differences actually bite, where one model takes &lt;code&gt;reasoning&lt;/code&gt; effort and another takes a thinking budget - per-model typing is the design I wish every SDK had. It is the single most senior-friendly decision in TanStack's API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools and agent loops: where the loop lives
&lt;/h2&gt;

&lt;p&gt;A tool definition looks similar in both, until you ask where the tool runs. The AI SDK's &lt;code&gt;tool()&lt;/code&gt; defines a server tool with an &lt;code&gt;execute&lt;/code&gt; function; client-side tools are handled separately, through the chat transport and &lt;code&gt;onToolCall&lt;/code&gt; on the client. TanStack's &lt;code&gt;toolDefinition()&lt;/code&gt; defines the schema once and attaches execution with &lt;code&gt;.server(fn)&lt;/code&gt; or &lt;code&gt;.client(fn)&lt;/code&gt; - isomorphic tools are a first-class concept, not a pattern. If your product has tools that genuinely live on both sides (a "highlight this row" client tool next to a "charge the card" server tool), TanStack's model will feel like it was designed by someone who shipped that product. Because it probably was.&lt;/p&gt;

&lt;p&gt;The loop itself: the AI SDK gives you &lt;code&gt;stopWhen&lt;/code&gt; conditions (&lt;code&gt;stepCountIs&lt;/code&gt;, &lt;code&gt;hasToolCall&lt;/code&gt;, or your own function) on &lt;code&gt;generateText&lt;/code&gt;/&lt;code&gt;streamText&lt;/code&gt;, and a &lt;code&gt;ToolLoopAgent&lt;/code&gt; class when you want the loop as a reusable object with its own instructions, tools, and typed runtime context. TanStack gives you &lt;code&gt;agentLoopStrategy&lt;/code&gt;: a function from loop state to boolean, with &lt;code&gt;maxIterations(n)&lt;/code&gt; and &lt;code&gt;untilFinishReason(...)&lt;/code&gt; as the built-ins, and &lt;code&gt;createChatOptions&lt;/code&gt; to share one loop configuration between server calls and the harness. Functionally these are the same machine. The difference is packaging: Vercel is converging on an Agent class hierarchy (&lt;code&gt;ToolLoopAgent&lt;/code&gt;, &lt;code&gt;WorkflowAgent&lt;/code&gt;, &lt;code&gt;HarnessAgent&lt;/code&gt;), TanStack on composable functions and strategies. I prefer functions for testability; teams that think in objects will prefer the classes. Neither preference should decide this for you.&lt;/p&gt;

&lt;p&gt;Approvals are where v7 quietly pulls ahead on rigor. Both SDKs gate a tool behind human approval - &lt;code&gt;needsApproval&lt;/code&gt; per tool exists in both, and TanStack adds batched approvals and interrupt-based resumption. But AI SDK 7 adds approval policies as functions (auto-approve, auto-deny, escalate) and, for high-risk flows, opt-in HMAC-signed approvals: the approved tool inputs are cryptographically bound to the approval token, and inputs and policies are revalidated before the run resumes. If your agent can move money, merge code, or delete data, that is not decoration. That is the control your auditor will ask about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured output: a result versus a message part
&lt;/h2&gt;

&lt;p&gt;Ask each SDK for JSON and you get a philosophical difference. The AI SDK treats structured output as a result you await: &lt;code&gt;generateObject&lt;/code&gt;, &lt;code&gt;streamObject&lt;/code&gt;, or the newer &lt;code&gt;Output.object()&lt;/code&gt; on a text call, with partial results streaming as the object fills in. TanStack treats it as a message part: &lt;code&gt;chat({ outputSchema })&lt;/code&gt; streams a typed object into the conversation, alongside text and tool calls, and it stays in the history like any other part. Under the hood both parse partial JSON as it arrives - the AI SDK exports &lt;code&gt;parsePartialJson&lt;/code&gt;, and &lt;code&gt;@tanstack/ai&lt;/code&gt; depends on a &lt;code&gt;partial-json&lt;/code&gt; parser; the mechanics are the same.&lt;/p&gt;

&lt;p&gt;Which model is right depends on what the object is. If the object is the end of the interaction - an extraction, a classification, a form fill - the result model is cleaner and the AI SDK's version is the more mature one, with dedicated error types like &lt;code&gt;NoObjectGeneratedError&lt;/code&gt; when the model fails to produce valid output. If the object is a step inside a longer conversation - a table the user keeps refining with the model - TanStack's part model saves you from bolting a side channel onto your message history. Know which of the two your product actually does before you let this axis sway you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Durability: the question that decides this in 2027
&lt;/h2&gt;

&lt;p&gt;An in-memory agent loop has a simple failure mode: the process restarts, the run is gone. Both projects know agents are outgrowing request-scoped loops, and both shipped an answer in the last five months. The answers are not equivalent, and this is the axis where I see the most confused takes.&lt;/p&gt;

&lt;p&gt;Vercel's answer is &lt;code&gt;WorkflowAgent&lt;/code&gt; in &lt;code&gt;@ai-sdk/workflow&lt;/code&gt;. Execution state persists to durable storage between steps, so a run survives deploys, process restarts, interruptions, and approvals that take hours. It is GA, it is the most complete durability story in TypeScript AI today, and the price is written in the import: your agent now runs on the Workflow SDK's execution model, with its directives and its platform assumptions. Durable, yes. Portable, no.&lt;/p&gt;

&lt;p&gt;TanStack's answer comes in two layers. The 1.0 core has a detachable run model - detach a run, journal it, let another process take it over, reap orphans - designed to work on any of its 11 sandbox providers rather than one platform's workflow engine. On top of that, the new &lt;code&gt;@tanstack/ai-harness&lt;/code&gt; adds durable sessions: a session log per thread, waits for approvals that survive restarts, mid-turn steering. But the harness ships at 0.x, explicitly experimental, and TanStack's own 1.0 post links to its recovery limits, including child-agent waits held in memory that do not resume across hosts. I respect the honesty. I would not yet bet a billing-critical agent on it.&lt;/p&gt;

&lt;p&gt;So the durability verdict is asymmetric. Need durable agents in production now: AI SDK plus Workflow, eyes open about the lock-in. Need runs that outlive a request but not a deploy, on infrastructure you own: TanStack's detachable runs are the more portable shape, with an experimental label you must price in. Need neither: ignore this axis entirely, it is the one most teams over-buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Middleware, telemetry, and the 3 a.m. test
&lt;/h2&gt;

&lt;p&gt;When an agent misbehaves in production, the SDK you want is the one that shows you the run. AI SDK 7 rebuilt telemetry around a single registration: &lt;code&gt;registerTelemetry(new OpenTelemetry())&lt;/code&gt; at startup instruments every SDK function, emitting traces that follow the latest GenAI semantic conventions, covering each step, tool execution, and usage record. It also emits on a Node.js tracing channel, adds per-step performance statistics (time to first output, tokens per second), and fires consistent lifecycle callbacks. Langfuse, Datadog, Sentry, LangSmith and friends plug into this. It is the most complete observability story I have seen in a TypeScript AI SDK, full stop.&lt;/p&gt;

&lt;p&gt;TanStack's telemetry story is younger but pointed the right way: &lt;code&gt;@tanstack/ai&lt;/code&gt; peer-depends on &lt;code&gt;@opentelemetry/api&lt;/code&gt; and ships an &lt;code&gt;./middlewares/otel&lt;/code&gt; subpath, and its middleware model is broader than Vercel's in one respect. &lt;code&gt;wrapLanguageModel()&lt;/code&gt; wraps the model - it sees prompts and completions. TanStack chat middleware hooks the whole run: config, chunks, tool calls, usage, finish. Persistence and memory are themselves middleware (&lt;code&gt;withPersistence&lt;/code&gt;, &lt;code&gt;memoryMiddleware&lt;/code&gt;), which tells you the middleware is a real extension point and not a garnish. For local debugging, TanStack's in-app devtools panel, showing tool calls and finish reasons on one timeline across server and client, is the tool I would reach for first. Verdict on this axis: production observability goes to Vercel today, and it is not close. Extension-point design goes to TanStack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing, errors, and the small print
&lt;/h2&gt;

&lt;p&gt;Both SDKs let you test without burning tokens, which should be table stakes and is not, across this industry. The AI SDK ships &lt;code&gt;ai/test&lt;/code&gt; with &lt;code&gt;MockLanguageModelV3&lt;/code&gt; and &lt;code&gt;simulateReadableStream&lt;/code&gt;; I verified both import cleanly from the installed package. TanStack ships &lt;code&gt;@tanstack/ai/testing&lt;/code&gt; with &lt;code&gt;fakeText()&lt;/code&gt; and a &lt;code&gt;FakeTextAdapter&lt;/code&gt;, and its own end-to-end suite runs every adapter against deterministic model fixtures with no live calls. Parity, honestly earned on both sides.&lt;/p&gt;

&lt;p&gt;Errors are where senior engineers should linger. The AI SDK exports a designed taxonomy: &lt;code&gt;APICallError&lt;/code&gt;, &lt;code&gt;RetryError&lt;/code&gt;, &lt;code&gt;InvalidToolInputError&lt;/code&gt;, &lt;code&gt;ToolCallRepairError&lt;/code&gt; (yes, the SDK can attempt to repair a malformed tool call), &lt;code&gt;InvalidToolApprovalSignatureError&lt;/code&gt;, &lt;code&gt;NoObjectGeneratedError&lt;/code&gt;, and dozens more. You can catch precisely and retry precisely. TanStack's error surface is smaller and younger - a &lt;code&gt;StandardSchemaValidationError&lt;/code&gt;, interrupt-resume validation errors, capability errors - adequate, not yet a taxonomy. One more small-print item in Vercel's favor: AI SDK 7 has first-class timeouts - total, per-step, per-chunk, per-tool - as typed config with a &lt;code&gt;TimeoutError&lt;/code&gt;. I found no equivalent single timeout surface in TanStack's 1.0 core exports; its docs lean on &lt;code&gt;chat({ retry })&lt;/code&gt; for rate-limited calls instead. And one in TanStack's favor: it runs on Node 18+. The &lt;code&gt;ai&lt;/code&gt; package now requires Node 22 or later, per its own &lt;code&gt;engines&lt;/code&gt; field. If you maintain a fleet pinned to older Node, that line in a package.json can decide this whole comparison for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Vercel AI SDK 7&lt;/th&gt;
&lt;th&gt;TanStack AI 1.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Current version (Nov 2, 2026)&lt;/td&gt;
&lt;td&gt;ai 7.0.159&lt;/td&gt;
&lt;td&gt;@tanstack/ai 1.0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wire protocol&lt;/td&gt;
&lt;td&gt;Own UI Message Stream&lt;/td&gt;
&lt;td&gt;AG-UI (open, bidirectional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type safety&lt;/td&gt;
&lt;td&gt;Per provider; typed tools and message parts&lt;/td&gt;
&lt;td&gt;Per model; typed options and client messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool definition&lt;/td&gt;
&lt;td&gt;tool() on the server; client tools via transport&lt;/td&gt;
&lt;td&gt;One toolDefinition(), .server() or .client()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent loop&lt;/td&gt;
&lt;td&gt;stopWhen conditions; ToolLoopAgent class&lt;/td&gt;
&lt;td&gt;agentLoopStrategy functions; maxIterations()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approvals&lt;/td&gt;
&lt;td&gt;needsApproval, policy functions, HMAC-signed approvals&lt;/td&gt;
&lt;td&gt;needsApproval, batched approvals, interrupts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;Output.object(), generateObject() - a result&lt;/td&gt;
&lt;td&gt;chat({ outputSchema }) - a streamed message part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable runs&lt;/td&gt;
&lt;td&gt;WorkflowAgent (GA, Workflow SDK)&lt;/td&gt;
&lt;td&gt;Detachable runs; harness sessions (experimental 0.x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middleware&lt;/td&gt;
&lt;td&gt;wrapLanguageModel() around the model&lt;/td&gt;
&lt;td&gt;Named hooks across the whole run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telemetry&lt;/td&gt;
&lt;td&gt;registerTelemetry, OTel GenAI conventions, tracing channel&lt;/td&gt;
&lt;td&gt;OTel middleware subpath; in-app devtools panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;MockLanguageModelV3 in ai/test&lt;/td&gt;
&lt;td&gt;fakeText() in @tanstack/ai/testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frameworks&lt;/td&gt;
&lt;td&gt;React, Vue, Svelte, Angular&lt;/td&gt;
&lt;td&gt;React, Vue, Solid, Svelte, Preact, Angular, Octane, Remix, vanilla, React Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Providers&lt;/td&gt;
&lt;td&gt;~38 first-party packages; Gateway built into core&lt;/td&gt;
&lt;td&gt;23 adapters plus any OpenAI-compatible endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node requirement&lt;/td&gt;
&lt;td&gt;22+&lt;/td&gt;
&lt;td&gt;18+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Migration cost, in both directions
&lt;/h2&gt;

&lt;p&gt;Moving from the AI SDK to TanStack is two migrations wearing a trench coat. The server calls are the cheap one: the mapping table above is real, and a route handler converts in under an hour. The protocol is the expensive one: every component that reads UI Message Stream parts, every AI Elements widget, your approval UI, your stream resumption logic, and your stored message format all change dialect at once. TanStack also imposes a discipline the AI SDK does not: &lt;code&gt;threadId&lt;/code&gt; is required on client and server in 1.0, and neither side invents one for you. That is correct design and still a migration task. Call the whole move days for a real chat product, and do it for the protocol or the typing, not for fashion.&lt;/p&gt;

&lt;p&gt;Moving the other way, from TanStack to the AI SDK, has the mirror-image cost, plus one recurring tax worth naming: cadence. The AI SDK shipped v5 in 2025, v6 on December 22, 2025, and v7 on June 25, 2026 - a major roughly every six months, each with a codemod (&lt;code&gt;npx @ai-sdk/codemod v7&lt;/code&gt; this round). The codemods are good and the migration guides are honest, but a major a half-year is a standing maintenance budget you should enter with open eyes. TanStack's 1.0 is explicitly a stability commitment - their words are "our formal commitment to the APIs you build on" - from a team of three maintainers, with 143 contributors. Believe the commitment; price the team size. Bus factor is an architectural property too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each one breaks
&lt;/h2&gt;

&lt;p&gt;The AI SDK breaks at the platform seam. The Gateway inside the core package, string model ids that route through it, the Workflow SDK under durable agents, the sandbox story centered on Vercel Sandbox: each default is reasonable, and together they mean the path of least resistance ends on Vercel's platform. You can decline every one of those defaults. Most teams, under deadline, will not. It also breaks at upgrade time, when the six-month major cadence meets a codebase with a hundred streamText call sites.&lt;/p&gt;

&lt;p&gt;TanStack breaks at the ecosystem seam. Three maintainers. A harness that is honest about being experimental. Fewer production war stories, fewer answered Stack Overflow equivalents, fewer hires who have run it before. Its devtools and docs are excellent, but when your AG-UI stream does something strange at scale, the crowd you can ask is smaller. And its very flexibility - bring your own persistence, your own store, your own sandbox provider - means the integration work Vercel does for you lands on your plate instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict: what I would pick, and why
&lt;/h2&gt;

&lt;p&gt;Default pick: &lt;strong&gt;the Vercel AI SDK 7&lt;/strong&gt;, for a Next.js product that needs to be in production this quarter and stay observable, testable, and debuggable when it gets there. Maturity is a feature. Sixteen million weekly downloads is a lot of other people's bug reports finding your bugs before you do, and the telemetry, timeout, and approval-hardening work in v7 is aimed exactly at the failures I have actually had in production AI features.&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;TanStack AI&lt;/strong&gt; instead when any of these is true for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your frontend is not React-only, or might not stay that way. Nine framework bindings and an open protocol mean the AI layer survives a frontend rewrite.&lt;/li&gt;
&lt;li&gt;You refuse platform gravity on principle or on contract. There is no gateway in the core, no platform layer at all, and the protocol is a public spec.&lt;/li&gt;
&lt;li&gt;Your tools live on both client and server, and you want one definition for them.&lt;/li&gt;
&lt;li&gt;Model-option type errors have burned you, and per-model typing is worth switching for.&lt;/li&gt;
&lt;li&gt;You are pinned to Node 18 or 20.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the flip conditions for my default: the day TanStack's harness hits a stable 1.0 with its recovery limits closed, the durability axis stops pointing at Vercel, and this comparison gets much harder. The day your app needs a non-React client speaking to the same agent backend, it flips immediately.&lt;/p&gt;

&lt;p&gt;Whichever you pick, wrap it. Put your own thin interface between your product code and the SDK's call sites - one module that owns chat, tools, and model selection. I made that case in full when I &lt;a href="https://dev.to/blog/ai-agent-typescript-harness-200-lines"&gt;built an agent harness in 200 lines of plain TypeScript&lt;/a&gt;: the loop is small, and owning the seam means an SDK migration, in either direction, is a week of work instead of a rewrite. If you are scoping an AI feature build and want a second pair of eyes on the architecture before you commit to either SDK, &lt;a href="https://dev.to/blog/hire-senior-nextjs-nestjs-developer-pakistan"&gt;that is the kind of work I take on&lt;/a&gt; - or just &lt;a href="https://dev.to/#contact"&gt;say hello&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is TanStack AI production-ready at 1.0?
&lt;/h3&gt;

&lt;p&gt;The core is. TanStack shipped 1.0 on October 23, 2026 with a formal stability commitment: core APIs are stable, deprecated 0.x APIs were removed, and upgrades from late 0.x are mostly a codemod plus a type check. The agent harness is the exception - it ships as 0.x, is explicitly experimental, and can change in a minor release. Build on chat(), tools, and middleware today; treat the harness as a beta you pin carefully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use TanStack AI with Next.js, or does it need TanStack Start?
&lt;/h3&gt;

&lt;p&gt;Next.js works fine. The docs list App Router API routes as a supported host, alongside TanStack Start, React Router v7, Express, Remix 3, and React Native. The server half is just a route handler that returns a streaming Response, and the client half is useChat from @tanstack/ai-react. Nothing in the 1.0 core requires TanStack Start.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is AG-UI, and why does the wire protocol matter?
&lt;/h3&gt;

&lt;p&gt;AG-UI is an open protocol for agent-to-UI communication: typed events for text, tool calls, state, and run lifecycle, flowing in both directions. TanStack AI speaks it natively - the @ag-ui/core package is a direct dependency of @tanstack/ai. The Vercel AI SDK speaks its own UI Message Stream protocol. The protocol decides which clients, bridges, and third-party agent UIs can talk to your backend without a translation layer, and it is the most expensive part of either SDK to replace later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which SDK has better type safety?
&lt;/h3&gt;

&lt;p&gt;TanStack AI goes one level deeper. The AI SDK gives you typed tool inputs, typed UI message parts, and provider packages with their own option types, which is already strong. TanStack types the model options per model: pick a model and TypeScript narrows the options and content types to what that specific model supports, so an unsupported option is a compile error instead of a runtime surprise. On ordinary tool and message typing, the two are comparable.&lt;/p&gt;

&lt;h3&gt;
  
  
  How hard is it to migrate from the Vercel AI SDK to TanStack AI?
&lt;/h3&gt;

&lt;p&gt;The call sites map almost one to one: streamText becomes chat(), generateObject becomes chat({ outputSchema }), stopWhen: stepCountIs(n) becomes agentLoopStrategy: maxIterations(n). The expensive part is the wire: your client stops speaking the UI Message Stream protocol and starts speaking AG-UI, so the chat UI, any AI Elements components, and anything that consumes the stream gets rewritten. Budget days for the protocol swap, hours for the server calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which one should I pick for a durable, long-running agent?
&lt;/h3&gt;

&lt;p&gt;Today, the Vercel AI SDK, if you accept its terms. AI SDK 7's WorkflowAgent persists execution state between steps, so runs survive deploys, restarts, and delayed approvals - but durability runs on the Workflow SDK, which is Vercel's execution model. TanStack's answer is a detachable run model (detach, journal, takeover) that works on any sandbox provider, plus a harness with durable sessions that is still experimental at 0.x and has documented recovery limits. If the run must survive a restart in production this quarter, WorkflowAgent is the proven path and portability is the price.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Hiring a Senior Next.js + NestJS Developer from Pakistan</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Sun, 01 Nov 2026 05:27:28 +0000</pubDate>
      <link>https://dev.to/zforzain2000/hiring-a-senior-nextjs-nestjs-developer-from-pakistan-57pp</link>
      <guid>https://dev.to/zforzain2000/hiring-a-senior-nextjs-nestjs-developer-from-pakistan-57pp</guid>
      <description>&lt;p&gt;&lt;br&gt;
  If you are hiring for a Next.js frontend and a NestJS backend, you have probably already felt the failure mode: two hires, two vocabularies, and an API contract that nobody owns. I work the other way. I am a full-stack engineer based in Pakistan, I ship both sides of that stack, and this page states plainly what I have shipped, how the overlap works, what the process looks like, and what I will not pretend to know. Rates are on request, and I explain why below instead of printing a number that would be wrong for half the projects that read it.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Hire me when you need one senior engineer to own a production Next.js plus NestJS system end to end, from the database schema to the deployed frontend, with real-time or queue-backed work inside it. I am based in Pakistan in UTC+5, I work remote-first, and I am open to senior roles and select contract projects. Start with a free 30-minute conversation, then a one-week scope step, and only then a quote tied to that scope.&lt;/p&gt;

&lt;p&gt;That is the whole offer. The rest of this page is the evidence behind it, plus a vetting checklist you can use on any candidate, including me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have actually shipped
&lt;/h2&gt;

&lt;p&gt;You should not hire on a stack list. Hire on systems that survived production, because production is where the easy answers break. My resume page states the headline honestly: 15-plus production projects across five client domains, shipping since January 2024, and I am a full-time software engineer at OBSCODE in Pakistan. The four flagship platforms on my work page are the proof I point hiring managers to first, because each one forced a different hard problem.&lt;/p&gt;

&lt;p&gt;Healthcare Ireland is the real-time and multi-tenant case. It is an enterprise platform for a major Irish healthcare group covering patient management, scheduling, clinical notes, billing, and analytics across branches and departments. I architected the frontend and contributed to the NestJS backend, on a stack that includes Socket.io for real-time work, BullMQ for background jobs, PostgreSQL, and Prisma. If your product has rooms, live updates, or jobs that must not silently fail, this is the closest proof I have.&lt;/p&gt;

&lt;p&gt;First Choice Recruitment is the four-app integration case. It is an end-to-end recruitment system spanning four interconnected applications and one NestJS backend, from a public application form through applicant processing, staff onboarding, and timesheet management. I led end-to-end development. The lesson that project taught me, and the one I now design for first, is that a shared backend only helps if the data model is designed for all four apps on day one. Bolt it on later and every new app pays the tax.&lt;/p&gt;

&lt;p&gt;ComplianceBot AI is the type-safe AI case. It is a multi-tenant SaaS product that runs an adaptive AI interview and generates type-safe construction-compliance documents, built with Next.js, TypeScript, Supabase, Google Gemini, the Vercel AI SDK, and Zod. I architected the full-stack application. The part that matters for hiring is not the AI demo. It is the boundary discipline: every AI output is validated against a Zod schema before it touches a document a client relies on.&lt;/p&gt;

&lt;p&gt;Otrip.io is the ecosystem case. It is a four-module travel platform with a consumer app, a marketing site, an admin panel, and a blog CMS, held together by one shared frontend architecture. I was frontend lead. The hiring signal here is restraint: one architecture, four surfaces, no drift into four separate codebases that happen to share a logo.&lt;/p&gt;

&lt;p&gt;You can read all four in full on my &lt;a href="https://dev.to/work"&gt;work page&lt;/a&gt; and my &lt;a href="https://dev.to/resume"&gt;resume&lt;/a&gt;. I would rather you read those and judge the work than take this page at face value.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack I ship in, and what each piece is for
&lt;/h2&gt;

&lt;p&gt;A stack list without a reason is marketing. Here is mine, with the job each piece does and the project that proved it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What it does in my systems&lt;/th&gt;
&lt;th&gt;Where it proved itself&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next.js + React&lt;/td&gt;
&lt;td&gt;The product surface: marketing pages, dashboards, and app screens in one TypeScript codebase&lt;/td&gt;
&lt;td&gt;ComplianceBot AI, Otrip.io, First Choice Recruitment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NestJS + TypeScript&lt;/td&gt;
&lt;td&gt;The backend: typed modules, guards, queues, and scheduled work behind one API&lt;/td&gt;
&lt;td&gt;Healthcare Ireland, First Choice Recruitment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL + Prisma&lt;/td&gt;
&lt;td&gt;The source of truth: relational data with typed access and migrations I can review&lt;/td&gt;
&lt;td&gt;Healthcare Ireland, First Choice Recruitment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Socket.io&lt;/td&gt;
&lt;td&gt;Real-time rooms and live updates when polling would lag or hammer the server&lt;/td&gt;
&lt;td&gt;Healthcare Ireland&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BullMQ + Redis&lt;/td&gt;
&lt;td&gt;Background jobs with retries, backoff, and dead-letter handling for work that must not vanish&lt;/td&gt;
&lt;td&gt;Healthcare Ireland&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel AI SDK + Zod&lt;/td&gt;
&lt;td&gt;AI features where the model output is validated before it becomes a record&lt;/td&gt;
&lt;td&gt;ComplianceBot AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;Auth, storage, and Postgres hosting where it removes undifferentiated backend work&lt;/td&gt;
&lt;td&gt;ComplianceBot AI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your project needs a piece outside this list, I will tell you in the first conversation whether I can learn it honestly on your timeline or whether you need someone who already ships in it. I do not pad a stack list to win a call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time zone overlap, stated without spin
&lt;/h2&gt;

&lt;p&gt;I am based in Pakistan in UTC+5 and I work remote-first. The overlap I state on my own site, and the only overlap claim I will make here, is this: a full working day of overlap with Europe, and a morning overlap with the US East Coast.&lt;/p&gt;

&lt;p&gt;That shapes how I run a project. Overlap hours are for decisions, demos, and unblocking. Async hours are for deep build work, with written updates so nobody wakes up to a mystery. Weekly demos are the forcing function: you see the same code I see, every week, instead of discovering drift at the end of a milestone.&lt;/p&gt;

&lt;p&gt;If your team needs someone online through a full US West Coast afternoon, I am not that hire, and I would rather lose the project in this paragraph than in week three. If your team runs on European hours, or on US East Coast mornings plus async afternoons, the overlap fits cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hiring me versus the other two options
&lt;/h2&gt;

&lt;p&gt;Most hiring managers I talk to are choosing between three shapes, not thirty candidates. Here is the honest tradeoff for each, including the cases where I lose.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;When it wins&lt;/th&gt;
&lt;th&gt;Where it hurts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One senior full-stack engineer (me)&lt;/td&gt;
&lt;td&gt;One owner for the frontend, backend, and the contract between them; faster decisions; one throat to choke, in the good sense&lt;/td&gt;
&lt;td&gt;One person cannot parallelize like a team; a very large scope needs more hands no matter how senior the hire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An agency&lt;/td&gt;
&lt;td&gt;You need a whole team at once and you want the agency to absorb hiring risk&lt;/td&gt;
&lt;td&gt;You pay for account management and handoffs; the engineer who sold the project is rarely the engineer who builds it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A junior in-house hire&lt;/td&gt;
&lt;td&gt;You want someone full-time inside your culture and you have senior engineers free to mentor&lt;/td&gt;
&lt;td&gt;Mentoring is real work, and a junior hire plus no senior reviewer is how a NestJS backend accumulates silent data bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My verdict on this table is simple. If the system is one product with one backend and one frontend, and the risk is in the architecture rather than in headcount, the single senior owner wins. If the risk is sheer volume, hire the team and do not pretend one person is a team.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to vet any senior NestJS developer, including me
&lt;/h2&gt;

&lt;p&gt;Use this checklist on me, and on every other candidate you screen. It is built from the failure modes I have actually seen, not from interview trivia.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask for a shipped system that matches your constraint, not your stack.&lt;/strong&gt; A candidate who built a todo app in NestJS and a candidate who shipped multi-tenant billing in NestJS both list the same framework. Only one has felt the constraint you are hiring for. Ask what broke in production and what changed after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask how tenant data stays isolated.&lt;/strong&gt; In a multi-tenant system, the dangerous bug is not a crash. It is one tenant seeing another tenant's data, quietly. A senior answer talks about row-level rules, query scoping, and tests that prove isolation, not just about middleware existing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask what happens when a queue job fails twice.&lt;/strong&gt; Retries without backoff hammer a failing dependency. Retries without a dead-letter queue lose the job silently. A senior answer names the retry policy, the backoff, the dead-letter destination, and the alert that fires when jobs land there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask how real-time state survives a second server.&lt;/strong&gt; Socket.io on one instance is easy. Rooms across instances need an adapter, usually Redis, and a plan for what happens when a client reconnects to a different instance. If the candidate has never scaled past one server, that is fine, as long as they say so and can describe the step they would take first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask how AI output is validated before it becomes a record.&lt;/strong&gt; If the product touches AI at all, an unvalidated model response is a data-integrity bug with good marketing. Zod validation at the boundary, with a retry or repair path, is the minimum honest answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask what they would refuse to build.&lt;/strong&gt; A candidate who agrees to everything has not thought about your project yet. A senior engineer has a list of things they will push back on, and reasons tied to your users, not to their preferences.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a candidate answers all six with specifics, you have a senior hire. If the answers stay at framework-feature level, you have a developer who knows NestJS syntax and has not yet owned a system.&lt;/p&gt;

&lt;h2&gt;
  
  
  How working with me runs
&lt;/h2&gt;

&lt;p&gt;My homepage already states this process, and I run it the same way for contract work so there are no surprises. Four steps, one outcome: your team owns the system before I step away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Conversation, 30 minutes, free.&lt;/strong&gt; You tell me about the problem, the team, and the constraint. I ask the questions an engineer would ask, including the uncomfortable ones about scope and timeline. If the fit is wrong, this is where we find out, at zero cost to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Scope, one week.&lt;/strong&gt; You get an architecture sketch, a risk list, deliverables, and milestones. The scope is honest: no padding, no scope-creep clauses buried in the fine print. This is also where a real quote becomes possible, because a quote needs a scope to attach to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Build, project-dependent.&lt;/strong&gt; Type-safe stack, accessibility from inception, weekly demos. You see the same code I see. If a demo surfaces a wrong turn, we correct it in that week, not at the end of the engagement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Ship and handoff, final week.&lt;/strong&gt; Production hardening, observability, and a runbook. The handoff is not a document dump. It is the point where your team can run, extend, and debug the system without me, and that is the test I hold myself to.&lt;/p&gt;

&lt;p&gt;If you are hiring for a full-time senior role rather than a contract, the same instinct applies in a different shape: the first conversation is still about the problem and the constraint, and the first week is still about learning the system before proposing rewrites. I do not join a team and redesign its backend in week one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rates, stated plainly
&lt;/h2&gt;

&lt;p&gt;Rates are on request. I know that frustrates a reader who wants a number, so here is the reasoning instead of a dodge.&lt;/p&gt;

&lt;p&gt;A flat rate on this page would be wrong for most projects that read it. A two-week integration and a multi-tenant platform with real-time rooms and queue-backed billing are not the same job, and pricing them identically would either overcharge the small project or under-resource the large one. Both outcomes hurt you.&lt;/p&gt;

&lt;p&gt;What sets the number, once we talk, is scope, duration, and ownership: how much of the system I own, how long the engagement runs, and what handoff looks like. The free conversation and the one-week scope step exist precisely so the quote attaches to something real. If you need a budget range to take to a decision-maker before that conversation, say so in the first message and I will tell you honestly whether your range and the scope can meet, rather than quoting a number I cannot stand behind.&lt;/p&gt;

&lt;p&gt;I will also be direct about what I do not do here. I do not publish a rate I have not quoted for a scoped project, I do not compete on being the cheapest option in a race to the bottom, and I do not discount by quietly cutting the handoff week. The handoff is the part that makes the system yours, and cutting it is not a saving.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I will not take on
&lt;/h2&gt;

&lt;p&gt;A hiring page that accepts everything is not credible, so here are my boundaries.&lt;/p&gt;

&lt;p&gt;I do not take on projects where the backend is an afterthought and the real ask is a landing page with a login form. That work deserves a frontend specialist, not a NestJS hire. I do not take on a rewrite where nobody can state what is wrong with the current system in one paragraph, because a rewrite without a diagnosis is a new system wearing an old system's name. And I do not take on engagements where the team cannot give one person ownership of the API contract, because a contract owned by committee is owned by no one, and the frontend and backend will drift apart in the first sprint.&lt;/p&gt;

&lt;p&gt;If your project falls outside those lines, the free conversation is still useful: I will say which shape fits better, even if that shape is not me.&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict: who should hire me, and who should not
&lt;/h2&gt;

&lt;p&gt;Hire me if you are building or fixing a production TypeScript system where the backend matters as much as the frontend, where multi-tenant data, real-time updates, or queue-backed work is part of the product rather than a feature request, and where you want one senior owner who will argue about architecture in week one and hand you a system your team can run in the final week. That is the work my four flagship platforms prove, and it is the work I do best.&lt;/p&gt;

&lt;p&gt;Do not hire me if you need a large team tomorrow, if you need full US West Coast afternoon coverage, or if you want the cheapest bid and are willing to trade the handoff week for it. Those are legitimate needs, and they point to a different hire. I would rather you read that here than discover it after a contract starts.&lt;/p&gt;

&lt;p&gt;If the first paragraph describes your project, the next step is simple and free. Start a conversation through the contact section on my homepage, tell me the problem and the constraint, and we will find out in 30 minutes whether this is a fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Are you available for full-time roles, contracts, or both?
&lt;/h3&gt;

&lt;p&gt;Both, within limits. I am open to senior roles and select contract projects, and I work remote-first from Pakistan in UTC+5. The first step is a free 30-minute conversation about the problem, the team, and the constraint. That call is where we find out if the fit is real, before either side commits to a scope or a rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why hire a developer from Pakistan for a Next.js and NestJS project?
&lt;/h3&gt;

&lt;p&gt;The practical reasons are overlap and a full-stack pairing that is hard to split. I work in UTC+5, which gives a full working day of overlap with Europe and a morning overlap with the US East Coast. On the technical side, one engineer who owns both the Next.js frontend and the NestJS backend removes the handoff where most full-stack projects lose time: the API contract between two teams.&lt;/p&gt;

&lt;h3&gt;
  
  
  What stack do you actually ship in?
&lt;/h3&gt;

&lt;p&gt;Next.js and React on the frontend, NestJS with TypeScript on the backend, PostgreSQL as the database, and Prisma where it fits. For real-time work I use Socket.io, and for background work I use BullMQ on Redis. For type-safe AI features I use the Vercel AI SDK with Zod validation at every boundary. That is the stack behind the production platforms on my work page, not a list of tools I have only read about.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do your rates work?
&lt;/h3&gt;

&lt;p&gt;Rates are on request, and I will not put a number on this page that I have not earned the right to quote. A fixed rate without scope is a guess, and a guess helps no one. After the free conversation and a one-week scope step with an architecture sketch, a risk list, deliverables, and milestones, you get a real quote tied to that scope. If the scope does not fit, I say so in that first call.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the first week of working with you look like?
&lt;/h3&gt;

&lt;p&gt;It looks like scope, not code. Week one produces an architecture sketch, a risk list, deliverables, and milestones, with honest scope and no padding. Then the build runs on weekly demos, so you see the same code I see, and the final week covers production hardening, observability, and a runbook so your team owns the system before I step away.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should I check before hiring any senior NestJS developer?
&lt;/h3&gt;

&lt;p&gt;Ask for a system they shipped that matches your constraint, not just your stack. Ask how they handle multi-tenant data isolation, how they test a queue that must not lose a job, and what they do when a real-time feature has to scale past one server. A senior answer names a tradeoff and a failure mode. A vague answer names a framework feature and stops there.&lt;/p&gt;

&lt;p&gt;Start the conversation at &lt;a href="https://dev.to/#contact"&gt;/#contact&lt;/a&gt;. Tell me the problem, the team, and the constraint, and I will reply with honest questions before I reply with a quote.&lt;/p&gt;

&lt;p&gt;{/* MONETIZATION NOTE: this is a freelance-lead page, not an affiliate post. No affiliate links are claimed. The CTA points at /#contact, the homepage contact section. */}&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>nestjs</category>
      <category>hiring</category>
      <category>freelancing</category>
    </item>
    <item>
      <title>Your AI Agent Needs No Framework: A 200-Line TypeScript Harness</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Sat, 31 Oct 2026 05:23:07 +0000</pubDate>
      <link>https://dev.to/zforzain2000/your-ai-agent-needs-no-framework-a-200-line-typescript-harness-13c8</link>
      <guid>https://dev.to/zforzain2000/your-ai-agent-needs-no-framework-a-200-line-typescript-harness-13c8</guid>
      <description>&lt;p&gt;&lt;br&gt;
  I have watched too many agent projects start with a framework install and end with a debugging session inside someone else's abstraction. The agent itself is not complicated. It is a loop: send messages to a model, run the tools it asks for, send the results back, and stop when it stops asking. I built that loop in plain TypeScript, ran it against a stub model to prove the limits work, and kept it. This post is that loop, the reasons I trust it, and the honest list of cases where I would still pick a framework.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Your agent does not need a framework. It needs a loop, a tool registry, three limits, and one approval gate. That is just over 200 lines of TypeScript, and every line is code you can read, test, and change when the agent misbehaves. A framework earns its place later, when you need durable runs, multi-agent routing, or observability you do not want to build. It does not earn its place on day one, when you have one agent and five tools and the framework is the biggest thing in the repo.&lt;/p&gt;

&lt;p&gt;This is not a new claim. InfoQ's October 26, 2026 piece on agentic harnesses traces the same path: linear prompt chains first, then graph workflows with branching and retries, and now code-executing harnesses that verify outputs by running them. The advice in that piece matches what I found in practice: validate model outputs, and pick the least complex design that meets your reliability, speed, and cost needs. Anthropic made a related point the same month, arguing for folding subagents into one main loop with progressively disclosed tools, because fewer lossy handoffs mean better quality at lower cost and latency. Both point the same way: the loop is the product, and the framework is optional packaging around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a harness actually is
&lt;/h2&gt;

&lt;p&gt;Strip the marketing away and a harness has six parts. Thoughtworks' harness guide lists them as instructions and context, tools, environment, state, feedback from tests and linters, and boundaries with human approval for high-risk actions. I agree with that list, and my loop covers the first five directly while the approval gate covers the sixth:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The loop.&lt;/strong&gt; One while loop that owns the message history and decides when to stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool registry.&lt;/strong&gt; A map from tool name to a description, a schema, and a handler. Nothing else in the system knows how tools work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The limits.&lt;/strong&gt; Max turns, max cost, and a repeated-call guard. These are the whole safety story for a first agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model interface.&lt;/strong&gt; One method, &lt;code&gt;complete()&lt;/code&gt;, so swapping models means swapping an adapter, not rewriting the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The verifier.&lt;/strong&gt; A check that runs on the final answer before the run counts as done. Mine is a function you pass in, not a second agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The approval gate.&lt;/strong&gt; Write actions and money actions wait for a human yes. Reads run free.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice what is missing: no graph DSL, no agent class hierarchy, no plugin system, no state machine framework. Those are fine tools, but each one is a bet that your agent will grow into needing it. My bet is the opposite one: most agents never grow past ten tools, and the ones that do are easier to migrate once you know exactly which part hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop, in full
&lt;/h2&gt;

&lt;p&gt;Here is the core. It is provider-agnostic, it has no dependencies, and I ran it against a stub model before writing this post to confirm the turn limit, the cost budget, the repeated-call guard, and the approval gate all fire as described.&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;// harness.ts - the loop, the registry, and the limits. No dependencies.&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;Message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&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="s2"&gt;assistant&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="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;content&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;toolCallId&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;toolCalls&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;[];&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;ToolCall&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&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;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&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;ModelResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;text&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;toolCalls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;costUsd&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="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ModelClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolDef&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;ModelResponse&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolDef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&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;description&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;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;needsApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;unknown&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Limits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;maxTurns&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="nl"&gt;maxCostUsd&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="nl"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;toolName&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="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;answer&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&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;AgentResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;answer&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;turns&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="nl"&gt;costUsd&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="o"&gt;|&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turn_limit&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="s2"&gt;cost_limit&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="s2"&gt;repeated_call&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="s2"&gt;blocked&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="s2"&gt;failed_verification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;turns&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="nl"&gt;costUsd&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;function&lt;/span&gt; &lt;span class="nf"&gt;sameCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&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 types are the contract. &lt;code&gt;Message&lt;/code&gt; is the only memory the agent has. &lt;code&gt;ToolDef&lt;/code&gt; is the only place a tool is defined. &lt;code&gt;Limits&lt;/code&gt; is the only place stopping rules live. When something goes wrong in production, one of these three places is where you look, and that is the whole point of keeping the harness small.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runAgent&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="kr"&gt;string&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="nx"&gt;ModelClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolDef&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Limits&lt;/span&gt;&lt;span class="p"&gt;,&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;AgentResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tools&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;t&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="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&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;turns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;costUsd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;lastCall&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;turns&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTurns&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turn_limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;costUsd&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="nx"&gt;costUsd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxCostUsd&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cost_limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;costUsd&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;res&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;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;turns&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="nx"&gt;costUsd&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;costUsd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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="nx"&gt;limits&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;limits&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed_verification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;costUsd&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;costUsd&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;call&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&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="nx"&gt;lastCall&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;sameCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;call&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repeated_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;costUsd&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;lastCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;call&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;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;registry&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;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toolCallId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&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="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Unknown tool: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="k"&gt;continue&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="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needsApproval&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approve&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;ok&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="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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;costUsd&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;try&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;result&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;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toolCallId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&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="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toolCallId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&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="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Tool error: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;message&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="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;Read the failure paths, because they are the harness. An unknown tool becomes a tool result, not a crash, so the model can correct itself. A tool error becomes a tool result for the same reason. A repeated call stops the run instead of burning your budget in a circle. A write tool with no approver defaults to blocked, not to allowed, which is the safe default and the one I would defend in a code review. The verifier runs only on a final answer, so a bad answer fails loudly instead of shipping quietly.&lt;/p&gt;

&lt;p&gt;One judgment call deserves a direct note: I run tool calls in sequence, not in parallel. Parallel calls are faster, and a framework will offer them by default. I keep them sequential in a first harness because tool results can depend on each other, error messages stay in order, and the message history stays readable when you debug a bad run. Add parallelism later, behind the registry, when a measured slow run justifies it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swapping the model without touching the loop
&lt;/h2&gt;

&lt;p&gt;The loop never imports a provider SDK. It talks to &lt;code&gt;ModelClient&lt;/code&gt;, and the adapter translates. This is the OpenAI-compatible adapter I use, built on fetch so it works with any provider that speaks the chat completions shape:&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;// openai.ts - one adapter. Change baseUrl and key to change providers.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ModelClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ToolDef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./harness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OpenAIToolCall&lt;/span&gt; &lt;span class="o"&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&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;arguments&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="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openAIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;apiKey&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;model&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;baseUrl&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="p"&gt;}):&lt;/span&gt; &lt;span class="nx"&gt;ModelClient&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;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.openai.com/v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolDef&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/chat/completions`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&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="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&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="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opts&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="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messages&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;m&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&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="s2"&gt;tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCallId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&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;c&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&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="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tools&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;t&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parameters&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="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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Model request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&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="s2"&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;data&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;choice&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;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OpenAIToolCall&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_calls&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;calls&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;c&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{}&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="na"&gt;costUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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;Two honest gaps in this adapter, stated plainly. First, &lt;code&gt;costUsd&lt;/code&gt; is 0 here because cost needs the usage block and a price table per model, and I did not want to fake a price in sample code. In my own runs I compute cost from &lt;code&gt;data.usage&lt;/code&gt; with a small price map and pass it through, because a cost limit that never moves is decoration. Second, tool arguments are parsed with &lt;code&gt;JSON.parse&lt;/code&gt; and not yet schema-validated in this snippet. Validate them inside each tool's &lt;code&gt;run&lt;/code&gt; function, or wrap the registry with a schema check, before you point this at real data. My Zod-validated output post covers the schema pattern I use for that step, and it ports directly: the schema lives on the tool definition, the handler receives only parsed args.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: refund status, with a gate
&lt;/h2&gt;

&lt;p&gt;Here is the smallest example that still shows the interesting parts: one read tool, one write tool behind approval, and a verifier that refuses an answer without an order id in it.&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;// main.ts - wire the loop to real tools. Reads run free, writes wait.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ToolDef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./harness&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;openAIClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./openai&lt;/span&gt;&lt;span class="dl"&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;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ord_a1b2c3d4e5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ord_f6g7h8i9j0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&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;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolDef&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_order_status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Look up an order by id and return its status.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&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="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orderId&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="na"&gt;needsApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&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="nx"&gt;args&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;orderId&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;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`No order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;issue_refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Refund an order. Only call after confirming the status.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&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="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orderId&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="na"&gt;needsApproval&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="na"&gt;run&lt;/span&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="nx"&gt;args&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&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="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&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="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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check order ord_f6g7h8i9j0 and refund it if it is still processing.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;openAIClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&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;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-5-mini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxTurns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxCostUsd&lt;/span&gt;&lt;span class="p"&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="na"&gt;approve&lt;/span&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Approve &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;args&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;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;APPROVE_WRITES&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yes&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="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answer&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;answer&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="s2"&gt;ord_&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="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The approval callback is deliberately boring: it reads an environment flag in this example, and in a real app it would ask a human in a UI or a chat message. The shape is what matters. The loop never decides whether a write is safe. It asks, and it stops on a no. That separation is the difference between an agent you can demo and an agent you can leave running.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tested, and what I did not
&lt;/h2&gt;

&lt;p&gt;I did not test this against a live model with real money for this post, and I will not pretend otherwise. What I did test, with a stub model that returns scripted tool calls, is the harness logic itself: the turn limit fires, the cost budget fires, the repeated-call guard fires on the second identical call, an unapproved write returns &lt;code&gt;blocked&lt;/code&gt;, and a final answer without an order id fails verification. Those are the behaviors that decide whether a bad run costs you cents or dollars, and they are testable without any API key. Test them in your own copy before you add a real model, because a loop you have never seen fail is a loop you do not understand yet.&lt;/p&gt;

&lt;p&gt;The other test that matters is a cost test with a real model on a small task, with &lt;code&gt;maxCostUsd&lt;/code&gt; set low on purpose. Run ten tasks, record turns and cost per task, and look at the spread. If one task costs ten times the median, read its message history before you raise the budget. The budget is not there to be raised. It is there to tell you which task shape is misbehaving.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would still pick a framework
&lt;/h2&gt;

&lt;p&gt;A 200-line loop is a starting position, not a religion. I would pick a framework, or at least a managed runtime, in four cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Why the loop stops being enough&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs must survive restarts&lt;/td&gt;
&lt;td&gt;A plain loop holds state in memory. A durable runtime checkpoints messages and tool results and resumes after a crash.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several agents coordinate&lt;/td&gt;
&lt;td&gt;Shared state, handoffs, and routing across agents are a second system, not a second loop.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs last days, not minutes&lt;/td&gt;
&lt;td&gt;Retries, backoff, and human approval queues across days need persistence and an operations story.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your team already knows the framework&lt;/td&gt;
&lt;td&gt;A deadline and a team that knows LangGraph beats a clean loop nobody else can maintain.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The test I apply is simple: if the next feature you need is a feature the framework already ships, and you can name the week it will hurt not to have it, adopt it then. If you cannot name that week, the framework is premature, and premature is just another word for a debugging session you scheduled for yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict: what I would pick, and why
&lt;/h2&gt;

&lt;p&gt;If you are building your first agent with a handful of tools, I would build the loop in this post and skip the framework. You will understand every failure mode, your cost limits will be real numbers instead of framework settings you never read, and you will know exactly what to keep when you eventually adopt a framework for durability or scale. The framework decision gets easier, not harder, after you have run your own loop, because you will be choosing it for a named gap instead of a vague fear of building it wrong.&lt;/p&gt;

&lt;p&gt;That verdict flips in one condition: when durability or multi-agent coordination is a requirement on day one, not a someday. Then I would pick the framework first, accept its abstraction cost, and keep my tools behind the same registry shape so the migration stays a rewrite of the loop, not of everything.&lt;/p&gt;

&lt;p&gt;If you build this loop, start with reads only, add one write tool behind approval, and set the cost budget low enough that a bad run embarrasses you instead of billing you. That is the whole method. The model is the least interesting part of the system. The harness is the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an AI agent harness?
&lt;/h3&gt;

&lt;p&gt;The harness is everything around the model that makes an agent reliable: the loop that calls the model, the tools it can call, the limits that stop runaway runs, the approval gates for risky actions, and the checks that verify the result. The model only proposes actions. The harness decides what runs, what the model sees next, and when the work is done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a framework like LangChain or LangGraph to build an agent?
&lt;/h3&gt;

&lt;p&gt;No, not for a single agent with a handful of tools. A tool-calling loop is a while loop over messages, a tool registry, and a few limits. You need a framework when you need durable runs that survive restarts, complex multi-agent routing, built-in observability, or a large tool ecosystem you do not want to maintain yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does the harness stop an agent from looping forever?
&lt;/h3&gt;

&lt;p&gt;Three limits do that job together: a maximum turn count, a maximum cost budget, and a repeated-call guard that stops the run when the model requests the exact same tool call twice in a row. A turn cap alone is not enough, because turns vary wildly in cost, so the cost budget is the binding limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I swap models without rewriting the agent?
&lt;/h3&gt;

&lt;p&gt;Keep the model behind one small interface with a single complete() method. The loop only talks to that interface. An OpenAI-compatible adapter built on fetch can point at OpenAI, or at any provider with a compatible endpoint, by changing the base URL and the key. The loop, the tools, and the limits never change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should tool arguments be validated?
&lt;/h3&gt;

&lt;p&gt;Yes, always, before the handler runs. A model can return malformed JSON, a wrong type, or an argument that looks plausible and is wrong. Validate every call against a schema, and return the validation error to the model as a tool result so it can retry with fixed arguments instead of crashing your loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I NOT build my own harness?
&lt;/h3&gt;

&lt;p&gt;When the run must survive a server restart, when several agents must coordinate with shared state, when you need retries and checkpoints across days rather than minutes, or when your team already knows a framework well and the project timeline is tight. In those cases the framework buys you durability and operations, and building your own loop buys you nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools I used
&lt;/h2&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;What it is for&lt;/th&gt;
&lt;th&gt;Official page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI API&lt;/td&gt;
&lt;td&gt;The model behind the adapter in this post&lt;/td&gt;
&lt;td&gt;&lt;a href="https://platform.openai.com/docs" rel="noopener noreferrer"&gt;https://platform.openai.com/docs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zod&lt;/td&gt;
&lt;td&gt;Schema validation for tool arguments (see my Zod output post)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://zod.dev" rel="noopener noreferrer"&gt;https://zod.dev&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;The language for the whole harness&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.typescriptlang.org" rel="noopener noreferrer"&gt;https://www.typescriptlang.org&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;{/* MONETIZATION NOTE: swap the tool links above for affiliate links when programs exist; never claim an affiliate relationship before it exists. */}&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/agentic-checkout-checklist-2026"&gt;Agentic Checkout Checklist: 9 Fixes So an AI Agent Can Buy From You&lt;/a&gt; - the agent-on-your-site side of the same story, and the internal link this stack item asked for.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/blog/zod-as-a-contract"&gt;Zod as a Contract: Type-Safe AI Output in Production&lt;/a&gt; - the schema pattern I use to validate tool arguments before a handler runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build agents for clients, or you want one built for your product, &lt;a href="https://dev.to/#contact"&gt;get in touch&lt;/a&gt;. I build production TypeScript systems, and an agent loop like this one is usually the first week of that work, not the whole project.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>agents</category>
      <category>llm</category>
    </item>
    <item>
      <title>Shopify Global Catalog REST Ends Nov 2: Migrate to MCP and UCP</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Thu, 29 Oct 2026 05:24:20 +0000</pubDate>
      <link>https://dev.to/zforzain2000/shopify-global-catalog-rest-ends-nov-2-migrate-to-mcp-and-ucp-1n8d</link>
      <guid>https://dev.to/zforzain2000/shopify-global-catalog-rest-ends-nov-2-migrate-to-mcp-and-ucp-1n8d</guid>
      <description>&lt;p&gt;&lt;br&gt;
  If your app searches products across Shopify stores, you have four days left, not a quarter. Shopify's Global Catalog REST API stops serving traffic on November 2, 2026, and the changelog is blunt about the scope: every request to the old host fails after that date, saved catalogs included. I read the changelog, the catalog docs, and the migration guides that are already live so you can skip the panic and do the swap in order. Short version: this is not a GraphQL rewrite, the endpoint swap is small, and the work that actually bites is finding every hidden caller and re-checking every downstream field.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Shopify deprecated the Global Catalog REST API in April 2026 and replaced it with Global Catalog MCP, which implements the Universal Commerce Protocol (UCP). On November 2, 2026, the REST API stops serving traffic. After that date, all four of these endpoints fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /global/v2/search&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /global/v2/p/{upid}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /global/v2/p/by-variant/{vid}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /global/v2/lookup&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The replacement lives at &lt;code&gt;https://catalog.shopify.com/api/ucp/mcp&lt;/code&gt; and exposes three tools. Search becomes &lt;code&gt;search_catalog&lt;/code&gt;, the two single-product lookups become &lt;code&gt;get_product&lt;/code&gt;, and batch lookup becomes &lt;code&gt;lookup_catalog&lt;/code&gt; with a limit of 50 identifiers per request on the Global Catalog. That mapping is the whole migration on paper. The rest of this post is about the parts that are not on paper: the agent profile every call now needs, the response shape changes, the callers you forgot about, and the downstream systems that read your catalog data.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, check whether this deadline is even yours
&lt;/h2&gt;

&lt;p&gt;This deadline only hits you if something in your stack calls the Global Catalog, which is the cross-merchant catalog. It is not the same as a single store's Storefront API, and it is not the same as the per-store Storefront Catalog MCP at &lt;code&gt;https://{shop}.myshopify.com/api/ucp/mcp&lt;/code&gt;. If your app only reads one merchant's own products, this post is not your migration. If your app compares products across merchants, builds recommendations across stores, or resolves a product ID it got from somewhere else in the Shopify ecosystem, check now.&lt;/p&gt;

&lt;p&gt;Run the audit before you write any migration code, because the audit decides how big the job is:&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;# Search every repo you own for the old host and paths&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"discover.shopifyapps.com/global"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.js"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"/global/v2/"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.js"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then go beyond your own repos. Check outbound request logs for the old host, check agency scripts and middleware wrappers, check scheduled jobs, and check any third-party App Store app that touches catalog data. Ask each vendor in writing whether they still call the old endpoints. The migration guides already live agree on this point: the endpoint swap is small, and the failures come from callers nobody remembered, including outdated app versions still in the wild. If a vendor cannot confirm, treat their integration as still calling the old API until you have proof otherwise.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  Do not start a GraphQL Admin API rewrite for this API. I keep seeing that suggestion in threads about Shopify migrations, and it aims at the wrong target here. GraphQL Admin covers your own store's data. The Global Catalog covers many merchants at once, and its replacement is the Global Catalog MCP on UCP. A GraphQL rewrite burns the four days you have left and still leaves the cross-merchant calls broken on November 2.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoint-to-tool map
&lt;/h2&gt;

&lt;p&gt;Here is the full map, taken from Shopify's changelog and catalog docs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Old REST endpoint&lt;/th&gt;
&lt;th&gt;New MCP tool&lt;/th&gt;
&lt;th&gt;What changes in the call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /global/v2/search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;search_catalog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Free-text query, filters, pagination, and buyer context move into the tool arguments. Results are clustered by Universal Product ID (UPID) and include offers from multiple merchants.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /global/v2/p/{upid}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;get_product&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass a product global ID and get full details, including all option combinations with availability signals and checkout links.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /global/v2/p/by-variant/{vid}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;get_product&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass a variant global ID instead of a product ID. Use the &lt;code&gt;selected&lt;/code&gt; argument to anchor a specific variant.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /global/v2/lookup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lookup_catalog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Batch lookup by identifier, up to 50 per request on the Global Catalog. Unresolved IDs are reported in &lt;code&gt;messages&lt;/code&gt;, so check that field instead of assuming every ID resolved.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two limits are worth keeping straight because the docs use the same tool names for two different catalogs. The Global Catalog &lt;code&gt;lookup_catalog&lt;/code&gt; resolves up to 50 identifiers per request. The per-store Storefront Catalog &lt;code&gt;lookup_catalog&lt;/code&gt; supports up to 10. If you copy a limit from a Storefront example into Global Catalog code, or the other way round, you will build a batching bug that only shows up at volume.&lt;/p&gt;

&lt;p&gt;The discovery flow also changes shape. The documented pattern is two steps: discover first with &lt;code&gt;search_catalog&lt;/code&gt; or &lt;code&gt;lookup_catalog&lt;/code&gt;, then evaluate with &lt;code&gt;get_product&lt;/code&gt; once a buyer has picked something, so you can refine the variant choice and read pricing and seller checkout links for the exact variant. &lt;code&gt;search_catalog&lt;/code&gt; also accepts a text query, an image, or a set of product IDs to find similar items, which the old search endpoint did not offer in the same form.&lt;/p&gt;

&lt;h2&gt;
  
  
  What every new call must carry
&lt;/h2&gt;

&lt;p&gt;The biggest behavioral change is not the URL. It is the agent profile. Every Global Catalog MCP request must include a &lt;code&gt;meta.ucp-agent.profile&lt;/code&gt; URL pointing at your agent's UCP profile, and the tools your agent can call depend on the capabilities that profile advertises. Shopify's catalog docs state the profile requirement plainly. Independent reports describe the failure mode when it is missing: a 422 response naming the missing profile. Build the profile into your client once, not per call site, so a future capability change is one edit.&lt;/p&gt;

&lt;p&gt;Saved catalogs change form too. The old API addressed a saved catalog through its URL. The new API takes the saved catalog's ID in the request body, as &lt;code&gt;catalog_id&lt;/code&gt;, according to the migration guides. If you port the old URL logic literally, saved-catalog calls will fail in a way that looks like a permissions problem. It is not permissions. It is the wrong addressing.&lt;/p&gt;

&lt;p&gt;Authentication is the one thing that does not change, according to the migration guides: existing Dev Dashboard bearer tokens keep working against the new endpoint. I am attributing that to the guides rather than the changelog, because the changelog itself lists the endpoint and tools without a separate auth section. Verify it with one authenticated call in your own environment before cutover day, and do not let an auth assumption be the thing you discover on November 2.&lt;/p&gt;

&lt;p&gt;Here is the shape of a call, adapted from the documented JSON-RPC pattern. Treat this as a starting template to verify against Shopify's live catalog docs, not as code I ran against your catalog:&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;// Minimal Global Catalog MCP search over JSON-RPC 2.0&lt;/span&gt;
&lt;span class="c1"&gt;// Verify the profile URL and scopes against Shopify's catalog docs&lt;/span&gt;
&lt;span class="c1"&gt;// before you ship this, and keep the profile in one shared client.&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;searchCatalog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://catalog.shopify.com/api/ucp/mcp&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json, text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Authorization: `Bearer ${process.env.SHOPIFY_DEV_TOKEN}`,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools/call&lt;/span&gt;&lt;span class="dl"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search_catalog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ucp-agent&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="na"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-agent.example/ucp/profile.json&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="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;address_country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;available&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="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&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;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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Catalog search failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&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="s2"&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;data&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;structuredContent&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="o"&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;Three details in that template deserve attention. First, the &lt;code&gt;Accept&lt;/code&gt; header allows both JSON and event-stream responses, because MCP servers can stream. Second, the buyer context (country and currency) now travels with the request, so a search that used to inherit context from somewhere else must pass it explicitly. Third, the products come back under &lt;code&gt;structuredContent&lt;/code&gt;, not as a top-level REST body, so every parser downstream of the old response shape needs a rewrite, not a rename.&lt;/p&gt;

&lt;h2&gt;
  
  
  The downstream-field audit most teams skip
&lt;/h2&gt;

&lt;p&gt;The endpoint swap takes an afternoon. The downstream audit takes the rest of the week, and it is where a November 2 migration actually fails. Catalog data rarely stops at the search response. It flows into pricing displays, stock checks, recommendation rankings, ERP records, and order creation. A renamed field, a re-nested variant, or a price that now arrives in minor units can pass your own smoke test and break a downstream system days later, when the bad data has already spread.&lt;/p&gt;

&lt;p&gt;Walk every field your code reads from the old response and map it to the new UCP catalog schema before you cut over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Price vs list price.&lt;/strong&gt; Each variant carries &lt;code&gt;price&lt;/code&gt;, the price the buyer pays when available, and optionally &lt;code&gt;list_price&lt;/code&gt;, the merchant's compare-at price for the buyer's market. If your old code treated the first price it found as the selling price, confirm which field that was. A strikethrough price shown as the selling price is a pricing bug your customers will find first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability signals.&lt;/strong&gt; &lt;code&gt;get_product&lt;/code&gt; returns availability signals per option combination. If your old code cached availability from search results, stop. Shopify's catalog guidance says not to cache search results, because they reflect merchant preferences on pricing, availability, and presentation. Cache policy is now a correctness issue, not a performance choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images.&lt;/strong&gt; Images may only be used in connection with the related merchant's product listing and must be rendered in real time, not downloaded to your servers, per the same guidance. If your pipeline downloads catalog images for re-hosting, that step has to go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unresolved IDs.&lt;/strong&gt; &lt;code&gt;lookup_catalog&lt;/code&gt; reports unresolved IDs in &lt;code&gt;messages&lt;/code&gt;. Your batch code must handle partial success: some IDs resolve, some do not, and the response is still a success. Code that assumes a batch either fully works or fully fails will silently drop products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkout authority.&lt;/strong&gt; Catalog responses reflect the business's current terms for that request, but they are not transactional commitments. Checkout is authoritative. Re-validate price and availability at checkout instead of trusting a catalog response you fetched earlier in the session, and never reuse a catalog response across sessions without re-validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the old and new paths side by side for at least one full day before you switch traffic. Log mismatches per field, not just per request, so a single renamed field shows up as a pattern instead of one noisy error. The goal of the shadow run is not to prove the new API works. It is to prove your downstream systems still agree with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four-day cutover plan
&lt;/h2&gt;

&lt;p&gt;Today is October 29. The cutoff is November 2. Here is the order I would use, with the risky work first:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
  Grep every repo, check outbound logs, and get written vendor confirmations. Map every old field your code reads to the new UCP schema. Decide whether you own a Global Catalog caller at all, and if you do, list every downstream consumer of its data.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Build the shared MCP client with the agent profile in one place. Port search, single-product lookup, variant lookup, and batch lookup to the three tools. Rewrite every parser for the new response shape. Do not ship yet.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Run old and new paths side by side. Compare field by field, fix the mismatches, and check the unresolved-ID path in batch lookup with a deliberately bad ID. Confirm saved catalogs resolve through &lt;code&gt;catalog_id&lt;/code&gt; in the body.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Switch traffic before the cutoff, not on it. Watch search-empty rates, lookup failure rates, and downstream pricing and order errors for 24 hours. Keep the old code path behind a flag for rollback, but remember the old endpoint itself will not come back, so rollback means fixing forward on the new API.&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;If you cannot finish the shadow run, cut over anyway before November 2 with the new path and a tight watch, because the alternative is a guaranteed outage on the old path. A new-path bug you can see in logs beats an old-path outage you cannot fix. If you have no Global Catalog caller at all, the right move is to do nothing and say so in your incident notes, so nobody re-opens this migration in a panic on November 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this migration is really about
&lt;/h2&gt;

&lt;p&gt;The protocol shift matters beyond this one deadline. UCP is the open standard Google and Shopify co-developed for agents to search catalogs, build carts, check out, link accounts, and track orders through one contract, instead of one custom integration per store. The Global Catalog MCP is the catalog slice of that contract, exposed as tools an agent can call. Your migration is therefore not just a URL change. It moves your cross-merchant discovery onto the same tool-calling pattern agents already use for single-store catalogs, carts, and checkout.&lt;/p&gt;

&lt;p&gt;That also reframes the money question. The teams that will get migration work out of this deadline are not the ones who can rename an endpoint. They are the ones who can audit a stack, find the hidden callers, map the downstream fields, and cut over without a pricing incident. If you run a Shopify app or an agentic-commerce integration, that audit is a sellable service this week: a fixed-scope check that answers whether a client calls the old API, what breaks on November 2, and what the new path costs. The deadline does the selling. Your job is to make the audit thorough enough that the client trusts the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools and sources
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool or doc&lt;/th&gt;
&lt;th&gt;What it is for&lt;/th&gt;
&lt;th&gt;Official link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shopify changelog: Global Catalog REST API stops serving traffic on November 2, 2026&lt;/td&gt;
&lt;td&gt;The deadline, the four endpoints, the new endpoint, and the three replacement tools&lt;/td&gt;
&lt;td&gt;&lt;a href="https://shopify.dev/changelog/posts/global-catalog-rest-api-stops-serving-traffic-on-november-2-2026" rel="noopener noreferrer"&gt;https://shopify.dev/changelog/posts/global-catalog-rest-api-stops-serving-traffic-on-november-2-2026&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shopify docs: Global Catalog MCP&lt;/td&gt;
&lt;td&gt;The two-step discover-then-evaluate flow, UPID clustering, and the &lt;code&gt;ucp&lt;/code&gt; CLI path&lt;/td&gt;
&lt;td&gt;&lt;a href="https://shopify.dev/docs/agents/catalog/global-catalog" rel="noopener noreferrer"&gt;https://shopify.dev/docs/agents/catalog/global-catalog&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shopify docs: About Catalogs&lt;/td&gt;
&lt;td&gt;The shared rules for both catalog interfaces, including the image and caching rules and the 50 vs 10 lookup limits&lt;/td&gt;
&lt;td&gt;&lt;a href="https://shopify.dev/docs/agents/catalog" rel="noopener noreferrer"&gt;https://shopify.dev/docs/agents/catalog&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;{/* MONETIZATION NOTE: swap the docs links above for affiliate links only where a program exists; Shopify docs have no affiliate program, so these stay as plain official links. The money path here is freelance migration audits and agentic-commerce consulting, not affiliate commissions. */}&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict: what I would do, and what I would not
&lt;/h2&gt;

&lt;p&gt;If I owned a Global Catalog caller today, I would start with the audit, not the code, and I would cut over on November 1 at the latest, with a full day of shadow-run logs behind me. I would not start a GraphQL rewrite, I would not cache search results or re-host images under the new rules, and I would not trust a vendor's silence as confirmation. The one condition that flips my answer is simple: if the audit proves nothing calls the old API, I would do nothing at all, and I would write that finding down so the question does not come back on deadline day. Four days is enough for a careful swap and not enough for a careless one, so spend the days on the callers and the fields, not on the endpoint rename.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  When does Shopify's Global Catalog REST API stop working?
&lt;/h3&gt;

&lt;p&gt;On November 2, 2026. Shopify's developer changelog says the API stops serving traffic on that date, including requests against saved catalogs. It was deprecated in April 2026 and replaced by Global Catalog MCP.&lt;/p&gt;

&lt;h3&gt;
  
  
  What replaces the Global Catalog REST API?
&lt;/h3&gt;

&lt;p&gt;Global Catalog MCP at &lt;code&gt;https://catalog.shopify.com/api/ucp/mcp&lt;/code&gt;. It implements the Universal Commerce Protocol (UCP) and exposes three catalog tools: &lt;code&gt;search_catalog&lt;/code&gt;, &lt;code&gt;lookup_catalog&lt;/code&gt;, and &lt;code&gt;get_product&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do the old REST endpoints map to the new MCP tools?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;GET /global/v2/search&lt;/code&gt; maps to &lt;code&gt;search_catalog&lt;/code&gt;. &lt;code&gt;GET /global/v2/p/{upid}&lt;/code&gt; and &lt;code&gt;GET /global/v2/p/by-variant/{vid}&lt;/code&gt; both map to &lt;code&gt;get_product&lt;/code&gt;. &lt;code&gt;POST /global/v2/lookup&lt;/code&gt; maps to &lt;code&gt;lookup_catalog&lt;/code&gt;, which resolves up to 50 identifiers per request on the Global Catalog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Global Catalog MCP need an agent profile on every call?
&lt;/h3&gt;

&lt;p&gt;Yes. Every request must include a &lt;code&gt;meta.ucp-agent.profile&lt;/code&gt; URL pointing at your agent's UCP profile. Shopify's catalog docs state this as a requirement, and independent migration reports describe a 422 response when the profile is missing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this the same as migrating to the Shopify GraphQL Admin API?
&lt;/h3&gt;

&lt;p&gt;No. The GraphQL Admin API covers a single store's own data. The Global Catalog covers products across many Shopify merchants, so its replacement is the cross-merchant Global Catalog MCP, not a per-store GraphQL rewrite.&lt;/p&gt;

&lt;h3&gt;
  
  
  What breaks first if I miss the November 2 deadline?
&lt;/h3&gt;

&lt;p&gt;Cross-merchant search goes empty and product lookups fail, because every call to &lt;code&gt;discover.shopifyapps.com/global&lt;/code&gt; fails after the cutoff. If catalog data feeds pricing, stock checks, or order creation downstream, the visible failure can arrive days later as wrong prices or stalled orders.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  The deadline, the four endpoints, the new endpoint, and the three tools come from Shopify's own developer changelog. The two-step discover-then-evaluate flow, UPID clustering, the agent profile requirement, and the image and caching rules come from Shopify's catalog docs. The hidden-caller warning, the downstream-field risk, the saved-catalog body parameter, and the unchanged-auth report come from the dev.to checklist and Medium final-week plan already live on this topic, which I read but did not copy. The TypeScript template above is an illustrative adaptation of the documented JSON-RPC pattern, not a run I performed against a live catalog, so verify the profile URL and scopes against the live docs before shipping. Search volume and difficulty for the primary keyword are Unknown: no reliable public estimate was available, and none is claimed here.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want this migration done on your app instead of reading about it?&lt;/strong&gt; I build commerce and agent integrations for a living - &lt;a href="https://dev.to/#contact"&gt;tell me about your stack&lt;/a&gt; and I will tell you honestly whether this is a one-day swap or a downstream audit first.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If agentic buying is the wider question behind this migration, start with my &lt;a href="https://dev.to/blog/agentic-checkout-checklist-2026"&gt;Agentic Checkout Checklist&lt;/a&gt; - being found in a catalog is the gate before an agent can buy.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>mcp</category>
      <category>ucp</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Profound vs Peec AI: Why the $95 Tracker Wins in 2026</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Sun, 25 Oct 2026 06:03:05 +0000</pubDate>
      <link>https://dev.to/zforzain2000/profound-vs-peec-ai-why-the-95-tracker-wins-in-2026-3n9e</link>
      <guid>https://dev.to/zforzain2000/profound-vs-peec-ai-why-the-95-tracker-wins-in-2026-3n9e</guid>
      <description>&lt;p&gt;&lt;br&gt;
  Readers keep landing on my site searching for Profound and Peec AI by name, and my &lt;a href="https://dev.to/blog/best-geo-tools-2026"&gt;GEO tools comparison&lt;/a&gt; only gives each of them a paragraph. My &lt;a href="https://dev.to/blog/scrunch-ai-alternatives-cheaper-geo-trackers"&gt;Scrunch AI alternatives post&lt;/a&gt; named Peec AI as the closest cheaper replacement for Scrunch, but it never put Peec head to head with Profound. This post fixes that: real starter pricing, real engine counts, the trial situation, and the one pricing change that flips the whole comparison.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer: buy Peec AI unless you are buying Enterprise
&lt;/h2&gt;

&lt;p&gt;If you have roughly $100 a month to spend on AI visibility tracking, Peec AI is the pick. For about $95 a month you get 50 prompts tracked across three engines of your choice, daily refreshes, and unlimited seats. Profound is the deeper platform and the stronger enterprise product, but its famous $99 starter tier tracked ChatGPT only, and its live pricing page now leads with a free trial plus custom Enterprise pricing rather than a published starter price. At starter money, Peec gives you three engines where Profound used to give you one, and today Profound may not sell you a starter plan at all. Everything below is the evidence for that verdict, including the pricing change you need to know before you trust any older comparison.&lt;/p&gt;



&lt;h2&gt;
  
  
  What each tool actually does
&lt;/h2&gt;

&lt;p&gt;Profound is an enterprise AI visibility platform built in New York in 2024. It tracks how brands appear in AI answers across ChatGPT, Perplexity, Google AI Overviews, Google AI Mode, Gemini, Copilot, Grok, and more, and it is best known for one asset no rival can copy easily: a dataset of real user conversations, now cited at more than 1.5 billion prompts, which powers its Prompt Volumes feature. Instead of guessing which questions buyers ask, Profound shows you questions real people actually typed, then tracks how your brand shows up in the answers. It also tracks AI crawler and referral traffic to your own site through Agent Analytics, and it has moved from measurement into action with content briefs built from the same data.&lt;/p&gt;

&lt;p&gt;Peec AI is a Berlin-based tracker built for speed. It tracks brand mentions, position, sentiment, and citations across the engines you pick, refreshes daily, and adds two features I rate highly for working teams: tracking at the level of specific model versions, so you see the effect the day a new model ships, and a clear split between sources an AI answer used and sources it actually cited. That split matters because a source can shape an answer without ever getting a visible citation, and only the citation earns you the link. Peec also runs prompt discovery, which surfaces questions you had not thought to track, and an AI shopping module for e-commerce teams that tracks visibility per product from a Shopify or CSV catalog.&lt;/p&gt;

&lt;p&gt;The difference in one sentence: Profound sells depth and data you cannot get elsewhere, Peec sells a fast, usable tracker at a price a small team can approve without a procurement meeting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The price table, with the catch stated plainly
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Prompts&lt;/th&gt;
&lt;th&gt;Engines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Profound old Starter&lt;/td&gt;
&lt;td&gt;$99 a month, billed annually&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;ChatGPT only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profound old Growth&lt;/td&gt;
&lt;td&gt;$399 a month, billed annually&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;3 engines, plus content generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profound Enterprise&lt;/td&gt;
&lt;td&gt;Custom quote&lt;/td&gt;
&lt;td&gt;Tailored&lt;/td&gt;
&lt;td&gt;Up to 10 engines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peec AI Starter&lt;/td&gt;
&lt;td&gt;About $95 a month&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;3 engines of your choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peec AI Pro&lt;/td&gt;
&lt;td&gt;About $245 a month&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;3 engines, more projects and countries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peec AI Advanced&lt;/td&gt;
&lt;td&gt;About $495 a month&lt;/td&gt;
&lt;td&gt;350&lt;/td&gt;
&lt;td&gt;3 engines, Looker Studio connector&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two warnings belong right under that table. First, Profound's $99 and $399 tiers are the prices cited across 2026 comparisons, but a pricing check on October 22, 2026 found those tiers no longer listed on Profound's pricing page, which now leads with a free trial and custom Enterprise pricing. Treat $99 as the historical starter price, not a price you can click and buy today. Second, Peec prices in euros on its own site, around 85 to 89 euros a month for Starter, which converts to roughly $95 depending on the exchange rate and whether you pay monthly or annually. Annual billing cuts about 15 percent. If you are budgeting in dollars, confirm the converted charge in checkout before you commit.&lt;/p&gt;

&lt;p&gt;The honest read of the table: Peec is the only one of the two with a starter plan you can clearly buy today at around $95. That fact alone decides this comparison for most readers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starter-tier trap: one engine is not a program
&lt;/h2&gt;

&lt;p&gt;Profound's old Starter plan is the most misleading price in this category. Ninety-nine dollars sounds like an entry point into GEO tracking, but it bought ChatGPT only and 50 prompts. If your buyers also ask Perplexity and Google AI Overviews, and most buyers do, you learned nothing about two of the three places that shape their decisions. The moment you wanted Perplexity coverage, you were at the old $399 Growth tier. That is a 4x jump from the headline price, and it is the number you should have budgeted from day one.&lt;/p&gt;

&lt;p&gt;Peec's Starter has the opposite shape. About $95 buys 50 prompts across three engines you choose, from ChatGPT, Perplexity, Google AI Overviews, Copilot, Gemini, and Naver AI. Cost per engine works out near $32, against $99 for Profound's single engine. The catch is the add-on structure: Gemini, Claude, and Google AI Mode cost extra per engine per month on Peec, so a team that needs all six engines pays meaningfully more than the headline. Do the add-on math for your exact engine list before you sign, on either tool.&lt;/p&gt;

&lt;p&gt;Prompt count is the other trap. Fifty prompts sounds small until you write the list: 10 to 20 buyer-intent questions, your brand and product names, and your top three competitors. Most small teams land under 50. If your list runs past 50, Peec Pro at about $245 for 150 prompts is still cheaper than Profound's old Growth tier, and it keeps the three-engine shape. If your list runs past 150, you are no longer a starter buyer on either platform, and the conversation becomes an enterprise negotiation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engine coverage: where each tool actually looks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Profound&lt;/th&gt;
&lt;th&gt;Peec AI standard plans&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, pick from 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, pick from 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google AI Overviews&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, pick from 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google AI Mode&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Paid add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, pick from 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Paid add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;Enterprise tiers&lt;/td&gt;
&lt;td&gt;Paid add-on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Enterprise tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Profound wins the coverage count at the top end: seven engines on its main platform and up to 10 on Enterprise, including Claude, Gemini, Grok, and DeepSeek territory that Peec reserves for Enterprise. Peec wins the coverage count at starter money: three engines you choose beats one engine you are given. Neither tool covers everything at its entry price, and any comparison that lists "10 engines" next to a $99 price tag is mixing an enterprise feature with a starter price. Read the tier column, not just the engine column.&lt;/p&gt;

&lt;h2&gt;
  
  
  Depth against speed: what you get for the money
&lt;/h2&gt;

&lt;p&gt;Profound's depth shows up in four places. Its Conversation Explorer and Prompt Volumes turn real user questions into a prompt set grounded in behavior, not guesswork. Its citation data comes with screenshots for audit, so you can verify a citation instead of trusting a dashboard number. Its refresh cadence on Growth was hourly, against Peec's daily refresh, so a fast-moving story shows up sooner. And its enterprise tier carries SOC 2 Type II compliance, SSO and SAML, API access, and dedicated support, which is what regulated buyers and large marketing teams actually need from a vendor.&lt;/p&gt;

&lt;p&gt;Peec's speed shows up in different places. Onboarding takes under an hour for most teams, against a Profound setup that reviewers put at hours per brand profile. Unlimited seats and unlimited countries sit on every Peec tier, so a five-person team in three markets pays the same as a solo user, where seat-based tools punish growth. Daily refreshes are slower than Profound's hourly cadence but fast enough for weekly content decisions, which is how most small teams actually work. And the Looker Studio connector on Advanced plugs Peec into reporting stacks agencies already use.&lt;/p&gt;

&lt;p&gt;My judgment on this trade: depth only pays when someone on your team acts on it weekly. A real-user prompt dataset is worth a premium if it changes which pages you write. It is worth nothing if it sits in a dashboard nobody opens. Speed pays immediately: a tracker your team actually checks every Monday beats a deeper tracker that needs a training session to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What neither tool does for you
&lt;/h2&gt;

&lt;p&gt;Both tools are measurement products. They show you the gap between your brand and your competitors inside AI answers. They do not close it. The content work stays yours: pages that answer the exact questions your buyers ask, written so an AI engine can extract a clean answer and cite your page for it. If you want the strategy behind that work, my &lt;a href="https://dev.to/blog/how-to-get-cited-by-chatgpt-perplexity"&gt;guide to getting cited by ChatGPT and Perplexity&lt;/a&gt; covers it in full, and it costs nothing.&lt;/p&gt;

&lt;p&gt;That point also sets the budget priority. If you have $95 a month and no pages worth citing, spend it on content first and track manually: run your key prompts in ChatGPT, Perplexity, and Google AI Overviews once a week and record whether your brand appears. Manual tracking breaks down past a dozen prompts, and that is the moment a tracker earns its fee. Buying a tracker before you have citable pages is paying to watch an empty scoreboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which buyer should pick which tool
&lt;/h2&gt;

&lt;p&gt;Pick Peec AI if you are a solo creator, a small team, or an agency running a handful of clients on starter budgets. You get three engines, 50 prompts, daily data, and unlimited seats for about $95 a month, with a seven-day trial that needs no credit card. Pick Peec Pro at about $245 when your prompt list passes 50 or you manage multiple projects, and Advanced at about $495 when you need the Looker Studio connector and weekly reporting. E-commerce teams with a catalog should test the AI shopping module before anything else, because product-level visibility is a feature neither Profound nor most rivals offer at this price.&lt;/p&gt;

&lt;p&gt;Pick Profound if you are a mid-market or enterprise brand that needs real-user prompt data, citation audits with screenshots, hourly refreshes, or compliance paperwork your procurement team demands. Budget for a custom Enterprise conversation, not a $99 checkout, and ask for the trial first: 50 prompts across three engines for seven days. Also pick Profound if your prompt list runs into the hundreds and you need segmentation by market, language, and competitor across 10 engines in one contract. At that scale, Peec's per-engine add-ons and project caps stop being the cheaper path.&lt;/p&gt;

&lt;p&gt;If your budget is tighter than $95, neither tool is the answer. Otterly AI's Lite plan at $29 a month covers 15 prompts across four engines, and my Scrunch alternatives post covers the free routes in full. A $29 tracker you check weekly beats a $95 tracker you cancel in month two.&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict: what I would pick, and why
&lt;/h2&gt;

&lt;p&gt;I would pick Peec AI. The deciding factor is not that Peec is slightly cheaper on paper. It is that Peec is the tool a small team can actually buy, set up in an afternoon, and check every week at starter money, with three engines instead of one. Profound is the better platform in absolute terms: better data, deeper analytics, stronger enterprise story. But a better platform you cannot buy at your budget is not a better purchase, and Profound's move upmarket means the $99 comparison that fills so many roundups no longer describes a checkout page.&lt;/p&gt;

&lt;p&gt;The verdict flips in three cases. If you need real-user prompt volumes to decide what to write, Profound's dataset is the only one of its kind here. If your compliance team requires SOC 2, SSO, and a contractual relationship, Profound Enterprise is built for that conversation. If you track hundreds of prompts across 10 engines, Profound's enterprise tiers scale cleaner than Peec's add-on ladder. Outside those three cases, take Peec's seven-day trial, run your real prompt list through it for a week, and only pay when the data changes a content decision. That is the test every GEO tool should pass before it takes your money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools mentioned
&lt;/h2&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;What it is&lt;/th&gt;
&lt;th&gt;Official site&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Profound&lt;/td&gt;
&lt;td&gt;Enterprise AI visibility platform with real-user prompt data&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.tryprofound.com" rel="noopener noreferrer"&gt;https://www.tryprofound.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peec AI&lt;/td&gt;
&lt;td&gt;Fast multi-engine GEO tracker for small teams and agencies&lt;/td&gt;
&lt;td&gt;&lt;a href="https://peec.ai" rel="noopener noreferrer"&gt;https://peec.ai&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Otterly AI&lt;/td&gt;
&lt;td&gt;Budget GEO tracker from $29 a month&lt;/td&gt;
&lt;td&gt;&lt;a href="https://otterly.ai" rel="noopener noreferrer"&gt;https://otterly.ai&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;{/* MONETIZATION NOTE: swap the official URLs above for affiliate links when Profound, Peec AI, or Otterly AI affiliate programs are joined; never claim affiliate status before it exists. */}&lt;/p&gt;

&lt;p&gt;If you want help turning GEO data into pages that actually get cited, I take on freelance AI-visibility and content projects. Reach me through the contact section on the &lt;a href="https://dev.to/#contact"&gt;homepage&lt;/a&gt; and tell me which engines your buyers use. I will tell you honestly whether a tracker is worth paying for yet, or whether your money belongs in content first.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Profound or Peec AI cheaper?
&lt;/h3&gt;

&lt;p&gt;Peec AI is cheaper at the starter level in practice. Its Starter plan runs about $95 a month for 50 prompts across three engines. Profound's widely cited $99 Starter covered one engine only, and Profound's live pricing page now leads with a free trial and custom Enterprise pricing instead of a published starter price.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many AI engines does Peec AI track?
&lt;/h3&gt;

&lt;p&gt;Peec AI's standard plans let you pick three engines from ChatGPT, Perplexity, Google AI Overviews, Copilot, Gemini, and Naver AI, with up to 13 models on Enterprise. Extra engines are paid add-ons, so count the add-on cost before you compare headline prices.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many AI engines does Profound track?
&lt;/h3&gt;

&lt;p&gt;Profound tracks seven engines on its main platform, including ChatGPT, Google AI Mode, Google AI Overviews, Gemini, Copilot, Perplexity, and Grok, with up to 10 on Enterprise plans. Its old Starter tier tracked ChatGPT only, which is why the $99 headline price misled so many buyers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Profound offer a free trial?
&lt;/h3&gt;

&lt;p&gt;Profound's current pricing page offers a free trial covering 50 prompts across three engines for seven days. Peec AI offers a seven-day free trial with no credit card required, so you can run both trials on the same prompt set before paying either vendor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which GEO tracker should a small team pick?
&lt;/h3&gt;

&lt;p&gt;Peec AI, in most cases. For about $95 a month you get three engines, 50 prompts, daily refreshes, and unlimited seats and countries. Pick Profound when you need real-user prompt data, enterprise compliance, or tracking at a scale where a custom Enterprise contract makes sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do GEO trackers improve your AI visibility by themselves?
&lt;/h3&gt;

&lt;p&gt;No. A tracker shows you where your brand appears in AI answers and which sources get cited. The visibility gain comes from the content work that follows: pages that answer the exact questions your buyers ask, structured so AI engines can extract and cite them.&lt;/p&gt;

</description>
      <category>geo</category>
      <category>ai</category>
      <category>tools</category>
      <category>comparison</category>
    </item>
    <item>
      <title>Scrunch AI Alternatives: 7 Cheaper GEO Trackers That Actually Work</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Sat, 24 Oct 2026 06:34:34 +0000</pubDate>
      <link>https://dev.to/zforzain2000/scrunch-ai-alternatives-7-cheaper-geo-trackers-that-actually-work-18ia</link>
      <guid>https://dev.to/zforzain2000/scrunch-ai-alternatives-7-cheaper-geo-trackers-that-actually-work-18ia</guid>
      <description>&lt;p&gt;&lt;br&gt;
  Someone typed "cheaper alternatives to scrunch for geo" into Google and&lt;br&gt;
  landed on my site. I had nothing for them. My &lt;a href="https://dev.to/blog/best-geo-tools-2026"&gt;GEO tools&lt;br&gt;
  comparison&lt;/a&gt; covers eight platforms, and my&lt;br&gt;
  &lt;a href="https://dev.to/blog/how-to-get-cited-by-chatgpt-perplexity"&gt;guide to getting cited by ChatGPT and&lt;br&gt;
  Perplexity&lt;/a&gt; explains the&lt;br&gt;
  strategy, but neither one answers the price question directly. This post is&lt;br&gt;
  the answer: what Scrunch AI's $250 a month actually buys, seven tools that&lt;br&gt;
  cost less, what you give up when you go cheap, and my verdict on which one&lt;br&gt;
  I would buy.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer: what I would buy instead of Scrunch
&lt;/h2&gt;

&lt;p&gt;If you track fewer than 50 prompts, Peec AI's Starter plan at roughly $85 a&lt;br&gt;
month is the closest like-for-like Scrunch replacement, and it saves you&lt;br&gt;
about $165 every month. On the tightest budget, Otterly AI's $29 Lite plan&lt;br&gt;
covers the four engines that matter most. If you can self-host, Limelit Open&lt;br&gt;
costs nothing at all. Scrunch only earns its $250 when you need 100-plus&lt;br&gt;
prompts, its crawler-facing AXP layer, or compliance paperwork your&lt;br&gt;
procurement team demands. Everything below is the evidence for that verdict.&lt;/p&gt;



&lt;h2&gt;
  
  
  What $250 a month actually buys you at Scrunch AI
&lt;/h2&gt;

&lt;p&gt;Scrunch AI positions itself as an "Agent Experience Platform," not just a&lt;br&gt;
dashboard. Its entry tier, called Starter or Core depending on which page you&lt;br&gt;
read, runs about $250 a month on annual billing, or $300 month-to-month,&lt;br&gt;
according to multiple published comparisons checked in October 2026. For that&lt;br&gt;
you get roughly 125 tracked prompts, four AI engines (ChatGPT, Perplexity,&lt;br&gt;
Google AI Overviews, and Copilot), around 5,000 tracked responses a month,&lt;br&gt;
and a 7-day free trial. Growth jumps to about $417 a month annually ($500&lt;br&gt;
monthly), and Enterprise is custom-priced.&lt;/p&gt;

&lt;p&gt;The two things that genuinely separate Scrunch from the cheaper tools below&lt;br&gt;
are its Agent Experience Platform, which serves a structured,&lt;br&gt;
AI-crawler-friendly version of your site alongside your human-facing pages,&lt;br&gt;
and its accuracy tooling: hallucination detection, citation insights showing&lt;br&gt;
which sources shape an answer, and brand-misinformation alerts. It also&lt;br&gt;
carries SOC 2 Type II compliance and a GA4 integration that connects AI&lt;br&gt;
citations to actual referral traffic. Those are real features, and they are&lt;br&gt;
the reason enterprise teams pay the premium.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  GEO pricing drifts constantly, and vendors rename tiers without telling&lt;br&gt;
  anyone. Every price below is a snapshot of vendor positioning as of October&lt;br&gt;
  2026, cross-checked against at least two published comparisons. Verify on&lt;br&gt;
  the vendor's pricing page before you buy, and treat any review quoting&lt;br&gt;
  prices without a date as stale. One more honest note: several comparisons&lt;br&gt;
  report that Scrunch gates its AXP layer, full nine-engine coverage, and API&lt;br&gt;
  access behind higher tiers, so the $250 entry plan is narrower than the&lt;br&gt;
  marketing suggests.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The 7 cheaper alternatives, compared
&lt;/h2&gt;

&lt;p&gt;Here is the at-a-glance table. Detailed verdicts follow, including the one&lt;br&gt;
free open-source option and the one genuinely free audit.&lt;/p&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;Entry price (Oct 2026)&lt;/th&gt;
&lt;th&gt;Prompts on entry&lt;/th&gt;
&lt;th&gt;Engines&lt;/th&gt;
&lt;th&gt;Free option&lt;/th&gt;
&lt;th&gt;The catch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Peec AI&lt;/td&gt;
&lt;td&gt;~$85/mo (€85)&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;3 of your choice&lt;/td&gt;
&lt;td&gt;7-day trial&lt;/td&gt;
&lt;td&gt;3-engine base; add-ons for full coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Otterly AI&lt;/td&gt;
&lt;td&gt;$29/mo&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;4 (ChatGPT, AI Overviews, Perplexity, Copilot)&lt;/td&gt;
&lt;td&gt;Trial, no card&lt;/td&gt;
&lt;td&gt;15 prompts is a baseline, not a program&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promptwatch&lt;/td&gt;
&lt;td&gt;$95/mo&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;Multi-engine&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Smaller vendor; verify on trial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ranqo&lt;/td&gt;
&lt;td&gt;$49/mo&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Multi-engine&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Figures are self-reported by the vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writesonic&lt;/td&gt;
&lt;td&gt;from $39/mo&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;Multi-engine&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Content suite you may not need&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HubSpot AI Search Grader&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;One-off audit&lt;/td&gt;
&lt;td&gt;Multi-engine&lt;/td&gt;
&lt;td&gt;Yes, fully free&lt;/td&gt;
&lt;td&gt;One-time check, no ongoing tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Limelit Open&lt;/td&gt;
&lt;td&gt;Free (self-hosted)&lt;/td&gt;
&lt;td&gt;Unlimited (your infra)&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Yes, open-source&lt;/td&gt;
&lt;td&gt;You run the server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  1. Peec AI: the closest Scrunch swap
&lt;/h3&gt;

&lt;p&gt;Peec AI is the most direct answer to "Scrunch, but cheaper." Its Starter plan&lt;br&gt;
is listed at €85 a month on monthly billing (about $95), dropping to €70 on&lt;br&gt;
annual billing, for 50 daily-tracked prompts with unlimited users. You pick&lt;br&gt;
three engines out of ChatGPT, Google AI Overviews, Google AI Mode, Microsoft&lt;br&gt;
Copilot, Perplexity, and Gemini. There is a one-week free trial that needs no&lt;br&gt;
credit card. Pro (€205 a month, 150 prompts) and Advanced (€425 a month, 350&lt;br&gt;
prompts) scale from there.&lt;/p&gt;

&lt;p&gt;What you get for roughly 60 percent less than Scrunch: prompt-based tracking&lt;br&gt;
that cleanly separates direct "source" citations from general brand&lt;br&gt;
"visibility" mentions, competitor and share-of-answer comparisons, and an&lt;br&gt;
agent that turns your tracking data into recommended actions. What you give&lt;br&gt;
up: the three-engine base means paying add-ons for full coverage, and there&lt;br&gt;
is no crawler-facing layer like Scrunch's AXP. If your prompt list fits in 50&lt;br&gt;
and you do not need AXP, this is the swap I would make first.&lt;/p&gt;

&lt;p&gt;{/* MONETIZATION NOTE: official site peec.ai - swap in affiliate link if a Peec AI partner program opens */}&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Otterly AI: the cheapest credible start
&lt;/h3&gt;

&lt;p&gt;Otterly AI's Lite plan is $29 a month ($25 on annual billing) for 15 tracked&lt;br&gt;
prompts, and it is the cheapest entry point I would actually recommend. Every&lt;br&gt;
plan includes daily tracking, unlimited team members, brand reports, and the&lt;br&gt;
same four base engines: ChatGPT, Google AI Overviews, Perplexity, and&lt;br&gt;
Microsoft Copilot. Standard ($189 a month, 100 prompts) adds API and MCP&lt;br&gt;
access plus Agent Analytics; Premium ($489 a month) goes to 400 prompts.&lt;/p&gt;

&lt;p&gt;The honest catch is arithmetic. Fifteen prompts is enough to learn whether&lt;br&gt;
AI search mentions you at all, not enough to run a real program across a&lt;br&gt;
buyer journey. And the engines Scrunch buyers ask about next, Claude, Gemini,&lt;br&gt;
and Google AI Mode, are paid add-ons here, not base features. Extra prompts&lt;br&gt;
cost $99 per 100 a month on Standard and Premium. Otterly is the right pick&lt;br&gt;
when budget is the binding constraint and you want a baseline this week, not&lt;br&gt;
when you already know you need 100 prompts.&lt;/p&gt;

&lt;p&gt;{/* MONETIZATION NOTE: official site otterly.ai - swap in affiliate link if an Otterly partner program opens */}&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Promptwatch: $95 for 50 prompts, plus Reddit
&lt;/h3&gt;

&lt;p&gt;Promptwatch sits at $95 a month for 50 tracked prompts, putting it shoulder&lt;br&gt;
to shoulder with Peec AI on price. Its differentiator is Reddit Insights:&lt;br&gt;
citation tracking that extends to Reddit and YouTube, which is exactly the&lt;br&gt;
coverage Scrunch's own comparisons admit Scrunch lacks. If your buyers ask&lt;br&gt;
questions on Reddit before they ever touch Google, that is a meaningful edge,&lt;br&gt;
not a checkbox.&lt;/p&gt;

&lt;p&gt;The catch is vendor size. Promptwatch is a smaller, newer vendor than Peec or&lt;br&gt;
Otterly, so run the trial on your own prompts and confirm the data quality&lt;br&gt;
before committing. Price per prompt is competitive; maturity is the open&lt;br&gt;
question.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Ranqo: $49 for 25 prompts
&lt;/h3&gt;

&lt;p&gt;Ranqo's own comparison page, updated in October 2026, lists a $49-a-month&lt;br&gt;
entry plan covering 25 prompts with nothing major held back, which makes it&lt;br&gt;
the cheapest per-prompt paid option on this list after Otterly's Lite. For a&lt;br&gt;
solo founder tracking one brand and a tight question set, 25 prompts at $49&lt;br&gt;
is a sensible middle ground between Otterly's 15-prompt baseline and Peec's&lt;br&gt;
$85 tier.&lt;/p&gt;

&lt;p&gt;The catch is sourcing: these figures come from Ranqo's own blog, and I found&lt;br&gt;
no independent review confirming the plan details. Treat it as a strong trial&lt;br&gt;
candidate, not a verified bargain, and confirm everything in-app during the&lt;br&gt;
trial.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Writesonic: tracking plus content, from $39
&lt;/h3&gt;

&lt;p&gt;Writesonic starts from $39 a month and bundles AI visibility monitoring with&lt;br&gt;
a full content suite, including an "Action Center" that turns citation gaps&lt;br&gt;
into concrete content recommendations. That bundle matters for a specific&lt;br&gt;
buyer: the team with no copywriting bandwidth. A pure tracker tells you which&lt;br&gt;
prompts you lose; Writesonic also drafts the fix.&lt;/p&gt;

&lt;p&gt;The catch is overlap. If you already pay for a content tool, a CMS workflow,&lt;br&gt;
or a writer, you are buying features twice. As a pure Scrunch replacement it&lt;br&gt;
is the least like-for-like option here, but as a "tracking plus execution"&lt;br&gt;
bundle for a lean team, the price is hard to argue with.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. HubSpot AI Search Grader: free, one afternoon
&lt;/h3&gt;

&lt;p&gt;HubSpot's AI Search Grader is a free one-off health check that grades your&lt;br&gt;
brand's AI visibility and sorts you into Leader, Challenger, or Niche Player&lt;br&gt;
against competitors. It costs nothing and takes an afternoon, which makes it&lt;br&gt;
the correct first step before spending any money at all.&lt;/p&gt;

&lt;p&gt;The catch is right in the name: grader, not tracker. It is a snapshot, not&lt;br&gt;
ongoing monitoring. Use it to decide whether AI search deserves a budget&lt;br&gt;
line, then pick a real tracker from this list. It is also the natural pick if&lt;br&gt;
you already live in HubSpot's ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Limelit Open: free and open-source, self-hosted
&lt;/h3&gt;

&lt;p&gt;Limelit Open is the outlier on this list and, for technical readers, possibly&lt;br&gt;
the most interesting. Released in October 2026 under the Apache 2.0 license,&lt;br&gt;
it is a self-hosted AI search visibility tracker: a single program, no&lt;br&gt;
database server to configure, data in a local file. It tracks seven engines&lt;br&gt;
(ChatGPT, Claude, Perplexity, Gemini, Google AI Overviews, Google AI Mode,&lt;br&gt;
and Bing Copilot), ships a built-in MCP server with fourteen read tools so an&lt;br&gt;
assistant like Claude can query your visibility data directly, and includes a&lt;br&gt;
free monthly allowance from Limelit's hosted service for five of the engines.&lt;br&gt;
A read-only demo runs unmodified from the public repo.&lt;/p&gt;

&lt;p&gt;The catch is that you are the infrastructure team. Setup, updates, backups,&lt;br&gt;
and scaling are yours. Advanced features like sentiment analysis, Search&lt;br&gt;
Console and GA4 integration, and autonomous agents sit behind Limelit Cloud&lt;br&gt;
credits. But if you are a developer who would rather self-host than&lt;br&gt;
subscribe, this is a genuine $0 ongoing tracker, and the MCP-native design&lt;br&gt;
fits an AI-assisted workflow better than any dashboard on this list.&lt;/p&gt;

&lt;p&gt;{/* MONETIZATION NOTE: Limelit is open-source (no affiliate possible) - link to limelit.co and the GitHub repo; monetize via the freelance audit CTA below instead */}&lt;/p&gt;

&lt;h2&gt;
  
  
  What you give up when you go cheap
&lt;/h2&gt;

&lt;p&gt;A cheaper tool is not a worse tool, but the discount comes from somewhere.&lt;br&gt;
Here is where, so you can decide with your eyes open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt caps bite faster than you expect.&lt;/strong&gt; Fifteen prompts sounds fine until&lt;br&gt;
you list your brand terms, product names, five competitors, and the actual&lt;br&gt;
questions buyers ask. Most real programs land between 30 and 80 prompts,&lt;br&gt;
which is why Peec's 50-prompt Starter and Ranqo's 25-prompt tier exist. Count&lt;br&gt;
your questions before you pick a plan, not after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engine coverage is where upsells hide.&lt;/strong&gt; The four engines everyone tracks&lt;br&gt;
are table stakes now. Claude, Gemini, and Google AI Mode are the ones that&lt;br&gt;
cost extra, on Otterly as add-ons, on Peec as a pick-three constraint, and on&lt;br&gt;
Scrunch itself as an Enterprise upsell. If your audience lives on Claude,&lt;br&gt;
price the add-ons before you compare sticker prices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody else has the AXP layer.&lt;/strong&gt; Scrunch's crawler-facing platform, serving&lt;br&gt;
an AI-optimized version of your site to crawlers, is genuinely unique on this&lt;br&gt;
list. If your site is JavaScript-heavy and AI crawlers struggle to read it,&lt;br&gt;
no $29 tracker fixes that. Fix the crawlability first (or buy Scrunch for&lt;br&gt;
this specific reason), because a tracker cannot measure what crawlers cannot&lt;br&gt;
see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance and scale stay enterprise.&lt;/strong&gt; SOC 2 Type II, SSO/SAML, audit&lt;br&gt;
logs, and thousand-prompt programs live on the expensive tiers everywhere,&lt;br&gt;
not just at Scrunch. If procurement needs paperwork, stop comparing $29 plans&lt;br&gt;
and get the enterprise quote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cheap trackers show the score; expensive ones draft the fix.&lt;/strong&gt; This is the&lt;br&gt;
realest divide in the category. A $29 tool tells you which prompts you lose.&lt;br&gt;
The expensive platforms, and Writesonic's Action Center, tell you what to&lt;br&gt;
publish next. The tool never does the content work for you either way, so&lt;br&gt;
budget for the writing no matter which tier you buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  My verdict: what I would pick, and why
&lt;/h2&gt;

&lt;p&gt;I would not pay Scrunch's $250 a month unless I had a specific reason, and&lt;br&gt;
"it looks like the market leader" is not a reason. Here is how I would&lt;br&gt;
decide, and when the answer flips.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solo dev or small team, under 50 prompts: Peec AI Starter.&lt;/strong&gt; It is the&lt;br&gt;
closest thing to Scrunch's tracking at roughly $165 a month less, with&lt;br&gt;
unlimited seats so the whole team can look at the data. This covers most&lt;br&gt;
indie hackers, agencies with a few clients, and SaaS teams under fifty&lt;br&gt;
people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Absolute minimum spend: Otterly AI Lite at $29.&lt;/strong&gt; Accept the 15-prompt&lt;br&gt;
limit as a learning budget. Run it for two months, learn which questions&lt;br&gt;
actually mention you, then upgrade to Standard or switch to Peec with a real&lt;br&gt;
prompt list instead of guesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical and patient: Limelit Open, $0.&lt;/strong&gt; If you can run a binary and read&lt;br&gt;
a dashboard, you get ongoing seven-engine tracking for the price of a VPS you&lt;br&gt;
probably already have. The MCP server is a genuine bonus if you work with&lt;br&gt;
Claude Code or similar agents daily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Already paying for HubSpot or Semrush: don't add a vendor yet.&lt;/strong&gt; Run&lt;br&gt;
HubSpot's free grader first. If you are on Semrush, their AI Visibility&lt;br&gt;
Toolkit add-on (about $99 a month per domain standalone) keeps you inside a&lt;br&gt;
suite you already pay for. A new subscription should be the last resort, not&lt;br&gt;
the first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay with Scrunch when:&lt;/strong&gt; you track 100 or more prompts, you want the&lt;br&gt;
AXP crawler-facing layer for a hard-to-crawl site, or your company requires&lt;br&gt;
SOC 2 paperwork. At 125 prompts on the entry tier, Scrunch's per-prompt math&lt;br&gt;
actually beats Peec's once you outgrow 50 prompts, so the crossover is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When my answer flips:&lt;/strong&gt; the moment your prompt list passes 100, or&lt;br&gt;
procurement asks for compliance docs, stop optimizing the $29 tier. Add-on&lt;br&gt;
engines and prompt packs stack up fast, and Scrunch's flat 125-prompt Core&lt;br&gt;
starts looking like the simpler, cheaper total. The cheapest tool is the one&lt;br&gt;
you don't outgrow in a quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the cheapest Scrunch AI alternative?
&lt;/h3&gt;

&lt;p&gt;Otterly AI's Lite plan at $29 a month is the cheapest credible paid tracker,&lt;br&gt;
covering 15 prompts across ChatGPT, Google AI Overviews, Perplexity, and&lt;br&gt;
Copilot. For zero cost, HubSpot's AI Search Grader gives a free one-off&lt;br&gt;
health check, and Limelit Open is a free open-source tracker you self-host&lt;br&gt;
for ongoing monitoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many prompts do I actually need to track for GEO?
&lt;/h3&gt;

&lt;p&gt;Most small teams need fewer than 50. Start with the 10 to 20 buyer-intent&lt;br&gt;
questions your customers actually ask, add your brand and product names, then&lt;br&gt;
add competitors. If that list fits under 50 prompts, every tool on this page&lt;br&gt;
except Scrunch's entry tier covers you, which is exactly why cheaper&lt;br&gt;
alternatives exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Scrunch AI worth $250 a month?
&lt;/h3&gt;

&lt;p&gt;It is worth it when you track 100 or more prompts, need its crawler-facing&lt;br&gt;
AXP layer, or your procurement process requires SOC 2 compliance. Below that,&lt;br&gt;
you are paying enterprise money for prompt capacity you will not use, and a&lt;br&gt;
$29 to $95 tool tracks the same engines for the questions that matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I track AI search visibility for free?
&lt;/h3&gt;

&lt;p&gt;Yes, three ways. HubSpot's AI Search Grader is a free one-off audit. Limelit&lt;br&gt;
Open is a free open-source tracker you run yourself with ongoing monitoring.&lt;br&gt;
Or do it manually: run your key prompts in ChatGPT, Gemini, and Perplexity on&lt;br&gt;
a schedule and record whether your brand appears. Manual tracking breaks down&lt;br&gt;
past a dozen prompts, which is when a paid tool pays for itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do cheap GEO tools track the same AI engines as Scrunch?
&lt;/h3&gt;

&lt;p&gt;Mostly. The four engines every buyer cares about, ChatGPT, Google AI&lt;br&gt;
Overviews, Perplexity, and Copilot, are covered by the cheap tools too. The&lt;br&gt;
gaps appear at Claude, Gemini, and Google AI Mode, which Otterly sells as&lt;br&gt;
add-ons and Peec AI lets you pick three of. Scrunch's entry tier covers four&lt;br&gt;
engines as well; wider coverage is an Enterprise upsell there too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will I lose my historical data if I switch GEO tools?
&lt;/h3&gt;

&lt;p&gt;Yes. Prompt history, share-of-voice trends, and citation records do not port&lt;br&gt;
between vendors, so export your reports before you cancel. The practical move&lt;br&gt;
is a one-month overlap: run the new tool alongside Scrunch, confirm the&lt;br&gt;
numbers tell the same story, then cut the $250 subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools, with official links
&lt;/h2&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;Official site&lt;/th&gt;
&lt;th&gt;Entry price (Oct 2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scrunch AI&lt;/td&gt;
&lt;td&gt;&lt;a href="https://scrunch.com" rel="noopener noreferrer"&gt;scrunch.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~$250/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peec AI&lt;/td&gt;
&lt;td&gt;&lt;a href="https://peec.ai" rel="noopener noreferrer"&gt;peec.ai&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;~$85/mo (€85)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Otterly AI&lt;/td&gt;
&lt;td&gt;&lt;a href="https://otterly.ai" rel="noopener noreferrer"&gt;otterly.ai&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;$29/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Limelit Open&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://limelit.co" rel="noopener noreferrer"&gt;limelit.co&lt;/a&gt; / &lt;a href="https://github.com/limelit-co/open" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Free, open-source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;{/* MONETIZATION NOTE: Peec AI and Otterly AI rows are affiliate-ready - swap the official URLs for partner links when programs open. Scrunch row stays a plain link (enterprise sales-led, no self-serve affiliate). Limelit row stays plain (open-source). */}&lt;/p&gt;




&lt;p&gt;If you would rather buy an answer than a subscription, I run GEO visibility&lt;br&gt;
audits for small teams: I map the prompts that matter in your space, check&lt;br&gt;
where you stand across the engines, and hand you the fix list. &lt;a href="https://dev.to/#contact"&gt;Get in&lt;br&gt;
touch&lt;/a&gt; and mention this post.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>marketing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>GPT-4o and GPT-4.1 API Retirement: The January 14 Migration Checklist</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Fri, 23 Oct 2026 05:25:33 +0000</pubDate>
      <link>https://dev.to/zforzain2000/gpt-4o-and-gpt-41-api-retirement-the-january-14-migration-checklist-ce5</link>
      <guid>https://dev.to/zforzain2000/gpt-4o-and-gpt-41-api-retirement-the-january-14-migration-checklist-ce5</guid>
      <description>&lt;p&gt;&lt;br&gt;
  OpenAI published a deprecation notice on October 14 with a date that is&lt;br&gt;
  already uncomfortably close: on January 14, 2027, GPT-4o, GPT-4o mini,&lt;br&gt;
  GPT-4.1 and GPT-4.1 mini leave the API. Not the chat app. The API that&lt;br&gt;
  your code, your cron jobs and your agents call. That is 92 days of&lt;br&gt;
  warning, against OpenAI's own policy of six months. I read the notice,&lt;br&gt;
  the pricing pages and four independent migration writeups, then wrote&lt;br&gt;
  down the 10 checks I would run on any production system before touching&lt;br&gt;
  a single model name.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;On January 14, 2027, five OpenAI snapshots stop answering: gpt-4o-2024-08-06, gpt-4o-2024-11-20, gpt-4o-mini-2024-07-18, gpt-4.1-2025-04-14 and gpt-4.1-mini-2025-04-14. The listed replacement is gpt-6-luna, which is dramatically cheaper on paper but is a reasoning model with different tool-calling behavior. Audit every model reference this week, test Luna on your own traffic, and finish the switch by mid-December so January is a monitoring month, not a firefight.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually retiring
&lt;/h2&gt;

&lt;p&gt;OpenAI's deprecations page is short and worth reproducing exactly, because paraphrasing model IDs is how migrations break:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Retiring snapshot&lt;/th&gt;
&lt;th&gt;Shutdown date&lt;/th&gt;
&lt;th&gt;Recommended replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4o-2024-08-06, gpt-4o-2024-11-20&lt;/td&gt;
&lt;td&gt;January 14, 2027&lt;/td&gt;
&lt;td&gt;gpt-6-luna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4o-mini-2024-07-18&lt;/td&gt;
&lt;td&gt;January 14, 2027&lt;/td&gt;
&lt;td&gt;gpt-6-luna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4.1-2025-04-14&lt;/td&gt;
&lt;td&gt;January 14, 2027&lt;/td&gt;
&lt;td&gt;gpt-6-luna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-4.1-mini-2025-04-14&lt;/td&gt;
&lt;td&gt;January 14, 2027&lt;/td&gt;
&lt;td&gt;gpt-6-luna&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two lines from that page carry most of the weight. First: "Fine-tuned versions of these models follow the same shutdown schedule as their underlying base models." Your &lt;code&gt;ft-gpt-4o&lt;/code&gt; custom model dies the same day. Second: the notice itself is the anomaly. OpenAI's own page sets a minimum notice of six months for generally available models, and this one gives 92 days. The policy carries an exception for safety or compliance retirements, but I found no statement from OpenAI tying this notice to that clause. Read it as it is: an unusually short window.&lt;/p&gt;



&lt;h2&gt;
  
  
  Check 1: one of the retiring snapshots dies today
&lt;/h2&gt;

&lt;p&gt;Before touching the January deadline, check the one that is already here. A separate notice from April 22, 2026 retires an older wave of models on October 23, 2026. Today. That wave includes &lt;code&gt;gpt-4o-2024-05-13&lt;/code&gt; (the very first GPT-4o snapshot), &lt;code&gt;gpt-4-turbo&lt;/code&gt;, &lt;code&gt;gpt-3.5-turbo&lt;/code&gt;, &lt;code&gt;gpt-4.1-nano&lt;/code&gt;, &lt;code&gt;o1&lt;/code&gt;, &lt;code&gt;o3-mini&lt;/code&gt; and &lt;code&gt;o4-mini&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your codebase pins any of these, something may already be failing while you read this. The recommended replacements for this wave are the GPT-5.6 family (&lt;code&gt;gpt-5.6-terra&lt;/code&gt;, &lt;code&gt;gpt-5.6-sol&lt;/code&gt;, &lt;code&gt;gpt-5.6-luna&lt;/code&gt;), not Luna, so do not blend the two migration lists. And keep December 11 on your radar too: the &lt;code&gt;o3-2025-04-16&lt;/code&gt; snapshot retires then, separate from both waves.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  The main app gets attention. The silent failures live in the auxiliary tooling: the code review bot configured a year ago, the changelog summarizer in CI, the script that labels support tickets. They keep working until the day they don't. Grep everything, not just the repo you are proud of.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: find every pinned reference
&lt;/h2&gt;

&lt;p&gt;This is the boring check that catches the expensive bugs. Search your code, your infrastructure and your vendor configs for the retiring model IDs:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"gpt-4o-2024-08-06&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-2024-11-20&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-mini-2024-07-18&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;gpt-4&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1-2025-04-14&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;gpt-4&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1-mini-2025-04-14"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.js"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.yaml"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.yml"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.env*"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not stop at the app code. Check CI workflows, prompt files, agent definitions, Docker build args, Terraform, and any &lt;code&gt;.env&lt;/code&gt; or secrets manager that stores a model name. Then check the tools you did not write: plugins, no-code automations and third-party integrations often hardcode a model ID at install time, and their vendors may or may not have migrated you. If your stack stores the model name in an environment variable, a single config change flips the whole system. If it does not, this migration is also the moment to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 3: do not forget the fine-tunes
&lt;/h2&gt;

&lt;p&gt;I keep repeating this because teams keep missing it: fine-tuned versions follow the base model's shutdown schedule. A fine-tune on GPT-4o is not a separate asset with its own lifecycle; it is a lease that expires with the base model.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is the replacement story. The reported model pages list fine-tuning as "Not supported" on gpt-6-luna, which means a model swap is not a plan for fine-tune teams at all. You need a retraining target: either a model that supports fine-tuning, or a rewrite of the workload as a prompted pipeline on Luna with enough evals to prove the quality holds. Either way, start the eval work now, because retraining on a new base model needs its own data and its own timeline, and January is closer than it feels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 4: Azure buyers get three extra months
&lt;/h2&gt;

&lt;p&gt;If you call OpenAI models through Azure, the dates differ. Microsoft's retirement schedule (last updated September 23, 2026) lists April 14, 2027 for the gpt-4o and GPT-4.1 snapshots that OpenAI itself removes on January 14. The very first GPT-4o snapshot, &lt;code&gt;gpt-4o-2024-05-13&lt;/code&gt;, retires on Azure on December 9, 2026.&lt;/p&gt;

&lt;p&gt;Treat the April date as planning buffer, not an exemption. Being pinned to Azure buys you time to migrate carefully; it does not buy you immunity from the migration. If your deployment spans both direct OpenAI API calls and Azure endpoints, migrate to the direct-API date. The system is only as durable as its earliest deadline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 5: understand the 92-day notice
&lt;/h2&gt;

&lt;p&gt;OpenAI's normal minimum is six months for generally available models. The October 14 notice gives 92 days. That is a third shorter runway than its own policy, with no reason stated.&lt;/p&gt;

&lt;p&gt;There is exactly one official escape hatch I could verify: OpenAI says that in some cases developers may be able to provision dedicated capacity for continued access after a model's shutdown date, arranged through the sales team. That is an enterprise conversation with enterprise pricing, not a migration plan. If you need it, start that conversation now, because sales cycles do not respect deprecation calendars.&lt;/p&gt;

&lt;p&gt;The honest read of the short notice is that model retirements are becoming routine. OpenAI, Anthropic and Google all deprecate older generations on a couple of years' cycle. Any pipeline that hardcodes a single model ID has a built-in expiry date, and the professional response is not outrage. It is a model layer you can swap, which is what Check 9 is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 6: GPT-6 Luna is a reasoning model, not a drop-in swap
&lt;/h2&gt;

&lt;p&gt;This is the trap the headline writers skip. Luna is dramatically cheaper per token, but it does not behave like GPT-4o or GPT-4.1, and the differences are exactly the kind that pass a smoke test and fail in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It reasons by default.&lt;/strong&gt; Luna's &lt;code&gt;reasoning.effort&lt;/code&gt; defaults to medium, and reasoning tokens are billed as output tokens. The same request can produce far more output tokens on Luna than on GPT-4o, so per-token prices do not translate directly to per-request costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calls need the right API.&lt;/strong&gt; Function calling through the Chat Completions API needs &lt;code&gt;reasoning_effort&lt;/code&gt; set to &lt;code&gt;none&lt;/code&gt;. Combining reasoning with tools requires the Responses API. If your agents call tools through Chat Completions, this is a code change, not a config flip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameters change.&lt;/strong&gt; Enabling reasoning drops &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;top_p&lt;/code&gt; and &lt;code&gt;logprobs&lt;/code&gt; that GPT-4.1 supported. If your prompts rely on temperature tuning for variety or consistency, expect different behavior and retest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set &lt;code&gt;reasoning_effort&lt;/code&gt; to &lt;code&gt;none&lt;/code&gt; if you want the closest thing to 4o-style behavior and cost. My recommendation: only enable reasoning for workloads where you have measured that it improves the output. Reasoning is a cost you choose, not a default you accept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 7: run the bill math before believing the headline
&lt;/h2&gt;

&lt;p&gt;The headline number is real and it is misleading at the same time. Standard list prices per million tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Fine-tuning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4o mini&lt;/td&gt;
&lt;td&gt;$0.15&lt;/td&gt;
&lt;td&gt;$0.60&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4.1&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;$8.00&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4.1 mini&lt;/td&gt;
&lt;td&gt;$0.40&lt;/td&gt;
&lt;td&gt;$1.60&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-6-luna&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Against GPT-4o that is 25x cheaper on input and 20x on output. But a worked example from the cost analysis tells the fuller story: on a month of 200M input and 20M output tokens with reasoning off, Luna costs $30 against $42 for GPT-4o mini. The saving is real. But at the default medium reasoning effort, the saving evaporates once Luna's reasoning tokens pass about 24M a month, roughly 1.2x the visible output. One reported catch: once a prompt exceeds 272,000 input tokens, the input rate doubles and the output rate rises 50 percent for the whole request.&lt;/p&gt;

&lt;p&gt;So do the math on your own traffic, with your own reasoning effort setting, not on the headline ratio. I ran through this exercise myself on my own API bills in my &lt;a href="https://dev.to/blog/claude-haiku-5-5-claude-code-cost"&gt;real cost of running an AI SaaS&lt;/a&gt; work, and the pattern holds every time: the headline price is the least important number in your bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 8: test the replacement on your own data
&lt;/h2&gt;

&lt;p&gt;Every careful writeup agrees on this step, and they are right. A newer model formats answers differently, follows the same instructions differently and costs differently. Move a copy of real traffic, not a curated sample, and compare three things: output quality against your existing evals, output format stability (parsers downstream of the model are the first casualty of a swap), and cost at your actual volume.&lt;/p&gt;

&lt;p&gt;If you have no evals, write a small one before the migration. Ten to twenty representative prompts with recorded expected outputs is enough to catch the big regressions. Shadow-run Luna beside the retiring model for a week and diff the outputs. The teams that get hurt by model retirements are never the ones that tested; they are the ones who treated the swap as a config change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 9: pin the replacement so this does not happen again
&lt;/h2&gt;

&lt;p&gt;The deeper lesson from this retirement is architectural. Pinning a dated snapshot does not avoid migration; it trades "whenever the alias is retargeted" for "the retirement date on the provider's schedule," which at least you can plan around. That is strictly better, and it is what made this checklist possible at all.&lt;/p&gt;

&lt;p&gt;For the next cycle, build the swap point once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the model name from an environment variable or config service, never hardcode it in application code.&lt;/li&gt;
&lt;li&gt;Pin to dated snapshots in production so a provider-side alias change cannot surprise you mid-week.&lt;/li&gt;
&lt;li&gt;Keep a per-workload mapping of model to API surface (Chat Completions vs Responses), so the next forced move is a table update, not an archaeology dig.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Model lifecycles in the API era are a couple of years at best. The teams that survive them calmly are the ones whose model choice is a row in a config file, not a string buried in twelve repos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 10: the timeline I would follow
&lt;/h2&gt;

&lt;p&gt;Deadlines work when they are earlier than the real one. Mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This week:&lt;/strong&gt; run Check 2 across every repo, config and vendor integration. Inventory fine-tunes (Check 3) and note which workloads call tools (Check 6).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By mid-December:&lt;/strong&gt; finish evals and shadow runs (Check 8), complete the bill math (Check 7), and switch production traffic. December is for migration; January is for monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;January:&lt;/strong&gt; watch error rates and costs on the new model for two weeks. Keep the old code path behind a feature flag until you are confident, then delete it. Dead code referencing dead models is how the next retirement surprises you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would actually do
&lt;/h2&gt;

&lt;p&gt;If I were handed this on Monday morning, I would not start with Luna. I would start with the grep, because in my experience the inventory is always worse than the team believes: at least one model reference will be hiding in a place nobody remembers. Then I would split workloads by tool use. Anything that calls tools through Chat Completions either gets &lt;code&gt;reasoning_effort: none&lt;/code&gt; on Luna or moves to the Responses API, and that decision is per workload, not per company. Fine-tune owners get the uncomfortable conversation early, because retraining has its own timeline and January does not negotiate. The price drop is genuinely good news, but I would treat it as a bonus discovered after the migration is proven, not as the reason for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which OpenAI models retire on January 14, 2027?
&lt;/h3&gt;

&lt;p&gt;Five snapshots across four models: gpt-4o-2024-08-06, gpt-4o-2024-11-20, gpt-4o-mini-2024-07-18, gpt-4.1-2025-04-14 and gpt-4.1-mini-2025-04-14. The notice was posted on October 14, 2026, which is only 92 days of warning. Fine-tuned versions of these models follow the same shutdown date as their base model.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should I migrate to instead of GPT-4o or GPT-4.1?
&lt;/h3&gt;

&lt;p&gt;OpenAI's deprecations page lists gpt-6-luna as the recommended replacement for all five retiring snapshots. It costs $0.10 per million input tokens and $0.50 per million output tokens against $2.50 and $10 for GPT-4o, but it is a reasoning model with different tool-calling behavior, so it is not a drop-in swap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does my fine-tuned GPT-4o model keep working after January 14?
&lt;/h3&gt;

&lt;p&gt;No. OpenAI states plainly that fine-tuned versions of these models follow the same shutdown schedule as their underlying base models. A fine-tune on GPT-4o dies the same day as GPT-4o, and gpt-6-luna does not support fine-tuning, so fine-tune teams need a retraining plan on a different model, not just a model swap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is GPT-6 Luna not a simple drop-in replacement?
&lt;/h3&gt;

&lt;p&gt;Luna is a reasoning model that defaults to medium reasoning effort, and reasoning tokens are billed as output tokens. Function calling through Chat Completions needs reasoning_effort set to none; combining tools with reasoning requires the Responses API; and turning reasoning on drops temperature, top_p and logprobs that GPT-4.1 supported.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Azure OpenAI retire GPT-4o on the same date?
&lt;/h3&gt;

&lt;p&gt;No. Microsoft's retirement schedule, last updated September 23, 2026, lists April 14, 2027 for the gpt-4o and GPT-4.1 snapshots that OpenAI removes on January 14. Azure buys you three extra months of buffer, not an exemption. Plan to be migrated by January anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if I miss the January 14 deadline?
&lt;/h3&gt;

&lt;p&gt;API requests to the retired model IDs start failing after that date, which means whatever still names them goes silent: chatbots, triage scripts, code review bots, summarizers. In some cases OpenAI says developers can provision dedicated capacity for continued access after shutdown through its sales team, but that is an enterprise escape hatch, not a plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The money angle
&lt;/h2&gt;

&lt;p&gt;Retirement deadlines are consulting gold. Every team pinned to GPT-4o or GPT-4.1 needs the same three things between now and mid-December: an audit, an eval-driven migration, and a bill review. If you offer OpenAI migration work to clients, this is your outreach angle for the next six weeks, and it expires on January 14. The same playbook applies to every future retirement, so the first engagement builds you a reusable offer.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  If your production system calls GPT-4o or GPT-4.1 and you want the audit and migration handled by someone who has done it before, reach out through the &lt;a href="https://dev.to/#contact"&gt;contact section on my homepage&lt;/a&gt;. I take on a small number of API migration and cost-optimization engagements each month.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI, &lt;a href="https://developers.openai.com/api/docs/deprecations" rel="noopener noreferrer"&gt;Deprecations&lt;/a&gt; - the October 14 and April 22 notices, fine-tuning schedule, dedicated capacity note&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sloppish.com/gpt-4o-goes-to-luna.html" rel="noopener noreferrer"&gt;Nothing Left to Tune&lt;/a&gt; - price table, fine-tuning status, reasoning effort levels&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tokencost.app/blog/gpt-4o-gpt-4-1-retirement-migration-cost" rel="noopener noreferrer"&gt;GPT-4o and GPT-4.1 Retirement: Migration Cost to Luna&lt;/a&gt; - cost math, Azure dates, cached input pricing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://origami.sa/en/blog/gpt-4o-retirement-api-migration/" rel="noopener noreferrer"&gt;Built on GPT-4o? OpenAI Switches It Off on January 14, 2027&lt;/a&gt; - audit-first timeline&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aivax.net/blog/pin-llm-model-id-or-use-alias-model-deprecations/" rel="noopener noreferrer"&gt;Pin the model id or use the alias&lt;/a&gt; - the October 23 wave table, snapshot pinning strategy&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devshot.co/archives/devshot-20261016-17" rel="noopener noreferrer"&gt;Devshot, October 16, 2026&lt;/a&gt; - tool-call behavior differences, long-context pricing threshold&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cellcog.ai/blog/gpt-4o-api-shutdown/" rel="noopener noreferrer"&gt;GPT-4o API Shutdown&lt;/a&gt; - the short-notice policy analysis, dedicated capacity&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://codemouse.ai/blog/gpt-4o-gpt-4-1-retirement-what-it-means-for-ai-code-review" rel="noopener noreferrer"&gt;What it means for AI code review&lt;/a&gt; - auxiliary tooling blind spots&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blueshore.ai/blog/openai-model-retirement-small-business-ai-automations/" rel="noopener noreferrer"&gt;Model retirement and small-business automations&lt;/a&gt; - testing before switching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;{/* MONETIZATION NOTE: swap the contact CTA for an affiliate placement if a relevant program (OpenAI partner, migration tooling) exists */}&lt;/p&gt;

</description>
      <category>openai</category>
      <category>ai</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Payoneer vs Wise vs Elevate Pay: What I Would Pick in Pakistan (2026)</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Thu, 22 Oct 2026 05:36:25 +0000</pubDate>
      <link>https://dev.to/zforzain2000/payoneer-vs-wise-vs-elevate-pay-what-i-would-pick-in-pakistan-2026-425</link>
      <guid>https://dev.to/zforzain2000/payoneer-vs-wise-vs-elevate-pay-what-i-would-pick-in-pakistan-2026-425</guid>
      <description>&lt;p&gt;&lt;br&gt;
  A $1,000 client payment lands very differently depending on the rail you&lt;br&gt;
  choose. Through Payoneer to a Pakistani bank, about $40 disappears in&lt;br&gt;
  receiving and withdrawal fees. Through Wise, the same $1,000 costs the&lt;br&gt;
  sender $6.29 and arrives at the mid-market rate. Through Elevate Pay, the&lt;br&gt;
  withdrawal fee is a flat $1.50. I read all three fee schedules, plus a&lt;br&gt;
  stack of 2026 user reviews, so you can pick your rail in ten minutes.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Payoneer costs roughly 4% all-in on a $1,000 payout but works everywhere. Wise costs 0.6-1.3% at the mid-market rate, but Pakistanis cannot hold a Wise account - your client must send. Elevate Pay charges a $1.50 flat withdrawal fee with an undisclosed FX spread. I would use Elevate for direct clients, Payoneer for marketplaces, and Wise whenever a client offers it.&lt;/p&gt;



&lt;p&gt;The question that decides everything is not "which is cheapest" but "who pays the fee." On Wise, your client pays. On Payoneer and Elevate, you pay. Keep that split in your head for the rest of this post, because it flips the ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payoneer in 2026: the 4% convenience tax
&lt;/h2&gt;

&lt;p&gt;A Pakistani freelancer receiving $1,000 through Payoneer pays 1% on the way in and up to 3% on the way out to a PKR bank. That is about $40 gone before the money reaches home. The 3% withdrawal fee is a 2026 increase from 2%, and there is a $29.95 annual fee if the account receives under $6,000 in 12 months.&lt;/p&gt;

&lt;p&gt;Payoneer is the default rail for a reason: it is the only one of the three that plugs directly into Upwork and Fiverr. Here is the 2026 fee reality, from Payoneer's own schedule as quoted by multiple 2026 fee guides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Receiving via your USD receiving account:&lt;/strong&gt; 1% (minimum $1). Payoneer issues you virtual US routing and account details, so a US client pays by domestic ACH. Because your local currency is PKR, USD counts as "non-local" and the 1% applies. Money from another Payoneer user is free; card payments cost up to 3.99% + $0.49.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Receiving from Upwork or Fiverr:&lt;/strong&gt; 1%. This is the number that matters most if marketplaces are your income.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Withdrawing to a Pakistani bank:&lt;/strong&gt; Pakistani accounts now pay a flat 3% on cross-currency withdrawals, up from 2% - a 50% increase reported in September 2026. Payoneer's global schedule says "up to 3%," and Pakistani users report the full 3% being charged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The worked example:&lt;/strong&gt; $1,000 from Upwork means $10 receiving + $30 withdrawal = &lt;strong&gt;about $40, or 4%&lt;/strong&gt;. Older fee tables put the same trip at roughly $31.50. One Pakistani guide also reports an additional ~2% conversion layer on top, but that figure appears in a single source and Payoneer's schedule already caps the total at 3% - do not stack the two. Payoneer shows the exact fee on your dashboard before you confirm; that quote is the only number that matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The annual fee trap:&lt;/strong&gt; $29.95 per year if your account receives under $6,000 in any 12 months. Older writeups say the threshold was $2,000 - the 2026 sources say $6,000. If you are just starting out and your first year is slow, this one bites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The card:&lt;/strong&gt; the Payoneer Mastercard costs $29.95/year, with a $24.95 activation fee in Pakistan and $3.15 per ATM withdrawal. Only get the card if you actually spend in USD; otherwise it is a subscription to nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;br&gt;
  Payoneer's refer-a-friend program pays &lt;strong&gt;$25 to you and $25 to the friend&lt;/strong&gt; once they receive $1,000 - and you must have received $1,000 yourself to be eligible. If you are onboarding freelancer friends anyway, route them through your link. It is the rare Payoneer fee flow that runs in your favor.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Payoneer is expensive the way a toll road is expensive: you pay because it is the only road that reaches the destination. For marketplace income, there is no real alternative. For direct clients, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wise: the cheapest rate you cannot sign up for
&lt;/h2&gt;

&lt;p&gt;Wise converts at the real mid-market rate and charges roughly 0.6-1.3% on a $1,000 transfer to Pakistan. The catch is structural: Wise does not onboard Pakistan residents, so you cannot hold a balance, get account details, or earn referral rewards. The model that works is your client sending from their Wise account straight to your Pakistani IBAN.&lt;/p&gt;

&lt;p&gt;This is the single most misunderstood fact in every "Payoneer vs Wise" thread aimed at Pakistanis. You are not choosing between two accounts. You are choosing between &lt;em&gt;your&lt;/em&gt; account (Payoneer) and &lt;em&gt;your client's&lt;/em&gt; account (Wise). Here is what the client-side numbers look like, from Wise's own pricing tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$1,000 from a Wise balance to a Pakistani bank:&lt;/strong&gt; $6.29 fee (0.63%), delivering about Rs 275,300 at the mid-market rate (September 2026 quote).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$1,000 funded by bank transfer:&lt;/strong&gt; $12.75 (1.28%). By debit card: $18.87. Same-day arrival after conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No account on your side, no monthly fees, no inactivity fees.&lt;/strong&gt; You hand the client your 24-character IBAN starting with PK and your full name. That is the entire setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The limits and quirks, all Pakistan-specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rs 35,000,000 per transfer&lt;/strong&gt; - generous enough that you will not hit it freelancing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No outbound from PKR.&lt;/strong&gt; Wise is a receive-only channel for Pakistanis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some recipient accounts get rejected:&lt;/strong&gt; Meezan Bank Express and Allied Bank Express accounts, Asaan accounts near their Rs 500,000 limit, and all business accounts. Use a regular personal account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No JazzCash/Easypaisa delivery.&lt;/strong&gt; Bank transfer only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The referral program is dead to you.&lt;/strong&gt; Wise pays invite rewards in a list of currencies that explicitly excludes PKR, and you cannot hold an account anyway. Do not build a strategy around it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical move: add one line to your invoices - &lt;em&gt;"Wise transfer to the IBAN below is preferred; it costs you about 1%."&lt;/em&gt; Clients who already use Wise will take the hint, and every payment that arrives this way costs you exactly zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Elevate Pay: the $1.50 flat-fee challenger
&lt;/h2&gt;

&lt;p&gt;Elevate Pay gives you a real US USD account in your name, free inbound ACH, and a flat $1.50 fee to withdraw to a Pakistani bank or wallet. The honest caveat is the exchange rate: Elevate's terms admit the rate "may include a markup" without publishing the percentage, and user reviews consistently say Wise quotes a better rate.&lt;/p&gt;

&lt;p&gt;Elevate is the Pakistani-built answer to Payoneer's fees - a US-registered service (Bloom Financial Technologies Inc) that hands freelancers actual USD account details. As of October 2026 it is alive and actively maintained (app updated October 7), with a 4.5-star Play Store rating across 5,700+ reviews. The fee structure is refreshingly simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inbound ACH:&lt;/strong&gt; free. Your US client sends a domestic transfer; $1,000 arrives as $1,000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inbound wire:&lt;/strong&gt; not live yet. The pricing page lists $12.50 with an "in the pipeline" note, so wire-only clients should stay on Payoneer for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Withdraw USD to PKR:&lt;/strong&gt; &lt;strong&gt;$1.50 flat&lt;/strong&gt;, no percentage, to a Pakistani bank, JazzCash, Easypaisa, or NayaPay. A running campaign makes NayaPay withdrawals $0 and instant - but no end date is published, so treat it as temporary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spread is the unknown.&lt;/strong&gt; No source publishes the FX markup percentage. The terms only say a markup "may" be included. Multiple 2026 user reviews report Wise beating Elevate's rate. My rule: a $1.50 fee means nothing if the rate is 1.5% worse than mid-market. Always compare the live in-app quote against the interbank rate (around Rs 277/USD in late October 2026) before confirming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits:&lt;/strong&gt; $5,000/day and $20,000/month receiving, raisable on request. Fine for most freelancers; a problem if you scale past it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signup:&lt;/strong&gt; NIC or passport, proof of income (Upwork payout report works), and proof of address. Officially 24-48 hours; a September 2026 review reports weeks of delays and repeated document requests. Start verification before you need the money, not after.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;br&gt;
  Two things to know before you route your rent money through Elevate. First, the company migrated banking partners in 2025 and users report accounts were closed for months during the move - the current sponsor bank is not even named in 2026 materials. Second, an October 2026 review describes a $3,498 withdrawal held by compliance for days with no timeline. Keep Payoneer warm as a backup rail until Elevate has earned your trust with smaller amounts.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Two genuinely useful 2026 developments: the &lt;strong&gt;NayaPay partnership&lt;/strong&gt; issues an automated ePRC with proper purpose codes (email &lt;a href="mailto:prc@nayapay.com"&gt;prc@nayapay.com&lt;/a&gt;) - the paperwork you need for the PSEB 0.25% tax filing I covered in my &lt;a href="https://dev.to/blog/freelancing-pakistan-web-developer-2026"&gt;freelancing guide&lt;/a&gt; - and one thing Elevate still cannot do is receive USD-to-USD inside Pakistan, which its own feature board ties to PSEB compliance. Payoneer handles that flow; Elevate does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real comparison: what $1,000 becomes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Payoneer&lt;/th&gt;
&lt;th&gt;Wise (client sends)&lt;/th&gt;
&lt;th&gt;Elevate Pay&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Receiving fee&lt;/td&gt;
&lt;td&gt;1% ($10)&lt;/td&gt;
&lt;td&gt;$0 to you&lt;/td&gt;
&lt;td&gt;Free (ACH)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Withdrawal / transfer fee&lt;/td&gt;
&lt;td&gt;3% ($30)&lt;/td&gt;
&lt;td&gt;$6.29-$12.75 (sender pays)&lt;/td&gt;
&lt;td&gt;$1.50 flat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exchange rate&lt;/td&gt;
&lt;td&gt;Payoneer's rate, up to 3% total&lt;/td&gt;
&lt;td&gt;Mid-market, zero markup&lt;/td&gt;
&lt;td&gt;Undisclosed markup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total cost on $1,000&lt;/td&gt;
&lt;td&gt;~$40 (4%)&lt;/td&gt;
&lt;td&gt;~$6.29-$12.75 (0.6-1.3%)&lt;/td&gt;
&lt;td&gt;$1.50 + unknown spread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who pays the fee&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Your client&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRC / remittance proof&lt;/td&gt;
&lt;td&gt;Request from your receiving bank&lt;/td&gt;
&lt;td&gt;Arrives as local transfer; confirm classification&lt;/td&gt;
&lt;td&gt;ePRC via NayaPay (codes 9186/9471)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly limits&lt;/td&gt;
&lt;td&gt;None published&lt;/td&gt;
&lt;td&gt;Rs 35M per transfer&lt;/td&gt;
&lt;td&gt;$5k/day, $20k/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Upwork/Fiverr payouts&lt;/td&gt;
&lt;td&gt;Direct clients who use Wise&lt;/td&gt;
&lt;td&gt;Your own USD account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Payoneer for everything, because it is the only name you know.&lt;/li&gt;
&lt;li&gt;Eat ~4% on every payout without thinking about it.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One rail, one point of failure, zero leverage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Marketplaces pay into Payoneer (1% in, no alternative).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Direct clients get your Elevate USD details or a nudge toward Wise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Compare the live quote against interbank before every withdrawal.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  My verdict: what I would pick, and why
&lt;/h2&gt;

&lt;p&gt;If I were setting up my payment rails from scratch in Pakistan today, I would run &lt;strong&gt;two rails, not one&lt;/strong&gt;. Elevate Pay becomes my USD receiving account for direct clients: free inbound ACH and a $1.50 withdrawal fee is simply the cheapest way to hold dollars in my own name. Payoneer stays connected to Upwork and Fiverr, because marketplaces leave me no choice and 4% is the cost of that convenience. And every invoice I send to a direct client gets the one-line Wise nudge, because a payment that costs me zero beats a payment that costs me $1.50.&lt;/p&gt;

&lt;p&gt;That verdict flips under four conditions, and you should know all four:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You cross $20,000/month.&lt;/strong&gt; Elevate's receiving cap forces the overflow onto Payoneer. Good problem to have; plan for it before it happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your client pays by wire only.&lt;/strong&gt; Elevate cannot receive wires yet. Payoneer's receiving account is the fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need a USD card.&lt;/strong&gt; Elevate's card situation is unverified in 2026 - do not count on it. Payoneer's Mastercard exists, at $29.95/year plus $24.95 activation in Pakistan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elevate's onboarding stalls.&lt;/strong&gt; If verification drags past two weeks, route that payment through Payoneer and keep pushing Elevate's support in parallel. Never let a rail you are still setting up hold a client's money hostage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The meta-lesson: the cheapest rail is the one where someone else pays the fee. Nudging clients toward Wise costs you one invoice line and saves you $40 per $1,000. That is the highest-ROI sentence in this entire post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set it up once, in this order
&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
  Sign up with your NIC or passport, an Upwork payout report or client contract as proof of income, and a recent bank statement or utility bill as proof of address. Officially this takes 24-48 hours; 2026 reviews say it can take weeks. Do this on a quiet Tuesday, not the day a $2,000 payment is due.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Leave your Upwork and Fiverr payouts pointed at Payoneer. Note the $29.95 annual fee if you receive under $6,000 in 12 months, and grab the $25 referral bonus for every freelancer friend you onboard after they cross $1,000 received.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Put this on your invoice: "Bank transfer to the USD account details below, or Wise to my Pakistani IBAN - Wise costs you about 1%." You have now made the cheapest option the easiest option.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Open your interbank rate, open the app's quote, subtract. On Elevate this is the whole game because the spread is unpublished. On Payoneer the dashboard shows the exact fee before you confirm. Never withdraw blind.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Every incoming payment needs a Proceeds Realization Certificate for the PSEB 0.25% freelancer tax filing. Elevate via NayaPay gives you an automated ePRC; with Payoneer and Wise, request it from the Pakistani bank that received the funds and confirm the transfer is classified as a remittance. The full tax setup is in my &lt;a href="https://dev.to/blog/freelancing-pakistan-web-developer-2026"&gt;freelancing guide&lt;/a&gt;.&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which is cheapest for Pakistani freelancers: Payoneer, Wise, or Elevate Pay?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on who pays the fee. Wise is cheapest overall at about 0.6-1.3% on $1,000, but your client pays it and you cannot hold a Wise account from Pakistan. Elevate Pay costs you a flat $1.50 withdrawal fee plus an undisclosed FX spread. Payoneer is the most expensive at roughly 4% all-in, but it is the only rail that plugs straight into Upwork and Fiverr.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I open a Wise account from Pakistan?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Wise does not onboard Pakistan residents, so you cannot hold a balance, get USD/EUR account details, or get a Wise card. What works is the reverse: your client sends from their Wise account directly to your Pakistani bank IBAN. You need no Wise account on your side, and the transfer arrives at the mid-market rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Elevate Pay legit and safe for Pakistani freelancers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Elevate Pay is a US-registered service (Bloom Financial Technologies Inc) giving you a real USD account in your name, FDIC-insured through its US banking partner. It is actively maintained (app updated October 2026) and widely used by Pakistani freelancers. Caveats from 2026 reviews: the FX spread is undisclosed, onboarding can take weeks, and the company changed banking partners in 2025. It is legit, but young - keep Payoneer as backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does Payoneer charge to withdraw $1,000 to a Pakistani bank in 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;About $40 all-in: 1% ($10) receiving fee when the money lands in your Payoneer USD receiving account, plus a 3% ($30) withdrawal fee to a Pakistani bank - the 3% rate is a 2026 increase from 2%. Older fee tables show closer to $31.50; Payoneer shows the exact figure on your dashboard before you confirm, and that quote is authoritative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I still need Payoneer if I use Upwork?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For marketplace work, yes. Upwork and Fiverr pay out to Payoneer directly at a 1% receiving fee, and neither can send to Elevate Pay or Wise accounts held by Pakistanis (Wise cannot be held at all). Elevate Pay works with Upwork through the Direct to U.S. Bank option, but Upwork charges $2.99 per withdrawal that way since September 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I get a PRC (Proceeds Realization Certificate) with these services?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask your bank for the PRC against the incoming transfer - you need it for the PSEB 0.25% freelancer tax filing. Elevate Pay users route through NayaPay, which issues an automated ePRC (email &lt;a href="mailto:prc@nayapay.com"&gt;prc@nayapay.com&lt;/a&gt;) with purpose codes 9186 or 9471. With Payoneer and Wise, request the certificate from the Pakistani bank that received the funds and confirm the transfer is classified as a remittance, not a local transfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools worth bookmarking
&lt;/h2&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;What it is&lt;/th&gt;
&lt;th&gt;Why it is on this list&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Payoneer&lt;/td&gt;
&lt;td&gt;The universal freelancer rail: USD receiving accounts, marketplace payouts, USD card&lt;/td&gt;
&lt;td&gt;Only rail for Upwork/Fiverr; $25 referral each side after $1,000 received&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wise&lt;/td&gt;
&lt;td&gt;Mid-market transfers your client sends to your IBAN&lt;/td&gt;
&lt;td&gt;Cheapest transparent rate; costs you $0 when the client sends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elevate Pay&lt;/td&gt;
&lt;td&gt;Pakistani-focused USD account, $1.50 flat withdrawals&lt;/td&gt;
&lt;td&gt;Cheapest way to hold dollars in your own name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NayaPay&lt;/td&gt;
&lt;td&gt;Local wallet + ePRC issuance for Elevate users&lt;/td&gt;
&lt;td&gt;$0 campaign withdrawals; automated PRC paperwork&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;{/* MONETIZATION NOTE: swap the Payoneer row link with the Payoneer refer-a-friend link ($25 each after $1,000 received) when the referral URL is available - do not hardcode until confirmed. &lt;em&gt;/}&lt;br&gt;
{/&lt;/em&gt; MONETIZATION NOTE: Wise referral rewards cannot be paid in PKR and Pakistanis cannot hold accounts - do NOT present Wise as an affiliate opportunity for this audience. &lt;em&gt;/}&lt;br&gt;
{/&lt;/em&gt; MONETIZATION NOTE: Fiverr Affiliates CPA ($15-150 per first-time buyer) fits the marketplace row of the freelancing funnel - add when the affiliate link is issued. */}&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  I help freelancers and founders getting paid from abroad sort the unglamorous plumbing: the accounts, the PRC paperwork, and the tax filing, in one pass. If you would rather skip the trial and error, reach me at &lt;a href="https://dev.to/#contact"&gt;/#contact&lt;/a&gt; and mention "payment rails."&lt;br&gt;
&lt;/p&gt;

</description>
      <category>freelancing</category>
      <category>fintech</category>
      <category>career</category>
      <category>money</category>
    </item>
    <item>
      <title>Agentic Checkout Checklist: 9 Fixes So an AI Agent Can Buy From You</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:29:34 +0000</pubDate>
      <link>https://dev.to/zforzain2000/agentic-checkout-checklist-9-fixes-so-an-ai-agent-can-buy-from-you-3adj</link>
      <guid>https://dev.to/zforzain2000/agentic-checkout-checklist-9-fixes-so-an-ai-agent-can-buy-from-you-3adj</guid>
      <description>&lt;p&gt;&lt;br&gt;
  Last week, Wix plugged an AI wallet into its small-business agents and&lt;br&gt;
  PayPal put agent checkout on 60 million living-room screens. Your&lt;br&gt;
  first thought was probably the same as mine: can an AI agent buy from&lt;br&gt;
  my store yet, and do I need to build something for it? I spent days&lt;br&gt;
  reading the protocols, the merchant pages, and the card-network rulebooks&lt;br&gt;
  so you can skip the news cycle and get the checklist instead. Short&lt;br&gt;
  version: the answer is yes, and the work is almost entirely on your own&lt;br&gt;
  checkout - the part you already own.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Yes, an AI agent can buy from your store today - but almost certainly&lt;br&gt;
through your own checkout page, not inside ChatGPT or Google AI Mode. The&lt;br&gt;
in-chat checkouts are gated to approved platforms, mostly US-only, and&lt;br&gt;
exclude subscriptions and digital goods. The agents that reach a normal&lt;br&gt;
store - Meta's Muse, Amazon's Buy for Me, OpenAI's dots, Gemini's Task&lt;br&gt;
agent - browse your product pages and finish payment on your site, through&lt;br&gt;
your existing payment provider. So the useful question is not "which&lt;br&gt;
protocol do I adopt" but "can an agent actually finish my checkout." Nine&lt;br&gt;
fixes decide that, and most of them are configuration, not code.&lt;/p&gt;
&lt;h2&gt;
  
  
  The three ways an agent pays you
&lt;/h2&gt;

&lt;p&gt;Before the checklist, you need the mental model, because "an AI agent&lt;br&gt;
buys from you" covers three different things. This framing comes from&lt;br&gt;
Okane Land's October 2026 review of the assistants' merchant pages and&lt;br&gt;
terms, and it is the clearest cut I have seen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;In-chat checkout.&lt;/strong&gt; The shopper stays in the assistant. The assistant
runs the checkout and calls your systems through a protocol - the
Agentic Commerce Protocol (ACP, from OpenAI and Stripe) or the
Universal Commerce Protocol (UCP, from Google and Shopify). This path
reaches sellers through Shopify, Etsy, PayPal, or Stripe, or by
application - and it is US-buyer focused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent on your site.&lt;/strong&gt; The shopper's agent opens your product page and
fills in your checkout the way a browser would, then pays through your
own payment provider. This works on any site an agent can finish - no
protocol, no approval, no platform middleman.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine payments for APIs.&lt;/strong&gt; A software agent pays per request for your
API through Stripe's Machine Payments Protocol or the x402 standard.
This is the path for sellers of downloads, courses, or API access -
agents buying compute, not t-shirts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you sell physical or digital goods on your own site, you mostly live in&lt;br&gt;
path two. If you sell API access, path three deserves its own post. Path&lt;br&gt;
one is the one getting all the headlines, so let me deflate it honestly&lt;br&gt;
before we get to work.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the in-chat checkouts actually cover
&lt;/h2&gt;

&lt;p&gt;Here is the part the announcements skip. I have read the current merchant&lt;br&gt;
terms so you can see how narrow path one really is for an independent&lt;br&gt;
seller:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Program&lt;/th&gt;
&lt;th&gt;Status in October 2026&lt;/th&gt;
&lt;th&gt;Who can sell&lt;/th&gt;
&lt;th&gt;Subscriptions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT / Agentic Commerce Protocol&lt;/td&gt;
&lt;td&gt;In-chat checkout scaled back; OpenAI moved to discovery-first and now sends shoppers to finish on the merchant's own site&lt;/td&gt;
&lt;td&gt;Shopify and Etsy catalogs; others apply and wait&lt;/td&gt;
&lt;td&gt;Not through plugins - physical goods only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google AI Mode and Gemini / Universal Commerce Protocol&lt;/td&gt;
&lt;td&gt;Live in the US&lt;/td&gt;
&lt;td&gt;Merchant Center sellers Google approves, or sellers connected through a platform like Shopify&lt;/td&gt;
&lt;td&gt;Excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Copilot Checkout&lt;/td&gt;
&lt;td&gt;Rolling out since January 8, 2026; a pilot for some merchants&lt;/td&gt;
&lt;td&gt;English-language merchants selling to US buyers, through PayPal, Shopify, or Stripe&lt;/td&gt;
&lt;td&gt;Not in Shopify's Copilot checkout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity Instant Buy&lt;/td&gt;
&lt;td&gt;Live in the US with PayPal&lt;/td&gt;
&lt;td&gt;Merchants on BigCommerce, Shopware, and Wix, among others&lt;/td&gt;
&lt;td&gt;No rule found&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three honest observations from that table. First, if you are not on&lt;br&gt;
Shopify, Etsy, BigCommerce, Wix, or Shopware, path one is not open to you&lt;br&gt;
today - and implementing ACP yourself does not fix that, because the&lt;br&gt;
protocol's own site says each AI platform manages its own merchant&lt;br&gt;
onboarding. Second, everything in path one sells to US buyers; if your&lt;br&gt;
customers are in Pakistan, Europe, or anywhere else, path two is your only&lt;br&gt;
path. Third, subscriptions and digital goods are excluded across the&lt;br&gt;
board in path one, while path two has no such restriction - an agent can&lt;br&gt;
buy your course or your SaaS plan on your own checkout exactly like a&lt;br&gt;
human can.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  AI-assisted buying is growing from a tiny base. A Frankfurt School study&lt;br&gt;
  of 973 online shops found chatbot referrals under 0.2% of traffic.&lt;br&gt;
  Ethercycle counted 0.10% of sessions and 0.12% of revenue from AI&lt;br&gt;
  assistants across ten established Shopify stores in 2026 - about&lt;br&gt;
  $131,000 out of $109.6 million. Shoppers are cautious too: about 14% of&lt;br&gt;
  Americans trust AI to place orders (YouGov), 11% have used AI to complete&lt;br&gt;
  a transaction, and seven in ten want the final say first (NMI). Fix your&lt;br&gt;
  checkout because it is cheap and durable, not because a tidal wave is&lt;br&gt;
  arriving tomorrow.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about: you eat the chargebacks
&lt;/h2&gt;

&lt;p&gt;This is the section that changes the math on how much to invest. No card&lt;br&gt;
network has moved the risk of agent purchases onto the agent. Visa's&lt;br&gt;
rulebook (April 2026 edition, with agent rules added in October 2025)&lt;br&gt;
treats the registered provider of an agent payment as the cardholder and&lt;br&gt;
holds the cardholder responsible for the agent's actions as if they had&lt;br&gt;
made the purchase themselves - and it forbids you from requiring a buyer&lt;br&gt;
to waive dispute rights, agent purchases included. Mastercard's merchant&lt;br&gt;
guide says standard fraud detection, chargeback rights, and dispute&lt;br&gt;
processes apply to agent purchases. Stripe's seller terms make you the&lt;br&gt;
merchant of record, responsible for disputes, refunds, and transaction&lt;br&gt;
fraud. Worldpay's one-line summary is the one I would tattoo on the&lt;br&gt;
project plan: &lt;strong&gt;no liability shift exists yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The unsettled case is the purchase that was authorized but wrong - the&lt;br&gt;
agent bought what the shopper did not mean. Two companies have started on&lt;br&gt;
it from the other side: Stripe's terms for agent operators make them&lt;br&gt;
answerable to Stripe for purchases caused by bugs, hallucinations, or&lt;br&gt;
misinterpretations, and American Express has announced a future protection&lt;br&gt;
for cardholders whose purchase strays from approved intent. Neither&lt;br&gt;
changes what you face when the chargeback arrives. So: keep your dispute&lt;br&gt;
evidence habits exactly as they are, and do not assume an agent's mistake&lt;br&gt;
is the agent's bill.&lt;/p&gt;
&lt;h2&gt;
  
  
  The checklist, part 1: make your checkout finishable
&lt;/h2&gt;

&lt;p&gt;These are the fixes that decide whether an agent completes a purchase on&lt;br&gt;
your site. Do them in order; each one removes a failure mode I keep seeing&lt;br&gt;
in the agent-buying reports.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
  An agent cannot log into a user account it does not have, and it cannot&lt;br&gt;
  create one reliably mid-purchase. If your checkout forces account&lt;br&gt;
  creation before payment, you have built a wall that stops agents and&lt;br&gt;
  annoys humans equally. Keep account creation after payment - optional,&lt;br&gt;
  on the confirmation page, never in the critical path.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Browser-driven agents finish fastest with saved wallets. Stripe's Link&lt;br&gt;
  is the one with documented agent support - Meta's Muse pays instantly at&lt;br&gt;
  the million-plus businesses that accept it, using the shopper's saved&lt;br&gt;
  payment method - and Google Pay covers the Google-side agents. If you&lt;br&gt;
  already take Stripe, Link is usually a toggle, not a project. If your&lt;br&gt;
  only options are manual card entry plus a local bank transfer, an agent&lt;br&gt;
  will stall at the payment form exactly where a distracted human would&lt;br&gt;
  abandon the cart.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  This one is counterintuitive and it is the fix most stores miss. Your&lt;br&gt;
  Cloudflare bot-fight mode, your WAF rules, your CAPTCHA on the checkout&lt;br&gt;
  page - they do not know the difference between a scraper and a signed&lt;br&gt;
  shopping agent, and they will block both. The networks have built the&lt;br&gt;
  plumbing for exactly this: Mastercard's Agent Pay builds on Web Bot Auth,&lt;br&gt;
  Visa's Trusted Agent Protocol aligns with it, and Cloudflare has counted&lt;br&gt;
  signed agents as verified bots since July 1 - with one warning worth&lt;br&gt;
  reading twice, that you may trust the intermediary operator but not&lt;br&gt;
  every end user driving it. The practical job: check your host or CDN's&lt;br&gt;
  bot settings and let verified agents through to the checkout, instead of&lt;br&gt;
  discovering the block in your failed-orders log.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Letting agents through does not mean letting them run wild. An agent&lt;br&gt;
  that retries a failing checkout can hammer your endpoints in a loop a&lt;br&gt;
  human never would. Put per-IP and per-session rate limits on cart and&lt;br&gt;
  checkout endpoints, and cap the retry behavior on your payment intents.&lt;br&gt;
  This is standard hardening, but agents make it load-bearing: the first&lt;br&gt;
  wave of agent traffic you see will include the buggy kind.&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist, part 2: be discoverable, then protect yourself
&lt;/h2&gt;

&lt;p&gt;Once an agent can finish your checkout, the next question is whether it&lt;br&gt;
can find your products - and what you do when something goes wrong.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;br&gt;
  Price, currency, availability, shipping cost, and the product name - in&lt;br&gt;
  the page itself, as text, not trapped inside images or JavaScript-only&lt;br&gt;
  widgets. Then mark it up: schema.org Product and Offer JSON-LD with the&lt;br&gt;
  same numbers. Google's own line is reassuring here: its AI features need&lt;br&gt;
  no special AI files or markup beyond what search already uses. The&lt;br&gt;
  stores that win agent discovery are the ones whose pages a dumb parser&lt;br&gt;
  can read, not the ones with the prettiest interactive configurator.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Agent discovery in path one runs on product feeds, not on vibes. If you&lt;br&gt;
  are on Shopify, your catalog is already in the channels Shopify&lt;br&gt;
  switches on by default. Otherwise, keep a clean feed in Google Merchant&lt;br&gt;
  Center and keep prices and stock accurate in it - stale feed data is&lt;br&gt;
  how an agent confidently sells a customer something you cannot ship.&lt;br&gt;
  Accuracy in the feed is a checkout fix, not a marketing chore.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Two bots decide whether you appear in AI answers. OpenAI's OAI-SearchBot&lt;br&gt;
  decides whether you show up in ChatGPT's search answers; GPTBot controls&lt;br&gt;
  training data and can be blocked independently. If you blocked all of&lt;br&gt;
  OpenAI's crawlers during the 2024 scraping panic and never revisited the&lt;br&gt;
  decision, you have opted out of ChatGPT product discovery entirely.&lt;br&gt;
  Review robots.txt this week: allow OAI-SearchBot, decide on GPTBot&lt;br&gt;
  separately. Five minutes of work, disproportionate payoff.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Remember the liability section: you are the merchant of record and no&lt;br&gt;
  network has shifted the risk. Log the agent signals your stack already&lt;br&gt;
  sees - signed-agent headers, the checkout session ID, the payment&lt;br&gt;
  token reference, the shipping address the agent submitted. From October&lt;br&gt;
  24, 2026, an agent provider's login ID can count as matching evidence&lt;br&gt;
  under Visa's Compelling Evidence 3.0 rules when you contest a fraud&lt;br&gt;
  dispute. Keep the logs now; the rule is only useful if the data exists.&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
  Theory ends here. Take a shopping agent you can access - Meta's Muse,&lt;br&gt;
  ChatGPT's shopping, Gemini's Task agent - point it at your own store,&lt;br&gt;
  and buy your cheapest product with your own card. Watch where it&lt;br&gt;
  hesitates: the variant picker it cannot parse, the shipping calculator&lt;br&gt;
  that needs a postal code before showing options, the wallet button that&lt;br&gt;
  does not render for the agent's session. Every snag you find is a&lt;br&gt;
  conversion fix for humans too, which is why this test pays for itself&lt;br&gt;
  even if agent traffic stays at 0.1% for another year.&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What to skip for now
&lt;/h2&gt;

&lt;p&gt;A checklist that only says "do" is a sales pitch. Here is the honest&lt;br&gt;
"don't" list, because most of the agentic-commerce noise wants your money&lt;br&gt;
or your weekends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't build an ACP endpoint or an MCP commerce server yet.&lt;/strong&gt; The
protocols are young and still converging - ACP is in beta, UCP's spec is
moving - and implementing one lists your products nowhere. Build it when
a channel you want requires it, not before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't buy an "agent SEO" tool.&lt;/strong&gt; Product discovery runs on the same
structured data and feeds that regular SEO already uses. If a vendor
cannot explain what their tool does beyond what schema.org and Merchant
Center do, they are selling you the press release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't chase in-chat checkout unless you are eligible.&lt;/strong&gt; If you are not
on the approved platforms selling to US buyers, path one is a waitlist,
not a project. Your energy belongs in path two, which is open today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't assume subscriptions will stay excluded.&lt;/strong&gt; The protocols
describe support for digital goods and subscriptions even though the
live checkouts exclude them. Build your checkout so the restriction is a
policy toggle, not an architecture limit.&lt;/li&gt;
&lt;/ul&gt;





- Applying to every agent waitlist the week it launches.
- Building an ACP server nobody's agent will call.
- Buying agent-SEO tools before your schema.org markup exists.
- Assuming the agent's mistake is the agent's bill.





- Guest checkout, a wallet agents use, bot rules that let signed agents through.
- Plain prices plus schema.org Product/Offer on every page.
- Clean product feeds and crawler rules reviewed this week.
- Dispute logs kept, one real agent purchase tested end to end.




&lt;h2&gt;
  
  
  The tools on the table
&lt;/h2&gt;

&lt;p&gt;{/* MONETIZATION NOTE (not rendered): links below are official product pages, NOT affiliate links yet. Swap for affiliate links once approved into a program; never label these as affiliate links until the swap is real. For this topic the primary money path is freelance agentic-commerce integration leads via the /#contact CTA below, plus display ads on the traffic. */}&lt;/p&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;What it does here&lt;/th&gt;
&lt;th&gt;Official link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;Agentic Commerce Suite, Shared Payment Tokens, Link wallet, Machine Payments Protocol&lt;/td&gt;
&lt;td&gt;&lt;a href="https://stripe.com" rel="noopener noreferrer"&gt;https://stripe.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PayPal&lt;/td&gt;
&lt;td&gt;Agentic commerce services, paypal.ai merchant infrastructure, native checkout on agent surfaces&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.paypal.com" rel="noopener noreferrer"&gt;https://www.paypal.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shopify&lt;/td&gt;
&lt;td&gt;Catalog feeds into ChatGPT, Google AI Mode, Copilot Checkout; Agentic plan for non-Shopify brands&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.shopify.com" rel="noopener noreferrer"&gt;https://www.shopify.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wix&lt;/td&gt;
&lt;td&gt;First CMS signatory of ACP; Site MCP, llms.txt, and structured data automatic on Wix stores&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.wix.com" rel="noopener noreferrer"&gt;https://www.wix.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Merchant Center&lt;/td&gt;
&lt;td&gt;Product feed that powers Google's agent discovery and UCP checkout eligibility&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.google.com/retail/solutions/merchant-center/" rel="noopener noreferrer"&gt;https://www.google.com/retail/solutions/merchant-center/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare&lt;/td&gt;
&lt;td&gt;Bot management: verified signed agents since July 1; tune rules so agents reach checkout&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.cloudflare.com" rel="noopener noreferrer"&gt;https://www.cloudflare.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  My verdict: what I would do, and why
&lt;/h2&gt;

&lt;p&gt;If this were my store, I would ignore every waitlist and spend one&lt;br&gt;
afternoon on path two. Guest checkout on, Link and Google Pay enabled,&lt;br&gt;
schema.org markup validated, product feed accurate, robots.txt reviewed,&lt;br&gt;
bot rules tuned to let signed agents through, dispute logging confirmed,&lt;br&gt;
and then one real agent purchase with my own card to find the snags. That&lt;br&gt;
is a day's work, most of it configuration, and it covers every agent that&lt;br&gt;
can reach a normal website today - which is nearly all of them.&lt;/p&gt;

&lt;p&gt;The condition that flips my answer: the day a channel you actually want -&lt;br&gt;
a marketplace, an assistant with your buyers in it - opens merchant&lt;br&gt;
onboarding with real volume behind it, join it that week. Until then, the&lt;br&gt;
in-chat checkout story is somebody else's platform war, and your checkout&lt;br&gt;
page is the territory you already control. An agent that can finish your&lt;br&gt;
checkout is worth more than a protocol badge on a waitlist page. Fix the&lt;br&gt;
finish line first.&lt;/p&gt;
&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Can an AI agent buy from my store right now?
&lt;/h3&gt;

&lt;p&gt;Yes, in most cases - but through your own checkout, not through a chat&lt;br&gt;
window. Assistants like Meta's Muse, Amazon's Buy for Me, OpenAI's dots,&lt;br&gt;
and Gemini's Task agent buy on merchants' own sites, paying through your&lt;br&gt;
existing payment provider. In-chat checkouts inside ChatGPT, Google AI&lt;br&gt;
Mode, and Perplexity are gated to approved platforms and US buyers.&lt;/p&gt;
&lt;h3&gt;
  
  
  What is the Agentic Commerce Protocol (ACP)?
&lt;/h3&gt;

&lt;p&gt;ACP is the open standard co-developed by OpenAI and Stripe (open source,&lt;br&gt;
Apache 2.0) that lets an AI agent talk to a merchant's backend: create a&lt;br&gt;
checkout session, update it, and complete the purchase with a delegated&lt;br&gt;
payment token. It can be implemented as a REST interface or as an MCP&lt;br&gt;
server. Implementing it does not list your products anywhere - each AI&lt;br&gt;
platform manages its own merchant onboarding.&lt;/p&gt;
&lt;h3&gt;
  
  
  What is the Universal Commerce Protocol (UCP)?
&lt;/h3&gt;

&lt;p&gt;UCP is Google's and Shopify's answer to the same problem: an open&lt;br&gt;
standard covering catalog search, cart, checkout, identity linking, and&lt;br&gt;
order tracking, designed so platforms, agents, and businesses&lt;br&gt;
interoperate through one protocol. Stripe joined its governing council in&lt;br&gt;
April 2026. Google's AI Mode and Gemini checkouts run on it for approved&lt;br&gt;
Merchant Center sellers.&lt;/p&gt;
&lt;h3&gt;
  
  
  Do I need to build anything new for agents to buy from my store?
&lt;/h3&gt;

&lt;p&gt;Probably not. Nothing in the agent-on-your-site path requires a protocol&lt;br&gt;
from you: what decides whether an agent finishes the purchase is your&lt;br&gt;
existing checkout. Allow guest checkout, accept a wallet agents use&lt;br&gt;
(Stripe Link, Google Pay), keep prices and availability as plain&lt;br&gt;
structured data, and make sure your bot protection lets verified agents&lt;br&gt;
through.&lt;/p&gt;
&lt;h3&gt;
  
  
  Who handles chargebacks when an AI agent makes a purchase?
&lt;/h3&gt;

&lt;p&gt;You do. Visa's rulebook treats the registered agent provider as the&lt;br&gt;
cardholder and holds the cardholder responsible for the agent's actions;&lt;br&gt;
it forbids merchants from requiring buyers to waive dispute rights.&lt;br&gt;
Stripe's seller terms keep disputes, refunds, and transaction fraud with&lt;br&gt;
the seller. Worldpay's summary: no liability shift exists yet.&lt;/p&gt;
&lt;h3&gt;
  
  
  Can AI agents buy subscriptions or digital products?
&lt;/h3&gt;

&lt;p&gt;Not through the in-chat checkouts - ChatGPT's rules allow physical goods&lt;br&gt;
only, Google excludes subscriptions and most digital goods, and Shopify&lt;br&gt;
excludes subscriptions from Copilot and Google AI Mode checkouts. The&lt;br&gt;
agent-on-your-site path has no such restriction: an agent can buy a&lt;br&gt;
subscription on your own checkout page exactly like a human can.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  The three-paths framing, the in-chat program statuses, the agent&lt;br&gt;
  behaviors (Buy for Me, Muse, dots, Gemini Task agent, Chrome auto&lt;br&gt;
  browse), the subscription exclusions, the liability rules, and all&lt;br&gt;
  traffic and survey figures come from Okane Land's researched review&lt;br&gt;
  "Can an AI agent buy from you yet?" (published October 14, 2026, read&lt;br&gt;
  October 21), which quotes the programs' own pages, the protocol docs,&lt;br&gt;
  and the card networks' rulebooks. The Wix Symphony plus Link wallet&lt;br&gt;
  announcement is from Wix's GlobeNewswire release of October 15, 2026;&lt;br&gt;
  the PayPal plus Glance integration is from their October 15, 2026&lt;br&gt;
  press release as reported by PYMNTS. The three-gate structure of the&lt;br&gt;
  checklist (finishable checkout, discoverability, dispute protection)&lt;br&gt;
  is my own synthesis. Nothing here is based on a test purchase I ran -&lt;br&gt;
  item 9 of the checklist is the test I recommend you run yourself.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want this done on your store instead of reading about it?&lt;/strong&gt; I build&lt;br&gt;
checkout and commerce integrations for a living - &lt;a href="https://dev.to/#contact"&gt;tell me about your&lt;br&gt;
store&lt;/a&gt; and I will tell you honestly whether you need an&lt;br&gt;
afternoon of configuration or something bigger.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If AI-driven discovery is the part that interests you, start with&lt;br&gt;
&lt;a href="https://dev.to/blog/how-to-get-cited-by-chatgpt-perplexity"&gt;how to get cited by ChatGPT and Perplexity&lt;/a&gt;&lt;br&gt;
and my &lt;a href="https://dev.to/blog/best-geo-tools-2026"&gt;GEO tools comparison&lt;/a&gt; - being found is&lt;br&gt;
the gate before being bought.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ecommerce</category>
      <category>webdev</category>
      <category>stripe</category>
    </item>
    <item>
      <title>Vercel vs VPS for Next.js: The $4 Server That Really Costs $104/Month</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Tue, 20 Oct 2026 05:36:30 +0000</pubDate>
      <link>https://dev.to/zforzain2000/vercel-vs-vps-for-nextjs-the-real-monthly-cost-in-2026-431o</link>
      <guid>https://dev.to/zforzain2000/vercel-vs-vps-for-nextjs-the-real-monthly-cost-in-2026-431o</guid>
      <description>&lt;h1&gt;
  
  
  Vercel vs VPS for Next.js: The Real Monthly Cost in 2026
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Vercel Pro is $20/seat plus metered usage; a Hetzner VPS is ~€4/month flat. I ran the bill math on four real workloads to find where the crossover hits.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I keep seeing the same two screenshots in developer group chats. One is a&lt;br&gt;
  Vercel invoice for a side project that went mildly viral. The other is a&lt;br&gt;
  smug "my Hetzner bill this month: €4.35" from someone running the same&lt;br&gt;
  stack on a VPS. Both people think the other one is doing it wrong. I have&lt;br&gt;
  run Next.js apps on both, and the honest answer is that both screenshots&lt;br&gt;
  are real - they are just describing different workloads. Here is the bill&lt;br&gt;
  math, with the four scenarios that actually decide it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Vercel wins when your app is small, your team is one or two people, and you&lt;br&gt;
would rather ship than babysit servers. A VPS wins the moment bandwidth&lt;br&gt;
grows past roughly a terabyte a month or your team needs more seats,&lt;br&gt;
because a €4/month VPS is flat while Vercel meters nearly everything.&lt;br&gt;
Bandwidth is the bill, not compute - and that is the whole decision in one&lt;br&gt;
sentence. Read the scenarios below to find which side of the crossover you&lt;br&gt;
are on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Vercel actually costs in October 2026
&lt;/h2&gt;

&lt;p&gt;Vercel's pricing page has moved from simple tiers to credit-based billing,&lt;br&gt;
so let me lay out what a Pro team actually pays. I pulled these from&lt;br&gt;
Vercel's own pricing page, fetched today.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan:&lt;/strong&gt; Hobby | &lt;strong&gt;Price:&lt;/strong&gt; $0 | &lt;strong&gt;The limits that matter:&lt;/strong&gt; Personal, non-commercial use only. 100GB/month data transfer, 1M function invocations/month, 4 hours of Fluid Active CPU/month, 1 developer seat&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan:&lt;/strong&gt; Pro | &lt;strong&gt;Price:&lt;/strong&gt; $20/user/month | &lt;strong&gt;The limits that matter:&lt;/strong&gt; Commercial use, 1TB bandwidth, 10M Edge requests/month, 1,000 GB-hours of function duration, a $20/month included usage credit, pay-as-you-go after that&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan:&lt;/strong&gt; Enterprise | &lt;strong&gt;Price:&lt;/strong&gt; Custom | &lt;strong&gt;The limits that matter:&lt;/strong&gt; SSO, audit logs, 99.99% SLA, compliance - you do not need this until compliance or procurement says you do&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three mechanics matter more than the table:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credit-based billing.&lt;/strong&gt; Since September 2025, Pro runs on credits. Your&lt;br&gt;
$20 seat includes a $20 usage credit that burns against bandwidth,&lt;br&gt;
function execution, and build minutes before overages start. It is not&lt;br&gt;
"free money" - it is just the included allowance wearing a different&lt;br&gt;
costume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bandwidth is the meter that breaks bills.&lt;/strong&gt; Independent 2026 pricing&lt;br&gt;
guides put usage-based bandwidth overage at roughly $0.15/GB once you pass&lt;br&gt;
the included allowance. That sounds small until a 500GB month costs you an&lt;br&gt;
extra $75 on top of seats. Vercel knows this too, which is why they offer&lt;br&gt;
Flat Rate CDN: a fixed monthly price for CDN requests and transfer on Pro,&lt;br&gt;
with no overages and built-in spike protection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spend guardrails exist - use them.&lt;/strong&gt; New teams get a default $200&lt;br&gt;
on-demand usage budget with email and SMS notifications, and you can set a&lt;br&gt;
hard limit that pauses projects at 100%. Turn this on before your first&lt;br&gt;
viral day, not after. Vercel has also added recursion protection, improved&lt;br&gt;
function defaults, and an Attack Challenge Mode over the past year -&lt;br&gt;
all aimed at runaway bills, including bots and DDoS traffic that you pay&lt;br&gt;
for.&lt;/p&gt;

&lt;p&gt;The honest summary: one seat on Pro with moderate traffic is genuinely&lt;br&gt;
$20-40/month. The horror stories start when seats multiply and bandwidth&lt;br&gt;
compounds, because both meters run every single month with no ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a VPS actually costs
&lt;/h2&gt;

&lt;p&gt;A VPS is the opposite deal: flat hardware, metered only by your own&lt;br&gt;
neglect. Current 2026 prices from independent developer comparisons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provider:&lt;/strong&gt; Hetzner CX22 | &lt;strong&gt;Entry that fits Next.js:&lt;/strong&gt; 2 vCPU, 4GB RAM, 40GB NVMe | &lt;strong&gt;Monthly price:&lt;/strong&gt; ~€4/month | &lt;strong&gt;Bandwidth:&lt;/strong&gt; 20TB included&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider:&lt;/strong&gt; DigitalOcean Basic | &lt;strong&gt;Entry that fits Next.js:&lt;/strong&gt; 1 vCPU, 2GB, 50GB SSD | &lt;strong&gt;Monthly price:&lt;/strong&gt; $12/month | &lt;strong&gt;Bandwidth:&lt;/strong&gt; 4TB included&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider:&lt;/strong&gt; DigitalOcean 4GB | &lt;strong&gt;Entry that fits Next.js:&lt;/strong&gt; 2 vCPU, 4GB, 80GB SSD | &lt;strong&gt;Monthly price:&lt;/strong&gt; $24/month | &lt;strong&gt;Bandwidth:&lt;/strong&gt; 4TB included&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider:&lt;/strong&gt; Vultr | &lt;strong&gt;Entry that fits Next.js:&lt;/strong&gt; 1 vCPU, 2GB (High Frequency) | &lt;strong&gt;Monthly price:&lt;/strong&gt; ~$12/month | &lt;strong&gt;Bandwidth:&lt;/strong&gt; generous allowance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider:&lt;/strong&gt; OVHcloud | &lt;strong&gt;Entry that fits Next.js:&lt;/strong&gt; 2 vCPU, 2GB | &lt;strong&gt;Monthly price:&lt;/strong&gt; ~$4.20/month | &lt;strong&gt;Bandwidth:&lt;/strong&gt; unmetered on most plans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Hetzner line is the one that ends arguments: 2 vCPUs and 4GB of RAM&lt;br&gt;
for about the price of a coffee, with 20TB of traffic. One developer&lt;br&gt;
benchmarked the CX22 against DigitalOcean's $24 Premium tier in 2026 and&lt;br&gt;
found Hetzner within a few percent on throughput - the 5-6x price gap is&lt;br&gt;
real, not a trick.&lt;/p&gt;

&lt;p&gt;But the price table hides the real cost of a VPS, which is you. Budget&lt;br&gt;
honestly: first setup with Docker and Coolify is 1-3 hours if you have&lt;br&gt;
done it before, longer if not. Monthly upkeep - updates, certificate&lt;br&gt;
renewals, backups, the occasional 2 AM "why is the site down" - is 30 to&lt;br&gt;
60 minutes in a normal month. At a $30-50/hour freelance rate, that is&lt;br&gt;
$15-50/month of your time. The VPS is still cheaper for almost every&lt;br&gt;
workload below enterprise scale, but it is not €4/month for everyone. It&lt;br&gt;
is €4 plus your ops labor.&lt;/p&gt;

&lt;p&gt;What closes most of the gap: &lt;strong&gt;Coolify&lt;/strong&gt; (free, open source). It gives you&lt;br&gt;
git-push deploys, automatic TLS, Postgres and Redis one-click services,&lt;br&gt;
and monitoring on your own VPS - roughly the Vercel developer experience&lt;br&gt;
for the price of the server. Dokku does the same for less money in&lt;br&gt;
complexity, more in manual config. I would not self-host a production&lt;br&gt;
client site in 2026 without one of these two.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one meter that decides your bill
&lt;/h2&gt;

&lt;p&gt;Every "Vercel is expensive" argument and every "VPS is cheap" argument is&lt;br&gt;
really an argument about bandwidth. Compute is nearly free everywhere in&lt;br&gt;
2026 - what differs is how the two models charge for moving bytes to&lt;br&gt;
users. Three numbers make the point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20TB&lt;/strong&gt; bandwidth included on a €4 Hetzner CX22 (Hetzner Cloud pricing, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1TB&lt;/strong&gt; bandwidth included on Vercel Pro (Per 2026 pricing guides; usage-based overage beyond it)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6x&lt;/strong&gt; price gap at 4GB RAM: DigitalOcean $24 vs Hetzner ~€4 (Independent dev.to VPS comparisons, 2026)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My rule, after running apps on both sides: &lt;strong&gt;if your app serves under&lt;br&gt;
~1TB/month and your team is one or two people, Vercel's price is&lt;br&gt;
reasonable rent for zero-ops. Past 1TB, or with three or more seats, do&lt;br&gt;
the VPS math seriously - the gap stops being a rounding error.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why seats matter so much: Vercel charges $20 per user per month whether&lt;br&gt;
they deploy daily or opened the dashboard twice. A three-person team pays&lt;br&gt;
$60/month before a single byte is served. The VPS does not know how many&lt;br&gt;
people are on your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bill math: four real scenarios
&lt;/h2&gt;

&lt;p&gt;All figures USD/month before tax. Bandwidth overage on Vercel's&lt;br&gt;
usage-based tier is roughly $0.15/GB per 2026 third-party pricing guides -&lt;br&gt;
treat it as a planning estimate, not a contract term.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 1: a side project with no revenue.&lt;/strong&gt; Vercel Hobby is $0 and&lt;br&gt;
perfectly legal - the non-commercial clause does not apply. A VPS costs&lt;br&gt;
you $4-12/month. Vercel wins outright: zero cost, zero ops, preview URLs&lt;br&gt;
for free. The only reason to use a VPS here is if you want the practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 2: a solo commercial SaaS, ~400GB/month, one seat.&lt;/strong&gt;&lt;br&gt;
Vercel Pro: $20/month, inside the included allowance. Hetzner CX22: ~€4&lt;br&gt;
(~$4.35). Vercel costs 4-5x more in raw dollars - but you bought zero&lt;br&gt;
server maintenance, instant rollbacks, and preview deployments for client&lt;br&gt;
demos. This is the scenario where the answer is genuinely "it depends on&lt;br&gt;
how you value your weekends." I pick Vercel here when the app is the&lt;br&gt;
product and client trust matters; I pick the VPS when it is my own&lt;br&gt;
experiment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 3: a 3-person team, 1.5TB/month.&lt;/strong&gt; Vercel: 3 x $20 seats = $60,&lt;br&gt;
plus ~500GB over the allowance at ~$0.15/GB = ~$75. Total: ~$135/month.&lt;br&gt;
VPS: €4-8/month, same app. The gap is roughly 15-25x. This is the&lt;br&gt;
crossover in the wild - seats times bandwidth is the compounding the&lt;br&gt;
marketing pages never show you. At this scale I self-host with Coolify and&lt;br&gt;
spend the saved $1,500/year on literally anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario 4: the viral month - 5TB in one spike.&lt;/strong&gt; Vercel usage-based:&lt;br&gt;
$60 in seats plus ~4TB of overage, which at ~$0.15/GB is on the order of&lt;br&gt;
$600 extra. That is an estimate, but the shape is right: the spike costs&lt;br&gt;
more than the year of hosting it would have replaced. This is exactly what&lt;br&gt;
Flat Rate CDN exists for - one fixed monthly price, no overages, spike&lt;br&gt;
protection built in. If your traffic is spiky or you have ever been on the&lt;br&gt;
front page of Hacker News, price Flat Rate CDN before you commit to&lt;br&gt;
usage-based. The VPS, for the record, costs €4 in the viral month too -&lt;br&gt;
though you should make sure your single box survives the spike, which is&lt;br&gt;
an ops question, not a billing one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scenario:&lt;/strong&gt; Non-commercial side project | &lt;strong&gt;Vercel Pro:&lt;/strong&gt; $0 (Hobby) | &lt;strong&gt;Hetzner VPS + Coolify:&lt;/strong&gt; ~$6 | &lt;strong&gt;Winner:&lt;/strong&gt; Vercel - free is free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenario:&lt;/strong&gt; Solo commercial SaaS, 400GB/mo | &lt;strong&gt;Vercel Pro:&lt;/strong&gt; ~$20 | &lt;strong&gt;Hetzner VPS + Coolify:&lt;/strong&gt; ~$4 + your ops time | &lt;strong&gt;Winner:&lt;/strong&gt; Depends on your hourly rate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenario:&lt;/strong&gt; 3-seat team, 1.5TB/mo | &lt;strong&gt;Vercel Pro:&lt;/strong&gt; ~$135 | &lt;strong&gt;Hetzner VPS + Coolify:&lt;/strong&gt; ~$4-8 | &lt;strong&gt;Winner:&lt;/strong&gt; VPS by ~15x&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenario:&lt;/strong&gt; Viral spike month, 5TB | &lt;strong&gt;Vercel Pro:&lt;/strong&gt; ~$660 est. (or Flat Rate CDN) | &lt;strong&gt;Hetzner VPS + Coolify:&lt;/strong&gt; ~$4 | &lt;strong&gt;Winner:&lt;/strong&gt; VPS, unless Flat Rate CDN&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The meter nobody prices: your ops time
&lt;/h2&gt;

&lt;p&gt;The VPS crowd quotes €4/month and stops. That is dishonest in the other&lt;br&gt;
direction. Here is what I actually spend on a Coolify-managed VPS per&lt;br&gt;
month: roughly 30-60 minutes of updates and the occasional alert, plus&lt;br&gt;
amortized setup time. When I &lt;a href="https://dev.to/blog/how-to-price-nextjs-project-2026"&gt;price a Next.js project for a client&lt;/a&gt;,&lt;br&gt;
I now line-item hosting with my maintenance rate attached, because the&lt;br&gt;
client pays for my time either way.&lt;/p&gt;

&lt;p&gt;Do the honest version of this math for yourself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your effective hourly rate (what an hour of your time is worth - use
your freelance rate, not your salary).&lt;/li&gt;
&lt;li&gt;VPS server cost + (estimated monthly ops hours x your rate).&lt;/li&gt;
&lt;li&gt;Compare against Vercel Pro: seats x $20 + estimated usage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For most solo developers, the honest VPS number lands around $40-80/month&lt;br&gt;
at real freelance rates - still half or less of a multi-seat Vercel bill&lt;br&gt;
at scale, but not the €4 meme number. Vercel's real product was never&lt;br&gt;
hosting. It is buying back your hours. Price the hours, not the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools on the table
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool:&lt;/strong&gt; Vercel Pro | &lt;strong&gt;What it is:&lt;/strong&gt; Zero-ops Next.js hosting: git-push deploys, edge network, preview URLs, instant rollbacks | &lt;strong&gt;Sticker price:&lt;/strong&gt; $20/user/month + metered usage | &lt;strong&gt;Official page:&lt;/strong&gt; vercel.com/pricing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool:&lt;/strong&gt; Hetzner Cloud | &lt;strong&gt;What it is:&lt;/strong&gt; The price-performance VPS: CX22 at ~€4/month with 20TB traffic | &lt;strong&gt;Sticker price:&lt;/strong&gt; ~€4-16/month | &lt;strong&gt;Official page:&lt;/strong&gt; hetzner.com/cloud&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool:&lt;/strong&gt; DigitalOcean | &lt;strong&gt;What it is:&lt;/strong&gt; The beginner-friendly VPS: 1-click marketplace, managed databases, $200/60-day trial credits | &lt;strong&gt;Sticker price:&lt;/strong&gt; $6-24/month for Basic droplets | &lt;strong&gt;Official page:&lt;/strong&gt; digitalocean.com&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool:&lt;/strong&gt; Vultr | &lt;strong&gt;What it is:&lt;/strong&gt; High-frequency VPS with 32+ global locations | &lt;strong&gt;Sticker price:&lt;/strong&gt; from ~$6/month | &lt;strong&gt;Official page:&lt;/strong&gt; vultr.com&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool:&lt;/strong&gt; Hostinger | &lt;strong&gt;What it is:&lt;/strong&gt; Budget VPS with a managed panel for non-ops people | &lt;strong&gt;Sticker price:&lt;/strong&gt; from ~$4-5/month | &lt;strong&gt;Official page:&lt;/strong&gt; hostinger.com&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool:&lt;/strong&gt; Coolify | &lt;strong&gt;What it is:&lt;/strong&gt; Free, open-source PaaS on your own VPS: git-push deploys, auto TLS, one-click Postgres/Redis | &lt;strong&gt;Sticker price:&lt;/strong&gt; $0 (self-hosted) | &lt;strong&gt;Official page:&lt;/strong&gt; coolify.io&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same cost-analysis discipline I apply to &lt;a href="https://dev.to/blog/how-to-price-nextjs-project-2026"&gt;client infrastructure&lt;br&gt;
decisions&lt;/a&gt; - hosting is a line&lt;br&gt;
item with a maintenance tail, not a sticker price.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-minute checklist before you decide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check your commercial status
&lt;/h3&gt;

&lt;p&gt;If the project makes money - or will within the year - Vercel Hobby is&lt;br&gt;
  off the table. That rule alone decides the answer for every freelancer&lt;br&gt;
  with client work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pull your last 3 months of bandwidth
&lt;/h3&gt;

&lt;p&gt;Vercel dashboard, Cloudflare, or your current host. Under ~1TB/month&lt;br&gt;
  with 1-2 people: Vercel Pro is fairly priced. Over ~1TB or 3+ people:&lt;br&gt;
  the VPS math deserves a spreadsheet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Count your seats, not your developers
&lt;/h3&gt;

&lt;p&gt;Vercel bills per user on the team, not per deployer. Multiply seats x&lt;br&gt;
  $20 and add it to the bandwidth estimate before comparing anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Price your ops time honestly
&lt;/h3&gt;

&lt;p&gt;VPS cost + (monthly ops hours x your hourly rate). If you have never&lt;br&gt;
  run a server before, budget 3-5 hours for the first month - the learning&lt;br&gt;
  is valuable, but it is not free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set the guardrails before traffic arrives
&lt;/h3&gt;

&lt;p&gt;On Vercel: turn on the $200 spend budget, enable hard limits, and price&lt;br&gt;
  Flat Rate CDN if your traffic spikes. On a VPS: set up backups and&lt;br&gt;
  uptime alerts on day one, not after the first outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still unclear
&lt;/h2&gt;

&lt;p&gt;Honest gaps as of October 20, 2026: Vercel's exact usage-based bandwidth&lt;br&gt;
overage rate varies by region and plan configuration, so the ~$0.15/GB&lt;br&gt;
figure from third-party guides is a planning estimate - confirm your&lt;br&gt;
actual rate in the Vercel dashboard before signing anything. Flat Rate&lt;br&gt;
CDN's exact fixed price is in Vercel's docs, not in this post, because it&lt;br&gt;
can change. Hetzner's headline prices exclude VAT, and their infamous&lt;br&gt;
account-verification process can reject legitimate signups, especially&lt;br&gt;
outside Europe - budget a backup provider. And the biggest unknown is&lt;br&gt;
yours: traffic forecasts are guesses. Pick the platform whose failure mode&lt;br&gt;
you can afford - a surprise Vercel invoice, or a 2 AM server alert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Vercel free for commercial Next.js apps?&lt;/strong&gt;&lt;br&gt;
No. Vercel's Hobby plan is free but limited to personal, non-commercial&lt;br&gt;
use - using it for a revenue-generating project violates their terms. Any&lt;br&gt;
commercial project needs Pro at $20 per user per month plus metered usage&lt;br&gt;
on top. That single rule kills the "Vercel is free" argument for anyone&lt;br&gt;
shipping client work or a paid product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does Vercel Pro really cost per month?&lt;/strong&gt;&lt;br&gt;
The sticker is $20 per seat per month, but the real bill is seats plus&lt;br&gt;
metered usage. Pro runs on credit-based billing: a $20 included credit&lt;br&gt;
covers usage, then you pay for bandwidth, function execution, and build&lt;br&gt;
minutes beyond the allowance. One seat with moderate traffic lands around&lt;br&gt;
$20-40; three seats with 1.5TB of bandwidth can land around $135. Set the&lt;br&gt;
$200 default spend budget and hard limits in the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is a $4-6 VPS enough to run a Next.js production app?&lt;/strong&gt;&lt;br&gt;
Yes, for most apps. A Hetzner CX22 (2 vCPU, 4GB RAM, 20TB traffic) at&lt;br&gt;
roughly €4/month runs a Next.js app, Postgres, and a reverse proxy with&lt;br&gt;
headroom. The real requirement is not hardware - it is your willingness&lt;br&gt;
to do the ops: Docker, TLS certificates, backups, and updates. Coolify&lt;br&gt;
turns most of that into a one-time setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the cheapest way to host Next.js in 2026?&lt;/strong&gt;&lt;br&gt;
For a non-commercial project: Vercel Hobby at $0, full stop. For a&lt;br&gt;
commercial project with real traffic: a €4/month VPS is the cheapest raw&lt;br&gt;
compute, but add your ops time at your real hourly rate before declaring&lt;br&gt;
victory. If your rate is $50/hour and the VPS costs you two hours a&lt;br&gt;
month, the VPS "costs" $104/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When does Vercel become more expensive than a VPS?&lt;/strong&gt;&lt;br&gt;
Three triggers, in order: extra seats ($20 each, every month, whether&lt;br&gt;
they deploy or not), bandwidth past the included allowance (metered per&lt;br&gt;
GB, and 2026 guides put usage-based overage around $0.15/GB), and heavy&lt;br&gt;
build pipelines. The crossover for a solo dev usually sits around&lt;br&gt;
1TB/month of bandwidth or two-plus seats. Past that, the VPS math is not&lt;br&gt;
close.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Vercel Flat Rate CDN?&lt;/strong&gt;&lt;br&gt;
An alternative billing option on the Pro plan: instead of usage-based CDN&lt;br&gt;
charges, you pay a fixed monthly amount for CDN requests and data&lt;br&gt;
transfer regardless of traffic. It is Vercel's answer to viral-spike&lt;br&gt;
bills - predictable cost, no overages. If your traffic is spiky or you&lt;br&gt;
cannot tolerate surprise invoices, price this before you commit to&lt;br&gt;
usage-based.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Attribution and honesty&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vercel's plan structure, Hobby limits, Pro's $20/seat price and $20&lt;br&gt;
  included usage credit, and the $200 default spend budget with hard limits&lt;br&gt;
  are from Vercel's own pricing page, fetched October 20, 2026. The&lt;br&gt;
  ~$0.15/GB bandwidth-overage figure and the 1TB Pro allowance come from&lt;br&gt;
  independent 2026 pricing guides (temps.sh, toolradar, northflank, pagepro,&lt;br&gt;
  deploywise) - treat them as planning estimates and confirm your actual&lt;br&gt;
  rate in the Vercel dashboard. VPS prices and the Hetzner-vs-DigitalOcean&lt;br&gt;
  benchmarks are from independent developer comparisons published on dev.to&lt;br&gt;
  in 2026. Scenario totals are my own bill math, labeled as estimates&lt;br&gt;
  where the underlying rates are themselves estimates. Hetzner prices&lt;br&gt;
  exclude VAT.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you are a founder or freelancer staring at a hosting bill that stopped&lt;br&gt;
making sense, this is exactly the kind of infrastructure audit I do for&lt;br&gt;
clients - &lt;a href="https://dev.to/#contact"&gt;get in touch&lt;/a&gt; and bring your last invoice. One bad&lt;br&gt;
month of bandwidth overages pays for the audit.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zainvibes.com/blog/vercel-vs-vps-nextjs-hosting-cost-2026" rel="noopener noreferrer"&gt;ZainVibes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Building with AI agents, or need a full-stack developer who ships? &lt;a href="https://zainvibes.com/about" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>devops</category>
      <category>hosting</category>
    </item>
    <item>
      <title>GitHub Copilot Retires 3 Models Nov 16: Your Migration Checklist</title>
      <dc:creator>MUHAMMAD ZAIN UL ABIDIN</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:55:33 +0000</pubDate>
      <link>https://dev.to/zforzain2000/github-copilot-retires-3-models-nov-16-your-migration-checklist-2ml6</link>
      <guid>https://dev.to/zforzain2000/github-copilot-retires-3-models-nov-16-your-migration-checklist-2ml6</guid>
      <description>&lt;p&gt;&lt;br&gt;
  GitHub published a changelog entry on October 15 with a date and three&lt;br&gt;
  model names: on November 16, 2026, Claude Opus 4.8, Claude Opus 5, and&lt;br&gt;
  GPT-5.6 Terra disappear from every GitHub Copilot experience. Not just&lt;br&gt;
  chat - inline edits, ask and agent modes, code completions, all of it.&lt;br&gt;
  I read the entry twice, then checked what it does not say, because the&lt;br&gt;
  sentence that worries me is the one most people will skip: "Please&lt;br&gt;
  update your workflows and integrations to use the supported models&lt;br&gt;
  before this date." This post is what that sentence actually asks you to&lt;br&gt;
  do, step by step, before the clock runs out.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;On November 16, 2026, GitHub removes Claude Opus 4.8, Claude Opus 5, and&lt;br&gt;
GPT-5.6 Terra from every Copilot surface. The suggested swaps are Claude&lt;br&gt;
Opus 5.5 (for both Opus models) and GPT-6.1 Sol or GPT-6 Luna (for Terra).&lt;br&gt;
Only individual annual subscribers keep Opus 4.8; everyone else loses all&lt;br&gt;
three. Audit anything that names these models by ID this week - prompts,&lt;br&gt;
scripts, agents, CI jobs, IDE settings - and confirm the replacements are&lt;br&gt;
enabled in your org's model policies. GitHub removes the old models&lt;br&gt;
itself; your job is everything that points at them.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is actually retiring
&lt;/h2&gt;

&lt;p&gt;GitHub's changelog table is unambiguous, so I am reproducing it exactly&lt;br&gt;
rather than paraphrasing it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Deprecation date&lt;/th&gt;
&lt;th&gt;Suggested alternative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.8*&lt;/td&gt;
&lt;td&gt;2026-11-16&lt;/td&gt;
&lt;td&gt;Claude Opus 5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;2026-11-16&lt;/td&gt;
&lt;td&gt;GPT-6.1 Sol or GPT-6 Luna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.6 Terra&lt;/td&gt;
&lt;td&gt;2026-11-16&lt;/td&gt;
&lt;td&gt;GPT-6.1 Sol or GPT-6 Luna&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* Claude Opus 4.8 stays available to individual GitHub Copilot&lt;br&gt;
subscribers on annual plans. That is the only exception. Everything else&lt;br&gt;
in this section applies to everyone.&lt;/p&gt;

&lt;p&gt;Two details in the changelog wording matter more than the table itself.&lt;br&gt;
First, the scope: "across all GitHub Copilot experiences (including&lt;br&gt;
Copilot Chat, inline edits, ask and agent modes, and code completions)."&lt;br&gt;
If your mental model is "the model picker in chat," widen it now. The&lt;br&gt;
model you pinned for inline edits in VS Code settings, the one your&lt;br&gt;
Copilot CLI script passes with a flag, the one your custom agent&lt;br&gt;
definition names - all of them are in scope until you prove otherwise.&lt;/p&gt;

&lt;p&gt;Second, this is the third wave of a steady pattern, not a one-off. GitHub&lt;br&gt;
retired earlier model generations through 2026 on the same cadence. The&lt;br&gt;
takeaway is not that Opus 5 was bad. The takeaway is that any model name&lt;br&gt;
you hardcode into your workflow has a shelf life measured in months, and&lt;br&gt;
the changelog is the only advance notice you get.&lt;/p&gt;



&lt;h2&gt;
  
  
  The exception that will confuse everyone
&lt;/h2&gt;

&lt;p&gt;The Opus 4.8 carve-out is narrow, and narrow exceptions are where teams&lt;br&gt;
make their most expensive wrong assumptions. Read it carefully: Claude&lt;br&gt;
Opus 4.8 "will remain available to individual GitHub Copilot subscribers&lt;br&gt;
on annual plans so they retain an Opus offering."&lt;/p&gt;

&lt;p&gt;That sentence leaves three groups out in the cold. If you pay monthly as&lt;br&gt;
an individual, you lose Opus 4.8. If your seat comes through Copilot&lt;br&gt;
Business or Copilot Enterprise, you lose Opus 4.8 - organization-managed&lt;br&gt;
seats are explicitly not covered. And nobody keeps Opus 5 or GPT-5.6&lt;br&gt;
Terra on any plan.&lt;/p&gt;

&lt;p&gt;Here is the judgment call I would make if I qualified for the exception:&lt;br&gt;
I would still migrate. Running agent workflows on Opus 4.8 while the&lt;br&gt;
rest of your team runs Opus 5.5 splits you across two model generations,&lt;br&gt;
and the exception is a retention gesture, not a commitment. GitHub framed&lt;br&gt;
it as "so they retain an Opus offering" - Opus 5.5 is the offering going&lt;br&gt;
forward. The exception keeps you comfortable today and stranded later.&lt;br&gt;
Use the remaining weeks to move to 5.5 and test it, same as everyone&lt;br&gt;
else.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  GitHub does not specify what happens to a request that names a retired&lt;br&gt;
  model after November 16. It may fail outright, or it may get silently&lt;br&gt;
  rerouted to whatever default GitHub assigns. I am not going to pick one&lt;br&gt;
  of those for you as fact, because the changelog does not. The safe&lt;br&gt;
  engineering assumption is that a pinned reference is broken by default,&lt;br&gt;
  and that is how the checklist below treats it.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration checklist
&lt;/h2&gt;

&lt;p&gt;This is the part the changelog's one sentence - "update your workflows&lt;br&gt;
and integrations" - actually requires. I have ordered it the way I would&lt;br&gt;
run it: the audit first, the admin check second, the judgment calls&lt;br&gt;
third.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  &lt;br&gt;
    The three retiring names are Claude Opus 4.8, Claude Opus 5, and&lt;br&gt;
    GPT-5.6 Terra. The replacements are Claude Opus 5.5, GPT-6.1 Sol, and&lt;br&gt;
    GPT-6 Luna. Commit that mapping now, because the rest of the work is&lt;br&gt;
    finding every place the old names live. A checklist built on the wrong&lt;br&gt;
    scope is worse than no checklist - it gives you confidence you have&lt;br&gt;
    not earned.&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
    Chat pickers are the easy part. The failures show up in unattended&lt;br&gt;
    places: prompt files, CI workflows, Copilot CLI scripts, custom agent&lt;br&gt;
    definitions, workspace settings that store a default model, and the&lt;br&gt;
    team runbooks that tell people which model to select. Run a search&lt;br&gt;
    across your repos before the cutoff. Two searches cover the common&lt;br&gt;
    spellings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;```
git grep -niE 'opus[ -]4[.-]8|5\.6[ -]terra'
git grep -niE 'opus[ -]5([^.0-9-]|$)'
```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The second pattern matches `opus-5` and `Opus 5` while skipping
`opus-5-5` and `Opus 5.5`. Adjust for how your tooling spells model
IDs - display names and machine identifiers often differ. Treat every
hit as a bug until it points at Opus 5.5, GPT-6.1 Sol, or GPT-6 Luna.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
  &lt;br&gt;
    This is the step that silently breaks teams even when every&lt;br&gt;
    individual does everything right. Under default model enablement, the&lt;br&gt;
    replacements are automatically enabled for Copilot Business and&lt;br&gt;
    Enterprise customers - unless an administrator turned off the global&lt;br&gt;
    default or explicitly disabled the model. If your org did either,&lt;br&gt;
    the new models stay off until someone enables them through the model&lt;br&gt;
    policies in Copilot settings. Open your enterprise or org model&lt;br&gt;
    configuration, find Opus 5.5, GPT-6.1 Sol, and GPT-6 Luna, and&lt;br&gt;
    confirm a non-admin user can actually select them in chat. A&lt;br&gt;
    replacement that exists but is not selectable is the same as no&lt;br&gt;
    replacement.&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
    Prompts tuned against Opus 5's output will not necessarily behave the&lt;br&gt;
    same on Opus 5.5, and prompts written for Terra will not behave the&lt;br&gt;
    same on Sol or Luna. Until November 16 you can still run the old&lt;br&gt;
    model and the new one side by side inside the tool your team already&lt;br&gt;
    uses. That is the window. Pick the three or four prompts your team&lt;br&gt;
    runs most, run each against both models, and diff the results. Yes,&lt;br&gt;
    you pay for both runs. The cost of two eval runs is trivial next to&lt;br&gt;
    the cost of discovering a behavior change in production on November&lt;br&gt;
    17.&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
    GitHub gave Terra users two replacements in different categories,&lt;br&gt;
    and the cheaper one will become your default by habit if you let it.&lt;br&gt;
    Do not accept that silently. Run both GPT-6.1 Sol and GPT-6 Luna&lt;br&gt;
    against a few of your real tasks this week and compare quality, not&lt;br&gt;
    just price. The price-per-token math only matters after a model&lt;br&gt;
    clears your quality bar. I would rather pay Sol's rate for a model&lt;br&gt;
    that passes my checks than save money on Luna output I have to&lt;br&gt;
    regenerate.&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
    The grep sweep finds machine references. It does not find the&lt;br&gt;
    onboarding doc that says "select Opus 5 for the hard refactors," the&lt;br&gt;
    wiki page with last quarter's model recommendations, or the Slack&lt;br&gt;
    message pinned in your team channel. Search those too. A runbook that&lt;br&gt;
    names a dead model trains every new hire to configure a broken&lt;br&gt;
    default.&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
    GitHub is explicit: "No action is required to remove the models&lt;br&gt;
    after they have been deprecated." It removes them. Resist the urge&lt;br&gt;
    to build a removal ritual or a post-cutoff cleanup ticket. The work&lt;br&gt;
    is all on the replacement side - enabled, selectable, evaluated,&lt;br&gt;
    documented. Once that is done, the cutoff is a nonevent for you,&lt;br&gt;
    which is the whole point.&lt;br&gt;
  &lt;br&gt;
&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;Removing the retired models from the picker after Nov 16&lt;/li&gt;
&lt;li&gt;Auto-enabling the replacements under default model enablement&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keeping Opus 4.8 for individual annual subscribers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finding every pinned model name in repos, scripts, agents, CI, and docs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enabling the replacements when your org turned off the global default&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Re-running evals on the replacements before the window closes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choosing between Sol and Luna deliberately&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  These models are not dying - Copilot is just closing a door
&lt;/h2&gt;

&lt;p&gt;Here is the part of the story most coverage gets wrong by omission. None&lt;br&gt;
of these models is being retired by the company that makes it. Anthropic's&lt;br&gt;
own model schedule keeps Claude Opus 5 available on its platforms until&lt;br&gt;
at least July 24, 2027, and Claude Opus 4.8 until at least May 28, 2027 -&lt;br&gt;
both well past the November 16 Copilot cutoff. After the date, you can&lt;br&gt;
still call them through the vendors' own APIs.&lt;/p&gt;

&lt;p&gt;That distinction is worth more than trivia. It means November 16 is a&lt;br&gt;
routing event, not a capability event. If you have workflows that genuinely&lt;br&gt;
need Opus 5's exact behavior and Copilot is your only route to it, the&lt;br&gt;
migration question is not "which Copilot model" but "which route" -&lt;br&gt;
Anthropic's API, a different IDE integration, or a gateway. I covered the&lt;br&gt;
cost math of routing work between models in my &lt;a href="https://dev.to/blog/claude-haiku-5-5-claude-code-cost"&gt;Haiku 5.5 cost&lt;br&gt;
post&lt;/a&gt;, and the same logic applies&lt;br&gt;
here: know what each route costs per task before you commit to it.&lt;/p&gt;

&lt;p&gt;And if you are rethinking which AI coding setup deserves your money after&lt;br&gt;
all this churn, I compared the current pricing landscape across the major&lt;br&gt;
tools in my &lt;a href="https://dev.to/blog/x-subscription-vs-cursor-pro-cost"&gt;X vs Cursor cost breakdown&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a retirement wave is a good moment to check whether your whole stack
still earns its seat.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The lesson I am taking from this
&lt;/h2&gt;

&lt;p&gt;I am going to name the framework because it is the decision rule I will&lt;br&gt;
actually use from now on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Dependency Rule:&lt;/strong&gt; a pinned model name is a dependency with a&lt;br&gt;
lifecycle. Inventory it like one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you pin &lt;code&gt;some-model-5&lt;/code&gt; in a config, you have done exactly what you&lt;br&gt;
do when you pin &lt;code&gt;some-library@5&lt;/code&gt; in package.json: you have taken a&lt;br&gt;
dependency on a specific version of someone else's software, with a&lt;br&gt;
deprecation date you do not control and a migration path you did not&lt;br&gt;
write. The healthy version of this is the healthy version of dependency&lt;br&gt;
management. Keep a short inventory: which model IDs each workflow uses,&lt;br&gt;
which config selects them, who owns the lifecycle notices, and what&lt;br&gt;
behavior must survive a swap. Record the version where the provider&lt;br&gt;
exposes it - a friendly display name may not describe the exact&lt;br&gt;
configuration you tested.&lt;/p&gt;

&lt;p&gt;The unhealthy version is what most teams have today: model names&lt;br&gt;
sprinkled through dotfiles, prompt files, and chat settings, owned by&lt;br&gt;
nobody, discovered only when the picker entry vanishes. This wave cost&lt;br&gt;
those teams a scramble. The next wave - and there will be a next wave,&lt;br&gt;
the cadence through 2026 makes that plain - will cost them the same&lt;br&gt;
scramble unless the inventory gets built now, while the memory of this&lt;br&gt;
one is fresh.&lt;/p&gt;

&lt;p&gt;Sixty days is a short life for a flagship model. Shorter than most&lt;br&gt;
procurement cycles, shorter than a careful evaluation, shorter than the&lt;br&gt;
comparison post that takes six weeks to write. The measurement is the&lt;br&gt;
only thing that compounds. Build the inventory, run the evals, and stop&lt;br&gt;
treating a model picker like a permanent fixture.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tools for the migration
&lt;/h2&gt;

&lt;p&gt;{/* MONETIZATION NOTE (not rendered): the links below are official product&lt;br&gt;
    and documentation pages. If GitHub, Anthropic, or an AI tooling vendor&lt;br&gt;
    opens an affiliate program, swap in affiliate links here and update the&lt;br&gt;
    disclosure. Never claim affiliate status before it exists. */}&lt;/p&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;What it does&lt;/th&gt;
&lt;th&gt;Why it matters here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.blog/changelog/2026-10-15-upcoming-deprecation-of-selected-github-copilot-models-in-mid-november/" rel="noopener noreferrer"&gt;GitHub Changelog&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The official Nov 16 deprecation notice&lt;/td&gt;
&lt;td&gt;The primary source for the table, the exception, and the enablement rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://docs.github.com/copilot" rel="noopener noreferrer"&gt;GitHub Copilot docs&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Supported models and model policies&lt;/td&gt;
&lt;td&gt;Where admins enable the replacement models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.anthropic.com" rel="noopener noreferrer"&gt;Anthropic API&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Direct API access to Claude models&lt;/td&gt;
&lt;td&gt;Opus 5 and 4.8 stay available here until mid-2027 at the earliest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
  The links above are plain official links, not affiliate links. If that&lt;br&gt;
  changes I will say so on the page, same policy as every post here.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which GitHub Copilot models retire on November 16, 2026?
&lt;/h3&gt;

&lt;p&gt;Three: Claude Opus 4.8, Claude Opus 5, and GPT-5.6 Terra. GitHub's October&lt;br&gt;
15, 2026 changelog entry says the deprecation covers every Copilot&lt;br&gt;
experience - Chat, inline edits, ask and agent modes, and code&lt;br&gt;
completions. The suggested replacements are Claude Opus 5.5 for both Opus&lt;br&gt;
models and GPT-6.1 Sol or GPT-6 Luna for Terra.&lt;/p&gt;
&lt;h3&gt;
  
  
  Does anyone keep access to the retired models after November 16?
&lt;/h3&gt;

&lt;p&gt;Only one carve-out exists: individual GitHub Copilot subscribers on&lt;br&gt;
annual plans keep Claude Opus 4.8, so they retain an Opus offering. The&lt;br&gt;
exception does not cover monthly individual plans, Copilot Business, or&lt;br&gt;
Copilot Enterprise seats - and it does not apply to Opus 5 or GPT-5.6&lt;br&gt;
Terra for anyone.&lt;/p&gt;
&lt;h3&gt;
  
  
  Will my Copilot workflows break on November 16?
&lt;/h3&gt;

&lt;p&gt;Anything that names a retiring model by ID is at risk: prompt files, CI&lt;br&gt;
workflows, Copilot CLI scripts, custom agent definitions, workspace&lt;br&gt;
settings with a pinned default model, and team runbooks. GitHub's&lt;br&gt;
changelog asks you to update workflows and integrations before the date&lt;br&gt;
but does not spell out the exact failure mode - requests may fail outright&lt;br&gt;
or get rerouted to a different default. Treat every pinned reference as&lt;br&gt;
broken until you move it.&lt;/p&gt;
&lt;h3&gt;
  
  
  What should I switch to before November 16?
&lt;/h3&gt;

&lt;p&gt;GitHub's suggested swaps are Claude Opus 5.5 (for Opus 4.8 and Opus 5)&lt;br&gt;
and GPT-6.1 Sol or GPT-6 Luna (for GPT-5.6 Terra). Under default model&lt;br&gt;
enablement, the replacements are automatically enabled for Copilot&lt;br&gt;
Business and Enterprise customers unless an admin turned off the global&lt;br&gt;
default or explicitly disabled the model. If that default is off, enable&lt;br&gt;
them through the model policies in Copilot settings.&lt;/p&gt;
&lt;h3&gt;
  
  
  Do I need to remove the retired models from my setup myself?
&lt;/h3&gt;

&lt;p&gt;No. GitHub's changelog states plainly that no action is required to&lt;br&gt;
remove the models after they are deprecated - GitHub removes them. Your&lt;br&gt;
job is the other direction: making sure the replacements are enabled and&lt;br&gt;
selectable, and updating anything that pins the old names, before&lt;br&gt;
November 16.&lt;/p&gt;
&lt;h3&gt;
  
  
  Are these models dead everywhere, or just in Copilot?
&lt;/h3&gt;

&lt;p&gt;Just in Copilot. Anthropic's own model schedule keeps Claude Opus 5&lt;br&gt;
available on its platforms until at least July 24, 2027 and Claude Opus&lt;br&gt;
4.8 until at least May 28, 2027. This is a Copilot routing decision, not&lt;br&gt;
a model shutdown - the same model can be alive at the vendor and dead&lt;br&gt;
inside your IDE.&lt;/p&gt;
&lt;h2&gt;
  
  
  Get your AI tooling migration done before the cutoff
&lt;/h2&gt;

&lt;p&gt;Model retirements, eval reruns, workflow audits - this is the kind of&lt;br&gt;
unglamorous work that keeps a team's AI tooling from breaking on a Monday&lt;br&gt;
morning. I help teams and founders handle exactly this: auditing pinned&lt;br&gt;
model references across repos and CI, setting up replacement evals, and&lt;br&gt;
building the model inventory so the next retirement wave is a nonevent.&lt;br&gt;
If your team runs on Copilot and November 16 is approaching fast, reach&lt;br&gt;
out through the &lt;a href="https://dev.to/#contact"&gt;contact section on my homepage&lt;/a&gt; and we will&lt;br&gt;
sort it before the cutoff.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
  A pinned model name is a dependency with a lifecycle. Inventory it like one.&lt;br&gt;
&lt;/p&gt;

</description>
      <category>github</category>
      <category>githubcopilot</category>
      <category>ai</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
