<?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: Samir Yuja</title>
    <description>The latest articles on DEV Community by Samir Yuja (@syuja).</description>
    <link>https://dev.to/syuja</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3962741%2Fabbed9c0-d180-4f25-92ce-aaccbcb845b7.png</url>
      <title>DEV Community: Samir Yuja</title>
      <link>https://dev.to/syuja</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/syuja"/>
    <language>en</language>
    <item>
      <title>A 7am Report on My Short-Term Rental</title>
      <dc:creator>Samir Yuja</dc:creator>
      <pubDate>Mon, 03 Aug 2026 01:01:00 +0000</pubDate>
      <link>https://dev.to/syuja/a-7am-report-on-my-short-term-rental-27n</link>
      <guid>https://dev.to/syuja/a-7am-report-on-my-short-term-rental-27n</guid>
      <description>&lt;p&gt;This is what 7am looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fse353ri4j4t0hx3s5n3f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fse353ri4j4t0hx3s5n3f.png" alt="Telegram digest showing battery status, occupancy for two rooms, and two LOW findings" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Who's in which room, when they check out, how much battery the front door lock has left, and that two knowledge base topics are duplicated. All of it already sits in the software I use to run the place.&lt;/p&gt;

&lt;p&gt;What isn't in there is that message. So I built the thing that writes it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Screenshots use synthetic data to protect guest privacy.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;I run a short-term rental: one property, three listings on Hospitable, the whole unit plus two rooms rented separately. My question at 7am is whether there's anything I need to do today, and what to do first. Four things stood between me and the answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checking cost four screens.&lt;/strong&gt; The app, then the inbox, then the calendar, then the devices page, just to establish that nothing was wrong. That was the answer most days. The cost was high enough that I checked less than I should have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The app doesn't always tell me.&lt;/strong&gt; Get logged out, miss an app update, and push notifications stop arriving with no signal that they've stopped. I don't find out something needed attention. I find out nothing did, which isn't the same thing as nothing happening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing was ranked.&lt;/strong&gt; A dashboard is a feed, and everything on it carries the same visual weight. A lock at 12% battery and a duplicated FAQ entry look equally urgent. I wanted the triage done before I read it, not while.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing cross-referenced.&lt;/strong&gt; Reservations, inquiries, reviews, and lock state each live on their own screen, three or four taps apart, with a load between each one. Anything that needed two at once, I was assembling in my head, half of it spent relocating a screen I'd been on an hour earlier and forgot which corner of the nav it lived in.&lt;/p&gt;

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

&lt;p&gt;Five pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hospitable API&lt;/strong&gt; (the REST API for my property management system, read-only token)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda&lt;/strong&gt; (runs the code on a schedule without a server)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge&lt;/strong&gt; (the cron trigger)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS SAM&lt;/strong&gt; (declares the AWS resources as a template)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram Bot API&lt;/strong&gt; (delivery)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.samiryuja.dev%2Fstatic%2Fimages%2Fhospitable-concierge%2Fhospitable_concierge_nightly_pipeline.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.samiryuja.dev%2Fstatic%2Fimages%2Fhospitable-concierge%2Fhospitable_concierge_nightly_pipeline.svg" alt="Pipeline diagram: EventBridge scheduler to fetch layer to five checks to formatter to Telegram" width="100" height="85.29411764705883"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's no LLM anywhere in it, every check is a deterministic rule, and the token has no write scope, so the worst bug I can ship is a wrong message.&lt;/p&gt;

