<?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: Oluwajuwon Omotayo</title>
    <description>The latest articles on DEV Community by Oluwajuwon Omotayo (@omotayojayone).</description>
    <link>https://dev.to/omotayojayone</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%2F1595229%2F319ae1a0-f55b-4c8f-8d35-1786cca8c527.png</url>
      <title>DEV Community: Oluwajuwon Omotayo</title>
      <link>https://dev.to/omotayojayone</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/omotayojayone"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Oluwajuwon Omotayo</dc:creator>
      <pubDate>Mon, 29 Jun 2026 09:49:21 +0000</pubDate>
      <link>https://dev.to/omotayojayone/-4089</link>
      <guid>https://dev.to/omotayojayone/-4089</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/omotayojayone/i-spent-months-building-the-ai-governance-layer-africa-was-missing-today-it-launches-em3" class="crayons-story__hidden-navigation-link"&gt;I Spent Months Building the AI Governance Layer Africa Was Missing. Today It Launches.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/omotayojayone" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F1595229%2F319ae1a0-f55b-4c8f-8d35-1786cca8c527.png" alt="omotayojayone profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/omotayojayone" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Oluwajuwon Omotayo
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Oluwajuwon Omotayo
                
              
              &lt;div id="story-author-preview-content-4019894" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/omotayojayone" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F1595229%2F319ae1a0-f55b-4c8f-8d35-1786cca8c527.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Oluwajuwon Omotayo&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/omotayojayone/i-spent-months-building-the-ai-governance-layer-africa-was-missing-today-it-launches-em3" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 29&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/omotayojayone/i-spent-months-building-the-ai-governance-layer-africa-was-missing-today-it-launches-em3" id="article-link-4019894"&gt;
          I Spent Months Building the AI Governance Layer Africa Was Missing. Today It Launches.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agents&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/omotayojayone/i-spent-months-building-the-ai-governance-layer-africa-was-missing-today-it-launches-em3#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>I Spent Months Building the AI Governance Layer Africa Was Missing. Today It Launches.</title>
      <dc:creator>Oluwajuwon Omotayo</dc:creator>
      <pubDate>Mon, 29 Jun 2026 09:48:33 +0000</pubDate>
      <link>https://dev.to/omotayojayone/i-spent-months-building-the-ai-governance-layer-africa-was-missing-today-it-launches-em3</link>
      <guid>https://dev.to/omotayojayone/i-spent-months-building-the-ai-governance-layer-africa-was-missing-today-it-launches-em3</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;comply54 is now live: an open-source AI agent compliance framework built specifically for African regulated industries&lt;/li&gt;
&lt;li&gt;It enforces African law at runtime, before your agent acts — not after an audit finds the violation&lt;/li&gt;
&lt;li&gt;21 policy packs across 12 jurisdictions: NDPA 2023, CBN controls, NIMC Act 2026, NFIU-AML, POPIA, Kenya DPA, and more&lt;/li&gt;
&lt;li&gt;Works with LangChain, LangGraph, CrewAI, and AutoGen via one import&lt;/li&gt;
&lt;li&gt;Violations return the exact law, section, and penalty — not a vague error message&lt;/li&gt;
&lt;li&gt;100% offline. Full audit trail. Apache 2.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://comply54.io" rel="noopener noreferrer"&gt;comply54.io&lt;/a&gt; | &lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/comply54/comply54" rel="noopener noreferrer"&gt;github.com/comply54/comply54&lt;/a&gt; | &lt;strong&gt;Product Hunt:&lt;/strong&gt; &lt;a href="https://www.producthunt.com/products/comply54" rel="noopener noreferrer"&gt;producthunt.com/products/comply54&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The problem I kept running into
&lt;/h2&gt;

&lt;p&gt;I build AI products in Lagos. Over the past year, I kept running into the same gap.&lt;/p&gt;

&lt;p&gt;AI agent frameworks like LangChain, LangGraph, CrewAI, and AutoGen are exceptional at giving agents capability. Memory, reasoning, tool use, multi-agent orchestration. They are well-designed, actively maintained, and widely adopted.&lt;/p&gt;

&lt;p&gt;None of them knows what the Central Bank of Nigeria says about transaction thresholds. None of them knows that Nigeria's NIMC Act 2026 prohibits retaining NIN data after a verification completes. None of them knows that the NFIU requires a Currency Transaction Report within 24 hours for transactions above a defined threshold.&lt;/p&gt;

&lt;p&gt;This is not a criticism of those frameworks. They were built for general capability, not jurisdiction-specific compliance. But it creates a real problem for anyone building AI agents that operate in African regulated industries.&lt;/p&gt;

&lt;p&gt;The agent can transfer money, access customer records, store identity data, and make financial decisions, all correctly from the framework's perspective, and still violate the law in ways that carry fines in the tens of millions of naira and criminal liability for responsible individuals.&lt;/p&gt;

&lt;p&gt;Every team building in this space is solving this problem on their own, from scratch, in ways that drift and accumulate technical debt. That is the infrastructure gap comply54 exists to close.&lt;/p&gt;




&lt;h2&gt;
  
  
  What comply54 does
&lt;/h2&gt;

&lt;p&gt;comply54 is a runtime policy enforcement layer for AI agents. It sits between your agent's decision to act and the tool execution that carries it out.&lt;/p&gt;

&lt;p&gt;Before any action runs, comply54 evaluates it against the applicable policy packs and returns one of four decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;allow&lt;/strong&gt; — proceed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;audit&lt;/strong&gt; — proceed but log with full regulatory context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;escalate&lt;/strong&gt; — pause and request human approval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;block&lt;/strong&gt; — prevent execution and return the exact violation with citation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a violation fires, the response is not a generic error. It is a structured object containing the regulation name, the specific section, the penalty clause, a unique audit ID, and the remediation guidance. Your compliance team can produce it to a regulator. Your engineering team can write tests against it.&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;comply54&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ComplyEngine&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ComplyEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jurisdictions&lt;/span&gt;&lt;span class="o"&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;NG&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="s"&gt;ZA&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="s"&gt;KE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transfer_funds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&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;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6_500_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&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="s"&gt;NGN&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="s"&gt;customer_tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&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;decision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# "block"
&lt;/span&gt;&lt;span class="nf"&gt;print&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;regulation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# "CBN NIP Framework 2023"
&lt;/span&gt;&lt;span class="nf"&gt;print&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;section&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# "Schedule 2, Tier 2 Transaction Limits"
&lt;/span&gt;&lt;span class="nf"&gt;print&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;penalty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# "Regulatory sanction, licence review"
&lt;/span&gt;&lt;span class="nf"&gt;print&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;audit_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# "cmp_01J3X..."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For JavaScript and TypeScript developers, the same API is available via the npm package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ComplyEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;comply54&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;engine&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;ComplyEngine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jurisdictions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;KE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;export_customer_data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dataCategories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;personal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;biometric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;customerCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// result.decision === "block"&lt;/span&gt;
&lt;span class="c1"&gt;// result.regulation === "NDPA 2023"&lt;/span&gt;
&lt;span class="c1"&gt;// result.section === "Section 25 — Cross-border Data Transfer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Framework integrations
&lt;/h2&gt;

&lt;p&gt;comply54 ships integration adapters for the four major agent frameworks. The goal is that adding compliance to an existing agent should take minutes, not days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangGraph:&lt;/strong&gt;&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;comply54.integrations.langgraph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;comply54_node&lt;/span&gt;

&lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comply54&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;comply54_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent&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="s"&gt;comply54&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comply54&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comply_decision&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allow&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="s"&gt;tools&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="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalate&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="s"&gt;human_review&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CrewAI:&lt;/strong&gt;&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;comply54.integrations.crewai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Comply54Tool&lt;/span&gt;

&lt;span class="n"&gt;compliance_tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Comply54Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jurisdiction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;compliance_tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="n"&gt;other_tools&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AutoGen and LangChain&lt;/strong&gt; follow the same pattern with their respective adapters.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is covered: 21 policy packs, 12 jurisdictions
&lt;/h2&gt;

&lt;p&gt;Every policy pack ships with the full regulatory citations embedded. Here is what is live today:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nigeria (7 packs)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NDPA 2023 — data residency, consent, cross-border transfer controls&lt;/li&gt;
&lt;li&gt;CBN Transaction Controls — tiered KYC limits, NIP thresholds, agent banking rules&lt;/li&gt;
&lt;li&gt;NIMC Act 2026 — NIN data prohibition post-verification, retention controls&lt;/li&gt;
&lt;li&gt;NFIU AML/STR — CTR thresholds, structuring detection, velocity controls&lt;/li&gt;
&lt;li&gt;BVN/NIN Protection — exposure blocking, log sanitization, verification gating&lt;/li&gt;
&lt;li&gt;POS Geo-Fencing — terminal location controls, cross-state transaction rules&lt;/li&gt;
&lt;li&gt;CBN Open Banking — consent validation, data minimisation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;South Africa (2 packs)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;POPIA — special information, cross-border adequacy, responsible party audit&lt;/li&gt;
&lt;li&gt;FSCA AI Conduct — suitability controls for financial AI recommendations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Kenya (2 packs)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;KDPA 2019 — transfer restrictions, sensitive data, data minimisation&lt;/li&gt;
&lt;li&gt;CBK Digital Credit — credit scoring controls, disclosure requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Regional and additional (10 packs)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ECOWAS cross-border transfer rules&lt;/li&gt;
&lt;li&gt;Rwanda, Ghana, Ethiopia, Tanzania, Uganda, Botswana, Mauritius, Egypt data protection&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why 100% offline matters
&lt;/h2&gt;

&lt;p&gt;comply54 evaluates entirely in-process. No network call is made during enforcement. No third-party API is consulted.&lt;/p&gt;

&lt;p&gt;This matters for three reasons.&lt;/p&gt;

