<?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: Priyanshu Singh</title>
    <description>The latest articles on DEV Community by Priyanshu Singh (@priyanshusingh9).</description>
    <link>https://dev.to/priyanshusingh9</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%2F4192375%2Fc747fca2-683f-4c16-86ae-61a3bd787a23.jpg</url>
      <title>DEV Community: Priyanshu Singh</title>
      <link>https://dev.to/priyanshusingh9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/priyanshusingh9"/>
    <language>en</language>
    <item>
      <title>From agentic development to agentic deployment: building Summit at hackCBS 9.0</title>
      <dc:creator>Priyanshu Singh</dc:creator>
      <pubDate>Tue, 03 Nov 2026 04:16:05 +0000</pubDate>
      <link>https://dev.to/priyanshusingh9/from-agentic-development-to-agentic-deployment-building-summit-at-hackcbs-90-4fng</link>
      <guid>https://dev.to/priyanshusingh9/from-agentic-development-to-agentic-deployment-building-summit-at-hackcbs-90-4fng</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/mlh-hackathon"&gt;MLH x DEV Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Summit is a Railway-style PaaS. You push code, and it builds and deploys your app on Kubernetes. It also runs AI agents as real workloads next to that app. Agents now write our code. We wanted them to deploy it and keep running after it ships.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why
&lt;/h3&gt;

&lt;p&gt;Coding agents changed how we write software. Our team wrote most of Summit with AI coding agents: we wrote the spec, the agent opened the PR, and we reviewed it. That part is agentic development, and it already works.&lt;/p&gt;

&lt;p&gt;Deployment hasn't caught up. Most platforms still treat an agent as an API call your app makes. Nothing keeps its state if the process dies. Nothing isolates the code it runs. Nothing tracks what it costs per user.&lt;/p&gt;

&lt;p&gt;So we built for the second half: &lt;strong&gt;agentic deployment&lt;/strong&gt;. A PaaS where a git push ships your app and your agents together, and those agents are durable, sandboxed and owned by a user and a project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F402rcf0m0bdznk73kwlk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F402rcf0m0bdznk73kwlk.png" alt="The loop" width="799" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How the deploy path works
&lt;/h3&gt;

&lt;p&gt;Summit is a pnpm + Turborepo monorepo: a Go control plane, a Next.js dashboard and an OpenAPI 3.1 contract shared between them.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Push.&lt;/strong&gt; A GitHub App receives the webhook. We verify the raw HMAC-SHA256 signature before anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record the intent.&lt;/strong&gt; The Go API writes the deployment and an &lt;code&gt;outbox_events&lt;/code&gt; row in the same Postgres transaction. Either both exist or neither does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dispatch.&lt;/strong&gt; The orchestrator claims outbox rows with &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;. If it crashes mid-job, the row is still there and another worker picks it up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build.&lt;/strong&gt; Each build attempt gets its own pod in a gVisor (&lt;code&gt;runsc&lt;/code&gt;) sandbox. Railpack detects the language and writes a build plan, and rootless BuildKit turns it into an OCI image pushed by digest. Customer code never touches the host kernel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run.&lt;/strong&gt; The reconciler turns intent into Kubernetes resources in a per-user namespace. Traefik gives it a public URL, Cilium enforces network policy, and a managed Postgres addon is one click away.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The outbox was the best decision we made. Every async step goes through it, so "the server restarted" never means "your deploy vanished."&lt;/p&gt;

&lt;h3&gt;
  
  
  How managed agents work
&lt;/h3&gt;

&lt;p&gt;This is the part I'm proudest of. An agent on Summit is a resource like a service: you create a definition, cut an immutable revision, deploy it to an environment and open sessions against it.&lt;/p&gt;