&lt;p&gt;One run, start to finish:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fetch.&lt;/strong&gt; Five endpoints, paginated. The message endpoint appears to allow two requests per minute per reservation, so the client paces itself instead of tripping rate limit errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check.&lt;/strong&gt; Data in, config in, findings out.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Flags&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unanswered_inquiry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prospective guest asked something and I haven't replied&lt;/td&gt;
&lt;td&gt;HIGH, escalating to CRITICAL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;turnover_gap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Upcoming check-in stacked close behind a checkout&lt;/td&gt;
&lt;td&gt;CRITICAL down to LOW, by gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smartlock_battery&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Below threshold, unreadable, or device offline&lt;/td&gt;
&lt;td&gt;CRITICAL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;actionable_review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Review I can still leave, window still open&lt;/td&gt;
&lt;td&gt;MEDIUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;knowledge_hub_hygiene&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Duplicate topics or duplicate entries&lt;/td&gt;
&lt;td&gt;LOW&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A room counts as occupied on the day someone checks out, since the run fires at 7am (&lt;code&gt;cron(0 11 * * ? *)&lt;/code&gt;, UTC) and checkout is 11am. And findings deduplicate by device ID, since the front door lock appears on all three listings but is one physical lock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Render and send.&lt;/strong&gt; Findings sort by severity into the digest.&lt;/p&gt;

&lt;p&gt;Two decisions there mattered more than the code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unknown gets flagged.&lt;/strong&gt; If the API won't tell me the lock's battery level, that's a CRITICAL, because a false alarm costs me thirty seconds and a dead lock costs me a guest stuck outside at midnight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unanswered inquiries.&lt;/strong&gt; An inquiry is a question from someone who hasn't booked and probably won't: how far is it from the Capitol, is there parking nearby. Low odds either way, but response rate feeds ranking on the booking platforms, so the ones that cost me are the ones I forget entirely. It flags within a few hours of going unanswered, then escalates if it's still sitting there after a few days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Severity is triage, not risk scoring.&lt;/strong&gt; I don't need an accurate score, I need to know what to handle first. A duplicated FAQ entry is a real problem and it is permanently LOW, because it will never be the thing I deal with before coffee. Here's a normal Tuesday:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2f90r2tiifph46sb0xh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2f90r2tiifph46sb0xh.png" alt="Telegram digest with one LOW finding and no urgent items" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One LOW finding, nothing to do. Most mornings look like this, which is exactly why the CRITICAL is worth reading when it shows up:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2f90r2tiifph46sb0xh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2f90r2tiifph46sb0xh.png" alt="Telegram digest with one LOW finding and no urgent items" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving it onto Lambda
&lt;/h2&gt;

&lt;p&gt;It ran as a CLI script first. Three things had to change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The entry point.&lt;/strong&gt; Locally, sending is behind a flag so that testing doesn't fire a Telegram message every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run &lt;span class="nt"&gt;-m&lt;/span&gt; hospitable &lt;span class="nt"&gt;--notify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lambda has no command line, so &lt;code&gt;sys.argv&lt;/code&gt; never contains the flag. Wrapping this entry point as-is would have run cleanly, logged output to CloudWatch, and sent nothing to Telegram. That's the one thing the digest exists to do. Lambda got its own handler instead: calls the pipeline directly, always sends, and skips the dev-only flags entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The timeout.&lt;/strong&gt; Two timeouts were in play, and only one was Lambda's. The function gets 300 seconds, never close. My HTTP client gave each request 20, and the reservations fetch blew past it when I invoked the deployed function directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda invoke &lt;span class="nt"&gt;--function-name&lt;/span&gt; AuditFunction &lt;span class="nt"&gt;--payload&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; /dev/stdout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Locally the same call took about 17 seconds, close enough that it had never failed on my machine. I raised the request timeout to 60 and added &lt;code&gt;ReadTimeout&lt;/code&gt; and &lt;code&gt;ConnectTimeout&lt;/code&gt; to the retry path, which until then had only caught 429s and 5xx.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The permission.&lt;/strong&gt; The schedule is declared inside the function resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;AuditFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hospitable.lambda_handler.handler&lt;/span&gt;
    &lt;span class="na"&gt;Runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3.12&lt;/span&gt;
    &lt;span class="na"&gt;MemorySize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;
    &lt;span class="na"&gt;Timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
    &lt;span class="na"&gt;Events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NightlySchedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Schedule&lt;/span&gt;
        &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cron(0 11 * * ? *)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Events&lt;/code&gt; being nested under the function is how SAM knows what the schedule points at. &lt;code&gt;NightlySchedule&lt;/code&gt; is just a label for the event, which ends up in the generated rule's name.&lt;/p&gt;

