<?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: Olivier Leplus</title>
    <description>The latest articles on DEV Community by Olivier Leplus (@olivierleplus).</description>
    <link>https://dev.to/olivierleplus</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%2F154793%2F08cbc391-e0c9-4412-b8dd-61b21fb6eea2.jpeg</url>
      <title>DEV Community: Olivier Leplus</title>
      <link>https://dev.to/olivierleplus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olivierleplus"/>
    <language>en</language>
    <item>
      <title>Cloudagotchi Part 4 : My Tamagotchi reads me the AWS news</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Mon, 03 Aug 2026 14:26:35 +0000</pubDate>
      <link>https://dev.to/aws/cloudagotchi-part-4-my-tamagotchi-reads-me-the-aws-news-9do</link>
      <guid>https://dev.to/aws/cloudagotchi-part-4-my-tamagotchi-reads-me-the-aws-news-9do</guid>
      <description>&lt;p&gt;Here's my problem with virtual pets, and honestly with most IoT demos: after the novelty fades, they don't &lt;em&gt;do&lt;/em&gt; anything for you. My pet from &lt;a href="https://dev.to/aws/cloudagotchi-part-3-it-gets-hungry-while-you-sleep-a-serverless-pet-brain-1l1j"&gt;part 3&lt;/a&gt; has feelings and memory, and I love it, but it contributes nothing to the household.&lt;/p&gt;

&lt;p&gt;Time to fix that. In this final part of the series, the pet gets a job: &lt;strong&gt;every morning at 7:03, it fetches the AWS "What's New" feed (thanks rss to still be around ❤️), has Amazon Bedrock rewrite the announcements as a briefing &lt;em&gt;in its own excitable little pet voice&lt;/em&gt;, has Amazon Polly speak it, and delivers it to the device, where a newspaper badge appears, and tapping it makes the pet read the news to me out loud through the onboard speaker.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's one of my favorite thing I've built so far this year. My Cloudagotchi is now better informed than I am before my morning hot chocolate (spoiler alert, I don't like cofee... sorry).&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through the full pipeline (RSS → Bedrock → Polly → S3 → MQTT → speaker) including the one decision that makes the device side almost embarrassingly simple: asking Polly for the &lt;em&gt;right audio format&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check (sorry 😅):&lt;/strong&gt; the pet's voice is Polly's neural TTS pitched up with SSML. Charming, but it won't fool anyone into thinking a soul lives in the device. Bedrock also occasionally gets &lt;em&gt;too&lt;/em&gt; excited about a niche database feature ("HUMAN. WAKE UP. Aurora has a new minor version"). And fair warning: the &lt;em&gt;cloud&lt;/em&gt; half of this article worked on the first deploy; the &lt;em&gt;device&lt;/em&gt; half taught me four embedded-streaming lessons the hard way. They're all documented below, symptoms included, so your afternoon goes better than mine. Cost, though, is genuinely negligible: one Haiku call and ~1,000 Polly characters a day is under a dollar a month.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Final code: &lt;code&gt;git checkout article-4&lt;/code&gt;, or just &lt;code&gt;main&lt;/code&gt;, since this completes the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&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%2Fo39aqo256shypi97x1oe.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%2Fo39aqo256shypi97x1oe.png" alt="Architecture diagram of the briefing pipeline: EventBridge triggers Lambda, which calls Bedrock and Polly, stores audio in S3, and publishes to MQTT for the device" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One Lambda, one bucket, one schedule. All the intelligence is cloud-side, which means when I want to change the pet's personality, tweak the briefing length, or switch news sources, it's just a &lt;code&gt;cdk deploy&lt;/code&gt;, and the firmware never knows anything happened. That's the payoff of the architecture we've been building since part 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 : The news, without an API key
&lt;/h2&gt;

&lt;p&gt;AWS publishes a plain old RSS feed of every announcement. No auth, no scraping, no SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FEED_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://aws.amazon.com/about-aws/whats-new/recent/feed/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchNews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&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;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FEED_URL&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;xml&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// Two regexes pull out &amp;lt;item&amp;gt; titles + links. For a feed this&lt;/span&gt;
  &lt;span class="c1"&gt;// regular, a full XML parser dependency would be pure ceremony.&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We only keep the titles. That sounds lossy, but it's deliberate: AWS announcement titles are already one-sentence summaries ("Amazon S3 now supports..."), and they're all Bedrock needs to pick the interesting ones. Feeding it full item descriptions doubles your tokens for basically no gain when the output is a 45-second briefing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 : Bedrock, in character
&lt;/h2&gt;

&lt;p&gt;This is where the feature goes from "RSS-to-speech" to &lt;em&gt;pet&lt;/em&gt;. The system prompt does the heavy lifting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PERSONA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are Cloudagotchi, a small, excitable AWS virtual pet who reads the morning AWS news to your human. You are affectionate, easily impressed, and sometimes admit you don't fully understand the more complicated services.
Summarize the following AWS announcements as a spoken morning briefing:
- 45 to 60 seconds when read aloud (about 120-150 words)
- Pick only the 3 most interesting items, one short sentence of WHY each matters
- End with one affectionate sign-off sentence
- Plain text only: no emoji, no markdown, no bullets (it will be read aloud)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the constraints that exist &lt;em&gt;because this is audio&lt;/em&gt;: a word-count target expressed in listening time, "no markdown, no bullets" (Polly would read them), and "one sentence of why it matters" so the briefing informs rather than recites. Then one &lt;code&gt;Converse&lt;/code&gt; call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="p"&gt;}&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;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConverseCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us.anthropic.claude-haiku-4-5-20251001-v1:0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// fast + cheap: perfect here&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="s1"&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="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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PERSONA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\nToday's announcements:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;titles&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;inferenceConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real output from my device this week:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Good morning good morning! I read the news while you slept and I have THREE things! Amazon Bedrock has new smaller models, which means pets like me might get even smarter, imagine! Lambda functions can now... okay I did not fully understand this one but it makes your functions start faster, and fast is good. And S3 got cheaper for cold data, which I think means winter storage? Anyway. I picked these just for you. Have the best day, okay?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have received worse briefings from humans...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why Haiku and not a bigger model?&lt;/strong&gt; The task is summarize-and-roleplay over ten headlines, well within a small model's comfort zone, it runs 365 times a year, and latency doesn't matter at 7 a.m. Matching model size to task is the closest thing GenAI has to a free lunch. Start small; upgrade only when the output disappoints you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3 : Polly, and the decision that saves the firmware
&lt;/h2&gt;

&lt;p&gt;Here's the part that trips people up when they put AI-generated audio on a microcontroller. Polly's default output is MP3, and an MP3 decoder on an ESP32 means integrating a decoding library, managing its buffers, and debugging its edge cases. On a hobby timeline, that's a weekend.&lt;/p&gt;

