{"slug": "i-built-ask-your-life-a-personal-coral-agent-that-answers-questions-about-your", "title": "I built 'Ask Your Life' — a personal Coral agent that answers questions about your money & deadlines with SQL", "summary": "A developer built \"Life Risk Radar,\" a personal Coral agent that answers plain-English questions about inbox, calendar, and financial data by executing real cross-source SQL joins. The agent, created for the WeMakeDevs Pirates of the Coral-bean hackathon, surfaces risks like missed deadlines or duplicate charges with evidence and drafted actions, pivoting from a dashboard approach to an agent that queries Coral's SQL engine directly.", "body_md": "\"Here are the personal admin risks that can cost me money, time, or access this week — with the receipts.\" 🪸\n\nThat one sentence is the whole pitch for **Life Risk Radar**, my entry for the WeMakeDevs **Pirates of the Coral-bean** hackathon (Personal Agent track). It's a personal agent you can *ask* — in plain English — about your inbox, calendar, and money, and it answers with a **real cross-source SQL join**, the **evidence behind every number**, and a **drafted action** you can send.\n\nThis post is the full build story: the problem, the architecture, how [Coral](https://www.wemakedevs.org/hackathons/coral) 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 `--`\n\nthat wasn't a comment, a Cloudflare 403, and a moment where I realised my SQL wasn't actually joining anything).\n\nMoney, time, and access leak through the cracks of everyday admin, and almost always silently:\n\nHere's the thing: the evidence for all of these is *already in your accounts.* 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.\n\nThe problem was never a lack of data. **The problem is that nobody joins it.** 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 *between* them.\n\nThat gap is exactly what Coral is built to close.\n\nLife Risk Radar has two modes, and the order matters.\n\n**1. Ask your life (the hero).** A command bar where you type a question:\n\nThe 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, **the exact SQL it ran**, and a ready-to-send drafted action.\n\n**2. Scan everything (the fallback).** 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.\n\nThe whole thing is light-mode, editorial, and deliberately *un*-dashboardy — because the star isn't a chart, it's the answer and the query behind it.\n\nI'll be honest about how this started, because the turning point is the most useful part of the story.\n\nMy first version was a tidy risk **dashboard**. Click \"Scan\", get ranked cards. It looked finished. Then I opened the SQL that powered it and caught myself:\n\n```\nWITH gmail AS (SELECT ... FROM life_files.gmail_messages),\n     deadlines AS (SELECT ... FROM life_files.manual_deadlines),\n     documents AS (SELECT ... FROM life_files.documents),\n     calendar_events AS (SELECT ... FROM life_files.calendar_events)\nSELECT * FROM duplicate_charges\nUNION ALL\nSELECT * FROM deadline_risks;\n```\n\nIt *declared* CTEs for five sources… and then **joined none of them**. 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.\n\nThat's backwards. The single most valuable thing Coral gives you is a **SQL interface that joins across totally different sources** — email, calendar, files, APIs — as if they were one database. If my SQL wasn't joining, I wasn't really using Coral.\n\nSo I pivoted. Not the data, not the UI work — the *thesis*. From \"a dashboard that reads from Coral\" to **\"an agent whose entire job is to ask Coral the right cross-source question.\"** That reframe is what turned a fine project into one with a point of view.\n\nIf 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 **join**) them with plain SQL. It also ships an MCP server (`coral mcp-stdio`\n\n) so an agent can use it as a tool directly.\n\nLife Risk Radar uses two sources, no extra plumbing:\n\n`life_files`\n\n`transactions`\n\n, `documents`\n\n, `manual_deadlines`\n\n, `calendar_events`\n\n, and `gmail_messages`\n\n. This is the reproducible demo data, so the project runs end-to-end without touching a personal inbox.`gmail`\n\n`message_search`\n\nand `message_details`\n\nfor going live later.Registering a source is two commands:\n\n```\ncoral source add --file sources/life_files/manifest.yaml\ncoral source test life_files\n```\n\n…and now five different \"islands\" are one schema you can join.\n\nHere's the request flow for a free-text question:\n\n```\nYou type a question\n        │\n        ▼\n┌──────────────────────┐   schema + question\n│  Claude (Opus 4.7)   │ ─────────────────────►  Coral SQL (read-only)\n└──────────────────────┘\n        │ validated against a SELECT-only allowlist\n        ▼\n┌──────────────────────┐\n│   coral sql … --      │   runs a REAL cross-source JOIN\n└──────────────────────┘\n        │ rows + evidence\n        ▼\n┌──────────────────────┐\n│  Claude (Opus 4.7)   │   reads the rows → headline + drafted action\n└──────────────────────┘\n        │\n        ▼\n   UI shows: verdict · result cards · THE SQL · draft to send\n```\n\nIt's a genuine two-step agent loop:\n\nThe UI then shows **the SQL it ran**. 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 *inspectable*. That matters a lot for a tool that's poking at your money.\n\nThe stack: **Next.js + TypeScript**, **Chakra UI** (a light editorial theme — Fraunces + Hanken Grotesk), **Coral** for the SQL/joins, and **Claude (Opus 4.7)** for NL→SQL and summarization, with prompt caching on the schema system prompt.\n\nHere's a question a dashboard would never answer well: *\"Which missing document is blocking the most deadlines?\"*\n\n```\nSELECT\n  doc.name AS missing_document,\n  COUNT(DISTINCT dl.id) AS deadlines_blocked,\n  STRING_AGG(DISTINCT dl.title, ' | ') AS blocked_items,\n  MIN(dl.due_at) AS earliest_due\nFROM life_files.documents doc\nJOIN life_files.manual_deadlines dl\n  ON (doc.tags LIKE '%passport%' AND LOWER(dl.title) LIKE '%passport%')\n  OR (doc.tags LIKE '%kyc%'  AND (LOWER(dl.title) LIKE '%kyc%' OR dl.category = 'kyc'))\n  OR (doc.tags LIKE '%bank%' AND LOWER(dl.title) LIKE '%bank%')\nWHERE doc.status = 'missing'\nGROUP BY doc.name\nORDER BY deadlines_blocked DESC;\n```\n\nThe answer:\n\n`address_proof.pdf`\n\nblocks 2 deadlines — a single point of failure.\n\n(Your passport appointmentandyour bank KYC both need it.)\n\nOne missing file, two missed deadlines, surfaced in a single row. That's an insight a join *produces* and a list of cards never will.\n\nThe other two flagship questions map to equally real joins:\n\n| Question | Cross-source join | What it attaches |\n|---|---|---|\n| \"Am I being double-charged?\" |\n`transactions` ⋈ `gmail_messages`\n|\nthe receipt email next to the duplicate charge |\n| \"What's costing me money this week?\" |\n`manual_deadlines` ⋈ `gmail_messages` ⋈ `calendar_events`\n|\nthe billing email and the calendar reminder |\n\nFor example, the duplicate-charge query joins your card transactions to the receipt email that explains them:\n\n```\nSELECT t.merchant, COUNT(*) AS charge_count, SUM(t.amount) AS total_amount,\n       MAX(g.subject) AS receipt_evidence\nFROM life_files.transactions t\nLEFT JOIN life_files.gmail_messages g\n  ON LOWER(g.subject) LIKE '%' || LOWER(t.merchant) || '%'\n  OR LOWER(g.body_text) LIKE '%' || LOWER(t.merchant) || '%'\nGROUP BY t.merchant\nHAVING COUNT(*) >= 2;\n```\n\n→ *\"Adobe charged 2× — $39.98 to review,\"* with the matching `\"Adobe payment receipt - $19.99\"`\n\nemail pulled in by the join. Three sources, one row, every number traceable to where it came from.\n\nQuestion → SQL is a single Claude call, schema-grounded and cached:\n\n``` js\nconst message = await client.messages.create({\n  model: \"claude-opus-4-7\",\n  max_tokens: 700,\n  system: [{ type: \"text\", text: schemaForPrompt(), cache_control: { type: \"ephemeral\" } }],\n  messages: [{ role: \"user\", content: `Question: ${question}\\nWrite exactly one Coral SQL query.` }]\n});\nconst sql = validateReadOnlySql(stripFences(textOf(message)));  // throws if unsafe\n```\n\nThen Coral runs it, and a second Claude call reads the *actual* rows and writes the human answer:\n\n``` js\nconst rows = await runCoral(sql);                 // real cross-source join\nconst summary = await summarizeWithClaude(question, columns, rows);\n// → { headline: \"…\", draft: { subject, body } }\n```\n\nThis is what makes it an agent rather than a search box: it reasons over the results, not just the question.\n\nTwo engineering constraints shaped the build.\n\n**1. The generated SQL is sandboxed.** Anything Claude writes is validated before it can reach Coral — single statement, `SELECT`\n\n/`WITH`\n\nonly, allowed schema only, no DDL/DML:\n\n```\nif (trimmed.includes(\";\"))        return reject(\"Only a single statement is allowed.\");\nif (!/^(select|with)\\b/i.test(trimmed)) return reject(\"Only SELECT/WITH is allowed.\");\nif (FORBIDDEN.test(trimmed))      return reject(\"Write/DDL keywords are not allowed.\");\n// every schema-qualified reference must be `life_files.*`\n```\n\nI unit-tested it against `DROP TABLE`\n\n, `UPDATE`\n\n, multi-statement injection, and a `secrets.users`\n\nreference — all rejected; legit `SELECT`\n\n/`WITH`\n\nover `life_files`\n\nallowed.\n\n**2. The demo can never break.** Three layers of graceful degradation:\n\nThat's the difference between \"works on my machine at 2am\" and \"works on stage.\"\n\nNo build is clean. The honest log:\n\n`documents ⋈ deadlines`\n\n, `transactions ⋈ gmail`\n\n) replaced TypeScript string-matching.`--`\n\nthat wasn't a comment.`-- \"What is costing me money this week?\"`\n\n. Passing that to `coral sql \"<query>\"`\n\nmade the CLI read the leading `--`\n\nas a `coral sql --format json -- \"$(cat query.sql)\"`\n\n.`HTTP 403`\n\n. The API key was valid — Dev.to sits behind Cloudflare, which blocks the default `Python-urllib`\n\nUser-Agent. A one-line `User-Agent`\n\nheader fixed it. Good reminder that 403 ≠ \"bad credentials.\"`node_modules`\n\n.`npm ci`\n\nwas the real fix — and a lesson to never trust a cached green check.The plumbing to read a live account already exists — the `gmail`\n\nHTTP source spec plus read-only OAuth. The remaining work is the genuinely hard part:\n\nBut the core bet is already proven: **your life is queryable, and the join is where the insight lives.**\n\nIf you build on Coral, the lesson I'd pass on is simple: **don't just read from your sources — join them.** 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.\n\nLife Risk Radar is built for the WeMakeDevs **Pirates of the Coral-bean** hackathon (Personal Agent track). Natural-language questions in, real cross-source SQL out, evidence and a drafted action every time. 🪸", "url": "https://wpnews.pro/news/i-built-ask-your-life-a-personal-coral-agent-that-answers-questions-about-your", "canonical_source": "https://dev.to/sahil9001/i-built-ask-your-life-a-personal-coral-agent-that-answers-questions-about-your-money--52a8", "published_at": "2026-05-27 10:23:24+00:00", "updated_at": "2026-05-27 10:40:13.459804+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-products", "natural-language-processing", "generative-ai"], "entities": ["Coral", "WeMakeDevs", "Life Risk Radar", "Claude", "SQL", "Gmail", "Calendar"], "alternates": {"html": "https://wpnews.pro/news/i-built-ask-your-life-a-personal-coral-agent-that-answers-questions-about-your", "markdown": "https://wpnews.pro/news/i-built-ask-your-life-a-personal-coral-agent-that-answers-questions-about-your.md", "text": "https://wpnews.pro/news/i-built-ask-your-life-a-personal-coral-agent-that-answers-questions-about-your.txt", "jsonld": "https://wpnews.pro/news/i-built-ask-your-life-a-personal-coral-agent-that-answers-questions-about-your.jsonld"}}