&lt;p&gt;Deploying that template 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;sam build
sam deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sam build&lt;/code&gt; resolves the dependencies and stages everything SAM needs into &lt;code&gt;.aws-sam/&lt;/code&gt;. &lt;code&gt;sam deploy&lt;/code&gt; packages that build, uploads it, and applies the CloudFormation changeset: function, EventBridge rule, IAM role, and permission, all four resources in one deploy.&lt;/p&gt;

&lt;p&gt;Those few lines in the template expand into four AWS resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lambda function&lt;/strong&gt; (the code itself)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge rule&lt;/strong&gt; (fires daily per the cron schedule above)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM execution role&lt;/strong&gt; (what the function may do once running)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda permission&lt;/strong&gt; (what EventBridge may do &lt;em&gt;to&lt;/em&gt; the function)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS is deny-by-default, so without that last one the rule exists, points at the function, fires on time, and gets refused, with no error surfacing anywhere you'd think to look.&lt;/p&gt;

&lt;p&gt;It ran unattended after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs it shipped
&lt;/h2&gt;

&lt;p&gt;The digest said the Queen room had a guest checking out Monday. Nobody was in the Queen room.&lt;/p&gt;

&lt;p&gt;What it had found was a booking request that expired weeks earlier. Someone asked about the room, never confirmed, and the request timed out.&lt;/p&gt;

&lt;p&gt;My code was supposed to skip those. It filtered on &lt;code&gt;not_accepted&lt;/code&gt;; the API says &lt;code&gt;not accepted&lt;/code&gt;, with a space. The filter never matched, so an expired request got counted as a guest. Two parts of the code carried their own copy of that filter, so one bad record caused two problems that looked unrelated. One put a phantom guest in the room list. The other measured the gap between that phantom's checkout and the real reservation behind it, found zero days, and escalated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyinm0qdgxu76ct69lz7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyinm0qdgxu76ct69lz7w.png" alt="Digest showing a phantom guest in the Queen room and a false CRITICAL turnover finding" width="799" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That CRITICAL is fictional. There was no tight turnover, because there was no departing guest. Same data, fix applied:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4uoeo4qhppkrsqov0f64.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4uoeo4qhppkrsqov0f64.png" alt="Same digest after the fix: Queen shows empty with the real upcoming arrival preserved" width="799" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The phantom is gone and the real arrival behind it survives.&lt;/p&gt;

&lt;p&gt;Same mistake, different field. The digest collapses its output depending on whether a listing is the whole unit or a room inside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;is_whole_unit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parent_child&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;child&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I expected &lt;code&gt;parent_child&lt;/code&gt; to hold a string. It's a dict (parent/child relationship metadata, not a label), so the comparison was always true and every listing rendered as the whole property.&lt;/p&gt;

&lt;p&gt;Both bugs are the same shape: I wrote what I expected the API to return, not what it actually returned, and both had passing tests because the tests encoded the same wrong expectation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Verification isn't validation.&lt;/strong&gt; My tests checked that the code did what I meant. They never checked that what I meant matched what the API actually sends. A fixture written from the same wrong belief as the code it tests will pass because of the bug, not despite it. Fixtures now come from recorded responses, not memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review by a model that didn't write the code.&lt;/strong&gt; A second model reads every diff before commit, cold, with no context on what I intended. It can catch a wrong assumption precisely because it doesn't share it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Allowlist, not denylist.&lt;/strong&gt; The fix wasn't correcting the string, it was inverting the filter:&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hdata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;res_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A denylist has to name every bad status and stay right forever. An allowlist just drops whatever it doesn't recognize, and for alerting that's the direction you want to be wrong in: a missed alert is one bad morning, a false CRITICAL every morning teaches me to ignore the channel.&lt;/p&gt;