&lt;p&gt;First, latency. A compliance check that adds 200ms to every tool call is a compliance check that engineers will route around. In-process evaluation adds single-digit milliseconds.&lt;/p&gt;

&lt;p&gt;Second, reliability. A governance layer that requires a network call to function will fail when the network fails. That is the worst possible time for your compliance layer to go offline.&lt;/p&gt;

&lt;p&gt;Third, data sovereignty. In jurisdictions with strict data residency requirements, sending decision context to an external service to evaluate compliance may itself be a compliance violation. Offline evaluation avoids this entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The audit trail
&lt;/h2&gt;

&lt;p&gt;Every evaluation — allow, audit, escalate, or block — generates a structured audit entry:&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;"audit_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cmp_01J3XKRM..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-29T08:42:11.203Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transfer_funds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"block"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"regulation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CBN NIP Framework 2023"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"section"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Schedule 2, Tier 2 Transaction Limits"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"penalty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Regulatory sanction, licence review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jurisdiction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payment-agent-v2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sess_8f2k..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"context_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:4a9c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evaluation_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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;p&gt;The &lt;code&gt;context_hash&lt;/code&gt; is a SHA-256 of the sanitised evaluation context — PII is never stored in the audit log, only its hash. This satisfies the NDPA's accountability requirements without creating a new data protection liability.&lt;/p&gt;




&lt;h2&gt;
  
  
  What comes from the earlier work
&lt;/h2&gt;

&lt;p&gt;comply54 did not appear from nowhere. The policy corpus builds directly on work I have been doing in public over the past few months.&lt;/p&gt;

&lt;p&gt;The African regulatory policy pack I contributed to Microsoft's Agent Governance Toolkit — which was merged into &lt;code&gt;microsoft/agent-governance-toolkit&lt;/code&gt; as PR #3077 — is the upstream source of the policy logic in comply54. The agt-policies-nigeria repo, which covers nine African countries and ECOWAS in OPA/Rego format, is the foundation that comply54's Python and TypeScript implementations are built on.&lt;/p&gt;

&lt;p&gt;If you contributed to, starred, or used any of those earlier projects, comply54 is the production-ready version of that work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is next
&lt;/h2&gt;

&lt;p&gt;The roadmap includes sector-specific profiles for fintech, healthtech, insurtech, and government AI deployments. The &lt;code&gt;@comply54/adapter-eve&lt;/code&gt; package for Vercel's Eve framework is in active development. Support for additional East and West African jurisdictions is planned based on community interest.&lt;/p&gt;

&lt;p&gt;The project is fully open source under Apache 2.0. Issues, PRs, and regulatory corrections from practitioners with real compliance domain expertise are the most valuable contributions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;Two questions I genuinely want input on from this community:&lt;/p&gt;

&lt;p&gt;First, if you are building AI agents for regulated industries outside Africa — in US healthcare, EU financial services, or similar — what does your compliance layer currently look like? Are you building it from scratch, using an existing tool, or deferring it?&lt;/p&gt;

&lt;p&gt;Second, if you work in Nigerian or African fintech and have spotted a regulatory gap in the policy packs, I want to hear about it. The most valuable thing a compliance practitioner can contribute is a citation: which regulation, which section, which agent action pattern it should govern.&lt;/p&gt;

&lt;p&gt;If comply54 is useful, an upvote on &lt;a href="https://www.producthunt.com/products/comply54" rel="noopener noreferrer"&gt;Product Hunt&lt;/a&gt; today would mean a lot. And a star on &lt;a href="https://github.com/comply54/comply54" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other developers find it.&lt;/p&gt;

&lt;p&gt;Thank you for reading.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a Policy Enforcement Layer for Vercel's Eve Agent Framework. Here's What I Learned About AI Governance the Hard Way.</title>
      <dc:creator>Oluwajuwon Omotayo</dc:creator>
      <pubDate>Mon, 22 Jun 2026 11:59:37 +0000</pubDate>
      <link>https://dev.to/omotayojayone/i-built-a-policy-enforcement-layer-for-vercels-eve-agent-framework-heres-what-i-learned-about-ai-1758</link>
      <guid>https://dev.to/omotayojayone/i-built-a-policy-enforcement-layer-for-vercels-eve-agent-framework-heres-what-i-learned-about-ai-1758</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Vercel's Eve (2.2k stars, actively developed) is a filesystem-first framework for building durable backend agents — but it ships with no governance layer&lt;/li&gt;
&lt;li&gt;I built &lt;a href="https://github.com/kingztech2019/eve-policy" rel="noopener noreferrer"&gt;&lt;code&gt;eve-policy&lt;/code&gt;&lt;/a&gt;, an open source policy enforcement library that wires into Eve's &lt;code&gt;needsApproval&lt;/code&gt; and &lt;code&gt;execute&lt;/code&gt; lifecycle with four-tier semantics: deny, escalate, audit, allow&lt;/li&gt;
&lt;li&gt;The most interesting design challenge: Eve's &lt;code&gt;needsApproval&lt;/code&gt; is synchronous and session-less, while &lt;code&gt;execute&lt;/code&gt; is async with full session context — which forced a two-phase evaluation model&lt;/li&gt;
&lt;li&gt;Built-in profiles include OWASP Agentic Top 10 (2026) and a financial services baseline covering USD/NGN/KES CTR thresholds&lt;/li&gt;
&lt;li&gt;Fail-closed by default: no matching rule means deny, not allow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/kingztech2019/eve-policy" rel="noopener noreferrer"&gt;github.com/kingztech2019/eve-policy&lt;/a&gt; · &lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/eve-policy" rel="noopener noreferrer"&gt;npmjs.com/package/eve-policy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Eve needs a governance layer
&lt;/h2&gt;

&lt;p&gt;Vercel's &lt;a href="https://github.com/vercel/eve" rel="noopener noreferrer"&gt;Eve&lt;/a&gt; is a framework for building durable backend agents — filesystem-first, TypeScript-native, deeply integrated with Vercel's infrastructure. You define an agent as a directory of files: &lt;code&gt;instructions.md&lt;/code&gt;, &lt;code&gt;tools/&lt;/code&gt;, &lt;code&gt;skills/&lt;/code&gt;, &lt;code&gt;channels/&lt;/code&gt;, &lt;code&gt;subagents/&lt;/code&gt;. It compiles to inspectable artifacts, exposes a stable HTTP protocol with &lt;code&gt;sessionId&lt;/code&gt; and &lt;code&gt;continuationToken&lt;/code&gt;, and manages the full lifecycle of agent runs.&lt;/p&gt;

&lt;p&gt;It is a genuinely well-designed framework. Eve thinks carefully about durability, composability, and the operational reality of running agents in production.&lt;/p&gt;

&lt;p&gt;What it does not ship with is a governance layer.&lt;/p&gt;

&lt;p&gt;That is not a criticism. Frameworks generally do not dictate compliance policy — that is a product concern, not a framework concern. But it creates a gap that anyone building a real AI agent with Eve will eventually hit.&lt;/p&gt;

&lt;p&gt;If your agent can call a &lt;code&gt;transfer_funds&lt;/code&gt; tool, who decides whether a ₦6.5 million transfer requires human approval? If your agent has a subagent that can write to a data store, what prevents that subagent from calling tools it should not? If your agent handles BVN data, how do you ensure it never appears in an audit log?&lt;/p&gt;

&lt;p&gt;These are governance questions. They do not belong scattered across individual tool implementations — that creates drift. They do not belong inside the agent loop — that couples your business rules to the framework. They belong in a dedicated layer that sits between Eve's lifecycle events and your tools' implementations.&lt;/p&gt;

&lt;p&gt;That is what &lt;code&gt;eve-policy&lt;/code&gt; is.&lt;/p&gt;




&lt;h2&gt;
  
  
  The design challenge: Eve's two-phase lifecycle
&lt;/h2&gt;

&lt;p&gt;The most technically interesting part of building &lt;code&gt;eve-policy&lt;/code&gt; was figuring out how to fit governance into Eve's lifecycle correctly.&lt;/p&gt;

&lt;p&gt;Eve exposes two lifecycle hooks on every tool:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;needsApproval(ctx)&lt;/code&gt;&lt;/strong&gt; — called synchronously before the turn completes, to decide whether to pause execution and wait for human approval. The context at this point is minimal: you have the tool name and the tool input, but no session yet. This hook must return a boolean synchronously — you cannot await anything here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;execute(input, ctx)&lt;/code&gt;&lt;/strong&gt; — called asynchronously when the tool actually runs. By this point, the full session context is available: &lt;code&gt;session.id&lt;/code&gt;, &lt;code&gt;session.auth&lt;/code&gt;, &lt;code&gt;session.parent&lt;/code&gt; (whether the caller is a subagent), and the complete turn history.&lt;/p&gt;

&lt;p&gt;This split forced a two-phase evaluation model in &lt;code&gt;eve-policy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 (approval time):&lt;/strong&gt; Input-based controls fire in &lt;code&gt;needsApproval&lt;/code&gt;. Does the amount exceed a reporting threshold? Does the input contain a card PAN pattern? Does the tool name match a blocked pattern? These questions can be answered from the tool input alone — no session needed, evaluation stays synchronous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 (execution time):&lt;/strong&gt; Session-aware controls fire in &lt;code&gt;execute&lt;/code&gt;. Is the caller a subagent? What is the principal type? Is this an already-approved tool? These questions require &lt;code&gt;ToolContext.session&lt;/code&gt;, which is only available in &lt;code&gt;execute&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same policy evaluates twice, but the context it has access to is different each time. This means a rule like "escalate financial writes from subagents" will see &lt;code&gt;isSubagentCall()&lt;/code&gt; return &lt;code&gt;false&lt;/code&gt; in &lt;code&gt;needsApproval&lt;/code&gt; (no session yet) and &lt;code&gt;true&lt;/code&gt; in &lt;code&gt;execute&lt;/code&gt; (session available). The first evaluation handles the approval decision; the second handles the execution decision.&lt;/p&gt;

