<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mabera</title>
    <description>The latest articles on DEV Community by Mabera (@husseinadeiza).</description>
    <link>https://dev.to/husseinadeiza</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4173930%2F62531f20-f324-49e8-9af8-49ced835c0a7.jpg</url>
      <title>DEV Community: Mabera</title>
      <link>https://dev.to/husseinadeiza</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/husseinadeiza"/>
    <language>en</language>
    <item>
      <title>What a call-graph traversal sees that contract-by-contract review cannot</title>
      <dc:creator>Mabera</dc:creator>
      <pubDate>Sat, 17 Oct 2026 21:00:57 +0000</pubDate>
      <link>https://dev.to/husseinadeiza/what-a-call-graph-traversal-sees-that-contract-by-contract-review-cannot-3gdk</link>
      <guid>https://dev.to/husseinadeiza/what-a-call-graph-traversal-sees-that-contract-by-contract-review-cannot-3gdk</guid>
      <description>&lt;p&gt;Most smart-contract review reads one contract at a time. You open the file, you look for the usual shapes — reentrancy, unchecked calls, oracle reads — and you write down what you find. The contract is the unit of analysis, and the audit report is a list of findings about that contract.&lt;/p&gt;

&lt;p&gt;But the largest exploits of the last decade did not happen inside one contract. They happened &lt;em&gt;on a route through several&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This post is about building an agent that refuses to certify a contract when its call graph reaches a weakness a real exploit actually used — and what the graph told me that reading contracts one at a time does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that doesn't show up contract-by-contract
&lt;/h2&gt;

&lt;p&gt;I seeded a small graph — 144 nodes and 217 edges at seed time (23 contracts, 76 functions, 34 &lt;code&gt;CALLS&lt;/code&gt; edges, 20 SWC patterns, 10 exploits, plus libraries and auditors; 160 nodes once the agent files its own verdicts). Then I asked one question of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:CALLS&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b:&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a.contract&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b.contract&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;a.contract&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.contract&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;30 routes cross a contract boundary.&lt;/strong&gt; Some of them are worth staring at:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;from&lt;/th&gt;
&lt;th&gt;to&lt;/th&gt;
&lt;th&gt;hops&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bZx.executeTrade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AaveV2LendingPool.flashLoan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bZx.executeTrade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AaveV2LendingPool.executeOperation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bZx.liquidate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cUSDC.redeem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MakerVault.liquidate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cUSDC.redeem&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1inchv3.swap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UniswapV2Router02._swap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of these functions is individually unremarkable. &lt;code&gt;cUSDC.redeem&lt;/code&gt; is not a vulnerability. &lt;code&gt;AaveV2LendingPool.flashLoan&lt;/code&gt; is the feature working as designed. &lt;code&gt;MakerVault.liquidate&lt;/code&gt; is a liquidation function doing liquidation.&lt;/p&gt;

&lt;p&gt;The risk is not in any node. It is in the &lt;strong&gt;route&lt;/strong&gt; — and a review that opens one file at a time never sees a route, because a route lives in the joins between files.&lt;/p&gt;

&lt;p&gt;That is the whole thesis, and it is why the graph is not decoration here. There is no contract you can point at and say "this is the dangerous one." There is a path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the graph say what's actually dangerous
&lt;/h2&gt;

&lt;p&gt;Once you can walk routes, the next problem is that most of them are fine. You need a rule that separates a route worth refusing from a route that is just... code calling code.&lt;/p&gt;

&lt;p&gt;The naive rule is "flag if any node on the path has a vulnerability." That rule is bad, and it took a real run to show me how bad.&lt;/p&gt;