&lt;p&gt;The honest part is that a green test suite is a large part of why I didn't look sooner. The alerts were wrong, I'd half-noticed they were wrong, and the suite said the logic was fine. What moved me was the digest making a claim I could check against physical reality: this room, this guest, this Monday.&lt;/p&gt;

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

&lt;p&gt;What exists today is deterministic. A scheduled API pull, five rules, a ranked notification, and no model anywhere in it.&lt;/p&gt;

&lt;p&gt;Next is answering rather than flagging. Retrieval over the full history of guest conversations, so it can draft a reply for approval instead of just telling me one is owed. Then clustering the recurring complaints hiding in months of individually forgettable messages, and keeping inference costs sane once there's a model in the loop at all.&lt;/p&gt;

&lt;p&gt;The bigger limitation is structural. All of this looks inward. It watches my operation and tells me what to fix, and it structurally cannot tell me why there aren't more bookings in the first place. That's a different problem, and a different tool.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://github.com/syuja/frontdesk" rel="noopener noreferrer"&gt;github.com/syuja/frontdesk&lt;/a&gt;. Read-only auditor over a real short-term rental. Implementation written by Claude Code, reviewed independently before commit, designed and debugged by me. All screenshots use synthetic data.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>python</category>
      <category>serverless</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Gave Claude the Keys to My Airbnb for Two Weeks. Here's What It Found.</title>
      <dc:creator>Samir Yuja</dc:creator>
      <pubDate>Wed, 10 Jun 2026 03:28:18 +0000</pubDate>
      <link>https://dev.to/syuja/i-gave-claude-the-keys-to-my-airbnb-for-two-weeks-heres-what-it-found-3peo</link>
      <guid>https://dev.to/syuja/i-gave-claude-the-keys-to-my-airbnb-for-two-weeks-heres-what-it-found-3peo</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://samiryuja.dev/blog/hospitable-mcp-airbnb-audit" rel="noopener noreferrer"&gt;samiryuja.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I connected the official Hospitable MCP server to Claude, pointed it at my Airbnb, and asked it to take a look. Here's what I found.&lt;/p&gt;

&lt;p&gt;A language model is only as good as the context it's given. On its own it can talk about short-term rentals in the abstract; it knows nothing about &lt;em&gt;mine&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;MCP&lt;/strong&gt; closes that gap — it gives the model live, authenticated access to my actual reservations, messages, reviews, and calendar. So instead of guessing, it can look at the real thing. I run a two-room rental I've managed for over a year, and I figured I had it under control. Two weeks of letting an agent actually look turned up a surprising amount that was quietly broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hospitable &lt;strong&gt;Professional&lt;/strong&gt; plan (the tier that includes MCP access)&lt;/li&gt;
&lt;li&gt;Claude Pro&lt;/li&gt;
&lt;li&gt;Connect them: &lt;strong&gt;Settings → Connectors → add custom connector → OAuth&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;~50 tools load: reservations, messages, calendar, reviews, the Knowledge Hub, tasks, smart locks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions were monitored the entire time&lt;/strong&gt; — no messages were ever sent to guests. Everything guest-facing stayed read-only or draft-and-paste: the agent proposes, I decide.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What an AI audit of a real business actually surfaces
&lt;/h2&gt;

&lt;p&gt;Here are the highlights of what it found, in four buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Liability
&lt;/h3&gt;

&lt;p&gt;The registered guest booked from overseas, but the person staying was his wife, who had no verified profile of her own. The agent caught it by reading the message thread. That matters because AirCover and host protections are written around the &lt;em&gt;registered&lt;/em&gt; guest, and nothing in the structured reservation data reveals a substitution. It only surfaces if someone actually reads what the guest wrote.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Revenue opportunities
&lt;/h3&gt;

