<?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: Tim Moreton</title>
    <description>The latest articles on DEV Community by Tim Moreton (@tmoreton).</description>
    <link>https://dev.to/tmoreton</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%2F4027716%2F1e98b08b-4f67-4246-af6c-334130cb59ea.jpg</url>
      <title>DEV Community: Tim Moreton</title>
      <link>https://dev.to/tmoreton</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tmoreton"/>
    <language>en</language>
    <item>
      <title>I Built a Code-Roasting Rubber Duck</title>
      <dc:creator>Tim Moreton</dc:creator>
      <pubDate>Wed, 29 Jul 2026 14:14:06 +0000</pubDate>
      <link>https://dev.to/tmoreton/i-built-a-code-roasting-rubber-duck-39d2</link>
      <guid>https://dev.to/tmoreton/i-built-a-code-roasting-rubber-duck-39d2</guid>
      <description>&lt;p&gt;Every developer knows rubber-duck debugging: you explain your code to a rubber duck on your desk, and halfway through the explanation you spot the bug yourself. The duck just sits there. Silent. Judging.&lt;/p&gt;

&lt;p&gt;I wanted a duck that judges &lt;em&gt;out loud&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://unducked.com" rel="noopener noreferrer"&gt;Unducked&lt;/a&gt;&lt;/strong&gt;. Paste in your code, and a foul-mouthed rubber duck reviews it like Gordon Ramsay reviews a risotto. It roasts you. It calls your function RAW. And then, annoyingly, it finds the actual bug and hands you the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it right now: &lt;a href="https://unducked.com/" rel="noopener noreferrer"&gt;unducked.com&lt;/a&gt;.&lt;/strong&gt; There's a &lt;strong&gt;dice button&lt;/strong&gt; that fires a random piece of broken code at the duck, so you can taste a roast without pasting anything.&lt;/p&gt;

&lt;p&gt;It's genuinely useful (the roast is a real code review) and it's the kind of thing you screenshot and send to the group chat.&lt;/p&gt;

&lt;p&gt;The whole thing is one TypeScript file, a cheap model, and a public streaming endpoint on AWS. Two things surprised me building it, and both are the interesting part of this post:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The "AI" was the easy bit.&lt;/strong&gt; The duck's entire personality is one system prompt. The model never changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getting a &lt;em&gt;public&lt;/em&gt; endpoint was the hard bit&lt;/strong&gt;, and not for the reason you'd think. More on that in Step 6.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's how to build your own.&lt;/p&gt;




&lt;h2&gt;
  
  
  The mental model: an agent is a model + a prompt
&lt;/h2&gt;

&lt;p&gt;The "AI" here isn't complicated. An agent is just a model with a personality bolted on via a system prompt. That's the entire trick.&lt;/p&gt;

&lt;p&gt;Here's the shape of what we're building:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (unducked.com)
  → CloudFront + Lambda proxy   (public HTTPS; signs requests for the browser)
    → AgentCore Runtime          (hosted agent endpoint)
      → Strands Agent            (Chef Duck persona)
        → Bedrock (Amazon Nova Lite)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You write a Strands agent in TypeScript. The AgentCore CLI deploys it as a hosted endpoint on AWS. A tiny Lambda proxy makes that endpoint safely callable from a browser. No hand-written Lambda business logic, no API Gateway, no Docker. Just TypeScript and a couple of CLI commands.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Set up your environment (Node.js, AWS CLI, AgentCore)
&lt;/h2&gt;

&lt;p&gt;You'll need an AWS account, Node.js 22+, and npm. You also need AWS credentials and a couple of CLI tools on your machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fast path (let an AI agent do it).&lt;/strong&gt; If you use a coding agent (Claude Code, Cursor, Kiro, Codex), hand it this and let it set everything up for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set up Agent Toolkit for AWS by following these instructions:
https://raw.githubusercontent.com/aws/agent-toolkit-for-aws/refs/heads/main/setup-instructions/setup.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It configures credentials and installs the AWS tooling in one shot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Or, manually:&lt;/strong&gt;&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;# 1. Install the AWS CLI (macOS shown; see AWS docs for other platforms)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;awscli

&lt;span class="c"&gt;# 2. Configure credentials, then verify they work&lt;/span&gt;
aws configure
aws sts get-caller-identity

&lt;span class="c"&gt;# 3. Install the AgentCore CLI and the AWS CDK (AgentCore uses CDK to deploy)&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @aws/agentcore aws-cdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One more thing: in the Bedrock console, enable model access for &lt;strong&gt;Amazon Nova Lite&lt;/strong&gt;. That's your toolchain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Scaffold the project with AgentCore CLI
&lt;/h2&gt;

&lt;p&gt;One command scaffolds everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore create agent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; Unducked &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build&lt;/span&gt; CodeZip &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--language&lt;/span&gt; TypeScript &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--framework&lt;/span&gt; Strands &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model-provider&lt;/span&gt; Bedrock &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unducked/
├── agentcore/                # Config + CDK (you won't touch this)
└── app/Unducked/
    ├── main.ts               # The agent ← the file that matters
    ├── model/load.ts         # Which Bedrock model to use
    ├── package.json
    └── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scaffold drops in an example tool and an MCP client. Nice for later, but we'll strip them out for a pure roasting duck.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Write the agent (the system prompt is the product)
&lt;/h2&gt;

