<?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: Sahil SIlare</title>
    <description>The latest articles on DEV Community by Sahil SIlare (@sahil9001).</description>
    <link>https://dev.to/sahil9001</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%2F241632%2F202956ef-d516-41e6-b5e6-63d29b5d1866.png</url>
      <title>DEV Community: Sahil SIlare</title>
      <link>https://dev.to/sahil9001</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sahil9001"/>
    <language>en</language>
    <item>
      <title>I built 'Ask Your Life' — a personal Coral agent that answers questions about your money &amp; deadlines with SQL</title>
      <dc:creator>Sahil SIlare</dc:creator>
      <pubDate>Wed, 27 May 2026 10:23:24 +0000</pubDate>
      <link>https://dev.to/sahil9001/i-built-ask-your-life-a-personal-coral-agent-that-answers-questions-about-your-money--52a8</link>
      <guid>https://dev.to/sahil9001/i-built-ask-your-life-a-personal-coral-agent-that-answers-questions-about-your-money--52a8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;"Here are the personal admin risks that can cost me money, time, or access this week — with the receipts." 🪸&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one sentence is the whole pitch for &lt;strong&gt;Life Risk Radar&lt;/strong&gt;, my entry for the WeMakeDevs &lt;strong&gt;Pirates of the Coral-bean&lt;/strong&gt; hackathon (Personal Agent track). It's a personal agent you can &lt;em&gt;ask&lt;/em&gt; — in plain English — about your inbox, calendar, and money, and it answers with a &lt;strong&gt;real cross-source SQL join&lt;/strong&gt;, the &lt;strong&gt;evidence behind every number&lt;/strong&gt;, and a &lt;strong&gt;drafted action&lt;/strong&gt; you can send.&lt;/p&gt;

&lt;p&gt;This post is the full build story: the problem, the architecture, how &lt;a href="https://www.wemakedevs.org/hackathons/coral" rel="noopener noreferrer"&gt;Coral&lt;/a&gt; turns "your life" into a queryable database, the agent loop that pairs Claude with Coral SQL, the safety model, and the things that broke along the way (a &lt;code&gt;--&lt;/code&gt; that wasn't a comment, a Cloudflare 403, and a moment where I realised my SQL wasn't actually joining anything).&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: your life leaks money through admin
&lt;/h2&gt;

&lt;p&gt;Money, time, and access leak through the cracks of everyday admin, and almost always silently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a free trial that &lt;strong&gt;auto-renews tomorrow&lt;/strong&gt; for $96/year 💸&lt;/li&gt;
&lt;li&gt;an Amazon &lt;strong&gt;return window&lt;/strong&gt; quietly closing 🧾&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;duplicate subscription charge&lt;/strong&gt; you never noticed 🪞&lt;/li&gt;
&lt;li&gt;a hotel &lt;strong&gt;free-cancellation deadline&lt;/strong&gt; that locks in a $280 fee ⏰&lt;/li&gt;
&lt;li&gt;a passport appointment you'll &lt;strong&gt;fail because a document is missing&lt;/strong&gt; 📄&lt;/li&gt;
&lt;li&gt;a bank &lt;strong&gt;KYC deadline&lt;/strong&gt; that could freeze your account 🪪&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the thing: the evidence for all of these is &lt;em&gt;already in your accounts.&lt;/em&gt; The renewal email is in Gmail. The charge is in your card statement. The appointment is on your Calendar. The missing file is in your documents folder.&lt;/p&gt;

&lt;p&gt;The problem was never a lack of data. &lt;strong&gt;The problem is that nobody joins it.&lt;/strong&gt; Your email doesn't know about your transactions. Your calendar doesn't know which document you're missing. Each source is an island, and the risk lives in the gaps &lt;em&gt;between&lt;/em&gt; them.&lt;/p&gt;

&lt;p&gt;That gap is exactly what Coral is built to close.&lt;/p&gt;




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

&lt;p&gt;Life Risk Radar has two modes, and the order matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Ask your life (the hero).&lt;/strong&gt; A command bar where you type a question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"What is costing me money this week?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Am I being double-charged anywhere?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Which missing document is blocking the most deadlines?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"What subscriptions or trials are about to renew?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent turns your question into Coral SQL, runs a genuine cross-source join, and answers with a one-line verdict, a result card per row, &lt;strong&gt;the exact SQL it ran&lt;/strong&gt;, and a ready-to-send drafted action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Scan everything (the fallback).&lt;/strong&gt; Don't want to ask? One button sweeps every source, ranks the risks, and lays them out as a board you can open for a step-by-step "close this risk" action plan.&lt;/p&gt;