&lt;p&gt;What I used instead: a route blocks only if a node on it carries a vulnerability &lt;strong&gt;that is connected to a real exploit&lt;/strong&gt; — a two-hop join, evaluated per node on the path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;UNWIND&lt;/span&gt; &lt;span class="n"&gt;$path&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;f:&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;hv:&lt;/span&gt;&lt;span class="n"&gt;HAS_VULNERABILITY&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;v:&lt;/span&gt;&lt;span class="n"&gt;Vulnerability&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;hv.status&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'exploited'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pattern'&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;OPTIONAL&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:EXPLOITED_BY&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;e:&lt;/span&gt;&lt;span class="n"&gt;Exploit&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;fname&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v.swc&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v.title&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v.severity&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hv.status&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
       &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;e.name&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;exploits&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
       &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;e.loss_usd&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;losses&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;EXPLOITED_BY&lt;/code&gt; edge is doing the real work. A vulnerability pattern that no exploit ever used is a &lt;em&gt;detector flag&lt;/em&gt;, not evidence — so the agent drops rows with no exploit behind them. What survives is a claim with teeth: this shape was used, in this incident, for this much money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result that made the design worth it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;KyberSwapPool&lt;/code&gt; gets refused. The graph names its own evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;route reaching &lt;code&gt;KyberSwapPool.getAmountsOut&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;blocked by &lt;code&gt;SWC-128&lt;/code&gt; (Oracle Manipulation)&lt;/li&gt;
&lt;li&gt;which the graph connects to five real exploits: KyberSwap Flash Loan Attack ($307M), Cream Finance ($130M), Mango Markets ($112M), Beanstalk ($182M), Euler Finance ($197M)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;$928M of documented loss behind one edge in the graph.&lt;/strong&gt; The agent is not computing a risk score from a heuristic. It is reading the connections and refusing on them, and it can show you the path it read.&lt;/p&gt;

&lt;p&gt;And the refusal is written back &lt;em&gt;into the graph&lt;/em&gt; rather than into a log — a &lt;code&gt;CertificationRequest{status: BLOCKED}&lt;/code&gt; linked via &lt;code&gt;BLOCKED_BY_PATH&lt;/code&gt; to the exact functions responsible, with the evidence path stored as JSON on the request. That matters for one reason: the decision becomes replayable. Load the request, re-run the same traversal, get the same verdict. All 12 blocked contracts replay identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that caught me out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. A parameterised variable-length bound doesn't work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wanted the hop count configurable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;$fn&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;e:&lt;/span&gt;&lt;span class="n"&gt;CALLS&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mf"&gt;1.&lt;/span&gt;&lt;span class="n"&gt;.$hops&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b:&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Encountered unhandled type in inlined properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The confusing part is the error text. It says &lt;em&gt;inlined properties&lt;/em&gt;, which sends you looking at your property maps — but &lt;code&gt;{name: $fn}&lt;/code&gt; works fine, and the parameter is a plain Python &lt;code&gt;int&lt;/code&gt;. The failure is specific to the &lt;strong&gt;hop bound&lt;/strong&gt;: it has to be a literal in the pattern. I expanded a validated integer from Python instead, and wrote the reason into the code comment so the next person doesn't lose the same hour.&lt;/p&gt;

&lt;p&gt;It fails at inline time, so the error points at the wrong part of the query. If you hit it: it's the &lt;code&gt;*1..N&lt;/code&gt;, not your properties.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. My detection rule was wrong, and the test caught it by disagreeing with itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First full run, the agent refused &lt;code&gt;UniswapV2Router02&lt;/code&gt; on an &lt;code&gt;SWC-103&lt;/code&gt; finding tagged &lt;code&gt;pattern&lt;/code&gt;. Which was fine, except the verdict's own wording said &lt;em&gt;"used in a real exploit"&lt;/em&gt; — and that SWC-103 node had no &lt;code&gt;EXPLOITED_BY&lt;/code&gt; edge at all. Zero exploits behind it.&lt;/p&gt;

&lt;p&gt;The bug was mine, not the graph's: I was treating "has a vulnerability" as the blocking condition. But a vulnerability node that no exploit ever touched is a theoretical pattern. Filtering those out flipped &lt;code&gt;UniswapV2Router02&lt;/code&gt; to CERTIFIED, which is correct — and it made the verdict text true, which it wasn't before.&lt;/p&gt;