&lt;p&gt;Here is what that looks like in the evaluation flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Eve lifecycle                    eve-policy evaluation
─────────────────────────────────────────────────────

  turn starts
       │
       ▼
  needsApproval(ctx)  ──────►  evaluatePolicy(policy, approvalCtx)
       │                            input-based rules only:
       │                            amountExceeds, fieldMatches,
       │                            toolNameIs, ...
       │  escalate? → true
       │  otherwise → false
       │
       │  [human approves if needsApproval=true]
       │
       ▼
  execute(input, ctx) ──────►  evaluatePolicy(policy, executeCtx)
       │                            session-aware rules now available:
       │                            isSubagentCall(), principalTypeIs(),
       │                            alreadyApproved(), ...
       │
       ├── deny?     → throw PolicyDenialError  (tool never runs)
       ├── escalate? → run tool, write audit entry
       ├── audit?    → run tool, write audit entry
       └── allow?    → run tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: governance decisions on the hot path must never block on I/O. All rule match predicates are synchronous by design. Async work (audit logging) runs fire-and-forget after the decision is made.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation and quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add eve-policy
&lt;span class="c"&gt;# or&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;eve-policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements: Node.js &amp;gt;= 18, Eve &amp;gt;= 0.10.0&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;definePolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withNamedPolicy&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;escalate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;audit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy/rules&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;amountExceeds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isSubagentCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolNameIs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy/rules&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FileAuditLogger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy/audit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Define your policy&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transferPolicy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;definePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transfer-controls&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-self-transfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Source and destination accounts cannot be the same&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="nf"&gt;escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ctr-threshold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;amountExceeds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Exceeds $10,000 CTR threshold — compliance review required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;riskLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;owaspRisks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASI02&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;escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subagent-financial-write&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isSubagentCall&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;toolNameIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transfer_funds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Financial writes from subagents require human approval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;riskLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;owaspRisks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASI03&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;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all-transfers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;toolNameIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transfer_funds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;All fund transfers logged for compliance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Wrap your Eve tool — one line, drop-in replacement&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;safeTool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withNamedPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transfer_funds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;transferTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;transferPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;auditLogger&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;FileAuditLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/var/log/agent-audit.jsonl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Use it in your agent exactly as before&lt;/span&gt;
&lt;span class="c1"&gt;// Eve calls needsApproval and execute automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;withNamedPolicy&lt;/code&gt; returns a new &lt;code&gt;ToolDefinition&lt;/code&gt; — same shape, same interface, fully compatible with Eve. The governance layer is invisible to the rest of the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The four effects
&lt;/h2&gt;

&lt;p&gt;Every rule has an effect: &lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;escalate&lt;/code&gt;, &lt;code&gt;audit&lt;/code&gt;, or &lt;code&gt;allow&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;&lt;code&gt;needsApproval&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;execute&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Audit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;throws &lt;code&gt;PolicyDenialError&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;escalate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;runs tool if approved&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;audit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;runs tool&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;runs tool&lt;/td&gt;
&lt;td&gt;opt-in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rules are evaluated in declaration order. First match wins. When no rule matches, &lt;code&gt;defaultEffect&lt;/code&gt; applies — and the default is &lt;code&gt;"deny"&lt;/code&gt;, not &lt;code&gt;"allow"&lt;/code&gt;. A governance layer with no matching rules should not silently permit execution.&lt;/p&gt;

&lt;p&gt;If you want allow-by-default with specific restrictions, set &lt;code&gt;defaultEffect: "allow"&lt;/code&gt; explicitly. This forces you to make the intention clear rather than discovering it by accident.&lt;/p&gt;




&lt;h2&gt;
  
  
  Composing policies
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;compose()&lt;/code&gt; is how production policies are built. It flattens rules from multiple component policies and applies deny-wins semantics for the default effect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compose&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;owaspTop10Policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;financialBaselinePolicy&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy/profiles&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;myAgentPolicy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;owaspTop10Policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// 20 rules covering ASI01–ASI10&lt;/span&gt;
  &lt;span class="nx"&gt;financialBaselinePolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// CTR thresholds, sanctions, card data, KYC&lt;/span&gt;
  &lt;span class="nx"&gt;myDomainPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// your own business rules&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// compose() guarantees:&lt;/span&gt;
