<?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: Lawrence Liu</title>
    <description>The latest articles on DEV Community by Lawrence Liu (@xqliu).</description>
    <link>https://dev.to/xqliu</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%2F606610%2Fb44d479c-d577-4434-8d98-642fcf9e6e37.jpeg</url>
      <title>DEV Community: Lawrence Liu</title>
      <link>https://dev.to/xqliu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xqliu"/>
    <language>en</language>
    <item>
      <title>Searching for Orbi's harness, part 1: what our reviewer missed</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Mon, 05 Oct 2026 15:19:59 +0000</pubDate>
      <link>https://dev.to/xqliu/searching-for-orbis-harness-part-1-what-our-reviewer-missed-1n0f</link>
      <guid>https://dev.to/xqliu/searching-for-orbis-harness-part-1-what-our-reviewer-missed-1n0f</guid>
      <description>&lt;p&gt;Orbi takes a GitHub issue and hands back a reviewed, merged pull request. One agent writes the fix, another reviews it, and the loop runs until the review passes. Around the models sits the harness: the prompts each role gets, the skills loaded next to them, the tools on the runner's &lt;code&gt;PATH&lt;/code&gt;, and the rules for when a review may pass.&lt;/p&gt;

&lt;p&gt;When a delivery goes wrong, one run can't tell you whether the model or the harness is to blame. So we stopped guessing and spent a night and the following morning running Orbi's full loop over and over: first on three open-source issues where Orbi had shipped a regression, then on nine ordinary bugs it had never seen, then on three whose first real-world fix had broken something. We swapped prompts, skills, tools, implementation models and review models, and graded every merged result with a hidden grader Orbi never saw.&lt;/p&gt;

&lt;p&gt;This is part 1: how we found the failures and built a harness that fixes them. &lt;a href="https://orbi.build/blog/is-the-regression-guard-worth-its-tokens/?ref=devto" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt; asks whether that harness is worth what it costs. The results and scripts are on the &lt;a href="https://orbi.build/benchmark/?ref=devto" rel="noopener noreferrer"&gt;benchmark page&lt;/a&gt; and in &lt;a href="https://github.com/orbi-build/orbi-bench" rel="noopener noreferrer"&gt;orbi-build/orbi-bench&lt;/a&gt;. Here's the short version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On the issues we tuned on, the harness moved the numbers more than any model swap we tried.&lt;/strong&gt; With &lt;code&gt;deepseek-flash&lt;/code&gt; in both roles, the original harness passed 6 of 15 runs on the three tuning issues. v13 to v15 passed 28 of 30. That's a training score. Takeaway 7 covers issues we didn't tune on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Compare before and after" is not enough.&lt;/strong&gt; Ask an agent to check for regressions and it checks examples it already believes in. One run tested U+0301, which was never broken, and skipped U+3099, which was. Inputs have to be generated by a program, across whole classes of values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An oracle written from the fix proves nothing.&lt;/strong&gt; A &lt;code&gt;gpt-5.6-sol&lt;/code&gt; run generated 1,200 tables, compared them with a "reference" that implemented the rule it had just written, reported &lt;code&gt;head wrong: 0&lt;/code&gt;, and shipped the regression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference libraries are wrong too.&lt;/strong&gt; &lt;code&gt;wcwidth&lt;/code&gt; 0.2.14 counts 👍🏽 as four columns. Trust it and you'll prove a regression correct. Cross-check two references and say which one you believe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The characters you strip are data.&lt;/strong&gt; The password leak needed a secret that itself ended in &lt;code&gt;\n&lt;/code&gt;. Agents always put the backslash after the secret, never inside it. Naming that input class fixed the leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own harness text can be the bug.&lt;/strong&gt; An example we wrote ("don't edit the generated changelog") was wrong for one project and steered Orbi wrong. Rules about a repository should come from the repository's own tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On ordinary bugs, the stronger harness costs about twice as much and buys nothing we could measure.&lt;/strong&gt; On nine unseen issues, the original harness, v15 and v15 with a &lt;code&gt;sol&lt;/code&gt; reviewer all passed 9 of 9. v15 used a median 2.0× the tokens and 1.45× the wall-clock time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents don't repeat the regressions humans made.&lt;/strong&gt; On three bugs whose first upstream fix had broken something (numpy, the OpenTelemetry Collector, aiohttp), the original harness passed 4 of 4 scored runs and v15 3 of 3 (it never finished a numpy run), graded on both the bug and the regression. The regression to guard against is the agent's own habit, rather than a general list of ways code breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review models matter where judgement matters.&lt;/strong&gt; Of the two chainloop failures in v13–v14, one was a &lt;code&gt;deepseek-flash&lt;/code&gt; review misjudging whether &lt;code&gt;\n&lt;/code&gt; was data or an escape; the other was the collision that v15 then fixed. A &lt;code&gt;sol&lt;/code&gt; reviewer passed 4 of 4 there, as did &lt;code&gt;deepseek-flash&lt;/code&gt; on v15, so treat this as a lead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A benchmark is software, and ours had bugs.&lt;/strong&gt; Three of our graders were wrong at some point, one snapshot silently dropped tracked files, and a reviewer once looked up the real upstream fix. Every one of these would have produced a confident, wrong number.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rest of this post shows where each one came from, mistakes included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we started
&lt;/h2&gt;

&lt;p&gt;On September 28 Orbi fixed three issues in forks of open-source projects: &lt;a href="https://github.com/orbi-build/pyinfra/pull/3" rel="noopener noreferrer"&gt;pyinfra&lt;/a&gt;, &lt;a href="https://github.com/orbi-build/chainloop/pull/3" rel="noopener noreferrer"&gt;chainloop&lt;/a&gt; and &lt;a href="https://github.com/orbi-build/fedify/pull/3" rel="noopener noreferrer"&gt;fedify&lt;/a&gt;. All three passed their test suites and Orbi's own review. We checked them by hand before sending anything upstream. Two were wrong.&lt;/p&gt;

&lt;p&gt;The chainloop fix was in a secret redactor. The issue said a JWT in front of an escaped quote got redacted together with the backslash. Orbi's fix trimmed trailing backslashes from each finding, which fixed the JWT and broke two other things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input:  DB_PASSWORD=Zq8mLp2Vx9rT\n next
output: DB_PASSWORD=[REDACTED:generic-password]\n next

input:  password: s
        Host: services.example.com
output: pa[REDACTED][REDACTED]word: [REDACTED]
        Ho[REDACTED]t: [REDACTED]ervice[REDACTED].example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Placeholders shortened in the second example.) In the first, the detector reports the password as &lt;code&gt;Zq8mLp2Vx9rT\n&lt;/code&gt;, ending in a literal backslash and &lt;code&gt;n&lt;/code&gt;. The fix trims those two characters off the finding, so they stay in the log in clear text. In the second, a one-character secret made the redactor replace every &lt;code&gt;s&lt;/code&gt; in the text.&lt;/p&gt;

&lt;p&gt;The pyinfra fix was about CJK text misaligning a results table. The fix counted wide characters as two columns. That aligned the issue's example and made 👍🏽 and a decomposed が worse than before.&lt;/p&gt;

&lt;p&gt;On both, Orbi's review, posted on the fork's issue, reads in full &lt;code&gt;review: pass, no findings&lt;/code&gt;. The review ran the tests and checked the issue's example, which passed. The regressions sat in inputs nobody tried.&lt;/p&gt;

&lt;p&gt;We sent one of the three upstream. The pyinfra fix was reworked by hand with Claude Code and opened as &lt;a href="https://github.com/pyinfra-dev/pyinfra/pull/1977" rel="noopener noreferrer"&gt;pyinfra-dev/pyinfra#1977&lt;/a&gt;, with the AI involvement disclosed. chainloop's waits for a fix we trust. fedify's code was fine, but fedify asks contributors to be assigned an issue before opening a PR, and the maintainer took this one and closed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we measured
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frjgawl4jcz948pavrijc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frjgawl4jcz948pavrijc.png" alt="Each run: a private snapshot of the repository goes through the real Orbi runner (claim, implement, PR, review, merge), then a hidden grader scores it. The grader was calibrated to fail the original code, fail Orbi's merged fork PR, and pass a hand-verified fix." width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The grader is calibrated before any variant runs. A grader that passes the bad fork PR measures nothing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For each issue we pushed a private snapshot of the repository at the commit Orbi had started from, opened the same issue there with the &lt;code&gt;ai-ready&lt;/code&gt; label, and pointed a real Orbi runner at it. The runner did what it does in production: claim, implement, open a PR, run the independent review, fix, merge. Then a hidden grader scored the merged code, together with the repository's own lint and type checks and its full test suite.&lt;/p&gt;

&lt;p&gt;Before any variant ran, each grader had to fail the original code, fail Orbi's merged fork PR, and pass a fix we had verified by hand. For chainloop, the two checks that fail on the fork PR are exactly the two outputs above. For pyinfra they are 👍🏽 and が.&lt;/p&gt;

&lt;p&gt;A variant is one combination of harness and models. We changed the prompt, the skills, the tools on &lt;code&gt;PATH&lt;/code&gt;, the implementation model, the review model, or a mix. The models were &lt;code&gt;deepseek-flash&lt;/code&gt;, &lt;code&gt;gpt-5.6-luna&lt;/code&gt; and &lt;code&gt;gpt-5.6-sol&lt;/code&gt; through Pi, Opus 5.5 through a Claude Code bridge, and GLM 5.3 flash through a zcode bridge. On the two bridges the same model both implements and reviews, and the agent loop is the bridge's own.&lt;/p&gt;

&lt;p&gt;Variants ran three or four at a time on one machine. Each had its own private repository, so no run could see another's branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The grid
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7tbk0egkl32q1g8i43x7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7tbk0egkl32q1g8i43x7.png" alt="Grid of harness versions against model setups. deepseek-flash for both roles goes from 6/15 on the original harness to 9/9 on v15; luna/sol on the original harness scores 1/3; Opus 5.5 scores 1/3, 2/3 and 3/4 on v1, v5 and v11; GLM 5.3 flash scores 2/3 on v14; deepseek implementing with sol reviewing scores 4/4 on v15, chainloop only." width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rows are harness versions, columns are implementer / reviewer. ds = deepseek-flash. Cells are passes / runs across the three repositories.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The grid is sparse on purpose. At every step we picked the cheapest run that could answer the question in front of us; we never set out to fill a full factorial. Most non-deepseek cells have two to four runs. Treat the other columns as leads and the &lt;code&gt;ds/ds&lt;/code&gt; column as the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the harness changes did
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftn1qnsjqif8jrbiacx0d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftn1qnsjqif8jrbiacx0d.png" alt="Bar chart of pass rate by harness version with deepseek-flash for both roles: v0 6/15, v1 5/7, v4 1/3, v5 4/4, v10 1/2, v11 6/9, v13 11/12, v14 8/9, v15 9/9." width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same model throughout. Pale bars have fewer than five runs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0wjonu039k4xier0ife.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0wjonu039k4xier0ife.png" alt="Per repository, original harness against v13 to v15: pyinfra 0/5 to 11/11, chainloop 5/6 to 10/12, fedify 1/4 to 7/7." width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;pyinfra and fedify moved the most. chainloop already passed most runs on the original harness. Its regression shows up rarely, and that's what makes it dangerous.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every rule below came from reading the transcript of a failed run. Each version builds on the one before.&lt;/p&gt;

&lt;h3&gt;
  
  
  v1: ask for a before/after comparison
&lt;/h3&gt;

&lt;p&gt;The first attempt was the obvious one: tell both roles to compare behaviour before and after the fix and write down what they checked. pyinfra went from 0/5 to 2/3.&lt;/p&gt;

&lt;p&gt;The failures are worth a close look. The agent did compare, but on examples it picked itself. In one run it tested combining marks with U+0301 (a Latin acute accent, which the old code already handled) and never tried U+3099 (the Japanese voiced sound mark, which the fix broke). Ask an agent to look for regressions and you get the ones it already expects.&lt;/p&gt;

&lt;h3&gt;
  
  
  v4: the same rule as a separate skill
&lt;/h3&gt;

&lt;p&gt;We moved the v1 rule into a standalone skill instead of the prompt. It scored 1/3. The transcripts show the skill was read. Three runs are too few to say the packaging hurt, but they do show it didn't help: the rule still let the agent pick its own samples.&lt;/p&gt;

&lt;h3&gt;
  
  
  v5: generate the inputs
&lt;/h3&gt;

&lt;p&gt;v5 required inputs to be generated by a program across whole classes (for text: ASCII, CJK and fullwidth, combining marks, emoji modifiers, zero-width sequences, ANSI codes; for syntax: escapes, even and odd backslash runs, quotes, delimiters), with the counts written into &lt;code&gt;.orbi/regression.md&lt;/code&gt; so the reviewer could check them. From then on, runs generated a thousand inputs or more. A v5 run generated 2,756 inputs for the redactor; the largest corpus we saw later, from an Opus run, was 1.19 million character units for the table. v5 passed 4 of 4.&lt;/p&gt;

&lt;h3&gt;
  
  
  v9 and v11: the oracle must not be the change
&lt;/h3&gt;

&lt;p&gt;Then a strong model showed us what generated inputs can't fix on their own. This is from a &lt;code&gt;gpt-5.6-sol&lt;/code&gt; run's own regression report on pyinfra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The comparison uses an independent reference formatter implementing that rule
...
- inputs: 1,200
- head wrong: 0
- base wrong: 1,111
- head wrong but base right (regressions): 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"That rule" was the rule the fix had just implemented: count East Asian wide characters as two columns. The "independent" reference was the fix written a second time, so of course it agreed. The 👍🏽 regression was still there.&lt;/p&gt;

&lt;p&gt;v11 requires an oracle that doesn't depend on the patch: an existing library, the old behaviour where it was right, or an invariant. It also asks the agent to list the dependency tree first, because the reference is often already installed.&lt;/p&gt;

&lt;h3&gt;
  
  
  v11: tools on &lt;code&gt;PATH&lt;/code&gt;, and a bug in our own text
&lt;/h3&gt;