&lt;p&gt;The lesson I'd generalise: &lt;strong&gt;a detection rule that can't explain itself in the same words it reports is probably wrong.&lt;/strong&gt; My one-line pitch claimed evidence-based refusal; the code was doing pattern-matching. The gap between the two is where the false positive lived.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;p&gt;Worth being precise, because "AI security tool" invites overclaiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It does not analyse source code.&lt;/strong&gt; No Slither, no static analysis, no bytecode. It reasons over vulnerabilities and exploits &lt;em&gt;already recorded in the graph&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a live audit tool.&lt;/strong&gt; The dataset is compiled from public post-mortems and the SWC registry, seeded into the graph. Ingestion from Etherscan is explicitly out of scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a scanner.&lt;/strong&gt; It will not find a new bug. It will tell you when the recorded evidence about a contract's reachable functions says "don't sign off on this."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a narrower claim than "AI audits your contracts," and it's the one the code actually supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a graph and not tables
&lt;/h2&gt;

&lt;p&gt;The remove-the-database test: delete FalkorDB and does the product still work?&lt;/p&gt;

&lt;p&gt;No. The product's core operation is &lt;em&gt;"walk outward from this function, match what you reach against real exploit shapes, and file the result on the graph."&lt;/em&gt; In a table store that is a recursive CTE for the walk, another join chain per node for the matching, and then you have to reconstruct the path from row sets — and you still don't get the route list back in one shot, which is the thing the verdict has to name.&lt;/p&gt;

&lt;p&gt;Cypher gives you the route &lt;em&gt;as a value&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;$fn&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;e:&lt;/span&gt;&lt;span class="n"&gt;CALLS&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b:&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hops&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
       &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="nf"&gt;nodes&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n.name&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
       &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="nf"&gt;nodes&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n.contract&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;contracts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;path&lt;/code&gt; is a first-class result. That's what lets a refusal say &lt;em&gt;"here is the 2-hop route through Aave that got you blocked"&lt;/em&gt; instead of &lt;em&gt;"risk detected."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If your domain has relationships that carry meaning — call graphs, dependency chains, counterparty exposure, attack surfaces — the interesting findings live in the paths, not the rows. A per-entity review will look complete and miss the thing that actually took $928M.&lt;/p&gt;

&lt;p&gt;The move that made this work wasn't a clever query. It was asking a question that only has an answer in a graph: &lt;strong&gt;is there a route from here to a weakness someone has already used?&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for Graph Hacks 2026 (WeMakeDevs × FalkorDB), track: Agents That Act on Connected Data. Source: &lt;a href="https://github.com/HusseinAdeiza/graphhacks-refusal-agent" rel="noopener noreferrer"&gt;github.com/HusseinAdeiza/graphhacks-refusal-agent&lt;/a&gt;. Built with AI coding assistance (Hermes Agent), disclosed per the event rules; every query, schema decision, and verdict rule was verified against a live FalkorDB, and the repo's end-to-end suite (&lt;code&gt;python3 -m agent.verify&lt;/code&gt;, 55 checks) is the proof it works as claimed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>database</category>
      <category>cypher</category>
    </item>
    <item>
      <title>I built an open-weight-model agent that finds OSS issues worth your time</title>
      <dc:creator>Mabera</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:52:24 +0000</pubDate>
      <link>https://dev.to/husseinadeiza/i-built-an-open-weight-model-agent-that-finds-oss-issues-worth-your-time-45f</link>
      <guid>https://dev.to/husseinadeiza/i-built-an-open-weight-model-agent-that-finds-oss-issues-worth-your-time-45f</guid>
      <description>&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ClaimScout&lt;/strong&gt; — a tool that tells you which open-source issues are actually worth your time.&lt;/p&gt;