&lt;p&gt;This bucket changed how I look at my inquiry inbox. Every inquiry is revenue waiting on a response, and the agent treated it that way: it read the full inquiry history and identified exactly where a faster or better-matched reply would have won the booking. One guest asked about renting the whole place; by the time a reply went out hours later, those dates had gone to another property. Another inquiry came in written in Spanish, and the automated rule answered in English because no multilingual template existed. Across the history, it flagged six inquiries where response time or language likely cost the &lt;strong&gt;conversion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;None of this is visible on any dashboard. What the agent produced was effectively a conversion punch list: respond faster to whole-home inquiries, add a Spanish template, and answer the recurring questions (parking, check-in) before guests have to ask. Questions are demand — the goal is turning them into bookings.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Things quietly broken
&lt;/h3&gt;

&lt;p&gt;The theme in this bucket: the business changed, and the documentation didn't keep up.&lt;/p&gt;

&lt;p&gt;The big one was &lt;strong&gt;stale door codes.&lt;/strong&gt; When I first set up the Airbnb, I hardcoded the entry codes into the Knowledge Hub — the reference Hospitable's AI uses to answer guests. Later I synced my Schlage lock with Hospitable, which generates a fresh code for every reservation. The original setup-era codes stayed behind in seven different Knowledge Hub entries, wrong for every guest since. If anyone had asked the AI "what's my door code?", it would have confidently handed them a code that opens nothing.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;typo&lt;/strong&gt; had been sitting in an active message rule for months: a small formatting error in the review request that fires after every checkout, the final touchpoint of every stay. Easy to fix, impossible to spot without going line by line through every rule, which no one does.&lt;/p&gt;

&lt;p&gt;The agent also caught a &lt;strong&gt;time-sensitive review window.&lt;/strong&gt; A recent guest had smoked inside and brought unregistered visitors, and my window to review them was about to close. Once that window shuts, future hosts get no warning, so that was the catch that mattered. It also surfaced a backlog of guest reviews I'd never responded to — lower stakes, but review responses are public and responsiveness factors into Airbnb's ranking, so the backlog went on the list rather than in the trash.&lt;/p&gt;

&lt;p&gt;Parking was in this bucket too. It's the single most common question guests ask, and the Knowledge Hub had no car parking entry at all. The Parking section existed, but it only covered bicycles.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Features I wasn't using but should be
&lt;/h3&gt;

&lt;p&gt;Some of what it surfaced wasn't broken, just sitting there unused. The Knowledge Hub was thin and partly wrong; now it's corrected and actually filled in. Tasks was completely empty, so I registered myself as a teammate and set up text alerts for upcoming cleanings. Right now that's redundant since it's just me. But the structure exists now, and the day I bring on a housekeeper, I swap in their number and nothing else changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually use it
&lt;/h2&gt;

&lt;p&gt;The dramatic catches are one thing; the reason it's still connected is the boring daily stuff.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;complaint clustering&lt;/strong&gt;, because it goes straight at my biggest pain point: a new guest arrives and starts complaining, and in the moment it's hard to tell a one-off from a pattern. I had the agent group every review across Airbnb and Booking.com by theme. The runaway top three:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bedroom doors didn't lock from the inside&lt;/strong&gt; — four guests, both rooms. The clear #1. Guests wanted keyed locks, not the privacy locks the rooms came with. The main entrance already had a Schlage deadbolt; the fix was recently replacing the bedroom privacy locks with keyed door knobs — and the clustering confirmed the complaints dropped off after the change.&lt;/li&gt;
&lt;li&gt;Location and transit — guests underestimating how far things were.&lt;/li&gt;
&lt;li&gt;Surprise at the shared-home setup — a listing-clarity issue, not a property one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The clustering also corrected an assumption I'd been carrying: I was sure Booking.com guests complained more than Airbnb guests. Totally wrong. The data showed the gap was a scoring-norm artifact, not a real satisfaction difference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9m0us7rkdc8ad449fto2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9m0us7rkdc8ad449fto2.png" alt="Review dashboard across Airbnb and Booking.com" width="800" height="697"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second is the &lt;strong&gt;weekly turnover outlook.&lt;/strong&gt; One question and I get a clean in-chat calendar of which days have a checkout and a check-in stacked up — the days I can't schedule anything else. Combined with the task alerts, I stopped double-booking myself — committing to a day in the office only to find out it was also a turnover day.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  False positives and rough edges
&lt;/h2&gt;