&lt;p&gt;fedify failed because of us. Our prompt included an example: "add a changelog fragment, don't edit the generated changelog". That's good advice for many projects, but fedify uses &lt;code&gt;sacho&lt;/code&gt; with materialization turned on, so every upstream commit edits both the fragment and &lt;code&gt;CHANGES.md&lt;/code&gt;. Runs that followed our example failed &lt;code&gt;sacho check&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix was to stop writing project rules into the harness. v11 says: find the repository's own contribution tooling, run its check command, and treat that as the rule. We also put &lt;code&gt;sacho&lt;/code&gt; and &lt;code&gt;deno&lt;/code&gt; on the runner's &lt;code&gt;PATH&lt;/code&gt;, because an agent guesses at any rule it can't run. fedify went from 1/4 to 3/3. The same change showed up in whether a fix could be sent upstream as is (conventional commit titles, sign-off, AI disclosure, no fork-local &lt;code&gt;#1&lt;/code&gt; references): 9 of 15 original-harness runs passed that check, and 39 of 39 from v11 to v15.&lt;/p&gt;

&lt;h3&gt;
  
  
  v13: the characters you strip are data
&lt;/h3&gt;

&lt;p&gt;The chainloop regression still got through v11 in some runs. Their generated corpora were large and varied, yet every one put the backslash after the secret: &lt;code&gt;secret\&lt;/code&gt;, &lt;code&gt;secret\\&lt;/code&gt;, &lt;code&gt;secret\n&lt;/code&gt;. None had a secret that itself ended in a backslash and &lt;code&gt;n&lt;/code&gt;, and that's the input that leaks.&lt;/p&gt;

&lt;p&gt;v13 names the class directly: when a change strips or trims characters, test values that themselves contain or end with those characters, and ask which producers (here, detection rules) can emit such a value. chainloop went to 4/5.&lt;/p&gt;

&lt;h3&gt;
  
  
  v14: cross-check reference implementations
&lt;/h3&gt;

&lt;p&gt;Once agents used real references, a new failure appeared. In the pyinfra environment, &lt;code&gt;wcwidth&lt;/code&gt; 0.2.14 counts 👍🏽 as four columns instead of two. An agent that trusted it would conclude the regression was correct and the old behaviour wrong. &lt;code&gt;rich&lt;/code&gt;, installed in the same environment, gets it right.&lt;/p&gt;

&lt;p&gt;v14 tells the agent to compare references when more than one exists and to say which one it trusts and why (usually the one with the newer Unicode data). 8/9.&lt;/p&gt;

&lt;h3&gt;
  
  
  v15: collision checks
&lt;/h3&gt;

&lt;p&gt;The one-character &lt;code&gt;s&lt;/code&gt; bug worked differently. A fix changed the needle it searched for (a trimmed secret) and then replaced every occurrence of the shorter needle. v15 adds: if a change alters text that is searched for or replaced, test where else that text appears. 9/9.&lt;/p&gt;

&lt;h3&gt;
  
  
  v17: a producer table (experimental)
&lt;/h3&gt;

&lt;p&gt;v17 asked the regression report to include a table of every producer that can emit the risky input, one row per rule, checked by the reviewer row by row. Agents did read the detector's rule file and list &lt;code&gt;generic-password&lt;/code&gt;. It scored 3/4 on chainloop, which we can't tell apart from v15.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the models did
&lt;/h2&gt;

&lt;p&gt;Model swaps are harder to read than harness changes. Each swap came with a different prompt version and, on the bridges, a different agent loop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;luna implementing, sol reviewing, original harness:&lt;/strong&gt; 1/3. It missed the same pyinfra regression as &lt;code&gt;deepseek-flash&lt;/code&gt;. A stronger model with the same instructions checked the same examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sol in both roles, v5:&lt;/strong&gt; 1/2, and the source of the circular oracle above. Strong models are good at building a convincing test harness around their own assumptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opus 5.5, v1, v5 and v11:&lt;/strong&gt; 1/3, 2/3 and 3/4. Opus built the deepest probes of any model, including the 1.19-million-unit corpus. Before v14 added cross-checking, it still trusted the outdated &lt;code&gt;wcwidth&lt;/code&gt; once. Its bridge also dropped skills until we fixed it after these runs (v12, the one Opus variant configured with skills, ran without them), so read the Opus cells as prompt-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GLM 5.3 flash, v14:&lt;/strong&gt; 2/3. It passed pyinfra and chainloop and failed fedify's &lt;code&gt;sacho check&lt;/code&gt;. Its bridge was silently dropping skills, which we found and fixed during this work, but the prompt carries the same rule, so we can't pin that failure on the missing skill. It was also slow: a median of 68 minutes a run, against 23 for &lt;code&gt;deepseek-flash&lt;/code&gt; on the same prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;deepseek implementing, sol reviewing, v15:&lt;/strong&gt; 4/4 on chainloop. That matches &lt;code&gt;deepseek-flash&lt;/code&gt; reviewing on v15 (4/4), so we can't yet say the stronger reviewer helps. It also took longer: a median of 29.5 minutes on chainloop, against 21 for v15.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these runs the harness moved the numbers far more than any model swap. That's a statement about this data. It doesn't say models don't matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nine issues the harness had never seen
&lt;/h2&gt;

&lt;p&gt;Everything above was tuned on the same three issues it was scored on. That's a training score. It can't tell us whether the rules generalize or just memorize three bugs, so we built a held-out set.&lt;/p&gt;

&lt;p&gt;We took nine real bug fixes merged upstream after August 10, 2026, three each in Go, TypeScript and Python: &lt;code&gt;gojq&lt;/code&gt; (empty regex matches in global mode), &lt;code&gt;urfave/cli&lt;/code&gt; (arguments after a bare &lt;code&gt;-&lt;/code&gt;), &lt;code&gt;pflag&lt;/code&gt; (wrapping after an unbreakable word), &lt;code&gt;magic-string&lt;/code&gt; (&lt;code&gt;$&lt;/code&gt; patterns in replacements), &lt;code&gt;recast&lt;/code&gt; (parentheses around mixed &lt;code&gt;??&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt;), &lt;code&gt;cron-parser&lt;/code&gt; (stringifying stepped ranges), &lt;code&gt;python-dotenv&lt;/code&gt; (backslashes in single-quoted values), &lt;code&gt;markdown-it-py&lt;/code&gt; (a blockquote at end of file with tables on) and &lt;code&gt;pyjwt&lt;/code&gt; (padded Base64URL segments). The hidden grader for each is the maintainer's own test from the fix, which neither we nor Orbi wrote, calibrated the same way: it fails before the fix and passes after it. The issue text Orbi saw was rewritten to remove links, numbers and any hint of the fix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx3xnxi51ann3s2lej83a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx3xnxi51ann3s2lej83a.png" alt="Results on nine unseen issues for the original harness, v15, and v15 with a sol reviewer: every run passed." width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nine issues, three setups, 27 runs, all passed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All three setups passed every held-out issue. The rules we tuned on three bugs didn't make Orbi worse on nine others in three languages. But these nine didn't need the rules either. They are ordinary bugs: the fix is local, and the tests the agent writes are close to the ones the maintainer wrote.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faiocla4c8jd6iol2vnnh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faiocla4c8jd6iol2vnnh.png" alt="Median tokens and minutes per run, original harness against v15, on the training issues and the held-out issues. On held-out issues v15 uses 5.8 million tokens against 2.8 million and 16 minutes against 11." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Medians per run. Tokens include cache reads, which were about 97% of the total.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And v15 cost more. On the held-out set, v15 used a median 5.8 million tokens per run against 2.8 million for the original harness, and 16 minutes against 11. On pflag it wrote 274 lines where the original wrote 88, most of it generated regression tests. About 97% of all tokens were cache reads, which on &lt;code&gt;deepseek-flash&lt;/code&gt; are cheap, so the bill is smaller than the token count suggests. The extra time is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues whose first real fix broke something
&lt;/h2&gt;

&lt;p&gt;The nine held-out bugs were easy for both harnesses, but the original incident was a fix that broke something else. So we looked for real cases of exactly that: an upstream fix that was merged, found to cause a regression, and fixed again. Three made the cut, each with a grader that fails the code before the first fix, fails the first fix itself, and passes the final one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;numpy&lt;/strong&gt;: &lt;code&gt;np.linspace(np.inf, np.inf, 5)&lt;/code&gt; returned NaNs. The first upstream fix handled infinities and broke &lt;code&gt;ndarray&lt;/code&gt; subclasses; astropy's &lt;code&gt;Quantity&lt;/code&gt; stopped working, and a second PR repaired it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry Collector, Elasticsearch exporter&lt;/strong&gt;: retries shared one &lt;code&gt;timeout&lt;/code&gt; budget, so data was dropped while Elasticsearch was down. The first fix stopped sharing the budget and, by accident, made &lt;code&gt;timeout&lt;/code&gt; effectively infinite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;aiohttp&lt;/strong&gt;: a compressed WebSocket message was corrupted when a PING or PONG arrived between its fragments. The first fix rejected valid compressed frames that followed an initial PONG.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Orbi saw only the original bug report. Nothing in the ticket mentioned the regression. Keeping working behaviour working is part of the job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0smgyx5shd5rsom5c2f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0smgyx5shd5rsom5c2f.png" alt="Three regression-prone bugs: numpy, otel-collector and aiohttp. The original harness passed 4 of 4 scored runs and v15 passed 3 of 3; v15 never completed a numpy run." width="799" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The grader tests both the original bug and the regression the first upstream fix introduced.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The original harness passed all four scored runs, and v15 all three. v15 never finished a numpy run: all three attempts were killed for memory, so its 3/3 covers aiohttp once and the OpenTelemetry Collector twice.&lt;/p&gt;

&lt;p&gt;We expected this set to separate the harnesses. It didn't, for a simple reason: the mistakes humans made on their first try aren't the mistakes this agent makes. None of the four original-harness runs reproduced the regression the first human fix had introduced. The regressions Orbi did ship, in pyinfra and chainloop, came from a different habit: checking the example in the issue and nothing around it.&lt;/p&gt;

&lt;p&gt;So a regression guard should target the agent's own failure modes rather than a general list of ways code breaks. With three bugs, that's as far as we can go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the benchmark taught us as much as running it
&lt;/h2&gt;

&lt;p&gt;We found more bugs in our own measurement than in the harness. Each would have produced a confident, wrong number.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A grader enforced the wrong rule.&lt;/strong&gt; Our first fedify grader required &lt;code&gt;CHANGES.md&lt;/code&gt; to stay untouched. Upstream always edits it. We switched to the project's own &lt;code&gt;sacho check&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A grader measured beyond the ticket.&lt;/strong&gt; The first chainloop grader also required newlines after a JWT to survive. The issue never asked for that, so every run "failed" a requirement nobody gave it. We split it into a core check (no worse than before) and an informational one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A grader measured the wrong thing.&lt;/strong&gt; The first pyinfra grader counted ANSI colour codes as columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The snapshot dropped files.&lt;/strong&gt; recast's &lt;code&gt;.gitignore&lt;/code&gt; ignores &lt;code&gt;*.js&lt;/code&gt; while the repository tracks three &lt;code&gt;.js&lt;/code&gt; test fixtures. We built snapshots with &lt;code&gt;git add -A&lt;/code&gt;, which skips ignored files, so the agent worked in a repository with files missing and the grader failed on them. We threw both runs away and reran them; &lt;code&gt;git add -A -f&lt;/code&gt; fixes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The answer leaked, twice.&lt;/strong&gt; Once, an old fork PR had left a cross-reference on the upstream issue, and a fedify review found it and treated it as the reference answer. Once, a pflag reviewer ran &lt;code&gt;gh pr view&lt;/code&gt; on the real upstream fix. In that case review happened after the code was written and passed in one round, so the result stands, but from then on every held-out run went through &lt;code&gt;gh&lt;/code&gt; and &lt;code&gt;git&lt;/code&gt; wrappers that only reach the benchmark's own repositories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our machine was part of the experiment.&lt;/strong&gt; 14 runs were thrown away: two for the recast snapshot above, ten killed for memory (numpy builds from source, and &lt;code&gt;/tmp&lt;/code&gt; was a RAM disk that agents filled with scratch files), and two that collided with an instance name a killed run had left behind. Memory pressure also killed the session driving the benchmark, twice. We now run one or two instances at a time, point &lt;code&gt;TMPDIR&lt;/code&gt; at disk, and a watchdog stops benchmark runs before the kernel does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API budget is shared.&lt;/strong&gt; Every run and every production runner on the account draws on the same GraphQL budget. We added a guard that holds the queue when the budget runs low. At first we blamed most of the drain on the production runners' branch cleanup. Measured properly, it costs about 400 points an hour, far less than the figure we first wrote down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule we took from this: before a grader scores anything, show it failing the known-bad version and passing the known-good one. When a result surprises you, read the transcript before you believe it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this left us
&lt;/h2&gt;

&lt;p&gt;After the first two rounds, v15 fixed every failure it was tuned on, broke nothing on twelve unseen bugs, and cost a median of about twice as much. You can't ship that as is. A guard that roughly doubles the median price of a fix has to show it catches something on bugs it has never seen, and the ordinary held-out bugs gave it nothing to catch.&lt;/p&gt;

&lt;p&gt;So the question for the next round was narrower and harder: on unseen bugs shaped like the ones Orbi actually got wrong, does the original harness fail, and can a cheaper harness keep the fix? &lt;a href="https://orbi.build/blog/is-the-regression-guard-worth-its-tokens/?ref=devto" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt; has the answer.&lt;/p&gt;

&lt;p&gt;This part has clear limits. The tuning set is three issues, the held-out sets are nine and three, and many cells have fewer than five runs. And a hidden grader only catches the defects someone knew to test for, even when that someone is the upstream maintainer.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you run coding agents
&lt;/h2&gt;

&lt;p&gt;Five things you can do this week, whatever agent you use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take a merged agent PR and write the one input you think it gets wrong. Run it. If it breaks, look at what your reviewer said.&lt;/li&gt;
&lt;li&gt;When your agent writes a regression check, find its oracle. If the oracle was derived from the change, the check is decoration.&lt;/li&gt;
&lt;li&gt;Make agents generate inputs with a program and report the counts. A reviewer can check a count; it can't check "I tested edge cases".&lt;/li&gt;
&lt;li&gt;Put the repository's own tools on the agent's &lt;code&gt;PATH&lt;/code&gt; and make their rules the source of truth, instead of writing project conventions into your prompts.&lt;/li&gt;
&lt;li&gt;If you benchmark, calibrate every grader against a known-bad and a known-good version first, and keep the agent away from the real answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://orbi.build/cloud/?ref=devto" rel="noopener noreferrer"&gt;Orbi Cloud&lt;/a&gt; runs the loop described here on your own issues. The benchmark is at &lt;a href="https://github.com/orbi-build/orbi-bench" rel="noopener noreferrer"&gt;orbi-build/orbi-bench&lt;/a&gt; if you want to run it against your own agent.&lt;/p&gt;

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

&lt;p&gt;Read the &lt;a href="https://orbi.build/compare/claude-code/?ref=devto" rel="noopener noreferrer"&gt;Claude Code comparison&lt;/a&gt; and &lt;a href="https://orbi.build/cloud/?ref=devto" rel="noopener noreferrer"&gt;Cloud&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://orbi.build/blog/searching-for-orbis-harness/?ref=devto" rel="noopener noreferrer"&gt;orbi.build&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>testing</category>
      <category>programming</category>
    </item>
    <item>
      <title>Seven steps of an Orbi Cloud setup, on video</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Sun, 27 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/xqliu/seven-steps-of-an-orbi-cloud-setup-on-video-481c</link>
      <guid>https://dev.to/xqliu/seven-steps-of-an-orbi-cloud-setup-on-video-481c</guid>
      <description>&lt;p&gt;The full setup is now on video: &lt;a href="https://www.youtube.com/watch?v=_OEaBwrLvvs" rel="noopener noreferrer"&gt;Orbi Cloud: from zero to a tagged release in seven steps&lt;/a&gt;. It is 83 seconds long, in English, and divided into chapters so you can skip to the step where you are stuck.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/_OEaBwrLvvs" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Watch the complete seven-step Orbi Cloud setup.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Seven key screens from the seven-step flow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhokshcl9it9eng1zc5nd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhokshcl9it9eng1zc5nd.png" alt="Orbi Cloud sign-in screen" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Step 1: Orbi Cloud sign-in screen.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgh5oegg41p59212380b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqgh5oegg41p59212380b.png" alt="GitHub App installation screen" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Step 2: GitHub App installation screen.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiuwxc8wbbenwqddly0jb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiuwxc8wbbenwqddly0jb.png" alt="Repository connection form" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Step 3: Repository connection form.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rk35aummcrf36bjrrz4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6rk35aummcrf36bjrrz4.png" alt="Environment provisioning status" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Step 4: Environment provisioning status.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7f6daf3q881xcshdj2zi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7f6daf3q881xcshdj2zi.png" alt="First Issue status screen" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Step 5: First Issue status screen.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxoyq9ht5hyt8iqctb617.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxoyq9ht5hyt8iqctb617.png" alt="Release form with version field" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Step 6: Release form with version field.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgiuel68d001s71221mkp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgiuel68d001s71221mkp.png" alt="Managed Cloud subscription screen" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Step 7: Managed Cloud subscription screen.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The seven steps are the current Cloud onboarding path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign in&lt;/strong&gt; with your GitHub identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install the Orbi GitHub App&lt;/strong&gt; on your account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect a repository&lt;/strong&gt;: the repo and the base branch Orbi works from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provision the environment.&lt;/strong&gt; About a minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your first Issue.&lt;/strong&gt; Label it &lt;code&gt;ai-ready&lt;/code&gt;, and Orbi writes the code and opens a pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cut a release.&lt;/strong&gt; Type the version; Orbi bumps it, tags it and publishes the GitHub Release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscribe.&lt;/strong&gt; The first 3 deliveries are free; paid plans are Solo at US$29/month with 100M tokens, or Pro at US$79/month with 300M tokens. When the allowance runs out, only new deliveries pause: your data stays intact, and you can cancel at any time.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  The seventh step is the one that starts paid delivery
&lt;/h2&gt;

&lt;p&gt;Steps one through six are setup and delivery: you are handing Orbi the things it cannot infer: an identity, an installation, a repository, a runtime, an Issue, and a release. Step seven is where you opt into the Managed Cloud plan after the 3 free deliveries.&lt;/p&gt;

&lt;p&gt;The release step still requires your decision: &lt;em&gt;you decide the version, Orbi never increments it for you.&lt;/em&gt; The runner will bump the version file, create the tag and publish the Release. It will not choose the number. A version is a claim about what changed and who should care. That is a product decision, and reading it off a counter would be pretending otherwise. The release scope comes from the Milestone carrying that exact title; without one, the release stops at scope derivation and tells you why.&lt;/p&gt;

&lt;p&gt;Step five is one label, and you can add it without leaving the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh issue edit &amp;lt;number&amp;gt; &lt;span class="nt"&gt;--add-label&lt;/span&gt; ai-ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you are actually looking at
&lt;/h2&gt;

&lt;p&gt;Every screen in the video is the product rendering, not a mockup: the sign-in card, the connect form with its repository and base-branch fields, the progress bar counting the step you are on, the connected-repository table with its provisioning status, the release form with the version box, and the subscription step. The GitHub App page is the real one at &lt;a href="https://github.com/apps/orbi-build" rel="noopener noreferrer"&gt;github.com/apps/orbi-build&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to read instead of watch, the same path is in the docs: &lt;a href="https://docs.orbi.build" rel="noopener noreferrer"&gt;docs.orbi.build&lt;/a&gt;. If you want to run the whole thing on your own machine instead of ours, that is the same product, &lt;a href="https://github.com/orbi-build/orbi" rel="noopener noreferrer"&gt;free to self-host&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Start here: &lt;a href="https://orbi.build/cloud/?ref=blog-seven-steps" rel="noopener noreferrer"&gt;orbi.build/cloud&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Read the &lt;a href="https://orbi.build/compare/jules/?ref=devto" rel="noopener noreferrer"&gt;Google Jules comparison&lt;/a&gt; and &lt;a href="https://orbi.build/cloud/?ref=devto" rel="noopener noreferrer"&gt;Cloud&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>github</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Docker image made it out on try three</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Sat, 26 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/xqliu/the-docker-image-made-it-out-on-try-three-4e9o</link>
      <guid>https://dev.to/xqliu/the-docker-image-made-it-out-on-try-three-4e9o</guid>
      <description>&lt;p&gt;Issue &lt;a href="https://github.com/orbi-build/orbi/issues/1031" rel="noopener noreferrer"&gt;#1031&lt;/a&gt; asked for a straightforward fix: publish the container built by &lt;code&gt;3rd/docker/Dockerfile&lt;/code&gt;, so a Docker search returns an image people can pull. The workflow now publishes on every release and on a manual dispatch: &lt;code&gt;linux/amd64&lt;/code&gt; and &lt;code&gt;linux/arm64&lt;/code&gt;, tagged with the release version and &lt;code&gt;latest&lt;/code&gt;, to both &lt;code&gt;ghcr.io/orbi-build/orbi&lt;/code&gt; and &lt;code&gt;docker.io/orbibuild/orbi&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Pull the published image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/orbi-build/orbi:latest
docker pull docker.io/orbibuild/orbi:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs walk from pull to a first delivered Issue: &lt;a href="https://docs.orbi.build/docker" rel="noopener noreferrer"&gt;docs.orbi.build/docker&lt;/a&gt;. The Hub overview lives at &lt;a href="https://hub.docker.com/r/orbibuild/orbi" rel="noopener noreferrer"&gt;hub.docker.com/r/orbibuild/orbi&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three failures, three different causes
&lt;/h2&gt;

&lt;p&gt;All of this happened on 2026-09-17, the day the workflow shipped with release v0.5.17. Each failure left a fix behind, and each fix is a public pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try 1 (release v0.5.17): two jobs, two build contexts
&lt;/h3&gt;

&lt;p&gt;The build-only verify job built the image fine; the publish job failed. Buildx could not find &lt;code&gt;/orbi-container-setup.sh&lt;/code&gt; and &lt;code&gt;/orbi-container-setup.service&lt;/code&gt;, the files the Dockerfile copies. The verify job pointed at the right directory; the publish job pointed at another context, so its build started from a view of the repository that was missing exactly those files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try 2 (manual dispatch): the Dockerfile path resolved from the workspace root
&lt;/h3&gt;

&lt;p&gt;Run &lt;a href="https://github.com/orbi-build/orbi/actions/runs/35215809993" rel="noopener noreferrer"&gt;35215809993&lt;/a&gt; failed all three build steps with &lt;code&gt;failed to read dockerfile: open Dockerfile: no such file or directory&lt;/code&gt;. &lt;a href="https://github.com/orbi-build/orbi/pull/1044" rel="noopener noreferrer"&gt;#1044&lt;/a&gt; names the cause: buildx resolves &lt;code&gt;file: Dockerfile&lt;/code&gt; against the workspace root, so the fix wrote &lt;code&gt;3rd/docker/Dockerfile&lt;/code&gt; in all three places. The fix's own pull-request checks then failed from the other side: the workflow derived the image tag from the git ref, and a pull request's ref is &lt;code&gt;1044/merge&lt;/code&gt;, which buildx rejects as &lt;code&gt;invalid reference format&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try 3 (manual dispatch): the images landed; the overview step kept the job red
&lt;/h3&gt;

&lt;p&gt;Run &lt;a href="https://github.com/orbi-build/orbi/actions/runs/35216638306" rel="noopener noreferrer"&gt;35216638306&lt;/a&gt; pushed &lt;code&gt;0.5.17&lt;/code&gt; and &lt;code&gt;latest&lt;/code&gt; to both registries, for &lt;code&gt;linux/amd64&lt;/code&gt; and &lt;code&gt;linux/arm64&lt;/code&gt;. The job still ended red: the only failing step was the Docker Hub overview update, answered 403 because the token lacked a permission. &lt;a href="https://github.com/orbi-build/orbi/pull/1046" rel="noopener noreferrer"&gt;#1046&lt;/a&gt; made that step &lt;code&gt;continue-on-error: true&lt;/code&gt;, so an overview hiccup can never fail a release again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three rules the failures bought
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Rule 1: every build spells out its paths
&lt;/h3&gt;

&lt;p&gt;Each build step now carries &lt;code&gt;context: 3rd/docker&lt;/code&gt; and &lt;code&gt;file: 3rd/docker/Dockerfile&lt;/code&gt;. Nothing depends on the working directory buildx happens to start from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 2: publish never runs on a PR ref
&lt;/h3&gt;

&lt;p&gt;The build-only job runs on pull requests and builds under a &lt;code&gt;pr-&amp;lt;n&amp;gt;&lt;/code&gt; tag with &lt;code&gt;push: false&lt;/code&gt;; the publish job is gated on the event name. A ref like &lt;code&gt;1044/merge&lt;/code&gt; can no longer reach a tag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3: an overview cannot fail a release
&lt;/h3&gt;

&lt;p&gt;The Docker Hub overview step runs with &lt;code&gt;continue-on-error: true&lt;/code&gt;. The release contract lives in the images; a description update answered 403 cannot turn a finished publish into a failed run.&lt;/p&gt;

&lt;p&gt;Checked against &lt;code&gt;.github/workflows/docker-image.yml&lt;/code&gt; on orbi-build/orbi main, 2026-09-18.&lt;/p&gt;

&lt;h2&gt;
  
  
  From pull to first delivery
&lt;/h2&gt;

&lt;p&gt;A pulled image still needs a model provider before it can deliver. &lt;a href="https://github.com/orbi-build/orbi/issues/1048" rel="noopener noreferrer"&gt;#1048&lt;/a&gt; added the &lt;code&gt;ORBI_PI_*&lt;/code&gt; environment variables: with &lt;code&gt;ORBI_PI_PROVIDER&lt;/code&gt;, &lt;code&gt;ORBI_PI_MODEL&lt;/code&gt;, &lt;code&gt;ORBI_PI_BASE_URL&lt;/code&gt; and &lt;code&gt;ORBI_PI_API_KEY&lt;/code&gt; set, the entrypoint generates &lt;code&gt;pi-providers.json&lt;/code&gt; on first start, and it never touches an existing configuration. &lt;a href="https://github.com/orbi-build/orbi/issues/1049" rel="noopener noreferrer"&gt;#1049&lt;/a&gt; rewrote the Quick start around that: &lt;code&gt;docker pull&lt;/code&gt;, then one copy-pasted &lt;code&gt;docker run&lt;/code&gt; that reaches a real delivery, with build-from-source moved to its own section.&lt;/p&gt;

&lt;p&gt;The first release after the fixes, v0.5.18, published green end to end. That is the state the next release inherits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull the image, create an Issue, get a PR
&lt;/h2&gt;

&lt;p&gt;The Quick start on &lt;a href="https://docs.orbi.build/docker" rel="noopener noreferrer"&gt;docs.orbi.build/docker&lt;/a&gt; takes you from &lt;code&gt;docker pull&lt;/code&gt; to a first delivery. The image overview lives on &lt;a href="https://hub.docker.com/r/orbibuild/orbi" rel="noopener noreferrer"&gt;hub.docker.com/r/orbibuild/orbi&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Read the &lt;a href="https://orbi.build/compare/openhands/?ref=devto" rel="noopener noreferrer"&gt;OpenHands comparison&lt;/a&gt; and &lt;a href="https://orbi.build/cloud/?ref=devto" rel="noopener noreferrer"&gt;Cloud&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>githubactions</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Claude Code in Actions: who presses merge?</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/xqliu/claude-code-in-actions-who-presses-merge-5b4o</link>
      <guid>https://dev.to/xqliu/claude-code-in-actions-who-presses-merge-5b4o</guid>
      <description>&lt;p&gt;Once &lt;a href="https://github.com/anthropics/claude-code-action" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; is wired into GitHub Actions, the implementation side is covered. The action answers questions, makes fixes, reviews a pull request, and posts progress to the thread. Its feature list stops there. The merge and the release still need an owner.&lt;/p&gt;

&lt;p&gt;That boundary makes sense for a general-purpose action. It still leaves the question teams meet almost immediately: a pull request is sitting there, tests are green, Claude wrote it and Claude reviewed it. Who presses merge?&lt;/p&gt;

&lt;p&gt;For the last month the answer on my own repository has been: nobody. 431 pull requests merged, 416 Issues walked the full delivery path, 40 releases tagged and published. I have not read the diffs. This post is what had to exist for that to be a reasonable thing to do rather than a reckless one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model is not the bottleneck
&lt;/h2&gt;

&lt;p&gt;The failure people expect is that the model writes something wrong. That happens, and tests catch most of it. The failure that actually costs you is different: the thing that decides "this is good enough to merge" is the same session that wrote the code, or it is a person who stopped reading carefully three weeks ago.&lt;/p&gt;

&lt;p&gt;So the first rule is that the reviewer is not the author. Not a second pass in the same session with a different prompt: a separate process, a fresh context, started after the pull request exists, reading the frozen diff. It does not know what the implementer was thinking, which is the point. If the code needs the author's intentions to make sense, the code is not finished.&lt;/p&gt;

&lt;p&gt;That reviewer can also fix what it finds and push to the same branch, then re-emit its verdict against the new head. A round that ends with findings is not a merge; it is another round. The loop is bounded at five rounds, and exhausting them is a stop, not a merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three conditions, checked at the moment of merge
&lt;/h2&gt;

&lt;p&gt;A verdict is not enough by itself, because time passes between the review and the merge. Three things are re-checked against the live remote:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous integration is green&lt;/strong&gt; on the exact head that was reviewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The head has not moved&lt;/strong&gt; since the verdict. A push after the review invalidates it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The base is current.&lt;/strong&gt; The pull request must contain the latest &lt;code&gt;main&lt;/code&gt;, absorbed by a plain merge, not a rebase that rewrites what was reviewed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the merge step that is one command, and the pin is the whole point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;merge &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--squash&lt;/span&gt; &lt;span class="nt"&gt;--match-head-commit&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REVIEWED_HEAD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the remote head moved after the review, that call fails instead of merging something nobody reviewed. Only then does the merge land, pinned to that exact commit. If any condition fails the Issue moves to a state that waits for the next tick, or stops for a human. There is no path where a delivery merges because it looked fine.&lt;/p&gt;

&lt;p&gt;These are ordinary conditions. Any team would say them out loud in a code review conversation. The difference is that they are three lines in a state machine rather than three habits in a person's head, so they hold at 3 a.m. on the ninetieth pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a person still does
&lt;/h2&gt;

&lt;p&gt;I write the Issue. That is the whole job, and it is more work than it sounds: one runtime outcome, the acceptance conditions written so that "done" is checkable by something that cannot ask me what I meant. A vague ticket produces a confident wrong delivery, and no gate downstream catches that, because the gate checks whether the code does what the ticket said.&lt;/p&gt;

&lt;p&gt;I also write the release ticket: a version number and a scope. The release itself is a deterministic state machine, not a model session. It freezes the base, waits for green CI, bumps the version, tags, publishes and closes the milestone. Any failure stops and waits for me.&lt;/p&gt;

&lt;p&gt;Between those two tickets, nobody is in the room.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are running the action today
&lt;/h2&gt;

&lt;p&gt;You do not need to replace it. The action is the execution layer; what is missing is the part that decides. Three things to add, in order of how much they buy you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A reviewer that is a different session from the implementer&lt;/strong&gt;, reading the frozen diff, with the authority to fail the delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A merge gate that re-checks CI, head and base&lt;/strong&gt; at merge time rather than trusting a verdict from ten minutes ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A release path that is deterministic&lt;/strong&gt;, so that shipping is not a model deciding it is probably fine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://github.com/orbi-build/orbi" rel="noopener noreferrer"&gt;Orbi&lt;/a&gt; is my implementation of exactly that, open source (AGPL-3.0) and self-hostable, and it drives its own repository: every one of those 431 pull requests is public, and so is every review verdict, in the Issue threads. It runs on the Pi agent harness with your own model: any OpenAI-compatible API, or a Codex subscription. Claude Code is not one of its engines.&lt;/p&gt;

&lt;p&gt;Or take the three conditions and build them into whatever you already run. The conditions matter more than whose code enforces them.&lt;/p&gt;

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

&lt;p&gt;Read the &lt;a href="https://orbi.build/compare/claude-code/?ref=devto" rel="noopener noreferrer"&gt;Claude Code comparison&lt;/a&gt;, &lt;a href="https://orbi.build/guides/ci-gates/?ref=devto" rel="noopener noreferrer"&gt;CI gates guide&lt;/a&gt;, and &lt;a href="https://orbi.build/cloud/?ref=devto" rel="noopener noreferrer"&gt;Cloud&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>githubactions</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>The k8e etcd ticket that closed too early</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Thu, 24 Sep 2026 12:38:54 +0000</pubDate>
      <link>https://dev.to/xqliu/the-k8e-etcd-ticket-that-closed-too-early-12n7</link>
      <guid>https://dev.to/xqliu/the-k8e-etcd-ticket-that-closed-too-early-12n7</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/xiaods/k8e" rel="noopener noreferrer"&gt;k8e&lt;/a&gt; is a Kubernetes distribution with an embedded etcd. It has 497 stars; Deshi (Tommy) Xiao maintains it. On September 20 he opened &lt;a href="https://github.com/xiaods/k8e/issues/612" rel="noopener noreferrer"&gt;issue #612&lt;/a&gt;, asking for an end-to-end robustness suite for the embedded etcd, delivered in four phases. Safe recovery after a crash comes first, then multi-node failure, backup restore and long-running stability.&lt;/p&gt;

&lt;p&gt;At 10:28 (UTC+8) he added the &lt;code&gt;ai-ready&lt;/code&gt; label. The times and numbers below come from the public PR, its reviews and the issue's label history, except the runner crash count, which comes from the runner's log.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first version was rejected
&lt;/h3&gt;

&lt;p&gt;Orbi opened &lt;a href="https://github.com/xiaods/k8e/pull/613" rel="noopener noreferrer"&gt;PR #613&lt;/a&gt; at 11:37 with phase one only: 10 new files, 2,483 lines. It had an operation history recorder, a recovery oracle, a SIGKILL test that kills a child process mid-write, a WAL corruption test and a quota test.&lt;/p&gt;

&lt;p&gt;At 11:46 the maintainer posted what SonarCloud and DeepSource had flagged. Orbi started pushing fixes at 11:57.&lt;/p&gt;

&lt;p&gt;At 13:21 the review on the maintainer's account came back &lt;strong&gt;REQUEST_CHANGES&lt;/strong&gt;. By then the PR was 2,695 lines, and the review had three findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;R1: the oracle accepted an impossible state.&lt;/strong&gt; Say two CAS writes both expect revision 3. A is acknowledged and B's outcome is unknown. If recovery shows B's value, something went wrong, because whichever write ran second must have failed its compare. The oracle accepted it anyway: it treated an unknown CAS as an unconditional put.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The history in question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  k is at rev 3
  A: CAS rev3 -&amp;gt; a  acked
  B: CAS rev3 -&amp;gt; b  unknown
  recovered: k = b
  oracle: OK   (should fail)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;R2: the process being killed was also the one taking notes.&lt;/strong&gt; The recorder ran inside the process the test SIGKILLs. A write could be acknowledged, and the process killed before the acknowledgement reached the log. That write would be logged as "unknown", so losing it would pass the test. The issue had asked for the recorder to sit outside the node under test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R3: an empty value looked like a missing key.&lt;/strong&gt; An empty payload was never hashed, so the oracle read "written as empty" as "does not exist". A lost empty key would pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The review also said the PR body's &lt;code&gt;Fixes #612&lt;/code&gt; would close a four-phase issue after phase one.&lt;/p&gt;

&lt;p&gt;None of the three was about style. Each meant the suite could report "recovered correctly" when the data had not recovered. A test suite that does that gives you confidence you haven't earned, which is worse than having no suite at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Orbi's own mistakes on the same PR
&lt;/h3&gt;

&lt;p&gt;Orbi's engine also reported three failures of its own, each in a PR comment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At 12:28 it tried to add a label the repository didn't have (&lt;code&gt;ai-awaiting-merge&lt;/code&gt;). At 12:37 the engine gave up and marked the issue &lt;code&gt;ai-blocked&lt;/code&gt;, which hands the next step to a human. The maintainer removed that label at 14:16 and put &lt;code&gt;ai-ready&lt;/code&gt; back at 14:41, and the work resumed.&lt;/li&gt;
&lt;li&gt;At 14:54 Orbi's own independent review produced no verdict line, so its result couldn't be parsed.&lt;/li&gt;
&lt;li&gt;At 15:19 its resume check demanded &lt;code&gt;Fixes #612&lt;/code&gt; in the PR body, the same line the maintainer's review had rightly asked Orbi to remove.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Orbi never cleared the third one, and the failure spread beyond the ticket: it crashed the whole runner process serving his repository, 84 times between 15:19 and 22:10 by the runner's own log, and his entire queue stalled. The public write-up, &lt;a href="https://github.com/orbi-build/orbi/issues/1219" rel="noopener noreferrer"&gt;orbi#1219&lt;/a&gt;, was filed partway through and counts 77. At 19:59 the maintainer commented on the issue, 继续修复吧 ("go ahead and keep fixing"). Nothing happened after that. The rule that every PR body must say &lt;code&gt;Fixes&lt;/code&gt; works for a single-shot issue and is wrong for a phased one; the engine enforced it anyway, and the human reviewer was right.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;At 15:05 one commit, &lt;code&gt;f8e9dd9c&lt;/code&gt;, addressed all three findings. An unknown CAS whose expected revision is older than the key's last acknowledged revision is now dropped. The child process only hosts the etcd member, while the parent runs the client and the recorder, so the recorder survives the kill. Every put and CAS payload is hashed, including the empty string.&lt;/p&gt;

&lt;p&gt;New tests cover R1 and R3 in both directions. The PR body now reads "Part of #612 — this PR lands the phase-1 layer only".&lt;/p&gt;

&lt;p&gt;At 22:12 the second review came back &lt;strong&gt;APPROVED&lt;/strong&gt;. It traced each fix and set two conditions. The first was to run the race tests independently. The second was to drop a stale &lt;code&gt;Fixes #612&lt;/code&gt; from Orbi's first commit message; the review called that one optional, because it believed only the PR body closes issues. It also listed the remaining gaps, which the design doc already disclosed. The maintainer merged it himself at 22:13, 38 seconds later, and the record doesn't show either condition being carried out first. CI on the merged head: 801 tests, 49 new, 0 failing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The issue closed anyway
&lt;/h3&gt;

&lt;p&gt;The review was wrong about that commit message. GitHub also closes an issue when a closing keyword in a merged commit message points at it. One second after the merge, #612 closed as &lt;em&gt;completed&lt;/em&gt;, closed by Orbi's first commit, with one phase of four landed. The first review had warned about exactly this outcome, and it came through the one place nobody had fixed.&lt;/p&gt;

&lt;p&gt;That was Orbi's fourth mistake on this ticket, and the one with the biggest consequence. Our engine's rules require &lt;code&gt;Fixes #N&lt;/code&gt; in the PR body, and the delivery agent also put it in its first commit message. Both reviews flagged the keyword, the first in the PR body and the second in the commit. As of this writing, #612 is still closed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The second PR: 3 hours 12 minutes from open to merge
&lt;/h3&gt;

&lt;p&gt;The next day brought &lt;a href="https://github.com/xiaods/k8e/issues/614" rel="noopener noreferrer"&gt;#614&lt;/a&gt;, rqlite compatibility for the M0 gate. The maintainer labelled it at 15:21. &lt;a href="https://github.com/xiaods/k8e/pull/615" rel="noopener noreferrer"&gt;PR #615&lt;/a&gt; added 3,294 lines across 11 files. It opened at 16:49 and was waiting for approval from 17:33. A LoopX review was posted from the maintainer's account, he approved at 20:00:52, and Orbi merged 29 seconds later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking back
&lt;/h3&gt;

&lt;p&gt;Orbi did not get this right the first time. The maintainer had to unblock the issue and put it back in the queue, ask Orbi to keep going when it stalled, and finally merge past one of Orbi's rules that was wrong for his ticket. The four-phase issue still closed after phase one.&lt;/p&gt;

&lt;p&gt;When the review said no, it gave reasons, and the fix landed on the same PR. On a hard ticket in someone else's codebase, that took 10 and a half hours from PR open to merge. Every failure, Orbi's included, is in the public record, so you can check this post against it.&lt;/p&gt;

&lt;p&gt;Two things changed in Orbi afterwards (&lt;a href="https://github.com/orbi-build/orbi/issues/1219" rel="noopener noreferrer"&gt;orbi#1219&lt;/a&gt;). A bad PR body now fails only its own ticket and no longer kills the runner. And Orbi's workflow contract now says a phased initiative must not be carried by one issue: the maintainer splits each phase into a child issue, and each child's PR closes that child and nothing else.&lt;/p&gt;

&lt;p&gt;If you maintain a repo with a queue of well-specified issues, &lt;a href="https://orbi.build/cloud/?ref=blog-k8e" rel="noopener noreferrer"&gt;Orbi Cloud&lt;/a&gt; runs the same loop on yours.&lt;/p&gt;

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

&lt;p&gt;Read the &lt;a href="https://orbi.build/compare/devin/?ref=devto" rel="noopener noreferrer"&gt;Devin comparison&lt;/a&gt; and &lt;a href="https://orbi.build/cloud/?ref=devto" rel="noopener noreferrer"&gt;Cloud&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>testing</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why Orbi is open source again</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Thu, 24 Sep 2026 12:37:00 +0000</pubDate>
      <link>https://dev.to/xqliu/why-orbi-is-open-source-again-47f7</link>
      <guid>https://dev.to/xqliu/why-orbi-is-open-source-again-47f7</guid>
      <description>&lt;p&gt;As of &lt;a href="https://github.com/orbi-build/orbi/releases/tag/v0.5.44" rel="noopener noreferrer"&gt;v0.5.44&lt;/a&gt;, released on September 24, Orbi is licensed under &lt;strong&gt;AGPL-3.0, or the Sustainable Use License (SUL) v1.0&lt;/strong&gt;. You pick one. Self-hosting is free under either, and GitHub now reports the repository's license as AGPL-3.0.&lt;/p&gt;

&lt;p&gt;This is the second license change in a month, so here is the whole history and the reasoning behind each step.&lt;/p&gt;

&lt;h3&gt;
  
  
  The timeline
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Aug 26&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/orbi-build/orbi/issues/81" rel="noopener noreferrer"&gt;#81&lt;/a&gt; added the LICENSE file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 4&lt;/td&gt;
&lt;td&gt;Sustainable Use License v1.0&lt;/td&gt;
&lt;td&gt;commit &lt;a href="https://github.com/orbi-build/orbi/commit/365c5bc" rel="noopener noreferrer"&gt;365c5bc&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 24&lt;/td&gt;
&lt;td&gt;AGPL-3.0 or SUL v1.0&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/orbi-build/orbi/issues/1332" rel="noopener noreferrer"&gt;#1332&lt;/a&gt;, shipped in v0.5.44&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why we left Apache on September 4
&lt;/h3&gt;

&lt;p&gt;Apache-2.0 lets anyone take the code and sell it as a hosted service. Orbi's runner is meant to run on your own machine, and the SUL draws its line in the same place: free for your own use, a separate agreement if you resell it as a service. It is the license n8n uses, and its restriction and patent clauses were copied word for word.&lt;/p&gt;

&lt;p&gt;Timing mattered too. The repository was ten days old and had a single copyright holder, so the change needed one person's agreement. The commit also added a relicensing clause to &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;. Contributors agree that their work may be released under a different license later, &lt;strong&gt;provided that license continues to allow self-hosted use free of charge&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Sustainable Use License cost us
&lt;/h3&gt;

&lt;p&gt;The SUL is not an OSI-approved license, so Orbi could not be described as open source. That turned out to matter more than we expected.&lt;/p&gt;

&lt;p&gt;Several lists that fit Orbi accept only open source projects. &lt;a href="https://github.com/kyrolabs/awesome-agents/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;kyrolabs/awesome-agents&lt;/a&gt; says a contribution "needs to be open source". &lt;a href="https://github.com/Jenqyang/Awesome-AI-Agents/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Awesome-AI-Agents&lt;/a&gt; says licenses that only make the source public "will usually be rejected". Everywhere else, every description of Orbi needed the same footnote: the source is public and self-hosting is free, but it is not OSI open source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why AGPL, and why keep the SUL
&lt;/h3&gt;

&lt;p&gt;AGPL-3.0 is OSI-approved. It also has a network clause: if you modify Orbi and offer it to others as a service, you have to publish your changes under the same license. It does not stop anyone from running an unmodified Orbi as a service. The SUL did stop that, and we are giving it up.&lt;/p&gt;

&lt;p&gt;Elastic made the same move in 2024, when it &lt;a href="https://www.elastic.co/blog/elasticsearch-is-open-source-again" rel="noopener noreferrer"&gt;added AGPL&lt;/a&gt; as an option next to its existing licenses. We kept the SUL as the alternative because some companies do not allow AGPL code at all. They can still use Orbi under the SUL.&lt;/p&gt;

&lt;p&gt;The relicensing clause made this possible, and its condition still holds. Self-hosting stays free under both licenses. The one outside contributor so far started contributing after the clause was in place.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changes for you
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting&lt;/strong&gt;: nothing. It is free under either license, with your own model and your own repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Describing Orbi&lt;/strong&gt;: you can call it open source now, and list it anywhere that requires an OSI license.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offering Orbi to others as a service&lt;/strong&gt;: under AGPL-3.0 you can, as long as you publish any modifications. The SUL does not allow it without a separate agreement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orbi Cloud&lt;/strong&gt;: unchanged. It is the paid hosted version, run by us.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package metadata carries both options as one SPDX expression, from &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;license&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"AGPL-3.0-only OR LicenseRef-Sustainable-Use-1.0"&lt;/span&gt;
&lt;span class="py"&gt;license-files&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"LICENSE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"docs/licenses/sustainable-use-license.md"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The files are in the repository: &lt;a href="https://github.com/orbi-build/orbi/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;code&gt;LICENSE&lt;/code&gt;&lt;/a&gt; holds the unmodified GNU AGPL-3.0 text, and &lt;a href="https://github.com/orbi-build/orbi/blob/main/docs/licenses/sustainable-use-license.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/licenses/sustainable-use-license.md&lt;/code&gt;&lt;/a&gt; holds the SUL and a short note on how the two options work.&lt;/p&gt;

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

&lt;p&gt;Read the &lt;a href="https://orbi.build/compare/openhands/?ref=devto" rel="noopener noreferrer"&gt;OpenHands comparison&lt;/a&gt; and &lt;a href="https://orbi.build/cloud/?ref=devto" rel="noopener noreferrer"&gt;Cloud&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>licensing</category>
      <category>ai</category>
      <category>github</category>
    </item>
    <item>
      <title>Waiting for an answer the user can't give</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:32:09 +0000</pubDate>
      <link>https://dev.to/xqliu/waiting-for-an-answer-the-user-cant-give-3chc</link>
      <guid>https://dev.to/xqliu/waiting-for-an-answer-the-user-cant-give-3chc</guid>
      <description>&lt;p&gt;There is a failure mode that does not look like failure. Nothing crashes. No error is logged. The system is doing exactly what it was designed to do, which is wait — and the person it is waiting for has no idea, and no way to respond.&lt;/p&gt;

&lt;p&gt;We shipped three of these. We found them in one afternoon, because we finally became our own customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The waiting is correct
&lt;/h2&gt;

&lt;p&gt;Our engine will not advance a version on its own. When a milestone closes, an idle tick opens a ticket: this version is complete, here are the candidates, which one is next. Then it stops.&lt;/p&gt;

&lt;p&gt;That lock is deliberate and we are not removing it. Deciding what ships next is a product decision. An engine that picks the next version number because the previous one filled up has confused a counter with a judgement.&lt;/p&gt;

&lt;p&gt;So the ticket is right. The pause is right. What was wrong is the sentence inside the ticket:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please run &lt;code&gt;orbi milestone set &amp;lt;target&amp;gt;&lt;/code&gt; to advance &lt;code&gt;active_milestone&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That instruction assumes a CLI and a shell on the host. A managed tenant has neither — their sandbox runs on our hardware, which is the entire point of it being managed. The engine asked a question, on a ticket the tenant could read, and accepted only an answer they could not produce.&lt;/p&gt;

&lt;p&gt;Worse, managed sandboxes never rendered &lt;code&gt;auto_next_milestone = false&lt;/code&gt; at all. So they did not get the ticket either. The engine paused silently and waited for a decision that nobody had been asked to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The day we noticed
&lt;/h2&gt;

&lt;p&gt;On 2026-09-22 we migrated &lt;code&gt;orbi-build/orbi&lt;/code&gt; — the engine's own repository — onto our hosted sandbox. It shipped v0.5.39 from that sandbox the same morning.&lt;/p&gt;

&lt;p&gt;Then the milestone closed, and everything stopped. No notification. No error. Delivery simply ceased, until a human noticed, created the v0.5.40 milestone by hand with &lt;code&gt;gh api&lt;/code&gt;, and edited the policy file directly.&lt;/p&gt;

&lt;p&gt;We had been running this gap in production for our customers. It took putting our own release line behind it to feel it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, in the UI
&lt;/h2&gt;

&lt;p&gt;Later the same day, a full walkthrough of the product — register, install the App, connect a repo, provision a sandbox, file an issue, deliver, merge, release — surfaced the second instance.&lt;/p&gt;

&lt;p&gt;The status page has a release form. It is a text box. You type a version number from memory, and that string becomes the release scope.&lt;/p&gt;

&lt;p&gt;The page never shows you which milestones exist. We checked: &lt;code&gt;grep -rn "milestone" src/pages/status.ts src/page.ts&lt;/code&gt; returns nothing. Not one line. Meanwhile, six hundred lines away in the same file, the page does this for issues:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These issues have not been handed to Orbi yet: label an issue &lt;code&gt;ai-ready&lt;/code&gt; and Orbi will start on it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The page already scans GitHub on your behalf and lists what you can act on. It just never extended that courtesy to milestones. So you type a version you half remember, and if you get it wrong nothing tells you — the rejection happens later, asynchronously, on a ticket you have to go find.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, in the config
&lt;/h2&gt;

&lt;p&gt;The third instance is the one that explains the other two.&lt;/p&gt;

&lt;p&gt;Our engine writes the active milestone into a repository variable on every tick. The value comes from &lt;code&gt;config.active_milestone&lt;/code&gt; — which lives in the sandbox's own &lt;code&gt;orbi.toml&lt;/code&gt; file. Not GitHub. Not our control plane.&lt;/p&gt;

&lt;p&gt;So the control plane can create a release ticket while the sandbox still believes nothing is in flight. Two systems, two answers, no reconciliation. In our production logs, every tick, for the repository we had just walked through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO active_milestone_variable_absent repo=orbi-build/orbi-beta-e2e-org-09182001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we looked at how a field gets from the control plane into that file, and found there is no general mechanism at all. There are four separate hand-written sync functions — &lt;code&gt;sync_orbi_toml_engine_track&lt;/code&gt;, &lt;code&gt;sync_orbi_toml_providers&lt;/code&gt;, &lt;code&gt;sync_orbi_toml_oauth&lt;/code&gt;, plus the initial render — each one added for a single field, each one staging a temp file and swapping it atomically. The comments record the accretion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Issue #116: the connect-time base branch rides the payload and must reach&lt;/span&gt;
&lt;span class="gh"&gt;# the rendered orbi.toml&lt;/span&gt;
&lt;span class="gh"&gt;# Issue #770: attribution_footer rides the same way&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding one field means editing four files. Which is why nobody added &lt;code&gt;active_milestone&lt;/code&gt;: &lt;code&gt;grep -rn "active_milestone" src/*.ts host-provisioner/*.sh&lt;/code&gt; returns nothing. The control plane does not know the field exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tenant can and cannot touch
&lt;/h2&gt;

&lt;p&gt;One design question resolved itself once we looked at the filesystem.&lt;/p&gt;

&lt;p&gt;We were about to specify merge semantics for "what if the tenant edited a managed field by hand". Then we checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /home/&amp;lt;sandbox&amp;gt;/orbi/orbi.toml
&lt;span class="go"&gt;-rw------- 1 orbi-... orbi-... 926 Sep 22 06:39 orbi.toml

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /home/&amp;lt;sandbox&amp;gt;/.ssh/
&lt;span class="gp"&gt;-rw------- ... id_ed25519       #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;outbound deploy key
&lt;span class="go"&gt;-rw-r--r-- ... id_ed25519.pub
-rw------- ... known_hosts
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;no authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mode &lt;code&gt;0600&lt;/code&gt;, owned by the sandbox user, and no &lt;code&gt;authorized_keys&lt;/code&gt;. The tenant cannot SSH in. There is no hand-editing to reconcile.&lt;/p&gt;

&lt;p&gt;That makes the design simpler than we assumed. The control plane is the sole writer, so the merge only has to preserve other control-plane fields — there is no external drift to detect.&lt;/p&gt;

&lt;p&gt;It also points at a stronger invariant worth holding: &lt;code&gt;orbi.toml&lt;/code&gt; should be a pure derivative, reconstructible from control-plane state alone.&lt;/p&gt;

&lt;p&gt;Today it is not. Engine track and providers live only in the sandbox; the control plane holds a partial truth. Those four sync functions each maintain a fragment of the real answer, and no single place can tell you what a sandbox is supposed to be configured as.&lt;/p&gt;

&lt;p&gt;That distinction — derivative versus authoritative — is the one that decides how much of this class of bug you get. A derivative file can be regenerated, diffed against intent, and repaired by a sweep. An authoritative file that only one machine holds can only be inspected by going there. Every one of the four sync functions exists because a field was made authoritative in the sandbox first, and a way to change it had to be retrofitted afterwards.&lt;/p&gt;

&lt;p&gt;We are not fixing that in one step. But knowing which invariant we want makes the next field's default obvious: the control plane holds it, the sandbox derives it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pull-versus-push question answered itself too
&lt;/h2&gt;

&lt;p&gt;For pushing config down to sandboxes, the obvious tradeoff is pull (the sandbox asks on a schedule) versus push (the control plane triggers a refresh).&lt;/p&gt;

&lt;p&gt;Push means opening an inbound channel to sandbox hosts. Pull means a delay of up to one refresh interval. The usual move is to weigh latency against attack surface.&lt;/p&gt;

&lt;p&gt;Except the interval is already one minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;systemctl &lt;span class="nb"&gt;cat &lt;/span&gt;orbi-cloud-provisioner.timer | &lt;span class="nb"&gt;grep &lt;/span&gt;OnUnit
&lt;span class="go"&gt;OnUnitActiveSec=1min
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the refresh endpoint's query is &lt;code&gt;WHERE r.status = 'active' AND t.status = 'active'&lt;/code&gt; — every live sandbox, every minute, not just pending ones. So a pull costs at most sixty seconds, on an action that already waits for the engine's next claim. There is no tradeoff to weigh. Push would open an inbound path to buy less than a minute.&lt;/p&gt;

&lt;p&gt;Measuring the thing before arguing about it took two commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  A wrong diagnosis, corrected by the database
&lt;/h2&gt;

&lt;p&gt;While reading production logs we found 288 identical errors in six hours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR milestone_reconcile_failed repo=zzuu080603/VCPToolBox-macOS
subprocess.CalledProcessError: ... /milestones?state=all ... exit status 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The obvious read was credential rot, so that is what we wrote down. Reproducing it inside the sandbox gave &lt;code&gt;401 Bad credentials&lt;/code&gt; — which seemed to confirm it, especially since the repository is public and readable with a maintainer token.&lt;/p&gt;

&lt;p&gt;Then we checked the database, and the diagnosis collapsed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;login=cdredfox    tenant=inactive  codeduck            repo=inactive
login=zzuu080603  tenant=active    VCPToolBox-macOS    repo=inactive  ← failing
login=zzuu080603  tenant=active    Tianshu-harness     repo=active    ← fine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both failing sandboxes belong to inactive repositories. The refresh endpoint filters on &lt;code&gt;status = 'active'&lt;/code&gt;, so those tokens are never refreshed — by design, correctly. The same tenant's active repository works perfectly. That is the control group, sitting right there in the same query.&lt;/p&gt;

&lt;p&gt;The real defect was never credentials. It is that we keep reconciling milestones for repositories that were switched off, forever, once per tick. The 401 is a symptom of a correct policy meeting a loop that should have stopped.&lt;/p&gt;

&lt;p&gt;Read the error, then read the data. In that order — we did it backwards and filed a ticket with the wrong root cause, which then had to be publicly corrected.&lt;/p&gt;

&lt;p&gt;What makes this worth writing down is not the mistake. It is that reproducing the error confirmed the &lt;em&gt;wrong&lt;/em&gt; diagnosis. Running the failing command inside the sandbox returned &lt;code&gt;401 Bad credentials&lt;/code&gt;, which is exactly what a credential problem looks like. The repository was public and readable with a maintainer token, which seemed to rule out "the repo is gone". Two pieces of evidence, both genuine, both pointing the wrong way.&lt;/p&gt;

&lt;p&gt;A reproduction tells you what happens. It does not tell you whether it is supposed to happen. The database row — &lt;code&gt;repo_status = inactive&lt;/code&gt; — was the only thing that could distinguish "this token should work and doesn't" from "this token was never meant to be refreshed". And the control group was sitting in the same query the whole time: the same tenant's active repository was working perfectly.&lt;/p&gt;

&lt;p&gt;The generalisable version: when an error reproduces, you have confirmed the mechanism, not the diagnosis. The next question is always whether some policy intends this outcome — and policy lives in data, not in logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What connects them
&lt;/h2&gt;

&lt;p&gt;Three defects, one shape: the system knew something the user needed, and did not say it.&lt;/p&gt;

&lt;p&gt;It knew which milestones existed, and made you type one from memory. It knew a decision was pending, and told you to use a CLI you do not have. It knew the control plane's intent, and left the sandbox holding a different answer.&lt;/p&gt;

&lt;p&gt;None of them logs an error, because none of them is an error. Every component is doing its job. The gap is between components, in the space where a human was supposed to be handed something and was not.&lt;/p&gt;

&lt;p&gt;That space is invisible from inside the code. You cannot grep for it, and your tests will not fail on it, because nothing is broken. You find it by standing where the user stands — which for us meant moving our own release line onto our own product and waiting for it to stall.&lt;/p&gt;

&lt;p&gt;It stalled in four hours.&lt;/p&gt;

&lt;p&gt;The tickets, all filed on 2026-09-22: orbi-cloud#878 (no path for a managed tenant to decide), orbi-cloud#869 (list the milestones in the UI), orbi-cloud#870 (a general config push instead of a fifth hand-written sync), and orbi#1283 (the one whose root cause we got wrong first).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://orbi.build/blog/waiting-for-someone-who-cannot-answer/?ref=dir-devto" rel="noopener noreferrer"&gt;orbi.build/blog/waiting-for-someone-who-cannot-answer&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
    <item>
      <title>Designing one slash command</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:31:53 +0000</pubDate>
      <link>https://dev.to/xqliu/designing-one-slash-command-6l</link>
      <guid>https://dev.to/xqliu/designing-one-slash-command-6l</guid>
      <description>&lt;p&gt;Our engine opens a ticket when it finishes a milestone. The ticket says: this version is done, here are the candidates, tell me which one is next. Then it waits, on purpose.&lt;/p&gt;

&lt;p&gt;The waiting is deliberate. Automatic version advancement is locked off, because deciding what ships next is not a decision a release engine should make for you.&lt;/p&gt;

&lt;p&gt;The problem was the answer. The ticket told you to run &lt;code&gt;orbi milestone set v0.5.40&lt;/code&gt; — a CLI command, on the host machine. Our managed tenants have neither. Their sandbox runs on our hardware. So the engine was asking a question that its own audience could not answer.&lt;/p&gt;

&lt;p&gt;Closing that gap meant building an in-ticket command: comment &lt;code&gt;/milestone v0.5.40&lt;/code&gt; and the engine does the rest. It is one small feature. It took a day of design because almost every choice in it has been made before, by people who then had to live with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not a verb
&lt;/h2&gt;

&lt;p&gt;The first argument was about the name. &lt;code&gt;/advance&lt;/code&gt; was the obvious candidate, and it is wrong.&lt;/p&gt;

&lt;p&gt;In-ticket commands are named after states, not actions. Prow has &lt;code&gt;/lgtm&lt;/code&gt;, &lt;code&gt;/hold&lt;/code&gt;, &lt;code&gt;/approve&lt;/code&gt;. bors has &lt;code&gt;bors r+&lt;/code&gt;. None of them is an imperative verb. This is not a style preference — it comes from what the command means. A CLI says &lt;em&gt;do this thing&lt;/em&gt;, so a verb fits. An in-ticket command says &lt;em&gt;set it to this&lt;/em&gt;, so a noun fits. &lt;code&gt;/milestone v0.5.40&lt;/code&gt; reads as "milestone: v0.5.40", the same shape as &lt;code&gt;/hold&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is a second reason, specific to us. The CLI is already called &lt;code&gt;orbi milestone set&lt;/code&gt;. Naming the in-ticket command &lt;code&gt;/milestone&lt;/code&gt; means the documentation says the word once. &lt;code&gt;advance&lt;/code&gt; would have been a third name for a thing that already has two.&lt;/p&gt;

&lt;p&gt;We also rejected &lt;code&gt;bump version&lt;/code&gt;, because it reads like editing the version string in &lt;code&gt;pyproject.toml&lt;/code&gt; — which is what the release process does, not what this command does. A name that makes people expect the wrong side effect is a bad name even if it is short.&lt;/p&gt;

&lt;p&gt;That last one is worth dwelling on, because it is the rule the other rejections follow from. A command name is a promise about side effects. &lt;code&gt;/advance&lt;/code&gt; promises something moved forward but does not say what — the version? the delivery? the queue? &lt;code&gt;bump version&lt;/code&gt; promises a file edit that never happens. Both are short, pronounceable, and wrong in the same way: the reader forms an expectation the implementation will not honour.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/milestone v0.5.40&lt;/code&gt; makes exactly one promise — the milestone is now v0.5.40 — and the three things it does are all in service of making that true. Creating the milestone, opening the release ticket, landing the config value: none of them is a surprise once you have read the name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no @
&lt;/h2&gt;

&lt;p&gt;The second argument: &lt;code&gt;/milestone&lt;/code&gt; or &lt;code&gt;@orbi milestone&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;The industry splits cleanly, and the dividing line is who controls the bot's account name.&lt;/p&gt;

&lt;p&gt;Prow and bors use a bare slash. They have to: the bot account name varies per deployment. Kubernetes runs &lt;code&gt;k8s-ci-robot&lt;/code&gt;, OpenShift runs &lt;code&gt;openshift-ci-robot&lt;/code&gt;. A syntax that hardcodes &lt;code&gt;@name&lt;/code&gt; cannot be forked.&lt;/p&gt;

&lt;p&gt;Dependabot and Copilot use &lt;code&gt;@bot&lt;/code&gt;. They can: there is exactly one Dependabot, its name is globally unique, and the mention buys a notification and autocomplete for free.&lt;/p&gt;

&lt;p&gt;We are the first kind. The engine is open source and self-hostable. More pointedly, the same engine already runs under two identities in our own org — on our bootstrap runner the comment author is a human account, and in a managed sandbox it is &lt;code&gt;orbi-build[bot]&lt;/code&gt;. We confirmed both on our own repository the same day. Binding the syntax to a name would break self-hosted users on day one.&lt;/p&gt;

&lt;p&gt;The tempting middle path is to accept both: match &lt;code&gt;@orbi milestone&lt;/code&gt; and &lt;code&gt;/milestone&lt;/code&gt;, and let people use whichever they like. We rejected that too, and the reason is not aesthetic. Two syntaxes mean two things to document, two things to test, and two things that can drift apart when someone adds the third command. The mention buys a notification — genuinely useful — but the cost is paid on every future command, forever, by everyone maintaining this. A notification is not worth a permanent fork in the grammar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The regex is three decisions in one
&lt;/h2&gt;

&lt;p&gt;Anchoring at the start of a line, with the multiline flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(?mi)^/milestone[\t ]+(\S+)[\t ]*$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prow writes this in Go. bors splits the comment into lines and anchors each one in Elixir. Two unrelated implementations, same choice — which is usually a sign the choice is load-bearing.&lt;/p&gt;

&lt;p&gt;It buys three things at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the command can appear on any line of a long comment, so you can explain yourself and then issue it&lt;/li&gt;
&lt;li&gt;it is automatically immune to GitHub's quote-reply, because &lt;code&gt;&amp;gt;&lt;/code&gt; pushes the slash off the line start&lt;/li&gt;
&lt;li&gt;it is automatically immune to indented code blocks, for the same reason&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does not buy is immunity to fenced code blocks. And that turned out to be the most interesting thing we read all day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug Prow still has
&lt;/h2&gt;

&lt;p&gt;A regex does not parse Markdown. If you write a comment explaining how to use &lt;code&gt;/hold&lt;/code&gt;, and you put the example inside triple backticks, the regex sees a line starting with &lt;code&gt;/hold&lt;/code&gt; and fires.&lt;/p&gt;

&lt;p&gt;Prow knows this. There is a function for it, &lt;code&gt;DropCodeBlock&lt;/code&gt;, in &lt;code&gt;pkg/markdown/code_block.go&lt;/code&gt;, with a 2025 copyright header. So we checked how widely it is used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &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;"DropCodeBlock"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.go &lt;span class="nb"&gt;.&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; _test.go
pkg/plugins/trigger/generic-comment.go:66
pkg/markdown/code_block.go:25 &lt;span class="o"&gt;(&lt;/span&gt;the definition&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call site. Out of fifty-plus plugins. Writing &lt;code&gt;/hold&lt;/code&gt; inside a fenced block on Prow fires today.&lt;/p&gt;

&lt;p&gt;This is not a criticism of Prow's engineers; it is a structural consequence of where they put the regex. Prow gives every plugin its own pattern — &lt;code&gt;pkg/plugins/hold/hold.go:41&lt;/code&gt; has &lt;code&gt;(?mi)^/hold(\s.*)?$&lt;/code&gt; written out locally. The convention is repo-wide but nothing enforces it, and when each command owns its own matching, a fix to the shared sanitiser cannot reach them. The fix exists. It just cannot arrive.&lt;/p&gt;

&lt;p&gt;So our shared layer compiles the pattern itself, from a declared verb. A command author supplies &lt;code&gt;verbs=("milestone",)&lt;/code&gt; and never sees a regex. They cannot omit the anchor, and they cannot skip the fenced-block stripping, because they were never given the opportunity to write either one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The escalation bors has
&lt;/h2&gt;

&lt;p&gt;The same question applies to permissions, and here the comparison inverts: bors has the better design and the worse bug.&lt;/p&gt;

&lt;p&gt;Prow makes permission each plugin's job. There is a shared helper, &lt;code&gt;TrustedUser(...)&lt;/code&gt; at &lt;code&gt;pkg/plugins/trigger/trigger.go:253&lt;/code&gt;, but a plugin has to remember to call it, and the standard differs per plugin — &lt;code&gt;lgtm.go:291&lt;/code&gt; checks collaborator status, while &lt;code&gt;override.go:317&lt;/code&gt; accepts repo admin or a GitHub team or top-level OWNERS. Worse, the &lt;code&gt;WhoCanUse&lt;/code&gt; string shown in help (&lt;code&gt;override.go:251&lt;/code&gt;) and the code that actually authorises (&lt;code&gt;override.go:326&lt;/code&gt;) are two separate truths, kept in sync by hand.&lt;/p&gt;

&lt;p&gt;bors declares instead. &lt;code&gt;required_permission_level_cmd/1&lt;/code&gt; in &lt;code&gt;lib/web/command.ex:356&lt;/code&gt; maps each command to &lt;code&gt;:none&lt;/code&gt;, &lt;code&gt;:member&lt;/code&gt; or &lt;code&gt;:reviewer&lt;/code&gt;. Then &lt;code&gt;run/1&lt;/code&gt; at &lt;code&gt;command.ex:337-354&lt;/code&gt; takes the strictest level across every command in one comment and checks once, before executing any of them. That is the right shape.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;run/2&lt;/code&gt; is reachable directly, and &lt;code&gt;command.ex:513-516&lt;/code&gt; reaches it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:retry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;commenter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;most_recent_cmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(%{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;commenter:&lt;/span&gt; &lt;span class="n"&gt;commenter&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;:retry&lt;/code&gt; requires only &lt;code&gt;:member&lt;/code&gt; (&lt;code&gt;command.ex:376&lt;/code&gt;). It replays a historical command under the original commenter's identity, and it enters through &lt;code&gt;run/2&lt;/code&gt;, so the gate in &lt;code&gt;run/1&lt;/code&gt; never runs. A member can replay a reviewer's command.&lt;/p&gt;

&lt;p&gt;The lesson is not "bors is insecure". It is that the gate was correct and the second entrance defeated it. One execution path, or the gate is decorative.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we took from each
&lt;/h2&gt;

&lt;p&gt;Two more findings worth copying, one in each direction.&lt;/p&gt;

&lt;p&gt;Prow's genuinely good idea: &lt;code&gt;ValidatePluginsUnknown&lt;/code&gt; at &lt;code&gt;pkg/plugins/config.go:1381&lt;/code&gt; rejects a configured-but-unregistered plugin by looking it up in the help table. That makes "shipped without documentation" a hard error rather than a code review note. Our registry entry carries usage and description, and a binding test fails if a registered command is missing from the docs.&lt;/p&gt;

&lt;p&gt;Prow's gap: &lt;code&gt;RegisterGenericCommentHandler&lt;/code&gt; at &lt;code&gt;pkg/plugins/plugins.go:176&lt;/code&gt; is a bare map assignment. Register the same name twice and the second silently overwrites the first; two plugins both matching &lt;code&gt;^/hold&lt;/code&gt; both run, in nondeterministic goroutine order. We raise on a duplicate name and on overlapping verbs, at import time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The registry already existed
&lt;/h2&gt;

&lt;p&gt;The shape we landed on was not invented for this. Our journal module has had it for months — &lt;code&gt;JOURNAL_EVENTS&lt;/code&gt; at &lt;code&gt;src/orbi/journal.py:167&lt;/code&gt; is an explicit dict, written out in the module. No decorators discovering things, no entry points, no dynamic import. Registration is validation: &lt;code&gt;event()&lt;/code&gt; raises on an unregistered name, so a typo fails immediately instead of writing a silently misnamed event. And a binding test pins the registry against the EN/ZH documentation tables and the exporter's known kinds, in all three directions.&lt;/p&gt;

&lt;p&gt;So the command registry looks the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommandSpec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;                              &lt;span class="c1"&gt;# the registry key and the receipt marker
&lt;/span&gt;    &lt;span class="n"&gt;verbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;                 &lt;span class="c1"&gt;# the shared layer builds the regex from these
&lt;/span&gt;    &lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;                        &lt;span class="c1"&gt;# declarative; the dispatcher enforces it
&lt;/span&gt;    &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# business rule; None means accepted
&lt;/span&gt;    &lt;span class="nb"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[...,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;             &lt;span class="c1"&gt;# raises TicketCommandError
&lt;/span&gt;    &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="n"&gt;TICKET_COMMANDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CommandSpec&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared layer owns — and solely owns — regex construction with the anchor, fenced-block stripping, quoted-line stripping, skipping the runner's own comments, permission enforcement, last-occurrence-wins, and receipts. &lt;code&gt;milestone_command.py&lt;/code&gt; shrinks to a registrant: a &lt;code&gt;validate&lt;/code&gt; that checks the version against the candidate set, and an &lt;code&gt;apply&lt;/code&gt; that runs the three steps.&lt;/p&gt;

&lt;p&gt;Adding a second command becomes "edit one dict, and the tests tell you what else you forgot".&lt;/p&gt;

&lt;h2&gt;
  
  
  One command, three idempotent steps
&lt;/h2&gt;

&lt;p&gt;The original ticket said this command changes a config value. That was wrong, and finding out why took a grep.&lt;/p&gt;

&lt;p&gt;A complete advance needs three things to happen. We had one of them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Engine capability before this work&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create the milestone&lt;/td&gt;
&lt;td&gt;None. &lt;code&gt;create_milestone&lt;/code&gt; and &lt;code&gt;milestones --method POST&lt;/code&gt; had zero hits repo-wide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open the release ticket&lt;/td&gt;
&lt;td&gt;None. Only &lt;code&gt;arm_release_ticket&lt;/code&gt;, which labels an &lt;em&gt;existing&lt;/em&gt; ticket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Land &lt;code&gt;active_milestone&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes — &lt;code&gt;rewrite_active_milestone_line&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The evidence was sitting in our own repository: v0.5.40's milestone had been created by hand with &lt;code&gt;gh api&lt;/code&gt;, and its release ticket did not exist at all.&lt;/p&gt;

&lt;p&gt;The reason it had gone unnoticed for so long is more uncomfortable. A release ticket template lives at &lt;code&gt;.github/release-ticket-template.md&lt;/code&gt;, fully specified. But &lt;code&gt;grep -rn "release-ticket-template" src/ prompts/ AGENTS.md&lt;/code&gt; returns nothing. No code has ever read it. Maintainers had been filling it in with a local client-side tool — a patch installed on exactly one machine. Managed tenants were hitting the unpatched original.&lt;/p&gt;

&lt;p&gt;The fix is not to ship that local tool. It depends on a specific client, and which client a tenant uses is not ours to decide. What belongs in the engine is the capability: render the repo's own template, fill the fields, open the ticket.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;/milestone v0.5.40&lt;/code&gt; does three things, each independently idempotent: create the milestone if absent, open the release ticket if absent, land the config value. Run it twice and you get one milestone and one ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  The branch we refused to write
&lt;/h2&gt;

&lt;p&gt;Landing the config value has two destinations, because &lt;code&gt;active_milestone&lt;/code&gt; can come from two places: a repository policy file (&lt;code&gt;.github/orbi.toml&lt;/code&gt;) or the host's own config. Write to the wrong one and the value either does not take effect or takes effect for the wrong scope.&lt;/p&gt;

&lt;p&gt;The obvious implementation is a runtime check: am I running in a managed sandbox or on a self-hosted runner? We refused to write that branch.&lt;/p&gt;

&lt;p&gt;There is no reliable signal for it. The nearest candidate, &lt;code&gt;engine_source_track&lt;/code&gt;, can be set on a bootstrap runner too, so the check would be a heuristic pretending to be a fact — and heuristics in a write path fail in the direction nobody tests.&lt;/p&gt;

&lt;p&gt;More importantly, the question is the wrong one. We do not need to know &lt;em&gt;where we are running&lt;/em&gt;. We need to know &lt;em&gt;where this value came from&lt;/em&gt;, and that is already known, precisely, by the code that read it: &lt;code&gt;resolve_policy&lt;/code&gt; in &lt;code&gt;src/orbi/repo_config.py:287-290&lt;/code&gt; knows whether &lt;code&gt;active_milestone&lt;/code&gt; came from the repo policy or the host config, because it is the thing that looked.&lt;/p&gt;

&lt;p&gt;So the branch keys off provenance, not environment. Policy-sourced values are written back to &lt;code&gt;.github/orbi.toml&lt;/code&gt; and pushed; host-sourced values go through &lt;code&gt;rewrite_active_milestone_line&lt;/code&gt;. Two paths, one question, and the answer is a fact rather than an inference.&lt;/p&gt;

&lt;p&gt;This generalises: when you find yourself about to detect your own environment, check whether the thing you actually need was already determined by whoever supplied the input.&lt;/p&gt;

&lt;p&gt;Every step is deterministic. No model is invoked — the command's arguments are the input, string substitution is the transformation, and a file write is the output. An LLM in that path would add nondeterminism to an operation whose entire value is being predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we deliberately did not decide
&lt;/h2&gt;

&lt;p&gt;There is a real open question underneath all of this, and we wrote it down rather than answering it by accident.&lt;/p&gt;

&lt;p&gt;Commands are currently read from exactly one place: the confirmation ticket the engine itself opened, on an idle tick, only when auto-advance is off. Three conditions stacked. Outside that intersection, a &lt;code&gt;/command&lt;/code&gt; is never seen.&lt;/p&gt;

&lt;p&gt;That is the narrowest possible scope, and it was the right call for a first command. But it decides which commands can exist. &lt;code&gt;/approve&lt;/code&gt; to clear a review gate, &lt;code&gt;/retry&lt;/code&gt; to requeue a blocked delivery, &lt;code&gt;/cancel&lt;/code&gt; to stop one in flight, &lt;code&gt;/model&lt;/code&gt; to override for a single run — none of them has anywhere to land, because delivery tickets are opened by humans and the engine never opens a confirmation ticket for them.&lt;/p&gt;

&lt;p&gt;Widening the scope to scan every &lt;code&gt;ai-*&lt;/code&gt; ticket costs N extra API calls per tick, a per-ticket cursor, and a rethink of how receipts dedupe across many tickets. That cost is only justified by a command that needs it.&lt;/p&gt;

&lt;p&gt;So the question is not "which option is better". It is: which of those four commands do we actually want, and is any of them worth a per-tick scan? If the answer is none for now, the narrow scope stands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "not yet" is a decision and not a dodge
&lt;/h2&gt;

&lt;p&gt;We parked this in a discussion thread rather than settling it, and that choice deserves the same scrutiny as the others.&lt;/p&gt;

&lt;p&gt;The refactor that lifts the shared layer out (#1294) touches every piece of machinery a wider scan would use — the parser, the permission gate, the receipt dedupe. It would have been easy, and superficially tidy, to widen the scope while we were in there. "We're already touching this code" is the most common reason a scope decision gets made by accident.&lt;/p&gt;

&lt;p&gt;But look at what the widening actually costs. Receipts currently dedupe by scanning one ticket's comment list; across many tickets that needs a different mechanism entirely. Every tick gains N API calls plus pagination and rate-limit handling. The reachable surface grows from "tickets we opened" to "every delivery ticket". Those are not incidental — they are the design of a different feature, and they would have been designed in passing, to serve commands nobody had specified yet.&lt;/p&gt;

&lt;p&gt;The alternative failure is just as real: deciding now to stay narrow, and writing that into the architecture so firmly that &lt;code&gt;/retry&lt;/code&gt; becomes expensive later. So #1294 states its own scope neutrality explicitly — it moves code and adds nothing, and it does not read the discussion's conclusion.&lt;/p&gt;

&lt;p&gt;The cost of widening is only justified by a command that needs it. Until one of &lt;code&gt;/approve&lt;/code&gt;, &lt;code&gt;/retry&lt;/code&gt;, &lt;code&gt;/cancel&lt;/code&gt; or &lt;code&gt;/model&lt;/code&gt; is specified, there is no information that would make the choice better than a coin flip. Writing the question down, with both option's costs enumerated, is what keeps it from being answered by whoever next opens that file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;The command shipped the same day. The implementation was +2073/-81 across 18 files, and the follow-up that lifts the shared layer out of the milestone module is specified and queued.&lt;/p&gt;

&lt;p&gt;The part worth keeping is not the feature. It is that two mature systems, both of which solved this before us, each left one defect visible in their source: Prow's fix that cannot reach its callers, and bors's gate with a second door. Both are the same class of mistake — a correct decision placed where it can be bypassed.&lt;/p&gt;

&lt;p&gt;We got to read both before writing ours. That is the actual luxury of building on open source, and it is worth the day it costs.&lt;/p&gt;

&lt;p&gt;The tickets: #1290 (the design), #1293 (the implementation), #1294 (the shared layer), and #1295 (where commands should be read from). Every Prow and bors line number above was grepped at HEAD on 2026-09-22, not recalled.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://orbi.build/blog/designing-a-slash-command/?ref=dir-devto" rel="noopener noreferrer"&gt;orbi.build/blog/designing-a-slash-command&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
    <item>
      <title>An autonomous coding agent that can tell itself no</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Sun, 20 Sep 2026 09:48:48 +0000</pubDate>
      <link>https://dev.to/xqliu/an-autonomous-coding-agent-that-can-tell-itself-no-39a7</link>
      <guid>https://dev.to/xqliu/an-autonomous-coding-agent-that-can-tell-itself-no-39a7</guid>
      <description>&lt;p&gt;Search for an autonomous coding agent and you will find a lot of tools that open pull requests. That part has been solved for a while. The part nobody shows you is the refusal: what happens when something reads the diff and says this does not ship.&lt;/p&gt;

&lt;p&gt;I want to walk through one delivery where that happened. The writing part is easy to demo. The refusal is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The delivery
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/orbi-build/orbi/issues/1018" rel="noopener noreferrer"&gt;Issue #1018&lt;/a&gt; described a real defect in our own release code. A change had moved the docs sync before the tag push, which opened three windows where a tagged release could exist without its docs page.&lt;/p&gt;

&lt;p&gt;The Issue got the &lt;code&gt;ai-ready&lt;/code&gt; label. Nothing else was set up.&lt;/p&gt;

&lt;p&gt;Orbi claimed it, worked in an isolated worktree, and opened &lt;a href="https://github.com/orbi-build/orbi/pull/1023" rel="noopener noreferrer"&gt;PR #1023&lt;/a&gt; against a recorded base commit. So far this is what every agent does.&lt;/p&gt;

&lt;p&gt;Then a second session, with a fresh context and no memory of writing the code, read that diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The refusal
&lt;/h2&gt;

&lt;p&gt;Round one came back with one Major finding. Not a style note:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Location:&lt;/strong&gt; &lt;code&gt;src/orbi/release.py:2377&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If &lt;code&gt;publish_release&lt;/code&gt; fails after the tag is pushed,&lt;br&gt;
&lt;code&gt;rollback_release_docs&lt;/code&gt; removes the docs commit while leaving the remote tag&lt;br&gt;
published. CI can then observe a released tag without its docs page,&lt;br&gt;
violating acceptance criteria.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix for the bug had reintroduced the bug, in a narrower window. The review named the file, the line, the mechanism, and what a correct fix would look like.&lt;/p&gt;

&lt;p&gt;Orbi read the finding, pushed a recovery path, and ran again. Round two came back too, this time on coverage: new branches with no tests. Three more commits.&lt;/p&gt;

&lt;p&gt;Nine commits in total, +427/-43, three hours and two minutes from the Issue to the merge. Then &lt;a href="https://github.com/orbi-build/orbi/releases/tag/v0.5.17" rel="noopener noreferrer"&gt;v0.5.17&lt;/a&gt;, with the Issue and the PR listed in the generated notes.&lt;/p&gt;

&lt;p&gt;No human decided anything in that window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part the screenshots cannot show
&lt;/h2&gt;

&lt;p&gt;Here is something I had to label on &lt;a href="https://youtu.be/Et6gBSaXqqA" rel="noopener noreferrer"&gt;the video we cut from this run&lt;/a&gt;, because the frames are misleading on their own.&lt;/p&gt;

&lt;p&gt;The runner posts through my own GitHub account. It has my token, so its commits carry my name and its review comments carry my name and my avatar. On screen, the review that blocked PR #1023 appears under &lt;code&gt;xqliu&lt;/code&gt;, with the badges &lt;code&gt;Collaborator&lt;/code&gt; and &lt;code&gt;Author&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A first-time visitor reads that as a developer reviewing his own pull request, which is exactly the impression the whole exercise is meant to refute. Directly below that comment sits a &lt;code&gt;github-actions&lt;/code&gt; comment wearing a &lt;code&gt;Bot&lt;/code&gt; badge, so the same screen teaches you what a bot looks like here and then shows you a review that is not one.&lt;/p&gt;

&lt;p&gt;I cannot make GitHub show it differently. What I can do is say it out loud: that was a second model, running in a separate session, and it had no memory of writing the code it refused.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes when the gate can fail
&lt;/h2&gt;

&lt;p&gt;An agent that writes code and opens a PR has moved the work to a human. Someone still has to read it, decide, and merge. The agent did the typing.&lt;/p&gt;

&lt;p&gt;Making the gate real turned out to be mostly plumbing. The verdict has to be bound to a specific commit, or a later push slips past a judgement that was issued for an earlier one. And the findings have to be specific enough that a fresh session, with no memory of the original work, can act on them: a check that only says "not done yet" leaves the next session guessing.&lt;/p&gt;

&lt;p&gt;The harder part is that it has to be allowed to fail the delivery outright. I have been comparing notes with a few maintainers who ship gates like this, and they all report the same thing: people leave the check advisory, because a blocking check slows down rapid iteration.&lt;/p&gt;

&lt;p&gt;I think that is about who is waiting rather than whether the check is right. Blocking a human's PR costs that human time right now, and they feel it. Blocking an agent's PR costs nobody anything, because the agent reads the finding and goes again at 3am while everyone is asleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;

&lt;p&gt;The whole loop is one container. systemd runs as PID 1 inside it, which is what the cgroup bind is for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; orbi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stop-signal&lt;/span&gt; SIGRTMIN+3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /run &lt;span class="nt"&gt;--tmpfs&lt;/span&gt; /tmp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cgroupns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="nt"&gt;-v&lt;/span&gt; /sys/fs/cgroup:/sys/fs/cgroup:rw &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; orbi-deploy:/orbi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; orbi-work:/work &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"github_pat_xxx"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORBI_SOURCE_REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"OWNER/REPO"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORBI_PI_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deepseek &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORBI_PI_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;deepseek-chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORBI_PI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.deepseek.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORBI_PI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-xxx"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/orbi-build/orbi:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine is whichever model you point those four variables at. Label an Issue &lt;code&gt;ai-ready&lt;/code&gt; and the next tick picks it up. The review runs as a separate session against the frozen head, and nothing merges unless that session passes it. Full walkthrough: &lt;a href="https://docs.orbi.build/docker" rel="noopener noreferrer"&gt;docs.orbi.build/docker&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ledger
&lt;/h2&gt;

&lt;p&gt;This is running on its own repository. As of today, 27 days in: 334 merged pull requests from &lt;code&gt;orbi/&lt;/code&gt; branches, 51 tagged releases.&lt;/p&gt;

&lt;p&gt;Every one of those is a link you can open, including the reviews that blocked something and the commits that came after.&lt;/p&gt;

&lt;p&gt;Orbi is open source and self-hostable: &lt;a href="https://github.com/orbi-build/orbi" rel="noopener noreferrer"&gt;github.com/orbi-build/orbi&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
    <item>
      <title>OKX Stock Perpetuals: What Happens During Dividends and Stock Splits? (2026 Guide)</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Mon, 09 Mar 2026 23:38:44 +0000</pubDate>
      <link>https://dev.to/xqliu/okx-stock-perpetuals-what-happens-during-dividends-and-stock-splits-2026-guide-16d0</link>
      <guid>https://dev.to/xqliu/okx-stock-perpetuals-what-happens-during-dividends-and-stock-splits-2026-guide-16d0</guid>
      <description>&lt;p&gt;OKX rolled out &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;stock perpetuals&lt;/a&gt; across 17 US tickers in February–March 2026 — TSLA, NVDA, AAPL, META, MSFT, GOOGL, QQQ, SPY, and more. 24/7 trading, 5x leverage, USDT-settled. It's a genuinely new way for crypto traders to get stock exposure. (We run &lt;a href="https://dev.to/day-29-multi-front"&gt;real trading experiments&lt;/a&gt; across multiple exchanges — including OKX — so we've spent time in the weeds on these products.)&lt;/p&gt;

&lt;p&gt;But the moment you hold a TSLA-PERP position through a dividend ex-date, you're going to ask: &lt;strong&gt;do I get the dividend?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And when NVDA eventually does another stock split, you'll panic: &lt;strong&gt;does my position just... break?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I went through OKX's full &lt;a href="https://www.okx.com/en-us/help/terms-and-conditions-for-stock-perpetuals" rel="noopener noreferrer"&gt;Terms and Conditions for Stock Perpetuals&lt;/a&gt; and the &lt;a href="https://www.okx.com/en-us/help/stock-perpetuals" rel="noopener noreferrer"&gt;product documentation&lt;/a&gt; to answer both questions. Here's the plain-English version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Answer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What happens to your OKX stock perp position&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cash dividend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You receive nothing. No payout, no adjustment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stock split&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OKX may adjust contract size/price to reflect the split.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reverse split&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same — contract adjustment at OKX's discretion.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Merger/acquisition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OKX may adjust or delist the contract.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spin-off&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OKX may adjust or create a new contract.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you came here for a quick answer: &lt;strong&gt;no dividends, yes to split adjustments.&lt;/strong&gt; Keep reading for the details that actually matter for your trading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dividends: You Get Zero
&lt;/h2&gt;

&lt;p&gt;OKX's terms are explicit about this. Section 3.1 of the Stock Perpetuals T&amp;amp;Cs states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Holders of Stock Perpetuals: Do not receive dividends, interest, or distributions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a fundamental difference from owning actual shares through a broker like &lt;a href="https://ibkr.com/referral/liu460" rel="noopener noreferrer"&gt;Interactive Brokers&lt;/a&gt;. When you buy AAPL on IBKR, you receive the quarterly $0.26/share dividend. When you hold AAPL-PERP on OKX, you receive nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters More Than You Think
&lt;/h3&gt;

&lt;p&gt;Let's do some math. Here are the approximate annual dividend yields for the stocks available as OKX perps (as of March 2026):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ticker&lt;/th&gt;
&lt;th&gt;Approx. Annual Dividend Yield&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;~0.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSFT&lt;/td&gt;
&lt;td&gt;~0.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVDA&lt;/td&gt;
&lt;td&gt;~0.03%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;META&lt;/td&gt;
&lt;td&gt;~0.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TSLA&lt;/td&gt;
&lt;td&gt;0% (no dividend)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QQQ ETF&lt;/td&gt;
&lt;td&gt;~0.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPY ETF&lt;/td&gt;
&lt;td&gt;~1.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For TSLA and NVDA, the dividend is basically irrelevant — TSLA doesn't pay one at all, and NVDA's yield is negligible.&lt;/p&gt;

&lt;p&gt;But for &lt;strong&gt;SPY-PERP&lt;/strong&gt;, missing 1.2% annual yield is real money. If you're holding a $10,000 long SPY-PERP position for a year, that's ~$120 in dividends you'll never see.&lt;/p&gt;

&lt;p&gt;This matters especially when you compare OKX stock perps vs a traditional broker. On IBKR, you pay margin interest but you receive dividends. On OKX, you pay funding rates and receive no dividends. The total cost gap is wider than funding rates alone suggest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Price Adjust on Ex-Dividend Day?
&lt;/h3&gt;

&lt;p&gt;This is the tricky part. In traditional markets, stock prices typically drop by the dividend amount on the ex-dividend date. Since OKX stock perps track a composite index based on real equity prices, the index price will also drop on ex-dividend day.&lt;/p&gt;

&lt;p&gt;That means: &lt;strong&gt;if you're long a stock perp through ex-dividend, the index price drops, your P&amp;amp;L takes a hit, and you don't get the dividend to compensate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For low-yield stocks like TSLA or NVDA, this is negligible. For SPY or QQQ, it's worth planning around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; If you're holding a long SPY-PERP or QQQ-PERP position and a significant ex-dividend date is approaching, consider the price impact. The index will likely dip by the dividend amount (~0.3% for a quarterly SPY dividend), and you won't receive any payout to offset it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stock Splits: OKX Will Adjust Your Contract
&lt;/h2&gt;

&lt;p&gt;Unlike dividends, stock splits get explicit treatment in OKX's documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In reaction to corporate actions that could potentially have a large impact on stock prices, such as stock split and reverse split, mergers and acquisitions, spin-offs, etc., OKX may implement corresponding contract adjustments in accordance with its applicable rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What "Contract Adjustment" Means in Practice
&lt;/h3&gt;

&lt;p&gt;When a stock split happens, the split itself doesn't change the company's market cap — it just changes the share price and count. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tesla's 3-for-1 split (August 2022):&lt;/strong&gt; Stock price went from ~$900 to ~$300, shares tripled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NVDA's 10-for-1 split (June 2024):&lt;/strong&gt; Stock price went from ~$1,200 to ~$120, shares multiplied by 10.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For stock perpetuals, OKX would need to adjust the contract to prevent a sudden 67% or 90% price crash that has nothing to do with actual market movements. The likely mechanism:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before split (example: NVDA 10-for-1)&lt;/th&gt;
&lt;th&gt;After adjustment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contract tracks NVDA at $1,200&lt;/td&gt;
&lt;td&gt;Contract tracks NVDA at $120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your position: 1 contract&lt;/td&gt;
&lt;td&gt;Your position: 10 contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notional value: $1,200&lt;/td&gt;
&lt;td&gt;Notional value: $1,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P&amp;amp;L impact: zero&lt;/td&gt;
&lt;td&gt;P&amp;amp;L impact: zero&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your total exposure stays the same. The number of contracts and the reference price change, but your margin, P&amp;amp;L, and liquidation price should all be adjusted accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "May" Problem
&lt;/h3&gt;

&lt;p&gt;Notice the wording: OKX &lt;strong&gt;"may"&lt;/strong&gt; implement adjustments. It's not guaranteed. And the adjustments happen &lt;strong&gt;"in accordance with its applicable rules"&lt;/strong&gt; — which OKX can update at any time.&lt;/p&gt;

&lt;p&gt;In practice, every major exchange that offers equity derivatives handles splits through contract adjustments. CME does it. IBKR does it. It would be commercially destructive for OKX not to. But the legal language gives them discretion on how and when.&lt;/p&gt;

&lt;p&gt;OKX says they'll publish notices before adjustments take effect:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where appropriate, OKX may publish additional notices describing the nature and timing of such adjustments prior to their effective date.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;My advice:&lt;/strong&gt; When a split is announced for any stock in the OKX perps lineup, check OKX announcements. Don't assume the adjustment is automatic. Close positions before the split if you're not comfortable with the uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse Splits, Mergers, and Spin-Offs
&lt;/h2&gt;

&lt;p&gt;These are rarer but worth understanding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reverse Split
&lt;/h3&gt;

&lt;p&gt;If a stock does a reverse split (e.g., 1-for-10), OKX would likely adjust the contract in the opposite direction — fewer contracts at a higher price. Same total notional.&lt;/p&gt;

&lt;p&gt;None of the current 17 OKX stock perp tickers are reverse-split candidates. Companies like TSLA, NVDA, AAPL, GOOGL, and META are all high-priced, high-cap stocks. But if OKX expands to smaller-cap names, this becomes relevant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mergers and Acquisitions
&lt;/h3&gt;

&lt;p&gt;If MSFT hypothetically acquired another company and restructured its shares, OKX has discretion to adjust the contract, delist it, or create new contracts. The T&amp;amp;Cs give OKX broad authority here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spin-Offs
&lt;/h3&gt;

&lt;p&gt;A spin-off creates a new entity from an existing company (like when eBay spun off PayPal). In traditional markets, shareholders receive shares in the new entity. On OKX stock perps, you won't receive a new position automatically. OKX may adjust the existing contract's pricing or create a new perp for the spin-off entity — but it's entirely at their discretion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Traditional Brokers Do Differently
&lt;/h2&gt;

&lt;p&gt;For comparison, here's how &lt;a href="https://ibkr.com/referral/liu460" rel="noopener noreferrer"&gt;Interactive Brokers&lt;/a&gt; handles the same events:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;IBKR (actual shares)&lt;/th&gt;
&lt;th&gt;OKX (stock perps)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cash dividend&lt;/td&gt;
&lt;td&gt;Deposited to your account&lt;/td&gt;
&lt;td&gt;Nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stock split&lt;/td&gt;
&lt;td&gt;Shares multiplied automatically&lt;/td&gt;
&lt;td&gt;Contract adjustment (at OKX's discretion)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse split&lt;/td&gt;
&lt;td&gt;Shares consolidated automatically&lt;/td&gt;
&lt;td&gt;Contract adjustment (at OKX's discretion)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merger (cash)&lt;/td&gt;
&lt;td&gt;Cash deposited to account&lt;/td&gt;
&lt;td&gt;Contract may be delisted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spin-off&lt;/td&gt;
&lt;td&gt;New shares deposited&lt;/td&gt;
&lt;td&gt;No automatic new position&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voting rights&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fundamental difference: when you own shares on IBKR, you have legal ownership with defined rights. When you hold stock perps on OKX, you hold a derivative contract where the exchange has discretion over corporate action handling.&lt;/p&gt;

&lt;p&gt;This isn't necessarily bad — it's just different. Stock perps give you 24/7 access, USDT settlement, and leverage without a brokerage account. The trade-off is less certainty around corporate actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Scenario: AAPL Ex-Dividend on OKX Stock Perps
&lt;/h2&gt;

&lt;p&gt;Let me walk through a concrete example.&lt;/p&gt;

&lt;p&gt;AAPL pays quarterly dividends. The most recent ex-dividend date was February 9, 2026, with a $0.26/share payout. The next one is estimated around May 9, 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; You're long 10 AAPL-PERP contracts on OKX, each tracking 1 share (approximately $237 per share as of March 2026).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the traditional market (IBKR):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You own 10 AAPL shares&lt;/li&gt;
&lt;li&gt;On ex-dividend day, you receive 10 × $0.26 = $2.60&lt;/li&gt;
&lt;li&gt;AAPL price drops ~$0.26 at open&lt;/li&gt;
&lt;li&gt;Net impact: roughly zero (dividend offsets price drop)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On OKX stock perps:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You hold 10 AAPL-PERP long&lt;/li&gt;
&lt;li&gt;On ex-dividend day, you receive $0.00&lt;/li&gt;
&lt;li&gt;The index price drops ~$0.26 (tracking the real market)&lt;/li&gt;
&lt;li&gt;Net impact: -$2.60 unrealized P&amp;amp;L hit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For AAPL's small dividend, this is $2.60 on a ~$2,370 position — about 0.1%. Not catastrophic. But for SPY's larger yield (quarterly ~$1.75/share), the impact compounds over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upcoming Dividend Calendar for OKX Stock Perp Tickers
&lt;/h2&gt;

&lt;p&gt;If you're actively trading these perps, keep these approximate ex-dividend dates in mind:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ticker&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;th&gt;Next Expected Ex-Date (approx.)&lt;/th&gt;
&lt;th&gt;Yield Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AAPL&lt;/td&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;~May 9, 2026&lt;/td&gt;
&lt;td&gt;~0.1%/quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSFT&lt;/td&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;~May 14, 2026&lt;/td&gt;
&lt;td&gt;~0.18%/quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVDA&lt;/td&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;~June 2026&lt;/td&gt;
&lt;td&gt;Negligible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;META&lt;/td&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;~May 2026&lt;/td&gt;
&lt;td&gt;~0.08%/quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TSLA&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GOOGL&lt;/td&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;~June 2026&lt;/td&gt;
&lt;td&gt;~0.05%/quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QQQ&lt;/td&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;~June 2026&lt;/td&gt;
&lt;td&gt;~0.15%/quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPY&lt;/td&gt;
&lt;td&gt;Quarterly&lt;/td&gt;
&lt;td&gt;~June 2026&lt;/td&gt;
&lt;td&gt;~0.3%/quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;For short-term trades (days to weeks):&lt;/strong&gt; Dividends are mostly irrelevant. The funding rate cost dwarfs the dividend impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For positions held over months:&lt;/strong&gt; Factor in the missed dividends, especially for SPY and QQQ. The full holding cost includes funding rates, missed dividends, and trading fees — all of which compound over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Tips for OKX Stock Perp Traders
&lt;/h2&gt;

&lt;p&gt;Based on everything above, here's what to actually do:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Don't hold long SPY/QQQ perps for yield
&lt;/h3&gt;

&lt;p&gt;If you want stock-market exposure plus dividend income, use a real broker. Stock perps are for trading, not passive investing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Watch for stock split announcements
&lt;/h3&gt;

&lt;p&gt;When a split is announced for any ticker in the OKX lineup, monitor OKX announcements for contract adjustment details. Consider reducing position size if the adjustment timeline is unclear.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Be aware of ex-dividend price drops
&lt;/h3&gt;

&lt;p&gt;For swing trades on dividend-paying stocks (AAPL, MSFT, SPY, QQQ), check the ex-dividend calendar. A long position through ex-date costs you the dividend amount in unrealized P&amp;amp;L.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. TSLA and NVDA are the cleanest perp trades
&lt;/h3&gt;

&lt;p&gt;No meaningful dividend drag. Pure price speculation. If you're trading stock perps for the first time, these two have the least corporate-action complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Read OKX notices before major events
&lt;/h3&gt;

&lt;p&gt;OKX says they'll publish notices for significant corporate actions. Subscribe to their announcements channel or check the &lt;a href="https://www.okx.com/en-us/help" rel="noopener noreferrer"&gt;OKX help center&lt;/a&gt; before holding through any corporate event.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Do I receive dividends on OKX stock perpetuals?
&lt;/h3&gt;

&lt;p&gt;No. OKX's Terms and Conditions explicitly state that stock perpetual holders "do not receive dividends, interest, or distributions." This applies to all 17 tickers including AAPL, MSFT, and SPY, which pay regular dividends on the underlying shares.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens to my OKX stock perp position during a stock split?
&lt;/h3&gt;

&lt;p&gt;OKX may adjust your contract — typically by multiplying the number of contracts and reducing the reference price so your total notional value stays the same. For example, in a 10-for-1 split, 1 contract at $1,200 becomes 10 contracts at $120. OKX publishes notices before major adjustments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the stock perp price drop on ex-dividend day?
&lt;/h3&gt;

&lt;p&gt;Yes. Since OKX stock perps track a composite index based on real equity prices, the index typically drops by the dividend amount on ex-dividend day. Long holders absorb this drop without receiving the dividend. For most tech stocks (TSLA, NVDA) the impact is negligible, but for SPY it can be ~0.3% per quarter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are OKX stock perpetuals better than buying real shares on Interactive Brokers?
&lt;/h3&gt;

&lt;p&gt;It depends on your use case. Stock perps offer 24/7 trading, USDT settlement, up to 5x leverage, and no KYC in some regions. But you miss dividends, face funding rate costs, and have less certainty around corporate actions. For short-term trading, &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;OKX stock perps&lt;/a&gt; work well. For long-term holding, a traditional broker like &lt;a href="https://ibkr.com/referral/liu460" rel="noopener noreferrer"&gt;Interactive Brokers&lt;/a&gt; is cheaper when you factor in dividends, funding rates, and corporate action certainty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I go short on OKX stock perps to profit from ex-dividend price drops?
&lt;/h3&gt;

&lt;p&gt;Technically yes — you could short before ex-dividend and profit from the price drop. But the drop is small (usually 0.1–0.3%) and already priced in by the market. After accounting for funding rates and trading fees, the edge is slim to nonexistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;OKX stock perpetuals are a powerful tool for short-term stock trading from a crypto account. But they're derivatives, not shares. You don't get dividends. You don't get voting rights. And corporate actions like splits are handled at OKX's discretion — not by law.&lt;/p&gt;

&lt;p&gt;For TSLA and NVDA, this barely matters. For SPY and QQQ, the missing dividend income adds up over time.&lt;/p&gt;

&lt;p&gt;Know what you're trading. &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;Check OKX's stock perpetual documentation&lt;/a&gt; for the latest rules, and factor corporate actions into your holding period decisions. If you're new to perpetual futures, our &lt;a href="https://dev.to/learn/ai-trading-rules"&gt;guide to AI trading rules&lt;/a&gt; covers the risk management fundamentals that apply to any perp position.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Affiliate Disclosure: This article contains affiliate links. If you sign up through these links, we may earn a commission at no extra cost to you. All opinions are based on real trading experience and publicly available documentation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Risk Warning: Stock perpetuals involve substantial risk including the potential loss of your entire investment. This article is for educational purposes only and does not constitute financial advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>okx</category>
      <category>trading</category>
      <category>dividends</category>
      <category>stockmarket</category>
    </item>
    <item>
      <title>OKX Stock Perpetuals Funding Rate vs IBKR Margin Interest: Which Costs Less? (2026)</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Mon, 09 Mar 2026 19:36:11 +0000</pubDate>
      <link>https://dev.to/xqliu/okx-stock-perpetuals-funding-rate-vs-ibkr-margin-interest-which-costs-less-2026-29nd</link>
      <guid>https://dev.to/xqliu/okx-stock-perpetuals-funding-rate-vs-ibkr-margin-interest-which-costs-less-2026-29nd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://supa.is/article/okx-stock-perpetuals-funding-rate-vs-ibkr-margin-interest-2026" rel="noopener noreferrer"&gt;supa.is&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  OKX Stock Perpetuals Funding Rate vs Interactive Brokers Margin Interest: Which Costs Less in 2026?
&lt;/h1&gt;

&lt;p&gt;OKX launched &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;stock perpetuals&lt;/a&gt; in February 2026 — TSLA, NVDA, AAPL, META, MSFT, QQQ, SPY, and 10 more tickers. You can now trade US stocks 24/7 with up to 5x leverage, settled in USDT.&lt;/p&gt;

&lt;p&gt;But here's the question nobody's answered yet: &lt;strong&gt;what does it actually cost to hold these positions overnight?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use both &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;OKX&lt;/a&gt; and &lt;a href="https://ibkr.com/referral/liu460" rel="noopener noreferrer"&gt;Interactive Brokers&lt;/a&gt; for real trading. OKX for crypto perpetuals. IBKR for a live USDJPY momentum strategy that runs 24/7 in production. So I pulled the numbers on both to see which platform is cheaper for holding US stock exposure.&lt;/p&gt;

&lt;p&gt;The answer isn't as obvious as you'd think.&lt;/p&gt;

&lt;h2&gt;
  
  
  How OKX Stock Perpetual Funding Rates Work
&lt;/h2&gt;

&lt;p&gt;Stock perpetuals on OKX follow the exact same funding mechanism as crypto perpetuals. The key mechanics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Funding is exchanged every 8 hours&lt;/strong&gt; (00:00, 08:00, 16:00 UTC)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Longs pay shorts&lt;/strong&gt; when funding is positive (most of the time for popular stocks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Funding rate formula&lt;/strong&gt;: based on premium index + interest rate component (baseline 0.01% per 8h)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You only pay if you hold a position at the settlement timestamp&lt;/strong&gt; — close before settlement, no fee&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The baseline interest rate component alone is &lt;strong&gt;0.01% per 8 hours&lt;/strong&gt;, which works out to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.01% × 3 settlements/day × 365 days = 10.95% annualized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that's just the interest rate floor. The actual funding rate also includes a premium component that fluctuates based on whether longs or shorts dominate. During bullish sentiment on popular stocks like TSLA or NVDA, the funding rate can spike well above 0.01%.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Does This Look Like on a Real Position?
&lt;/h3&gt;

&lt;p&gt;Let's say you go long $10,000 worth of TSLA stock perpetuals on OKX.&lt;/p&gt;

&lt;p&gt;At the baseline 0.01% funding rate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Timeframe&lt;/th&gt;
&lt;th&gt;Funding Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per 8h settlement&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per day (3 settlements)&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per week&lt;/td&gt;
&lt;td&gt;$21.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per month (30 days)&lt;/td&gt;
&lt;td&gt;$90.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per year&lt;/td&gt;
&lt;td&gt;$1,095&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's &lt;strong&gt;10.95% annualized&lt;/strong&gt; at the minimum rate. When funding spikes during bullish runs (0.03%–0.05% per 8h is common), you're looking at &lt;strong&gt;33%–55% annualized&lt;/strong&gt;. Ouch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Interactive Brokers Margin Interest Works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ibkr.com/referral/liu460" rel="noopener noreferrer"&gt;Interactive Brokers&lt;/a&gt; charges margin interest as a simple annual percentage rate (APR), calculated daily and billed monthly.&lt;/p&gt;

&lt;p&gt;As of March 2026, IBKR Pro USD margin rates are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Loan Balance&lt;/th&gt;
&lt;th&gt;Rate (APR)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;≤ $100,000&lt;/td&gt;
&lt;td&gt;5.14% (BM + 1.5%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$100K–$1M&lt;/td&gt;
&lt;td&gt;4.64% (BM + 1.0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$1M–$50M&lt;/td&gt;
&lt;td&gt;4.39% (BM + 0.75%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The benchmark (BM) is the Fed Funds Effective Rate, currently around 3.64%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Same $10,000 TSLA Position on IBKR
&lt;/h3&gt;

&lt;p&gt;If you buy $10,000 of TSLA on margin at IBKR (assuming 50% initial margin, so $5,000 borrowed):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Timeframe&lt;/th&gt;
&lt;th&gt;Interest Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per day&lt;/td&gt;
&lt;td&gt;$0.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per week&lt;/td&gt;
&lt;td&gt;$4.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per month&lt;/td&gt;
&lt;td&gt;$21.42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per year&lt;/td&gt;
&lt;td&gt;$257&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Wait — that's only on the $5,000 you actually borrow. If you have $10,000 cash and use zero margin, your holding cost is &lt;strong&gt;$0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On OKX stock perps, you pay funding on the &lt;strong&gt;entire notional position&lt;/strong&gt; regardless of your margin. There's no way to avoid it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Comparison: Side by Side
&lt;/h2&gt;

&lt;p&gt;Here's the honest cost breakdown for a $10,000 long TSLA position held for 30 days:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost Factor&lt;/th&gt;
&lt;th&gt;OKX Stock Perps&lt;/th&gt;
&lt;th&gt;IBKR (50% margin)&lt;/th&gt;
&lt;th&gt;IBKR (cash, no margin)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Funding / Interest&lt;/td&gt;
&lt;td&gt;$90–$150+&lt;/td&gt;
&lt;td&gt;$21.42&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trading fee (entry)&lt;/td&gt;
&lt;td&gt;~$5 (0.05% taker)&lt;/td&gt;
&lt;td&gt;~$1 (tiered)&lt;/td&gt;
&lt;td&gt;~$1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trading fee (exit)&lt;/td&gt;
&lt;td&gt;~$5&lt;/td&gt;
&lt;td&gt;~$1&lt;/td&gt;
&lt;td&gt;~$1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dividend rights&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voting rights&lt;/td&gt;
&lt;td&gt;❌ None&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total 30-day cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$100–$160+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$23&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;IBKR wins on pure cost by a landslide. Not even close for buy-and-hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  When OKX Stock Perps Actually Win
&lt;/h2&gt;

&lt;p&gt;Before you close this tab thinking "IBKR always wins" — there are real scenarios where stock perpetuals make more sense:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Weekend and After-Hours Trading
&lt;/h3&gt;

&lt;p&gt;IBKR doesn't let you trade most US stocks on Saturday or Sunday. OKX stock perps trade &lt;strong&gt;24/7/365&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If NVDA drops 8% on a Sunday because of a news event, OKX perp traders can react immediately. IBKR users wait until Monday's pre-market. That gap can be worth far more than the funding rate difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Crypto as Collateral
&lt;/h3&gt;

&lt;p&gt;On OKX, you can use BTC or ETH as margin for stock perps through unified account mode. No need to sell your crypto to get stock exposure.&lt;/p&gt;

&lt;p&gt;On IBKR, you need USD (or supported fiat currencies) in your account. Want to trade TSLA? Sell your crypto first, transfer fiat, wait 1-3 days for settlement.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No KYC in Some Regions
&lt;/h3&gt;

&lt;p&gt;OKX stock perps are available to crypto-native users without the full brokerage KYC process that IBKR requires (W-8BEN, proof of address, income verification, etc.).&lt;/p&gt;

&lt;p&gt;If you're outside the US and want quick stock exposure without a 3-week account approval process, OKX gets you in the door faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Short-Term Trades (&amp;lt; 24 Hours)
&lt;/h3&gt;

&lt;p&gt;Here's the thing most people miss: &lt;strong&gt;if you open and close a position between funding settlements, you pay zero funding.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Funding settles at 00:00, 08:00, and 16:00 UTC. If you enter a TSLA long at 08:05 UTC and exit at 15:55 UTC, you pay nothing except the trading fee (~0.05% taker each way, so ~0.1% round trip).&lt;/p&gt;

&lt;p&gt;On IBKR, margin interest accrues daily regardless. For day trades or swing trades under 8 hours, OKX perps can actually be cheaper.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Short Selling Simplicity
&lt;/h3&gt;

&lt;p&gt;Shorting on IBKR requires locating shares to borrow, and you pay a stock borrow fee that varies wildly — hard-to-borrow stocks like GME or TSLA can cost 20%+ annualized.&lt;/p&gt;

&lt;p&gt;On OKX, shorting is as simple as clicking "Sell" on the perpetual. No borrowing needed. And if funding is negative (shorts get paid), you actually earn money while holding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost Nobody Talks About: Funding Rate Volatility
&lt;/h2&gt;

&lt;p&gt;The biggest risk with OKX stock perps isn't the baseline rate — it's the &lt;strong&gt;unpredictability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;IBKR margin interest is stable: 5.14% APR doesn't change day-to-day. You know exactly what you'll pay.&lt;/p&gt;

&lt;p&gt;OKX funding rates swing wildly based on market sentiment. During a TSLA rally, I've seen crypto perp funding rates hit 0.1% per 8h — that's &lt;strong&gt;109.5% annualized&lt;/strong&gt;. Stock perps are new, so we don't have months of data yet, but the mechanism is identical.&lt;/p&gt;

&lt;p&gt;If you're planning a multi-week hold, this unpredictability is a genuine risk that's hard to price in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Recommendation: Use Both (Seriously)
&lt;/h2&gt;

&lt;p&gt;After running the numbers, here's my actual setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Long-term stock exposure (weeks to months)&lt;/strong&gt;: &lt;a href="https://ibkr.com/referral/liu460" rel="noopener noreferrer"&gt;IBKR&lt;/a&gt;. The cost advantage is enormous. You get dividends, voting rights, and stable, predictable costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick trades and weekend positioning&lt;/strong&gt;: &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;OKX stock perps&lt;/a&gt;. The 24/7 access and crypto collateral flexibility are genuinely useful. Just don't hold through multiple funding windows if you don't have to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-term momentum plays (&amp;lt;8h)&lt;/strong&gt;: OKX perps, timed between funding settlements. Potentially zero funding cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The platforms serve different purposes. Treating stock perps as a replacement for a brokerage account will destroy your returns through funding fees. But using them as a complement — especially for weekend hedging or quick reactions — is a legitimate edge.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How often does OKX charge funding fees on stock perpetuals?
&lt;/h3&gt;

&lt;p&gt;Every 8 hours at 00:00, 08:00, and 16:00 UTC. If you close your position before the next settlement, you don't pay. Stock perps follow the same funding mechanism as crypto perpetuals on OKX.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Interactive Brokers margin interest charged daily?
&lt;/h3&gt;

&lt;p&gt;Yes. IBKR accrues interest daily based on your margin loan balance and posts the actual charge monthly (third business day of the following month). The rate is a fixed annual percentage, currently 5.14% for USD balances under $100K on IBKR Pro.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do you get dividends on OKX stock perpetuals?
&lt;/h3&gt;

&lt;p&gt;No. OKX's terms explicitly state: "Holders of stock perpetuals do not receive dividends, interest, or distributions." You also get no voting rights. For dividend stocks like AAPL or MSFT (which pay quarterly), this is a real cost to factor in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Bitcoin as collateral for stock perpetuals on OKX?
&lt;/h3&gt;

&lt;p&gt;Yes, through OKX's unified account mode (multi-currency margin or portfolio margin). You can use BTC, ETH, or other supported assets as collateral without selling them first. This is one of the key advantages over traditional brokerages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which is better for day trading US stocks — OKX perps or IBKR?
&lt;/h3&gt;

&lt;p&gt;For pure day trading during US market hours, IBKR is cheaper (lower commissions, no funding risk). But OKX perps have an edge for after-hours and weekend trades when IBKR is closed. If you time trades between OKX funding windows, you can avoid funding fees entirely on intraday moves.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I use both OKX and Interactive Brokers for real trading. Links in this article are affiliate/referral links — they don't cost you anything extra, but they help support this site. Always do your own research before trading with leverage.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Risk Warning: Stock perpetuals and margin trading involve substantial risk of loss. Funding rates are variable and can significantly exceed projected costs. Past cost comparisons do not guarantee future rates. Never trade with money you can't afford to lose.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related articles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/article/how-to-trade-stock-perpetuals-on-okx-tesla-nvidia-apple-2026"&gt;How to Trade Stock Perpetuals on OKX: TSLA, NVDA, AAPL Step-by-Step (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/article/okx-stock-perpetuals-vs-interactive-brokers-which-better-us-stocks-2026"&gt;OKX Stock Perps vs Interactive Brokers: Which Is Better for US Stocks? (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/article/interactive-brokers-python-api-2026-automated-trading-live-markets"&gt;IB Python API 2026: Build a Live Trading System&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>okx</category>
      <category>interactivebrokers</category>
      <category>stockperpetuals</category>
      <category>fundingrate</category>
    </item>
    <item>
      <title>OKX Withdrawal: Which Network Is Cheapest? Save 90% on Fees (2026 Chain Guide)</title>
      <dc:creator>Lawrence Liu</dc:creator>
      <pubDate>Sun, 08 Mar 2026 23:36:03 +0000</pubDate>
      <link>https://dev.to/xqliu/okx-withdrawal-which-network-is-cheapest-save-90-on-fees-2026-chain-guide-53go</link>
      <guid>https://dev.to/xqliu/okx-withdrawal-which-network-is-cheapest-save-90-on-fees-2026-chain-guide-53go</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://luckyclaw.win/learn/okx-withdrawal-cheapest-network-save-fees-2026" rel="noopener noreferrer"&gt;LuckyClaw&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  OKX Withdrawal: Which Network Is Cheapest? Save 90% on Fees (2026 Chain Guide)
&lt;/h1&gt;

&lt;p&gt;Every time you withdraw crypto from OKX, you're making a choice that could cost you $0.08 or $3.50. Same token, same amount — the only difference is which network you pick.&lt;/p&gt;

&lt;p&gt;I've been withdrawing USDT and other tokens from &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;OKX&lt;/a&gt; for over a year. After dozens of withdrawals across different chains, I know exactly which networks save money and which ones burn it. This guide breaks down every option so you never overpay again.&lt;/p&gt;

&lt;h2&gt;
  
  
  How OKX Withdrawal Fees Work
&lt;/h2&gt;

&lt;p&gt;Before diving into specific networks, here's what you need to understand: OKX uses a &lt;strong&gt;dynamic fee model&lt;/strong&gt;. The withdrawal fee you see isn't a fixed number — it adjusts based on real-time blockchain congestion.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fees change throughout the day.&lt;/strong&gt; Ethereum at 3 AM UTC is cheaper than Ethereum during US market hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OKX doesn't add a hidden markup&lt;/strong&gt; on top of network fees (unlike some exchanges that pad the fee).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You always see the exact fee&lt;/strong&gt; before confirming the withdrawal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing that catches new users off guard: OKX &lt;strong&gt;does not charge fees for deposits&lt;/strong&gt;. The withdrawal fee is the only cost, and it's paid by the sender (you).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Complete OKX Withdrawal Fee Comparison (2026)
&lt;/h2&gt;

&lt;p&gt;Here's what you'll actually pay to withdraw USDT from OKX on each supported network, ranked from cheapest to most expensive:&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 1: Near-Free (Under $0.10)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Network&lt;/th&gt;
&lt;th&gt;Typical USDT Fee&lt;/th&gt;
&lt;th&gt;Confirmation Time&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solana (SPL)&lt;/td&gt;
&lt;td&gt;~$0.01&lt;/td&gt;
&lt;td&gt;~5 seconds&lt;/td&gt;
&lt;td&gt;DeFi wallets, Phantom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polygon (PoS)&lt;/td&gt;
&lt;td&gt;~$0.08&lt;/td&gt;
&lt;td&gt;~5 seconds&lt;/td&gt;
&lt;td&gt;DeFi, gaming wallets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avalanche (C-Chain)&lt;/td&gt;
&lt;td&gt;~$0.05&lt;/td&gt;
&lt;td&gt;~2 seconds&lt;/td&gt;
&lt;td&gt;DeFi protocols&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimism&lt;/td&gt;
&lt;td&gt;~$0.08&lt;/td&gt;
&lt;td&gt;~2 minutes&lt;/td&gt;
&lt;td&gt;L2 DeFi, Aave/Uniswap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Tier 2: Cheap (Under $0.50)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Network&lt;/th&gt;
&lt;th&gt;Typical USDT Fee&lt;/th&gt;
&lt;th&gt;Confirmation Time&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Arbitrum One&lt;/td&gt;
&lt;td&gt;~$0.10&lt;/td&gt;
&lt;td&gt;~2 minutes&lt;/td&gt;
&lt;td&gt;Hyperliquid bridging, L2 DeFi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tron (TRC-20)&lt;/td&gt;
&lt;td&gt;~$0.10–$1.00&lt;/td&gt;
&lt;td&gt;~3 seconds&lt;/td&gt;
&lt;td&gt;CEX-to-CEX transfers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BNB Chain (BEP-20)&lt;/td&gt;
&lt;td&gt;~$0.10–$0.50&lt;/td&gt;
&lt;td&gt;~5 seconds&lt;/td&gt;
&lt;td&gt;Binance ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Tier 3: Expensive (Avoid If Possible)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Network&lt;/th&gt;
&lt;th&gt;Typical USDT Fee&lt;/th&gt;
&lt;th&gt;Confirmation Time&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ethereum (ERC-20)&lt;/td&gt;
&lt;td&gt;~$1.50–$5.00+&lt;/td&gt;
&lt;td&gt;~2–5 minutes&lt;/td&gt;
&lt;td&gt;Only when destination requires it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The takeaway:&lt;/strong&gt; Choosing Polygon or Solana over Ethereum can save you 90%+ on every single withdrawal. For a $500 USDT withdrawal, you're saving roughly $3–5 each time. Do that 10 times a month and you've saved $30–50 just by picking the right chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Network Should You Actually Use?
&lt;/h2&gt;

&lt;p&gt;The cheapest network isn't always the right one. Here's my decision framework based on what I actually do:&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending to Another Exchange (CEX-to-CEX)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use TRC-20 or BNB Chain (BEP-20).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These two networks have the widest support across exchanges. Nearly every major exchange — Binance, Bybit, KuCoin, Gate.io — supports TRC-20 and BEP-20 deposits. The fees are low and confirmations are fast.&lt;/p&gt;

&lt;p&gt;I used to default to TRC-20 for everything, but BEP-20 has become equally reliable with slightly faster finality in my experience.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Critical rule:&lt;/strong&gt; Always verify the receiving exchange supports the network you're using. Sending BEP-20 USDT to an ERC-20 address will lose your funds permanently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Sending to a DeFi Wallet
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use the network your DeFi protocol lives on.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're heading to Aave on Arbitrum, withdraw on Arbitrum. Going to a Solana DEX? Withdraw on Solana. This avoids the extra step (and cost) of bridging later.&lt;/p&gt;

&lt;p&gt;This is where OKX's multi-chain support really shines — they support &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;over 70 blockchains&lt;/a&gt;, so you can almost always withdraw directly to the chain you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bridging to Hyperliquid
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use Arbitrum One.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're moving funds to Hyperliquid for perp trading, Arbitrum is your only option since Hyperliquid runs on Arbitrum. I covered a similar flow in my &lt;a href="https://dev.to/day-29-multi-front"&gt;trading journal&lt;/a&gt; when I opened a second front on OKX — withdraw USDC on Arbitrum, then deposit directly to your target. The whole process takes about 5 minutes and costs under $0.20.&lt;/p&gt;

&lt;h3&gt;
  
  
  When You're Forced to Use Ethereum (ERC-20)
&lt;/h3&gt;

&lt;p&gt;Some destinations only accept ERC-20. Certain DeFi protocols, older wallets, or institutional custodians require the Ethereum mainnet. When you have no choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time it right.&lt;/strong&gt; Ethereum gas fees drop significantly during off-peak hours (weekends, early UTC mornings).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch your withdrawals.&lt;/strong&gt; Instead of 5 small withdrawals at $3 each, do 1 larger withdrawal for $3 total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider OKX's internal transfer&lt;/strong&gt; — if you're sending to another OKX user, use the internal transfer feature (zero fees, instant).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-Step: How to Choose the Cheapest Network on OKX
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Go to Assets → Withdraw&lt;/strong&gt; and select your token (e.g., USDT)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enter the destination address&lt;/strong&gt; — OKX will auto-detect compatible networks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click the network selector&lt;/strong&gt; — you'll see a dropdown listing every available chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare the fee column&lt;/strong&gt; — OKX shows the exact fee in real-time for each network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the minimum withdrawal&lt;/strong&gt; — some networks have minimums (e.g., 10 USDT minimum on some chains)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select the cheapest compatible network&lt;/strong&gt; and confirm&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key is step 4: OKX shows you the live fee for every network side by side. You don't need to guess — just pick the lowest number that matches your destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pro Tips I've Learned the Hard Way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. OKX Internal Transfer = Zero Fees
&lt;/h3&gt;

&lt;p&gt;If you're sending to another OKX user, skip the blockchain entirely. Go to &lt;strong&gt;Assets → Transfer → Internal Transfer&lt;/strong&gt; and send using their OKX email, phone, or UID. It's instant and completely free.&lt;/p&gt;

&lt;p&gt;I use this constantly when moving funds between my own accounts. Why pay $0.10 when you can pay $0?&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use OKX Web3 Wallet for DEX Swaps
&lt;/h3&gt;

&lt;p&gt;OKX's built-in Web3 wallet has a &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;DEX aggregator&lt;/a&gt; that finds the best swap routes across chains. If you need to convert tokens or bridge between networks, this often costs less than withdrawing to an external wallet and swapping there.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Check Fees at Different Times
&lt;/h3&gt;

&lt;p&gt;I've seen Ethereum ERC-20 fees swing from $1.50 to $8.00 within the same day. If you're not in a rush, check fees during low-traffic hours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cheapest:&lt;/strong&gt; Saturday/Sunday, 02:00–08:00 UTC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most expensive:&lt;/strong&gt; Tuesday–Thursday, 14:00–18:00 UTC (US market hours)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Convert Before Withdrawing
&lt;/h3&gt;

&lt;p&gt;Sometimes it's cheaper to convert your token to a different stablecoin that has lower fees on your target network. For example, if you're heading to Solana, converting USDT to USDC on OKX (via Convert — zero trading fees on the Convert feature) and then withdrawing USDC on Solana can sometimes be even cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes That Cost You Money
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Always using ERC-20 "because it's Ethereum."&lt;/strong&gt; Ethereum is the most trusted network, but that trust comes with a 10–50x fee premium. Unless the destination requires ERC-20, use a cheaper chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Not checking if the destination supports your chosen network.&lt;/strong&gt; I've seen people withdraw on Polygon and then panic because their exchange doesn't support Polygon deposits. Always verify &lt;strong&gt;both sides&lt;/strong&gt; before sending.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Making many small withdrawals.&lt;/strong&gt; Each withdrawal incurs the full network fee regardless of amount. Withdrawing $50 costs the same fee as withdrawing $5,000. Batch your withdrawals to minimize the number of transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Ignoring minimum withdrawal amounts.&lt;/strong&gt; Some networks have minimums. If you're trying to withdraw a small amount, a network with a lower minimum might be a better choice even if the fee is slightly higher.&lt;/p&gt;

&lt;h2&gt;
  
  
  OKX Withdrawal Fees vs Other Exchanges
&lt;/h2&gt;

&lt;p&gt;OKX's dynamic fee model generally matches or undercuts competitors for withdrawal fees. The real advantage is &lt;strong&gt;network variety&lt;/strong&gt; — OKX supports more withdrawal networks than most exchanges, giving you more cheap options.&lt;/p&gt;

&lt;p&gt;For context: some exchanges charge a flat withdrawal fee that includes a markup over the actual network cost. OKX's approach of passing through the real-time network fee means you're paying closer to the true blockchain cost.&lt;/p&gt;

&lt;p&gt;If you're not yet on OKX, you can &lt;a href="https://www.okx.com/join/26750180" rel="noopener noreferrer"&gt;sign up here&lt;/a&gt; and start benefiting from their multi-network withdrawal options immediately. And if you're curious how an AI uses these features to trade autonomously, check out &lt;a href="https://dev.to/learn/ai-trading-rules"&gt;how our trading system works&lt;/a&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is the cheapest network to withdraw USDT from OKX?
&lt;/h3&gt;

&lt;p&gt;Solana (SPL) and Polygon (PoS) are consistently the cheapest options, typically costing under $0.10 per withdrawal. However, make sure your destination wallet or exchange supports whichever network you choose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does OKX charge fees for deposits?
&lt;/h3&gt;

&lt;p&gt;No. OKX does not charge any fees for crypto deposits. Only withdrawals incur a network fee, which varies by blockchain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I withdraw from OKX to another OKX account for free?
&lt;/h3&gt;

&lt;p&gt;Yes. OKX's internal transfer feature lets you send crypto to other OKX users instantly with zero fees. Use their email, phone number, or OKX UID instead of a blockchain address.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my OKX withdrawal fee higher than usual?
&lt;/h3&gt;

&lt;p&gt;OKX uses dynamic fees that adjust with real-time blockchain congestion. If Ethereum gas prices spike, your ERC-20 withdrawal fee goes up. Try again during off-peak hours or switch to a cheaper network like TRC-20 or Arbitrum.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it safe to use cheaper networks like Polygon or Solana for withdrawals?
&lt;/h3&gt;

&lt;p&gt;Yes. These networks are fully operational blockchains with their own security models. The fee difference reflects the cost of block space, not security quality. Polygon and Solana process billions of dollars in transactions daily. The main risk isn't the network — it's sending to an address on a network your destination doesn't support.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article contains affiliate links. If you sign up through our links, we may earn a commission at no extra cost to you. All opinions and fee data are based on firsthand experience. Cryptocurrency trading involves risk — never withdraw more than you can afford to have in transit.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>okx</category>
      <category>crypto</category>
      <category>blockchain</category>
      <category>trading</category>
    </item>
  </channel>
</rss>
