<?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>Crypto Forem</title>
    <description>The most recent home feed on Crypto Forem.</description>
    <link>https://crypto.forem.com</link>
    <atom:link rel="self" type="application/rss+xml" href="https://crypto.forem.com/feed"/>
    <language>en</language>
    <item>
      <title>Anonymous Membership Proofs on Midnight: Building Privacy-Preserving Allowlists</title>
      <dc:creator>BossChaos</dc:creator>
      <pubDate>Sat, 02 May 2026 13:05:55 +0000</pubDate>
      <link>https://crypto.forem.com/bosschaos/anonymous-membership-proofs-on-midnight-building-privacy-preserving-allowlists-mge</link>
      <guid>https://crypto.forem.com/bosschaos/anonymous-membership-proofs-on-midnight-building-privacy-preserving-allowlists-mge</guid>
      <description>&lt;h1&gt;
  
  
  Anonymous Membership Proofs on Midnight: Building Privacy-Preserving Allowlists
&lt;/h1&gt;

&lt;p&gt;Last month, I was tasked with building an allowlist system for a Midnight dApp. The requirement seemed simple: let authorized users access a feature without revealing who they are. In the clear-text world, you'd just check &lt;code&gt;if (user in allowedList)&lt;/code&gt;. But on a privacy platform, that &lt;code&gt;if&lt;/code&gt; statement leaks everything.&lt;/p&gt;

&lt;p&gt;This tutorial walks through building a complete anonymous membership proof system — from the Compact contract on-chain to the TypeScript tooling that generates Merkle proofs locally. We'll cover sparse Merkle trees, depth-20 path verification, nullifier-based replay prevention, and admin root management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Merkle Trees for Allowlists?
&lt;/h2&gt;

&lt;p&gt;Traditional allowlists publish every member's address on-chain. That's fine for transparency, but terrible for privacy. A Merkle tree solves this differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Off-chain&lt;/strong&gt;: The admin maintains a list of member secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-chain&lt;/strong&gt;: Only a single 32-byte hash (the Merkle root) is stored&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof&lt;/strong&gt;: A member proves they know a secret that hashes to a leaf in the tree, without revealing which leaf
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Root (on-chain)
                   /    \
                 H01    H23
                /  \    /  \
               H0  H1  H2  H3
              / \  / \ / \ / \
             L0 L1 L2 L3 ...  (2^20 leaves)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To prove you're L1, you provide H0, H23, and the path indices. The verifier recomputes the root and checks it matches the on-chain value. Your secret (L1's preimage) stays private.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compact Contract
&lt;/h2&gt;

&lt;p&gt;The contract manages three pieces of state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export ledger merkle_root: Bytes&amp;lt;32&amp;gt;;
export ledger admin_commitment: Bytes&amp;lt;32&amp;gt;;
export ledger used_nullifiers: Set&amp;lt;Bytes&amp;lt;32&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Witnesses (Secret Inputs)
&lt;/h3&gt;