&lt;p&gt;The whole thing is light-mode, editorial, and deliberately &lt;em&gt;un&lt;/em&gt;-dashboardy — because the star isn't a chart, it's the answer and the query behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pivot: from dashboard to agent
&lt;/h2&gt;

&lt;p&gt;I'll be honest about how this started, because the turning point is the most useful part of the story.&lt;/p&gt;

&lt;p&gt;My first version was a tidy risk &lt;strong&gt;dashboard&lt;/strong&gt;. Click "Scan", get ranked cards. It looked finished. Then I opened the SQL that powered it and caught myself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;gmail&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;life_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gmail_messages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
     &lt;span class="n"&gt;deadlines&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;life_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manual_deadlines&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
     &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;life_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
     &lt;span class="n"&gt;calendar_events&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;life_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calendar_events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;duplicate_charges&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deadline_risks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It &lt;em&gt;declared&lt;/em&gt; CTEs for five sources… and then &lt;strong&gt;joined none of them&lt;/strong&gt;. The "cross-source evidence" was actually being stitched together afterwards in TypeScript with a fuzzy string matcher. I was using Coral as a fancy file reader, not as a join engine.&lt;/p&gt;

&lt;p&gt;That's backwards. The single most valuable thing Coral gives you is a &lt;strong&gt;SQL interface that joins across totally different sources&lt;/strong&gt; — email, calendar, files, APIs — as if they were one database. If my SQL wasn't joining, I wasn't really using Coral.&lt;/p&gt;

&lt;p&gt;So I pivoted. Not the data, not the UI work — the &lt;em&gt;thesis&lt;/em&gt;. From "a dashboard that reads from Coral" to &lt;strong&gt;"an agent whose entire job is to ask Coral the right cross-source question."&lt;/strong&gt; That reframe is what turned a fine project into one with a point of view.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Coral fits
&lt;/h2&gt;

&lt;p&gt;If you haven't used it: Coral is a local-first SQL engine that points at "sources" — APIs, files, calendars, databases — described by small spec files, and lets you query (and &lt;strong&gt;join&lt;/strong&gt;) them with plain SQL. It also ships an MCP server (&lt;code&gt;coral mcp-stdio&lt;/code&gt;) so an agent can use it as a tool directly.&lt;/p&gt;

&lt;p&gt;Life Risk Radar uses two sources, no extra plumbing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;life_files&lt;/code&gt;&lt;/strong&gt; — a JSONL-backed source exposing five tables: &lt;code&gt;transactions&lt;/code&gt;, &lt;code&gt;documents&lt;/code&gt;, &lt;code&gt;manual_deadlines&lt;/code&gt;, &lt;code&gt;calendar_events&lt;/code&gt;, and &lt;code&gt;gmail_messages&lt;/code&gt;. This is the reproducible demo data, so the project runs end-to-end without touching a personal inbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gmail&lt;/code&gt;&lt;/strong&gt; — an HTTP source spec that hits the real Gmail API with an OAuth token, exposing &lt;code&gt;message_search&lt;/code&gt; and &lt;code&gt;message_details&lt;/code&gt; for going live later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Registering a source is two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;coral &lt;span class="nb"&gt;source &lt;/span&gt;add &lt;span class="nt"&gt;--file&lt;/span&gt; sources/life_files/manifest.yaml
coral &lt;span class="nb"&gt;source test &lt;/span&gt;life_files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and now five different "islands" are one schema you can join.&lt;/p&gt;




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

&lt;p&gt;Here's the request flow for a free-text question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You type a question
        │
        ▼
┌──────────────────────┐   schema + question
│  Claude (Opus 4.7)   │ ─────────────────────►  Coral SQL (read-only)
└──────────────────────┘
        │ validated against a SELECT-only allowlist
        ▼
┌──────────────────────┐
│   coral sql … --      │   runs a REAL cross-source JOIN
└──────────────────────┘
        │ rows + evidence
        ▼
┌──────────────────────┐
│  Claude (Opus 4.7)   │   reads the rows → headline + drafted action
└──────────────────────┘
        │
        ▼
   UI shows: verdict · result cards · THE SQL · draft to send
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a genuine two-step agent loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Question → SQL.&lt;/strong&gt; Claude is given the schema and writes one Coral query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate.&lt;/strong&gt; The generated SQL passes through a read-only allowlist before it can touch anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run.&lt;/strong&gt; Coral executes the join and returns rows with evidence attached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rows → answer.&lt;/strong&gt; Claude reads the actual results and writes the headline and a drafted action.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The UI then shows &lt;strong&gt;the SQL it ran&lt;/strong&gt;. That transparency is the design centerpiece — when you can see the query and the joined evidence, the agent stops feeling like magic and starts feeling &lt;em&gt;inspectable&lt;/em&gt;. That matters a lot for a tool that's poking at your money.&lt;/p&gt;