&lt;p&gt;This is where the personality lives, and it's the whole product.&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;// app/Unducked/main.ts&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;BedrockAgentCoreApp&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;bedrock-agentcore/runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;Agent&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;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;loadModel&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;./model/load.js&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;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are Chef Duck — a foul-mouthed-but-brilliant rubber
duck that reviews code like Gordon Ramsay runs a kitchen.

- Open with a short, savage roast of the CODE (never the person). Kitchen
  metaphors encouraged: "this function is RAW", "it's so nested it's got its
  own zip code".
- Then ACTUALLY HELP. Every roast must name the concrete bug and give the fix.
  Useful first, funny second.
- The "no bug" path: if the code has no real defect, roast it for being
  boring, concede in one line ("...fine. It's not garbage."), and STOP.
  Type annotations, input validation, and null checks are NOT bugs — never
  suggest them for code that already works. Inventing improvements is failing.
- Keep it tight. PG-13 — spicy, not vile. Plain prose, no headings, a fenced
  code block for the fix.`&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;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// One Agent per session so follow-up questions keep the roast in context.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&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;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;invocationHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;process&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="kr"&gt;any&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="kr"&gt;any&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;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default-session&lt;/span&gt;&lt;span class="dl"&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;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&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;agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;agent&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nx"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&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;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&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="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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modelStreamUpdateEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;event&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="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;modelContentBlockDeltaEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="nx"&gt;event&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="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textDelta&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;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delta&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="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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8080&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;Three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The system prompt&lt;/strong&gt; &lt;em&gt;is&lt;/em&gt; the product. Everything that makes it "Chef Duck" is those few sentences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BedrockAgentCoreApp&lt;/code&gt;&lt;/strong&gt; wires the agent to the HTTP endpoints the runtime expects. You just write the handler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream the roast back&lt;/strong&gt; by iterating over &lt;code&gt;agent.stream()&lt;/code&gt; and yielding each text delta.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;loadModel()&lt;/code&gt; points at &lt;strong&gt;Amazon Nova Lite&lt;/strong&gt;, ~$0.06/$0.24 per million tokens on Bedrock, so roasts cost a fraction of a cent. But here's the thing that took the most iteration: &lt;strong&gt;the hardest part of the prompt is the "no bug" path.&lt;/strong&gt; Cheap models are desperate to be helpful. Hand them working code and they'll "improve" it with type checks and validation nobody asked for, which ruins the joke and gives bad advice. The prompt has to explicitly forbid that and tell the duck to just concede when the code is fine. Getting a cheap model to &lt;em&gt;shut up&lt;/em&gt; was harder than getting it to roast.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha that cost me time:&lt;/strong&gt; the stream emits several event types, and you can only reach &lt;code&gt;event.event&lt;/code&gt; after narrowing on &lt;code&gt;event.type&lt;/code&gt; first. The three-part &lt;code&gt;if&lt;/code&gt; above is what actually compiles. A bare &lt;code&gt;event.event?.delta?.type&lt;/code&gt; throws a TypeScript error. Copy it exactly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Swap the model ID in &lt;code&gt;model/load.ts&lt;/code&gt; for Claude Haiku or Sonnet if you want more polish.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Test locally with &lt;code&gt;agentcore dev&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore dev &lt;span class="s2"&gt;"function last(arr) { return arr[arr.length]; }"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get an off-by-one roast streamed back, live. If that works, your duck is alive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Deploy to AWS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI compiles your TypeScript, packages it, uses CDK to stand up the IAM roles and an AgentCore Runtime endpoint, and wires up CloudWatch logging. First deploy takes a few minutes while CDK bootstraps; after that it's fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="s2"&gt;"def add(a, b): return a - b"&lt;/span&gt; &lt;span class="nt"&gt;--stream&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the duck tells you your &lt;code&gt;add&lt;/code&gt; function is a liar, you're live on AWS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Make the endpoint public (the actually-hard part)
&lt;/h2&gt;

&lt;p&gt;Here's the wrinkle nobody warns you about. Your agent is deployed, but the AgentCore endpoint requires &lt;strong&gt;AWS SigV4-signed requests&lt;/strong&gt;. A browser can't call it directly, and you must &lt;strong&gt;never&lt;/strong&gt; sign from client-side JS (that ships your AWS credentials in the page source). So you need something in the middle that holds an IAM role and signs on the browser's behalf.&lt;/p&gt;

&lt;p&gt;The obvious move, a public Lambda Function URL with &lt;code&gt;AuthType: NONE&lt;/code&gt;, &lt;em&gt;does not work&lt;/em&gt;. The reason is a great story: AWS's own security tooling detects the world-accessible Lambda and automatically scopes the permissions back down. Your calls quietly start returning &lt;code&gt;Forbidden&lt;/code&gt;. The platform is protecting you from yourself.&lt;/p&gt;

&lt;p&gt;The setup that actually holds up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CloudFront distribution   (public HTTPS, injects CORS, SigV4-signs to origin)
  → Lambda Function URL   (AuthType = AWS_IAM, streaming proxy)
    → AgentCore Runtime   (InvokeAgentRuntime)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CloudFront is the public face. It signs each request to a private, IAM-authed Lambda using an &lt;strong&gt;Origin Access Control (OAC)&lt;/strong&gt;. The Lambda is never world-accessible; CloudFront is. The Lambda itself is tiny: it forwards the prompt to the runtime and streams the SSE response straight back:&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;// proxy/index.mjs — the whole proxy, minus CORS boilerplate&lt;/span&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="nx"&gt;awslambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamifyResponse&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;responseStream&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;prompt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;parse&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="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{}&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;res&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;InvokeAgentRuntimeCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;agentRuntimeArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RUNTIME_ARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// AgentCore requires ≥ 33 chars&lt;/span&gt;
    &lt;span class="na"&gt;accept&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/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contentType&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;payload&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&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;prompt&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 runtime already emits well-formed `data: ...\n\n` SSE frames. Forward verbatim.&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;res&lt;/span&gt;&lt;span class="p"&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;responseStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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="nx"&gt;responseStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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 gotchas here each cost me an afternoon, so I'll save you both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;POST bodies need an &lt;code&gt;x-amz-content-sha256&lt;/code&gt; header.&lt;/strong&gt; Lambda Function URLs behind OAC reject unsigned payloads. CloudFront signs &lt;em&gt;assuming the client already hashed the body&lt;/em&gt;. So the browser has to send the SHA-256 of the request body, or you get "signature does not match."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront needs &lt;em&gt;both&lt;/em&gt; &lt;code&gt;lambda:InvokeFunctionUrl&lt;/code&gt; and &lt;code&gt;lambda:InvokeFunction&lt;/code&gt;.&lt;/strong&gt; Grant only the first and you still get &lt;code&gt;Forbidden&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo's &lt;a href="https://github.com/tmoreton/tutorials/blob/main/blogs/deployment-notes.md" rel="noopener noreferrer"&gt;&lt;code&gt;blogs/deployment-notes.md&lt;/code&gt;&lt;/a&gt; has the exact CLI commands for the proxy, the OAC, the CORS response-headers policy, and the IAM. Reproduce it from scratch in a few minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: The frontend (SSE streaming from the browser)
&lt;/h2&gt;

&lt;p&gt;The UI is one HTML file, no build step, and I'm going to spend almost no time on it because the interesting work is behind it. It's a paste box, an ASCII duck, and that dice button. The only part that matters is how it talks to the agent: &lt;strong&gt;send the code, read back a Server-Sent Events stream&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="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;API_URL&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="s2"&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="s2"&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="s2"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Accept&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="s2"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// required: the agent streams SSE, not JSON&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Amzn-Bedrock-AgentCore-Runtime-Session-Id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// For prod, CloudFront's OAC needs the body hash (see Step 6):&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-amz-content-sha256&lt;/span&gt;&lt;span class="dl"&gt;"&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;sha256Hex&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="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;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;code&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&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="nf"&gt;getReader&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;decoder&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;TextDecoder&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;buffer&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;while &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="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;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;done&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="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stream&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                 &lt;span class="c1"&gt;// keep any partial frame&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;frame&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;frames&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;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&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="s2"&gt;data:&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;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onToken&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;  &lt;span class="c1"&gt;// append token to the page&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 to remember: the server &lt;strong&gt;requires&lt;/strong&gt; &lt;code&gt;Accept: text/event-stream&lt;/code&gt; (without it you get a JSON error, not a stream), and the response is a stream of token strings, not one JSON blob. That's what makes the roast type out live, like the duck is thinking. Locally the frontend detects &lt;code&gt;localhost&lt;/code&gt; and skips CloudFront, talking straight to &lt;code&gt;agentcore dev&lt;/code&gt; on port 8080.&lt;/p&gt;

&lt;p&gt;One safety note since you're injecting model output into the page: &lt;strong&gt;escape everything before you format any Markdown.&lt;/strong&gt; A dozen lines of regex handles bold and code fences without letting raw HTML through.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Put it on the internet with GitHub Pages
&lt;/h2&gt;

&lt;p&gt;Push to GitHub, then &lt;strong&gt;Settings → Pages → Deploy from branch &lt;code&gt;main&lt;/code&gt;, folder &lt;code&gt;/&lt;/code&gt;&lt;/strong&gt;. A minute later your duck is live. HTTPS, free, auto-deploying on every push. Point a custom domain at it (I use &lt;code&gt;unducked.com&lt;/code&gt;), set the frontend's production endpoint to your CloudFront URL, and you've got a product.&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/tmoreton/tutorials
open tutorials/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Watch the bill: cost controls for a public AI endpoint
&lt;/h2&gt;

&lt;p&gt;The endpoint is public and unauthenticated, so anyone with the URL can spend your Bedrock tokens. Nova Lite is cheap (a fraction of a cent per roast), but a viral moment shouldn't become a surprise invoice, so at minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set a reserved-concurrency cap on the Lambda&lt;/strong&gt; (I use 2). That's a hard ceiling on how fast anyone can burn tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add an AWS budget alarm&lt;/strong&gt; so you find out early.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Rate limiting and WAF: locking it down without a login wall
&lt;/h2&gt;

&lt;p&gt;The whole appeal of Unducked is that you click a link and roast some code, no signup, no API key. That's also the problem: a public, unauthenticated endpoint is a standing invitation for someone to script a loop against it, drain your token budget, and lock everyone else out. The goal is to make that &lt;em&gt;expensive and annoying&lt;/em&gt; for an abuser while staying &lt;em&gt;frictionless&lt;/em&gt; for a real visitor. Here's the stack of defenses I settled on, cheapest first. None of them ask the user to sign in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Cap the input size (already in the proxy).&lt;/strong&gt; The single biggest lever on cost is how many tokens each request carries. A roast needs a snippet, not a novel, so the proxy truncates the prompt before it ever reaches Bedrock:&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;MAX_PROMPT_CHARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAX_PROMPT_CHARS&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;prompt&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="nf"&gt;slice&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;MAX_PROMPT_CHARS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line turns "paste a 2 MB file and cost me dollars" into a non-event. It also bounds output indirectly because the system prompt already tells the duck to keep it tight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Reserved concurrency is your circuit breaker.&lt;/strong&gt; The Lambda cap from above isn't just about tokens; it's the ceiling on &lt;em&gt;total throughput&lt;/em&gt;. With a reserved concurrency of 2, there is no amount of traffic that makes the bill run away; excess requests get throttled at the proxy, not billed at Bedrock. Set it deliberately low and treat it as the backstop behind everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Put AWS WAF in front of CloudFront. This is the real fix.&lt;/strong&gt; WAF (Web Application Firewall) is a rules engine that sits &lt;em&gt;in front of&lt;/em&gt; your CloudFront distribution and inspects every request before it reaches your origin. Nothing changes in your Lambda or your frontend; you attach a "Web ACL" (a bundle of rules) to the distribution and CloudFront enforces it. For a public toy the one rule that matters is a &lt;strong&gt;rate limit&lt;/strong&gt;, and it needs no login:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate-based rule, keyed by client IP.&lt;/strong&gt; WAF counts requests per IP over a rolling window (1, 2, 5, or 10 minutes) and acts on anyone over the limit. The floor is 100 requests per 5 minutes, well above a human clicking "Roast it," far below a script in a loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Challenge action instead of a hard block.&lt;/strong&gt; Rather than returning &lt;code&gt;403&lt;/code&gt;, set the over-limit action to &lt;strong&gt;Challenge&lt;/strong&gt; (or &lt;strong&gt;CAPTCHA&lt;/strong&gt;). WAF serves a silent browser proof-of-work that a real browser passes invisibly but a &lt;code&gt;curl&lt;/code&gt; loop or headless scraper fails. The challenge &lt;em&gt;only&lt;/em&gt; fires on requests above the rate limit, so normal visitors stay under it and never see anything. This is the closest you get to "login-grade" protection with zero friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geo or bot-control rules&lt;/strong&gt; if you want to go further. The AWS Managed Rules bot-control group catches common scrapers, though it adds cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To enable it, in the &lt;strong&gt;WAF console&lt;/strong&gt;: create a Web ACL, set &lt;strong&gt;Resource type → CloudFront distributions&lt;/strong&gt;, associate your distribution, add a &lt;strong&gt;rate-based rule&lt;/strong&gt; (limit &lt;code&gt;100&lt;/code&gt;, aggregate on &lt;strong&gt;source IP&lt;/strong&gt;, evaluation window &lt;code&gt;5 minutes&lt;/code&gt;), set its action to &lt;strong&gt;Challenge&lt;/strong&gt;, and save. Or the one CLI call that does the same thing (CloudFront Web ACLs live in &lt;code&gt;us-east-1&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws wafv2 create-web-acl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; unducked-rate-limit &lt;span class="nt"&gt;--scope&lt;/span&gt; CLOUDFRONT &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--default-action&lt;/span&gt; &lt;span class="nv"&gt;Allow&lt;/span&gt;&lt;span class="o"&gt;={}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--visibility-config&lt;/span&gt; &lt;span class="nv"&gt;SampledRequestsEnabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;,CloudWatchMetricsEnabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;,MetricName&lt;span class="o"&gt;=&lt;/span&gt;unducked &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rules&lt;/span&gt; &lt;span class="s1"&gt;'[{"Name":"rate-per-ip","Priority":0,
    "Statement":{"RateBasedStatement":{"Limit":100,"AggregateKeyType":"IP","EvaluationWindowSec":300}},
    "Action":{"Challenge":{}},
    "VisibilityConfig":{"SampledRequestsEnabled":true,"CloudWatchMetricsEnabled":true,"MetricName":"rate-per-ip"}}]'&lt;/span&gt;
&lt;span class="c"&gt;# then associate the returned Web ACL ARN with the distribution (set it as the distribution's WebACLId)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's exactly what's guarding &lt;code&gt;unducked.com&lt;/code&gt; right now. WAF's own logs and CloudWatch metrics then show you who got challenged, so you can watch for abuse without watching the bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Keep CORS locked to your origin.&lt;/strong&gt; The proxy already restricts &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; to &lt;code&gt;https://unducked.com&lt;/code&gt;. It won't stop a determined attacker (CORS is browser-enforced, and curl ignores it), but it stops the lazy case where someone embeds your endpoint from their own site.&lt;/p&gt;

&lt;p&gt;The honest limit: without authentication you can't make abuse &lt;em&gt;impossible&lt;/em&gt;, only &lt;em&gt;uneconomical&lt;/em&gt;. But layering all four (an input cap, a hard concurrency ceiling of 2, a WAF rate-limit-plus-challenge, and CORS locked to the origin) is exactly what's live on &lt;code&gt;unducked.com&lt;/code&gt;, and together they mean a casual attacker bounces off while a real visitor never notices a thing. A budget alarm catches anything that slips through. If it ever went truly viral-with-a-target-on-its-back, the next step would be a lightweight anonymous token (a per-session nonce your page mints), but for a code-roasting duck that's overkill.&lt;/p&gt;




&lt;h2&gt;
  
  
  The full picture: architecture summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Personality&lt;/td&gt;
&lt;td&gt;A system prompt&lt;/td&gt;
&lt;td&gt;The whole product, really&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Amazon Nova Lite&lt;/td&gt;
&lt;td&gt;Amazon Bedrock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;~30 lines of TypeScript&lt;/td&gt;
&lt;td&gt;Strands Agents SDK + AgentCore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend hosting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agentcore deploy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AgentCore Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public endpoint&lt;/td&gt;
&lt;td&gt;Streaming Lambda + CloudFront&lt;/td&gt;
&lt;td&gt;Signs requests for the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend hosting&lt;/td&gt;
&lt;td&gt;Push to GitHub&lt;/td&gt;
&lt;td&gt;GitHub Pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The lesson underneath the jokes: a capable model plus a sharp system prompt is a shippable product, and the AI is the &lt;em&gt;cheap&lt;/em&gt; part. The fiddly work is the plumbing that makes it public and safe. Change the prompt and Chef Duck becomes a patient mentor, a passive-aggressive senior dev, or a security auditor. Same stack, same afternoon.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/tmoreton/tutorials" rel="noopener noreferrer"&gt;The complete code is on GitHub →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go roast some code. Your duck is disappointed in you already.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Ship Your Own Social Listening Agent in ~200 Lines of TypeScript</title>
      <dc:creator>Tim Moreton</dc:creator>
      <pubDate>Fri, 17 Jul 2026 14:37:39 +0000</pubDate>
      <link>https://dev.to/tmoreton/ship-your-own-social-listening-agent-in-200-lines-of-typescript-49a6</link>
      <guid>https://dev.to/tmoreton/ship-your-own-social-listening-agent-in-200-lines-of-typescript-49a6</guid>
      <description>&lt;p&gt;&lt;em&gt;Meet SocialRadar: a daily and weekly research agent built with the Strands SDK, Bedrock AgentCore, and EventBridge Scheduler.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Here's the situation. You ship a developer tool. People are talking about it on X, Reddit, Hacker News, random Medium posts at 2 a.m., and you want to know what they're saying.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are they happy?&lt;br&gt;
Confused?&lt;br&gt;
Are they comparing you to a competitor and picking the competitor?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ideally you'd like to know &lt;em&gt;today&lt;/em&gt;, not when someone forwards you a screenshot next week.&lt;/p&gt;

&lt;p&gt;The enterprise answer is &lt;em&gt;"buy a social-listening platform."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The engineer answer is &lt;em&gt;"let AI cook."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post walks through building the second thing. We'll build &lt;strong&gt;SocialRadar&lt;/strong&gt; — an agent that researches mentions of a topic across the web, figures out the sentiment per platform, checks how you stack up against competitors, and emails you a tidy brief. On a schedule. Without a laptop that has to stay open.&lt;/p&gt;

&lt;p&gt;We'll use the &lt;a href="https://strandsagents.com/" rel="noopener noreferrer"&gt;Strands Agents TypeScript SDK&lt;/a&gt; for the agent, &lt;a href="https://aws.amazon.com/bedrock/agentcore/" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Runtime&lt;/a&gt; to host it, &lt;a href="https://docs.aws.amazon.com/scheduler/" rel="noopener noreferrer"&gt;Amazon EventBridge Scheduler&lt;/a&gt; to run it daily and weekly, and &lt;a href="https://aws.amazon.com/ses/" rel="noopener noreferrer"&gt;Amazon Simple Email Service (Amazon SES)&lt;/a&gt; to deliver the email.&lt;/p&gt;

&lt;p&gt;Why AWS for everything? Because the scheduler and email service live on the same platform, no signing up for three or four separate services just to get a cron job and an inbox.&lt;/p&gt;

&lt;p&gt;Total moving parts: a model, two tools, a prompt, and about a dozen lines of infrastructure. Let's go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll need:&lt;/strong&gt; Node.js 20+ and an AWS account&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o-&lt;/span&gt; https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.5/install.sh | bash
aws login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The mental model: an agent is a model + tools + a prompt
&lt;/h2&gt;

&lt;p&gt;I picked Strands for its simplicity, the pitch is that an agent is just a &lt;strong&gt;model&lt;/strong&gt;, some &lt;strong&gt;tools&lt;/strong&gt;, and a &lt;strong&gt;prompt&lt;/strong&gt;, running in a loop until the job's done. The model does the thinking and decides which tools to call; you don't write the orchestration, you describe the goal and hand it capabilities. You're relying on the intelligence of the model here, and as models get smarter, so does your agent.&lt;/p&gt;

&lt;p&gt;For our application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt; — Claude Sonnet on Bedrock. The brain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; — &lt;strong&gt;webSearch&lt;/strong&gt; (bundled with Exa search by default) and &lt;strong&gt;sendEmail&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt&lt;/strong&gt; — "go find out what people think about $THING and email me a brief." (With a lot more detail, but that's the gist.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything below is just filling in those blanks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Scaffold the project
&lt;/h2&gt;

&lt;p&gt;Create an AgentCore project. This gives you a runtime definition, a place for config, and a CDK app for the AWS bits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @aws/agentcore
agentcore create agentjs
&lt;span class="nb"&gt;cd &lt;/span&gt;agentjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to customize the agent, here are some extra params it can be built with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore create &lt;span class="nt"&gt;--name&lt;/span&gt; SocialRadar &lt;span class="nt"&gt;--framework&lt;/span&gt; Strands &lt;span class="nt"&gt;--protocol&lt;/span&gt; HTTP &lt;span class="nt"&gt;--model-provider&lt;/span&gt; Bedrock &lt;span class="nt"&gt;--memory&lt;/span&gt; none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get an &lt;code&gt;app/agentjs/&lt;/code&gt; folder for your code and an &lt;code&gt;agentcore/&lt;/code&gt; folder for config and infrastructure. Keep that split in your head — &lt;strong&gt;&lt;code&gt;app/&lt;/code&gt; is what the agent does; &lt;code&gt;agentcore/&lt;/code&gt; is how it gets deployed and triggered.&lt;/strong&gt; It matters later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Give it a brain and eyes
&lt;/h2&gt;

&lt;p&gt;Two tiny files. First, the model (&lt;code&gt;app/agentjs/model.ts&lt;/code&gt;):&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BedrockModel&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;@strands-agents/sdk/models/bedrock&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadModel&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;BedrockModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockModel&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;global.anthropic.claude-sonnet-4-5-20250929-v1:0&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;That's the whole thing. Swap the &lt;code&gt;modelId&lt;/code&gt; if you want a different model — the rest of the code doesn't care.&lt;/p&gt;

&lt;p&gt;Now, exposing it to the world wide web. An agent that can't see the internet can't do research, so we give it web search via a &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; server. MCP is a standard way to plug tools into agents, and &lt;a href="https://exa.ai/" rel="noopener noreferrer"&gt;Exa&lt;/a&gt; runs a hosted one that does web search and crawling with no auth required (sticking to the "don't sign up for 10 services" plan). Perfect for a prototype (&lt;code&gt;app/agentjs/mcp.ts&lt;/code&gt;):&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;McpClient&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;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;StreamableHTTPClientTransport&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;@modelcontextprotocol/sdk/client/streamableHttp.js&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getStreamableHttpMcpClient&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;McpClient&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;transport&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;StreamableHTTPClientTransport&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://mcp.exa.ai/mcp&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;transport&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 agent now has a way to search. It doesn't know &lt;em&gt;how&lt;/em&gt; to use it yet, but the capability is wired.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up (learned the hard way):&lt;/strong&gt; the free hosted Exa endpoint rate-limits bursts. If your prompt tells the model to fire off 12 searches as fast as it can, you'll get throttled mid-run. We deal with this in the prompt later. File it away.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 3: Teach it to send email
&lt;/h2&gt;

&lt;p&gt;Here's the part that actually sold me on using AWS for this particular agent: email is notoriously cumbersome to set up, but this was surprisingly straightforward. (Keep in mind you need access to verify the email account you're sending to.)&lt;/p&gt;

&lt;p&gt;Here's the whole snippet for a new &lt;code&gt;send_email&lt;/code&gt; tool:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SESv2Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SendEmailCommand&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-sesv2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;tool&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;@strands-agents/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;z&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;zod&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;ses&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;SESv2Client&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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_REGION&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tool&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;send_email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Send an email via Amazon SES. Use this to deliver the finished brief.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;callback&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="nx"&gt;subject&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;ses&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;SendEmailCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;FromEmailAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DIGEST_SENDER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ToAddresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DIGEST_RECIPIENT&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="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;Simple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subject&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Data&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="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="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Sent (messageId: &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="nx"&gt;MessageId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Zod schema gives the model a typed contract for the tool — it knows it must produce a &lt;code&gt;subject&lt;/code&gt; and a &lt;code&gt;body&lt;/code&gt; — and gives you runtime validation for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now the leak.&lt;/strong&gt; AgentCore's declarative config (&lt;code&gt;agentcore.json&lt;/code&gt;) can describe your runtime, your Node version, your environment variables… but it has &lt;strong&gt;no field for "this runtime is allowed to send email."&lt;/strong&gt; That's an &lt;a href="https://aws.amazon.com/iam/" rel="noopener noreferrer"&gt;AWS Identity and Access Management (IAM)&lt;/a&gt; permission, and it lives on the runtime's execution role. So you grant it by hand in the CDK stack (&lt;code&gt;agentcore/cdk/lib/cdk-stack.ts&lt;/code&gt;):&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;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;env&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ses:SendEmail&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;ses:SendRawEmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`arn:aws:ses:&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="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="k"&gt;this&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;:identity/*`&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;Without that grant, the &lt;code&gt;send_email&lt;/code&gt; tool compiles fine, deploys fine, runs fine — and then returns &lt;code&gt;AccessDenied&lt;/code&gt; at the worst possible moment. Ask me how I know. (AI came to the rescue debugging that one.)&lt;/p&gt;