&lt;p&gt;Point it at the repos you care about and it hands back a short list of issues that are genuinely &lt;em&gt;yours to take&lt;/em&gt;: unclaimed, low-traffic, bounded bugs — not assigned work, not bot chores, not architecture epics dressed up as quick wins. Then, if you want, an open-weight model ranks that shortlist against your skills so the best first contribution floats to the top.&lt;/p&gt;

&lt;p&gt;Who would pay for this? Anyone trying to land merged pull requests — hackathon entrants, bug-bounty hunters, developers building a hiring portfolio. The problem is real and expensive: I lost &lt;em&gt;days&lt;/em&gt; this month trawling issue trackers for work that turned out to be already claimed, vaguely scoped, or a months-long epic. ClaimScout turns that grep-and-pray into a one-line command.&lt;/p&gt;

&lt;p&gt;It found the two bugs I actually shipped PRs for — so I'm user number one, with receipts.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/HusseinAdeiza/claimscout" rel="noopener noreferrer"&gt;https://github.com/HusseinAdeiza/claimscout&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Here's a real run, not a mockup. Scanning &lt;code&gt;marginfi-v2&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;$ claimscout scan --repos 0dotxyz/marginfi-v2

0dotxyz/marginfi-v2: 10 claimable
  #705   unclaimed, 0 comment(s), created 2026-10-12  Same-asset-emode weight inversion
         labels: bug
  #682   unclaimed, 0 comment(s), created 2026-09-18  Allow empty-but-active integration balances to close
         labels: bug, good first issue
  #701   unclaimed, 0 comment(s), created 2026-10-10  Liquidation interplay with Order tagging
         labels: bug, low priority
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line is a real, open, unassigned issue. Then the open-weight model ranks the shortlist against a Rust/Anchor skill profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;claimscout scan &lt;span class="nt"&gt;--repos&lt;/span&gt; 0dotxyz/marginfi-v2 &lt;span class="nt"&gt;--max-comments&lt;/span&gt; 0 &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | claimscout-rank &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="s2"&gt;"rust,anchor,solana,security"&lt;/span&gt;

 1. &lt;span class="c"&gt;#682   [90]  Allow empty-but-active integration balances to close&lt;/span&gt;
      Bounded bug with clear reproduction, matches contributor&lt;span class="s1"&gt;'s skills
 2. #705   [80]  Same-asset-emode weight inversion
      Bounded bug, matches contributor'&lt;/span&gt;s skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those two — #682 and #705 — are exactly the issues I went on to build and open PRs for. The ranker put them at the top on its own.&lt;/p&gt;

&lt;p&gt;Run the whole thing yourself with &lt;code&gt;./examples/demo.sh&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The core is ~250 lines of dependency-free Python (stdlib only, Python 3.9+). The scanner fetches and filters deterministically; the ranker is a separate, optional single-call step.&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="c1"&gt;# The filter that does the real work — an issue is reported only if it is ALL of:
#   an issue (not a PR), unassigned, low-traffic, not architecture, not bot-authored,
#   and actionable (bug / good-first-issue / help-wanted, or a recent unlabelled report)
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pull_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;   &lt;span class="c1"&gt;# PRs are not claimable issues
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assignees&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;             &lt;span class="k"&gt;continue&lt;/span&gt;   &lt;span class="c1"&gt;# already assigned
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comments&lt;/span&gt;&lt;span class="sh"&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="n"&gt;max_comments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;continue&lt;/span&gt;   &lt;span class="c1"&gt;# contested / already discussed
&lt;/span&gt;    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;labels&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARCH_LABELS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# epic/roadmap/spike
&lt;/span&gt;    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[bot]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full source, tests, and the ranker are in the repo.&lt;/p&gt;

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