&lt;p&gt;The stack: &lt;strong&gt;Next.js + TypeScript&lt;/strong&gt;, &lt;strong&gt;Chakra UI&lt;/strong&gt; (a light editorial theme — Fraunces + Hanken Grotesk), &lt;strong&gt;Coral&lt;/strong&gt; for the SQL/joins, and &lt;strong&gt;Claude (Opus 4.7)&lt;/strong&gt; for NL→SQL and summarization, with prompt caching on the schema system prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  The query I'm proud of: single point of failure
&lt;/h2&gt;

&lt;p&gt;Here's a question a dashboard would never answer well: &lt;em&gt;"Which missing document is blocking the most deadlines?"&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;missing_document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;deadlines_blocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;STRING_AGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' | '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocked_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;due_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;earliest_due&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;life_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;life_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manual_deadlines&lt;/span&gt; &lt;span class="n"&gt;dl&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%passport%'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%passport%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%kyc%'&lt;/span&gt;  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%kyc%'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'kyc'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%bank%'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%bank%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'missing'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;deadlines_blocked&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;address_proof.pdf&lt;/code&gt; blocks 2 deadlines — a single point of failure.&lt;/strong&gt;&lt;br&gt;
(Your passport appointment &lt;em&gt;and&lt;/em&gt; your bank KYC both need it.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One missing file, two missed deadlines, surfaced in a single row. That's an insight a join &lt;em&gt;produces&lt;/em&gt; and a list of cards never will.&lt;/p&gt;

&lt;p&gt;The other two flagship questions map to equally real joins:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Cross-source join&lt;/th&gt;
&lt;th&gt;What it attaches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Am I being double-charged?"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;transactions&lt;/code&gt; ⋈ &lt;code&gt;gmail_messages&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the receipt email next to the duplicate charge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What's costing me money this week?"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;manual_deadlines&lt;/code&gt; ⋈ &lt;code&gt;gmail_messages&lt;/code&gt; ⋈ &lt;code&gt;calendar_events&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the billing email &lt;em&gt;and&lt;/em&gt; the calendar reminder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, the duplicate-charge query joins your card transactions to the receipt email that explains them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;charge_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&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="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;receipt_evidence&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;life_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;life_files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gmail_messages&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;LOWER&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="n"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt;
  &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;LOWER&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="n"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merchant&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&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="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;em&gt;"Adobe charged 2× — $39.98 to review,"&lt;/em&gt; with the matching &lt;code&gt;"Adobe payment receipt - $19.99"&lt;/code&gt; email pulled in by the join. Three sources, one row, every number traceable to where it came from.&lt;/p&gt;




&lt;h2&gt;
  
  
  The agent loop, in code
&lt;/h2&gt;

&lt;p&gt;Question → SQL is a single Claude call, schema-grounded and cached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;schemaForPrompt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;cache_control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ephemeral&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;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Question: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nWrite exactly one Coral SQL query.`&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;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateReadOnlySql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stripFences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;textOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;  &lt;span class="c1"&gt;// throws if unsafe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then Coral runs it, and a second Claude call reads the &lt;em&gt;actual&lt;/em&gt; rows and writes the human answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runCoral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;// real cross-source join&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;summarizeWithClaude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// → { headline: "…", draft: { subject, body } }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what makes it an agent rather than a search box: it reasons over the results, not just the question.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping it safe — and unbreakable in a demo
&lt;/h2&gt;

&lt;p&gt;Two engineering constraints shaped the build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The generated SQL is sandboxed.&lt;/strong&gt; Anything Claude writes is validated before it can reach Coral — single statement, &lt;code&gt;SELECT&lt;/code&gt;/&lt;code&gt;WITH&lt;/code&gt; only, allowed schema only, no DDL/DML:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Only a single statement is allowed.&lt;/span&gt;&lt;span class="dl"&gt;"&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="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;select|with&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&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;trimmed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Only SELECT/WITH is allowed.&lt;/span&gt;&lt;span class="dl"&gt;"&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;FORBIDDEN&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;trimmed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write/DDL keywords are not allowed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// every schema-qualified reference must be `life_files.*`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I unit-tested it against &lt;code&gt;DROP TABLE&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, multi-statement injection, and a &lt;code&gt;secrets.users&lt;/code&gt; reference — all rejected; legit &lt;code&gt;SELECT&lt;/code&gt;/&lt;code&gt;WITH&lt;/code&gt; over &lt;code&gt;life_files&lt;/code&gt; allowed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The demo can never break.&lt;/strong&gt; Three layers of graceful degradation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The four headline questions ship with &lt;strong&gt;hand-vetted join queries&lt;/strong&gt;, so the app works with &lt;strong&gt;no API key at all&lt;/strong&gt; — Claude only powers free-text questions.&lt;/li&gt;
&lt;li&gt;If the Coral CLI isn't present (e.g. a serverless deploy), it &lt;strong&gt;falls back to computing the same answers from the local JSONL&lt;/strong&gt; in TypeScript.&lt;/li&gt;
&lt;li&gt;The demo runs on &lt;strong&gt;seeded, reproducible data&lt;/strong&gt; — a dedicated demo Google account and local sample files — so anyone can run it end-to-end without touching a personal inbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the difference between "works on my machine at 2am" and "works on stage."&lt;/p&gt;




&lt;h2&gt;
  
  
  Things that broke (and what they taught me)
&lt;/h2&gt;

&lt;p&gt;No build is clean. The honest log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My SQL wasn't joining.&lt;/strong&gt; Covered above — the most important fix wasn't code, it was the thesis. Real joins (&lt;code&gt;documents ⋈ deadlines&lt;/code&gt;, &lt;code&gt;transactions ⋈ gmail&lt;/code&gt;) replaced TypeScript string-matching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;--&lt;/code&gt; that wasn't a comment.&lt;/strong&gt; My query files start with &lt;code&gt;-- "What is costing me money this week?"&lt;/code&gt;. Passing that to &lt;code&gt;coral sql "&amp;lt;query&amp;gt;"&lt;/code&gt; made the CLI read the leading &lt;code&gt;--&lt;/code&gt; as a &lt;em&gt;flag&lt;/em&gt; and error out. The fix: the end-of-options separator — &lt;code&gt;coral sql --format json -- "$(cat query.sql)"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 403 that wasn't an auth problem.&lt;/strong&gt; Publishing this very post failed with &lt;code&gt;HTTP 403&lt;/code&gt;. The API key was valid — Dev.to sits behind Cloudflare, which blocks the default &lt;code&gt;Python-urllib&lt;/code&gt; User-Agent. A one-line &lt;code&gt;User-Agent&lt;/code&gt; header fixed it. Good reminder that 403 ≠ "bad credentials."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A half-installed &lt;code&gt;node_modules&lt;/code&gt;.&lt;/strong&gt; An interrupted install left empty package folders that passed a stale incremental typecheck but failed a clean build. A clean &lt;code&gt;npm ci&lt;/code&gt; was the real fix — and a lesson to never trust a cached green check.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's next: connecting real accounts
&lt;/h2&gt;

&lt;p&gt;The plumbing to read a live account already exists — the &lt;code&gt;gmail&lt;/code&gt; HTTP source spec plus read-only OAuth. The remaining work is the genuinely hard part:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generalized extraction.&lt;/strong&gt; The demo's risk rules know about "Notion," "Adobe," "passport." A real inbox needs to turn &lt;em&gt;arbitrary&lt;/em&gt; emails into amounts, dates, and intents — a great fit for an LLM extraction step feeding the same Coral tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A live transactions feed&lt;/strong&gt; (statement import or an aggregator) so duplicate detection runs on real spend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A live Calendar source spec&lt;/strong&gt;, mirroring the Gmail one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But the core bet is already proven: &lt;strong&gt;your life is queryable, and the join is where the insight lives.&lt;/strong&gt;&lt;/p&gt;




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

&lt;p&gt;If you build on Coral, the lesson I'd pass on is simple: &lt;strong&gt;don't just read from your sources — join them.&lt;/strong&gt; A list of records is a chore; a joined row is an insight ("this one missing file blocks two deadlines"). And if you put the generated SQL on screen, your agent earns trust instead of asking for it.&lt;/p&gt;

&lt;p&gt;Life Risk Radar is built for the WeMakeDevs &lt;strong&gt;Pirates of the Coral-bean&lt;/strong&gt; hackathon (Personal Agent track). Natural-language questions in, real cross-source SQL out, evidence and a drafted action every time. 🪸&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sql</category>
      <category>webdev</category>
      <category>hackathon</category>
    </item>
  </channel>
</rss>