&lt;span class="c1"&gt;// - Rules evaluated in order across all component policies&lt;/span&gt;
&lt;span class="c1"&gt;// - First match wins within a single evaluation&lt;/span&gt;
&lt;span class="c1"&gt;// - defaultEffect is the STRICTEST across all composed policies&lt;/span&gt;
&lt;span class="c1"&gt;// - If any component is deny, composed policy is deny&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important constraint: avoid &lt;code&gt;always()&lt;/code&gt; catch-all rules inside composable policies. If a component policy catches every call with &lt;code&gt;always()&lt;/code&gt;, it prevents later policies' specific rules from ever firing. Use &lt;code&gt;defaultEffect&lt;/code&gt; for fallthrough behaviour instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Built-in profiles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  OWASP Agentic Top 10 (2026)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;owaspTop10Policy&lt;/code&gt; provides coverage for all ten risks in the OWASP Top 10 for Agentic Applications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ASI01 (Goal Hijack):&lt;/strong&gt; Deny shell tools, prompt injection patterns, goal-modification language&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASI02 (Tool Misuse):&lt;/strong&gt; Deny unapproved file writes and card PAN in input; escalate network calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASI03 (Identity Abuse):&lt;/strong&gt; Escalate privileged tools invoked from subagents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASI04 (Supply Chain):&lt;/strong&gt; Escalate runtime package installs and dynamic tool registration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASI05 (Code Execution):&lt;/strong&gt; Deny &lt;code&gt;eval()&lt;/code&gt;/&lt;code&gt;exec()&lt;/code&gt; patterns; escalate code runner tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASI06 (Context Poisoning):&lt;/strong&gt; Escalate memory writes; audit memory reads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASI07 (Inter-Agent Comms):&lt;/strong&gt; Escalate cross-agent and cross-session invocations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASI08 (Cascading Failures):&lt;/strong&gt; Escalate bulk operations exceeding 100 items&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASI09 (Trust Exploitation):&lt;/strong&gt; Deny impersonation language; escalate approval-bypass patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASI10 (Rogue Agents):&lt;/strong&gt; Deny self-replication and self-deployment; escalate autonomous scheduling&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Financial services baseline
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;financialBaselinePolicy&lt;/code&gt; covers jurisdiction-agnostic financial controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deny self-transfers, negative amounts, sanctioned countries (OFAC/FATF list), card PANs, CVV in input&lt;/li&gt;
&lt;li&gt;Escalate USD/NGN/KES CTR thresholds, international wire transfers, KYC bypass language, subagent financial writes&lt;/li&gt;
&lt;li&gt;Audit customer PII access, moderate-value transactions, report generation&lt;/li&gt;
&lt;li&gt;Allow explicit read-only tools (&lt;code&gt;get_balance&lt;/code&gt;, &lt;code&gt;get_exchange_rate&lt;/code&gt;, &lt;code&gt;check_kyc_status&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The audit trail
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;escalate&lt;/code&gt;, and &lt;code&gt;audit&lt;/code&gt; decision writes a structured entry to your configured logger. The schema:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuditEntry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// UUID v4&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// ISO 8601&lt;/span&gt;
  &lt;span class="nl"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;ruleName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// which rule fired (or "default")&lt;/span&gt;
  &lt;span class="nl"&gt;policyName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;riskLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;owaspRisks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="c1"&gt;// e.g. ["ASI01", "ASI03"]&lt;/span&gt;
  &lt;span class="nl"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sanitizedInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// secrets redacted automatically&lt;/span&gt;
  &lt;span class="nl"&gt;evaluationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;denied&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;executed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automatic sanitization replaces keys matching &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;cvv&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;, &lt;code&gt;ssn&lt;/code&gt;, &lt;code&gt;nin&lt;/code&gt;, &lt;code&gt;pin&lt;/code&gt; with &lt;code&gt;[REDACTED]&lt;/code&gt;. Strings over 500 characters are truncated. The audit log itself should never become a liability.&lt;/p&gt;

&lt;p&gt;Four backends ship out of the box: &lt;code&gt;ConsoleAuditLogger&lt;/code&gt;, &lt;code&gt;FileAuditLogger&lt;/code&gt;, &lt;code&gt;MultiAuditLogger&lt;/code&gt;, and &lt;code&gt;InMemoryAuditLogger&lt;/code&gt; (for tests).&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing without a live runtime
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;evaluatePolicy&lt;/code&gt; runs the policy evaluation synchronously without needing an Eve runtime. Combined with &lt;code&gt;InMemoryAuditLogger&lt;/code&gt;, you can write comprehensive policy tests with standard unit test tooling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;evaluatePolicy&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InMemoryAuditLogger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy/audit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PolicyContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="nx"&gt;PolicyContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toolInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;approvedTools&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;Set&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;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transferPolicy&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="s2"&gt;escalates NGN transfers above ₦5M&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluatePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;transferPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transfer_funds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NGN&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effect&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalate&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owaspRisks&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASI02&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;denies self-transfers&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluatePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;transferPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nf"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transfer_funds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acc-001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acc-001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;})&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="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effect&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allows read-only balance check&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluatePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transferPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_balance&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effect&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No network calls. No Eve process. Fast and deterministic.&lt;/p&gt;




&lt;h2&gt;
  
  
  OWASP coverage reporting
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;uncoveredOwaspRisks()&lt;/code&gt; tells you which ASI risks your policy does not address. Use it as a CI gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uncoveredOwaspRisks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;owaspCoverageReport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eve-policy&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;gaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uncoveredOwaspRisks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myPolicy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Policy missing OWASP coverage: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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;This is the kind of thing that prevents "we have a governance layer" from drifting into "we have a governance layer with an unnoticed gap in ASI07."&lt;/p&gt;




&lt;h2&gt;
  
  
  What is coming next
&lt;/h2&gt;

&lt;p&gt;The jurisdiction-specific policy packs — NDPA 2023, CBN AML/CFT, NFIU reporting, POPIA, Kenya DPA — are being developed as part of a separate package called &lt;code&gt;@comply54/adapter-eve&lt;/code&gt;, which will provide pre-composed African regulatory profiles for direct use with &lt;code&gt;eve-policy&lt;/code&gt;. That package is under active development and not yet released.&lt;/p&gt;

&lt;p&gt;The underlying policy corpus (in OPA/Rego format) is already open source as &lt;a href="https://github.com/kingztech2019/agt-policies-nigeria" rel="noopener noreferrer"&gt;&lt;code&gt;agt-policies-nigeria&lt;/code&gt;&lt;/a&gt;, which was contributed upstream to &lt;a href="https://github.com/microsoft/agent-governance-toolkit" rel="noopener noreferrer"&gt;microsoft/agent-governance-toolkit&lt;/a&gt; earlier this month. The &lt;code&gt;@comply54/adapter-eve&lt;/code&gt; package will port those same frameworks into &lt;code&gt;eve-policy&lt;/code&gt;'s TypeScript rule format.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;Two questions I am genuinely curious about from people building with Eve or similar agent frameworks:&lt;/p&gt;

&lt;p&gt;First — how are you currently handling governance for tools that need human approval? Are you implementing &lt;code&gt;needsApproval&lt;/code&gt; directly in each tool, centralising it somewhere, or leaving it for later?&lt;/p&gt;

&lt;p&gt;Second — the fail-closed default (&lt;code&gt;defaultEffect: "deny"&lt;/code&gt;) felt like the obvious choice to me, but I have seen arguments for fail-open defaults in development environments. What is your team's approach?&lt;/p&gt;

&lt;p&gt;Drop your thoughts in the comments. And if &lt;code&gt;eve-policy&lt;/code&gt; is useful, a star on the &lt;a href="https://github.com/kingztech2019/eve-policy" rel="noopener noreferrer"&gt;repo&lt;/a&gt; helps others find it.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Found a Gap in Microsoft's AI Governance Toolkit. So I Built a Policy Pack to Fill It.</title>
      <dc:creator>Oluwajuwon Omotayo</dc:creator>
      <pubDate>Sun, 14 Jun 2026 11:34:16 +0000</pubDate>
      <link>https://dev.to/omotayojayone/i-found-a-gap-in-microsofts-ai-governance-toolkit-so-i-built-a-policy-pack-to-fill-it-2lf7</link>
      <guid>https://dev.to/omotayojayone/i-found-a-gap-in-microsofts-ai-governance-toolkit-so-i-built-a-policy-pack-to-fill-it-2lf7</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft's &lt;a href="https://github.com/microsoft/agent-governance-toolkit" rel="noopener noreferrer"&gt;Agent Governance Toolkit (AGT)&lt;/a&gt; ships policy coverage for OWASP Agentic AI Top 10, NIST AI RMF, EU AI Act, SOC 2, and HIPAA — but nothing for African regulatory frameworks&lt;/li&gt;
&lt;li&gt;I built &lt;a href="https://github.com/kingztech2019/agt-policies-nigeria" rel="noopener noreferrer"&gt;&lt;code&gt;agt-policies-nigeria&lt;/code&gt;&lt;/a&gt;, an open source community policy pack covering NDPA 2023 (Nigeria), CBN transaction limits, POS geo-fencing, BVN/NIN protection, NFIU AML/STR, and POPIA (South Africa)&lt;/li&gt;
&lt;li&gt;Drop-in YAML policy files — no new infrastructure, no SDK changes, compatible with AGT's &lt;code&gt;agent-os-kernel&lt;/code&gt; via &lt;code&gt;GovernancePolicy&lt;/code&gt; + &lt;code&gt;PolicyInterceptor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Includes a live demo: a Nigerian fintech support agent attempts 5 actions, the governance layer intercepts each one in real time&lt;/li&gt;
&lt;li&gt;MIT licensed, contributions welcome — especially from anyone with direct CBN/NDPA/NFIU compliance experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/kingztech2019/agt-policies-nigeria" rel="noopener noreferrer"&gt;github.com/kingztech2019/agt-policies-nigeria&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;If you have looked at Microsoft's Agent Governance Toolkit, you know it is a solid piece of infrastructure for putting policy guardrails around what AI agents are allowed to do. Block certain outputs, require human approval for certain actions, audit every tool call. The policy coverage that ships with it maps onto some of the most important global frameworks: OWASP's Agentic AI Top 10, NIST's AI Risk Management Framework, the EU AI Act, SOC 2, HIPAA.&lt;/p&gt;

&lt;p&gt;If you are building AI agents for a US healthcare company or a European fintech, AGT gives you policy packs that map directly onto your regulatory environment.&lt;/p&gt;

&lt;p&gt;I went looking for the African equivalent. Specifically, I wanted to know: if I am building an AI agent for a Nigerian fintech — something that touches BVNs, NINs, CBN transaction limits, NDPA data residency rules — is there an AGT policy pack for that?&lt;/p&gt;

&lt;p&gt;There is not. AGT covers five major global frameworks and zero African ones.&lt;/p&gt;

&lt;p&gt;This is not a criticism of the AGT team. It is just a reflection of where the global AI governance conversation currently sits. Nobody contributing to AGT is thinking about NFIU suspicious transaction thresholds because that is not the regulatory environment most contributors operate in.&lt;/p&gt;

&lt;p&gt;But AI agents are being deployed in Nigerian fintech, insurtech, and banking right now, making decisions that touch regulated financial data and sensitive personal identifiers. There was no governance tooling built for that context.&lt;/p&gt;

&lt;p&gt;So I built a community policy pack to fill it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What &lt;code&gt;agt-policies-nigeria&lt;/code&gt; covers
&lt;/h2&gt;

&lt;p&gt;Six policy packs, each mapping to a specific African regulatory framework, each shipped as a drop-in YAML file.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ndpa-data-residency.yaml&lt;/code&gt; — Nigeria Data Protection Act 2023
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Blocks agent actions that route personal data outside Nigeria without adequate safeguards&lt;/li&gt;
&lt;li&gt;Requires approval for bulk data export operations&lt;/li&gt;
&lt;li&gt;Denies processing of sensitive personal data (health, biometric, ethnic origin) without conditions&lt;/li&gt;
&lt;li&gt;Audits all PII-touching tool calls for NDPC accountability requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;cbn-transaction-limits.yaml&lt;/code&gt; — Central Bank of Nigeria regulations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tiered KYC limits: Tier 1 accounts capped at ₦50k daily, Tier 3 at ₦5M&lt;/li&gt;
&lt;li&gt;Requires human approval for transfers approaching or exceeding NIP limits (₦10M)&lt;/li&gt;
&lt;li&gt;Blocks autonomous agent self-approval of financial transactions — separation of duties enforced at the policy layer&lt;/li&gt;
&lt;li&gt;USSD and contactless transaction ceiling enforcement&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;pos-geofencing.yaml&lt;/code&gt; — CBN Agent Banking Guidelines
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Denies POS tool calls where terminal location context is absent or mismatched&lt;/li&gt;
&lt;li&gt;Requires approval for POS registration changes and cross-state transactions&lt;/li&gt;
&lt;li&gt;Audits all terminal activation and transaction events&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;bvn-nin-protection.yaml&lt;/code&gt; — NIBSS / NIN Regulations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Detects and blocks BVN/NIN patterns in agent output before logging or exposure&lt;/li&gt;
&lt;li&gt;Denies passing BVN/NIN to external endpoints without approval&lt;/li&gt;
&lt;li&gt;Requires human-in-the-loop for any BVN verification action&lt;/li&gt;
&lt;li&gt;Masks identifiers in the audit trail itself&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;nfiu-aml-str.yaml&lt;/code&gt; — NFIU AML/CFT Regulations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Requires approval for transactions at or above the ₦5M CTR threshold&lt;/li&gt;
&lt;li&gt;Detects structuring patterns (smurfing — multiple amounts just under a threshold)&lt;/li&gt;
&lt;li&gt;Velocity controls flag unusual transaction frequency within a session&lt;/li&gt;
&lt;li&gt;Blocks agents from autonomously completing transactions that should trigger an STR&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;popia-south-africa.yaml&lt;/code&gt; — Protection of Personal Information Act (South Africa)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Blocks cross-border transfers to non-POPIA-adequate jurisdictions&lt;/li&gt;
&lt;li&gt;Denies processing of special personal information without lawful conditions&lt;/li&gt;
&lt;li&gt;Detects SA ID numbers in agent output and blocks exposure&lt;/li&gt;
&lt;li&gt;Audits all personal information processing for responsible party accountability&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How it works with AGT
&lt;/h2&gt;

&lt;p&gt;The policy files are plain YAML rule definitions. To use them, you load the relevant pack(s), extract the blocked patterns, and wire them into AGT's &lt;code&gt;GovernancePolicy&lt;/code&gt; and &lt;code&gt;PolicyInterceptor&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_os.integrations&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GovernancePolicy&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent_os.integrations.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PolicyInterceptor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ToolCallRequest&lt;/span&gt;

&lt;span class="c1"&gt;# Load regex patterns from any policy file(s)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_patterns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy_files&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;patterns&lt;/span&gt; &lt;span class="o"&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;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;policy_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&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;rule&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc&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;rules&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="n"&gt;cond&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&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;condition&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="n"&gt;cond&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;operator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;cond&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;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule&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;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deny&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="s"&gt;block&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="s"&gt;escalate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&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;patterns&lt;/span&gt;

&lt;span class="n"&gt;patterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_patterns&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;policies/cbn-transaction-limits.yaml&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="s"&gt;policies/bvn-nin-protection.yaml&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="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GovernancePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nigerian-fintech&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;blocked_patterns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;log_all_calls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;interceptor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PolicyInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No new infrastructure. The policy files are validated using AGT's own compliance linter, so you can confirm every pack is well-formed before deploying:&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;agent_compliance.lint_policy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lint_file&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;policies&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*.yaml&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lint_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;✅&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;❌&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;p&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The demo: 5 actions, 5 decisions
&lt;/h2&gt;

&lt;p&gt;The repo includes an end-to-end demo at &lt;code&gt;examples/nigerian-fintech-demo/&lt;/code&gt;. It simulates a Nigerian fintech support agent attempting five actions, with the governance layer intercepting each one live based on the loaded policy files:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Policy Pack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;₦6.5M refund attempt&lt;/td&gt;
&lt;td&gt;⏳ ESCALATED&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cbn-transaction-limits.yaml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;BVN exposed in response&lt;/td&gt;
&lt;td&gt;❌ BLOCKED&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bvn-nin-protection.yaml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Export records to AWS US-East-1&lt;/td&gt;
&lt;td&gt;⏳ ESCALATED&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ndpa-data-residency.yaml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;KYC bypass + payment&lt;/td&gt;
&lt;td&gt;⏳ ESCALATED&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nfiu-aml-str.yaml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Normal customer lookup&lt;/td&gt;
&lt;td&gt;✅ ALLOWED&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every decision is written to a timestamped audit log satisfying NDPA section 30 accountability requirements.&lt;/p&gt;

&lt;p&gt;Run it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
.venv/bin/pip &lt;span class="nb"&gt;install &lt;/span&gt;agent-os-kernel agent-governance-toolkit-compliance
.venv/bin/python3 examples/nigerian-fintech-demo/demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I find most useful about this demo is that it makes "AI governance" concrete. It is easy to talk about governance in the abstract — policy documents, compliance frameworks, risk classifications. This demo shows what it actually looks like at the point of execution: an agent tries to do something, a policy intercepts it, a decision gets logged with a citation to the specific regulation that justifies it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters beyond Nigeria
&lt;/h2&gt;

&lt;p&gt;Nigeria is moving toward a risk-based AI regulatory framework. NITDA is expected to become the primary regulator for AI systems deployed in the country, with risk classification, mandatory audits for high-risk systems, and fines for non-compliance.&lt;/p&gt;

&lt;p&gt;When that enforcement begins, companies running AI agents that touch financial data or personal identifiers will need to demonstrate that those agents operate within documented governance boundaries — with an audit trail.&lt;/p&gt;

&lt;p&gt;Most companies will not be able to demonstrate that today. Not because they are negligent, but because the tooling to build that governance layer for the Nigerian regulatory context did not exist.&lt;/p&gt;

&lt;p&gt;This pack is an attempt to close that gap before enforcement makes it urgent — and the same gap almost certainly exists for other African markets. The roadmap includes a Kenya Data Protection Act 2019 pack, ECOWAS cross-border transfer rules, SIM swap fraud detection patterns, NAICOM insurtech AI governance rules, and SEC Nigeria capital markets AI rules. None of those exist yet either.&lt;/p&gt;




&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;This project is intentionally incomplete, and the parts that matter most need input from people with real regulatory experience, not just engineering experience.&lt;/p&gt;

&lt;p&gt;If you work in Nigerian fintech compliance, NDPA enforcement, or NFIU reporting and something in these policy packs is wrong, incomplete, or could be sharper — that is exactly the contribution this needs.&lt;/p&gt;

&lt;p&gt;To propose a new rule:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open an issue describing the regulation, the specific obligation, and the agent action pattern it should govern&lt;/li&gt;
&lt;li&gt;Reference the exact regulatory citation (e.g. "NDPA 2023 s.25(1)(b)")&lt;/li&gt;
&lt;li&gt;Submit a PR with the rule and a test case in &lt;code&gt;examples/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;See &lt;a href="https://github.com/kingztech2019/agt-policies-nigeria/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; for full guidelines.&lt;/p&gt;

&lt;p&gt;There's also a planned &lt;code&gt;ndpa-2023-mapping.md&lt;/code&gt; — a full NDPA → AGT control mapping intended as a contribution back to the AGT upstream repo, once this pack has real-world validation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;A few questions for the community:&lt;/p&gt;

&lt;p&gt;If you are building AI agents for regulated industries outside the US/EU — what regulatory frameworks are you working with, and is there existing tooling for them, or are you building governance layers from scratch?&lt;/p&gt;

&lt;p&gt;And for anyone who has used AGT or similar agent governance toolkits — how are you currently handling region-specific compliance that the toolkit does not cover out of the box?&lt;/p&gt;

&lt;p&gt;I'd also genuinely like to hear from anyone working in African fintech compliance. The roadmap for this project depends on practitioners pointing out what's missing or wrong.&lt;/p&gt;

&lt;p&gt;Star the repo if this is useful: &lt;a href="https://github.com/kingztech2019/agt-policies-nigeria" rel="noopener noreferrer"&gt;github.com/kingztech2019/agt-policies-nigeria&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>python</category>
      <category>africa</category>
    </item>
    <item>
      <title>Your Project Board Looks Fine. So, Why Do You Keep Missing Deadlines? I Built Cadence to Find Out.</title>
      <dc:creator>Oluwajuwon Omotayo</dc:creator>
      <pubDate>Sat, 13 Jun 2026 17:34:24 +0000</pubDate>
      <link>https://dev.to/omotayojayone/your-project-board-looks-fine-so-why-do-you-keep-missing-deadlines-i-built-cadence-to-find-out-5elm</link>
      <guid>https://dev.to/omotayojayone/your-project-board-looks-fine-so-why-do-you-keep-missing-deadlines-i-built-cadence-to-find-out-5elm</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I built Cadence, an open source delivery analytics tool for Plane that surfaces cycle time, bottlenecks, team health, and Monte Carlo delivery forecasts&lt;/li&gt;
&lt;li&gt;It connects read-only to any Plane workspace, syncs issue history, and computes the metrics that Plane's board view does not show you&lt;/li&gt;
&lt;li&gt;This post walks through the Monte Carlo forecasting engine in detail — how it turns historical throughput into a probability distribution of delivery dates&lt;/li&gt;
&lt;li&gt;Live demo: &lt;a href="https://cadence-plane-analytics.netlify.app" rel="noopener noreferrer"&gt;cadence-plane-analytics.netlify.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/kingztech2019/cadence-plane-analytics" rel="noopener noreferrer"&gt;github.com/kingztech2019/cadence-plane-analytics&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The problem with project boards
&lt;/h2&gt;

&lt;p&gt;Every team I have worked with uses some flavour of Kanban board. Plane, Jira, Linear, Trello — the shape is always the same. Columns for backlog, in progress, review, done. Cards move left to right. The board looks fine. Cards are moving. Sprints are closing.&lt;/p&gt;

&lt;p&gt;And yet, deadlines keep slipping.&lt;/p&gt;

&lt;p&gt;The board tells you the state of work right now. It does not tell you the things that actually determine whether you will hit your deadline:&lt;/p&gt;

&lt;p&gt;How long does work actually take, from the moment someone picks it up to the moment it ships? Not the estimate — the real distribution, including the outliers.&lt;/p&gt;

&lt;p&gt;Where does work get stuck? Not "QA is sometimes slow" — which stage, for how long, and is it getting worse?&lt;/p&gt;

&lt;p&gt;Who on the team is quietly overloaded, and who has been stuck on the same ticket for three weeks?&lt;/p&gt;

&lt;p&gt;Given everything we know about how this team has shipped over the last few months, what is the realistic range of dates for finishing what's left in this sprint?&lt;/p&gt;

&lt;p&gt;Plane is a genuinely good open source project management tool. But like most PM tools, it shows you the board. It does not answer these questions. I built Cadence to answer them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Cadence does
&lt;/h2&gt;

&lt;p&gt;Cadence connects to a Plane workspace — cloud or self-hosted — via API token or OAuth. It is strictly read-only; it never writes to your workspace, never modifies issues. It continuously syncs your issues and their full state-transition history into its own database, then computes a set of analytics that Plane's dashboards do not surface.&lt;/p&gt;

&lt;p&gt;The metrics fall into three groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delivery speed metrics&lt;/strong&gt; — cycle time (with P50/P85 percentiles and scatter distribution), lead time (created to done, including wait time), flow efficiency (active time vs total time, benchmarked against the ~15% industry median), and sprint-over-sprint velocity comparisons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flow health metrics&lt;/strong&gt; — a Cumulative Flow Diagram showing work-in-progress across stages over time, a bottleneck tracker that identifies which stage delays work most and escalates to "Critical" after three consecutive 30-day periods of being the bottleneck, scope creep tracking per sprint, and an At-Risk Radar showing in-progress issues that have already exceeded their stage's P85 duration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team and forecasting&lt;/strong&gt; — per-person throughput and cycle time, team health flags for overloaded members and high reactivation rates, cross-project contributor views, and the Monte Carlo delivery forecast.&lt;/p&gt;

&lt;p&gt;There is also a "Project Pulse" strip that appears on every page — a one-line signal surfacing the most important thing happening in a project right now, whether that's a new at-risk issue, a trend change, or a persistent bottleneck.&lt;/p&gt;

&lt;p&gt;And an AI Sprint Retrospective feature, powered by OpenRouter, that generates a narrative comparing the current sprint to the prior one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive: the Monte Carlo forecast
&lt;/h2&gt;

&lt;p&gt;This is the feature I want to focus on, because I think it is the one most teams have never had access to, and it changes how you think about deadlines once you have it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem with traditional estimates
&lt;/h3&gt;

&lt;p&gt;Most teams forecast delivery dates by summing story point estimates and dividing by velocity. "We have 40 points left, we do 10 points per sprint, so 4 more sprints."&lt;/p&gt;

&lt;p&gt;This produces a single number. A single number implies certainty. But software delivery is not certain — it is a distribution. Some tickets take half a day. Some take three weeks. The average hides the variance, and the variance is exactly the thing that determines whether your "4 sprints" estimate is realistic or wishful thinking.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Monte Carlo simulation does instead
&lt;/h3&gt;

&lt;p&gt;Instead of asking "what is our average throughput," Cadence asks a different question: "given everything we know about how this team has actually shipped work, what does a realistic range of outcomes look like?"&lt;/p&gt;

&lt;p&gt;The approach is straightforward conceptually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take the team's actual historical throughput — how many issues were completed per week, for as much history as is available.&lt;/li&gt;
&lt;li&gt;Run a simulation: starting from the current backlog size, repeatedly sample a random week's throughput from that history and subtract it from the remaining backlog, counting how many weeks it takes to reach zero.&lt;/li&gt;
&lt;li&gt;Repeat this simulation 10,000 times.&lt;/li&gt;
&lt;li&gt;Each of those 10,000 runs produces a "weeks to completion" number. Sort them.&lt;/li&gt;
&lt;li&gt;The result is a distribution. The P50 (median) is the date by which there's a 50% chance you're done. The P85 is the date by which there's an 85% chance. The P95 gives you the conservative, "if things go badly" estimate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is fundamentally different from a single-point estimate. Instead of telling a stakeholder "we'll be done March 15," Cadence tells them "there's a 50% chance we're done by March 15, an 85% chance by March 22, and a 95% chance by March 29." That is a much more honest — and much more useful — thing to communicate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why sampling from real history matters
&lt;/h3&gt;

&lt;p&gt;The key design decision is that the simulation samples from the team's &lt;em&gt;actual&lt;/em&gt; historical weekly throughput, not from an assumed distribution like a normal curve.&lt;/p&gt;

&lt;p&gt;Real team throughput is not normally distributed. It has good weeks and bad weeks — public holidays, a team member on leave, an unexpectedly gnarly bug that ate three days, a week where everything just clicked and the team shipped double the usual amount. A normal distribution would smooth all of that away. Sampling from real history preserves it.&lt;/p&gt;

&lt;p&gt;This means the forecast naturally accounts for the team's actual rhythm, including its bad weeks, without anyone having to manually adjust for "but we have a public holiday next month" or "two people are on leave in March." If those patterns are reflected in the historical data — and over enough history, recurring patterns like holiday seasons tend to be — the simulation captures them implicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation shape
&lt;/h3&gt;

&lt;p&gt;At a high level, the forecast computation in Cadence looks like this:&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;function&lt;/span&gt; &lt;span class="nf"&gt;monteCarloForecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;weeklyThroughput&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="c1"&gt;// historical completions per week&lt;/span&gt;
  &lt;span class="nx"&gt;remainingBacklog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;simulations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;p85&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;p95&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="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;simulations&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;remainingBacklog&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;weeks&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="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Sample a random week from actual history&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sampledThroughput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nx"&gt;weeklyThroughput&lt;/span&gt;&lt;span class="p"&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;weeklyThroughput&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="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;sampledThroughput&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;weeks&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Safety cap to avoid infinite loops on zero-throughput history&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;weeks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;520&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;results&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;weeks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;p50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&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;simulations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="na"&gt;p85&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&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;simulations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="na"&gt;p95&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&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;simulations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual implementation in Cadence does this as a PostgreSQL-backed computation over synced issue data — &lt;code&gt;metricsService.ts&lt;/code&gt; builds the weekly throughput series from the issue state-transition history that the sync workers have already pulled in, then simulates that series.&lt;/p&gt;

&lt;p&gt;A few practical details that mattered in getting this right:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimum history requirements.&lt;/strong&gt; With very little history — say, two or three weeks — the simulation will technically run, but the result is close to meaningless. Cadence surfaces a warning when there isn't enough historical data for the forecast to be statistically useful, rather than presenting a confident-looking number built on noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backlog definition.&lt;/strong&gt; "Remaining backlog" needs a clear definition — is it all open issues in the project, or a filtered subset (a specific sprint, a specific label)? Cadence's forecast respects the same filter bar that every other metric page uses, so you can ask "given our current velocity, how long will it take to clear everything tagged &lt;code&gt;v2-launch&lt;/code&gt;?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recomputation frequency.&lt;/strong&gt; The forecast is recomputed as new sync data comes in, not on every page load. Running 10,000 simulations on every request would be wasteful given that the underlying data only changes when Plane issues change state — which the incremental sync picks up every 30 minutes, or in real time if webhooks are configured.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this looks like in practice
&lt;/h3&gt;

&lt;p&gt;On the Forecast page, you pick a target — typically "everything in the current sprint" or "everything in a specific milestone" — and Cadence shows you the P50/P85/P95 dates directly, along with the underlying throughput history the simulation drew from.&lt;/p&gt;

&lt;p&gt;The practical effect: instead of a sprint planning conversation that ends with "we think we can get this done by Friday," you get a conversation that starts with "there's an 85% chance this is done by Friday, and a 50% chance it's done by Wednesday — do we want to scope down to hit the earlier date with more confidence, or are we comfortable with Friday as the realistic target?"&lt;/p&gt;

&lt;p&gt;That is a fundamentally better conversation to have at the start of a sprint than at the end of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;p&gt;For anyone interested in the broader system, Cadence is a Turborepo monorepo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apps/web&lt;/code&gt;&lt;/strong&gt; — Next.js 15 (App Router), Tailwind CSS v4, Recharts for all the visualisations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apps/api&lt;/code&gt;&lt;/strong&gt; — Fastify 5 on Node.js 22, exposing the REST API and running the BullMQ workers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;packages/shared&lt;/code&gt;&lt;/strong&gt; — TypeScript types shared between frontend and backend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sync pipeline uses a two-priority BullMQ queue backed by Redis. When you connect a workspace, a high-priority job backfills the last 90 days of issue history first — typically done in about 5 minutes — so you have usable charts quickly, while a low-priority job backfills the complete history in the background, which can take 30-60 minutes depending on workspace size. After the initial backfill, an incremental sync polls Plane every 30 minutes for changes, with optional real-time updates via Plane webhooks (HMAC-SHA256 verified).&lt;/p&gt;

&lt;p&gt;All of this runs against PostgreSQL 16, leaning on &lt;code&gt;PERCENTILE_CONT&lt;/code&gt; for the P50/P85 calculations and window functions for the time-series metrics like the CFD.&lt;/p&gt;

&lt;p&gt;Security-wise: Plane API tokens are encrypted at rest with AES-256-GCM, webhook signatures are verified with constant-time comparison, and shareable dashboard links use 16-byte random hex tokens that cannot be enumerated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The whole thing is open source under AGPL-3.0. There's a live demo at &lt;a href="https://cadence-plane-analytics.netlify.app" rel="noopener noreferrer"&gt;cadence-plane-analytics.netlify.app&lt;/a&gt;, or you can self-host with Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kingztech2019/cadence-plane-analytics.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cadence-plane-analytics
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# set POSTGRES_PASSWORD, JWT_SECRET, ENCRYPTION_KEY in .env&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;localhost:3001&lt;/code&gt;, sign up, and connect your Plane workspace URL and API token from &lt;strong&gt;Connections&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;I am curious about two things from teams using Plane, Jira, Linear, or similar tools.&lt;/p&gt;

&lt;p&gt;First — if you have used Monte Carlo forecasting before (or tools like it, such as Actionable Agile or Nave), how has it changed sprint planning conversations on your team? Did stakeholders push back on probabilistic estimates versus single-date estimates?&lt;/p&gt;

&lt;p&gt;Second — what metric do you wish your PM tool surfaced that it currently does not? I have a roadmap (multi-workspace comparisons, SLA tracking, label/epic analytics) but I would rather build what teams actually need than what I assume they need.&lt;/p&gt;

&lt;p&gt;Drop your thoughts below. And if Cadence looks useful, a star on the &lt;a href="https://github.com/kingztech2019/cadence-plane-analytics" rel="noopener noreferrer"&gt;repo&lt;/a&gt; helps others find it.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>typescript</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built an Open Source Go SDK for Squad by GTCO. Here Is What I Learned About Payment API Design.</title>
      <dc:creator>Oluwajuwon Omotayo</dc:creator>
      <pubDate>Thu, 11 Jun 2026 10:14:55 +0000</pubDate>
      <link>https://dev.to/omotayojayone/i-built-an-open-source-go-sdk-for-squad-by-gtco-here-is-what-i-learned-about-payment-api-design-n09</link>
      <guid>https://dev.to/omotayojayone/i-built-an-open-source-go-sdk-for-squad-by-gtco-here-is-what-i-learned-about-payment-api-design-n09</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I built &lt;code&gt;go-squad&lt;/code&gt; — a comprehensive, idiomatic Go SDK for the Squad by GTCO payment gateway&lt;/li&gt;
&lt;li&gt;Zero external runtime dependencies, full test coverage, and a &lt;code&gt;squadtest&lt;/code&gt; mock server for unit testing integrations without hitting real APIs&lt;/li&gt;
&lt;li&gt;This post covers the design decisions behind the SDK and what building it taught me about payment API design in the Nigerian fintech context&lt;/li&gt;
&lt;/ul&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/kingztech2019/go-squad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://pkg.go.dev/github.com/kingztech2019/go-squad" rel="noopener noreferrer"&gt;pkg.go.dev/github.com/kingztech2019/go-squad&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/kingztech2019/go-squad" rel="noopener noreferrer"&gt;github.com/kingztech2019/go-squad&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;I have been building fintech products in Nigeria for a while. Squad by GTCO is one of the most capable payment gateways in the market — it handles transactions, virtual accounts, transfers, VAS, sub-merchant management, disputes, and webhooks. The API surface is genuinely broad.&lt;/p&gt;

&lt;p&gt;But every time I integrated it into a Go project, I was writing the same boilerplate. HTTP clients. JSON marshalling. Pagination loops. Webhook signature validation. Error handling for the specific error shapes Squad returns. And every project had slightly different, slightly wrong implementations of the same things.&lt;/p&gt;

&lt;p&gt;The most common bug I kept seeing — in my own code and in code I reviewed — was the kobo confusion.&lt;/p&gt;

&lt;p&gt;The Squad API uses kobo as the base denomination for NGN amounts. ₦5,000 is &lt;code&gt;500000&lt;/code&gt; in the API. If you pass &lt;code&gt;5000&lt;/code&gt; instead, you charge the customer ₦50, and nobody notices until reconciliation. It is the kind of bug that slips through tests because the payment succeeds, just for the wrong amount.&lt;/p&gt;

&lt;p&gt;I wanted a single, well-tested library that handled all of this correctly, consistently, and idiomatically in Go. So I built one.&lt;/p&gt;


&lt;h2&gt;
  
  
  What the SDK covers
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SQUAD_SECRET_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c"&gt;// Payments&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transactions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitiatePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitiatePaymentParams&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;"customer@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NGN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;// ₦5,000 — no kobo confusion&lt;/span&gt;
    &lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"NGN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CallbackURL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://yoursite.com/callback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c"&gt;// Virtual accounts&lt;/span&gt;
&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VirtualAccounts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateVirtualAccountParams&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;CustomerIdentifier&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"cust-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;          &lt;span class="s"&gt;"Adaeze"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;           &lt;span class="s"&gt;"Okafor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MobileNum&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;          &lt;span class="s"&gt;"2348012345678"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;              &lt;span class="s"&gt;"adaeze@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;BVN&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                &lt;span class="s"&gt;"12345678901"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DOB&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;                &lt;span class="s"&gt;"01/01/1990"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c"&gt;// Transfers&lt;/span&gt;
&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transfers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FundsTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FundsTransferParams&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TransactionRef&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"pay-out-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NGN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;BankCode&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;"057"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AccountNumber&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"0123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AccountName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;"NGN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c"&gt;// VAS — airtime, data, cable TV, electricity, SMS&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VAS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BuyAirtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BuyAirtimeParams&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PhoneNumber&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"2348012345678"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NGN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"MTN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TransactionRef&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"air-001"&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 full surface covers transactions, virtual accounts, transfers, sub-merchant management, disputes with evidence upload, all VAS operations, and webhooks.&lt;/p&gt;


&lt;h2&gt;
  
  
  Design decisions worth talking about
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Money helpers that prevent the most common bug
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NGN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c"&gt;// → 500000 (₦5,000 in kobo)&lt;/span&gt;
&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NGN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c"&gt;// → 100    (₦1.00 in kobo)&lt;/span&gt;
&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c"&gt;// → 5000   ($50.00 in cents)&lt;/span&gt;
&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromKobo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// → 500.0  (display value)&lt;/span&gt;
&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// → 50.0   (display value)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the first thing I added. Every amount field in the SDK is &lt;code&gt;int64&lt;/code&gt; (kobo), not &lt;code&gt;float64&lt;/code&gt; (naira). You cannot accidentally pass a naira amount — the type system pushes you toward the helpers. If you call &lt;code&gt;squad.NGN(5000)&lt;/code&gt;, you get &lt;code&gt;500000&lt;/code&gt;. There is no ambiguity.&lt;/p&gt;

&lt;p&gt;This sounds obvious. But I have seen this bug in production codebases multiple times. The fix is architectural — make the wrong thing hard to write.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Auto-pagination iterator
&lt;/h3&gt;

&lt;p&gt;Every listing endpoint in the Squad API is paginated. Most codebases I have seen implement the same manual loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Without iterator — repetitive and error-prone&lt;/span&gt;
&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;for&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;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transfers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAllTransactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferListParams&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PerPage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transfers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&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="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transfers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&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 SDK exposes an &lt;code&gt;Iter&lt;/code&gt; type that fetches pages lazily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// With iterator — clean, handles all edge cases automatically&lt;/span&gt;
&lt;span class="n"&gt;iter&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transfers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransferListParams&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PerPage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&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;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Item&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="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&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 iterator is generic — &lt;code&gt;Iter[T]&lt;/code&gt; — so it works across all listing endpoints with full type safety. Page fetching happens lazily on each &lt;code&gt;Next()&lt;/code&gt; call, so you only pay for the pages you actually consume.&lt;/p&gt;

&lt;p&gt;This pattern is borrowed from the Stripe Go SDK, which I think is the gold standard for payment SDK design.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Idempotency keys to prevent duplicate charges
&lt;/h3&gt;

&lt;p&gt;Network failures during payment requests are a real problem in Nigeria. If you retry a payment initiation request after a timeout, you risk charging the customer twice.&lt;/p&gt;

&lt;p&gt;The SDK supports idempotency keys at the context level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GenerateIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c"&gt;// Store key in your DB alongside the order BEFORE calling Squad.&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order-"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;orderID&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transactions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitiatePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// On network failure, retry with the SAME ctx — same key, no double charge.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you can opt into automatic key generation for all POST requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithAutoIdempotency&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design decision here is that idempotency is attached to the context, not the params struct. This means you can inject it from your middleware layer without touching your business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The webhook router
&lt;/h3&gt;

&lt;p&gt;Webhook handling in most codebases is a giant switch statement on event type strings, with manual signature validation scattered across the handler. The SDK ships a typed webhook router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWebhookRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SQUAD_SECRET_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;OnTransactionSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&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;body&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebhookTransactionBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&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;fulfillOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransactionRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;OnVirtualAccountCredit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&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;body&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebhookVirtualAccountBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&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;creditCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerIdentifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;OnDisputeOpened&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&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;body&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebhookDisputeBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&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;notifyTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TicketID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;OnError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&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;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"webhook error: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&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;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/webhook/squad"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router validates the HMAC-SHA512 signature, parses the body into the correct typed struct based on the event type, and dispatches to your handler. It implements &lt;code&gt;http.Handler&lt;/code&gt; directly so you can register it with any standard Go HTTP framework.&lt;/p&gt;

&lt;p&gt;Signature validation uses constant-time comparison to prevent timing attacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The &lt;code&gt;squadtest&lt;/code&gt; mock server
&lt;/h3&gt;

&lt;p&gt;This is the part I am most pleased with.&lt;/p&gt;

&lt;p&gt;Testing payment integrations is painful. You either hit a real sandbox (slow, flaky, requires network) or you mock at the HTTP level (verbose, not type-safe). The &lt;code&gt;squadtest&lt;/code&gt; package provides a full mock Squad API server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestMyCheckoutService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;srv&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;squadtest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// starts a mock server, shuts down when test ends&lt;/span&gt;

    &lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnInitiatePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitiatePaymentParams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitiatePaymentResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NGN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unexpected amount: %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitiatePaymentResponse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;CheckoutURL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"https://fake-checkout.squadco.com/abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;TransactionRef&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TransactionRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c"&gt;// Inject srv.Client() into your service under test&lt;/span&gt;
    &lt;span class="n"&gt;myService&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checkout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;myService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NGN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Assert on what your code actually sent&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"expected 1 request, got %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestCount&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mock server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts and stops automatically with the test lifecycle via &lt;code&gt;t.Cleanup&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Returns typed responses that match the real API shape&lt;/li&gt;
&lt;li&gt;Records all requests so you can assert on what your code sent&lt;/li&gt;
&lt;li&gt;Supports custom handlers for any endpoint via &lt;code&gt;srv.Handle(method, path, fn)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No real network calls. No sandbox credentials needed in CI. Fast and deterministic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/kingztech2019/go-squad/squadtest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What building this taught me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nigerian payment APIs have genuinely different constraints from Western ones.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The kobo denomination issue is the obvious one. But there are subtler things. The NUBAN virtual account flow requires BVN, and building that into a type-safe API surface means thinking carefully about which fields are always required versus sometimes required. The VAS surface — airtime, data, cable TV, electricity — reflects a market where bills are paid through payment APIs rather than through bank direct debits. That changes the shape of the API significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero external runtime dependencies is worth the effort.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The SDK has no runtime dependencies beyond the Go standard library. Every dependency you add is a dependency your users have to manage, audit, and update. For a payments library — where supply chain security matters — keeping the dependency surface minimal is not just aesthetics. It is a security posture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The test infrastructure is part of the product.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;squadtest&lt;/code&gt; package is not a nice-to-have. It is one of the most important parts of the library. If developers cannot test their integration easily, they will not write tests. If they do not write tests, they will have production bugs. A payment SDK that makes testing hard is a payment SDK that ships broken integrations into production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using it in your project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/kingztech2019/go-squad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires Go 1.21 or later.&lt;/p&gt;

&lt;p&gt;Sandbox is auto-detected from your key prefix — if your key starts with &lt;code&gt;sandbox_sk_&lt;/code&gt;, the SDK routes to the sandbox API automatically. No configuration needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;

    &lt;span class="n"&gt;squad&lt;/span&gt; &lt;span class="s"&gt;"github.com/kingztech2019/go-squad"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;squad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SQUAD_SECRET_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full documentation on pkg.go.dev: &lt;a href="https://pkg.go.dev/github.com/kingztech2019/go-squad" rel="noopener noreferrer"&gt;pkg.go.dev/github.com/kingztech2019/go-squad&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;Contributions are welcome. The areas I am most interested in help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More &lt;code&gt;squadtest&lt;/code&gt; handler coverage for endpoints not yet mocked&lt;/li&gt;
&lt;li&gt;Additional examples in the &lt;code&gt;examples/&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;Documentation improvements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href="https://github.com/kingztech2019/go-squad/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; for guidelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;A few questions I am genuinely curious about from the Go community:&lt;/p&gt;

&lt;p&gt;If you have integrated payment gateways in Go before — what is the most common mistake you have seen in payment integration code? And is there a pattern in the SDK you would have designed differently?&lt;/p&gt;

&lt;p&gt;Drop your thoughts in the comments. I read everything.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>nigeria</category>
      <category>squadco</category>
    </item>
    <item>
      <title>I Tried to Build a SaaS First. Here Is What It Cost Me.</title>
      <dc:creator>Oluwajuwon Omotayo</dc:creator>
      <pubDate>Wed, 10 Jun 2026 17:17:31 +0000</pubDate>
      <link>https://dev.to/omotayojayone/i-tried-to-build-a-saas-first-here-is-what-it-cost-me-5f14</link>
      <guid>https://dev.to/omotayojayone/i-tried-to-build-a-saas-first-here-is-what-it-cost-me-5f14</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Most developers jump straight to building a SaaS product before they have any distribution. The product fails not because it is bad, but because nobody knows it exists.&lt;/li&gt;
&lt;li&gt;Services are not a consolation prize. They are the fastest way to validate your product idea, earn your first revenue, and build the market knowledge your product needs before you write a line of code.&lt;/li&gt;
&lt;li&gt;The right sequencing is: &lt;strong&gt;service first, distribution second, product third.&lt;/strong&gt; In that order.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Let me tell you something I have not said publicly before.&lt;/p&gt;

&lt;p&gt;Before GinuxAI. Before BuySmart. Before any of the products I write about — I spent months building things nobody asked for.&lt;/p&gt;

&lt;p&gt;Not because I was not skilled enough. I was.&lt;/p&gt;

&lt;p&gt;Not because the ideas were bad. Some were genuinely good.&lt;/p&gt;

&lt;p&gt;I built them before I had a single user. Before I had validated that anyone would pay. Before I had talked to enough people to understand whether the problem I was solving was painful enough to make someone open their wallet.&lt;/p&gt;

&lt;p&gt;I went straight to the product.&lt;/p&gt;

&lt;p&gt;And I paid for it in months, in opportunity cost, and in the quiet frustration of finishing something and realising I now had to start from zero on the harder problem: getting anyone to care.&lt;/p&gt;

&lt;p&gt;This article is about what I learned. And what I wish someone had told me before I learned it the expensive way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The SaaS dream and why it is a trap for most developers
&lt;/h2&gt;

&lt;p&gt;There is a version of the SaaS story that gets told constantly in developer communities.&lt;/p&gt;

&lt;p&gt;Build a product. Launch it. Get users. Generate monthly recurring revenue. Scale.&lt;/p&gt;

&lt;p&gt;It sounds clean. It sounds like a system. It sounds like the logical extension of the engineering mindset we already have — define the problem, build the solution, deploy it.&lt;/p&gt;

&lt;p&gt;But there is a step that version of the story leaves out entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distribution.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The developers you see online who have built successful SaaS products — the ones doing serious MRR, the ones with the Twitter threads about their revenue growth — almost all of them had something before the product.&lt;/p&gt;

&lt;p&gt;An audience. A following. A network of people who already trusted them. A distribution channel they had been compounding for years before they had anything to sell.&lt;/p&gt;

&lt;p&gt;When they launched, they were not launching into silence. They were pulling a lever that was already built.&lt;/p&gt;

&lt;p&gt;Most developers do not have that lever. I did not have it.&lt;/p&gt;

&lt;p&gt;And without distribution, even the best product shouts into a void.&lt;/p&gt;

&lt;p&gt;Your product can be objectively better than the alternative. It can be faster, cheaper, and more thoughtfully designed. And it will still sit undiscovered because nobody knows you exist and you have no mechanism to change that.&lt;/p&gt;

&lt;p&gt;The product was never the bottleneck.&lt;/p&gt;

&lt;p&gt;Distribution was.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I should have done first
&lt;/h2&gt;

&lt;p&gt;The fastest path to a product that people actually want is through service.&lt;/p&gt;

&lt;p&gt;Not consulting forever. Not replacing one job with another. But starting with service deliberately, as a strategy, before you write the first line of your product's code.&lt;/p&gt;

&lt;p&gt;Every conversation you have with a paying service client is market research that no survey, no Reddit thread, and no validation framework can replicate.&lt;/p&gt;

&lt;p&gt;You are inside their workflow. You are watching them use the broken tools they currently tolerate. You are hearing the exact words they use to describe their problem — which are almost never the words a developer would use to describe the same problem. You are seeing which part of the process makes them frustrated enough to pay someone to fix it.&lt;/p&gt;

&lt;p&gt;That intelligence is worth more than months of building in isolation.&lt;/p&gt;

&lt;p&gt;And here is the part that changes everything: &lt;strong&gt;they are paying you to gather it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The client paying you for a service is, without knowing it, funding your research into the product you will eventually build. They are telling you the price point before you have to guess it. They are telling you the features that matter and the ones that do not. They are pre-validating your roadmap with every conversation.&lt;/p&gt;

&lt;p&gt;When you eventually build the product, you are not guessing. You are building what you already know people will pay for because they have been paying for the manual version of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The identity shift nobody talks about
&lt;/h2&gt;

&lt;p&gt;There is a deeper reason most developers never make the transition from employee to founder. And it is not the one they think it is.&lt;/p&gt;

&lt;p&gt;It is not lack of skill. Developers are technically exceptional. That is not the gap.&lt;/p&gt;

&lt;p&gt;It is not lack of ideas. Most developers have more ideas than they know what to do with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gap is identity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For most of us, the mental model of what we are goes like this: I am a developer. I build things. I ship features. I solve technical problems. Someone else sells them.&lt;/p&gt;

&lt;p&gt;That mental model is completely fine inside a job. It is lethal when you are trying to build something for yourself.&lt;/p&gt;

&lt;p&gt;Because when you are building for yourself, you are not just the person who builds the thing. You are the person who identifies which thing to build, who decides what it costs, who explains why it is worth that, and who takes responsibility for whether it works.&lt;/p&gt;

&lt;p&gt;Services force that shift.&lt;/p&gt;

&lt;p&gt;The moment you sit across from a business owner, tell them their current process is costing them money, and propose a specific solution at a specific price — you are no longer an employee. You cannot be. Employees do not negotiate outcomes. Employees do not own the result.&lt;/p&gt;

&lt;p&gt;That conversation changes you in a way that shipping another feature inside a product nobody uses never will.&lt;/p&gt;




&lt;h2&gt;
  
  
  The sequencing that actually works
&lt;/h2&gt;

&lt;p&gt;Here is what this looks like in practice.&lt;/p&gt;

&lt;p&gt;Start with people you already know. Not strangers. Not cold emails. The people in your phone, your family, your former colleagues. Think about which of them runs a business or works inside one.&lt;/p&gt;

&lt;p&gt;Call them. Not to pitch. To ask one question: &lt;strong&gt;what is the most frustrating, time-consuming thing in your work right now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Listen properly. Not to identify a technical solution immediately. To understand the shape of the pain.&lt;/p&gt;

&lt;p&gt;When you hear something you know you can fix — offer to fix it. Charge for the outcome, not the hours. Deliver something that works. Document everything you learned.&lt;/p&gt;

&lt;p&gt;Then find another client with a similar problem. And then another.&lt;/p&gt;

&lt;p&gt;After three to five clients in the same space, you will start to see the pattern. The same friction. The same workflow. The same words to describe the same frustration. &lt;strong&gt;That pattern is your product.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Build for that pattern and you will know before you write the first line of code that someone will pay for what you are building. Because they already have.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where I am now
&lt;/h2&gt;

&lt;p&gt;GinuxAI is live. BuySmart is open source and growing.&lt;/p&gt;

&lt;p&gt;Both are better products than they would have been if I had built them in isolation from the beginning. Because in the process of building publicly, writing about the market, and having conversations with real users, I have been doing exactly what services do — getting close to real people with real problems and letting that shape what I build.&lt;/p&gt;

&lt;p&gt;But I would be lying if I said I got the sequencing perfectly right from the start.&lt;/p&gt;

&lt;p&gt;The honest version is: I learned the sequencing by getting parts of it wrong. By building before validating. By discovering, after the fact, that some of the assumptions I built into early versions of the product were assumptions nobody had actually confirmed.&lt;/p&gt;

&lt;p&gt;That is not a disaster. It is how almost everyone learns.&lt;/p&gt;

&lt;p&gt;But you do not have to learn it the same way I did.&lt;/p&gt;

&lt;p&gt;The sequencing is not complicated.&lt;/p&gt;

&lt;p&gt;Solve a real problem for a real person. Get paid for it. Learn everything from that client. Repeat until you see the pattern. Then build the product that solves the pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Services first. Distribution second. Product third.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In that order. Not because it is the comfortable path. Because it is the one that actually works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;I am curious where you are in this journey.&lt;/p&gt;

&lt;p&gt;Are you currently in build mode — working on a product before you have paying users? Or have you gone through the service-first path and built something from validated demand?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop your experience in the comments.&lt;/strong&gt; Especially if you tried to skip straight to SaaS and paid for it the same way I did. I want to know what the pattern looks like for developers in different markets and contexts.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about building products in Africa, what the best builders on the continent are doing differently, and what I am learning as a founder. Follow me here on Dev.to or subscribe to my Substack at &lt;a href="https://oluwajuwonomotayo.substack.com" rel="noopener noreferrer"&gt;oluwajuwonomotayo.substack.com&lt;/a&gt; for the full archive.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>startup</category>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