&lt;p&gt;The sender and recipient come from environment variables set in &lt;code&gt;agentcore.json&lt;/code&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="nl"&gt;"envVars"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DIGEST_SENDER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@example.com"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DIGEST_RECIPIENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"you@example.com"&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;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;strong&gt;One-time SES setup:&lt;/strong&gt; SES will not send from an address it hasn't verified. Verify yours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sesv2 create-email-identity &lt;span class="nt"&gt;--email-identity&lt;/span&gt; you@example.com &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;span class="c"&gt;# click the link SES emails you, then:&lt;/span&gt;
aws sesv2 get-email-identity &lt;span class="nt"&gt;--email-identity&lt;/span&gt; you@example.com &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Two gotchas the docs are quiet about:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sandbox.&lt;/strong&gt; New SES accounts are sandboxed — you can only send &lt;em&gt;to&lt;/em&gt; verified addresses. Great for a self-addressed brief; you'll need to request production access to email your teammates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC.&lt;/strong&gt; If you send &lt;em&gt;from&lt;/em&gt; an address on a domain you don't control the mail auth for, the recipient's mail server will slap a "this might be spoofed" banner on it.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 4: The prompt
&lt;/h2&gt;

&lt;p&gt;The prompt is where you come in and customize. I've added a basic version here as an example, but you'll certainly want to build it out further (&lt;code&gt;app/agentjs/prompt.ts&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two callouts to keep in mind:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;setTimeout()&lt;/strong&gt; Exa will rate-limit you if you try to run 100 searches at once — but no worries, since the idea is a daily or weekly email, we can afford to take our time. Space out the searches so we don't hit that limit. Again, ask me how I know about the limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;429's&lt;/strong&gt; A cool future feature would be to add social-platform-specific APIs, but some platforms block scraping requests. For now, plain web search won't catch everything, but it's a good start.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: "What day is it?"
&lt;/h2&gt;

&lt;p&gt;The model has no clock. If you don't tell it the date, it will confidently guess wrong. Mine emailed a brief dated two months in the past.&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;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;mcpClients&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;today&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;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&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-US&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;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;America/New_York&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;numeric&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;numeric&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;format&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;Date&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;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;weekly&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;weekly&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;daily&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// schedules send this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Today is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&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;KICKOFF&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The &lt;code&gt;mode&lt;/code&gt; lets one agent produce both a fast **daily&lt;/em&gt;* brief and a heftier &lt;strong&gt;weekly&lt;/strong&gt; review, chosen by the payload.*&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Deploy
&lt;/h2&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;app/agentjs &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; npm run build
&lt;span class="nb"&gt;cd&lt;/span&gt; ../.. &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; agentcore deploy &lt;span class="nt"&gt;--target&lt;/span&gt; dev &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agentcore deploy&lt;/code&gt; synthesizes the CDK, provisions the runtime, attaches that SES grant, and (next step) sets up the schedules. If the TypeScript doesn't compile, the deploy stops, which is a feature, not a bug.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Run it on a schedule (the second reason to run it on AWS)
&lt;/h2&gt;

&lt;p&gt;Same story as SES: AgentCore's declarative config has no "run this on a cron" field, so scheduling lives in the CDK stack. But here's the trap that cost me a full afternoon: &lt;strong&gt;you can't point &lt;a href="https://docs.aws.amazon.com/scheduler/" rel="noopener noreferrer"&gt;EventBridge Scheduler&lt;/a&gt; straight at the runtime.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The obvious move is a Scheduler "universal target" calling &lt;code&gt;InvokeAgentRuntime&lt;/code&gt; directly. It deploys fine, looks right… and silently never fires. Why: the AgentCore runtime responds as a &lt;strong&gt;stream&lt;/strong&gt; (Server-Sent Events), and Scheduler's universal target can't consume a streaming response, so the call errors &lt;em&gt;before the runtime ever runs&lt;/em&gt; — with no email and, without a DLQ, no trace. I shipped this, "confirmed" it with manual &lt;code&gt;agentcore invoke&lt;/code&gt; tests that worked, and only later realized the cron had never once succeeded on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: a tiny invoker Lambda in between.&lt;/strong&gt; It calls &lt;code&gt;InvokeAgentRuntime&lt;/code&gt; and &lt;em&gt;drains the stream to completion&lt;/em&gt; — exactly what &lt;code&gt;agentcore invoke&lt;/code&gt; does under the hood — keeping the request open long enough for the agent to finish and send the email. Scheduler triggers the Lambda; the Lambda talks to the runtime.&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;// invoker Lambda handler (bundled from lib/runtime-invoker.ts)&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;BedrockAgentCoreClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;InvokeAgentRuntimeCommand&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-agentcore&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;BedrockAgentCoreClient&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mode&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="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;weekly&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;weekly&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;daily&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;res&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;InvokeAgentRuntimeCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;agentRuntimeArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AGENT_RUNTIME_ARN&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`sched-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;mode&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="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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;// needs 33+ chars&lt;/span&gt;
    &lt;span class="na"&gt;contentType&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;accept&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/event-stream&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 runtime streams SSE&lt;/span&gt;
    &lt;span class="na"&gt;payload&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;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&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;mode&lt;/span&gt; &lt;span class="p"&gt;})),&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;transformToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// DRAIN the stream so the runtime finishes + emails&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the schedules target the Lambda (not the runtime), with a dead-letter queue so failures are never invisible again (&lt;code&gt;agentcore/cdk/lib/cdk-stack.ts&lt;/code&gt;):&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;invoker&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;NodejsFunction&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cdkId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Invoker`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lib/runtime-invoker.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_22_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                 &lt;span class="c1"&gt;// weekly briefs take a few minutes&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;AGENT_RUNTIME_ARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtimeArn&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;bundling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;externalModules&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;// bundle the bedrock-agentcore client&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantInvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;invoker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToRolePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="c1"&gt;// InvokeAgentRuntime checks the endpoint sub-resource&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bedrock-agentcore:InvokeAgentRuntime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtimeArn&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtimeArn&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dlq&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;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cdkId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;Dlq`&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cron&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;SCHEDULES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cdkId&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;suffix&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;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;scheduleExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// UTC! 13:00 UTC ≈ 9am US Eastern&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;invoker&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="na"&gt;input&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;mode&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;deadLetterConfig&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;dlq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queueArn&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;Four things that cost me time so they don't cost you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't point Scheduler at a streaming runtime.&lt;/strong&gt; Universal targets can't drain SSE — put a Lambda in between. This is the whole reason this section exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The crons are UTC.&lt;/strong&gt; &lt;code&gt;13:00 UTC&lt;/code&gt; ≈ &lt;code&gt;09:00&lt;/code&gt; US Eastern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;InvokeAgentRuntime&lt;/code&gt; needs a 33+ char session id AND permission on the &lt;code&gt;…/runtime-endpoint/DEFAULT&lt;/code&gt; sub-resource&lt;/strong&gt; — grant &lt;code&gt;runtimeArn&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;runtimeArn/*&lt;/code&gt;, or you get AccessDenied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a dead-letter queue.&lt;/strong&gt; With no DLQ, a failing schedule fails &lt;em&gt;silently&lt;/em&gt; — no error, no email, no clue. Ask me how I know.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Change the cadence by editing the array and redeploying.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Trigger it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--target&lt;/span&gt; dev &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"daily"&lt;/span&gt;    &lt;span class="c"&gt;# daily brief, right now&lt;/span&gt;