&lt;p&gt;These are the prover-side inputs that never appear on-chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;witness getSecret(): Bytes&amp;lt;32&amp;gt;;
witness getContext(): Bytes&amp;lt;32&amp;gt;;
witness getSiblings(): Vector&amp;lt;20, Bytes&amp;lt;32&amp;gt;&amp;gt;;
witness getPathIndices(): Vector&amp;lt;20, Boolean&amp;gt;;
witness getAdminSecret(): Bytes&amp;lt;32&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Recomputing the Merkle Path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;circuit hashLevelNode(is_right: Boolean, current: Bytes&amp;lt;32&amp;gt;, sibling: Bytes&amp;lt;32&amp;gt;): Bytes&amp;lt;32&amp;gt; {
  if (is_right) {
    return persistentHash&amp;lt;Vector&amp;lt;3, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
      pad(32, "zk-allowlist:node:v1"),
      sibling,
      current
    ]);
  } else {
    return persistentHash&amp;lt;Vector&amp;lt;3, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([
      pad(32, "zk-allowlist:node:v1"),
      current,
      sibling
    ]);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Checking Membership
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;circuit isMember(): (Bytes&amp;lt;32&amp;gt;, Bytes&amp;lt;32&amp;gt;) {
  let secret = getSecret();
  let context = getContext();
  let leaf = poseidonHash(secret);
  let computed_root = leaf;
  let siblings = getSiblings();
  let indices = getPathIndices();

  for (i in 0..20) {
    computed_root = hashLevelNode(indices[i], computed_root, siblings[i]);
  }

  assert(computed_root == merkle_root.read(), "Invalid membership proof");

  let nullifier = persistentHash&amp;lt;Vector&amp;lt;2, Bytes&amp;lt;32&amp;gt;&amp;gt;&amp;gt;([secret, context]);
  assert(not used_nullifiers.contains(nullifier), "Nullifier already used");

  (computed_root, nullifier)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Admin Root Management
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export circuit setRoot(new_root: Bytes&amp;lt;32&amp;gt;): [] {
  let admin_secret = getAdminSecret();
  let commitment = poseidonHash(admin_secret);
  assert(commitment == admin_commitment.read(), "Not authorized");
  merkle_root.write(disclose(new_root));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The TypeScript Tooling
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sparse Merkle Tree Implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MerkleTree&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;leaves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HashHex&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="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HashHex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;zeroHashes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HashHex&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zeroHashes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeZeroHashes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;insertLeaf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leafHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HashHex&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leafIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leaves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leaves&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leafHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;leafIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;level&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parentIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leftChild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentIndex&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rightChild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentIndex&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parentHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;leftChild&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rightChild&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parentHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;currentIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parentIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;leafIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Complete Flow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Admin Sets Up the Contract
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ADMIN_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ADMIN_COMMITMENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$ADMIN_SECRET&lt;/span&gt; | poseidon-hash&lt;span class="si"&gt;)&lt;/span&gt;
compact deploy &lt;span class="nt"&gt;--ledger&lt;/span&gt; &lt;span class="nv"&gt;admin_commitment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$ADMIN_COMMITMENT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Add Members Off-Chain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;midnight-allowlist add-member &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"alice-secret-123"&lt;/span&gt;
&lt;span class="nv"&gt;ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;midnight-allowlist get-root&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Push Root On-Chain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;compact call setRoot &lt;span class="nt"&gt;--arg&lt;/span&gt; &lt;span class="nv"&gt;new_root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt; &lt;span class="nt"&gt;--witness&lt;/span&gt; &lt;span class="nv"&gt;admin_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$ADMIN_SECRET&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Member Generates and Submits Proof
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PROOF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;midnight-allowlist generate-proof &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="s2"&gt;"alice-secret-123"&lt;/span&gt; &lt;span class="nt"&gt;--context&lt;/span&gt; &lt;span class="s2"&gt;"voting-round-1"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
compact call proveMembership &lt;span class="nt"&gt;--proof&lt;/span&gt; &lt;span class="nv"&gt;$PROOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Edge Cases and Gotchas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Zero Hash Collisions
&lt;/h3&gt;

&lt;p&gt;The sparse tree uses pre-computed zero hashes. Make sure your &lt;code&gt;computeZeroHashes&lt;/code&gt; function matches exactly what the Compact contract expects.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Context Binding
&lt;/h3&gt;

&lt;p&gt;The nullifier is &lt;code&gt;hash(secret || context)&lt;/code&gt;. Use distinct contexts for different operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VOTE_CONTEXT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;governance-vote-q2-2026&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AIRDROP_CONTEXT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token-airdrop-genesis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Tree Capacity Planning
&lt;/h3&gt;

&lt;p&gt;A depth-20 tree supports ~1M members. Each additional level doubles capacity but increases proof generation time linearly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ZK Allowlist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should verify valid membership proof&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MerkleTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertLeaf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hashLeaf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alice-secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateMerkleProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;verifyProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;hashLeaf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alice-secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This system handles the core membership proof flow. Production deployments should consider batch root updates, Merkle tree snapshots, circuit optimization, and frontend integration.&lt;/p&gt;

&lt;p&gt;The complete source code is available in the companion repository linked in the PR.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This tutorial is part of the Midnight Network bounty program. For more developer resources, visit &lt;a href="https://docs.midnight.network" rel="noopener noreferrer"&gt;docs.midnight.network&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>web3</category>
    </item>
    <item>
      <title>Building an AI-Powered Identity Manager</title>
      <dc:creator>Me Y</dc:creator>
      <pubDate>Sat, 02 May 2026 13:05:23 +0000</pubDate>
      <link>https://crypto.forem.com/me_y_fb1f776419302946b34e/building-an-ai-powered-identity-manager-2pcp</link>
      <guid>https://crypto.forem.com/me_y_fb1f776419302946b34e/building-an-ai-powered-identity-manager-2pcp</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is my submission for &lt;a href="https://dev.to/deved/build-apps-with-google-ai-studio"&gt;DEV Education Track: Build Apps with Google AI Studio&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built the AI Business Card Architect, a comprehensive identity management platform that goes beyond simple design. It utilizes the Imagen API to generate unique, minimalist brand logos based on company names.&lt;/p&gt;

&lt;p&gt;The app features a "Guest First" approach with an optional CS50-inspired local authentication system, allowing users to manage multiple profiles (Personal, Business, Family) each with its own independent vault. Key prompts focused on "Professional minimalist logo synthesis" and "Smart contrast accessibility logic" to ensure every generated card is print-ready and digitally accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ai-business-card-architect.vercel.app/" rel="noopener noreferrer"&gt;https://ai-business-card-architect.vercel.app/&lt;/a&gt;&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Flmqwc16cz93hsfxsqrnj.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.amazonaws.com%2Fuploads%2Farticles%2Flmqwc16cz93hsfxsqrnj.png" alt="Cards generated &amp;amp; Logo" width="800" height="459"&gt;&lt;/a&gt;&lt;br&gt;
Key Features Screenshots:&lt;/p&gt;

&lt;p&gt;AI Forge: Instant logo generation using Imagen.&lt;br&gt;
Multi-Profile Switcher: Managing different identities seamlessly.&lt;br&gt;
3-in-1 Bulk Archive: Exporting Standard, Centered, and Modern Split layouts in one click.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Experience
&lt;/h2&gt;

&lt;p&gt;Working through this track was an eye-opener regarding the synergy between Generative AI and traditional application logic.&lt;/p&gt;

&lt;p&gt;What I learned: I learned how to bridge the gap between "Guest" usage and "Authenticated" persistence without friction. Integrating the Imagen API taught me the importance of prompt engineering for consistent branding.&lt;/p&gt;

&lt;p&gt;What was surprising: The most surprising part was how effectively Gemini 3 Flash handled complex "Smart Contrast" algorithms, automatically flipping text colors to maintain readability based on the generated image's relative luminance. It felt less like coding and more like "Architecting" a brain for the app.&lt;/p&gt;

</description>
      <category>deved</category>
      <category>learngoogleaistudio</category>
      <category>ai</category>
      <category>gemini</category>
    </item>
    <item>
      <title>How React Works (Part 5)? The React Lifecycle From the Inside: When Things Actually Run</title>
      <dc:creator>Sam Abaasi</dc:creator>
      <pubDate>Sat, 02 May 2026 13:03:02 +0000</pubDate>
      <link>https://crypto.forem.com/samabaasi/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-oj4</link>
      <guid>https://crypto.forem.com/samabaasi/how-react-works-part-5-the-react-lifecycle-from-the-inside-when-things-actually-run-oj4</guid>
      <description>&lt;h2&gt;
  
  
  The React Lifecycle From the Inside: When Things Actually Run
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; How React Works Under the Hood&lt;br&gt;
&lt;strong&gt;Part 1:&lt;/strong&gt; &lt;a href="https://dev.to/samabaasi/how-react-works-part-1motivation-behind-react-fiber-time-slicing-suspense-4gf4"&gt;Motivation Behind React Fiber: Time Slicing &amp;amp; Suspense&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Part 2:&lt;/strong&gt; &lt;a href="https://dev.to/samabaasi/how-react-works-part-2-why-react-had-to-build-its-own-execution-engine-119k"&gt;Why React Had to Build Its Own Execution Engine&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Part 3:&lt;/strong&gt; &lt;a href="https://dev.to/samabaasi/how-react-works-part-3-how-react-finds-what-actually-changed-17nd"&gt;How React Finds What Actually Changed&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Part 4:&lt;/strong&gt; &lt;a href="https://dev.to/samabaasi/how-react-works-part-4-the-idea-that-makes-suspense-possible-4gok"&gt;The Idea That Makes Suspense Possible&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Prerequisites:&lt;/strong&gt; Read Parts 1–4 first.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  A Puzzle Most React Developers Get Wrong
&lt;/h2&gt;

&lt;p&gt;Quick quiz. What order do these log?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useLayoutEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;layout effect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;effect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most people guess: render → effect → layout effect, or render → layout effect → effect.&lt;/p&gt;

&lt;p&gt;The answer is: &lt;strong&gt;render → layout effect → effect.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But more importantly — &lt;em&gt;why?&lt;/em&gt; Why does &lt;code&gt;useLayoutEffect&lt;/code&gt; run before &lt;code&gt;useEffect&lt;/code&gt;? Why does &lt;code&gt;useEffect&lt;/code&gt; run at all after the component already returned? What does "after the browser paints" actually mean, and is that even always true?&lt;/p&gt;

&lt;p&gt;This article answers all of that. And by the end, you'll be able to look at any component and predict exactly when each piece of it runs — and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Phases of a React Render
&lt;/h2&gt;

&lt;p&gt;Before we get into effects, we need to remember the pipeline from Part 2. Every React update goes through three phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Render&lt;/strong&gt; — React runs your component functions, diffs the trees, figures out what changed. Nothing is written to the DOM yet. This is where &lt;code&gt;console.log('render')&lt;/code&gt; fires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit&lt;/strong&gt; — React takes everything it figured out during Render and applies it to the real DOM. This is synchronous and uninterruptible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Effects&lt;/strong&gt; — After the DOM is updated, React runs your effects.&lt;/p&gt;

&lt;p&gt;The key insight is that effects are &lt;em&gt;not&lt;/em&gt; part of rendering. They're a separate step that happens after the DOM is already updated. This is why you can safely read the DOM inside an effect — by the time it runs, the DOM reflects the current state.&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.amazonaws.com%2Fuploads%2Farticles%2F05tpi8uej7ozfaygozmz.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.amazonaws.com%2Fuploads%2Farticles%2F05tpi8uej7ozfaygozmz.png" alt="three phases — Render → Commit → Effects, with browser paint between Commit and Passive Effects" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What &lt;code&gt;useEffect&lt;/code&gt; Actually Is
&lt;/h2&gt;

&lt;p&gt;Here's the mental model most developers have: &lt;code&gt;useEffect&lt;/code&gt; is "code that runs after render." That's roughly right but missing the important details.&lt;/p&gt;

&lt;p&gt;Here's the more accurate model: &lt;strong&gt;&lt;code&gt;useEffect&lt;/code&gt; is a declaration that you have side effects to run after React is done with the DOM.&lt;/strong&gt; You're not scheduling a callback — you're telling React about work that needs to happen, and React decides when to run it.&lt;/p&gt;

&lt;p&gt;The distinction matters because React doesn't run effects immediately after commit. It &lt;em&gt;schedules&lt;/em&gt; them.&lt;/p&gt;

&lt;p&gt;From jser.dev's lifecycle article — here's what actually happens:&lt;/p&gt;

&lt;p&gt;After the Commit phase finishes updating the DOM, React schedules your &lt;code&gt;useEffect&lt;/code&gt; callbacks as a separate task in the Scheduler's queue — the same Scheduler we covered in Part 2. That means effects run in a &lt;strong&gt;new macro task&lt;/strong&gt;, after the browser has had a chance to paint the updated screen.&lt;/p&gt;

&lt;p&gt;This is why the React docs say &lt;code&gt;useEffect&lt;/code&gt; runs "after the browser paints." The sequence looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Render phase — your component functions run
2. Commit phase — DOM is updated
3. Browser paints the screen
4. Scheduler fires — useEffect callbacks run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser gets step 3 because steps 1-2 are one macro task, and step 4 is a new macro task. Between any two macro tasks, the browser can paint.&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.amazonaws.com%2Fuploads%2Farticles%2F6kpib2lhqyxsnu1ibbnz.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.amazonaws.com%2Fuploads%2Farticles%2F6kpib2lhqyxsnu1ibbnz.png" alt="macro task timeline — render+commit → browser paint → new macro task → useEffect runs" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cleanup: Why It Runs Before the Next Effect
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;useEffect&lt;/code&gt; can return a cleanup function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// cleanup&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;id&lt;/code&gt; changes and the effect needs to re-run, React runs the &lt;em&gt;cleanup of the previous effect first&lt;/em&gt;, then runs the new effect. Always in that order.&lt;/p&gt;

&lt;p&gt;This is also true on unmount — the cleanup runs when the component leaves the tree.&lt;/p&gt;

&lt;p&gt;The reason is straightforward: React never wants two instances of the same effect active at the same time. Before setting up the new subscription, it tears down the old one. This is React being deliberate about side effects — always clean up before you set up again.&lt;/p&gt;

&lt;p&gt;From jser.dev's effect lifecycle article: cleanups run first in &lt;code&gt;commitPassiveUnmountEffects&lt;/code&gt;, then new effects run in &lt;code&gt;commitPassiveMountEffects&lt;/code&gt;. Both happen in the same scheduled task, in the same order as the tree (children before parents, same as &lt;code&gt;completeWork&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;useLayoutEffect&lt;/code&gt;: The Synchronous Version
&lt;/h2&gt;

&lt;p&gt;Here's the critical difference: &lt;code&gt;useLayoutEffect&lt;/code&gt; runs &lt;strong&gt;synchronously inside the Commit phase&lt;/strong&gt;, before the browser paints.&lt;/p&gt;

&lt;p&gt;The sequence with &lt;code&gt;useLayoutEffect&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Render phase — component functions run
2. Commit phase — DOM is updated
3. useLayoutEffect callbacks run ← here, synchronously, before paint
4. Browser paints
5. useEffect callbacks run ← here, in next macro task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why &lt;code&gt;useLayoutEffect&lt;/code&gt; fires before &lt;code&gt;useEffect&lt;/code&gt; in our opening quiz — it's not "earlier in the same phase," it's in a completely different phase.&lt;/p&gt;

&lt;p&gt;And this is why &lt;code&gt;useLayoutEffect&lt;/code&gt; exists at all. If you need to &lt;strong&gt;read the DOM after it's updated but before the user sees it&lt;/strong&gt; — for example, measuring an element's size to position a tooltip — &lt;code&gt;useLayoutEffect&lt;/code&gt; is the only place where that's safe and accurate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// useLayoutEffect — correct for DOM measurements&lt;/span&gt;
&lt;span class="nf"&gt;useLayoutEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;setTooltipPosition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// useEffect — would cause a visible flicker for measurements&lt;/span&gt;
&lt;span class="c1"&gt;// because the browser already painted before this runs&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;setTooltipPosition&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// too late&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use &lt;code&gt;useEffect&lt;/code&gt; for a DOM measurement that affects layout, the user will briefly see the wrong layout (before &lt;code&gt;useEffect&lt;/code&gt; runs), then see it jump to the correct layout (after). That flicker is exactly what &lt;code&gt;useLayoutEffect&lt;/code&gt; prevents.&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.amazonaws.com%2Fuploads%2Farticles%2Fgtzmw529c4p9o93d3kt7.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.amazonaws.com%2Fuploads%2Farticles%2Fgtzmw529c4p9o93d3kt7.png" alt="useLayoutEffect vs useEffect — same DOM, different timing relative to browser paint" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The "After Paint" Rule Has an Exception
&lt;/h2&gt;

&lt;p&gt;Here's something jser.dev discovered that React's own documentation gets wrong.&lt;/p&gt;

&lt;p&gt;The docs say &lt;code&gt;useEffect&lt;/code&gt; always runs after the browser paints. But that's not strictly true.&lt;/p&gt;

&lt;p&gt;Sometimes React runs &lt;code&gt;useEffect&lt;/code&gt; callbacks &lt;em&gt;before&lt;/em&gt; paint. This happens when React determines it's more important to show the latest UI as quickly as possible — for example, when a re-render is triggered by a user interaction, or when effects are scheduled under layout effects. In those cases, React won't wait for a paint before running the effects.&lt;/p&gt;

&lt;p&gt;From jser.dev's paint timing article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I'd explain the timing of running useEffect() callbacks as follows. useEffect() callbacks are run after DOM mutation is done. Most of the time, they are run asynchronously after paint, but React might run them synchronously before paint when it is more important to show the latest UI — for example when re-render is caused by user interactions or scheduled under layout effects, or simply if React has the time internally to do so."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The practical implication: never rely on &lt;code&gt;useEffect&lt;/code&gt; to run after paint for correctness. If your code requires running after the browser paints, &lt;code&gt;useLayoutEffect&lt;/code&gt; with its explicit synchronous-before-paint guarantee is actually the more predictable choice. And if you truly need to run something after paint for non-DOM reasons, the gap is usually small enough not to matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dependency Array: What It Actually Controls
&lt;/h2&gt;

&lt;p&gt;The dependency array in &lt;code&gt;useEffect&lt;/code&gt; and &lt;code&gt;useLayoutEffect&lt;/code&gt; doesn't control &lt;em&gt;whether&lt;/em&gt; the effect runs — it controls &lt;em&gt;when&lt;/em&gt; it re-runs.&lt;/p&gt;

&lt;p&gt;React compares the current values of the dependency array to the previous ones using &lt;code&gt;Object.is&lt;/code&gt; (similar to &lt;code&gt;===&lt;/code&gt; but handles &lt;code&gt;NaN&lt;/code&gt; and &lt;code&gt;-0&lt;/code&gt; correctly). If any value changed, React marks the effect with a flag indicating it should run again. If nothing changed, the effect is skipped this cycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// only re-runs when id changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No dependency array&lt;/strong&gt; — re-runs after every render. The effect has no conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty array &lt;code&gt;[]&lt;/code&gt;&lt;/strong&gt; — runs once on mount, cleanup runs on unmount. Effect has no dependencies that can change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Array with values&lt;/strong&gt; — re-runs whenever any listed value changes between renders.&lt;/p&gt;

&lt;p&gt;The important thing to understand: React doesn't track &lt;em&gt;what&lt;/em&gt; you use inside the effect. It trusts the dependency array you provide. If you use &lt;code&gt;userId&lt;/code&gt; inside the effect but forget to put it in the array, React won't re-run the effect when &lt;code&gt;userId&lt;/code&gt; changes — and you get a stale value bug. This is why &lt;code&gt;eslint-plugin-react-hooks&lt;/code&gt; exists: it statically analyzes your effect and warns when the array is incomplete.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Lifecycle in One Timeline
&lt;/h2&gt;

&lt;p&gt;The pattern is consistent across every scenario. On mount, the component renders, React commits the DOM, &lt;code&gt;useLayoutEffect&lt;/code&gt; fires synchronously before the browser paints, the browser paints, then &lt;code&gt;useEffect&lt;/code&gt; runs in the next macro task. On update, cleanups always run before new effects — layout cleanup first, then layout create, then paint, then passive cleanup, then passive create. On unmount, both cleanups run in the same order, layout before passive, with paint in between.&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.amazonaws.com%2Fuploads%2Farticles%2Fp8ldjd8e0nuyeycevq2o.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.amazonaws.com%2Fuploads%2Farticles%2Fp8ldjd8e0nuyeycevq2o.png" alt="full lifecycle — mount / update / unmount with all effect timings" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things are always true regardless of the scenario: layout effects are synchronous and pre-paint, passive effects are scheduled and post-commit. And cleanups always run before the next effect of the same type — React never has two instances of the same effect active at once.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use Which
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;useEffect&lt;/code&gt; for everything by default. Most side effects — data fetching, subscriptions, logging, timers — don't need to happen before the browser paints. Running them after paint keeps the UI responsive and is the right choice in the vast majority of cases.&lt;/p&gt;

&lt;p&gt;Reach for &lt;code&gt;useLayoutEffect&lt;/code&gt; only when you need to read or modify the DOM before the user sees it — measuring element dimensions, positioning a tooltip relative to another element, or preventing a visual flicker. The practical test is simple: if swapping &lt;code&gt;useLayoutEffect&lt;/code&gt; for &lt;code&gt;useEffect&lt;/code&gt; causes a visible jump or flash in the UI, you needed &lt;code&gt;useLayoutEffect&lt;/code&gt;. If it looks identical, stick with &lt;code&gt;useEffect&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Coming in Part 6
&lt;/h2&gt;

&lt;p&gt;In Part 6 we go inside &lt;code&gt;useState&lt;/code&gt; — how state is stored on the Fiber, what happens when you call &lt;code&gt;setState&lt;/code&gt;, and why calling &lt;code&gt;setState&lt;/code&gt; multiple times in one event handler doesn't cause multiple re-renders.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎬 Watch These
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JSer (jser.dev) — &lt;a href="https://www.youtube.com/watch?v=Ggmdo7TORNc&amp;amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;amp;index=16" rel="noopener noreferrer"&gt;The lifecycle of effect hooks in React&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
The source for the Effect object internals, &lt;code&gt;HookHasEffect&lt;/code&gt; tag, &lt;code&gt;flushPassiveEffects&lt;/code&gt;, and cleanup ordering in this article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSer (jser.dev) — &lt;a href="https://www.youtube.com/watch?v=6HLvyiYv7HI&amp;amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;amp;index=10" rel="noopener noreferrer"&gt;How does useLayoutEffect() work internally?&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
The synchronous commit-phase timing of layout effects vs passive effects — the source for the timing difference explained here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSer (jser.dev) — &lt;a href="https://www.youtube.com/watch?v=2iXKUJpnALU&amp;amp;list=PLvx8w9g4qv_p-OS-XdbB3Ux_6DMXhAJC3&amp;amp;index=35" rel="noopener noreferrer"&gt;When do useEffect() callbacks get run? Before paint or after paint?&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
The surprising finding that React.dev's "always after paint" description is inaccurate — and the real timing rules.&lt;/p&gt;




&lt;h2&gt;
  
  
  🙏 Sources &amp;amp; Thanks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://jser.dev" rel="noopener noreferrer"&gt;jser.dev&lt;/a&gt;&lt;/strong&gt; — all timing mechanics in this article come from JSer's source-level analysis. Articles used: &lt;a href="https://jser.dev/react/2022/01/19/lifecycle-of-effect-hook/" rel="noopener noreferrer"&gt;The lifecycle of effect hooks in React&lt;/a&gt;, &lt;a href="https://jser.dev/react/2021/12/04/how-does-useLayoutEffect-work/" rel="noopener noreferrer"&gt;How does useLayoutEffect() work internally?&lt;/a&gt;, &lt;a href="https://jser.dev/2023-07-08-how-does-useeffect-work/" rel="noopener noreferrer"&gt;How does useEffect() work internally in React?&lt;/a&gt;, and &lt;a href="https://jser.dev/2023-08-09-effects-run-paint/" rel="noopener noreferrer"&gt;When do useEffect() callbacks get run?&lt;/a&gt; — including the quote about React.dev's inaccurate description.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;React source&lt;/strong&gt; — &lt;code&gt;ReactFiberCommitWork.js&lt;/code&gt; (&lt;code&gt;commitLayoutEffects&lt;/code&gt;, &lt;code&gt;commitPassiveMountEffects&lt;/code&gt;, &lt;code&gt;commitPassiveUnmountEffects&lt;/code&gt;) and &lt;code&gt;ReactFiberWorkLoop.js&lt;/code&gt; (&lt;code&gt;scheduleCallback&lt;/code&gt; for passive effects).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.lydiahallie.io/" rel="noopener noreferrer"&gt;Lydia Hallie&lt;/a&gt;&lt;/strong&gt; — for JavaScript visualizations that shaped this series' style.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Part 6 is next — how &lt;code&gt;useState&lt;/code&gt; actually works: where state lives, what happens when you call &lt;code&gt;setState&lt;/code&gt;, and why multiple calls in one handler don't cause multiple re-renders. 🔧&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;#react&lt;/code&gt; &lt;code&gt;#javascript&lt;/code&gt; &lt;code&gt;#webdev&lt;/code&gt; &lt;code&gt;#tutorial&lt;/code&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>hooks</category>
      <category>useeffect</category>
    </item>
    <item>
      <title>TestSprite — Panduan Cepat (Indonesian Translation)</title>
      <dc:creator>placecel427-source</dc:creator>
      <pubDate>Sat, 02 May 2026 12:54:44 +0000</pubDate>
      <link>https://crypto.forem.com/placecel427source/testsprite-panduan-cepat-indonesian-translation-119b</link>
      <guid>https://crypto.forem.com/placecel427source/testsprite-panduan-cepat-indonesian-translation-119b</guid>
      <description>&lt;h1&gt;
  
  
  TestSprite — Panduan Cepat
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Apa itu TestSprite?
&lt;/h2&gt;

&lt;p&gt;TestSprite adalah platform otomasi testing terpadu untuk aplikasi web modern. Dengan menganalisis UI Anda secara real-time, TestSprite secara otomatis menghasilkan test cases integrasi yang komprehensif dan memeliharanya seiring perubahan aplikasi Anda.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Masalah yang dipecahkan:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test sulit ditingkatkan saat UI berubah&lt;/li&gt;
&lt;li&gt;Tim QA menghabiskan jutaan jam menulis test yang sama berulang kali&lt;/li&gt;
&lt;li&gt;Test regresi sering gagal karena selector yang tidak valid&lt;/li&gt;
&lt;li&gt;Onboarding engineer testing baru memakan waktu berminggu-minggu&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TestSprite menghilangkan 80% dari pekerjaan manual tersebut dengan AI yang memahami aplikasi Anda.&lt;/p&gt;




&lt;h2&gt;
  
  
  Instalasi
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Daftar di TestSprite
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Kunjungi dashboard&lt;/span&gt;
https://app.testsprite.com/signup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buat akun dengan email kerja Anda. Verifikasi email, selesai.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hubungkan Aplikasi Anda
&lt;/h3&gt;

&lt;p&gt;Di dashboard TestSprite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Klik &lt;strong&gt;"Add Project"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Masukkan URL aplikasi web Anda (misal: &lt;code&gt;https://myapp.local:3000&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;TestSprite akan scan UI Anda
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Optional: Tambahkan snippet ini ke aplikasi Anda untuk integrasi lebih dalam&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.testsprite.com/v1/agent.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;TestSprite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_PROJECT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Tunggu Analisis AI
&lt;/h3&gt;

&lt;p&gt;TestSprite memindai aplikasi Anda selama 2-5 menit, memetakan setiap halaman, form, tombol, dan interaksi. Ini adalah satu-satunya setup yang Anda butuhkan.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test Pertama Anda
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Menghasilkan Test Cases Otomatis
&lt;/h3&gt;

&lt;p&gt;Setelah analisis selesai:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Buka tab &lt;strong&gt;"Generated Tests"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Lihat test suite yang dibuat AI (sudah terstruktur)&lt;/li&gt;
&lt;li&gt;Klik &lt;strong&gt;"Run All Tests"&lt;/strong&gt; untuk validasi awal
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test akan dijalankan di berbagai browser:&lt;/span&gt;
&lt;span class="c"&gt;# - Chrome (latest)&lt;/span&gt;
&lt;span class="c"&gt;# - Firefox (latest)&lt;/span&gt;
&lt;span class="c"&gt;# - Safari (latest)&lt;/span&gt;
&lt;span class="c"&gt;# - Edge (latest)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hasil Test
&lt;/h3&gt;

&lt;p&gt;Setiap test report menampilkan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Passed: X test&lt;/li&gt;
&lt;li&gt;❌ Failed: Y test (dengan screenshot)&lt;/li&gt;
&lt;li&gt;⏱️ Waktu eksekusi total&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Jika ada kegagalan, TestSprite langsung menunjukkan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Element dengan selector ".submit-btn" tidak ditemukan
Saran: Gunakan selector alternatif "button[type="submit"]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Merawat Test Saat UI Berubah
&lt;/h2&gt;

&lt;p&gt;Ini adalah "magic" TestSprite. Ketika Anda update UI:&lt;/p&gt;

&lt;h3&gt;
  
  
  Skenario: Anda mengganti &lt;code&gt;.primary-button&lt;/code&gt; menjadi &lt;code&gt;[data-testid="primary-btn"]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Tanpa TestSprite&lt;/strong&gt; (cara lama):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test gagal ❌&lt;/li&gt;
&lt;li&gt;Engineer membuka setiap test file&lt;/li&gt;
&lt;li&gt;Mencari &lt;code&gt;.primary-button&lt;/code&gt; di 50+ test&lt;/li&gt;
&lt;li&gt;Update secara manual&lt;/li&gt;
&lt;li&gt;30 menit yang terbuang&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Dengan TestSprite&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test mulai gagal&lt;/li&gt;
&lt;li&gt;TestSprite mendeteksi perubahan selector&lt;/li&gt;
&lt;li&gt;Secara otomatis menemukan selector baru&lt;/li&gt;
&lt;li&gt;Test berjalan lagi ✅&lt;/li&gt;
&lt;li&gt;Anda tidak perlu berbuat apa-apa&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Proses ini disebut &lt;strong&gt;"Self-Healing Tests"&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mengintegrasikan dengan CI/CD
&lt;/h2&gt;

&lt;p&gt;TestSprite bekerja dengan pipeline Anda:&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run TestSprite Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start Application&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm start &amp;amp;&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run TestSprite Tests&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testsprite/action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;project-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TESTSPRITE_PROJECT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TESTSPRITE_API_KEY }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Report&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testsprite-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./testsprite-report.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GitLab CI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;

&lt;span class="na"&gt;testsprite&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testsprite/runner:latest&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;testsprite run --project-id $TESTSPRITE_PROJECT_ID --api-key $TESTSPRITE_API_KEY&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;testsprite-report.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Jenkins
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;stages&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"TestSprite"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"""
          docker run --rm \
            -e TESTSPRITE_PROJECT_ID=$TESTSPRITE_PROJECT_ID \
            -e TESTSPRITE_API_KEY=$TESTSPRITE_API_KEY \
            testsprite/runner:latest \
            testsprite run
        """&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Kategori Test yang Didukung
&lt;/h2&gt;

&lt;p&gt;TestSprite secara otomatis membuat test untuk:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Form Testing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Validasi input (field required, tipe data)
✓ Submit form dengan berbagai kombinasi nilai
✓ Handling error message
✓ Reset form
✓ Penyimpanan draft otomatis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Navigation Testing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Link internal (routing)
✓ Breadcrumb navigation
✓ Menu dropdown
✓ Tab switching
✓ Pagination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. User Interaction Testing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Click events
✓ Hover effects
✓ Keyboard shortcuts
✓ Scroll behavior
✓ Modal/dialog handling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Data Presentation Testing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Table rendering (sorting, filtering)
✓ List pagination
✓ Search functionality
✓ Data formatting
✓ Empty state handling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. API Integration Testing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ POST/GET/PUT/DELETE endpoints
✓ Error handling (404, 500, timeout)
✓ Data mismatch detection
✓ Rate limiting behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Contoh Test Output
&lt;/h2&gt;

&lt;p&gt;Setelah menjalankan test, Anda melihat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📊 TestSprite Test Report
══════════════════════════════════════════

Project: My E-Commerce App
Run Date: 2026-05-01 14:35:12 UTC
Duration: 4m 23s

Summary:
  ✅ Passed: 127
  ❌ Failed: 3
  ⏭️  Skipped: 2

Pass Rate: 97.7% ✓

Failed Tests:
─────────────────────────────────────────

1. [FAILED] Login form dengan email invalid
   Expected: Error message muncul
   Actual: Tombol submit tetap aktif
   Screenshot: https://testsprite.com/report/screenshot/1

2. [FAILED] Cart update quantity
   Expected: Total harga update
   Actual: Total harga tetap sama
   Diff: -$15 (expected)

3. [FAILED] Mobile responsive - navigation menu
   Expected: Menu hamburger muncul di width 480px
   Actual: Menu hamburger tidak visible
   Device: iPhone 12 Mini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Konfigurasi Lanjutan
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Custom Test Parameters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"testConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"browsers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"chrome"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"firefox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"safari"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"devices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"desktop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tablet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mobile"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"retryFailedTests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parallelism"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"screenshotOnFailure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"videoRecording"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"apiBaseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.myapp.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"excludePatterns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"/admin/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"/internal/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Setup untuk berbagai environment&lt;/span&gt;

&lt;span class="c"&gt;# Development&lt;/span&gt;
&lt;span class="nv"&gt;TESTSPRITE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev
&lt;span class="nv"&gt;TESTSPRITE_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3000

&lt;span class="c"&gt;# Staging&lt;/span&gt;
&lt;span class="nv"&gt;TESTSPRITE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging
&lt;span class="nv"&gt;TESTSPRITE_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://staging.myapp.com

&lt;span class="c"&gt;# Production&lt;/span&gt;
&lt;span class="nv"&gt;TESTSPRITE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod
&lt;span class="nv"&gt;TESTSPRITE_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://myapp.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Tips &amp;amp; Trik
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Jangan Manual Test Lagi
&lt;/h3&gt;

&lt;p&gt;Setelah TestSprite setup, batalkan kebiasaan manual testing. AI lebih cepat dan konsisten.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Update Kode dengan Percaya Diri
&lt;/h3&gt;

&lt;p&gt;Refactor tanpa takut memecah sesuatu. TestSprite akan memberi tahu Anda dalam menit, bukan jam.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Gunakan untuk Regression Sebelum Deploy
&lt;/h3&gt;

&lt;p&gt;Sebelum production release, biarkan TestSprite memvalidasi seluruh aplikasi dalam 5 menit.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Monitor Test Trends
&lt;/h3&gt;

&lt;p&gt;Dashboard menunjukkan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass rate over time&lt;/li&gt;
&lt;li&gt;Test execution time trends&lt;/li&gt;
&lt;li&gt;Most flaky tests&lt;/li&gt;
&lt;li&gt;Coverage by feature&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Bagikan Laporan dengan Stakeholder
&lt;/h3&gt;

&lt;p&gt;Export report sebagai HTML/PDF dan kirim ke manager/product owner. Mereka ingin bukti bahwa kualitas terjaga.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  Issue: "Application tidak bisa diakses dari TestSprite runner"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solusi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Jika app berjalan locally, expose dengan ngrok&lt;/span&gt;
ngrok http 3000

&lt;span class="c"&gt;# Lalu setup TestSprite dengan URL ngrok&lt;/span&gt;
https://abcd1234.ngrok.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: "Test sering timeout"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Penyebab:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API terlalu lambat&lt;/li&gt;
&lt;li&gt;Aplikasi hang saat startup&lt;/li&gt;
&lt;li&gt;Network unstable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Solusi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Naikkan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dari&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="err"&gt;s&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apiWaitTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: "TestSprite tidak mendeteksi elemen dynamic (lazy-loaded)"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solusi:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Tambahkan indikator untuk TestSprite&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;testsprite&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-loaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Dynamic content here */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TestSprite akan tunggu hingga elemen ini render sebelum test mulai.&lt;/p&gt;




&lt;h2&gt;
  
  
  Langkah Selanjutnya
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;✅ Daftar di &lt;a href="https://app.testsprite.com" rel="noopener noreferrer"&gt;https://app.testsprite.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✅ Setup project Anda&lt;/li&gt;
&lt;li&gt;✅ Jalankan test pertama&lt;/li&gt;
&lt;li&gt;✅ Integrasikan dengan CI/CD&lt;/li&gt;
&lt;li&gt;✅ Monitor dashboard setiap hari&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Dukungan
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📧 Email: &lt;a href="mailto:support@testsprite.com"&gt;support@testsprite.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 Chat: app.testsprite.com/chat&lt;/li&gt;
&lt;li&gt;📖 Docs: &lt;a href="https://docs.testsprite.com" rel="noopener noreferrer"&gt;https://docs.testsprite.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐛 Bug Report: &lt;a href="https://github.com/testsprite/issues" rel="noopener noreferrer"&gt;https://github.com/testsprite/issues&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Selamat testing! 🚀&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Also available on GitHub Gist:&lt;/strong&gt; &lt;a href="https://gist.github.com/placecel427-source/ec857abad852411161ad34d1c0b0f68d" rel="noopener noreferrer"&gt;https://gist.github.com/placecel427-source/ec857abad852411161ad34d1c0b0f68d&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>devops</category>
      <category>indonesian</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cursor Composer 2: The Cache Economy Behind a 10x Cheaper Coding Agent</title>
      <dc:creator>Hiroshi Toyama</dc:creator>
      <pubDate>Sat, 02 May 2026 12:53:01 +0000</pubDate>
      <link>https://crypto.forem.com/toyama0919/cursor-composer-2-the-cache-economy-behind-a-10x-cheaper-coding-agent-15cj</link>
      <guid>https://crypto.forem.com/toyama0919/cursor-composer-2-the-cache-economy-behind-a-10x-cheaper-coding-agent-15cj</guid>
      <description>&lt;p&gt;Cursor's Composer 2 shipped in March 2026 as the centerpiece of the Cursor 2.0 overhaul. The headline numbers—$0.50/1M input tokens, outperforming frontier models on SWE-bench Multilingual—look like marketing. The cache read mechanism is where the real story is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Specialized Model at All
&lt;/h2&gt;

&lt;p&gt;Prior Cursor versions proxied Claude or GPT-4. Composer 2 is trained exclusively on coding data via continued pre-training and reinforcement learning. The obvious question is: what's cut?&lt;/p&gt;

&lt;p&gt;Everything that isn't code. Composer 2 has no meaningful capability for poetry, history, ethics debates, or anything outside software development. That constraint lets Anysphere run a model that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understands intra-repo dependency graphs (if you fix A, B also needs updating)&lt;/li&gt;
&lt;li&gt;Navigates hundreds of files in a single long-horizon task&lt;/li&gt;
&lt;li&gt;Runs natively in sandboxed terminals and a built-in browser loop&lt;/li&gt;
&lt;li&gt;Costs a fraction of what a general-purpose frontier model costs to serve&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pricing reflects this. As of May 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input (1M tokens)&lt;/th&gt;
&lt;th&gt;Output (1M tokens)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Composer 2 Standard&lt;/td&gt;
&lt;td&gt;$0.50&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composer 2 Fast&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;td&gt;$7.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude 4.6 Opus&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;$25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Standard vs Fast: Same Weights, Different Queue
&lt;/h2&gt;

&lt;p&gt;Anysphere's own language is unambiguous: "Same intelligence." The two variants share identical model weights and parameters. Fast gets priority queue on high-end GPUs (H800/B200 class); Standard runs on lower-priority compute with higher latency tolerance.&lt;/p&gt;

&lt;p&gt;This is a deliberate architectural choice. Inference cost scales with compute priority, not model capability. If you can tolerate a 10–30 second response delay, you get the same output for 1/3 the price.&lt;/p&gt;

&lt;p&gt;The practical split that Cursor power users have settled on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interactive sessions (Fast):&lt;/strong&gt; You're watching the output in real time. Latency kills flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fire-and-forget tasks (Standard):&lt;/strong&gt; Refactor 100 test files, generate JSDoc across the repo, migrate an entire API surface. Start it, close the laptop, come back to results.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Cache Read Economy
&lt;/h2&gt;

&lt;p&gt;This is the mechanism that makes Standard compelling for large codebases.&lt;/p&gt;

&lt;p&gt;Every request to Composer 2 sends context: directory structure, recently opened files, conversation history. On the second, fifth, tenth turn of the same session, the majority of that context is identical to what was already sent. That's the cache.&lt;/p&gt;

&lt;p&gt;Cache read rates as of May 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;New input&lt;/th&gt;
&lt;th&gt;Cache read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;$0.50/1M&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.20/1M&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;$1.50/1M&lt;/td&gt;
&lt;td&gt;$0.35/1M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By turn 5 of a non-trivial session, 80%+ of your input tokens are cache reads, not fresh input. Standard's cache read rate ($0.20) is 43% cheaper than Fast's ($0.35), and 60% cheaper than Standard's own new input rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concrete impact:&lt;/strong&gt; A refactoring session with 10 back-and-forth turns on a large codebase might consume 10M tokens. With Standard and healthy cache hits, that lands around $1.50–$2.00. The same session on Fast: $4.00–$5.00. On Claude 4.6 Opus: potentially $20+.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cache Bug (March–April 2026)
&lt;/h2&gt;

&lt;p&gt;The cache story has a footnote worth documenting.&lt;/p&gt;

&lt;p&gt;From late March through early April 2026, a backend bug caused Composer 2 Standard to emit cache read counts of zero—every request treated as fresh input at $0.50/1M even when the context was identical to the previous turn. Users reported credit burn rates 10x higher than expected. The irony: switching to Fast (which costs 3x more per token) actually resulted in lower total cost because cache was functioning there.&lt;/p&gt;

&lt;p&gt;Cursor's team (Dean and Mohit on the forum thread) acknowledged the bug and pushed a fix around April 7. As of v2.1.116+, the behavior appears stable.&lt;/p&gt;

&lt;p&gt;The diagnostic check: open &lt;code&gt;cursor.com/settings&lt;/code&gt; → Usage. If &lt;code&gt;Cache Read&lt;/code&gt; tokens are consistently below 40% on a multi-turn session against the same codebase, something is wrong. Expected range is 40–90% depending on how varied your requests are.&lt;/p&gt;

&lt;p&gt;If you hit zero cache read consistently, copy the Request ID from the chat header and contact support. Cursor has been issuing credit refunds for the overbilling period.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing with Claude Code's Cache
&lt;/h2&gt;

&lt;p&gt;Claude Code (Anthropic's CLI tool) has its own prompt caching via &lt;code&gt;cache_control&lt;/code&gt; markers, but with a key structural difference: TTL.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Write cost&lt;/th&gt;
&lt;th&gt;Read cost&lt;/th&gt;
&lt;th&gt;TTL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;td&gt;1.25× input&lt;/td&gt;
&lt;td&gt;~10% of input&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ENABLE_PROMPT_CACHING_1H=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.0× input&lt;/td&gt;
&lt;td&gt;~10% of input&lt;/td&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 5-minute default is brutal for any session where you read documentation, test code, or think between turns. The 1-hour option (available since Claude Code v2.1.108) adds to the write cost but eliminates repeated cache misses across the kind of natural pauses that happen in real work.&lt;/p&gt;

&lt;p&gt;To enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.zshrc or ~/.bashrc&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ENABLE_PROMPT_CACHING_1H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify with &lt;code&gt;usage&lt;/code&gt; output during a session—look for &lt;code&gt;ephemeral_1h_input_tokens&lt;/code&gt; in the log. If you only see &lt;code&gt;ephemeral_5m_&lt;/code&gt;, the variable isn't being picked up.&lt;/p&gt;

&lt;p&gt;Note: there were also TTL-related bugs in this period that forced resets to 5-minute behavior. Keep Claude Code at the latest version.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Usage Data
&lt;/h2&gt;

&lt;p&gt;I exported my own Cursor usage history and analyzed it. Here's what a month looks like across models (442 requests):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Avg cost/request&lt;/th&gt;
&lt;th&gt;Cache read ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Composer 2 Standard&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;$0.19&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composer 2 Fast&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;$0.32&lt;/td&gt;
&lt;td&gt;78.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude 4.6 Sonnet&lt;/td&gt;
&lt;td&gt;212&lt;/td&gt;
&lt;td&gt;$0.37&lt;/td&gt;
&lt;td&gt;84.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude 4.6 Opus&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;$0.90&lt;/td&gt;
&lt;td&gt;79.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 88.3% cache read ratio on Standard is the headline. For an average request consuming ~390K tokens, 88% of those are cache reads at $0.20/1M rather than fresh input at $0.50/1M. Without that cache hit rate, the average cost per request would be ~$0.40 instead of $0.19.&lt;/p&gt;

&lt;p&gt;The top Opus requests peaked at $4.25/request (3.9M total tokens, 3.8M of which were cache reads). Even with excellent cache ratios, Opus's higher base rates mean the same cache-heavy session costs 4–5× more than Composer 2 Standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Decision
&lt;/h2&gt;

&lt;p&gt;Composer 2 is not "Claude but cheap." It's a purpose-built agent runtime that has traded general intelligence for deep coding capability and cost efficiency at the infrastructure level. The Standard/Fast split exists because long-horizon agentic tasks don't need millisecond response times—and charging for that latency premium on 10-turn refactoring sessions is wasteful.&lt;/p&gt;

&lt;p&gt;The model choice that makes sense given this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default to Standard&lt;/strong&gt; for any multi-file task where you'll have more than 3–4 turns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch to Fast&lt;/strong&gt; for interactive chat where you're watching output incrementally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use frontier models (Opus, Claude 4.7)&lt;/strong&gt; only when Composer 2 hits a genuine capability ceiling—complex algorithmic reasoning, architecture decisions that span non-code domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cache makes Standard not just "slower Fast," but a qualitatively different operational mode: background processing with cost amortized over a long context window that grows cheaper the more you reuse it.&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>ai</category>
      <category>productivity</category>
      <category>codingtools</category>
    </item>
    <item>
      <title>Why Software Isn’t Built for AI Agents</title>
      <dc:creator>Gaurav Talesara</dc:creator>
      <pubDate>Sat, 02 May 2026 12:52:03 +0000</pubDate>
      <link>https://crypto.forem.com/gaurav_talesara/why-software-isnt-built-for-ai-agents-3ik5</link>
      <guid>https://crypto.forem.com/gaurav_talesara/why-software-isnt-built-for-ai-agents-3ik5</guid>
      <description>&lt;p&gt;The next users of your software won’t be humans.&lt;br&gt;
They’ll be agents.&lt;/p&gt;

&lt;p&gt;And most software today is completely unprepared for that.&lt;/p&gt;

&lt;p&gt;Right now, AI agents are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browsing websites&lt;/li&gt;
&lt;li&gt;Filling forms&lt;/li&gt;
&lt;li&gt;Clicking buttons&lt;/li&gt;
&lt;li&gt;Navigating dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not scale. That’s a workaround.&lt;/p&gt;

&lt;p&gt;We’re forcing machines to behave like humans—because our systems were never designed for anything else.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Core Problem
&lt;/h2&gt;

&lt;p&gt;Modern software is built around a simple assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A human will be sitting in front of a screen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assumption drives everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI-heavy workflows&lt;/li&gt;
&lt;li&gt;Step-by-step interactions&lt;/li&gt;
&lt;li&gt;Documentation meant to be read, not executed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But agents don’t need interfaces.&lt;br&gt;
They need &lt;strong&gt;interfaces they can reason about and execute against programmatically&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Where Current Systems Break for Agents
&lt;/h2&gt;

&lt;p&gt;Let’s break this down from a systems perspective.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. UI-First Architecture
&lt;/h3&gt;

&lt;p&gt;Most SaaS products expose functionality through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboards&lt;/li&gt;
&lt;li&gt;Forms&lt;/li&gt;
&lt;li&gt;Buttons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents interacting with these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rely on scraping or automation layers&lt;/li&gt;
&lt;li&gt;Break when UI changes&lt;/li&gt;
&lt;li&gt;Lack reliability&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  2. Non-Deterministic Outputs
&lt;/h3&gt;

&lt;p&gt;Agents need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured responses&lt;/li&gt;
&lt;li&gt;Predictable schemas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, they get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML pages&lt;/li&gt;
&lt;li&gt;Inconsistent API responses&lt;/li&gt;
&lt;li&gt;Unstructured data&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  3. Human-Centric Authentication
&lt;/h3&gt;

&lt;p&gt;Current flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth screens&lt;/li&gt;
&lt;li&gt;Email verification&lt;/li&gt;
&lt;li&gt;CAPTCHA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are friction points for agents trying to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discover tools&lt;/li&gt;
&lt;li&gt;Authenticate&lt;/li&gt;
&lt;li&gt;Execute tasks autonomously&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  4. Documentation Isn’t Machine-Readable
&lt;/h3&gt;

&lt;p&gt;Docs today are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Written for humans&lt;/li&gt;
&lt;li&gt;Scattered across pages&lt;/li&gt;
&lt;li&gt;Hard to parse programmatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured capability descriptions&lt;/li&gt;
&lt;li&gt;Executable contracts&lt;/li&gt;
&lt;li&gt;Clear input/output expectations&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  APIs Alone Are Not the Answer
&lt;/h2&gt;

&lt;p&gt;A common assumption is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We already have APIs, so we’re agent-ready.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s not true.&lt;/p&gt;

&lt;p&gt;APIs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too generic&lt;/li&gt;
&lt;li&gt;Often inconsistent&lt;/li&gt;
&lt;li&gt;Not designed for autonomous decision-making&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents need more than endpoints.&lt;/p&gt;

&lt;p&gt;They need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Action schemas&lt;/strong&gt; (what can be done, not just how)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic contracts&lt;/strong&gt; (guaranteed outputs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capability discovery&lt;/strong&gt; (what tools exist and when to use them)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  What “Agent-First Software” Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;If we design systems for agents as first-class users, the architecture changes.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Machine-Readable Interfaces
&lt;/h3&gt;

&lt;p&gt;Instead of UI-first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured APIs with strict schemas&lt;/li&gt;
&lt;li&gt;Tool definitions with clear contracts&lt;/li&gt;
&lt;li&gt;Standardized input/output formats&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  2. Programmatic Onboarding
&lt;/h3&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signup → verify → explore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discover → authenticate → execute&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Auto-provisioned credentials&lt;/li&gt;
&lt;li&gt;Machine-readable pricing/limits&lt;/li&gt;
&lt;li&gt;Capability endpoints&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  3. Permissioned Execution
&lt;/h3&gt;

&lt;p&gt;Agents need controlled autonomy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scoped access tokens&lt;/li&gt;
&lt;li&gt;Role-based permissions&lt;/li&gt;
&lt;li&gt;Execution boundaries&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  4. Deterministic Execution Layer
&lt;/h3&gt;

&lt;p&gt;Every action should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictable&lt;/li&gt;
&lt;li&gt;Retry-safe&lt;/li&gt;
&lt;li&gt;Observable&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  5. Observability for Agents
&lt;/h3&gt;

&lt;p&gt;Traditional logs aren’t enough.&lt;/p&gt;

&lt;p&gt;We need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decision tracing&lt;/li&gt;
&lt;li&gt;Tool-call lineage&lt;/li&gt;
&lt;li&gt;Cost per execution&lt;/li&gt;
&lt;li&gt;Latency breakdowns&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  A Practical Agent-System Architecture
&lt;/h2&gt;

&lt;p&gt;A simplified flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent
  ↓
Planner (decides what to do)
  ↓
Tool Registry (what tools are available)
  ↓
Execution Layer (calls APIs/tools)
  ↓
Response Validator (ensures correctness)
  ↓
Memory (stores context + learnings)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer is critical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Planner&lt;/strong&gt; → reasoning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool registry&lt;/strong&gt; → discoverability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution&lt;/strong&gt; → action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validator&lt;/strong&gt; → reliability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; → continuity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is very different from traditional request-response systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the Opportunity Is
&lt;/h2&gt;

&lt;p&gt;Most people today are focused on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How do we build better agents?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the bigger opportunity is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How do we build better systems for agents to operate on?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every major category is open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CRM → agent-native workflows&lt;/li&gt;
&lt;li&gt;Payments → programmable financial actions&lt;/li&gt;
&lt;li&gt;Support → autonomous resolution systems&lt;/li&gt;
&lt;li&gt;Analytics → queryable, structured insights&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not as add-ons.&lt;br&gt;
But as &lt;strong&gt;core design principles&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Most People Get Wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ❌ “APIs are enough”
&lt;/h3&gt;

&lt;p&gt;They’re not.&lt;br&gt;
Agents need structured, reliable, discoverable systems.&lt;/p&gt;




&lt;h3&gt;
  
  
  ❌ “Just add AI on top”
&lt;/h3&gt;

&lt;p&gt;That creates brittle layers, not scalable systems.&lt;/p&gt;




&lt;h3&gt;
  
  
  ❌ “Agents will replace software”
&lt;/h3&gt;

&lt;p&gt;No.&lt;br&gt;
Agents will &lt;strong&gt;consume software differently&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Shift That’s Coming
&lt;/h2&gt;

&lt;p&gt;We’re moving from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Human-first software
→ to&lt;/li&gt;
&lt;li&gt;Agent-first systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t a feature upgrade.&lt;/p&gt;

&lt;p&gt;It’s a &lt;strong&gt;paradigm shift in how software is designed and consumed&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The companies that win won’t be the ones with the smartest agents.&lt;/p&gt;

&lt;p&gt;They’ll be the ones:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agents prefer to use.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  👋 If You’re Building in This Space
&lt;/h2&gt;

&lt;p&gt;I’m currently working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent-based systems&lt;/li&gt;
&lt;li&gt;Automation architectures&lt;/li&gt;
&lt;li&gt;AI-native SaaS workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re exploring similar problems or thinking about building agent-first products, I’d be interested to exchange ideas.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>api</category>
      <category>architecture</category>
    </item>
    <item>
      <title>When Your Training Loss Is Lying to You Building a Tenacious-Specific Sales Outreach Benchmark Eyoel Nebiyu · May 2026</title>
      <dc:creator>Eyoel Nebiyu</dc:creator>
      <pubDate>Sat, 02 May 2026 12:51:59 +0000</pubDate>
      <link>https://crypto.forem.com/eyorata/when-your-training-loss-is-lying-to-you-building-a-tenacious-specific-sales-outreach-benchmark-2jgd</link>
      <guid>https://crypto.forem.com/eyorata/when-your-training-loss-is-lying-to-you-building-a-tenacious-specific-sales-outreach-benchmark-2jgd</guid>
      <description>&lt;p&gt;This post documents a real negative result: my trained model worked… but a well-written prompt worked better.&lt;/p&gt;

&lt;p&gt;TL;DR&lt;/p&gt;

&lt;p&gt;I built a 266-task evaluation benchmark for B2B sales-outreach agents — something existing benchmarks don’t measure well.&lt;/p&gt;

&lt;p&gt;Then I trained a small preference-learning judge model using SimPO.&lt;/p&gt;

&lt;p&gt;What happened surprised me:&lt;/p&gt;

&lt;p&gt;Training accuracy → 100%&lt;br&gt;
Held-out accuracy → 25%&lt;/p&gt;

&lt;p&gt;Classic overfitting.&lt;/p&gt;

&lt;p&gt;But the real lesson wasn’t about the model.&lt;/p&gt;

&lt;p&gt;It was about the data.&lt;/p&gt;

&lt;p&gt;After fixing dataset construction:&lt;/p&gt;

&lt;p&gt;Held-out accuracy improved to 0.417 (Delta A +25pp)&lt;br&gt;
A carefully prompted untrained model scored 0.833&lt;/p&gt;

&lt;p&gt;👉 Conclusion:&lt;br&gt;
At this scale, judging B2B sales tone is mostly a prompt-following problem, not a preference-learning problem.&lt;/p&gt;

&lt;p&gt;Project Links&lt;br&gt;
Dataset: &lt;a href="https://huggingface.co/datasets/eyorata/tenacious_bench_v0.1" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/eyorata/tenacious_bench_v0.1&lt;/a&gt;&lt;br&gt;
Judge Model: &lt;a href="https://huggingface.co/eyorata/tenacious-judge-simpo-qwen25-3b" rel="noopener noreferrer"&gt;https://huggingface.co/eyorata/tenacious-judge-simpo-qwen25-3b&lt;/a&gt;&lt;br&gt;
Code: &lt;a href="https://github.com/eyorata/sales_evaluation_bench" rel="noopener noreferrer"&gt;https://github.com/eyorata/sales_evaluation_bench&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Total experiment cost: $0.041&lt;/p&gt;

&lt;p&gt;The Problem: Existing Benchmarks Miss Real Sales Failures&lt;/p&gt;

&lt;p&gt;Benchmarks like τ²-Bench retail, MT-Bench, or AlpacaEval are excellent at evaluating:&lt;/p&gt;

&lt;p&gt;tool use&lt;br&gt;
reasoning&lt;br&gt;
conversation flow&lt;/p&gt;

&lt;p&gt;But they don’t measure what actually kills B2B deals.&lt;/p&gt;

&lt;p&gt;The agent I wanted to evaluate had to:&lt;/p&gt;

&lt;p&gt;interpret hiring signals (funding, layoffs, leadership changes)&lt;br&gt;
segment prospects correctly&lt;br&gt;
write grounded outreach emails&lt;br&gt;
avoid over-promising capacity&lt;br&gt;
respect opt-outs and booking rules&lt;/p&gt;

&lt;p&gt;Retail benchmarks simply don’t test these behaviors.&lt;/p&gt;

&lt;p&gt;Example real failures from earlier experiments:&lt;/p&gt;

&lt;p&gt;Auto-booking meetings when prospects only said “let me check my calendar.”&lt;br&gt;
Re-engaging after opt-out, risking brand damage.&lt;/p&gt;

&lt;p&gt;Those failures cost real money — but no public benchmark grades them.&lt;/p&gt;

&lt;p&gt;So I built one.&lt;/p&gt;

&lt;p&gt;Designing the Benchmark&lt;/p&gt;

&lt;p&gt;The rule I set early:&lt;/p&gt;

&lt;p&gt;Every rubric must be machine-gradable.&lt;/p&gt;

&lt;p&gt;No vague scoring like “sounds professional.”&lt;/p&gt;

&lt;p&gt;Instead, tasks check things like:&lt;/p&gt;

&lt;p&gt;banned phrases absent&lt;br&gt;
at least one signal referenced&lt;br&gt;
no unsupported commitments&lt;br&gt;
tone markers satisfied&lt;br&gt;
correct action class detected&lt;/p&gt;

&lt;p&gt;Each task returns a numeric score between 0 and 1.&lt;/p&gt;

&lt;p&gt;No humans needed during evaluation.&lt;/p&gt;

&lt;p&gt;The Dataset&lt;/p&gt;

&lt;p&gt;266 tasks across five generation modes:&lt;/p&gt;

&lt;p&gt;Mode    Why it exists&lt;br&gt;
Programmatic generation deterministic coverage&lt;br&gt;
Trace-derived tasks grounded realism&lt;br&gt;
Multi-LLM synthesis harder edge cases&lt;br&gt;
Hand-authored adversarial   stress testing&lt;br&gt;
Style-guide gold pairs  real preference ground truth&lt;/p&gt;

&lt;p&gt;Partitions:&lt;/p&gt;

&lt;p&gt;Train — 50%&lt;br&gt;
Dev — 30%&lt;br&gt;
Held-out — 20%&lt;br&gt;
Preventing Data Leakage&lt;/p&gt;

&lt;p&gt;I enforced three contamination checks:&lt;/p&gt;

&lt;p&gt;No shared 8-grams between train and held-out tasks&lt;br&gt;
Embedding similarity threshold&lt;br&gt;
Time-window filtering for public signals&lt;/p&gt;

&lt;p&gt;Result: 0 contamination violations.&lt;/p&gt;

&lt;p&gt;Why I Chose Preference Training (Path B)&lt;/p&gt;

&lt;p&gt;Week 10 analysis showed the model could already write fluent emails.&lt;/p&gt;

&lt;p&gt;The real problem was:&lt;/p&gt;

&lt;p&gt;👉 it couldn’t judge its own output.&lt;/p&gt;

&lt;p&gt;So instead of improving generation, I trained a judge model using SimPO.&lt;/p&gt;

&lt;p&gt;Setup:&lt;/p&gt;

&lt;p&gt;Algorithm: SimPO (reference-free preference learning)&lt;br&gt;
Trainer: TRL CPOTrainer&lt;br&gt;
Backbone: Qwen2.5-3B&lt;br&gt;
LoRA fine-tuning&lt;br&gt;
Hardware: free Colab T4&lt;br&gt;
The First Run: Perfect Training, Terrible Reality&lt;/p&gt;

&lt;p&gt;Training looked amazing:&lt;/p&gt;

&lt;p&gt;loss dropped smoothly&lt;br&gt;
train accuracy hit 1.00&lt;br&gt;
reward margins increased&lt;/p&gt;

&lt;p&gt;But evaluation stayed stuck:&lt;/p&gt;

&lt;p&gt;Train accuracy: 1.00&lt;br&gt;
Held-out accuracy: 0.25&lt;/p&gt;

&lt;p&gt;This is the moment many ML projects go wrong.&lt;/p&gt;

&lt;p&gt;The instinct is:&lt;/p&gt;

&lt;p&gt;bigger model&lt;br&gt;
more steps&lt;br&gt;
different hyperparameters&lt;/p&gt;

&lt;p&gt;I almost did that.&lt;/p&gt;

&lt;p&gt;Instead, I read the data.&lt;/p&gt;

&lt;p&gt;The Real Problem Was the Dataset&lt;/p&gt;

&lt;p&gt;Training examples used templated synthetic emails:&lt;/p&gt;

&lt;p&gt;“Thank you for your interest…”&lt;/p&gt;

&lt;p&gt;Held-out examples were real style-guide drafts:&lt;/p&gt;

&lt;p&gt;“You closed your $14M Series A in February…”&lt;/p&gt;

&lt;p&gt;The model learned a useless shortcut:&lt;/p&gt;

&lt;p&gt;👉 prefer one template phrase over another.&lt;/p&gt;

&lt;p&gt;It wasn’t learning tone — it was learning templates.&lt;/p&gt;

&lt;p&gt;The Fix&lt;/p&gt;

&lt;p&gt;I didn’t retrain immediately.&lt;/p&gt;

&lt;p&gt;I fixed the data.&lt;/p&gt;

&lt;p&gt;Using a stronger model, I rewrote all training “chosen” examples into authentic Tenacious voice, enforcing:&lt;/p&gt;

&lt;p&gt;five tone markers&lt;br&gt;
banned phrase rules&lt;br&gt;
grounded signals&lt;br&gt;
evaluator score ≥ 0.7&lt;/p&gt;

&lt;p&gt;Cost: $0.04&lt;/p&gt;

&lt;p&gt;Same algorithm. Same setup.&lt;/p&gt;

&lt;p&gt;Only the data changed.&lt;/p&gt;

&lt;p&gt;The Honest Results&lt;br&gt;
Metric  v1  v2&lt;br&gt;
Train accuracy  1.00    1.00&lt;br&gt;
Held-out accuracy   0.25    0.417&lt;br&gt;
Delta A vs baseline 0   +25pp&lt;br&gt;
Prompt baseline — 0.833&lt;br&gt;
Latency 258ms   417ms&lt;br&gt;
Finding #1 — Training Helped&lt;/p&gt;

&lt;p&gt;The trained judge beat the untrained backbone.&lt;/p&gt;

&lt;p&gt;So the methodology worked.&lt;/p&gt;

&lt;p&gt;Finding #2 — Prompting Won Anyway&lt;/p&gt;

&lt;p&gt;A carefully designed rubric prompt on the same backbone scored:&lt;/p&gt;

&lt;p&gt;0.833 accuracy&lt;/p&gt;

&lt;p&gt;No training required.&lt;/p&gt;

&lt;p&gt;The Real Lesson&lt;/p&gt;

&lt;p&gt;At this scale:&lt;/p&gt;

&lt;p&gt;B2B tone judgment is a prompt-following problem more than a preference-learning problem.&lt;/p&gt;

&lt;p&gt;The base model already understands tone.&lt;/p&gt;

&lt;p&gt;It just needs explicit rules.&lt;/p&gt;

&lt;p&gt;This is a legitimate negative result — and an important one.&lt;/p&gt;

&lt;p&gt;About Delta C&lt;/p&gt;

&lt;p&gt;I didn’t claim cross-benchmark improvement.&lt;/p&gt;

&lt;p&gt;The model wasn’t trained on retail tasks, so comparing against τ²-Bench retail would be misleading.&lt;/p&gt;

&lt;p&gt;Sometimes the honest result is:&lt;/p&gt;

&lt;p&gt;improvement is domain-specific.&lt;/p&gt;

&lt;p&gt;Limitations (Important)&lt;/p&gt;

&lt;p&gt;Only 12 held-out tasks currently contain preference pairs.&lt;/p&gt;

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

&lt;p&gt;wide confidence intervals&lt;br&gt;
small-n uncertainty&lt;/p&gt;

&lt;p&gt;This limitation is documented rather than hidden.&lt;/p&gt;

&lt;p&gt;What’s Next&lt;br&gt;
Dataset v0.2&lt;br&gt;
expand preference slice from 12 → 30 tasks&lt;br&gt;
clarify rubric ambiguity detected during calibration&lt;br&gt;
Model v0.2&lt;br&gt;
Qwen2.5-7B SimPO run&lt;br&gt;
same training recipe&lt;br&gt;
Future Ablation&lt;/p&gt;

&lt;p&gt;Compare against a strong commercial model using only prompting.&lt;/p&gt;

&lt;p&gt;The Big Engineering Lesson&lt;/p&gt;

&lt;p&gt;The hardest decision wasn’t choosing the algorithm.&lt;/p&gt;

&lt;p&gt;It was not retraining when training metrics looked perfect.&lt;/p&gt;

&lt;p&gt;Clean training loss often means:&lt;/p&gt;

&lt;p&gt;👉 the model learned something easy, not something useful.&lt;/p&gt;

&lt;p&gt;Fixing the data cost $0.04.&lt;/p&gt;

&lt;p&gt;Blindly scaling compute would have cost days.&lt;/p&gt;

&lt;p&gt;If Your Training Loss Looks Too Good…&lt;/p&gt;

&lt;p&gt;It probably is.&lt;/p&gt;

&lt;p&gt;Check the data before blaming the model.&lt;/p&gt;

&lt;p&gt;Acknowledgements&lt;/p&gt;

&lt;p&gt;Work completed within the 10Academy TRP1 program using:&lt;/p&gt;

&lt;p&gt;TRL + SimPO&lt;br&gt;
Unsloth QLoRA training&lt;br&gt;
Google Colab T4&lt;br&gt;
OpenRouter multi-LLM routing&lt;/p&gt;

&lt;p&gt;@dataset{tenacious_bench_v01_2026,&lt;br&gt;
  title  = {Tenacious-Bench},&lt;br&gt;
  author = {Nebiyu, Eyoel},&lt;br&gt;
  year   = 2026,&lt;br&gt;
  version = {0.1},&lt;br&gt;
  license = {CC-BY-4.0}&lt;br&gt;
}&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I built a DuckDB extension to handle chemistry data without pandas or RDKit</title>
      <dc:creator>nk_Enuke</dc:creator>
      <pubDate>Sat, 02 May 2026 12:50:36 +0000</pubDate>
      <link>https://crypto.forem.com/nk_maker/i-built-a-duckdb-extension-to-handle-chemistry-data-without-pandas-or-rdkit-mjk</link>
      <guid>https://crypto.forem.com/nk_maker/i-built-a-duckdb-extension-to-handle-chemistry-data-without-pandas-or-rdkit-mjk</guid>
      <description>&lt;h1&gt;
  
  
  Idea
&lt;/h1&gt;

&lt;p&gt;While reproducing top solutions of a &lt;a href="https://zenn.dev/nkwork9999/articles/neurips-polymer-2025" rel="noopener noreferrer"&gt;chemistry data competition&lt;/a&gt;, I started building a DuckDB community extension for handling chemistry data directly in SQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it can do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Parse SMILES, InChI, PDB and other chemistry formats directly — no pandas, no RDKit on the side&lt;/li&gt;
&lt;li&gt;Plug into DuckDB's native CSV/Parquet/Iceberg/S3/HTTP readers, so ingestion + light preprocessing happens in one query&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Background
&lt;/h1&gt;

&lt;p&gt;What is chemistry data, anyway? One of the canonical forms is &lt;strong&gt;SMILES&lt;/strong&gt;, a notation that encodes a molecular structure as a plain string.&lt;/p&gt;

&lt;p&gt;The standard library for reading and processing SMILES is &lt;strong&gt;RDKit&lt;/strong&gt;. For example, ethanol is &lt;code&gt;CCO&lt;/code&gt; in SMILES, and RDKit will give you the molecular formula &lt;code&gt;C2H6O&lt;/code&gt; from it.&lt;/p&gt;

&lt;p&gt;RDKit doesn't stop there — molecular weight, fingerprints, descriptors, substructure search, and so on. It's basically the must-have library for chemistry data work. (Internally, the algorithms come from a bunch of different papers...)&lt;/p&gt;

&lt;p&gt;So you might say: do we even need a new extension? In practice though, I kept hitting these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doesn't always work cleanly on Google Colab (often needs a runtime restart)&lt;/li&gt;
&lt;li&gt;pandas version conflicts&lt;/li&gt;
&lt;li&gt;To export results, you usually have to bounce through pandas anyway, or pull in yet another tool depending on the feature you want&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  What I built
&lt;/h1&gt;

&lt;p&gt;So, here's a DuckDB extension that addresses these.&lt;/p&gt;

&lt;h2&gt;
  
  
  ducksmiles
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://duckdb.org/community_extensions/extensions/ducksmiles" rel="noopener noreferrer"&gt;https://duckdb.org/community_extensions/extensions/ducksmiles&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I implemented a mix of features RDKit covers and a few that are awkward with RDKit alone.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compute molecular formula / weight from a SMILES string and write back to CSV
→ The most basic chemistry preprocessing flow, in a single SQL line&lt;/li&gt;
&lt;li&gt;Decompose an InChI string into its formula / connection / hydrogen / charge / stereo layers, and compare two InChIs at the skeleton level
→ With RDKit you have to round-trip through a Mol object; ducksmiles pulls the layers straight from the string&lt;/li&gt;
&lt;li&gt;Convert SMILES ⇄ SELFIES (a notation that's easier to use for ML models)
→ Normally you'd need a separate &lt;code&gt;selfies&lt;/code&gt; package; this is built in&lt;/li&gt;
&lt;li&gt;Aggregate atom / chain / residue counts from PDB / CIF / XYZ (3D structure files) with SQL
→ Get metadata from heavy structure files without ever opening them in code&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;NULL&lt;/code&gt; instead of throwing on invalid SMILES or structures
→ A million-row batch won't blow up because of one malformed row&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Try it out
&lt;/h1&gt;

&lt;p&gt;I'll use two public datasets — &lt;a href="https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/delaney-processed.csv" rel="noopener noreferrer"&gt;DeepChem ESOL CSV&lt;/a&gt; and &lt;a href="https://files.rcsb.org/download/1AKE.pdb" rel="noopener noreferrer"&gt;RCSB PDB&lt;/a&gt; — and paste the actual CLI output for each of the five capabilities above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;duckdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;INSTALL&lt;/span&gt; &lt;span class="n"&gt;ducksmiles&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;community&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;ducksmiles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. SMILES → formula / weight
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mol_formula&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mol_weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_csv_auto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/delaney-processed.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────┬───────────┬─────────┐
│                         smiles                         │  formula  │ weight  │
├────────────────────────────────────────────────────────┼───────────┼─────────┤
│ OCC3OC(OCC2OC(OC(C#N)c1ccccc1)C(O)C(O)C2O)C(O)C(O)C3O  │ NULL      │ NULL    │
│ Cc1occc1C(=O)Nc2ccccc2                                 │ C12H11NO2 │ 201.225 │
│ CC(C)=CCCC(C)=CC(=O)                                   │ C10H16O   │ 152.237 │
│ c1ccc2c(c1)ccc3c2ccc4c5ccccc5ccc43                     │ C22H14    │ 278.354 │
│ c1ccsc1                                                │ C4H4S     │ 84.136  │
└────────────────────────────────────────────────────────┴───────────┴─────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The public CSV URL goes straight into &lt;code&gt;read_csv_auto&lt;/code&gt;, and you get formulas and weights right there. The first row's complex SMILES returns &lt;code&gt;NULL&lt;/code&gt; rather than throwing.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Decompose an InChI into its layers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;inchi_formula&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inchi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;inchi_connections&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inchi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;inchi_hydrogens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inchi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hydrogens&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Ethanol'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Caffeine'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'InChI=1S/C8H10N4O2/c1-10-4-9-6-5(10)7(13)12(3)8(14)11(6)2/h4H,1-3H3'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&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="n"&gt;inchi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┬───────────┬───────────────────────────────────────┬────────────┐
│   name   │  formula  │              connections              │ hydrogens  │
├──────────┼───────────┼───────────────────────────────────────┼────────────┤
│ Ethanol  │ C2H6O     │ 1-2-3                                 │ 3H,2H2,1H3 │
│ Caffeine │ C8H10N4O2 │ 1-10-4-9-6-5(10)7(13)12(3)8(14)11(6)2 │ 4H,1-3H3   │
└──────────┴───────────┴───────────────────────────────────────┴────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;InChI is a notation that bundles atom connectivity, hydrogen positions, and other facets into a single string, split across labeled sections. For example, ethanol looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3
        └─────┘ └────┘ └────────┘
        formula  conn.    hydrogens
       inchi_formula  inchi_connections  inchi_hydrogens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normally you'd round-trip through RDKit's &lt;code&gt;Mol&lt;/code&gt; object to extract pieces. With ducksmiles, you pull the section you want straight from the string. The query result above (&lt;code&gt;C2H6O&lt;/code&gt; / &lt;code&gt;1-2-3&lt;/code&gt; / &lt;code&gt;3H,2H2,1H3&lt;/code&gt;) is exactly the substring of those layers in the original InChI.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. SMILES ⇄ SELFIES roundtrip
&lt;/h2&gt;

&lt;p&gt;Pulling some short SMILES out of ESOL and running SMILES → SELFIES → SMILES:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;smiles_to_selfies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;selfies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;selfies_to_smiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles_to_selfies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;roundtrip&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_csv_auto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://deepchemdata.s3-us-west-1.amazonaws.com/datasets/delaney-processed.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────┬─────────────────────────────────────────┬───────────┐
│  smiles   │                 selfies                 │ roundtrip │
├───────────┼─────────────────────────────────────────┼───────────┤
│ c1ccsc1   │ [c][Ring1][C][c][c][s][c][Ring1][C]     │ cccsc     │
│ O=C1CCCN1 │ [O][=C][Ring1][C][C][C][C][N][Ring1][C] │ O=CCCCN   │
│ CCCC=C    │ [C][C][C][C][=C]                        │ CCCC=C    │
│ CC(C)Cl   │ [C][C][Branch1][C][C][Cl]               │ CC(C)Cl   │
│ CCC(C)CO  │ [C][C][C][Branch1][C][C][C][O]          │ CCC(C)CO  │
└───────────┴─────────────────────────────────────────┴───────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both directions of the conversion run inside DuckDB. Normally you'd need a separate Python &lt;code&gt;selfies&lt;/code&gt; package on the side.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Metadata from PDB structure files
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;read_text&lt;/code&gt; accepts HTTPS URLs and arrays directly, so you can pull three PDB files straight from RCSB and aggregate them in one go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;INSTALL&lt;/span&gt; &lt;span class="n"&gt;httpfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;httpfs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;structure_atom_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;atoms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;structure_chain_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;chains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;structure_residue_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;residues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;structure_model_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="s1"&gt;'https://files.rcsb.org/download/1AKE.pdb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'https://files.rcsb.org/download/1CRN.pdb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'https://files.rcsb.org/download/4HHB.pdb'&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────┬───────┬────────┬──────────┬────────┐
│                 filename                 │ atoms │ chains │ residues │ models │
├──────────────────────────────────────────┼───────┼────────┼──────────┼────────┤
│ https://files.rcsb.org/download/1AKE.pdb │ 3816  │ 2      │ 428      │ 1      │
│ https://files.rcsb.org/download/1CRN.pdb │ 327   │ 1      │ 46       │ 1      │
│ https://files.rcsb.org/download/4HHB.pdb │ 4779  │ 4      │ 574      │ 1      │
└──────────────────────────────────────────┴───────┴────────┴──────────┴────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;read_text&lt;/code&gt; returns the file content as a &lt;code&gt;content&lt;/code&gt; column, so you just feed it to the &lt;code&gt;structure_*&lt;/code&gt; functions. You also get &lt;code&gt;filename&lt;/code&gt; for free, which makes it easy to keep track of the original file when you scale this to thousands.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Bad input returns NULL instead of crashing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mol_is_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mol_formula&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mol_weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CCO'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'not_a_smiles'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'c1ccccc1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CC(=O)O'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smiles&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────┬───────┬─────────┬────────┐
│    smiles    │ valid │ formula │ weight │
├──────────────┼───────┼─────────┼────────┤
│ CCO          │ true  │ C2H6O   │ 46.069 │
│ not_a_smiles │ false │ NULL    │ NULL   │
│ c1ccccc1     │ true  │ C6H6    │ 78.114 │
│              │ false │ NULL    │ NULL   │
│ CC(=O)O      │ true  │ C2H4O2  │ 60.052 │
└──────────────┴───────┴─────────┴────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with malformed SMILES or empty strings mixed in, those rows just become &lt;code&gt;NULL&lt;/code&gt; — the query keeps going. It's built for batch.&lt;/p&gt;

&lt;h1&gt;
  
  
  Closing thoughts
&lt;/h1&gt;

&lt;p&gt;One thing that struck me while working with chemistry data is just how many formats there are.&lt;br&gt;&lt;br&gt;
SMILES, InChI, SDF... each has its own format, and each tends to want its own dedicated tool.&lt;/p&gt;

&lt;p&gt;On top of that, RDKit itself has a huge surface — its "descriptors" alone come in around 200 varieties, each backed by different papers.&lt;/p&gt;

&lt;p&gt;What I want ducksmiles to grow into is a single tool that absorbs those plumbing and preprocessing concerns, so you can move faster without juggling toolchains. There's a lot to learn from the prior art, and I plan to chip away at it over time.&lt;/p&gt;

&lt;p&gt;Also — DuckDB itself is a general-purpose engine, so it can't cover every domain's specific needs. That's exactly what community extensions are for, and the bar to author one is surprisingly low. If you have a domain pain point that DuckDB doesn't quite cover, please give it a try.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>duckdb</category>
      <category>chemistry</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Building WeaveLLM: Why .NET Deserves a Better then LangChain</title>
      <dc:creator>Harshil Shah</dc:creator>
      <pubDate>Sat, 02 May 2026 12:49:31 +0000</pubDate>
      <link>https://crypto.forem.com/harshil_shah_baded487d158/building-weavellm-why-net-deserves-a-better-then-langchain-2hgi</link>
      <guid>https://crypto.forem.com/harshil_shah_baded487d158/building-weavellm-why-net-deserves-a-better-then-langchain-2hgi</guid>
      <description>&lt;h1&gt;
  
  
  Building WeaveLLM: Why .NET Deserves a Better LangChain
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Tags: dotnet, ai, csharp, llm&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Cover image: architecture diagram of WeaveLLM pipeline&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Here's a thing I keep running into: .NET developers building serious AI features, and the ecosystem basically telling them to just use Python. LangChain, LlamaIndex, DSPy — every major orchestration framework is Python-first. .NET is an afterthought, if it shows up at all.&lt;/p&gt;

&lt;p&gt;But C# developers aren't waiting around. They're shipping customer support bots, RAG pipelines, code-review agents — right now, in production. They're just doing it by calling OpenAI's REST API by hand, copy-pasting retry logic into every service class, and hoping nothing throws in an async chain at 2 AM.&lt;/p&gt;

&lt;p&gt;LangChain does have a .NET port. I've used it. It's incomplete, the types don't map cleanly to .NET idioms, and it leans on exceptions as its main error-handling strategy — which is genuinely painful to compose in async code. The deeper issue is that LangChain was designed around Python's dynamic type system. Porting it to C# without rethinking the API from scratch gives you a framework that fights the language the entire time.&lt;/p&gt;

&lt;p&gt;So I built WeaveLLM instead. Started as a hobby project, turned into something I actually want to use at work. It's a .NET 8 AI orchestration library designed specifically for C# — railway-oriented results, &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; streaming, an ASP.NET-style middleware pipeline, and fully generic chains that catch type mismatches at compile time rather than in a 3 AM Slack alert. Here are the four decisions that shaped it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Design Decision 1: &lt;code&gt;ChainResult&amp;lt;T&amp;gt;&lt;/code&gt; Over Exceptions
&lt;/h2&gt;

&lt;p&gt;Let me describe a problem you've almost certainly hit. You call an LLM, it fails — rate limited, timed out, bad input, provider down — and now you need to handle that failure &lt;em&gt;somewhere&lt;/em&gt;. In an exception-based framework, that means try/catch at every composition point. Stack a few chains together and you've got nested try/catch blocks all the way down, each one trying to figure out which exception type maps to which recovery strategy.&lt;/p&gt;

&lt;p&gt;It's not that exceptions are wrong. It's that they're invisible. A method signature like &lt;code&gt;Task&amp;lt;string&amp;gt;&lt;/code&gt; tells you nothing about whether it can fail and how.&lt;/p&gt;

&lt;p&gt;WeaveLLM uses railway-oriented programming. Every chain execution returns &lt;code&gt;ChainResult&amp;lt;T&amp;gt;&lt;/code&gt; — a result type where errors are values you work with, not surprises that unwind your stack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Errors are data, never thrown&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsSuccess&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsFailure&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ChainError&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TokenUsage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;TokenUsage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TokenUsage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChainError&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Projects success value to new type; failure passes through unchanged&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TNext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TNext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TNext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Destructure into (isSuccess, value, error)&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Deconstruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;ChainError&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Errors come with structure too, not just a message string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ChainError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;InnerException&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ChainError&lt;/span&gt; &lt;span class="nf"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Timeout"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ChainError&lt;/span&gt; &lt;span class="nf"&gt;RateLimited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"RateLimited"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ChainError&lt;/span&gt; &lt;span class="nf"&gt;InvalidInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"InvalidInput"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ChainError&lt;/span&gt; &lt;span class="nf"&gt;ProviderError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ProviderError"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&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;Here's what that looks like at the call site, compared to the try/catch version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The old way — try/catch at every layer&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RateLimitException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* retry */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fallback */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* log */&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// WeaveLLM — errors are values you can switch on&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;isSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"RateLimited"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fallbackChain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"Timeout"&lt;/span&gt;     &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;             &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUpperInvariant&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// transforms success, ignores failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ChainResult&amp;lt;T&amp;gt;&lt;/code&gt; also bundles &lt;code&gt;TokenUsage&lt;/code&gt; and a &lt;code&gt;Metadata&lt;/code&gt; dictionary that middleware layers write into without breaking the type contract. If you've ever used F# or Rust, this is the same railway pattern — errors short-circuit, successes flow through, and &lt;code&gt;Map()&lt;/code&gt; lets you transform values without having to unwrap and re-wrap manually.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Error handling&lt;/th&gt;
&lt;th&gt;Composability&lt;/th&gt;
&lt;th&gt;Observability built-in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw exceptions&lt;/td&gt;
&lt;td&gt;try/catch at every level&lt;/td&gt;
&lt;td&gt;Hard to chain&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangChain.NET&lt;/td&gt;
&lt;td&gt;Mix of exceptions and nulls&lt;/td&gt;
&lt;td&gt;Inconsistent&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WeaveLLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Values (ChainResult&amp;lt;T&amp;gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Monadic Map/Deconstruct&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TokenUsage + Metadata&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Design Decision 2: &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; for Streaming
&lt;/h2&gt;

&lt;p&gt;Streaming isn't a polish feature — it's the thing users actually notice. A response that starts rendering in 200ms feels fast, even if the total generation takes 8 seconds. A response that hangs for 8 seconds and then dumps a wall of text feels broken, even if the numbers are the same.&lt;/p&gt;

&lt;p&gt;Python gets async generators for this. They work great in Python. In C#, the equivalent is &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;, which has been in .NET since Core 3.0 and plugs directly into &lt;code&gt;await foreach&lt;/code&gt;, LINQ, and ASP.NET Core's response pipeline. WeaveLLM makes it part of the core contract, not an optional add-on.&lt;/p&gt;

&lt;p&gt;Every chain has two execution paths: a request/response &lt;code&gt;ExecuteAsync&lt;/code&gt; and a token-by-token &lt;code&gt;StreamAsync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IChain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ChainContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;StreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ChainContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&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 provider layer exposes the same interface. Here's what consuming it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IStreamingChatModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IChatModel&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;StreamChatAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ChatOptions&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Each token prints as it arrives&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StreamChatAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&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;And in ASP.NET Core, wiring up a streaming endpoint with Server-Sent Events is about 12 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/stream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IChain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/event-stream"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChainContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tell me a story"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\n\n"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FlushAsync&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;Compare that to callback-based streaming, which is what a lot of .NET AI libraries still use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Callback-based — no backpressure, no real cancellation support,&lt;/span&gt;
&lt;span class="c1"&gt;// can't await async work per-token&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&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;With &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; you get &lt;code&gt;CancellationToken&lt;/code&gt; integration automatically (the consumer cancels mid-stream and the producer stops), full LINQ support via &lt;code&gt;System.Linq.Async&lt;/code&gt;, and no adapter layer between your chain and the framework. It's just the language doing what it already does well.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Streaming model&lt;/th&gt;
&lt;th&gt;Backpressure&lt;/th&gt;
&lt;th&gt;CancellationToken&lt;/th&gt;
&lt;th&gt;LINQ composable&lt;/th&gt;
&lt;th&gt;ASP.NET SSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Callbacks&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Awkward&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Custom plumbing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Events&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Custom plumbing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IAsyncEnumerable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Built-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Built-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Native&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Design Decision 3: The Middleware Pipeline
&lt;/h2&gt;

&lt;p&gt;If you've built anything in ASP.NET Core, you already know how middleware works: components that wrap a request, can inspect or modify it, can short-circuit or pass it through, and compose in a predictable order. Every .NET developer has this mental model. It made sense to borrow it directly for LLM chains.&lt;/p&gt;

&lt;p&gt;The interface is deliberately close to ASP.NET's &lt;code&gt;RequestDelegate&lt;/code&gt; shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;delegate&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ChainDelegate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ChainContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IChainMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ChainContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ChainDelegate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;next&lt;/code&gt; is everything downstream. Call it to continue. Skip it to short-circuit — a cache hit, a circuit breaker tripping. Call it and then do something with the result — tracing, cost tracking, PII scrubbing. WeaveLLM ships six middleware implementations out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddWeaveLLM&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"OpenAI:ApiKey"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddReActAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxSteps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myLlmChain&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RetryMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backoffSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CacheMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RateLimitingMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestsPerMinute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TracingMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activitySource&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CostMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PiiScrubbingMiddleware&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing your own is implementing one method. Here's a logging middleware, complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoggingMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IChainMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;LoggingMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ChainContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ChainDelegate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Chain {Name} starting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChainName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Chain {Name} finished: {Status} in {Duration}ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChainName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"OK"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalMilliseconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LangChain's version of this is "callbacks" — a grab-bag of optional hooks (&lt;code&gt;on_llm_start&lt;/code&gt;, &lt;code&gt;on_llm_end&lt;/code&gt;, &lt;code&gt;on_chain_error&lt;/code&gt;) registered globally and fired via reflection. They can't short-circuit. They can't replace the result. They don't compose with each other in any meaningful way. It's a notification system dressed up as a pipeline.&lt;/p&gt;

&lt;p&gt;WeaveLLM's middleware is an actual pipeline. Each component decides whether to call &lt;code&gt;next&lt;/code&gt;, what to do with the result, and whether to replace or propagate. That's the difference between observability you can build on and hooks you can only listen to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design Decision 4: Generic Chains with Compile-Time Type Safety
&lt;/h2&gt;

&lt;p&gt;LangChain chains are stringly typed by default. Inputs and outputs are usually &lt;code&gt;Dictionary&amp;lt;string, string&amp;gt;&lt;/code&gt; and the framework resolves what goes where at runtime. In Python, that's fine — you've got a REPL, you find the bug fast. In C#, you find it in production when a downstream chain reaches for a key the upstream chain forgot to set.&lt;/p&gt;

&lt;p&gt;It's an unforced error. The type system is right there.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;IChain&amp;lt;TInput, TOutput&amp;gt;&lt;/code&gt; makes the contract explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IChain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChainResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ChainContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;StreamAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ChainContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Connectable variant for fluent Pipe() composition&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IConnectableChain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IChain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IConnectableChain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TNext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Pipe&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TNext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;IChain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TNext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;IConnectableChain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IChainMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOutput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Pipe&amp;lt;TNext&amp;gt;()&lt;/code&gt; enforces that the output type of the left chain matches the input type of the right one — at compile time. Not at test time, not in staging. At &lt;code&gt;dotnet build&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;UserQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;SearchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Chunks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;FinalAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;ConfidenceScore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// This compiles — types line up&lt;/span&gt;
&lt;span class="n"&gt;IConnectableChain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FinalAnswer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ragPipeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;retrievalChain&lt;/span&gt;         &lt;span class="c1"&gt;// IChain&amp;lt;UserQuery, SearchResults&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rerankerChain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// IChain&amp;lt;SearchResults, SearchResults&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generatorChain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// IChain&amp;lt;SearchResults, FinalAnswer&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// This doesn't compile — SearchResults != UserQuery, caught immediately&lt;/span&gt;
&lt;span class="c1"&gt;// retrievalChain.Pipe(generatorChain); // CS ERROR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ComposedChain&lt;/code&gt; also handles failure propagation: if the first chain returns a failure, the second never runs and the error forwards unchanged. No null checks, no manual short-circuiting.&lt;/p&gt;

&lt;p&gt;The practical payoff shows up when you're actually using the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ragPipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What is the WeaveLLM license?"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ChainContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;SessionId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user-123"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Answer: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Confidence: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfidenceScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;P0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Cost: $&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TokenUsage&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;EstimatedCostUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F4&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&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;Full IntelliSense, no casting, no &lt;code&gt;as&lt;/code&gt; checks. If you rename a property on &lt;code&gt;FinalAnswer&lt;/code&gt;, the compiler tells you everywhere it breaks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type safety&lt;/th&gt;
&lt;th&gt;LangChain (Python)&lt;/th&gt;
&lt;th&gt;LangChain.NET&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;WeaveLLM&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input/output types&lt;/td&gt;
&lt;td&gt;Dynamic dict&lt;/td&gt;
&lt;td&gt;Dynamic dict&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Generic &lt;code&gt;IChain&amp;lt;TIn, TOut&amp;gt;&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mismatched pipes caught&lt;/td&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compile time&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDE completion on results&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Full IntelliSense&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactor safety&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compiler-enforced&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What Ships in v0.1.0-alpha
&lt;/h2&gt;

&lt;p&gt;WeaveLLM v0.1.0-alpha is on NuGet across five packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package WeaveLLM.Core
dotnet add package WeaveLLM.Providers
dotnet add package WeaveLLM.Memory
dotnet add package WeaveLLM.Observability
dotnet add package WeaveLLM.Extensions.DependencyInjection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Providers — four, all production-tested:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI (gpt-4o, embeddings, streaming)&lt;/li&gt;
&lt;li&gt;Anthropic (claude-sonnet, streaming)&lt;/li&gt;
&lt;li&gt;Ollama (local inference, embeddings — no API key needed)&lt;/li&gt;
&lt;li&gt;HuggingFace (Inference API, embeddings)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All share &lt;code&gt;IChatModel&lt;/code&gt;. Swap provider with one line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents — three patterns:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ReActAgent&lt;/code&gt; — Thought → Action → Observation loop until Final Answer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PlanAndExecuteAgent&lt;/code&gt; — separate planning and execution phases for complex tasks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AgentGraph&amp;lt;TState&amp;gt;&lt;/code&gt; — state machine for multi-agent workflows with typed shared state and conditional branching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Memory and RAG — full pipeline:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IMemoryStore&lt;/code&gt; with in-memory, Qdrant, and Postgres (pgvector) backends&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DefaultRagPipeline&lt;/code&gt; with recursive text splitting, hybrid BM25 + vector search, and Reciprocal Rank Fusion&lt;/li&gt;
&lt;li&gt;Document loaders for plain text, Markdown, and directories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Middleware — six built-in:&lt;/strong&gt;&lt;br&gt;
Retry, caching, rate-limiting, OpenTelemetry tracing, per-request cost estimation, and PII scrubbing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability — baked in, not bolted on:&lt;/strong&gt;&lt;br&gt;
OpenTelemetry tracing and metrics via the &lt;code&gt;WeaveLLM&lt;/code&gt; &lt;code&gt;ActivitySource&lt;/code&gt; and &lt;code&gt;Meter&lt;/code&gt;. Per-request token usage and estimated USD cost tracked across all providers.&lt;/p&gt;


&lt;h2&gt;
  
  
  Where It's Going
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;v0.2.0-alpha (next 6–8 weeks):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure OpenAI provider&lt;/li&gt;
&lt;li&gt;Multi-modal input (image + text messages)&lt;/li&gt;
&lt;li&gt;Streaming agents — &lt;code&gt;IAsyncEnumerable&amp;lt;AgentStep&amp;gt;&lt;/code&gt; for real-time reasoning step visibility&lt;/li&gt;
&lt;li&gt;Redis memory backend&lt;/li&gt;
&lt;li&gt;Structured output support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;v1.0.0 — Q4 2026:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frozen public API with full semver guarantee&lt;/li&gt;
&lt;li&gt;Docs site with guides, API reference, and runnable samples&lt;/li&gt;
&lt;li&gt;Benchmark suite against LangChain Python — qualitative claims become numbers&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Honest Alpha Warning
&lt;/h2&gt;

&lt;p&gt;This is a hobby project that got serious. The core abstractions are stable and I won't break them before v1.0. All four providers are tested and working. Some edge cases are still being hardened and breaking changes are possible in non-core areas before v1.0.&lt;/p&gt;

&lt;p&gt;I built this because I was frustrated, not because I had a product roadmap. If you've hit the same frustrations with .NET LLM tooling, that's the target audience — and the best time to shape what v1.0 looks like is right now.&lt;/p&gt;

&lt;p&gt;If you want to contribute, &lt;code&gt;good-first-issue&lt;/code&gt; labels are a good starting point: adding provider adapters, writing integration tests against real APIs, extending the middleware library. Adding a new provider is just implementing &lt;code&gt;IChatModel&lt;/code&gt; — the middleware, streaming, and type-safety machinery comes for free.&lt;/p&gt;


&lt;h2&gt;
  
  
  Licence
&lt;/h2&gt;

&lt;p&gt;MIT. Use it, fork it, ship it in your own projects. No credit needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Copyright (c) 2026 WeaveLLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The railway result, the async stream, the ASP.NET-style middleware, the generic chain — none of these are Python idioms with a C# skin on top. They're what this kind of library looks like when it starts from C# instead of ending there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Source code, samples, and NuGet links: &lt;a href="https://github.com/harshil-inspire2/WeaveLLM" rel="noopener noreferrer"&gt;github.com/harshil-inspire2/WeaveLLM&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Keywords: .NET AI, LangChain alternative, AI orchestration, C# LLM, dotnet AI framework, IAsyncEnumerable streaming, railway-oriented programming, ASP.NET middleware pattern&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>llm</category>
    </item>
    <item>
      <title>Além do Código: O Guia Estratégico de Comunicação para Devs Brasileiros no Mercado Americano</title>
      <dc:creator>Kyousuke Natsume</dc:creator>
      <pubDate>Sat, 02 May 2026 12:46:59 +0000</pubDate>
      <link>https://crypto.forem.com/kyousuke/alem-do-codigo-o-guia-estrategico-de-comunicacao-para-devs-brasileiros-no-mercado-americano-1f8d</link>
      <guid>https://crypto.forem.com/kyousuke/alem-do-codigo-o-guia-estrategico-de-comunicacao-para-devs-brasileiros-no-mercado-americano-1f8d</guid>
      <description>&lt;p&gt;Para conquistar o mercado norte-americano de tecnologia, não basta apenas ser um gênio do código. O Brasil se tornou um polo de &lt;em&gt;nearshoring&lt;/em&gt; devido à competência técnica e fuso horário favorável, mas o que realmente sustenta essas parcerias é a &lt;strong&gt;confiança corporativa&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Se você quer parar de ser visto como "apenas um executor" e se tornar um parceiro estratégico, aqui está o guia direto ao ponto para adaptar sua comunicação e comportamento ao estilo dos EUA.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Confiança: Esqueça o Cafezinho, Foque no Código
&lt;/h2&gt;

&lt;p&gt;No Brasil, confiamos em quem gostamos (confiança &lt;strong&gt;afetiva&lt;/strong&gt;). Nos EUA, eles confiam em quem entrega (confiança &lt;strong&gt;cognitiva&lt;/strong&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A lógica americana:&lt;/strong&gt; "Se o trabalho é bom, a relação é segura".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A adaptação:&lt;/strong&gt; Priorize marcos técnicos e prazos antes de tentar criar um vínculo pessoal. Deixe o &lt;em&gt;small talk&lt;/em&gt; para depois que a entrega estiver garantida.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Comunicação: O Fim das "Entrelinhas"
&lt;/h2&gt;

&lt;p&gt;O Brasil é uma cultura de &lt;strong&gt;alto contexto&lt;/strong&gt; (muitas nuances e mensagens implícitas). Os EUA são o oposto: &lt;strong&gt;baixo contexto&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regra de ouro:&lt;/strong&gt; A responsabilidade de ser entendido é sua, não de quem ouve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seja literal:&lt;/strong&gt; Diga exatamente o que quer dizer. Evite ambiguidades.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documente TUDO:&lt;/strong&gt; Acordos verbais não bastam. Envie um e-mail de resumo após cada reunião com decisões, responsáveis e prazos. Isso sinaliza estrutura e profissionalismo.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Postura: De "Yes-Man" a Consultor
&lt;/h2&gt;

&lt;p&gt;A hierarquia americana é igualitária. O silêncio em uma reunião não é visto como respeito, mas como falta de iniciativa ou desengajamento.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ownership:&lt;/strong&gt; Assuma a responsabilidade pelas tarefas. Não apenas reporte problemas, sugira soluções.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questione:&lt;/strong&gt; Se um requisito é tecnicamente inviável, fale! O cliente americano paga pela sua expertise, não por alguém que apenas diz "sim" para tudo.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. A Santidade do Tempo
&lt;/h2&gt;

&lt;p&gt;Para o americano, "tempo é dinheiro" e pontualidade é um indicador direto de confiabilidade.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2 minutos antes:&lt;/strong&gt; Entre nas chamadas de vídeo antes do horário.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Previsibilidade:&lt;/strong&gt; Se vai atrasar um entregável, avise com antecedência (e não no dia do prazo), apresentando uma nova data e o motivo técnico.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Dicionário de Sobrevivência: O Perigo do "I'll Try"
&lt;/h2&gt;

&lt;p&gt;Muitas vezes, a tradução literal destrói sua credibilidade. Confira a troca de &lt;em&gt;mindset&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Em vez de dizer... (Baixa Confiança)&lt;/th&gt;
&lt;th&gt;Diga isto... (Alta Confiança)&lt;/th&gt;
&lt;th&gt;Por que funciona?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"I'll try to have it by Friday."&lt;/td&gt;
&lt;td&gt;"I will deliver this by Friday."&lt;/td&gt;
&lt;td&gt;Elimina a dúvida e assume compromisso.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"I think we can solve this."&lt;/td&gt;
&lt;td&gt;"Based on my analysis, we will implement X."&lt;/td&gt;
&lt;td&gt;Troca o "acho" por evidência e ação.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Maybe we should consider X?"&lt;/td&gt;
&lt;td&gt;"I recommend X because it improves scalability by Y%."&lt;/td&gt;
&lt;td&gt;Te posiciona como consultor especialista.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"I'm not sure, let me check."&lt;/td&gt;
&lt;td&gt;"I will verify the docs and answer by 2 PM."&lt;/td&gt;
&lt;td&gt;Fornece um prazo claro para a resolução.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  6. Transparência Radical em Crises
&lt;/h2&gt;

&lt;p&gt;Quando um bug crítico aparece, o instinto brasileiro é resolver antes de contar. Para o americano, esse silêncio parece incompetência.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Reporte imediatamente:&lt;/strong&gt; O cliente prefere saber do problema cedo.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;RCA (Root Cause Analysis):&lt;/strong&gt; Use dados técnicos. Explique a causa raiz e o plano de mitigação.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Fale a língua deles:&lt;/strong&gt; Use termos como "débito técnico", "trade-offs" e "escalabilidade" para justificar decisões.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  TL;DR (Resumo para o dev ocupado):
&lt;/h3&gt;

&lt;p&gt;Para ser respeitado nos EUA: seja &lt;strong&gt;pontual&lt;/strong&gt;, seja &lt;strong&gt;explícito&lt;/strong&gt;, &lt;strong&gt;documente&lt;/strong&gt; suas decisões e trate o cliente como um &lt;strong&gt;parceiro igual&lt;/strong&gt;, não como um chefe inquestionável.&lt;/p&gt;

&lt;p&gt;A confiança não é algo que se pede; é algo que se demonstra na consistência das suas ações.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Referências e Leituras Recomendadas
&lt;/h2&gt;

&lt;p&gt;Para quem deseja se aprofundar, recomendo fortemente as fontes que serviram de base para este guia:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;The Culture Map (Erin Meyer)&lt;/strong&gt; – A bíblia para entender a diferença entre confiança cognitiva e afetiva.

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://erinmeyer.com/building-trust-across-cultures/" rel="noopener noreferrer"&gt;Building Trust Across Cultures - Erin Meyer&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Teoria de Contexto Cultural (Edward T. Hall)&lt;/strong&gt; – Essencial para entender a transição do "Alto Contexto" brasileiro para o "Baixo Contexto" americano.

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/High-context_and_low-context_cultures" rel="noopener noreferrer"&gt;High-context and low-context cultures - Wikipedia&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Nearshoring no Brasil (Relatórios de Mercado)&lt;/strong&gt; – Por que o Brasil é o parceiro estratégico ideal para os EUA.

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.vintasoftware.com/blog/why-brazil-is-a-top-destination-for-outsourcing-software-development" rel="noopener noreferrer"&gt;Why Brazil is a top destination for software development&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>braziliandevs</category>
      <category>management</category>
    </item>
    <item>
      <title>Sneaky version control trick</title>
      <dc:creator>Aaron Maxwell</dc:creator>
      <pubDate>Sat, 02 May 2026 12:45:00 +0000</pubDate>
      <link>https://crypto.forem.com/aaronmaxwell/sneaky-version-control-trick-2nek</link>
      <guid>https://crypto.forem.com/aaronmaxwell/sneaky-version-control-trick-2nek</guid>
      <description>&lt;p&gt;I was writing a program to process some CSV data, exported from a vendor. And it has a date-time column in some creative, funky format.&lt;/p&gt;

&lt;p&gt;To help with ingesting this into a Pandas dataframe, I wrote a converter function: it takes that string, and returns a Python datetime object. Simple, right?&lt;/p&gt;

&lt;p&gt;But you know, this is a PERFECT place to write a unit test. And I decided to use Pytest, starting with this simple test:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;myprog&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse_vendor_datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_parse_vendor_datetime&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_vendor_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12/25/38 1:14am EST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2038&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run the test, and it fails, like it should. Then I write parse_vendor_datetime(), and the test passes. Great!&lt;/p&gt;

&lt;p&gt;Now, what no one ever talks about is how TDD works with version control. The secret is that there's actually a really productive way to do it.&lt;/p&gt;

&lt;p&gt;Because after I check this test-passing code into version control, in my private branch... the next thing I do is run my program on some real data. And I find it doesn't handle time zones quite right.&lt;/p&gt;

&lt;p&gt;Okay... so here's what I do. I get an example of the problematic data, and add to my test function:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_parse_vendor_datetime&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_vendor_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12/25/38 1:14am EST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2038&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;

    &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_vendor_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;08/17/38 11:05pm EDT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2038&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run it, and verify it fails like it should. And I check THAT into version control - again, in my private branch. Because when I have a correctly failing test for a bug, that's progress. Even if that test isn't passing yet.&lt;/p&gt;

&lt;p&gt;But it bugs me that this code is repetitive. Doesn't it bug you?  Ugh. Makes my skin crawl, just looking at it.&lt;/p&gt;

&lt;p&gt;So I refactor it, to use something Pytest calls "parametrized tests":&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;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dt_str,expected&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12/25/38 1:14am EST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2038&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;08/17/38 11:05pm EDT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2038&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_parse_vendor_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_vendor_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See how the test code is not repetitive anymore? Even if you don't now what this "parametrize()" thing is, because I haven't explained it, you still understand this is an improvement. If I need to change something, or add another test case, I only need to change the code in one place.&lt;/p&gt;

&lt;p&gt;So now, I check THIS into version control. The test is still failing. But changing the code to be less repetitive - more maintainable - is still forward progress, and worthy of checkpointing.&lt;/p&gt;

&lt;p&gt;And: Because I had checked in the code before it too, if I mess up the test somehow, I can always roll back to the point it was correct.&lt;/p&gt;

&lt;p&gt;What's fascinating:&lt;/p&gt;

&lt;p&gt;When you build on this process, and add a few more ingredients... there's a COGNITIVE benefit.&lt;/p&gt;

&lt;p&gt;It's potent. You can actually get into a state of flow very easily. In a way that's easy to maintain, and to keep your focus, and become extremely productive.&lt;/p&gt;

&lt;p&gt;Having a blast, as you continue cranking out code. Riding that wave for as long as you want. It comes out of how you use version control along with writing automated tests, and doing test-driven development.&lt;/p&gt;

&lt;p&gt;If you liked this, you will enjoy the &lt;a href="https://powerfulpython.com/newsletter-devto/" rel="noopener noreferrer"&gt;Powerful Python Newsletter&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Scalar Setup ASP.NET Core Web API</title>
      <dc:creator>Sharad Aade</dc:creator>
      <pubDate>Sat, 02 May 2026 12:44:34 +0000</pubDate>
      <link>https://crypto.forem.com/sharadaade/scalar-setupt-aspnet-core-web-api-26cj</link>
      <guid>https://crypto.forem.com/sharadaade/scalar-setupt-aspnet-core-web-api-26cj</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Step - 1&lt;br&gt;
Install NuGet Package&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Fdj27h59yx1xnc3t1pzo1.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.amazonaws.com%2Fuploads%2Farticles%2Fdj27h59yx1xnc3t1pzo1.png" alt=" " width="702" height="91"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Step - 2&lt;br&gt;
Add Code in - &lt;code&gt;Properties/launchSettings.json&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;"launchBrowser": true,&lt;br&gt;
"launchUrl": "scalar",&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step - 3
Add Code in &lt;code&gt;Program.cs&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;if (app.Environment.IsDevelopment())&lt;br&gt;
{&lt;br&gt;
    app.MapOpenApi();&lt;br&gt;
    app.MapScalarApiReference();&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step - 4
Run the project
&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.amazonaws.com%2Fuploads%2Farticles%2Fn8ool7c2vjwa912jt0el.png" alt=" " width="149" height="48"&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>webapi</category>
      <category>aspdotnetcore</category>
      <category>jwt</category>
    </item>
  </channel>
</rss>