&lt;p&gt;Two deliberate pieces:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The fetch/filter loop is plain code — no model in it.&lt;/strong&gt; This is the whole design. I spent weeks watching small open-weight models try to drive multi-step agent loops, and they fail in a specific, ugly way: they call one tool five to eight times, give up, and the run still reports "ok." So ClaimScout never asks the model to fetch or filter. That's deterministic, testable Python. The model is only invoked at the very end, on an already-clean shortlist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The model does one stateless call.&lt;/strong&gt; When you enable ranking, ClaimScout sends the filtered shortlist to an open-weight model (&lt;code&gt;meta/llama-3.2-11b&lt;/code&gt; via NVIDIA's OpenAI-compatible endpoint by default; any OpenAI-compatible server — local Ollama, vLLM — works) and asks for a ranked JSON array. No tool calls, no multi-turn loop, no agent harness. That's the exact workload a small model handles well.&lt;/p&gt;

&lt;p&gt;It still needs guardrails. On the first live test the model answered with a Python code block instead of JSON, so the ranker strips code fences, extracts the array even when it's wrapped in prose, and issues one strict "JSON only" retry before giving up. There's a test for each of those failure modes — because I watched them happen.&lt;/p&gt;

&lt;p&gt;19 stdlib &lt;code&gt;unittest&lt;/code&gt; tests cover the triage filter, the messy-JSON parsing, and prompt construction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;This is the part that decides whether the "side hustle" actually works, and it comes down to cost.&lt;/p&gt;

&lt;p&gt;ClaimScout is useful &lt;em&gt;per scan&lt;/em&gt; and used &lt;em&gt;often&lt;/em&gt; — you run it every time you sit down to find work. A version built on a hosted API would call a paid model on every fetch, and that per-run cost is more than a contributor chasing a $100 bounty would ever pay. The economics only close because the model is &lt;strong&gt;open-weight and cheap to run&lt;/strong&gt;. The open model isn't a nice-to-have here; it's the reason the product can exist at a price anyone would pay.&lt;/p&gt;

&lt;p&gt;Second, it keeps your data yours. ClaimScout reads your repos and your skill profile. Running inference on an open model you control means that never has to leave for a third party to log.&lt;/p&gt;

&lt;p&gt;Third, openness is what let me build around the model's &lt;em&gt;real&lt;/em&gt; limits instead of an idealized demo. Because the model is open and I could test it directly, I designed the deterministic/model split around how it actually behaves — the same lesson that makes the tool reliable is the lesson that closed, black-box APIs hide from you until production.&lt;/p&gt;

&lt;p&gt;Where the open approach beat a closed one: a hosted model would have cost more per scan than the tool is worth, and would have forced my fetch logic behind an API I couldn't inspect. The open-weight model made a cheap, private, inspectable tool possible — which is the only version of this that works as a hustle.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;ClaimScout was built &lt;em&gt;inside&lt;/em&gt; an agent session — an AI coding agent driving the terminal, running the real toolchain, and iterating against live output. That's fitting, because the tool is itself about making agent-assisted open-source work reliable.&lt;/p&gt;

&lt;p&gt;The session is where several of the design decisions were forced into the open rather than guessed at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model returned Python code instead of JSON&lt;/strong&gt; on the first live ranker test. I caught it, added code-fence stripping, prose-array extraction, and a strict retry — then wrote a test for each failure mode. A closed API would have hidden that until production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;amix&lt;/code&gt; of 16 delayed narration clips collapsed to ~0s&lt;/strong&gt; because &lt;code&gt;amix&lt;/code&gt; truncates to the shortest stream. Fixed by padding each clip with &lt;code&gt;apad&lt;/code&gt; to the full timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;edge-tts silently dropped a line&lt;/strong&gt; (0-byte file) twice — fixed with retries, which is now why the voiceover generator re-attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest thread through all of it: build around what the open model &lt;em&gt;actually&lt;/em&gt; does, verified by running it, not what the demo says it should do. That's the same principle ClaimScout encodes in its architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;Me — and it paid off concretely.&lt;/p&gt;