&lt;p&gt;Under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go stays the source of truth.&lt;/strong&gt; Agents, revisions, deployments, sessions and runs live in Postgres, scoped to &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;project_id&lt;/code&gt;. Every create takes an &lt;code&gt;Idempotency-Key&lt;/code&gt;. Replay the same request and you get the same resource; change the body and you get a &lt;code&gt;409&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One Rivet Actor per session.&lt;/strong&gt; The orchestrator hands &lt;code&gt;agent.run.submit&lt;/code&gt; events to a small TypeScript adapter. The adapter finds or creates the session's actor, keyed by &lt;code&gt;["summit-agent-v1", project_id, deployment_id, session_id]&lt;/code&gt;. Keys are generated server-side, never by the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pi Durable runs inside the actor.&lt;/strong&gt; Each actor owns exactly one Pi Durable conversation. We wrote a typed adapter so Pi stores its transcript and request IDs in the actor's own SQLite database (&lt;code&gt;c.db&lt;/code&gt;) instead of local files. Pi's full storage conformance suite passes on it: 25 of 25 cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State survives.&lt;/strong&gt; When an actor sleeps and wakes, it reopens the harness and recovers the whole conversation, plus its idempotency state. Delete a session and Rivet destroys that one actor and its storage. Other sessions don't notice.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;We first tried one actor per deployment holding many conversations. It failed on a real constraint: Pi Durable 1.4.0 has no API to delete a single conversation, so purging one user's session would have meant raw SQL against Pi's internal tables. One actor per session made deletion clean, so we switched.&lt;/p&gt;

&lt;p&gt;The next milestone is the execution boundary. Agent tools will run in a fresh gVisor sandbox per run attempt, with default-deny networking, and model calls will go through a gateway that hands out scoped credentials, so no raw provider key ever reaches an actor.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write the contract first.&lt;/strong&gt; We kept the OpenAPI file as the source of truth. With four people and a lot of agent-written code, that file stopped most integration fights before they started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents need specs, not prompts.&lt;/strong&gt; Our agents did their best work from tight docs: an &lt;code&gt;AGENTS.md&lt;/code&gt;, ADRs and small tickets. Vague asks got vague code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability is a design choice.&lt;/strong&gt; The outbox and the actor-per-session model both came from asking "what happens if this dies right now?"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Partner Technologies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub.&lt;/strong&gt; The GitHub App is the front door to Summit. We built the full flow in Go: an RS256 JWT for the app, cached installation tokens, raw HMAC webhook verification and a normalizer that turns push events into deploy intents. A worker credential broker gives each build a short-lived token scoped to its repo, so build pods never hold the app's private key. Repos, installations and deploy sources each have their own API handlers and their own tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean.&lt;/strong&gt; We were in the DigitalOcean track. Summit's fleet runs on DigitalOcean Droplets. Plain VMs meant we could set each machine up the way a PaaS needs, down to the gVisor runtime that our build sandboxes depend on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Gemini.&lt;/strong&gt; Gemini was our main coding agent. We used it to turn specs and tickets into PRs across the Go backend and the TypeScript adapter. It worked best with small, well-scoped tickets and a repo-level &lt;code&gt;AGENTS.md&lt;/code&gt; that described our conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hackathon Experience
&lt;/h2&gt;

&lt;p&gt;We built Summit at &lt;strong&gt;hackCBS 9.0&lt;/strong&gt; in Delhi as a team of four: Deepanshu, Rudra, Arpit and me. We split it the way the system splits. Deepanshu took the frontend, Arpit the platform database, Rudra the Kubernetes orchestrator, and I took the Go API, the builder and the agents.&lt;/p&gt;

&lt;p&gt;The event ran for 36 hours, and the management was really smooth. At any moment there were at least two people around to help us, and they sorted out whatever we needed on the spot. That let us spend the whole time on the hack itself.&lt;/p&gt;

&lt;p&gt;Summit is open source: &lt;a href="https://github.com/Deepanshu-2006/Summit" rel="noopener noreferrer"&gt;github.com/Deepanshu-2006/Summit&lt;/a&gt;. If you've tried running agents in production, I'd love to hear what broke first.&lt;/p&gt;