&lt;p&gt;If I only told you the good parts you shouldn't believe me. The honest column:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It flagged a problem that wasn't one.&lt;/strong&gt; Reviewing my message templates, the agent called out messages where the &lt;code&gt;%smartlock_code%&lt;/code&gt; shortcode hadn't resolved. The real explanation: those guests had already checked out, and their codes had expired with the reservation. The lesson is that the agent doesn't automatically know the lifecycle of the data it's reading — what looks broken on a past reservation can be perfectly normal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writes can fail silently.&lt;/strong&gt; Adding an item to a Knowledge Hub that had never been initialized succeeded with no error at all — but nothing actually appeared until I generated the hub in the dashboard. A success response that doesn't result in the thing existing is the kind of surprise that bites you later.&lt;/p&gt;

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

&lt;p&gt;The obvious next step is automating the loop I ran by hand these two weeks — issue comes in, gets detected, a fix gets drafted for me to approve. I'm still working out exactly what that should look like, so I'll leave it there for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that stuck with me
&lt;/h2&gt;

&lt;p&gt;What surprised me wasn't any one finding — it was the &lt;strong&gt;speed of the audit.&lt;/strong&gt; Catching a months-old typo without reading every message rule by hand. Clustering a year of reviews into three themes in seconds. Fewer complaints, less of my time spent firefighting, more of it back. The most useful thing an agent did here wasn't anything clever — it was simply &lt;em&gt;looking&lt;/em&gt;, systematically, at data I already had, and sorting it into the four things that actually matter: liabilities, &lt;strong&gt;revenue opportunities&lt;/strong&gt;, the things that had quietly snuck past me, and the features I should have been using all along.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with the official Hospitable MCP server connected to Claude. Real two-room property, real findings. Guest details kept deliberately vague.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>airbnb</category>
      <category>hospitable</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Futbol Report — building a multi-model LLM comparison on AWS Lambda</title>
      <dc:creator>Samir Yuja</dc:creator>
      <pubDate>Mon, 01 Jun 2026 13:17:21 +0000</pubDate>
      <link>https://dev.to/syuja/futbol-report-building-a-multi-model-llm-comparison-on-aws-lambda-1n1l</link>
      <guid>https://dev.to/syuja/futbol-report-building-a-multi-model-llm-comparison-on-aws-lambda-1n1l</guid>
      <description>&lt;p&gt;A few months ago I set up a soccer-digest bot that sends me a Telegram message every few days with fixtures, results, transfer news, and manager changes. It started as a &lt;code&gt;tmux&lt;/code&gt; session running Claude Code on a small Linux server, fired by a cron job. It worked. It also went down occasionally, and I had no good way to inspect what it was producing.&lt;/p&gt;

&lt;p&gt;I wanted to do two things at once: make it more reliable, and turn it into something more interesting than "one bot sending one report." The result is &lt;a href="https://samiryuja.dev/projects/futbol-report" rel="noopener noreferrer"&gt;Futbol Report&lt;/a&gt; — a scheduled job on AWS that sends the &lt;em&gt;same prompt and search context&lt;/em&gt; to four different language models (Claude, Kimi, Qwen, Gemma) every three days, stores all four reports in Redis, and renders them side-by-side on this site with live voting.&lt;/p&gt;