&lt;p&gt;I built ClaimScout to solve my own Hacktoberfest problem, then used it the same week. It surfaced &lt;code&gt;marginfi-v2&lt;/code&gt; #682 (a stuck integration-balance bug) and #705 (a same-asset e-mode weight inversion). I opened PRs for both. Earlier in the session, the same fetch-and-filter approach caught a cosmos-sdk PR getting closed by a maintainer — the kind of signal you only get if something is watching your PRs, not just the issue board.&lt;/p&gt;

&lt;p&gt;The first "user" is a developer who'd been burned by claimed-and-gone issues, and what they asked for next was exactly what I built: &lt;em&gt;just tell me which ones are actually mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Giving AI Agents a Memory of Your Crisp Inbox</title>
      <dc:creator>Mabera</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:16:59 +0000</pubDate>
      <link>https://dev.to/husseinadeiza/giving-ai-agents-a-memory-of-your-crisp-inbox-4k0</link>
      <guid>https://dev.to/husseinadeiza/giving-ai-agents-a-memory-of-your-crisp-inbox-4k0</guid>
      <description>&lt;p&gt;&lt;strong&gt;Giving AI Agents a Memory of Your Crisp Inbox&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI agents forget everything. Every LLM call starts from zero — no idea what your customer asked yesterday, what your team answered, or how a bug was actually diagnosed. Cognee is an open-source memory layer that fixes this: you feed it your data, it builds a knowledge graph, and agents can then recall that knowledge across sessions.&lt;/p&gt;

&lt;p&gt;The hard part is getting the data in. Cognee has a handful of connectors, but the world runs on dozens of tools. As part of Mergetober (open-source month with Cognee), I built a Crisp connector — wiring the shared-inbox support tool into Cognee so agents can answer questions about what customers actually asked.&lt;/p&gt;

&lt;p&gt;This is the story of how it works, and a few design decisions that were less obvious than they looked.&lt;/p&gt;

&lt;p&gt;The problem: chat inboxes don't fit neatly&lt;/p&gt;

&lt;p&gt;Crisp is a shared inbox — visitors chat with your team, conversations accumulate. At first glance it's simple: read conversations, feed them to Cognee. But chat data has a shape that fights you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sessions are short and numerous. One conversation might be three messages ("how do I reset?" → "click here" → "thanks"). Ingest each message as its own node and your graph explodes into thousands of near-identical fragments.&lt;/li&gt;
&lt;li&gt;There's no delete feed. When a conversation is closed or aged out, Crisp just... stops listing it. There's no webhook saying "this was deleted."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both of these drove the design.&lt;/p&gt;

&lt;p&gt;Decision 1: one node per conversation, not per message&lt;/p&gt;

&lt;p&gt;The connector aggregates an entire conversation into a single markdown document — visitor context plus the full transcript:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Visitor: Ada · chat&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visitor:&lt;/strong&gt; I can't find my invoice&lt;br&gt;
&lt;strong&gt;Agent:&lt;/strong&gt; I've resent it to your email — check spam?&lt;br&gt;
&lt;strong&gt;Visitor:&lt;/strong&gt; Got it, thanks!&lt;br&gt;
This keeps the graph readable and matches how humans think about a support thread as one unit of meaning. It's the same call the existing Slack connector makes, and it's what the Cognee maintainers flagged in the issue itself.&lt;/p&gt;

&lt;p&gt;Decision 2: full-snapshot sync, because there's no delete feed&lt;/p&gt;

&lt;p&gt;Cognee already has a "forget-on-delete" mechanism: if a record drops out of the snapshot between two syncs, its orphan-cleanup removes it from the graph. But that only works if the connector loads a complete snapshot each run — the exact set of conversations currently visible — rather than merging in new ones.&lt;/p&gt;