</description>
      <category>mlhacks</category>
      <category>devchallenge</category>
      <category>hackathon</category>
      <category>ai</category>
    </item>
    <item>
      <title>Follow the Money: Building a Graph-Based AML Investigator with FalkorDB</title>
      <dc:creator>Priyanshu Singh</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:18:20 +0000</pubDate>
      <link>https://dev.to/priyanshusingh9/follow-the-money-building-a-graph-based-aml-investigator-with-falkordb-4b5l</link>
      <guid>https://dev.to/priyanshusingh9/follow-the-money-building-a-graph-based-aml-investigator-with-falkordb-4b5l</guid>
      <description>&lt;p&gt;&lt;em&gt;Tripwire: a fraud investigator that walks the money trail in FalkorDB. Built for Graph Hacks (WeMakeDevs × FalkorDB), Track 1: Agents That Act on Connected Data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On day two, our investigator took 3.5 seconds per case. That was annoying. Then we found out it was also wrong. For our demo transaction, &lt;code&gt;hi8-tx-03863669&lt;/code&gt;, it said the sender had 2 counterparties and no fan-out. The real answer was 7 counterparties and 7 fan-out matches. One Cypher quirk had quietly cut the investigation window short.&lt;/p&gt;

&lt;p&gt;This post covers how we built an agent that follows money through a five-million-transaction graph, the query bugs that made it slow &lt;em&gt;and&lt;/em&gt; wrong, and the evaluation trap that almost gave us a headline number we'd have had to walk back.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/PriyanshuSingh9/tripwire" rel="noopener noreferrer"&gt;https://github.com/PriyanshuSingh9/tripwire&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A score tells you where to look
&lt;/h2&gt;

&lt;p&gt;A fraud model scores one row: an amount, two accounts, a timestamp. Laundering doesn't live in one row. It lives in what happens next: the receiver forwards almost the same amount an hour later, then the next account does it again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftpm45a6dhlmyjc3n9eyn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftpm45a6dhlmyjc3n9eyn.png" alt="Fig 1. The same transaction, before and after you follow the money." width="800" height="348"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 1. One row and one score, versus the same transaction traced: 65 paths, 15 transactions, 17 counterparty records.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tripwire splits the work up. LightGBM ranks transactions. When one is worth a look, an investigator walks the graph in FalkorDB and writes a report where every claim links to a transfer you can check. The design comes from Coinbase's write-up on their fraud agent: a bounded LLM reviewer that sits &lt;em&gt;behind&lt;/em&gt; the ML and the rules, not in place of them. We wanted to see how far that idea goes when the reviewer gets a graph instead of a flat history.&lt;/p&gt;
&lt;h2&gt;
  
  
  The graph, and the rule we didn't break
&lt;/h2&gt;

&lt;p&gt;The data is IBM's synthetic AMLworld &lt;strong&gt;HI-Small&lt;/strong&gt; set: 5,078,345 transactions over 18 days, with 5,177 labelled as laundering (fan-outs, fan-ins, cycles, layering).&lt;/p&gt;

&lt;p&gt;The rule: &lt;strong&gt;a feature can only use what was knowable before the transaction happened.&lt;/strong&gt; That means a frozen chronological split, same-minute groups kept in one split, half-open windows &lt;code&gt;[t - w, t)&lt;/code&gt;, and labels kept out of the graph, the features and the API. The rule paid off. Partway through, we caught our first feature file still carrying the label column, one careless join away from a fake result. Now features are built from an explicit allowlist, and labels are joined only inside the trainer.&lt;/p&gt;

&lt;p&gt;We started with &lt;code&gt;(:Account)-[:TRANSFER]-&amp;gt;(:Account)&lt;/code&gt;, then promoted every transfer to its own node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(:Account)-[:SENT]-&amp;gt;(:Transaction)-[:RECEIVED]-&amp;gt;(:Account)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpuka6290dk78f644imtr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpuka6290dk78f644imtr.png" alt="Fig 2. Every transfer is a node the agent can cite. Labels never enter the graph." width="800" height="319"&gt;&lt;/a&gt;. Every transfer is a node the agent can cite. Labels never enter the graph._&lt;/p&gt;

&lt;p&gt;The investigator's job is to &lt;em&gt;cite&lt;/em&gt; transfers, so each one needs a stable ID (&lt;code&gt;hi8-tx-{row}&lt;/code&gt;). A path it returns is then a list of real records, not something inferred from edges. &lt;code&gt;CALL db.propertyKeys()&lt;/code&gt; on the loaded graph shows no label property at all, so nothing, the agent included, can peek.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three tools, zero generated Cypher
&lt;/h2&gt;

