<?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: billkhiz</title>
    <description>The latest articles on DEV Community by billkhiz (@billkhiz).</description>
    <link>https://dev.to/billkhiz</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3690195%2Ffe7480cb-0a1b-48fb-9f3c-0b12f86d7bbb.jpg</url>
      <title>DEV Community: billkhiz</title>
      <link>https://dev.to/billkhiz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/billkhiz"/>
    <language>en</language>
    <item>
      <title>I built a machine-readable UK Chart of Accounts for Python (because one didn't exist)</title>
      <dc:creator>billkhiz</dc:creator>
      <pubDate>Wed, 01 Apr 2026 16:19:39 +0000</pubDate>
      <link>https://dev.to/billkhiz/i-built-a-machine-readable-uk-chart-of-accounts-for-python-because-one-didnt-exist-30m6</link>
      <guid>https://dev.to/billkhiz/i-built-a-machine-readable-uk-chart-of-accounts-for-python-because-one-didnt-exist-30m6</guid>
      <description>&lt;h2&gt;
  
  
  Table Of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What it does&lt;/li&gt;
&lt;li&gt;Quick example&lt;/li&gt;
&lt;li&gt;Why VAT treatments matter&lt;/li&gt;
&lt;li&gt;The LLM use case&lt;/li&gt;
&lt;li&gt;HMRC box mappings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever tried to build accounting software for the UK market, you've hit the same wall I did: there's no clean, machine-readable UK Chart of Accounts available on PyPI.&lt;/p&gt;

&lt;p&gt;US-centric ones exist. Plenty of them. But UK accounting has different categories, VAT treatments, and HMRC-specific codes that don't map neatly onto American standards.&lt;/p&gt;

&lt;p&gt;So rejoice UK accountants, I built one.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;uk-chart-of-accounts&lt;/code&gt; is a Python library with 166 standard UK nominal codes - the numbered category system every UK business uses to classify transactions. Each code includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Account type&lt;/strong&gt; with double-entry rules (does a debit increase or decrease this account?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VAT treatment&lt;/strong&gt; (standard 20%, reduced 5%, zero-rated, exempt, or outside scope)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HMRC box mappings&lt;/strong&gt; (CT600, VAT Return, FPS/RTI, EPS, CIS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags&lt;/strong&gt; for searchable grouping (motor, payroll, premises, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions&lt;/strong&gt; on complex codes explaining the nuances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero dependencies. Pure Python. Works with Python 3.10+.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick example
&lt;/h2&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;uk_coa&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChartOfAccounts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VatRate&lt;/span&gt;

&lt;span class="n"&gt;coa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChartOfAccounts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Look up any nominal code
&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coa&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="mi"&gt;7602&lt;/span&gt;&lt;span class="p"&gt;)&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;name&lt;/span&gt;          &lt;span class="c1"&gt;# "Accountancy Fees"
&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;vat&lt;/span&gt;           &lt;span class="c1"&gt;# VatRate.STANDARD
&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;vat_rate_pct&lt;/span&gt;  &lt;span class="c1"&gt;# 0.20
&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;debit_increase&lt;/span&gt;  &lt;span class="c1"&gt;# True
&lt;/span&gt;
&lt;span class="c1"&gt;# Search
&lt;/span&gt;&lt;span class="n"&gt;coa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insurance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# All accounts with "insurance" in the name
&lt;/span&gt;&lt;span class="n"&gt;coa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_vat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VatRate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXEMPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# All VAT-exempt accounts
&lt;/span&gt;&lt;span class="n"&gt;coa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;motor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# All motor-related accounts
&lt;/span&gt;
&lt;span class="c1"&gt;# Export for LLM prompts
&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;coa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_prompt_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Why VAT treatments matter
&lt;/h2&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h3&gt;
  
  
  💡 Why VAT treatments matter
&lt;/h3&gt;

&lt;p&gt;Getting this wrong means incorrect VAT returns. This library has every code's VAT treatment pre-set, including nuances like residential vs. commercial rent.&lt;br&gt;

&lt;/p&gt;
&lt;/div&gt;



&lt;p&gt;Different expenses have different treatments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Treatment&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;Most business expenses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reduced&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;Domestic energy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-rated&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;Books, children's clothes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exempt&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Insurance, bank charges, Royal Mail postage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outside scope&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Wages, taxes, depreciation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the part most non-UK developers get wrong. UK VAT isn't just "add 20%".&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM use case
&lt;/h2&gt;

&lt;p&gt;If you're building AI-powered bookkeeping tools, the &lt;code&gt;to_prompt_context()&lt;/code&gt; method formats the entire chart as structured text you can inject into an LLM prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Feed the chart to an LLM for transaction categorisation
&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;coa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_prompt_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AccountType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OVERHEAD&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Given this chart of accounts:
&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

Categorise this transaction: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TESCO 15.40 GBP&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This gives the model the full code structure, names, and VAT treatments without you having to maintain prompt templates.&lt;/p&gt;
&lt;h2&gt;
  
  
  HMRC box mappings
&lt;/h2&gt;

&lt;p&gt;Each code references the HMRC form and box it feeds into:&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="n"&gt;corp_tax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coa&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="mi"&gt;2110&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;corp_tax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hmrc_box&lt;/span&gt;  &lt;span class="c1"&gt;# "CT600 Box 86"
&lt;/span&gt;
&lt;span class="n"&gt;entertainment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coa&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="mi"&gt;7403&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;entertainment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hmrc_box&lt;/span&gt;  &lt;span class="c1"&gt;# "CT600 Box 46"
&lt;/span&gt;&lt;span class="n"&gt;entertainment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;
&lt;span class="c1"&gt;# "VAT on business entertainment is blocked from input tax
#  recovery (HMRC VAT Notice 700/65). Disallowable for
#  corporation tax - must be added back on CT600."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Mappings cover CT600, VAT Return (Boxes 1-9), FPS/RTI, EPS, and CIS returns.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;uk-chart-of-accounts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I'm a finance professional who builds AI tools for UK accounting. This library came from extracting the reference data layer of a larger bookkeeping automation project. The codes, VAT treatments, and HMRC mappings are standard public knowledge - I've just packaged them in a way that's actually usable in code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/billkhiz-bit/uk-chart-of-accounts" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Star the repo or open a PR&lt;/a&gt;
&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/billkhiz-bit" rel="noopener noreferrer"&gt;
        billkhiz-bit
      &lt;/a&gt; / &lt;a href="https://github.com/billkhiz-bit/uk-chart-of-accounts" rel="noopener noreferrer"&gt;
        uk-chart-of-accounts
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Machine-readable UK Chart of Accounts for Python. 166 nominal codes with VAT treatments and HMRC mappings.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;uk-chart-of-accounts&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Machine-readable UK Chart of Accounts for Python. 166 standard nominal codes with account types, VAT treatments, and HMRC box mappings.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pip install uk-chart-of-accounts&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick start&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="highlight highlight-source-python notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;uk_coa&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;ChartOfAccounts&lt;/span&gt;, &lt;span class="pl-v"&gt;AccountType&lt;/span&gt;, &lt;span class="pl-v"&gt;VatRate&lt;/span&gt;
&lt;span class="pl-s1"&gt;coa&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;ChartOfAccounts&lt;/span&gt;()

&lt;span class="pl-c"&gt;# Look up by code&lt;/span&gt;
&lt;span class="pl-s1"&gt;account&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;coa&lt;/span&gt;.&lt;span class="pl-c1"&gt;get&lt;/span&gt;(&lt;span class="pl-c1"&gt;7602&lt;/span&gt;)
&lt;span class="pl-s1"&gt;account&lt;/span&gt;.&lt;span class="pl-c1"&gt;name&lt;/span&gt;          &lt;span class="pl-c"&gt;# "Accountancy Fees"&lt;/span&gt;
&lt;span class="pl-s1"&gt;account&lt;/span&gt;.&lt;span class="pl-c1"&gt;type&lt;/span&gt;          &lt;span class="pl-c"&gt;# AccountType.OVERHEAD&lt;/span&gt;
&lt;span class="pl-s1"&gt;account&lt;/span&gt;.&lt;span class="pl-c1"&gt;vat&lt;/span&gt;           &lt;span class="pl-c"&gt;# VatRate.STANDARD&lt;/span&gt;
&lt;span class="pl-s1"&gt;account&lt;/span&gt;.&lt;span class="pl-c1"&gt;vat_rate_pct&lt;/span&gt;  &lt;span class="pl-c"&gt;# 0.20&lt;/span&gt;
&lt;span class="pl-s1"&gt;account&lt;/span&gt;.&lt;span class="pl-c1"&gt;debit_increase&lt;/span&gt;  &lt;span class="pl-c"&gt;# True (expenses increase on debit side)&lt;/span&gt;

&lt;span class="pl-c"&gt;# Search&lt;/span&gt;
&lt;span class="pl-s1"&gt;coa&lt;/span&gt;.&lt;span class="pl-c1"&gt;search&lt;/span&gt;(&lt;span class="pl-s"&gt;"insurance"&lt;/span&gt;)           &lt;span class="pl-c"&gt;# All accounts with "insurance" in the name&lt;/span&gt;
&lt;span class="pl-s1"&gt;coa&lt;/span&gt;.&lt;span class="pl-c1"&gt;by_type&lt;/span&gt;(&lt;span class="pl-v"&gt;AccountType&lt;/span&gt;.&lt;span class="pl-c1"&gt;INCOME&lt;/span&gt;)   &lt;span class="pl-c"&gt;# All income accounts&lt;/span&gt;
&lt;span class="pl-s1"&gt;coa&lt;/span&gt;.&lt;span class="pl-c1"&gt;by_vat&lt;/span&gt;(&lt;span class="pl-v"&gt;VatRate&lt;/span&gt;.&lt;span class="pl-c1"&gt;EXEMPT&lt;/span&gt;)        &lt;span class="pl-c"&gt;# All VAT-exempt accounts&lt;/span&gt;
&lt;span class="pl-s1"&gt;coa&lt;/span&gt;.&lt;span class="pl-c1"&gt;by_tag&lt;/span&gt;(&lt;span class="pl-s"&gt;"motor"&lt;/span&gt;)               &lt;span class="pl-c"&gt;# All motor-related accounts&lt;/span&gt;
&lt;span class="pl-s1"&gt;coa&lt;/span&gt;.&lt;span class="pl-c1"&gt;code_range&lt;/span&gt;(&lt;span class="pl-c1"&gt;7000&lt;/span&gt;, &lt;span class="pl-c1"&gt;7012&lt;/span&gt;)        &lt;span class="pl-c"&gt;# Payroll overheads&lt;/span&gt;

&lt;span class="pl-c"&gt;# Convenience&lt;/span&gt;
&lt;span class="pl-s1"&gt;coa&lt;/span&gt;.&lt;span class="pl-c1"&gt;expenses&lt;/span&gt;()                    &lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/billkhiz-bit/uk-chart-of-accounts" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



</description>
      <category>python</category>
      <category>accounting</category>
      <category>opensource</category>
      <category>fintech</category>
    </item>
    <item>
      <title>I built an AI bookkeeping agent that reached the AWS semifinals from 10,000+ entries</title>
      <dc:creator>billkhiz</dc:creator>
      <pubDate>Mon, 30 Mar 2026 14:06:31 +0000</pubDate>
      <link>https://dev.to/billkhiz/i-built-an-ai-bookkeeping-agent-that-reached-the-aws-semifinals-from-10000-entries-2mcc</link>
      <guid>https://dev.to/billkhiz/i-built-an-ai-bookkeeping-agent-that-reached-the-aws-semifinals-from-10000-entries-2mcc</guid>
      <description>&lt;h2&gt;
  
  
  Table Of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The architecture&lt;/li&gt;
&lt;li&gt;The categorisation engine&lt;/li&gt;
&lt;li&gt;Few-shot learning that actually improves over time&lt;/li&gt;
&lt;li&gt;Handling real-world bank statements&lt;/li&gt;
&lt;li&gt;Batched processing with concurrency control&lt;/li&gt;
&lt;li&gt;Double-entry done right&lt;/li&gt;
&lt;li&gt;What I learned&lt;/li&gt;
&lt;li&gt;The numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every month, I sit down with bank statements from multiple clients and manually assign each transaction to the correct nominal code — a process called transaction categorisation.&lt;/p&gt;

&lt;p&gt;It takes hours. There are 166 standard UK nominal codes, five VAT rate categories, and endless edge cases. "AMAZON MARKETPLACE" could be office supplies, stock purchases, or a personal expense depending on the client. Multiply that across hundreds of transactions per client, per month, and you start to understand why 75% of CPAs are expected to retire in the next decade with fewer graduates replacing them.&lt;/p&gt;

&lt;p&gt;So I built LedgerAgent - an AI-powered bookkeeping agent that categorises bank transactions automatically using Amazon Bedrock. It reached the semifinals of the AWS 10,000 AIdeas competition (top ~1,000 from over 10,000 entries) in the EMEA Commercial Solutions category.&lt;/p&gt;

&lt;p&gt;Here it is in action:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/KxKg6Alylu0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here's how it works under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
Stack: React 19 + Express + 8 AWS services (Bedrock, DynamoDB, S3, SQS, Lambda, API Gateway, EventBridge, Cognito)&lt;br&gt;

&lt;/div&gt;


&lt;p&gt;LedgerAgent uses 8 AWS services working together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (React 19)
    │
    ├── Cognito JWT auth
    │
Express Server (port 3001)
    │
    ├── Amazon Bedrock ──── Claude 3.5 Haiku (categorisation)
    │                       Claude 3.5 Sonnet (receipt OCR)
    ├── DynamoDB ─────────── Client vault (transactions, learned patterns)
    ├── S3 ───────────────── File storage (uploads, receipts, backups)
    ├── SQS ──────────────── Async job queue (large batches)
    │     │
    │     └── Lambda ─────── Serverless batch processor
    │
    ├── API Gateway ──────── REST endpoint for job status
    └── EventBridge ──────── Daily DynamoDB → S3 backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The frontend is React 19 with Vite and Tailwind. The backend is Express running on Node.js 20. All AI inference runs through Amazon Bedrock - Claude 3.5 Haiku for transaction categorisation (fast and cheap) and Claude 3.5 Sonnet for receipt OCR (multimodal image understanding).&lt;/p&gt;

&lt;p&gt;The key design decision was using DynamoDB as a persistent "vault" for each client. Every accounting practice manages multiple clients, and each client has their own transaction history, confirmed categorisations, and learned patterns. DynamoDB's pay-per-request billing made this economical - so I'm not paying for idle capacity between categorisation runs.&lt;/p&gt;
&lt;h2&gt;
  
  
  The categorisation engine
&lt;/h2&gt;

&lt;p&gt;The core of LedgerAgent is the &lt;code&gt;chartOfAccounts.mjs&lt;/code&gt; service. It loads two data files at startup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nominal_codes.json&lt;/code&gt; — 166 UK standard accounting codes (from 1001 Fixed Assets through to 9999 Suspense)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;global_rules.json&lt;/code&gt; — 365 vendor-to-category mapping rules built from my experience coding thousands of real transactions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system prompt establishes a UK bookkeeper persona with the full code reference. When a transaction comes in, the &lt;code&gt;buildUserMessage&lt;/code&gt; function constructs the prompt:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual flow — simplified&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildUserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confirmedExamples&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Transaction details (date, description, amount)&lt;/span&gt;
  &lt;span class="c1"&gt;// 2. Any previously confirmed categorisations for this client&lt;/span&gt;
  &lt;span class="c1"&gt;//    injected as few-shot examples&lt;/span&gt;
  &lt;span class="c1"&gt;// 3. Request structured JSON response with&lt;/span&gt;
  &lt;span class="c1"&gt;//    account_code, account_name, confidence, reasoning&lt;/span&gt;

  &lt;span class="c1"&gt;// The full prompt includes the 166 UK nominal codes&lt;/span&gt;
  &lt;span class="c1"&gt;// and 365 vendor-to-category rules as system context&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Bedrock returns a structured JSON response with the nominal code, account name, a confidence level (high, medium, or low), and a reasoning string explaining the decision. The confidence scoring was essential - it tells me which transactions I can trust and which need manual review.&lt;/p&gt;
&lt;h2&gt;
  
  
  Few-shot learning that actually improves over time
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most proud of. When I review a categorisation and confirm it's correct (or manually correct it), that decision gets saved to the client's &lt;code&gt;confirmedExamples&lt;/code&gt; array in DynamoDB:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual flow — the key insight is per-client learning&lt;/span&gt;
&lt;span class="c1"&gt;// When a user confirms "AMAZON MARKETPLACE → 7502 Stationery",&lt;/span&gt;
&lt;span class="c1"&gt;// that decision is stored against the client in DynamoDB.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Next time we categorise for that client, confirmed examples&lt;/span&gt;
&lt;span class="c1"&gt;// are injected into the prompt as few-shot context.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Max 50 examples per client, deduplicated by description.&lt;/span&gt;
&lt;span class="c1"&gt;// This means a retail client and a tech consultancy categorise&lt;/span&gt;
&lt;span class="c1"&gt;// the same vendor differently — because their confirmed&lt;/span&gt;
&lt;span class="c1"&gt;// examples are different.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The next time I categorise transactions for that same client, those confirmed examples are injected into the Bedrock prompt as few-shot context. The model sees: "Last time you saw AMAZON MARKETPLACE for this client, it was coded to 7502 Stationery &amp;amp; Printing."&lt;/p&gt;

&lt;p&gt;This creates a per-client learning loop. A retail client's Amazon purchases get categorised differently from a tech consultancy's Amazon purchases - because the confirmed examples are client-specific. After confirming 20-30 transactions, accuracy jumps noticeably because the model has real context about how this particular business operates.&lt;/p&gt;
&lt;h2&gt;
  
  
  Handling real-world bank statements
&lt;/h2&gt;

&lt;p&gt;UK bank CSVs are a mess. Every bank uses different column names, different date formats, and different ways of representing debits and credits. The &lt;code&gt;csvParser.mjs&lt;/code&gt; service handles this with intelligent column detection:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified from csvParser.mjs&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectColumns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&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;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&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="sr"&gt;/date|trans.*date|posted|value.*date/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/description|narrative|details|memo|payee/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^amount$|^value$|^sum$|^total$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/debit|dr|money.*out|paid.*out/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/credit|cr|money.*in|paid.*in/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;map&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;It handles three different amount formats: a single amount column (negative for debits), separate debit and credit columns, and amounts with pound signs and comma formatting. This means I can upload a Lloyds statement, a Barclays statement, and an HSBC statement without any manual configuration.&lt;/p&gt;
&lt;h2&gt;
  
  
  Batched processing with concurrency control
&lt;/h2&gt;

&lt;p&gt;For large bank statements (100+ transactions), hitting Bedrock sequentially would take minutes. LedgerAgent uses a parallel worker pool with concurrency of 3:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual flow — concurrency-controlled batch processing&lt;/span&gt;
&lt;span class="c1"&gt;// Transactions are processed in parallel chunks (concurrency of 3)&lt;/span&gt;
&lt;span class="c1"&gt;// to balance speed against Bedrock rate limits.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// For batches over 100 transactions, the async pipeline kicks in:&lt;/span&gt;
&lt;span class="c1"&gt;// Express → SQS queue → Lambda picks up job → Bedrock AI → DynamoDB&lt;/span&gt;
&lt;span class="c1"&gt;// Frontend polls API Gateway for completion status.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For even larger batches, the async pipeline kicks in - transactions get sent to SQS, picked up by a Lambda function, processed against Bedrock, and results are written back to DynamoDB. The frontend polls for completion via API Gateway.&lt;/p&gt;
&lt;h2&gt;
  
  
  Double-entry done right
&lt;/h2&gt;

&lt;p&gt;One thing that surprised me during development: most "AI bookkeeping" demos I've seen online produce a single-entry list of categorised transactions. That's not bookkeeping - it's just labelling. Real bookkeeping requires double-entry, where every transaction creates two ledger entries that must balance.&lt;/p&gt;

&lt;p&gt;In LedgerAgent, the bank account (nominal code 1200) acts as the contra account for every transaction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transaction type&lt;/th&gt;
&lt;th&gt;Bank account (1200)&lt;/th&gt;
&lt;th&gt;Categorised account&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Money out&lt;/td&gt;
&lt;td&gt;Credit&lt;/td&gt;
&lt;td&gt;Debit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Money in&lt;/td&gt;
&lt;td&gt;Debit&lt;/td&gt;
&lt;td&gt;Credit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trial balance splits automatically at the code 4000 boundary - codes below 4000 go on the Balance Sheet (assets, liabilities, equity), codes 4000 and above go on the Profit &amp;amp; Loss (income, expenses). Total debits must always equal total credits.&lt;/p&gt;

&lt;p&gt;This sounds basic to anyone with accounting training, but getting an AI system to consistently produce balanced double-entry output required careful prompt engineering and validation logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
&lt;strong&gt;Key takeaways:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Domain knowledge is the moat - not the AI wrapper&lt;/li&gt;
&lt;li&gt;Few-shot learning beats fine-tuning when per-client variation is high&lt;/li&gt;
&lt;li&gt;Confidence scoring changes the entire review workflow

&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Domain knowledge is the moat.&lt;/strong&gt; The 166 nominal codes, 365 vendor rules, VAT rate handling, and double-entry logic aren't things you can prompt-engineer from scratch. They come from years of sitting with bank statements. Any developer can connect to Bedrock — few can tell you that a Deliveroo transaction for a sole trader should be coded to 7901 (Staff Welfare) not 7400 (Travel &amp;amp; Subsistence) unless it was a client entertainment expense, in which case it's 7601 (Entertaining).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Few-shot learning beats fine-tuning for this use case.&lt;/strong&gt; I considered fine-tuning a model on accounting data, but the per-client variation is too high. A retail business and a tech consultancy categorise the same vendors completely differently. Dynamic few-shot context from confirmed examples handles this naturally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence scoring changes the workflow.&lt;/strong&gt; Without confidence scores, you'd have to review every single categorisation. With them, I can filter to "low confidence" transactions and review only the 10-15% that genuinely need human judgement. The rest can be confirmed in bulk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;166 UK nominal codes mapped&lt;/li&gt;
&lt;li&gt;365 vendor-to-category rules&lt;/li&gt;
&lt;li&gt;5,860 lines of code across 39 source files&lt;/li&gt;
&lt;li&gt;8 AWS services integrated&lt;/li&gt;
&lt;li&gt;Top ~1,000 from 10,000+ entries in AWS AIdeas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LedgerAgent is currently a tool I use for my own practice, but I'm planning to open it up to other small accountancy firms. If you're an accountant drowning in manual transaction categorisation, or a developer building fintech tools, I'd like to hear from you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://x.com/cubitt33" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Connect with me on X/Twitter to discuss AI in Fintech!&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;If you're interested in the code or want to connect, check out the repository and my profile:&lt;/p&gt;

&lt;p&gt;Check out my &lt;a href="https://github.com/billkhiz-bit" rel="noopener noreferrer"&gt;GitHub Profile&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__3690195"&gt;
    &lt;a href="/billkhiz" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3690195%2Ffe7480cb-0a1b-48fb-9f3c-0b12f86d7bbb.jpg" alt="billkhiz image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/billkhiz"&gt;billkhiz&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/billkhiz"&gt;I make stuff&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;&lt;em&gt;Built with React 19, Express, Amazon Bedrock (Claude 3.5 Haiku + Sonnet), DynamoDB, S3, SQS, Lambda, API Gateway, EventBridge, and Cognito.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>fintech</category>
      <category>accounting</category>
    </item>
    <item>
      <title>I Applied Anthropic's Internal Skills Playbook to My Projects - Here's What Changed</title>
      <dc:creator>billkhiz</dc:creator>
      <pubDate>Wed, 18 Mar 2026 17:25:36 +0000</pubDate>
      <link>https://dev.to/billkhiz/i-applied-anthropics-internal-skills-playbook-to-my-projects-heres-what-changed-3m4h</link>
      <guid>https://dev.to/billkhiz/i-applied-anthropics-internal-skills-playbook-to-my-projects-heres-what-changed-3m4h</guid>
      <description>&lt;p&gt;&lt;a href="https://x.com/trq212" rel="noopener noreferrer"&gt;@trq212&lt;/a&gt; recently published "Lessons from Building Claude Code: How We Use Skills", Anthropic's internal playbook for how they build and use Skills in Claude Code.&lt;/p&gt;

&lt;p&gt;I don't come from a software engineering background. I work in accounting, and I use Claude Code across projects ranging from helping with my bookkeeping automation, AWS Lambda (which I used for the AWS 10,000 Ideas competition) recently to backends to mobile appications (I was very excited to publish my first application to the Google and iOS stores :D). Skills sounded useful but I never knew how to implement them appropriately or how to structure them properly.&lt;/p&gt;

&lt;p&gt;I spent an afternoon applying every recommendation from the article to my own setup across 5 active projects in Claude Code. The following is what I built, what worked, and I recommend you use for your own projects.&lt;/p&gt;

&lt;p&gt;Let's start with this: Skills are "just markdown files"&lt;/p&gt;

&lt;p&gt;They're surprisingly not. A skill is a folder. It can contain scripts, config files, reference data, templates, anything Claude might need.&lt;/p&gt;

&lt;p&gt;Here's what my AWS debugging skill now looks like:&lt;/p&gt;

&lt;p&gt;aws-debug/&lt;br&gt;
    SKILL.md: main instructions&lt;br&gt;
    config.json: which AWS profile to use per project&lt;br&gt;
    references/services.md: maps my projects to Lambda functions, log groups, common errors &lt;/p&gt;

&lt;p&gt;The key thing here: Claude sees the SKILL.md when I say "check the logs." But it only opens references/services.md when it actually needs to look up a specific log group or service mapping. So it's only loaded when relevant.&lt;/p&gt;

&lt;p&gt;The 6 skills that changed my workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;/gotcha, the skill that improves all other skills&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This one is my favourite. Every time Claude makes a mistake, I can type:&lt;/p&gt;

&lt;p&gt;/gotcha Claude forgot --profile flightmap&lt;/p&gt;

&lt;p&gt;And it figures out which skill that belongs to, opens the file, and adds it to the Gotchas section. It's quick and simple.&lt;/p&gt;

&lt;p&gt;The original article makes this point that the Gotchas section is the most valuable part of any skill. I completely agree. But the problem is nobody goes back and updates their skills after writing them. /gotcha fixes that.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;/careful, on-demand production safety&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An on-demand hook that blocks destructive commands (rm -rf, DROP TABLE, git push --force, AWS deletes) for the current session only.&lt;/p&gt;

&lt;p&gt;I mentioned it before - I come from a non technical background so this is incredibly useful. Not ideal to have on all the time but something you should initiate before pushing anything live or anywhere near production.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;/aws-debug, a debugging runbook&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of me manually going into CloudWatch every time something breaks, this skill walks Claude through a proper investigation. Checks the logs, look for cold start timeouts, missing env vars, permission errors, and then writes up a structured report.&lt;/p&gt;

&lt;p&gt;I put a config.json in there with my AWS profiles for each project. That way Claude never forgets which --profile flag to use. That was literally one of the first gotchas I wrote into the skill because it kept getting it wrong.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;/bookkeeping-verify, domain-specific verification&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is where it gets interesting for non-engineers. I categorise bank transactions for accounting clients, hundreds of them per company, into Sage 50 nominal codes. After I'm done categorising, this skill runs through everything and checks: did I miss any? Is the same payee showing up under different categories? Any duplicates? Any codes that don't exist?&lt;/p&gt;

&lt;p&gt;There's a reference file in there (references/categories.md) with all the valid categories and a table of common mistakes. Things like PayPal fees ending up under General Expenses when they should be Bank Charges, or HMRC payments going to the wrong nominal code or account. Claude reads that file during verification but it's not loaded the rest of the time.&lt;/p&gt;

&lt;p&gt;This is a good example of skills going beyond pure software engineering. If your work has any kind of quality checklist, you can turn it into a skill.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;/explain, making code accessible&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I built this one because I'm not from a technical background (have I mentioned this before?) and sometimes I Want Claude to explain what a piece of code does in plain English. Either using analogies or starting with the big picture before the details.&lt;/p&gt;

&lt;p&gt;If you're learning as you go or working with code outside your comfort zone, something like this is worth building.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enhanced /preflight with progressive disclosure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I already had a preflight skill that runs before every commit. But it was a single file trying to cover security, frontend, backend, and accessibility. Now it looks like:&lt;/p&gt;

&lt;p&gt;preflight/&lt;br&gt;
    SKILL.md: the main orchestrator (reads sub-files as needed)&lt;br&gt;
    checks/security.md: detailed security checklist + project-specific notes&lt;br&gt;
    frontend.md: accessibility, performance, design consistency&lt;br&gt;
    backend.md: API safety, Lambda gotchas, Python patterns&lt;/p&gt;

&lt;p&gt;Claude reads the main file and then only pulls in the relevant checks file based on what kind of project it's in. Working on a React app? It reads frontend.md. Working on a Lambda backend? It reads backend.md. The rest stays out of the way.&lt;/p&gt;

&lt;p&gt;A few things I have since learned:&lt;/p&gt;

&lt;p&gt;Write descriptions for Claude, not for you&lt;/p&gt;

&lt;p&gt;The description field at the top of your SKILL.md isn't a summary. It's what Claude uses to decide whether to trigger the skill. So write it like a trigger condition.&lt;/p&gt;

&lt;p&gt;Bad: "AWS debugging tool"&lt;/p&gt;

&lt;p&gt;Good: "Debug AWS Lambda errors, API Gateway issues, or CloudWatch anomalies. Use when the user reports a Lambda failure, 5xx error, timeout, or says 'check the logs'"&lt;/p&gt;

&lt;p&gt;Wire skills into your project CLAUDE.md&lt;/p&gt;

&lt;p&gt;The skill knows how to do something. Your project's CLAUDE.md tells Claude when to do it.&lt;/p&gt;

&lt;p&gt;In my bookkeeping project I added:&lt;br&gt;
  Run /bookkeeping-verify after categorising any company&lt;/p&gt;

&lt;p&gt;In my AWS projects:&lt;br&gt;
  Run /careful before touching live AWS resources&lt;br&gt;
  Use /aws-debug when Lambda errors occur&lt;/p&gt;

&lt;p&gt;The skills work in any project, but each project's CLAUDE.md gives them context.&lt;/p&gt;

&lt;p&gt;Use config files for stuff that changes per project&lt;/p&gt;

&lt;p&gt;If your skill needs something specific to you or your project (AWS profile, a channel name, a database), put a config.json in the skill folder. Claude reads it when the skill runs.&lt;/p&gt;

&lt;p&gt;Global skills vs project overrides&lt;/p&gt;

&lt;p&gt;I put reusable skills in ~/.claude/skills/ so they're available everywhere. Some projects also have their own skills in .claude/skills/ that override the global ones. Right now I've got 13 global skills and 6 project-level overrides across 4 projects.&lt;/p&gt;

&lt;p&gt;If you're starting from scratch&lt;/p&gt;

&lt;p&gt;Start with two skills: /preflight and /gotcha. Preflight stops you committing broken code. Gotcha captures mistakes so they don't happen twice. Build everything else from there.&lt;/p&gt;

&lt;p&gt;Don't try to write a perfect skill on day one. Anthropic's own team says their best skills started as a few lines and one gotcha, and got better over time because people kept adding to them. That tracks with my experience too.&lt;/p&gt;

&lt;p&gt;Use folders. The moment your skill has more than a couple of sections of reference material, split it into sub-files. Claude only reads what it needs.&lt;/p&gt;

&lt;p&gt;Don't write stuff Claude already knows. Focus on things that are specific to you: your project requirements, your conventions, the edge cases that keep tripping you up.&lt;/p&gt;

&lt;p&gt;Always launch Claude from your project directory. I know this sounds obvious and, to be honest, I still forget to do this myself. When Claude starts in the right place, it picks up your project CLAUDE.md and all your project-level skills. Start from the wrong directory and none of that loads instantly.&lt;/p&gt;

&lt;p&gt;And if you're not an engineer, that's fine. If your work has repeatable processes or domain knowledge that Claude doesn't have by default, it works the same way. My bookkeeping verification skill has nothing to do with code. It's just a quality checklist for accounting data.&lt;/p&gt;

&lt;p&gt;How long did this take?&lt;/p&gt;

&lt;p&gt;One afternoon to set everything up. After that it's just /gotcha whenever something goes wrong. The skills keep getting better on their own.&lt;/p&gt;

&lt;p&gt;My full setup&lt;/p&gt;

&lt;p&gt;~/.claude/skills/ (13 global skills)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;audit: weekly codebase audit&lt;/li&gt;
&lt;li&gt;aws-debug: AWS debugging runbook (+config, +references)&lt;/li&gt;
&lt;li&gt;bookkeeping-verify: transaction verification (+references)&lt;/li&gt;
&lt;li&gt;careful: on-demand prod safety hook&lt;/li&gt;
&lt;li&gt;docs: documentation generation&lt;/li&gt;
&lt;li&gt;explain: plain English code explainer&lt;/li&gt;
&lt;li&gt;gotcha: capture mistakes into skill gotchas&lt;/li&gt;
&lt;li&gt;migrate: dependency upgrades&lt;/li&gt;
&lt;li&gt;perf: performance analysis&lt;/li&gt;
&lt;li&gt;preflight: pre-commit gates (+checks/ sub-files)&lt;/li&gt;
&lt;li&gt;refactor: code cleanup&lt;/li&gt;
&lt;li&gt;release: generic release pipeline&lt;/li&gt;
&lt;li&gt;test: test generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus 6 project-level overrides across 4 projects for specific release and preflight workflows.&lt;/p&gt;

&lt;p&gt;Thank you very much to @trq212 for publishing the original article. Went from "I should probably sort out my skills at some point" to actually having a setup I am very happy with (for now :D).&lt;/p&gt;

&lt;p&gt;What skills have you built? I would be keen to see what others are doing with this.&lt;/p&gt;

&lt;p&gt;The key thing here: Claude sees the SKILL.md when I say "check the logs." But it only opens references/services.md when it actually needs to look up a specific log group or service mapping. So it's only loaded when relevant.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
    </item>
    <item>
      <title>Building Heritage Keeper: A Gemini Live Agent for Family Story Preservation</title>
      <dc:creator>billkhiz</dc:creator>
      <pubDate>Mon, 16 Mar 2026 21:36:00 +0000</pubDate>
      <link>https://dev.to/billkhiz/building-heritage-keeper-a-gemini-live-agent-for-family-story-preservatio-57fk</link>
      <guid>https://dev.to/billkhiz/building-heritage-keeper-a-gemini-live-agent-for-family-story-preservatio-57fk</guid>
      <description>&lt;p&gt;&lt;em&gt;How I used the Gemini Live API with native audio, function calling, and Google Search grounding to build an AI agent that turns family conversations into illustrated timelines.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was created for the purposes of entering the Gemini Live Agent Challenge hackathon. #GeminiLiveAgentChallenge&lt;/em&gt;&lt;/p&gt;




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

&lt;p&gt;My grandmother came to London from Jamaica in the 1950s. She had stories about Brixton in the Windrush era, about what things cost, about neighbours and churches and dance halls. Most of those stories were never written down.&lt;/p&gt;

&lt;p&gt;This is true for nearly every family. The stories exist in the memories of older generations - rich, vivid, emotional - but they're never preserved. Traditional approaches (family tree software, memoir-writing tools) feel like work. They require forms, dates, and data entry. Nobody wants to do that.&lt;/p&gt;

&lt;p&gt;What if preserving family history was as easy as having a conversation?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Heritage Keeper
&lt;/h2&gt;

&lt;p&gt;Heritage Keeper is a voice-first AI agent built on the Gemini Live API. You simply talk about your memories, and the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listens&lt;/strong&gt; via real-time audio streaming&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extracts&lt;/strong&gt; names, dates, places, and relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saves&lt;/strong&gt; each memory as a timeline entry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finds&lt;/strong&gt; historical photographs from Wikimedia Commons&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builds&lt;/strong&gt; a family tree from the people you mention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounds&lt;/strong&gt; historical facts using Google Search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adds context&lt;/strong&gt; - cost of living, daily life, world events&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No forms. No data entry. Just talk.&lt;/p&gt;

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

&lt;p&gt;The browser captures microphone audio as PCM 16-bit at 16kHz and streams it over a WebSocket to an Express server on Google Cloud Run. The server maintains a bidirectional session with the Gemini Live API using the Google GenAI SDK. Gemini responds with native audio (24kHz) and function calls.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser (React 19)&lt;/strong&gt; communicates via WebSocket with PCM audio and JSON messages to the &lt;strong&gt;Express Server on Cloud Run&lt;/strong&gt;, which connects to the &lt;strong&gt;Gemini Live API&lt;/strong&gt; (gemini-2.5-flash-native-audio). The agent has access to 5 function-calling tools, Google Search grounding, and the Wikimedia Commons API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Tools
&lt;/h2&gt;

&lt;p&gt;I designed five function-calling tools that the agent uses autonomously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;save_story&lt;/strong&gt; - Extracts year, title, summary, location, Then/Now descriptions, cost of living, daily life, events, and photo search queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;search_photos&lt;/strong&gt; - Queries Wikimedia Commons for historical photographs with bitmap-only filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;add_family_member&lt;/strong&gt; - Adds a person to the family tree with generation number and relationship&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;get_family_tree&lt;/strong&gt; - Retrieves the current tree (so the agent knows who's already been mentioned)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;get_timeline&lt;/strong&gt; - Retrieves saved stories (so the agent can reference previous memories)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent decides when to call each tool based on the conversation. When you say "my grandmother came to London in 1955", it calls save_story AND add_family_member AND search_photos - all autonomously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Search Grounding
&lt;/h2&gt;

&lt;p&gt;One of the most impactful additions was enabling Google Search grounding alongside function calling. This means when the agent generates historical facts about 1950s Brixton, it can verify them against Google Search results. The grounding sources are stored per story and displayed as clickable links - so users can verify the facts themselves.&lt;/p&gt;

&lt;p&gt;This transforms AI-generated context from "maybe true" to "verifiably true."&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Thought Parts Need Filtering
&lt;/h3&gt;

&lt;p&gt;The gemini-2.5-flash-native-audio model includes internal reasoning ("thought" parts) in its responses. Without filtering, users see the model's chain-of-thought ("Interpreting 'Funny Bob'... I'm hesitant to categorise this..."). The fix was checking each response part and only forwarding actual responses, not internal reasoning. A small code change with massive UX impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Message Format Varies
&lt;/h3&gt;

&lt;p&gt;The SDK's onmessage callback can pass different message formats - a LiveServerMessage, a MessageEvent, or even a JSON string. My parser needed to handle all three cases, with a graceful fallback for raw audio binary data that would otherwise crash the JSON parser.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cost of Living &amp;gt; Music Trivia
&lt;/h3&gt;

&lt;p&gt;I initially included "popular music" and "film/TV" as cultural context. But for family heritage, knowing that "a house cost £2,500 and the weekly wage was £15" is far more powerful than knowing what song was number one. It grounds the story in lived reality.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Auto-Reconnect Is Essential for Live APIs
&lt;/h3&gt;

&lt;p&gt;WebSocket connections to the Gemini Live API can drop (Cloud Run timeouts, network blips). Exponential backoff reconnection (1s, 2s, 4s) keeps the experience seamless.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Voice Commands for Family Trees
&lt;/h3&gt;

&lt;p&gt;Users want to build family trees quickly by voice - "Bob is my father", "Elena is Bob's mother." The agent needed specific instructions to handle these short commands with just an add_family_member call, without trying to create a full story entry.&lt;/p&gt;

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

&lt;p&gt;Heritage Keeper is a prototype built for the Gemini Live Agent Challenge. The natural evolution is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User accounts&lt;/strong&gt; with Firestore persistence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Family collaboration&lt;/strong&gt; - multiple members contributing to the same timeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Genealogy API integration&lt;/strong&gt; for data enrichment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile app&lt;/strong&gt; for recording stories on the go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core insight remains: the best tool for preserving family history is a good conversation partner.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Heritage Keeper was built for the &lt;a href="https://geminiliveagentchallenge.devpost.com/" rel="noopener noreferrer"&gt;Gemini Live Agent Challenge 2026&lt;/a&gt;. Try it at &lt;a href="https://heritage-keeper-87502328327.us-central1.run.app" rel="noopener noreferrer"&gt;heritage-keeper-87502328327.us-central1.run.app&lt;/a&gt;. View the source on &lt;a href="https://github.com/billkhiz-bit/heritage-keeper" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>gemini</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