&lt;p&gt;This post is about how it works, what I learned from running it, and the deployment war stories — the bits that are usually edited out of "here's my side project" writeups.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the comparison shows
&lt;/h2&gt;

&lt;p&gt;Same input, four models, no editing. The page has a dropdown to switch between past runs and a vote button under each report. Vote tallies persist.&lt;/p&gt;

&lt;p&gt;The point isn't to crown a "best" model. It's to make differences visible on a &lt;em&gt;real, repeated task&lt;/em&gt;. Placed next to each other, the models diverge in ways that are easy to see — how faithfully they follow the requested format, what they choose to include or filter out, and how long their reports run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the pipeline works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EventBridge Scheduler  (every 3 days)
        ↓
   AWS Lambda  (Python 3.12)
        ├── Brave Search    (~13 queries: fixtures, results, transfers, manager changes)
        └── OpenRouter      (same context → 4 models)
        ↓
   Redis  (Vercel)
        ↓
   Next.js page  (server-rendered comparison + voting)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every three days EventBridge fires a Lambda function. The Lambda calls Brave Search with about a dozen queries, then sends the same compiled context to four models through OpenRouter. Each model's report goes into Redis under a timestamped key. The Next.js site, deployed on Vercel, reads from the same Redis and renders the comparison page.&lt;/p&gt;

&lt;p&gt;A few decisions worth surfacing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenRouter as the inference layer.&lt;/strong&gt; One API instead of four, and adding or swapping a model is a one-line change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-rendered comparison page.&lt;/strong&gt; The data only changes every few days, so there's no point fetching it from the browser. The server reads Redis and sends back the finished page. Only the vote button runs in the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis with a 90-day TTL on report keys.&lt;/strong&gt; Redis fit the access pattern — small payloads (a few KB per report) and pure key lookups by timestamp, no queries. The TTL means old reports expire automatically; votes and the run index have no TTL, so voting history is never evicted even if memory fills.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned from running it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Search results aren't deterministic.&lt;/strong&gt; Running the same query thirty minutes apart returns different result sets — that's just how live ranking works. So context has to be held fixed &lt;em&gt;within&lt;/em&gt; a run for the comparison to be fair (one Brave call feeds all four models).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A simple anti-hallucination clause worked across all four models.&lt;/strong&gt; After the first run hallucinated fixtures, I added &lt;em&gt;"use ONLY facts present in the provided search results"&lt;/em&gt; to the prompt. None of the four models invented data after that — same clause, same effect, across four different labs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Models filter context differently.&lt;/strong&gt; One run, Brave's results included an out-of-scope Indian Super League match. Three models filtered it out; one led its report with it. Same prompt, same data, different prioritization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Output length varies wildly with model size.&lt;/strong&gt; Claude and Kimi used most of the available context. Gemma — by far the cheapest model — collapsed the same input into one-line summaries. Cost and level of detail are correlated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Format adherence varies too.&lt;/strong&gt; Claude followed the prompt's structure most faithfully. Gemma dropped most of it. Qwen and Kimi were in between.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. The pipeline survived the off-season without code changes.&lt;/strong&gt; When Serie A ended, fixture queries returned nothing useful. I added two new search categories (transfers, manager changes) and one line to the prompt — &lt;em&gt;"if fixtures are sparse, lead with transfer news."&lt;/em&gt; Reports stayed substantive: Allegri sacking at Milan, Guardiola leaving City, World Cup buildup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deployment war stories
&lt;/h2&gt;