&lt;p&gt;The agent never writes a query. It picks from three parameterised tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;trace_funds&lt;/code&gt;&lt;/strong&gt; follows money upstream or downstream, 1 to 3 hops, inside a time window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;inspect_counterparties&lt;/code&gt;&lt;/strong&gt; shows who's on each side and what else they've been doing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;detect_flow_patterns&lt;/code&gt;&lt;/strong&gt; checks for fan-out, fan-in and pass-through.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every tool has hard limits (depth 1–3, a window of 30 days at most, 100 results at most, a 5 s timeout), and every result comes back with transaction IDs, amounts, the path and &lt;code&gt;query_duration_ms&lt;/code&gt;. This is the outgoing depth-2 branch of &lt;code&gt;trace_funds&lt;/code&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="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;anchor:&lt;/span&gt;&lt;span class="n"&gt;Transaction&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;transaction_id:&lt;/span&gt; &lt;span class="n"&gt;$transaction_id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchor.timestamp_epoch&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;$time_window_seconds&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;anchor.timestamp_epoch&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;$time_window_seconds&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hi&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;anchor&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;:RECEIVED&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="n"&gt;a0&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;:SENT&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="n"&gt;t1&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;:RECEIVED&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="n"&gt;a1&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;:SENT&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="n"&gt;t2&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;:RECEIVED&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="n"&gt;a2&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;t1.timestamp_epoch&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t1.timestamp_epoch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;
  &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t2.timestamp_epoch&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t2.timestamp_epoch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;anchor.transaction_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t1.transaction_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t1.amount_paid&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;t2.transaction_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t2.amount_paid&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a0.account_key&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a1.account_key&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a2.account_key&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;$result_limit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At depth 1, the demo case returns 15 transfers, all one hop away: payments into the anchor's sender (its receiver sent nothing onward inside the window). At depth 2 the same bounded call returns 65 paths through the same 15 transactions: 15 one-hop records plus 50 two-hop chains that link them through the sender account, every record keeping its own ID and amount.&lt;/p&gt;

&lt;p&gt;Without a graph, that's a self-join on a five-million-row table for every hop. In FalkorDB, it's one pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM never gets the last word
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LightGBM&lt;/strong&gt; scores. A &lt;strong&gt;deterministic policy&lt;/strong&gt; turns the score into &lt;code&gt;allow&lt;/code&gt; or &lt;code&gt;review&lt;/code&gt;. The LLM can't change either one.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;investigator&lt;/strong&gt; picks the tools and drafts the report. Gemini is opt-in, and the default report is deterministic.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;verifier&lt;/strong&gt; checks every ID, path, amount and date in the report against what the tools actually returned. Anything that doesn't match gets dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0tza5y7a37n4pnqncc0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0tza5y7a37n4pnqncc0.png" alt="Fig 3. The LLM chooses graph tools and writes. It never scores, and it never gets the last word." width="800" height="415"&gt;&lt;/a&gt;. The LLM chooses graph tools and writes. It never scores, and it never gets the last word._&lt;/p&gt;

&lt;p&gt;Because of that, the agent can say no. On a quiet transaction from the same slice, &lt;code&gt;hi8-tx-03988788&lt;/code&gt;, the deterministic investigation returns &lt;code&gt;outcome: no_matching_path&lt;/code&gt;: zero paths, seven related transfers on record, and no attempt to dress the empty result up as safety. The workbench spells it out: "The graph was queried and nothing matched within these bounds. This is not evidence that the transaction is safe." For a compliance tool, a refusal like that matters as much as a catch.&lt;/p&gt;

&lt;p&gt;The same three tools are exposed over MCP: &lt;code&gt;api.mcp_graph&lt;/code&gt; serves them as typed, bounded MCP tools that delegate to the same parameterised queries, the agent calls them through an in-process MCP client session by default (&lt;code&gt;direct&lt;/code&gt; transport is one config flag away), and the same server runs over stdio for external clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  The day &lt;code&gt;fan_in&lt;/code&gt; took 891 ms
&lt;/h2&gt;