&lt;p&gt;Or you change &lt;strong&gt;one parameter&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AudioStream&lt;/span&gt; &lt;span class="p"&gt;}&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;polly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SynthesizeSpeechCommand&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="s2"&gt;`&amp;lt;speak&amp;gt;&amp;lt;prosody pitch="+20%" rate="105%"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/prosody&amp;gt;&amp;lt;/speak&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;TextType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ssml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;VoiceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Justin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;OutputFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pcm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// ← the whole trick. Raw samples, no codec.&lt;/span&gt;
  &lt;span class="na"&gt;SampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;16000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pcm&lt;/code&gt; gives us raw 16-bit mono samples, exactly what the board's ES8311 audio codec eats natively. The device-side "decoder" becomes: &lt;em&gt;skip 44 bytes, write the rest to the speaker.&lt;/em&gt; We wrap the PCM in a WAV header in the Lambda (44 bytes of 1991 technology, hand-written in &lt;a href="https://github.com/tagazok/cloudagotchi/blob/main/backend/lambda/news/briefing.mjs" rel="noopener noreferrer"&gt;&lt;code&gt;briefing.mjs&lt;/code&gt;&lt;/a&gt;) so the file is also playable in a browser for debugging.&lt;/p&gt;

&lt;p&gt;The SSML &lt;code&gt;prosody&lt;/code&gt; tag is the pet's larynx: &lt;code&gt;pitch="+20%"&lt;/code&gt; turns Polly's "Justin" into something convincingly small and cute. Free squeakiness.&lt;/p&gt;

&lt;p&gt;The result is ~1.8 MB per minute, too big for an MQTT message (128 KB limit), which is why S3 enters the picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wav&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;url&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;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetObjectCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MQTT carries the pointer; HTTPS carries the payload.&lt;/strong&gt; The presigned URL means the device downloads from a private bucket with zero AWS credentials beyond its IoT certificate, the URL &lt;em&gt;is&lt;/em&gt; the authorization, and it expires at lunchtime, because news does too. A lifecycle rule deletes the files after a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 : The paper delivery
&lt;/h2&gt;

&lt;p&gt;The Lambda's last act is ringing every pet's doorbell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &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;thing&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;  &lt;span class="c1"&gt;// iot.ListThings&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="nx"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thingName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudagotchi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;iotData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PublishCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`cloudagotchi/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thingName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/briefing`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headline&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And EventBridge Scheduler (same service that makes the pet hungry) makes it a morning ritual. Note the timezone-aware cron, a Scheduler feature that classic EventBridge rules don't have:&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;new&lt;/span&gt; &lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MorningSchedule&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;scheduleExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cron(3 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;scheduleExpressionTimezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Europe/Paris&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// the pet lives where I live&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5 — The device: badge, tap, play
&lt;/h2&gt;

&lt;p&gt;On the firmware side, the briefing handler stores the URL and shows an orange banner with the headline, built from the same LVGL vocabulary as part 2 (and it wakes the pet if it was dozing; the paperboy rings). The interesting bit is what happens on tap:&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%2F7gmtnc2z838athlikmcx.jpeg" 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%2F7gmtnc2z838athlikmcx.jpeg" alt="The device showing the orange news badge below the pet's face" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;on_briefing_tapped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Audio streaming blocks for ~1 minute: never do that in an&lt;/span&gt;
    &lt;span class="c1"&gt;// LVGL callback, or the whole UI freezes mid-squish.&lt;/span&gt;
    &lt;span class="c1"&gt;// Low priority, pinned to core 1: the download must never&lt;/span&gt;
    &lt;span class="c1"&gt;// starve the Wi-Fi/TCP stack on core 0.&lt;/span&gt;
    &lt;span class="n"&gt;xTaskCreatePinnedToCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;play_briefing_task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"briefing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and the entire audio "stack", which streams the WAV chunk-by-chunk so it never needs to fit in RAM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;esp_http_client_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// 16 KB at a time&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// skip the 44-byte WAV header once, then:&lt;/span&gt;
    &lt;span class="n"&gt;esp_codec_dev_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_speaker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// → speaker&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the player. HTTP in, codec out, no decoder in between, the dividend of choosing &lt;code&gt;pcm&lt;/code&gt; back in step 3. One design decision in a Lambda function deleted an entire firmware subsystem.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The morning after
&lt;/h2&gt;

&lt;p&gt;Deploy, flash, go to bed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk deploy CloudagotchiNewsStack
&lt;span class="nb"&gt;cd&lt;/span&gt; ../firmware &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; idf.py flash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 7:03, the Lambda wakes, reads the feed, writes the script, records the voice, parks the file, rings the doorbell. On the desk, an orange banner slides across the pet's feet: &lt;em&gt;"News! Amazon Bedrock announces..., tap to listen."&lt;/em&gt; pet does its proud little squish.&lt;/p&gt;

&lt;p&gt;Tap. A tiny, pitched-up voice fills the room with genuine enthusiasm about storage-class pricing.&lt;/p&gt;

&lt;p&gt;Four articles ago this was a dev board in shrink wrap. Now it's a creature with a body (LVGL), reflexes (touch + IMU), a memory (DynamoDB), a metabolism (EventBridge), and as of this morning, a &lt;em&gt;job&lt;/em&gt; (Bedrock + Polly). Every layer still visible in the code, every layer replaceable without touching the others.&lt;/p&gt;




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

&lt;p&gt;Beyond the whimsy, this article's pipeline (&lt;strong&gt;schedule → fetch → LLM transform → TTS → object storage → presigned pointer → thin device&lt;/strong&gt;) is a genuinely reusable shape. Swap the RSS feed for your CI status, your support queue, or your kid's school newsletter, and the persona prompt for whatever voice should deliver it, and you've got an ambient audio briefing device for anything. The device firmware never changes. That's the quiet lesson of the whole series: &lt;strong&gt;put the personality where you can redeploy it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if you build one, please, show it to me!&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;The Cloudagotchi repo&lt;/a&gt; : the finished project is &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html" rel="noopener noreferrer"&gt;Amazon Bedrock Converse API&lt;/a&gt; : one API, many models&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/polly/latest/dg/ssml.html" rel="noopener noreferrer"&gt;Amazon Polly SSML reference&lt;/a&gt; : &lt;code&gt;prosody&lt;/code&gt; is just the beginning&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/about-aws/whats-new/recent/feed/" rel="noopener noreferrer"&gt;AWS "What's New" RSS&lt;/a&gt; : the pet's news source&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/protocols/esp_http_client.html" rel="noopener noreferrer"&gt;ESP-IDF HTTP client&lt;/a&gt; : the streaming download&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>iot</category>
      <category>ai</category>
    </item>
    <item>
      <title>Cloudagotchi Part 3 : It gets hungry while you sleep: A serverless pet brain</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Mon, 27 Jul 2026 13:14:36 +0000</pubDate>
      <link>https://dev.to/aws/cloudagotchi-part-3-it-gets-hungry-while-you-sleep-a-serverless-pet-brain-1l1j</link>
      <guid>https://dev.to/aws/cloudagotchi-part-3-it-gets-hungry-while-you-sleep-a-serverless-pet-brain-1l1j</guid>
      <description>&lt;p&gt;At the end of &lt;a href="https://dev.to/aws/cloudagotchi-part-2-giving-it-a-face-sprites-and-touch-on-a-18-amoled-22nd"&gt;part 2&lt;/a&gt;, our pet had a face, reflexes, and a serious philosophical problem: no memory. You could pet it a hundred times, reboot the board, and it woke up with the same hardcoded stats, feeling nothing, remembering nothing. Every day was its first day.&lt;/p&gt;

&lt;p&gt;Today the pet gets what every Tamagotchi fundamentally is: &lt;strong&gt;a state machine with feelings.&lt;/strong&gt; Its hunger, energy and mood become rows in DynamoDB. A Lambda decides how a snack or a shake changes them. And, the part I find genuinely delightful, &lt;strong&gt;EventBridge Scheduler makes time pass&lt;/strong&gt;, so the pet gets hungry overnight &lt;em&gt;while the device is completely powered off&lt;/em&gt;. When you flash the firmware Monday morning, the pet wakes up, asks the cloud "what did I miss?", and gets visibly sad about the answer.&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through the whole brain: the data model (it's one table with one row per pet... gloriously boring), the decay math that makes offline time count, IoT rules that invoke Lambda without a single line of polling code, and the loop back down to the device.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check (sorry 😅):&lt;/strong&gt; there's a &lt;code&gt;Scan&lt;/code&gt; in this article. A full-table &lt;code&gt;Scan&lt;/code&gt;, in production-adjacent code, published on the internet under my name. It's the right call for a fleet of three pets and I'll explain the honest scaling path when we get there, but if your DynamoDB instincts just twitched, I respect you, and I ask you to hold that thought until the 💡 box.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Code for this article: &lt;code&gt;git checkout article-3&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture, now with a brain
&lt;/h2&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%2Fr2kz1fxadqv0qi8zzs5v.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%2Fr2kz1fxadqv0qi8zzs5v.png" alt="Architecture diagram showing IoT rules flowing to Lambda, Lambda reading/writing DynamoDB, and EventBridge Scheduler triggering the decay Lambda" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two Lambdas, one table, one schedule, two IoT rules. The entire brain deploys with &lt;code&gt;cdk deploy&lt;/code&gt; and costs approximately nothing (the decay Lambda runs 48 times a day; the free tier laughs at this).&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model: one row of feelings
&lt;/h2&gt;

&lt;p&gt;Every pet is one item in DynamoDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deviceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudagotchi-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hunger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"energy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mood"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1752192000000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No GSIs, no sort keys, no single-table-design galaxy brain. The partition key is the thing name from part 1, and &lt;code&gt;updatedAt&lt;/code&gt; is the timestamp that makes everything else in this article work.&lt;/p&gt;

&lt;p&gt;The CDK for it is appropriately tiny:&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;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PetState&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;tableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudagotchi-pets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deviceId&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;billingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BillingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// it's a pet, not a bank&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The rules of life: pure functions first
&lt;/h2&gt;

&lt;p&gt;Before any AWS SDK calls, the pet's biology lives in &lt;a href="https://github.com/tagazok/cloudagotchi/blob/main/backend/lambda/brain/pet-logic.mjs" rel="noopener noreferrer"&gt;&lt;code&gt;pet-logic.mjs&lt;/code&gt;&lt;/a&gt;, pure functions, no I/O, trivially testable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stats decay per hour of real time. Tuned so a full day of neglect&lt;/span&gt;
&lt;span class="c1"&gt;// makes the pet visibly sad — without starving it to zero over a weekend.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DECAY_PER_HOUR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// What each interaction does.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;INTERACTION_EFFECTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;play&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// fun is exhausting&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the single most important function in the whole project. Not a scheduled job, a &lt;em&gt;calculation&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** The cloud remembers, so the pet ages even while powered off. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyDecay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&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;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hunger&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;DECAY_PER_HOUR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hunger&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;DECAY_PER_HOUR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;   &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;DECAY_PER_HOUR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the part that trips people up about "time passing" in serverless systems: &lt;strong&gt;you don't need a process running to make time pass.&lt;/strong&gt; Time passes for free, you only need to &lt;em&gt;account&lt;/em&gt; for it whenever you next look. &lt;code&gt;applyDecay&lt;/code&gt; computes elapsed hours since &lt;code&gt;updatedAt&lt;/code&gt; and applies them, whether that's 30 minutes (scheduler tick) or a weekend (you went camping, the pet noticed).&lt;/p&gt;

&lt;p&gt;Finally, three stats collapse into one face for the device to render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;moodOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hourOfDay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hourOfDay&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;hourOfDay&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sleeping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mood&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;worst&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;happy&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;worst&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;neutral&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sad&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Math.min&lt;/code&gt; : the pet's outlook is determined by its &lt;em&gt;worst&lt;/em&gt; stat, not its average. As a design decision this is straight from the Tamagotchi school: you can't cuddle your way out of starvation. 😝&lt;/p&gt;

&lt;h2&gt;
  
  
  From MQTT to Lambda: IoT rules
&lt;/h2&gt;

&lt;p&gt;How does a &lt;code&gt;{"type":"feed"}&lt;/code&gt; on an MQTT topic become a Lambda invocation? &lt;strong&gt;IoT rules&lt;/strong&gt;, SQL statements (yes, SQL) that run against the message stream:&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;new&lt;/span&gt; &lt;span class="nx"&gt;iot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnTopicRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;InteractionRule&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;ruleName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cloudagotchi_interactions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;topicRulePayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`SELECT *, topic(2) AS deviceId, topic(3) AS kind
          FROM 'cloudagotchi/+/interaction'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;functionArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;onInteraction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionArn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two tricks earn their keep here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;topic(2)&lt;/code&gt; extracts the second topic segment (the device's thing name) and injects it into the event as &lt;code&gt;deviceId&lt;/code&gt;. &lt;strong&gt;The device never has to say who it is in the payload&lt;/strong&gt;, which means a buggy (or malicious) device can't claim to be a different pet. Identity comes from the topic, and part 1's policy already guarantees a device can only publish on its own topics. The security work we did in article 1 quietly pays rent here.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;+&lt;/code&gt; wildcard means one rule serves every pet, current and future. Provision a hundred devices; deploy nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a twin rule on the &lt;code&gt;hello&lt;/code&gt; topic, pointing at the same Lambda.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interaction Lambda: load, decay, apply, save, reply
&lt;/h2&gt;

&lt;p&gt;The handler reads like the pet's inner monologue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// injected by the IoT rule SQL&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. Load the pet, or meet it for the first time.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="p"&gt;}&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;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;deviceId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;newbornPet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Time passed since we last looked. It always does.&lt;/span&gt;
  &lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyDecay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. A "hello" just wants the current state; an interaction changes it.&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;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interaction&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="nx"&gt;pet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyInteraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Persist, then tell the device what it now feels.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;publishState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2 is doing quiet heavy lifting: because &lt;em&gt;every&lt;/em&gt; code path decays first, the pet's state is always correct-as-of-now, no matter which event arrived or how long it's been. The &lt;code&gt;hello&lt;/code&gt; flow (device boots, publishes &lt;code&gt;hello&lt;/code&gt;, Lambda answers with fresh state) is the same code path as feeding, minus one &lt;code&gt;if&lt;/code&gt;. "What did I miss?" is just "load + decay + reply."&lt;/p&gt;

&lt;p&gt;The reply goes back down over MQTT via the IoT data plane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;iot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PublishCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`cloudagotchi/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/state`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;qos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;hunger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;energy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;face&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;moodOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  EventBridge Scheduler: the heartbeat of the universe
&lt;/h2&gt;

&lt;p&gt;One question remains: if nobody interacts and the device never says hello, who makes the pet sad? This does:&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;new&lt;/span&gt; &lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DecaySchedule&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;scheduleExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate(30 minutes)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;flexibleTimeWindow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OFF&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schedulerRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleArn&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 30 minutes, the decay Lambda sweeps all pets, ages them, and pushes fresh state to any device that's listening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ScanCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pet&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;Items&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;aged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyDecay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aged&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;publishState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;aged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// offline device? IoT drops it; the&lt;/span&gt;
                               &lt;span class="c1"&gt;// "hello" on next boot catches it up.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;About that &lt;code&gt;Scan&lt;/code&gt; (deep breath).&lt;/strong&gt; For a hobby fleet, a 48×/day scan of a table with single-digit rows is free and honest. At real scale you'd flip the model: don't sweep, &lt;em&gt;don't even store decayed values&lt;/em&gt;: store only &lt;code&gt;updatedAt&lt;/code&gt; plus the stats at last interaction, and compute decay on read (we already do!). Then the scheduled sweep exists solely to push updates to online devices, and you'd drive it from the list of &lt;em&gt;connected&lt;/em&gt; things instead of the whole table. The lazy-evaluation trick in &lt;code&gt;applyDecay&lt;/code&gt; is the scalable part; the Scan is the shortcut bolted onto it. Know which is which and you can ship both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Closing the loop on the device
&lt;/h2&gt;

&lt;p&gt;The firmware's &lt;code&gt;on_cloud_message&lt;/code&gt; callback from part 1 finally has a job, parse the state and hand it to the face from part 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;cJSON&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cJSON_ParseWithLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pet_ui_set_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hunger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;valueint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;energy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;valueint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;mood&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;valueint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mood_from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;face&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;valuestring&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen lines of cJSON (in the repo), and the loop is closed: &lt;strong&gt;DynamoDB is now the pet's soul, the screen is just its face.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;Deploy and flash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk deploy CloudagotchiBrainStack
&lt;span class="nb"&gt;cd&lt;/span&gt; ../firmware &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; idf.py flash monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fme5cmd9i9l1dm2x0hyff.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%2Fme5cmd9i9l1dm2x0hyff.png" alt="Screenshot of the DynamoDB item showing the pet's row with hunger, energy, and mood values" width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now run the experiment that justifies this whole architecture. Tap the pet, the bars nudge up (device → rule → Lambda → DynamoDB → MQTT → face, ~200 ms round trip). Then unplug the board. Go live your life for a day. Plug it back in and watch the boot sequence: &lt;code&gt;hello&lt;/code&gt; goes up, and the state that comes back reflects every offline hour. The pet's smile flattens. Its hunger bar sits noticeably lower. It &lt;em&gt;missed you&lt;/em&gt;, and it has the DynamoDB row to prove it.&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%2Fl3xl5vtg3u0bti5qr4om.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%2Fl3xl5vtg3u0bti5qr4om.png" alt="Side-by-side photos: The pet happy with full bars vs. the pet sad after a day unplugged with low bars" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm not saying I felt guilty. I'm saying I fed it immediately.&lt;/p&gt;




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

&lt;p&gt;Strip away the pet and what we built is the canonical pattern for &lt;strong&gt;cloud-authoritative device state&lt;/strong&gt;: device sends events, cloud computes truth, truth flows back down, and &lt;em&gt;lazy time-accounting on read&lt;/em&gt; replaces any always-running process. That's a smart thermostat's schedule, a subscription's billing period, a game's energy system, an IoT sensor's calibration drift. The Tamagotchi version just has better animations.&lt;/p&gt;

&lt;p&gt;Our pet now has a body, reflexes, memory, and consequences. One thing left in the series, and it's the one I've been saving: giving it a &lt;em&gt;job&lt;/em&gt;. In &lt;strong&gt;part 4&lt;/strong&gt;, the pet fetches the AWS news every morning, has Amazon Bedrock rewrite them in its own voice, and reads them to me out loud through Amazon Polly. My Cloudagotchi becomes my news anchor. 📰&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;The Cloudagotchi repo&lt;/a&gt; : &lt;code&gt;git checkout article-3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/iot-sql-reference.html" rel="noopener noreferrer"&gt;AWS IoT SQL reference&lt;/a&gt; : &lt;code&gt;topic()&lt;/code&gt; and friends&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html" rel="noopener noreferrer"&gt;EventBridge Scheduler docs&lt;/a&gt; : cron for your serverless universe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html" rel="noopener noreferrer"&gt;DynamoDB data modeling&lt;/a&gt; : for when your pet fleet IPOs&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambda</category>
      <category>dynamodb</category>
    </item>
    <item>
      <title>Cloudagotchi Part 2 : Giving it a face: Sprites and touch on a 1.8" screen</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Thu, 23 Jul 2026 15:26:14 +0000</pubDate>
      <link>https://dev.to/aws/cloudagotchi-part-2-giving-it-a-face-sprites-and-touch-on-a-18-amoled-22nd</link>
      <guid>https://dev.to/aws/cloudagotchi-part-2-giving-it-a-face-sprites-and-touch-on-a-18-amoled-22nd</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/aws/meet-cloudagotchi-building-a-virtual-pet-with-a-cloud-brain-part-1-54j5"&gt;part 1&lt;/a&gt;, we gave our virtual pet a nervous system: the ESP32-S3 now speaks mutual-TLS MQTT with AWS IoT Core, and we can chat with it from the AWS console. Very satisfying. Also very much a black screen with a serial log, a pet only a backend developer could love.&lt;/p&gt;

&lt;p&gt;Today we fix that. By the end of this article, a face blinks at you from the screen: two soft eyes and an orange smile that looks suspiciously familiar if you've ever received a package (or used AWS ^^). Tap it and it beams. Shake the board and it goes wide-eyed. Ignore it for two minutes and it dozes off with a little &lt;em&gt;zzz&lt;/em&gt;. And every one of those interactions is published to the cloud, ready for the brain we'll build in part 3.&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%2Fsexudzccgph1501mkr52.jpg" 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%2Fsexudzccgph1501mkr52.jpg" alt="The Cloudagotchi happy face with stat bars on the 1.8" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through building a &lt;strong&gt;sprite-based&lt;/strong&gt; pet face with LVGL plus touch input, a shake detector in ~30 lines of IMU code, and the very honest story of why I ended up with sprites after my first approach fell apart on real hardware.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check (sorry 😅):&lt;/strong&gt; my first version of this pet was "elegant" a pet drawn procedurally from LVGL arcs and rounded rectangles, animated at 30 fps. It looked great in my head and became a corrupted mess of color bands on the actual panel. This article teaches the approach that &lt;em&gt;survived contact with the hardware&lt;/em&gt;. The detours are documented in the ⚠️ boxes, because they'll save you an afternoon each.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The code for this article is on the &lt;code&gt;article-2&lt;/code&gt; branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/tagazok/cloudagotchi &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;cloudagotchi
git checkout article-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The board does the hard part: the BSP
&lt;/h2&gt;

&lt;p&gt;Getting a display, touch controller, and LVGL configured from scratch on embedded hardware is normally an afternoon of datasheets and SPI timing tantrums, even more for me who is a JavaScript developer 😅. The reason I picked ESP-IDF over Arduino in part 1 pays off right here: Waveshare publishes an official &lt;strong&gt;Board Support Package&lt;/strong&gt; (BSP) for this exact board as a managed component. One line in &lt;code&gt;idf_component.yml&lt;/code&gt;:&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;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;waveshare/esp32_s3_touch_amoled_1_8&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^2.0.3'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and one function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"bsp/esp32_s3_touch_amoled_1_8.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;bsp_display_start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// AMOLED on, touch on, LVGL initialized.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind that call, the BSP configures the CO5300 display driver over QSPI, the touch controller over I2C, and starts an LVGL task with display flushing hooked up.&lt;/p&gt;

&lt;p&gt;Also, know this quirk: &lt;strong&gt;flashing leaves the panel dark.&lt;/strong&gt; The panel keeps running during &lt;code&gt;idf.py flash&lt;/code&gt; with nobody driving it. If you also have this problem after flashing, reset the board (&lt;code&gt;Ctrl+T&lt;/code&gt; &lt;code&gt;Ctrl+R&lt;/code&gt; in the monitor) and it comes up clean.&lt;/p&gt;

&lt;p&gt;One more rule to remember, because it will bite you exactly once: &lt;strong&gt;LVGL isn't thread-safe.&lt;/strong&gt; The BSP runs LVGL in its own task, so any other task touching the UI must take the lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;bsp_display_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// 1. take the LVGL mutex (0 = wait forever)&lt;/span&gt;
&lt;span class="n"&gt;lv_bar_set_value&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;    &lt;span class="c1"&gt;// 2. mutate the UI&lt;/span&gt;
&lt;span class="n"&gt;bsp_display_unlock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// 3. give it back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forget it and you get the embedded classic: a crash, or worse, a silent no-op.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why sprites, not shapes: a war story in one paragraph
&lt;/h2&gt;

&lt;p&gt;My first face was procedural: the head an &lt;code&gt;lv_obj&lt;/code&gt; with a big corner radius, the mouth an &lt;code&gt;lv_arc&lt;/code&gt;, everything animated by LVGL's animation engine: bobbing, blinking, squishing. Beautiful in theory. On hardware, the panel's QSPI transaction queue would overflow under load (Wi-Fi bring-up, TLS downloads) and &lt;strong&gt;silently drop color transfers&lt;/strong&gt;. Animated widgets self-heal, they redraw every frame. But each dropped chunk of a &lt;em&gt;static&lt;/em&gt; widget stays on screen as a garbage band forever. The more the UI animated, the more the queue overflowed, the worse the corruption. I tried buffer tuning, cache settings, custom DMA allocations; some attempts made it &lt;em&gt;worse&lt;/em&gt; in spectacular new ways.&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%2Fnki4xpd7i0gdssittetx.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%2Fnki4xpd7i0gdssittetx.png" alt="Corrupted display showing color banding from the procedural-drawing approach" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The approach that survived: &lt;strong&gt;make the display's job trivial.&lt;/strong&gt; Pre-render every expression as a sprite baked into flash. The face is ONE static image widget; changing expression = swapping which sprite is shown = one clean rectangular blit. A blink, a mood change, a talking mouth, all the same cheap operation. Nothing redraws unless an event happens, so nothing can outrun the display.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The face: ONE static image widget, centered. That's the whole pet.&lt;/span&gt;
&lt;span class="n"&gt;s_face&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lv_image_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;lv_image_set_src&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_face&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_neutral&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;lv_obj_align&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_face&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LV_ALIGN_CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The sprite pipeline: from PNG to C array
&lt;/h2&gt;

&lt;p&gt;The art is a set of PNGs, one per expression (neutral, happy, sad, sleeping, three talking mouths, plus per-mood blink frames). I generated mine with an AI image generator and composited them onto a &lt;strong&gt;common canvas with identical eye positions&lt;/strong&gt; so frames don't jitter when swapped.&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%2F81p4i2sfu8c0popjmvn2.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%2F81p4i2sfu8c0popjmvn2.png" alt="Sprite strip showing all face frames side by side on a black background" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each PNG becomes a C file via a small Python script (in the repo): pixels converted to &lt;strong&gt;RGB565A8&lt;/strong&gt;, 16-bit color plus an 8-bit alpha plane, LVGL 9's friendliest transparent format, and wrapped in an &lt;code&gt;lv_image_dsc_t&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;lv_image_dsc_t&lt;/span&gt; &lt;span class="n"&gt;face_happy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;magic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LV_IMAGE_HEADER_MAGIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LV_COLOR_FORMAT_RGB565A8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;162&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stride&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;420&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;happy_map&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;happy_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// lives in flash, memory-mapped, zero RAM cost&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven faces at ~100 KB each sounds heavy, but they compile into the 16 MB flash and LVGL blits them straight from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blinking, by image swap
&lt;/h2&gt;

&lt;p&gt;No animation engine needed, a blink is two sprite swaps on a timer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;blink_timer_cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_timer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_talking&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;s_dozing&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;s_mood&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PET_MOOD_SLEEPING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Dedicated blink frame: closed eyes, SAME mouth as the current mood —&lt;/span&gt;
    &lt;span class="c1"&gt;// a blink should only move the eyes.&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_mood&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PET_MOOD_HAPPY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_blink_happy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PET_MOOD_SAD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_blink_sad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;default:&lt;/span&gt;             &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_blink_neutral&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;lv_timer_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blink_close_cb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// reopen after 120 ms&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  The doze: a pet that gets bored
&lt;/h2&gt;

&lt;p&gt;Leave the pet alone for two minutes and it falls asleep (the sleeping sprite, complete with a little &lt;em&gt;zzz&lt;/em&gt;). Touch it or shake it and it wakes. One LVGL timer plus a reset-on-activity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 2 minutes with no interaction → the pet nods off.&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;idle_timer_cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_timer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_talking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// reading aloud is not boredom&lt;/span&gt;
    &lt;span class="n"&gt;s_dozing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;face_sleeping&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Any interaction resets the boredom clock — and wakes the pet.&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;note_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_idle_timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;lv_timer_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_idle_timer&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="n"&gt;s_dozing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;s_dozing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;set_face&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mood_sprite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_mood&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A deliberate design choice: &lt;strong&gt;cloud updates don't wake it.&lt;/strong&gt; The stat bars refresh quietly under the napping face; only &lt;em&gt;human&lt;/em&gt; interaction wakes the pet. Nobody likes being woken by a cron job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HUD: three stats, three icons, one secret button
&lt;/h2&gt;

&lt;p&gt;Across the top: three bars with icons : 🥑 hunger (avocado green), ⚡ energy (teal), ❤️ mood (pink). The icons are tiny 28×28 sprites drawn programmatically by the same PNG→C pipeline.&lt;/p&gt;

&lt;p&gt;The avocado is not just a label, &lt;strong&gt;it's a button.&lt;/strong&gt; Tapping it feeds the pet (we wire up what "feed" &lt;em&gt;means&lt;/em&gt; in part 3; today it publishes the interaction):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;food_clicked_cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_event_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pet_ui_react_happy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                           &lt;span class="c1"&gt;// nom nom, instantly&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_on_interaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s_on_interaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PET_INTERACTION_FEED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Shake detection: the IMU in 30 lines
&lt;/h2&gt;

&lt;p&gt;The board has a QMI8658 6-axis IMU. You might reach for a gesture-recognition library. Don't, a shake is beautifully simple physics: &lt;strong&gt;at rest the accelerometer reads ~1 g of gravity (1000 mg); a shake is the magnitude repeatedly leaving that baseline.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The Waveshare qmi8658 component hands us calibrated milli-g values&lt;/span&gt;
&lt;span class="c1"&gt;// (we asked for mg units once, at init, with qmi8658_set_accel_unit_mg).&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;qmi8658_read_accel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;s_imu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Total acceleration, all axes combined. At rest: ~1000 mg.&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;mag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqrtf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ax&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ay&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. A "jolt" = a reading well above resting gravity.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mag&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;jolts&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Three jolts within 800 ms = the human is shaking us.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jolts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;within_window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;on_shake&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;cooldown&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// 4. then ignore everything for 1.5s (debounce!)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full version (in &lt;a href="https://github.com/tagazok/cloudagotchi/blob/main/firmware/main/app_imu.c" rel="noopener noreferrer"&gt;&lt;code&gt;app_imu.c&lt;/code&gt;&lt;/a&gt;) polls at 50 Hz from its own FreeRTOS task, plenty for human hands.&lt;/p&gt;

&lt;p&gt;The three constants are worth tuning to taste:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constant&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Feel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SHAKE_THRESHOLD_MG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1800 mg (1.8 g)&lt;/td&gt;
&lt;td&gt;Lower = hair-trigger, higher = vigorous shaking required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SHAKE_JOLTS_NEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Filters out bumping the desk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SHAKE_COOLDOWN_MS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1500&lt;/td&gt;
&lt;td&gt;One shake = one event, not twelve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Debounce is the whole game.&lt;/strong&gt; My first version fired 14 "play" events per shake, which in part 3's economy would have turned one wiggle into a pet so overstimulated it refused dinner.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One threading gotcha that bit me: the shake callback runs on the &lt;strong&gt;IMU task&lt;/strong&gt;, not the LVGL task, so the reaction functions take the display lock (it's recursive, so they're safe from touch callbacks too).&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring body to nervous system
&lt;/h2&gt;

&lt;p&gt;Everything converges in &lt;code&gt;main.c&lt;/code&gt;, and the flow reads like the pet's reflex arc: input, reaction on screen, report to the cloud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;on_shake&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pet_ui_react_startled&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// reflex: wide eyes, instantly&lt;/span&gt;
    &lt;span class="n"&gt;app_mqtt_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"interaction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;play&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// then tell the brain&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;app_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Face first: the pet appears before Wi-Fi even starts.&lt;/span&gt;
    &lt;span class="n"&gt;pet_ui_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_interaction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;pet_ui_set_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PET_MOOD_NEUTRAL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;app_imu_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_shake&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;app_wifi_connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;app_mqtt_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_cloud_message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That ordering is a deliberate UX decision: &lt;strong&gt;react locally first, sync to the cloud second.&lt;/strong&gt; The happy face flashes the instant you tap, not a network round-trip later. The cloud is the source of truth for &lt;em&gt;state&lt;/em&gt; (part 3), but never sits between a touch and its reaction. Pets have reflexes; only their &lt;em&gt;feelings&lt;/em&gt; need a brain.&lt;/p&gt;

&lt;p&gt;Flash it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;firmware &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; idf.py flash monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and there it is. A face, blinking on that ridiculous little AMOLED, beaming when you tap it, nodding off when you don't. Meanwhile, in the AWS console (MQTT test client, &lt;code&gt;cloudagotchi/#&lt;/code&gt;), your affection is now telemetry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/interaction&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pet"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/interaction&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"feed"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/interaction&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"play"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&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%2Fy3d2dikynja55ab8vubh.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%2Fy3d2dikynja55ab8vubh.png" alt="AWS IoT MQTT test client showing interaction messages arriving" width="800" height="830"&gt;&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;Two takeaways travel well beyond virtual pets. First, &lt;strong&gt;on constrained display hardware, sprites beat shapes&lt;/strong&gt;: pre-rendering moves all the expensive work to build time and reduces the runtime to "copy rectangle", the exact operation embedded panels are good at. The corollary: the less your UI redraws, the less can go wrong, and event-driven beats animated. Second, the &lt;strong&gt;local-reflex / cloud-state split&lt;/strong&gt; is &lt;em&gt;the&lt;/em&gt; interaction pattern for connected devices. Anything that must feel instant lives on the device; anything that must be remembered lives in the cloud. Get that boundary right and your device feels alive even on flaky hotel Wi-Fi.&lt;/p&gt;

&lt;p&gt;Right now, though, our pet publishes its little heart out into the void. Nobody listens. Its stats are hardcoded at 80. It is, developmentally speaking, a very cute reflex with no memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next article:&lt;/strong&gt; the brain. DynamoDB remembers, Lambda decides, EventBridge Scheduler makes time pass, and the pet finally gets hungry while you sleep.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;The Cloudagotchi repo&lt;/a&gt; : &lt;code&gt;git checkout article-2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lvgl.io/docs/open/main-modules/images/overview" rel="noopener noreferrer"&gt;LVGL images documentation&lt;/a&gt; : formats, &lt;code&gt;lv_image_dsc_t&lt;/code&gt;, and the online converter&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://components.espressif.com/components/waveshare/esp32_s3_touch_amoled_1_8" rel="noopener noreferrer"&gt;Waveshare BSP on the ESP Component Registry&lt;/a&gt; : the one-liner display setup&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>esp32</category>
      <category>embedded</category>
      <category>lvgl</category>
      <category>iot</category>
    </item>
    <item>
      <title>Meet Cloudagotchi : Building a virtual pet with a cloud brain (Part 1)</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Tue, 21 Jul 2026 09:38:15 +0000</pubDate>
      <link>https://dev.to/aws/meet-cloudagotchi-building-a-virtual-pet-with-a-cloud-brain-part-1-54j5</link>
      <guid>https://dev.to/aws/meet-cloudagotchi-building-a-virtual-pet-with-a-cloud-brain-part-1-54j5</guid>
      <description>&lt;p&gt;Remember Tamagotchis? Those little egg-shaped keychains from the 90s with a pixelated pet that got hungry, got sad, and, if you were a negligent eight-year-old like me, got &lt;em&gt;dead&lt;/em&gt; during math class.&lt;/p&gt;

&lt;p&gt;I've been looking for an excuse to play with the &lt;a href="https://www.waveshare.com/esp32-s3-touch-amoled-1.8.htm" rel="noopener noreferrer"&gt;Waveshare ESP32-S3 Touch AMOLED 1.8"&lt;/a&gt;, a $30 board with a gorgeous 368×448 AMOLED touchscreen, an accelerometer, a microphone, and a speaker. And I found one: I'm building a Tamagotchi. But with a twist that makes it worth a four-article series&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%2Fwlqe0dllq7hmnrathipe.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%2Fwlqe0dllq7hmnrathipe.png" alt=" " width="799" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pet's body lives on the device. Its brain lives in AWS.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The device renders an adorable pet and captures your taps and shakes. But its hunger, mood, and energy are rows in DynamoDB. It gets hungry overnight because EventBridge Scheduler says so. And, my favorite part (and because it's 2026, we can't not have genAI in a project 🤪), every morning it fetches the AWS news, has Amazon Bedrock rewrite them in its own squeaky personality, and reads them to me out loud through Amazon Polly. A Tamagotchi with a job.&lt;/p&gt;

&lt;p&gt;In this series, I will walk you through the whole build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This article&lt;/strong&gt; : the architecture, and connecting the ESP32-S3 to AWS IoT Core (the pet's nervous system).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt; : giving it a face: animations and touch with LVGL on the AMOLED.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3&lt;/strong&gt; : the serverless brain: Lambda, DynamoDB, and a pet that gets hungry while you sleep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4&lt;/strong&gt; : Bedrock + Polly: my Tamagotchi reads me the AWS news.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check before you get too excited (sorry 😅):&lt;/strong&gt; this is a hobby project, not a product. You'll need the specific Waveshare board (or the patience to adapt the code to yours), an AWS account, and a USB-C cable. The AWS bill for the whole series is well under a dollar a month, but it is not zero, and neither is the time you'll spend staring at &lt;code&gt;idf.py monitor&lt;/code&gt;. Worth it, though.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All the code lives in &lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;this repository&lt;/a&gt;. Each article has its own branch. For this one, &lt;code&gt;git checkout article-1&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why put a pet's brain in the cloud?
&lt;/h2&gt;

&lt;p&gt;Fair question. The original Tamagotchi ran on a chip with less power than my USB cable. Why does mine need a cloud?&lt;/p&gt;

&lt;p&gt;Three reasons, and they're the same reasons real IoT products are built this way:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The pet needs to...&lt;/th&gt;
&lt;th&gt;Which means...&lt;/th&gt;
&lt;th&gt;AWS gives us...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Get hungry &lt;em&gt;while the device is off&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Time must pass server-side&lt;/td&gt;
&lt;td&gt;EventBridge Scheduler + Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remember it was sad yesterday&lt;/td&gt;
&lt;td&gt;State must outlive reboots&lt;/td&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Say something new every day&lt;/td&gt;
&lt;td&gt;Logic must evolve without reflashing&lt;/td&gt;
&lt;td&gt;Bedrock, deployed behind Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is going to save us a lot of time, trust me (been there flashing the device for every little change I did...). When the pet's brain is a Lambda function, giving it a new behavior is just a &lt;code&gt;cdk deploy&lt;/code&gt;, not a firmware update. The device stays dumb and stable; the personality iterates daily.&lt;/p&gt;

&lt;p&gt;This is, not coincidentally, the exact architecture of every serious connected device you own. Your robot vacuum is a Tamagotchi with a motor. We're just building the fun version.&lt;/p&gt;

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

&lt;p&gt;Here's the full picture we're building toward across the series:&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%2F8gm4nxh46xywggoyz2r7.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%2F8gm4nxh46xywggoyz2r7.png" alt="Cloudagotchi architecture diagram" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Today we're building the arrow in the middle. No pet on screen yet. First, the nervous system, then, the face. (If you came here for cuteness, part 2 will deliver, I promise.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The pet's vocabulary: MQTT topics
&lt;/h2&gt;

&lt;p&gt;The device and cloud talk over MQTT, the lingua franca of IoT: a lightweight pub/sub protocol where everyone publishes to &lt;em&gt;topics&lt;/em&gt;. Designing the topic space first is the IoT equivalent of designing your API routes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudagotchi/{thing}/hello&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;device → cloud&lt;/td&gt;
&lt;td&gt;"I just woke up, what did I miss?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudagotchi/{thing}/interaction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;device → cloud&lt;/td&gt;
&lt;td&gt;"The human fed/petted/played with me"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudagotchi/{thing}/state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cloud → device&lt;/td&gt;
&lt;td&gt;"Here are your current stats, render them"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cloudagotchi/{thing}/briefing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cloud → device&lt;/td&gt;
&lt;td&gt;"Fresh AWS news, go fetch the audio"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;{thing}&lt;/code&gt; is the device's unique name (&lt;code&gt;cloudagotchi-01&lt;/code&gt;). Baking it into the topic path matters for security, as you're about to see.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — The cloud side: an IoT policy with trust issues
&lt;/h2&gt;

&lt;p&gt;A device talking to AWS IoT Core authenticates with an &lt;strong&gt;X.509 certificate&lt;/strong&gt;, a private key that lives on the device and never leaves it. No API keys in the firmware, no secrets in your repo.&lt;/p&gt;

&lt;p&gt;But authentication says &lt;em&gt;who you are&lt;/em&gt;, not &lt;em&gt;what you may do&lt;/em&gt;. That's the &lt;strong&gt;IoT policy&lt;/strong&gt;, and here's the part that trips people up: a lazy policy (&lt;code&gt;iot:*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt;, every tutorial's dirty secret) means any compromised device can impersonate the whole fleet. Ours pins every permission to the device's own identity:&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="c1"&gt;// backend/lib/iot-stack.ts — the interesting part.&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CloudagotchiDevicePolicy&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;policyName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CloudagotchiDevicePolicy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;policyDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2012-10-17&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. You may connect ONLY under your own name...&lt;/span&gt;
        &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iot:Connect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`arn:aws:iot:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:client/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{iot:Connection.Thing.ThingName}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. ...publish ONLY on your own topics...&lt;/span&gt;
        &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iot:Publish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`arn:aws:iot:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:topic/cloudagotchi/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{iot:Connection.Thing.ThingName}/*`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 3. ...and listen ONLY to your own topics.&lt;/span&gt;
        &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iot:Subscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`arn:aws:iot:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:topicfilter/cloudagotchi/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{iot:Connection.Thing.ThingName}/*`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="c1"&gt;// (+ iot:Receive, same resource shape — see the repo)&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The magic is &lt;code&gt;${iot:Connection.Thing.ThingName}&lt;/code&gt;, an IoT policy variable that resolves &lt;em&gt;at connection time&lt;/em&gt; to the name of the thing attached to the certificate. One policy serves the whole fleet, yet &lt;code&gt;cloudagotchi-01&lt;/code&gt; physically cannot publish on &lt;code&gt;cloudagotchi-02&lt;/code&gt;'s topics. If your pet ever gets kidnapped at a conference (mine will, eventually), the blast radius is one sad pet.&lt;/p&gt;

&lt;p&gt;Deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk deploy CloudagotchiIotStack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 — A birth certificate for the pet
&lt;/h2&gt;

&lt;p&gt;Certificates are secrets, so they don't belong in CloudFormation state, we create them per-device with a small script (&lt;a href="https://github.com/tagazok/cloudagotchi/blob/main/scripts/provision-device.sh" rel="noopener noreferrer"&gt;&lt;code&gt;scripts/provision-device.sh&lt;/code&gt;&lt;/a&gt; in the repo). The gist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the thing (the device's identity in AWS).&lt;/span&gt;
aws iot create-thing &lt;span class="nt"&gt;--thing-name&lt;/span&gt; cloudagotchi-01

&lt;span class="c"&gt;# Create a certificate + private key, saved straight into the firmware tree.&lt;/span&gt;
&lt;span class="nv"&gt;CERT_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws iot create-keys-and-certificate &lt;span class="nt"&gt;--set-as-active&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-pem-outfile&lt;/span&gt; firmware/main/certs/device-cert.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--private-key-outfile&lt;/span&gt;   firmware/main/certs/device-key.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; certificateArn &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Tie it all together: this cert may act as this thing, under this policy.&lt;/span&gt;
aws iot attach-policy &lt;span class="nt"&gt;--policy-name&lt;/span&gt; CloudagotchiDevicePolicy &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CERT_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
aws iot attach-thing-principal &lt;span class="nt"&gt;--thing-name&lt;/span&gt; cloudagotchi-01 &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CERT_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command in the repo version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/provision-device.sh cloudagotchi-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The &lt;code&gt;.pem&lt;/code&gt; files are gitignored, keep it that way.&lt;/strong&gt; The private key is the pet's identity. Anyone who has it &lt;em&gt;is&lt;/em&gt; your pet, as far as AWS is concerned. The repo ignores &lt;code&gt;certs/*.pem&lt;/code&gt; except Amazon's public root CA, and you should never weaken that line.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3 — The firmware: Wi-Fi, then mutual TLS
&lt;/h2&gt;

&lt;p&gt;The firmware is plain &lt;strong&gt;ESP-IDF&lt;/strong&gt; (Espressif's native framework) plus the official Waveshare board support package. Why not Arduino? ESP-IDF's built-in &lt;code&gt;esp-mqtt&lt;/code&gt; client does mutual TLS natively, and the BSP hands us the display, touch, and audio drivers we'll need in parts 2 and 4, less glue code overall, which matters when every line has to fit in a blog post.&lt;/p&gt;

&lt;p&gt;The build embeds the three credential files into the binary, no filesystem juggling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="c1"&gt;# firmware/main/CMakeLists.txt&lt;/span&gt;
&lt;span class="nf"&gt;idf_component_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    SRCS &lt;span class="s2"&gt;"main.c"&lt;/span&gt; &lt;span class="s2"&gt;"app_wifi.c"&lt;/span&gt; &lt;span class="s2"&gt;"app_mqtt.c"&lt;/span&gt;
    INCLUDE_DIRS &lt;span class="s2"&gt;"."&lt;/span&gt;
    EMBED_TXTFILES
        &lt;span class="s2"&gt;"certs/AmazonRootCA1.pem"&lt;/span&gt;
        &lt;span class="s2"&gt;"certs/device-cert.pem"&lt;/span&gt;
        &lt;span class="s2"&gt;"certs/device-key.pem"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the MQTT connection is disarmingly small. This is the entire handshake with AWS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// firmware/main/app_mqtt.c — the heart of it.&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;esp_mqtt_client_config_t&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;broker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. AWS IoT speaks MQTT over TLS on port 8883.&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mqtts://"&lt;/span&gt; &lt;span class="n"&gt;CONFIG_CLOUDAGOTCHI_IOT_ENDPOINT&lt;/span&gt; &lt;span class="s"&gt;":8883"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. We verify we're talking to the real AWS...&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;certificate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root_ca_pem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 3. ...and AWS verifies it's talking to the real us.&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authentication&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;certificate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device_cert_pem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device_key_pem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;// 4. Our policy requires client id == thing name. Not optional!&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONFIG_CLOUDAGOTCHI_THING_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;s_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;esp_mqtt_client_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;esp_mqtt_client_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's mutual TLS: both sides prove who they are before a single byte of MQTT flows. Step 4 is the one people forget, if the client id doesn't match the thing name, our policy's &lt;code&gt;iot:Connect&lt;/code&gt; statement rejects the connection, and you get the single most cryptic failure mode in IoT: an endless connect/disconnect loop with no error message. You've been warned. 😅&lt;/p&gt;

&lt;p&gt;On connect, the pet introduces itself and subscribes to its own topics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;MQTT_EVENT_CONNECTED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// Listen to everything addressed to *this* pet...&lt;/span&gt;
    &lt;span class="n"&gt;esp_mqtt_client_subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cloudagotchi/"&lt;/span&gt; &lt;span class="n"&gt;THING&lt;/span&gt; &lt;span class="s"&gt;"/#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...and announce we're awake. In part 3, a Lambda answers&lt;/span&gt;
    &lt;span class="c1"&gt;// this with the pet's current state. Today, only silence. 🥲&lt;/span&gt;
    &lt;span class="n"&gt;app_mqtt_publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;firmware&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;article-1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main loop just heartbeats every 30 seconds so we have a pulse to watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Proof of life
&lt;/h2&gt;

&lt;p&gt;Configure and flash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;firmware
idf.py set-target esp32s3
idf.py menuconfig     &lt;span class="c"&gt;# → "Cloudagotchi Configuration": Wi-Fi + IoT endpoint + thing name&lt;/span&gt;
idf.py flash monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Your IoT endpoint: &lt;code&gt;aws iot describe-endpoint --endpoint-type iot:Data-ATS&lt;/code&gt;.)&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%2F6pltx1edswatzaozos1f.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%2F6pltx1edswatzaozos1f.png" alt="Cloudagotchi menuconfig screenshot" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Region gotcha that will produce the most confusing failure of your build.&lt;/strong&gt; The device's endpoint and your deployed stacks must be in the &lt;strong&gt;same region&lt;/strong&gt;, the endpoint hostname looks almost identical across regions (&lt;code&gt;xxxx-ats.iot.us-east-1...&lt;/code&gt; vs &lt;code&gt;xxxx-ats.iot.us-west-2...&lt;/code&gt;), and if they don't match, TLS connects fine and then AWS silently drops you: an endless connect/EOF loop with no error message anywhere. Always take the endpoint from the same CLI region you deployed to (check with &lt;code&gt;aws configure get region&lt;/code&gt;), and pass that same value everywhere it's needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the gods of embedded development smile upon you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I (2841) app_wifi: Wi-Fi connected
I (4127) app_mqtt: Connected to AWS IoT Core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the fun part. Open the &lt;strong&gt;AWS IoT console → MQTT test client&lt;/strong&gt;, subscribe to &lt;code&gt;cloudagotchi/#&lt;/code&gt;, and watch your pet's heartbeat arrive from your desk:&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%2Fs28671i3jwi5rxgot8ek.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%2Fs28671i3jwi5rxgot8ek.png" alt="AWS IoT MQTT test client showing heartbeat messages" width="800" height="854"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/heartbeat&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"alive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And talk back! Publish &lt;code&gt;{"msg":"hello little one"}&lt;/code&gt; to &lt;code&gt;cloudagotchi/cloudagotchi-01/greeting&lt;/code&gt; and watch it land in your serial monitor:&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%2F3t42r69d5rl9yglrckc0.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%2F3t42r69d5rl9yglrckc0.png" alt="Publishing a message from the AWS IoT console to the device" width="800" height="480"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;I&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;91204&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cloudagotchi:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cloud&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;says&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;cloudagotchi/cloudagotchi&lt;/span&gt;&lt;span class="mi"&gt;-01&lt;/span&gt;&lt;span class="err"&gt;/greeting&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"hello little one"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A message typed in a browser, through AWS, to a $30 board on your desk, in about 100 ms. It never stops being cool.&lt;/p&gt;




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

&lt;p&gt;Everything we built today (certificate identity, least-privilege topic isolation, a scoped policy shared by a fleet) is the same foundation you'd lay for a real product with a million devices. The difference between a toy IoT tutorial and production IoT isn't the hardware; it's whether &lt;code&gt;${iot:Connection.Thing.ThingName}&lt;/code&gt; shows up in the policy. Now it's in ours, and we never have to think about it again.&lt;/p&gt;

&lt;p&gt;Our pet has a nervous system. It can feel (receive) and speak (publish). What it doesn't have is a face, it's currently the philosophical-zombie phase of the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next article:&lt;/strong&gt; we fix that. LVGL on the AMOLED, a pet with blinking eyes and moods, touch interactions, and the accelerometer, because a pet you can't tickle by shaking the board is just a dashboard.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;The Cloudagotchi repo&lt;/a&gt; : &lt;code&gt;git checkout article-1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.waveshare.com/wiki/ESP32-S3-Touch-AMOLED-1.8" rel="noopener noreferrer"&gt;Waveshare ESP32-S3 Touch AMOLED 1.8 wiki&lt;/a&gt; : board docs and schematics&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/iot/latest/developerguide/thing-policy-variables.html" rel="noopener noreferrer"&gt;AWS IoT Core policy variables&lt;/a&gt; : the &lt;code&gt;ThingName&lt;/code&gt; trick, officially&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/protocols/mqtt.html" rel="noopener noreferrer"&gt;ESP-MQTT docs&lt;/a&gt; : the client we used&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Disclosure:&lt;/strong&gt; I'm a Developer Advocate at AWS, that's the cloud my Tamagotchi thinks with. The device-side patterns (mutual TLS, scoped topics) apply to any MQTT broker.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>iot</category>
      <category>esp32</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a Tamagotchi with its brain in AWS (and it reads me the news)</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:30:09 +0000</pubDate>
      <link>https://dev.to/aws/i-built-a-tamagotchi-with-its-brain-in-aws-and-it-reads-me-the-news-481n</link>
      <guid>https://dev.to/aws/i-built-a-tamagotchi-with-its-brain-in-aws-and-it-reads-me-the-news-481n</guid>
      <description>&lt;p&gt;Remember Tamagotchis? Those little egg-shaped keychains with a pixelated pet that got hungry, got sad, and, if you were a negligent eight-year-old like me, got &lt;em&gt;dead&lt;/em&gt; during math class.&lt;/p&gt;

&lt;p&gt;I built one. Except mine has a 368×448 AMOLED screen, an accelerometer, a speaker... and its brain runs on AWS.&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;Cloudagotchi&lt;/strong&gt;:&lt;/p&gt;

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

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

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;gets hungry while the device is off&lt;/strong&gt; : time passes in the cloud, not on the chip. EventBridge Scheduler decides when it's dinner time.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;remembers you ignored it yesterday&lt;/strong&gt; : its feelings are a DynamoDB row. You can't reset a guilt trip by rebooting.&lt;/li&gt;
&lt;li&gt;You &lt;strong&gt;feed it by tapping an avocado&lt;/strong&gt;, pet it by tapping its face, play with it by shaking the board. Every interaction is an MQTT round trip through AWS IoT Core.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;dozes off&lt;/strong&gt; if you leave it alone, complete with a little &lt;em&gt;zzz&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;And my favorite part: &lt;strong&gt;every morning at 7:03, it reads me the AWS news out loud&lt;/strong&gt; : Amazon Bedrock rewrites the headlines in the pet's cute voice, Amazon Polly gives it speech, and the little face moves its mouth while it talks. A Tamagotchi with a job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works (the 30-second version)
&lt;/h2&gt;

&lt;p&gt;The device is the body; AWS is the brain. The ESP32 renders sprites and captures your taps. But every feeling, every stat, every decision comes from the cloud:&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%2Fnk991ulg0j8z9nwjzqfx.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%2Fnk991ulg0j8z9nwjzqfx.png" alt="Cloudagotchi Architecture Diagram" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The stack, at a glance: &lt;strong&gt;ESP-IDF + LVGL&lt;/strong&gt; on a &lt;a href="https://www.waveshare.com/esp32-s3-touch-amoled-1.8.htm" rel="noopener noreferrer"&gt;Waveshare ESP32-S3 Touch AMOLED 1.8"&lt;/a&gt;, talking mutual-TLS MQTT to &lt;strong&gt;AWS IoT Core&lt;/strong&gt;, with &lt;strong&gt;Lambda&lt;/strong&gt;, &lt;strong&gt;DynamoDB&lt;/strong&gt; and &lt;strong&gt;EventBridge Scheduler&lt;/strong&gt; as the serverless brain, and a &lt;strong&gt;Bedrock → Polly → S3&lt;/strong&gt; pipeline for the morning briefing. All infrastructure defined with &lt;strong&gt;CDK&lt;/strong&gt;. Total running cost: well under a dollar a month.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Not coincidentally, this is the exact architecture of every serious connected device you own. Your robot vacuum is a Tamagotchi with a motor. We're just building the fun version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The series
&lt;/h2&gt;

&lt;p&gt;I'm turning the whole build into four articles, each with its own git branch so you can follow along commit by commit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This is article 0 ^^&lt;/strong&gt;: It's a teaser so you come back to see the rest of the project 🤫&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/aws/meet-cloudagotchi-building-a-virtual-pet-with-a-cloud-brain-part-1-54j5"&gt;Meet Cloudagotchi: A virtual pet with a cloud brain&lt;/a&gt;&lt;/strong&gt; : the architecture, and connecting the board to AWS IoT Core with mutual TLS (plus the IoT policy trick that makes a kidnapped pet a non-event).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/aws/cloudagotchi-part-2-giving-it-a-face-sprites-and-touch-on-a-18-amoled-22nd"&gt;Giving it a face&lt;/a&gt;&lt;/strong&gt; : sprite-based LVGL UI, blinking, dozing, shake detection... and the very honest story of how my first "elegant" procedural face became a corrupted mess on real hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/aws/cloudagotchi-part-3-it-gets-hungry-while-you-sleep-a-serverless-pet-brain-1l1j"&gt;It gets hungry while You sleep&lt;/a&gt;&lt;/strong&gt; : the serverless state machine: Lambda, DynamoDB, and the lazy-time-accounting trick that makes a pet age without a single always-on process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/aws/cloudagotchi-part-4-my-tamagotchi-reads-me-the-aws-news-9do"&gt;My tamagotchi reads me the AWS News&lt;/a&gt;&lt;/strong&gt; : Bedrock as a squeaky-voiced news anchor, Polly's one-parameter trick that deleted an entire firmware subsystem, and the four embedded-streaming gotchas nobody warns you about.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fair warning: this series contains real war stories. Panels that wedge themselves, buffers defeated by presigned URLs, a TCP window doing trans-Atlantic math against me. The failures were the best teachers, so they're all in there.&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%2Fwb868sgv6tr3orc0jp5y.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%2Fwb868sgv6tr3orc0jp5y.png" alt="The Cloudagotchi device on my desk" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1 drops on Tuesday, July 20th. Follow me to catch it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code is open source: &lt;a href="https://github.com/tagazok/cloudagotchi" rel="noopener noreferrer"&gt;github.com/tagazok/cloudagotchi&lt;/a&gt;. And, if you build one, please show me your pet. 👻&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iot</category>
      <category>esp32</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI Without a Backend: The Browser's Built-in AI APIs for Web Developers</title>
      <dc:creator>Olivier Leplus</dc:creator>
      <pubDate>Wed, 08 Jul 2026 14:12:29 +0000</pubDate>
      <link>https://dev.to/olivierleplus/ai-without-a-backend-the-browsers-built-in-ai-apis-for-web-developers-2745</link>
      <guid>https://dev.to/olivierleplus/ai-without-a-backend-the-browsers-built-in-ai-apis-for-web-developers-2745</guid>
      <description>&lt;p&gt;For years, "adding AI to your web app" meant the same thing: sign up for an API key, wire up a backend proxy so you don't leak that key to the browser, meter your token usage, handle rate limits, and pray your bill doesn't explode when a feature goes viral. Every summary, every classification, every bit of generated text was a round-trip to someone else's data center.&lt;/p&gt;

&lt;p&gt;Chrome is quietly changing that. A family of &lt;strong&gt;built-in AI APIs&lt;/strong&gt; now ships a language model — Google's Gemini Nano — &lt;em&gt;inside the browser&lt;/em&gt;. Your JavaScript can summarize an article, translate a comment, or generate structured data from an image without a single network call to an inference server. No API key. No backend. No per-token bill. And because the model runs on-device, the user's data never leaves their machine.&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through what these APIs are, how to use them safely, and two hands-on examples: summarizing user reviews, and turning a product photo into structured JSON with the Prompt API.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Reality check before you get too excited (sorry 😅):&lt;/strong&gt; these APIs are desktop-only for now (no mobile), require a fairly capable machine, and the model is a ~4 GB download on first use. They're production-viable as &lt;em&gt;progressive enhancements&lt;/em&gt;, not as your only code path. We'll cover graceful degradation throughout.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The lineup: task APIs vs. the Prompt API
&lt;/h2&gt;

&lt;p&gt;Chrome's built-in AI comes in two flavors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task-specific APIs&lt;/strong&gt; do one job well, with a clean, purpose-built interface:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Translator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Translates text between languages on-device.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language Detector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Detects the language of a piece of text.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Summarizer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Condenses long text into key points, a TL;DR, a teaser, or a headline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Writer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generates new text from a prompt (e.g. a product description).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rewriter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Restructures or improves existing text (shorter, more formal, etc.).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The Prompt API&lt;/strong&gt; (also called the &lt;strong&gt;LanguageModel API&lt;/strong&gt;) is the general-purpose escape hatch. When no task API fits, you talk to Gemini Nano directly with your own prompts — and it's &lt;strong&gt;multimodal&lt;/strong&gt;, accepting text, images, and audio as input.&lt;/p&gt;

&lt;p&gt;The mental model: reach for a &lt;strong&gt;task API&lt;/strong&gt; when your need matches one (translation, summarization, rewriting) — it's simpler and more predictable. Reach for the &lt;strong&gt;Prompt API&lt;/strong&gt; when you need something custom, structured, or multimodal.&lt;/p&gt;

&lt;p&gt;All of these share the same lifecycle, so once you learn one, you know them all.&lt;/p&gt;




&lt;h2&gt;
  
  
  The lifecycle every built-in AI API shares
&lt;/h2&gt;

&lt;p&gt;Every API follows the same three-step pattern: &lt;strong&gt;feature-detect → check availability → create and use.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Feature detection
&lt;/h3&gt;

&lt;p&gt;The interfaces live as globals (&lt;code&gt;Summarizer&lt;/code&gt;, &lt;code&gt;LanguageModel&lt;/code&gt;, &lt;code&gt;Translator&lt;/code&gt;, …). Check before you touch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// The Summarizer API exists in this browser.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Availability and the model download
&lt;/h3&gt;

&lt;p&gt;Here's the part that trips people up. The API being &lt;em&gt;present&lt;/em&gt; doesn't mean the model is &lt;em&gt;ready&lt;/em&gt;. Gemini Nano is downloaded separately the first time an origin uses it — and it's a multi-gigabyte download.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;availability()&lt;/code&gt; tells you where you stand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&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;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// "unavailable"  → not supported on this device/config&lt;/span&gt;
&lt;span class="c1"&gt;// "downloadable" → supported, but the model must download first&lt;/span&gt;
&lt;span class="c1"&gt;// "downloading"  → download in progress&lt;/span&gt;
&lt;span class="c1"&gt;// "available"    → ready to go right now&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Create the instance (and show download progress)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;create()&lt;/code&gt; instantiates the API. If the model still needs downloading, &lt;code&gt;create()&lt;/code&gt; triggers it — so &lt;strong&gt;always give the user feedback&lt;/strong&gt;, because "downloading 4 GB" is not something to do silently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&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;Summarizer&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="nf"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downloadprogress&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Model download: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loaded&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The download happens once per device, not once per site.&lt;/strong&gt; After Gemini Nano is on disk, every origin that uses these APIs shares it. Chrome manages it like a cache — if disk space runs low it may evict the model and re-download later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create()&lt;/code&gt; &lt;strong&gt;should be triggered by a user gesture.&lt;/strong&gt; Kicking off a huge download on page load is hostile. Gate it behind a click, and check &lt;code&gt;navigator.userActivation.isActive&lt;/code&gt; if you want to be strict.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Use case #1 — Summarizing user reviews
&lt;/h2&gt;

&lt;p&gt;Picture a product page with dozens of customer reviews. Nobody reads all of them. A classic use case: a "Summarize reviews" button that distills them into a few key points, computed entirely in the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  The summarizer's knobs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Summarizer.create()&lt;/code&gt; takes options that shape the output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;type&lt;/code&gt; — &lt;code&gt;"key-points"&lt;/code&gt; (default), &lt;code&gt;"tldr"&lt;/code&gt;, &lt;code&gt;"teaser"&lt;/code&gt;, or &lt;code&gt;"headline"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;format&lt;/code&gt; — &lt;code&gt;"markdown"&lt;/code&gt; (default) or &lt;code&gt;"plain-text"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;length&lt;/code&gt; — &lt;code&gt;"short"&lt;/code&gt; (default), &lt;code&gt;"medium"&lt;/code&gt;, or &lt;code&gt;"long"&lt;/code&gt;. What that means depends on the type: a short &lt;code&gt;key-points&lt;/code&gt; summary is 3 bullets; a short &lt;code&gt;tldr&lt;/code&gt; is one sentence.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sharedContext&lt;/code&gt; — background info that helps the model do a better job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Putting it together
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;summarizeReviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Feature detection — degrade gracefully if unsupported.&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="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Availability check.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;availability&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;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;availability&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;availability&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unavailable&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Create the summarizer, tuned for product reviews.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&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;Summarizer&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;sharedContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;These are customer product reviews from an e-commerce store. &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarize the overall sentiment and the points customers mention most.&lt;/span&gt;&lt;span class="dl"&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="s1"&gt;key-points&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;markdown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downloadprogress&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;updateProgressUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loaded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// your UI hook&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Summarize.&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="nx"&gt;summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Focus on product quality, value for money, and shipping.&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wiring it to a button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;summarizeBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// innerText, not innerHTML — feed the model clean text, not markup.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reviewsText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#reviews&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;summarizeBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;}&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;summarizeReviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&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="nx"&gt;supported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Progressive enhancement: fall back to your server, or just hide the feature.&lt;/span&gt;
    &lt;span class="nx"&gt;summaryEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Review summaries aren’t available on this device.&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;summaryEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;summarizeBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Feed it clean text.&lt;/strong&gt; Use &lt;code&gt;innerText&lt;/code&gt;, not &lt;code&gt;innerHTML&lt;/code&gt;. The model summarizes better when it isn't wading through &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; soup, and you don't waste the context window on markup. If you retrieve the feedback from an API, you can also give it the json directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Streaming for long input
&lt;/h3&gt;

&lt;p&gt;For a big block of text, waiting for the whole summary feels sluggish. Stream it instead and render tokens as they arrive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarizeStreaming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Focus on product quality and value for money.&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="nx"&gt;summaryEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&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;for&lt;/span&gt; &lt;span class="k"&gt;await &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;chunk&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;summaryEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// each chunk is an incremental piece of text&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A note on languages
&lt;/h3&gt;

&lt;p&gt;The Summarizer currently supports &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;ja&lt;/code&gt;, &lt;code&gt;es&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, and &lt;code&gt;fr&lt;/code&gt;. Declaring the languages up front lets the browser reject an unsupported combination early instead of failing mid-request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&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;Summarizer&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;key-points&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedInputLanguages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;outputLanguage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Use case #2 — From a product photo to structured JSON with the Prompt API
&lt;/h2&gt;

&lt;p&gt;The task APIs are great when your need fits one of them. But what about something bespoke — like: &lt;em&gt;"Here's a photo of a product. Generate a draft review and auto-fill the review form."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's a job for the &lt;strong&gt;Prompt API&lt;/strong&gt;, and it shows off two of its best features: &lt;strong&gt;multimodal input&lt;/strong&gt; (an image) and &lt;strong&gt;structured output&lt;/strong&gt; (a JSON schema).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Create a multimodal session
&lt;/h3&gt;

&lt;p&gt;Because we're sending an image, we declare &lt;code&gt;image&lt;/code&gt; (and &lt;code&gt;text&lt;/code&gt;) as expected inputs when creating the session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;LanguageModel&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;expectedInputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="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="s1"&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;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="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="s1"&gt;image&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;expectedOutputs&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="s1"&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;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="nf"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downloadprogress&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;updateProgressUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loaded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Define the shape you want back
&lt;/h3&gt;

&lt;p&gt;Prompting &lt;em&gt;"reply in JSON"&lt;/em&gt; and hoping for the best is a recipe for parse errors. Instead, pass a &lt;strong&gt;JSON Schema&lt;/strong&gt; via &lt;code&gt;responseConstraint&lt;/code&gt;, and the model is &lt;em&gt;constrained&lt;/em&gt; to produce output matching it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const reviewSchema = {
  title:       { type: 'string' },
  description: { type: 'string' },
  rating:      { type: 'number', minimum: 1, maximum: 5 },
  pros:        { type: 'array', items: { type: 'string' } },
  cons:        { type: 'array', items: { type: 'string' } },
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Step 3 — Prompt with text + image, constrained to the schema
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;content&lt;/code&gt; of a message is an array of typed parts. We combine a text instruction with the image, and pass the schema as a constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateReviewFromPhoto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="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="s1"&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="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="s1"&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are given a photo of a product. Write a short, honest &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
              &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer-style review of it. Return the result as JSON.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="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="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageElement&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;responseConstraint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reviewSchema&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Guaranteed to match the schema — safe to parse.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling it against an image on the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#product-photo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// an &amp;lt;img&amp;gt; element&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;review&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;generateReviewFromPhoto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   title: "Sleek and sturdy everyday mug",&lt;/span&gt;
&lt;span class="c1"&gt;//   description: "A clean matte-white ceramic mug that looks premium...",&lt;/span&gt;
&lt;span class="c1"&gt;//   rating: 4,&lt;/span&gt;
&lt;span class="c1"&gt;//   pros: ["Solid build", "Comfortable handle"],&lt;/span&gt;
&lt;span class="c1"&gt;//   cons: ["Slightly heavier than expected"]&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the output is schema-constrained, you skip the usual defensive gymnastics — no regex-scraping a JSON blob out of a chatty response, no &lt;code&gt;try/catch&lt;/code&gt; around a hopeful &lt;code&gt;JSON.parse&lt;/code&gt;. You get an object shaped exactly like your database row.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why the schema matters.&lt;/strong&gt; Without &lt;code&gt;responseConstraint&lt;/code&gt;, the model &lt;em&gt;might&lt;/em&gt; honor a "reply in JSON" instruction — most of the time. With it, the output is structurally guaranteed. In production, "most of the time" is where the 2 a.m. pages come from. Use the schema!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Reusing the session
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;LanguageModel&lt;/code&gt; session keeps context across prompts, so you can hold a conversation or run several images through the same instance. When you're done, free the resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Building responsibly: this is progressive enhancement
&lt;/h2&gt;

&lt;p&gt;The single most important design principle for built-in AI: &lt;strong&gt;treat it as an enhancement, never a hard dependency.&lt;/strong&gt; A big chunk of your users won't have it — wrong OS, mobile device, insufficient hardware, or the model simply hasn't downloaded yet.&lt;/p&gt;

&lt;p&gt;A robust feature does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSummarizer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarizer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;availability&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;Summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;availability&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;availability&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unavailable&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Summarizer&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="cm"&gt;/* … */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&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;getSummarizer&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;summarizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// On-device path: instant, private, free.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Fallback: call your server-side summarizer, or hide the feature entirely.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep these in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Desktop only, for now.&lt;/strong&gt; The foundation-model APIs (Prompt, Summarizer, Writer, Rewriter) don't run on Android or iOS yet. Translator and Language Detector are desktop-only too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real hardware requirements.&lt;/strong&gt; Roughly: &amp;gt;4 GB of GPU VRAM &lt;em&gt;or&lt;/em&gt; a decent CPU with 16 GB RAM, plus ~22 GB free disk for Chrome's profile volume. Not every laptop qualifies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first run is a download.&lt;/strong&gt; Multiple gigabytes. Always show progress, and never block first paint on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy is a genuine selling point.&lt;/strong&gt; Inference is local; user data never hits a server. For a reviews summarizer or a content classifier handling user text, that's a real advantage — say so.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When the browser can't: a cloud fallback
&lt;/h2&gt;

&lt;p&gt;Progressive enhancement is only half a strategy if the "else" branch just hides the feature. For a lot of products you want the capability to work &lt;em&gt;everywhere&lt;/em&gt; — on the mobile users, the older laptops, the browsers that haven't shipped these APIs yet — while still giving the on-device path to everyone who can use it.&lt;/p&gt;

&lt;p&gt;Think of it as a spectrum rather than a rivalry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-device (built-in AI):&lt;/strong&gt; free, private, offline-capable, zero infra. Ideal for lightweight, high-frequency, privacy-sensitive tasks — summarizing reviews, classifying text, drafting a first pass. The trade-off is a small model with a modest context window, and it's not on every device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud (a hosted model):&lt;/strong&gt; handles heavy reasoning, large context windows, and a specific frontier model when you need it — at the cost of latency, per-token pricing, and a backend to secure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nice part: your feature-detection branch is already the seam between them. Where the on-device path falls through, call your cloud model instead. Here's the same &lt;code&gt;getSummarizer()&lt;/code&gt; pattern with a fallback to &lt;a href="https://aws.amazon.com/bedrock/" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, which exposes many foundation models behind one API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client side — same seam as before, now with a real fallback.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizer&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;getSummarizer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;summary&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;summarizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// On-device path: instant, private, free.&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="nx"&gt;summarizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reviewsText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Cloud path: works on any device, at the cost of a network round-trip.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/summarize&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="nx"&gt;reviewsText&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;}&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server side (Node/Lambda) — a thin proxy to Amazon Bedrock.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ConverseCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/client-bedrock-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;response&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="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConverseCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anthropic.claude-3-5-haiku-20241022-v1:0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;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="s1"&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="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="s2"&gt;`Summarize these product reviews as key points:\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}));&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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&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="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same feature, two engines: the browser serves everyone it can for free, and the cloud quietly covers the rest. Keeping the API key server-side (never in the browser) is exactly the backend concern the on-device path lets you &lt;em&gt;skip&lt;/em&gt; for the majority of your users — so you pay for cloud inference only on the requests that genuinely need it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Disclosure:&lt;/strong&gt; I'm a Developer Advocate at AWS, so Bedrock is the cloud fallback I reach for. The pattern is vendor-neutral — any hosted model API slots into the same &lt;code&gt;else&lt;/code&gt; branch.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;Built-in AI shifts a whole class of features from "backend project with ongoing costs" to "a few dozen lines of client-side JavaScript." No key to rotate, no proxy to secure, no bill that scales with usage, and no user data leaving the device.&lt;/p&gt;

&lt;p&gt;It won't replace big cloud models for heavy reasoning, and it isn't everywhere yet. But for the everyday tasks that make up so much of real web work — summarize this, classify that, translate the other thing, extract structured data from a mess — the model is right there in the browser, waiting for a function call.&lt;/p&gt;




&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.chrome.com/docs/ai/" rel="noopener noreferrer"&gt;Chrome Built-in AI docs&lt;/a&gt; — start here&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Summarizer_API" rel="noopener noreferrer"&gt;Summarizer API doc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/ai/prompt-api" rel="noopener noreferrer"&gt;Prompt API guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.chrome.com/docs/ai/structured-output-for-prompt-api" rel="noopener noreferrer"&gt;Structured output with the Prompt API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Tip: install the &lt;code&gt;@types/dom-chromium-ai&lt;/code&gt; npm package for TypeScript typings.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>bedrock</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