&lt;p&gt;The interesting part of moving the generator to Lambda was the several hours of debugging in the middle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python runtime mismatch.&lt;/strong&gt; AWS defaulted the new Lambda to Python 3.14. My deployment package was built for 3.12. The error didn't say "wrong Python version" — it said &lt;code&gt;No module named 'pydantic_core._pydantic_core'&lt;/code&gt;, because the compiled C extension is a &lt;code&gt;cpython-312&lt;/code&gt; &lt;code&gt;.so&lt;/code&gt; that won't load under 3.14. Fix: match the runtime to the build target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mac vs Linux binaries.&lt;/strong&gt; Even after pinning the Python version, &lt;code&gt;pydantic-core&lt;/code&gt; kept loading the macOS binary into my zip. I was using &lt;code&gt;uv&lt;/code&gt; for packaging — &lt;code&gt;uv pip install --only-binary&lt;/code&gt; is supposed to fetch a Linux wheel but reliably didn't here. Switching to vanilla &lt;code&gt;python3 -m pip install --platform manylinux2014_x86_64 --only-binary=:all:&lt;/code&gt; finally pulled the right artifact. The newer tool I trusted was the problem; the older boring tool worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional imports for dual environments.&lt;/strong&gt; &lt;code&gt;python-dotenv&lt;/code&gt; is great locally — reads &lt;code&gt;.env&lt;/code&gt;, populates &lt;code&gt;os.environ&lt;/code&gt;. On Lambda, environment variables come from AWS directly, and &lt;code&gt;python-dotenv&lt;/code&gt; is just dead weight that doesn't ship in the runtime. Wrap the import:&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
    &lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# dotenv not needed on Lambda — env vars are set directly
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same code works in both environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Default timeout.&lt;/strong&gt; Lambda's default timeout is 3 seconds. My pipeline needs about 4 minutes — 13 sequential Brave calls plus four sequential model generations. Bump it to 900 seconds (Lambda's max).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reaching for the CLI over the browser.&lt;/strong&gt; I couldn't get the AWS console's "Upload .zip file" button to reliably refresh the deployed code — the SHA-256 hash kept matching the previous upload even after I selected a new file. It probably works fine most of the time; in my case &lt;code&gt;aws lambda update-function-code&lt;/code&gt; from the CLI was faster and easier, and it's the path I use now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets are easy to leak when copy-pasting console output.&lt;/strong&gt; I rotated three API keys during this project — twice because I pasted command output that included environment variables. Painful in proportion to how preventable it is. Wiring &lt;code&gt;gitleaks&lt;/code&gt; into a GitHub Actions workflow is next on the list.&lt;/p&gt;

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

&lt;p&gt;In priority order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Telegram delivery from the Lambda&lt;/strong&gt; — restore the original use case so the digest pings me when a new run finishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI security scanning&lt;/strong&gt; — &lt;code&gt;gitleaks&lt;/code&gt; for secrets and &lt;code&gt;osv-scanner&lt;/code&gt; for dependency CVEs, both wired into a GitHub Actions workflow on each repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallelize the model calls&lt;/strong&gt; — sequential right now (~4 minutes); concurrent would cut runtime by ~75%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full page content for grounding&lt;/strong&gt; — Brave returns 1-2 sentence snippets, so thin context yields thin reports. Firecrawl or a readability extractor would fix this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side vote dedup&lt;/strong&gt; — votes are deduped per-browser via &lt;code&gt;localStorage&lt;/code&gt;; clearing storage or going incognito gets around it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://samiryuja.dev/projects/futbol-report" rel="noopener noreferrer"&gt;The live comparison&lt;/a&gt; — latest run, history, voting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/syuja/futbol-report" rel="noopener noreferrer"&gt;Generator repo&lt;/a&gt; — Python pipeline, Lambda packaging, EventBridge schedule&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/syuja/samiryuja.dev" rel="noopener noreferrer"&gt;Site repo&lt;/a&gt; — the Next.js side, comparison page, vote API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Thanks
&lt;/h2&gt;

&lt;p&gt;To Ryan — for letting me run the original bot on his machine over Tailscale, and for the steady stream of articles and ideas that shaped a lot of the thinking behind this project.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>serverless</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