&lt;p&gt;On a one-million-transaction slice, one investigation (five tool calls) took about &lt;strong&gt;3.5 s&lt;/strong&gt;. Even an account with five transfers spent around a second finding zero matches. We found four causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The planner picked the wrong index.&lt;/strong&gt; For account-anchored queries, our range index on &lt;code&gt;timestamp_epoch&lt;/code&gt; made the planner scan &lt;em&gt;every transaction in the window&lt;/em&gt; (528,982 rows for one day) before filtering down to the account. We dropped that index and bound the indexed anchor with &lt;code&gt;WITH&lt;/code&gt; before expanding. Now cost grows with the account's degree, not with the size of the graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UNION ... ORDER BY ... LIMIT&lt;/code&gt; only applies to the last branch.&lt;/strong&gt; Our "latest activity" lookup returned the wrong epoch for 497 of the 500 accounts in our equivalence fixture. That's the bug that ended windows early and turned 7 counterparties into 2. It also let a 3-hop trace with &lt;code&gt;LIMIT 50&lt;/code&gt; return &lt;strong&gt;3,404 paths&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass-through built a cross product&lt;/strong&gt; of every incoming and outgoing leg before limiting. Now it builds a histogram of outgoing timestamps first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pointless second hops&lt;/strong&gt; went out just to read a key the transaction node already had.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx287tceu8wb5dnurrfrp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx287tceu8wb5dnurrfrp.png" alt="Fig 4. Dropping one index took a zero-match query from about 1 s to about 3 ms." width="800" height="348"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 4. Dropping one index took a zero-match query from about 1 s to about 3 ms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We froze the old queries and wrote equivalence tests before touching anything, then re-measured on the same slice:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Falrc80ozsu2tpec2pg4u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Falrc80ozsu2tpec2pg4u.png" alt="Fig 5. Same real slice, same case, P50 latency on a log scale." width="800" height="348"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 5. Same real slice, same case, P50 latency on a log scale.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then we loaded all 5,078,345 transactions: 521 s to load, every count reconciled, &lt;strong&gt;2.5 GB&lt;/strong&gt; of graph memory (we'd projected about 7 GiB from a sample). The demo case now investigates in &lt;strong&gt;23.2 ms P50&lt;/strong&gt;, and hub accounts with up to 169,756 connections take 207 ms P95.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0c4vfdc0ssawvp5k2eic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0c4vfdc0ssawvp5k2eic.png" alt="Fig 6. Measured beats projected. Serial, single client, shared dev machine." width="799" height="296"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 6. Measured beats projected. Serial, single client, shared dev machine.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Did the graph help the model? Yes, with a twist
&lt;/h2&gt;

&lt;p&gt;On the frozen test set (761,639 rows), all three candidate models were committed before any of them saw test data, and each was scored on test once. The selected model reached &lt;strong&gt;0.753 average precision&lt;/strong&gt; and caught 47.7% of laundering in the top 0.1% of alerts, up from 15.9% for the baseline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyvhkdqis9uljjhaxjy6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyvhkdqis9uljjhaxjy6.png" alt="Fig 7. One easy tail inflates the headline. The Sept 9-10 bulk number is the one to trust." width="800" height="385"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 7. One easy tail inflates the headline. The Sept 9-10 bulk number is the one to trust.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the twist. The last 1,108 test rows are &lt;strong&gt;59% laundering&lt;/strong&gt;, and every model finds them easily. That tail holds 42% of all test positives. The fair comparison is the Sept 9–10 bulk, where AP goes from &lt;strong&gt;0.088 to 0.600&lt;/strong&gt;, close to the 0.609 we saw on validation. We report both numbers.&lt;/p&gt;

&lt;p&gt;There was a second trap too. In our first iteration, graph features made AP &lt;em&gt;worse&lt;/em&gt; (0.159 → 0.152). The cause turned out to be one overfit model, not the features. Across seeds on validation, the same graph columns helped every time.&lt;/p&gt;

&lt;p&gt;One caveat on those headline numbers. Review caught afterwards that &lt;code&gt;log_received_over_paid&lt;/code&gt;, one of the feature-family columns, compared raw amounts even when the paid and received legs were in different currencies. The guard is in place now: the ratio is null unless both legs share one currency and are strictly positive, and a separate &lt;code&gt;currency_mismatch&lt;/code&gt; flag carries the mismatched case, with tests for mismatched, zero, and missing amounts. The 0.753 and 0.600 figures above were produced before that guard. Re-running the corrected candidate on the frozen test cohort means a new, ledgered test look, and that has not happened yet, so the numbers stay labelled pre-fix rather than quietly re-scored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where FalkorDB earned its place
&lt;/h2&gt;

&lt;p&gt;Computing offline features through FalkorDB took &lt;strong&gt;1,563 s for 5 columns&lt;/strong&gt;. DuckDB does &lt;strong&gt;76 features in 177 s&lt;/strong&gt;, and its graph columns match on all 5,078,345 rows. So batch features run as SQL, and FalkorDB answers the live, per-case questions: where did this money go, who's around it, does it look like a fan-in. Those answer in milliseconds, as a path you can show a person.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmrtxsg1k7f2e7os8jbgk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmrtxsg1k7f2e7os8jbgk.png" alt="Fig 8. Scan with SQL. Walk outward from one entity with the graph." width="799" height="244"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 8. Scan with SQL. Walk outward from one entity with the graph.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;FalkorDB features we leaned on: parameterised Cypher; range indexes on &lt;code&gt;account_key&lt;/code&gt; and &lt;code&gt;transaction_id&lt;/code&gt; (and learning when dropping one makes the planner better); &lt;code&gt;db.propertyKeys()&lt;/code&gt; as a leakage check; client-side &lt;code&gt;timeout=&lt;/code&gt; on &lt;code&gt;graph.query&lt;/code&gt;; &lt;code&gt;GRAPH.MEMORY USAGE&lt;/code&gt; and &lt;code&gt;INFO memory&lt;/code&gt; for capacity; and the FalkorDB Browser on localhost:3001 for eyeballing paths. No &lt;code&gt;algo.*&lt;/code&gt; procedures made the cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself on day one
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the graph for questions that start at one entity and walk outward, not for scans.&lt;/strong&gt; Index the wrong property and the planner turns the first kind into the second.&lt;/li&gt;
&lt;li&gt;Measure on real data early. Our 7 GiB projection and our 3.5 s investigation were both wrong.&lt;/li&gt;
&lt;li&gt;Freeze old queries before optimising. A faster answer that's different is a bug.&lt;/li&gt;
&lt;li&gt;Check where your positives sit in time.&lt;/li&gt;
&lt;li&gt;Make the LLM optional and the verifier mandatory.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Tripwire scores synthetic transactions with LightGBM, then investigates the risky ones by walking a five-million-transaction FalkorDB graph through three bounded tools. Every claim in its reports is checked against real transfers. Query fixes took an investigation from 3.5 s to about 25 ms and made it correct. On the hard part of the test set, past-only graph features lifted AP from 0.088 to 0.600. Synthetic data, research demo: it says nothing about real-world AML performance.&lt;/p&gt;

&lt;p&gt;What would you want an investigator agent to refuse to claim?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Kanishka (&lt;a href="https://github.com/Kaniisshka" rel="noopener noreferrer"&gt;@Kaniisshka&lt;/a&gt;), Ashwani Jha (&lt;a href="https://github.com/Ashwanijha1405" rel="noopener noreferrer"&gt;@Ashwanijha1405&lt;/a&gt;), Parth Garg (&lt;a href="https://github.com/Parth06102006" rel="noopener noreferrer"&gt;@Parth06102006&lt;/a&gt;), and Priyanshu Singh (&lt;a href="https://github.com/PriyanshuSingh9" rel="noopener noreferrer"&gt;@PriyanshuSingh9&lt;/a&gt;). The graph model, queries, evaluation protocol and decisions are ours, and we can explain every one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>database</category>
      <category>devchallenge</category>
    </item>
  </channel>
</rss>