&lt;p&gt;So the connector uses write_disposition="replace": every sync rewrites staging with all current conversations. A conversation deleted upstream simply isn't in the new snapshot, and Cognee forgets it. Unchanged conversations keep a stable content-hash ID, so they're not re-processed and re-cognified.&lt;/p&gt;

&lt;p&gt;The subtle trap: a partial snapshot must never land&lt;/p&gt;

&lt;p&gt;Here's the part that took the most care. Because "absent from snapshot" is interpreted as "deleted," a partial snapshot — say the API returned an error halfway through — would make Cognee forget a batch of perfectly live conversations. A flaky network would silently erase memory.&lt;/p&gt;

&lt;p&gt;So the connector treats a render or API failure as fatal: it aborts the run and leaves the previous memory intact, rather than committing a truncated snapshot. Transient errors (rate-limits, 5xx, timeouts) are retried with backoff first; only a persistent failure aborts. Getting this wrong would be the kind of bug that quietly corrupts memory for weeks before anyone notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental sync&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/topoteretes/cognee" rel="noopener noreferrer"&gt;https://github.com/topoteretes/cognee&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Re-cognifying the entire inbox on every run is wasteful. Crisp exposes updated_at on each conversation, so the connector takes an optional since= watermark and only renders conversations updated after it. Combined with the stable content-hash IDs, a daily sync only touches what actually changed.&lt;/p&gt;

&lt;p&gt;Auth: Crisp's two-part token&lt;/p&gt;

&lt;p&gt;Crisp authenticates with a two-part keypair (identifier + key) sent as HTTP Basic, plus an X-Crisp-Tier: plugin header, plus a website_id selecting the workspace. Not OAuth — a plugin token you generate from the Marketplace. Straightforward once you know the trio.&lt;/p&gt;

&lt;p&gt;Using it&lt;/p&gt;

&lt;p&gt;pip install cognee-community-connector-crisp&lt;/p&gt;

&lt;p&gt;export CRISP_IDENTIFIER="..."&lt;br&gt;
export CRISP_KEY="..."&lt;br&gt;
export CRISP_WEBSITE_ID="..."&lt;/p&gt;

&lt;p&gt;import cognee&lt;br&gt;
from cognee_community_connector_crisp import crisp_source&lt;/p&gt;

&lt;p&gt;await cognee.remember(&lt;br&gt;
    crisp_source(),          # or since= for incremental&lt;br&gt;
    dataset_name="crisp",&lt;br&gt;
    max_rows_per_table=0,    # ingest the whole inbox&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;answer = await cognee.search(&lt;br&gt;
    query_text="What did customers complain about this month?",&lt;br&gt;
    query_type=cognee.SearchType.GRAPH_COMPLETION,&lt;br&gt;
    datasets=["crisp"],&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;Then you can ask your agent "summarize the billing complaints from last week" and it actually knows — because the inbox is in its memory.&lt;/p&gt;

&lt;p&gt;Testing without a live account&lt;/p&gt;

&lt;p&gt;The test suite mocks the Crisp API (no token needed) and covers the parts most likely to break: message/conversation rendering, Crisp's page-number pagination, the document-source tagging that routes conversations through normal cognify, and the full-snapshot forget-on-delete behavior — including the case where a conversation vanishes from the listing between syncs and must be forgotten. 13 tests, all passing, plus ruff clean.&lt;/p&gt;

&lt;p&gt;What's next&lt;/p&gt;

&lt;p&gt;The connector is open as a PR against cognee-community. Once merged, "ask my Crisp inbox" becomes one crisp_source() away — and it's a template for the next chat-tool connector, because the same full-snapshot + aggregate-per-thread pattern applies to Intercom, Front, Zendesk, and the rest.&lt;/p&gt;

&lt;p&gt;If you're doing Mergetober too, the Crisp connector is a decent reference for the shape a Cognee data-source connector should take. And if you try it against your own inbox, I'd genuinely like to hear what breaks — chat APIs always have one more pagination quirk than the docs admit.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>machinelearning</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