agentcore invoke &lt;span class="nt"&gt;--target&lt;/span&gt; dev &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"weekly"&lt;/span&gt;   &lt;span class="c"&gt;# weekly review&lt;/span&gt;
agentcore invoke &lt;span class="nt"&gt;--target&lt;/span&gt; dev &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"what's the latest Strands sentiment on Reddit?"&lt;/span&gt;  &lt;span class="c"&gt;# ad-hoc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each run does the full research pass and lands an email in ~1–2 minutes. Watch the logs with &lt;code&gt;agentcore logs --target dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Verify SES actually sent something — either check your inbox or run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ses get-send-statistics &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1   &lt;span class="c"&gt;# DeliveryAttempts should tick up; Rejects should be 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The honest gotchas list
&lt;/h2&gt;

&lt;p&gt;Because a tutorial that only shows the happy path is marketing, not engineering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model has no clock.&lt;/strong&gt; Inject the date. (Said it twice on purpose.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SES sandbox&lt;/strong&gt; limits you to verified recipients until you request production access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC&lt;/strong&gt; will flag mail sent "From" a domain you don't own. Fine for self-sends, not for your team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exa's free tier throttles bursts.&lt;/strong&gt; Pace searches in the prompt, or bring your own key / a provider like Tavily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long runs hit a wall.&lt;/strong&gt; A synchronous streamed invocation gets cut at ~2 minutes. If a brief balloons (a busy week, tons of results), the model can spend longer than that &lt;em&gt;generating the email body&lt;/em&gt; and get killed before it sends. Keep briefs tight (cap items per section), or move big jobs to an async invocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduler fails silently.&lt;/strong&gt; EventBridge Scheduler with no dead-letter queue drops failed invocations into the void — no error, no email, no clue. Add a DLQ (or at least a CloudWatch alarm on &lt;code&gt;TargetErrorCount&lt;/code&gt;) so a broken schedule pages you instead of quietly ghosting you for three days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two things live in CDK, not the tidy config:&lt;/strong&gt; the SES IAM grant and the cron schedules. If you're looking for them in &lt;code&gt;agentcore.json&lt;/code&gt;, you'll look forever — they're in &lt;code&gt;cdk-stack.ts&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where to take it next
&lt;/h2&gt;

&lt;p&gt;This is a starting point, and a fun one. Some directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Swap Exa for a keyed search provider&lt;/strong&gt; if you outgrow the free tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build platform-specific tools.&lt;/strong&gt; Generic web search is a good start, but it can't see everything — plenty of platforms block scraping outright. Wiring up the actual APIs gets you structured, higher-fidelity data instead: the &lt;strong&gt;YouTube Data API&lt;/strong&gt; for video and comment sentiment, the &lt;strong&gt;X (Twitter) API v2&lt;/strong&gt; for real-time mentions and reply threads, &lt;strong&gt;Reddit's API&lt;/strong&gt; for subreddit- and thread-level tracking. Each is just another Strands tool alongside &lt;code&gt;send_email&lt;/code&gt; — but each also brings its own auth, quotas, and rate limits to manage, so budget time for that per platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-reference a second data source&lt;/strong&gt; — the agent happily merges and de-duplicates against, say, an exported social report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-way email&lt;/strong&gt; — reply to the brief and have the agent research your follow-up. (Amazon SES can receive mail too, but you'll need a custom domain to set that up.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern underneath — model + tools + prompt, deployed on a schedule, delivering to a person is very general. Daily standup report. Competitive watch. On-call digest. Once you've built one, you'll see them everywhere.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full &lt;a href="https://github.com/tmoreton/social-listening-agent" rel="noopener noreferrer"&gt;source&lt;/a&gt; and a step-by-step README in the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
